This toy project is a RESTful application developed for ArtiCleaning (invented), designed to remotely control household cleaning robots. The system allows users to define the environment (maps) in which the robots operate, submit cleaning plans (movement sequences), and retrieve historical data of past cleaning sessions.
The solution addresses the following core requirements:
- Environment Management: Parsing and storing rectangular grid maps from different file formats (JSON, TXT).
- Robot Control: Executing movement plans while handling collision detection with obstacles and boundary checks.
- Model Variants: Supporting different robot capabilities, specifically Basic (cleans everywhere) and Premium (skips already clean tiles to save energy).
- Persistence: Storing session metrics (duration, tiles cleaned, status) in a relational database and exporting them via CSV.
Read the full assignment description here.
To ensure the application is maintainable, scalable, and robust, the following software design principles and strategies were adopted:
The application is structured into distinct layers, each with clear responsibilities.
About the domain logic, the Agent and the Environment are distinct entities, and their interaction is managed by the CleaningSession class, that implements the action-perception.
The API layer is completely decoupled from the core logic, allowing for easy modifications or replacements of either layer without affecting the other.
More specifically, the codebase is organized as follows:
- Core Domain (
src/core.py,src/env.py,src/agent.py): Contains the business logic (Agents, Environment, Actions) independent of the web framework. - Service Layer (
src/session.py): Orchestrates the interaction between the Agent and the Environment via theCleaningSessionclass. - API Layer (
src/api/): Handles HTTP requests, validation (Pydantic), and state management (FastAPI), acting as the entry point. - Infrastructure (
src/utils/): Handles implementation details like Database access, File I/O, and Logging.
Key design patterns were utilized to solve specific structural problems:
-
Strategy Pattern (Polymorphism):
- Implementation: The
Agentclass is defined as an Abstract Base Class (ABC). Concrete implementations, likeBasicAgent(blind cleaning) andPremiumAgent(sensor-based cleaning), implement different cleaning policies. - Benefit: This allows the
CleaningSessionto operate without knowing the specific type of robot it is controlling. New robot models can be added in the future without modifying the action-perception loop.
- Implementation: The
-
Factory Method:
- Implementation: The
MapParserclass utilizes class methods (load_from_stream) to instantiate the parser (JsonMapParserorTxtMapParser). Each parser knows how to read and convert its specific file format into a standard internal grid representation (numpy array with0representing walkable tiles and1representing non-walkable tiles). - Benefit: Encapsulates the object creation logic. Supporting a new file format (e.g., YAML) only requires adding a new parser class.
- Implementation: The
-
Repository Pattern:
- Implementation: The
SessionRepositoryclass abstracts the data access layer. - Benefit: The domain logic and API endpoints interact with high-level methods like
save_session()rather than writing raw SQL queries. This makes the code cleaner and allows for easier unit testing or future migration to a different database engine (e.g., PostgreSQL).
- Implementation: The
Despite being a toy application, I tried to anticipate potential concurrency issues that could arise in a production environment with multiple simultaneous users. The solutions implemented are simple (not production-grade probably) yet effective for the scope of this project:
-
Multi-User Map Isolation (In-Memory State):
- Problem: In a multi-user environment, User A might upload a map while User B is uploading a different one. If there were only one "current map" in the global state (as it was at the beginning), User B would overwrite User A's environment before User A could start the cleaning session.
- Solution: The
AppStateclass maintains a dictionary of maps. When a file is uploaded via/set-map, a unique UUID is generated (map_id). This ID is returned to the client and must be provided in the subsequent/cleanrequest. This ensures that every cleaning session operates on the specific map instance intended by the user, isolating concurrent workflows.
-
Database Concurrency (SQLite Locking):
- Problem: SQLite is a file-based database that locks the file during write operations. If two robots finish their sessions simultaneously and try to write to the DB, a "Database is locked" error could crash one of the requests.
- Solution: The
SessionRepositoryinitializes the SQLite connection with atimeout=30parameter. This instructs the connection to wait up to 30 seconds for the lock to be released by another thread before raising an exception. This effectively creates a queue for write operations, ensuring data integrity under load without immediate failures.
-
Dependency Injection:
- Implementation: FastAPI's dependency injection system (
Depends(get_app_state)) is used to inject theAppStateandSessionRepositoryinto endpoints. - Benefit: This ensures a single point of truth for the application state and manages the lifecycle of database connections efficiently.
- Implementation: FastAPI's dependency injection system (
- Collision Handling: The environment rigorously checks bounds and obstacles. Exceptions (
CollisionError) are caught and translated into specific session statuses ("ERROR") rather than crashing the server. - Input Validation: Pydantic schemas enforce strict typing on API inputs (e.g., ensuring steps > 0, valid cardinal directions).
The simulation is built around a standard Agent-Environment interaction loop. This logic resides entirely within src/core.py, src/env.py, and src/agent.py.
The environment models the physical world. It maintains two distinct layers of state:
- Obstacle Layer (Static): Defined by the input map. Contains non-walkable tiles (
1) and walkable tiles (0). - Dirt Layer (Dynamic): Tracks the cleanliness of the floor. By default, every tile is initialized as Dirty.
The environment exposes a step(action) method that:
- Updates the robot's position (checking for collisions).
- Updates the dirt layer (if a cleaning action is performed).
- Returns a new Observation (
Obs), which includes the current coordinates and sensor data (e.g.,is_dirtyflag).
Unlike simple navigation tasks, this domain requires the robot to make two simultaneous decisions at every time step: the next move (given by the plan) and whether to clean the current tile. Therefore, an Action is a composite object consisting of:
- MoveAction:
NORTH,SOUTH,EAST,WEST, orSTAY. - CleanAction:
CLEAN: removes dirt from the current tile.SKIP: save resources by skipping cleaning.
The Agent class is an abstract base class that holds the Motion Plan (the sequence of steps parsed from the user's input). However, the decision of whether to clean or not is delegated to the policy() method, which is implemented differently by each robot model:
The Basic robot has no dirt sensors. It cleans every tile it traverses, even if it visits the same tile twice.
- Policy: Always returns
CleanAction.CLEAN, regardless of the observation.
The Premium robot is equipped with dirt sensors. It inspects the obs.info['is_dirty'] data provided by the environment. It optimizes resource usage: if a path leads the robot over a tile it has already cleaned, it will pass through without incrementing the "cleaned tiles" counter.
- Policy:
- If
is_dirty == True: ReturnsCleanAction.CLEAN. - If
is_dirty == False: ReturnsCleanAction.SKIP.
- If
The CleaningSession class orchestrates the interaction loop. At each time step, the robot:
- observes the current position (and detects eventual collisions)
- decides whether to clean based on its policy
- fetches the next move from its motion plan
The action (clean current tile + move to the next tile) of the robot is implemented by the envirnoment that:
- updates the dirt state of the current tile
- updates the robot's position
The same repeats at the next time step, till the end of the plan.
The project is structured to separate configuration, source code, tests, and deployment definitions.
.
├── configs/
│ └── app.json # Application configuration (DB path, Logging levels, Formats)
├── src/
│ ├── api/
│ │ ├── main.py # FastAPI entry point & Endpoint definitions
│ │ ├── schemas.py # Pydantic models for Input/Output validation
│ │ ├── plan_parser.py # Plan parsing and expansion logic
│ │ ├── dependencies.py # Dependency injection helpers
│ │ └── app_state.py # Global state definition
│ ├── utils/
│ │ ├── persistence.py # SQLite repository and SessionRecord handling
│ │ ├── map.py # Map parsing logic (JSON/TXT Factories)
│ │ ├── log.py # Logging setup
│ │ └── config.py # Configuration loader (Pydantic settings)
│ ├── agent.py # Robot logic (Basic vs Premium implementations)
│ ├── env.py # Grid Environment & Movement logic
│ ├── session.py # Action-Perception loop manager
│ └── core.py # Abstract Base Classes (ABC) and Data Structures
├── tests/ # Unit and Integration tests
├── Dockerfile # Containerization instructions
├── requirements.txt # Python dependencies
└── README.md # Project documentation
Moreover, on startup, the application reads the configuration from configs/app.json, initializes the logging system (logs everything to logs/app.log for debug purposes), and sets up the SQLite database (in data/persistence/sessions.db) if it does not already exist.
Note on the logging system: while currently it logs to a file, in a production environment, I would consider integrating with a centralized logging system for better traceability and monitoring. For this reason, I did not invested much time in structured logging, but it is listed as a TODO for future improvements.
- Docker Engine (for containerized execution)
- Python 3.10+ (only required for local development/testing outside Docker)
The easiest way to run the application is using the provided Docker container. This ensures all dependencies and environment configurations are handled automatically.
-
Build the Image:
docker build -t articleaning-api . -
Run the Container: Map port 8000 of the container to your host machine.
docker run -p 8000:8000 --name cleaning-robot articleaning-api
Optional: Persistence By default, the SQLite database is stored inside the container. To persist the database and logs across container restarts, mount a volume:
docker run -p 8000:8000 -v $(pwd)/data:/app/data articleaning-api
The API will be accessible at http://127.0.0.1:8000/docs.
If you wish to modify the code or run the application without Docker:
-
Install Dependencies: It is recommended to use a virtual environment.
python -m venv .env source .env/bin/activate pip install -r requirements.txt -
Run the Server: Start the application using Uvicorn with reload enabled.
uvicorn src.api.main:app --reload
FastAPI provides an interactive Swagger UI to explore and test the endpoints. Once the app is running, visit:
-
Upload Map (
POST /set-map):- Upload one of the sample files (e.g.,
map.txt). - Copy the
map_idfrom the response.
- Upload one of the sample files (e.g.,
-
Clean (
POST /clean):- Use the
map_idfrom the previous step. - Define a robot type (
basicorpremium) and a movement plan. - Example Payload:
{ "map_id": "paste-your-uuid-here", "start_pos": [0, 0], "robot_type": "premium", "plan": [ {"direction": "S", "steps": 2}, {"direction": "E", "steps": 3} ] }
- Use the
-
Get History (
GET /history):- Download a CSV containing metrics for all executed sessions.
The project includes a test suite located in the tests/ directory.
To run the tests locally:
pytestTo run tests inside the Docker container:
docker run --rm articleaning-api pytest