April Robots Backends
Online Backend
We use a layered architecture design for April Robots Online backend (Online backend). We have Presentation –> Service –> Data Access layers structure:
- Presentation – routers;
- process requests (body and query params)
- set access levels
- return response with proper status
- provide endpoint documentation
- basic validation (via Pydantic)
- Service – services;
- do the stuff with the data using Data Access layer
- complex validation, raising HTTPException-s
- Data Access – repositories (repository pattern);
- all operations with the database used by services
- can also raise HTTPExceptions like 404
Online backend is built with FastAPI and uses MongoDB. There are different conventions for writing code in Python/FastAPI covered here.
Online backend repository is “modular” – i.e. the app is divided into self-contained modules with their own router(s), service, schemas, repository(ies), models, etc. A generic structure of a module is:
- init.py – module initialization.
- config.py – module-specific configs (
class Config(BaseSettings): ...). - constants.py – constants and enums not appropriate for the module config.
- dependencies – SvcDep and other things with FastAPI dependencies.
- models.py – app models (correspond to DB table/file) and necessary schemas for models with complex fields.
- repository – repository-level logic, i.e. database queries. Repositories inherit from the base class
BaseRepository[T]. - router.py – routes and routers.
- schemas.py – schemas defining data input and output formats for endpoints.
- service.py – service-level logic. Defined as a general
class Service: .... - utils.py – module-specific utility functions.
When creating a new module, use the structure above and omit files that are not applicable to your module. The GitHub repository contains a detailed explanation of the structure of the backend.
Offline Backend
Offline Backend is very similar to Online Backend. However, we use DictDataBase library for file-based database because it is easier than shipping Mongo. As such, repositories of Offline Backend are completely different and rely on DictDataBase API. The GitHub repository contains a detailed explanation of the structure of the backend.
In addition, Offline Backend is structured to keep assets (images, scripts) downloaded from Online Backend during the synchronization flow.
Crucially, Offline Backend also contains everything needed to build (compile) the Offline App into a Windows executable, including Electron and installer/updater scripts.
Entity relationship diagram:
---
config:
layout: elk
---
erDiagram
CHILD_MODEL {
string _id PK "Primary Key"
string name "First Name"
string surname "Last Name"
bool is_active "True, False"
date birthday_date "Date of Birth"
enum[string] gender "male, female, other"
list[json] skills "Array of SKILL_EMBEDDED"
list[json] radar_graphs "Array of RADAR_GRAPH_EMBEDDED"
list[string] session_ids "References SESSION_MODEL._id"
datetime last_sync_at "Last Sync Time"
datetime created_at "Creation Time"
%% updated at
%% center_ids
}
%% ChildCenters
%% child_id
%% center_ids
SKILL_EMBEDDED {
string _id "Refers to SKILL_MODEL._id"
int value "Range: 1-100"
}
RADAR_GRAPH_EMBEDDED {
datetime created_at "Graph creation date and time"
list[json] skills "Array of SKILL_GRAPH_EMBEDDED"
}
SKILL_GRAPH_EMBEDDED {
string _id "Refers to SKILL_MODEL._id"
int value "Range: 1-10"
}
SESSION_MODEL {
string _id PK "Primary Key"
string child_id "FK CHILD_MODEL._id"
datetime started_at "Session Start Time"
nullable[datetime] ended_at "Session End Time"
list[json] assessment_questions "Array of ASSESSMENT_QUESTION_COLLECTED"
list[json] performed_actions "Array of PERFORMED_ACTION"
list[json] robot_sessions "Array of ROBOT_SESSION"
datetime last_sync_at "Last Sync Time"
datetime created_at "Creation Time"
}
ASSESSMENT_QUESTION_COLLECTED {
string _id "Refers to ASSESSMENT_QUESTION_MODEL._id"
string question "Localized question"
nullable[int] answer "Range: 1-10"
}
PERFORMED_ACTION {
string action_id "Refers to ACTION_MODEL._id"
datetime requested_at "Action Start Requested Time"
enum[string] language "kk, en, ru"
list[json] specialist_grades "Array of SPECIALIST_GRADE_COLLECTED"
nullable[enum[string]] status "queued, running, completed, failed, stopped, none"
nullable[datetime] started_at "Action Start Time"
nullable[datetime] ended_at "Action End Time"
nullable[list[json]] robot_metrics "Array of ROBOT_METRIC_COLLECTED"
nullable[json] metrics_raw_data "Dictionary of raw metrics data"
}
ROBOT_SESSION {
string _id PK "Primary Key"
datetime started_at "Session Start Time"
nullable[datetime] ended_at "Session End Time"
enum[string] robot "nao, furhat"
}
ROBOT_METRIC_COLLECTED {
string _id "Refers to ROBOT_METRIC_MODEL._id"
float value "Collected value, >= 0"
%% float max_value "Range: 1-10"
json translations "Copied translations of { name, unit, description }"
float normalized "Normalized value"
enum[string] status "approved, disapproved, n/a"
}
SPECIALIST_GRADE_COLLECTED {
string skill_id "FK SKILL_MODEL._id"
nullable[int] value "Grade value; range: 0-10"
json translations "Copied translations of { name, description }"
}
AREA_MODEL {
string _id PK "Primary Key"
slug string "Slug string"
nullable[string] image_path "Image path"
json translations "Localized translations of { name, description }"
datetime last_sync_at "Last Sync Time"
datetime created_at "Creation Time"
}
DOMAIN_MODEL {
string _id PK "Primary Key"
slug string "Slug string"
list[string] area_ids "FK AREA_MODEL._id"
nullable[string] image_path "Image path"
json translations "Localized translations of { name, description }"
datetime last_sync_at "Last Sync Time"
datetime created_at "Creation Time"
}
SKILL_MODEL {
string _id PK "Primary Key"
slug string "Slug string"
list[string] domain_id "FK DOMAIN_MODEL._id"
json translations "Localized translations of { name, description }"
list[json] robot_metrics "Array of ROBOT_METRIC_CONFIG"
datetime last_sync_at "Last Sync Time"
datetime created_at "Creation Time"
}
ROBOT_METRIC_CONFIG {
string _id "Refers to ROBOT_METRIC_MODEL._id"
json translations "Copied translations of { name, unit, description }"
float weight "Weight of the metric"
float threshold "Threshold value"
}
ACTION_MODEL {
string _id PK "Primary Key"
string category_id "FK CATEGORY_MODEL.id"
enum[string] robot_type "nao, furhat"
list[string] skill_ids "FK SKILL_MODEL._id"
nullable[string] image_path "Nullable image path"
list[string] specialist_grades "Array of SPECIALIST_GRADE_INLINE"
json translations "Localized translations of { name, description, script_path, max_metrics }; <br> max_metrics is an object of type { ROBOT_METRIC_MODEL._id => max_val (float) }"
datetime last_sync_at "Last Sync Time"
datetime created_at "Creation Time"
}
CATEGORY_MODEL {
string _id PK "Primary Key"
string slug "Unique URL-friendly string"
enum[string] robot_type "nao, furhat"
nullable[string] image_path "Nullable image path"
json translations "Localized translations of { name, description }"
datetime last_sync_at "Last Sync Time"
datetime created_at "Creation Time"
}
SPECIALIST_GRADE_INLINE {
string skill_id "FK SKILL_MODEL._id"
int max_val "Range: 1-10"
enum[string] scale_type "numeric, boolean"
json translation "Localized translations of { name, description }"
}
ROBOT_METRIC_MODEL {
string _id PK "Primary Key"
string slug "Unique slug"
json translations "Localized translations of { name, unit, description }"
datetime last_sync_at "Last Sync Time"
datetime created_at "Creation Time"
}
ASSESSMENT_QUESTION_MODEL {
string _id PK "Primary Key"
json translations "Localized translations of { question, description }"
datetime last_sync_at "Last Sync Time"
datetime created_at "Creation Time"
}
%% Embedding Relationships (solid lines, 'includes')
CHILD_MODEL ||--o{ SKILL_EMBEDDED : includes
CHILD_MODEL ||--o{ RADAR_GRAPH_EMBEDDED : includes
RADAR_GRAPH_EMBEDDED ||--o{ SKILL_GRAPH_EMBEDDED : includes
SESSION_MODEL ||--o{ ROBOT_SESSION : includes
SESSION_MODEL ||--o{ PERFORMED_ACTION : includes
SESSION_MODEL ||--o{ ASSESSMENT_QUESTION_COLLECTED : includes
ACTION_MODEL ||--o{ SPECIALIST_GRADE_INLINE : includes
SKILL_MODEL ||--o{ ROBOT_METRIC_CONFIG : includes
PERFORMED_ACTION ||--o{ ROBOT_METRIC_COLLECTED : includes
PERFORMED_ACTION ||--o{ SPECIALIST_GRADE_COLLECTED : includes
%% Reference Relationships (dotted lines with arrows, 'references')
SESSION_MODEL }|..|| CHILD_MODEL : references
CHILD_MODEL ||..|{ SESSION_MODEL : references
SKILL_MODEL }|..|| DOMAIN_MODEL : references
DOMAIN_MODEL }|..|{ AREA_MODEL : references
SPECIALIST_GRADE_COLLECTED }|..|| SKILL_MODEL : references
SPECIALIST_GRADE_INLINE }|..|| SKILL_MODEL : references
ACTION_MODEL }|..|| CATEGORY_MODEL : references
ACTION_MODEL }|..|{ SKILL_MODEL : references
PERFORMED_ACTION }|..|| ACTION_MODEL : references
SKILL_EMBEDDED ||..|| SKILL_MODEL : references
SKILL_GRAPH_EMBEDDED ||..|| SKILL_MODEL : references
ROBOT_METRIC_COLLECTED ||..|| ROBOT_METRIC_MODEL : references
ROBOT_METRIC_CONFIG ||..|| ROBOT_METRIC_MODEL: references
ASSESSMENT_QUESTION_COLLECTED ||..|| ASSESSMENT_QUESTION_MODEL : references
%% SESSION_MODEL ||..|| ASSESSMENT_QUESTION_MODEL : uses
%% add metrics embedded and grades embedded
Source code:
---
config:
layout: elk
---
erDiagram
CHILD_MODEL {
string _id PK "Primary Key"
string name "First Name"
string surname "Last Name"
bool is_active "True, False"
date birthday_date "Date of Birth"
enum[string] gender "male, female, other"
list[json] skills "Array of SKILL_EMBEDDED"
list[json] radar_graphs "Array of RADAR_GRAPH_EMBEDDED"
list[string] session_ids "References SESSION_MODEL._id"
datetime last_sync_at "Last Sync Time"
datetime created_at "Creation Time"
%% updated at
%% center_ids
}
%% ChildCenters
%% child_id
%% center_ids
SKILL_EMBEDDED {
string _id "Refers to SKILL_MODEL._id"
int value "Range: 1-100"
}
RADAR_GRAPH_EMBEDDED {
datetime created_at "Graph creation date and time"
list[json] skills "Array of SKILL_GRAPH_EMBEDDED"
}
SKILL_GRAPH_EMBEDDED {
string _id "Refers to SKILL_MODEL._id"
int value "Range: 1-10"
}
SESSION_MODEL {
string _id PK "Primary Key"
string child_id "FK CHILD_MODEL._id"
datetime started_at "Session Start Time"
nullable[datetime] ended_at "Session End Time"
list[json] assessment_questions "Array of ASSESSMENT_QUESTION_COLLECTED"
list[json] performed_actions "Array of PERFORMED_ACTION"
list[json] robot_sessions "Array of ROBOT_SESSION"
datetime last_sync_at "Last Sync Time"
datetime created_at "Creation Time"
}
ASSESSMENT_QUESTION_COLLECTED {
string _id "Refers to ASSESSMENT_QUESTION_MODEL._id"
string question "Localized question"
nullable[int] answer "Range: 1-10"
}
PERFORMED_ACTION {
string action_id "Refers to ACTION_MODEL._id"
datetime requested_at "Action Start Requested Time"
enum[string] language "kk, en, ru"
list[json] specialist_grades "Array of SPECIALIST_GRADE_COLLECTED"
nullable[enum[string]] status "queued, running, completed, failed, stopped, none"
nullable[datetime] started_at "Action Start Time"
nullable[datetime] ended_at "Action End Time"
nullable[list[json]] robot_metrics "Array of ROBOT_METRIC_COLLECTED"
nullable[json] metrics_raw_data "Dictionary of raw metrics data"
}
ROBOT_SESSION {
string _id PK "Primary Key"
datetime started_at "Session Start Time"
nullable[datetime] ended_at "Session End Time"
enum[string] robot "nao, furhat"
}
ROBOT_METRIC_COLLECTED {
string _id "Refers to ROBOT_METRIC_MODEL._id"
float value "Collected value, >= 0"
%% float max_value "Range: 1-10"
json translations "Copied translations of { name, unit, description }"
float normalized "Normalized value"
enum[string] status "approved, disapproved, n/a"
}
SPECIALIST_GRADE_COLLECTED {
string skill_id "FK SKILL_MODEL._id"
nullable[int] value "Grade value; range: 0-10"
json translations "Copied translations of { name, description }"
}
AREA_MODEL {
string _id PK "Primary Key"
slug string "Slug string"
nullable[string] image_path "Image path"
json translations "Localized translations of { name, description }"
datetime last_sync_at "Last Sync Time"
datetime created_at "Creation Time"
}
DOMAIN_MODEL {
string _id PK "Primary Key"
slug string "Slug string"
list[string] area_ids "FK AREA_MODEL._id"
nullable[string] image_path "Image path"
json translations "Localized translations of { name, description }"
datetime last_sync_at "Last Sync Time"
datetime created_at "Creation Time"
}
SKILL_MODEL {
string _id PK "Primary Key"
slug string "Slug string"
list[string] domain_id "FK DOMAIN_MODEL._id"
json translations "Localized translations of { name, description }"
list[json] robot_metrics "Array of ROBOT_METRIC_CONFIG"
datetime last_sync_at "Last Sync Time"
datetime created_at "Creation Time"
}
ROBOT_METRIC_CONFIG {
string _id "Refers to ROBOT_METRIC_MODEL._id"
json translations "Copied translations of { name, unit, description }"
float weight "Weight of the metric"
float threshold "Threshold value"
}
ACTION_MODEL {
string _id PK "Primary Key"
string category_id "FK CATEGORY_MODEL.id"
enum[string] robot_type "nao, furhat"
list[string] skill_ids "FK SKILL_MODEL._id"
nullable[string] image_path "Nullable image path"
list[string] specialist_grades "Array of SPECIALIST_GRADE_INLINE"
json translations "Localized translations of { name, description, script_path, max_metrics }; <br> max_metrics is an object of type { ROBOT_METRIC_MODEL._id => max_val (float) }"
datetime last_sync_at "Last Sync Time"
datetime created_at "Creation Time"
}
CATEGORY_MODEL {
string _id PK "Primary Key"
string slug "Unique URL-friendly string"
enum[string] robot_type "nao, furhat"
nullable[string] image_path "Nullable image path"
json translations "Localized translations of { name, description }"
datetime last_sync_at "Last Sync Time"
datetime created_at "Creation Time"
}
SPECIALIST_GRADE_INLINE {
string skill_id "FK SKILL_MODEL._id"
int max_val "Range: 1-10"
enum[string] scale_type "numeric, boolean"
json translation "Localized translations of { name, description }"
}
ROBOT_METRIC_MODEL {
string _id PK "Primary Key"
string slug "Unique slug"
json translations "Localized translations of { name, unit, description }"
datetime last_sync_at "Last Sync Time"
datetime created_at "Creation Time"
}
ASSESSMENT_QUESTION_MODEL {
string _id PK "Primary Key"
json translations "Localized translations of { question, description }"
datetime last_sync_at "Last Sync Time"
datetime created_at "Creation Time"
}
%% Embedding Relationships (solid lines, 'includes')
CHILD_MODEL ||--o{ SKILL_EMBEDDED : includes
CHILD_MODEL ||--o{ RADAR_GRAPH_EMBEDDED : includes
RADAR_GRAPH_EMBEDDED ||--o{ SKILL_GRAPH_EMBEDDED : includes
SESSION_MODEL ||--o{ ROBOT_SESSION : includes
SESSION_MODEL ||--o{ PERFORMED_ACTION : includes
SESSION_MODEL ||--o{ ASSESSMENT_QUESTION_COLLECTED : includes
ACTION_MODEL ||--o{ SPECIALIST_GRADE_INLINE : includes
SKILL_MODEL ||--o{ ROBOT_METRIC_CONFIG : includes
PERFORMED_ACTION ||--o{ ROBOT_METRIC_COLLECTED : includes
PERFORMED_ACTION ||--o{ SPECIALIST_GRADE_COLLECTED : includes
%% Reference Relationships (dotted lines with arrows, 'references')
SESSION_MODEL }|..|| CHILD_MODEL : references
CHILD_MODEL ||..|{ SESSION_MODEL : references
SKILL_MODEL }|..|| DOMAIN_MODEL : references
DOMAIN_MODEL }|..|{ AREA_MODEL : references
SPECIALIST_GRADE_COLLECTED }|..|| SKILL_MODEL : references
SPECIALIST_GRADE_INLINE }|..|| SKILL_MODEL : references
ACTION_MODEL }|..|| CATEGORY_MODEL : references
ACTION_MODEL }|..|{ SKILL_MODEL : references
PERFORMED_ACTION }|..|| ACTION_MODEL : references
SKILL_EMBEDDED ||..|| SKILL_MODEL : references
SKILL_GRAPH_EMBEDDED ||..|| SKILL_MODEL : references
ROBOT_METRIC_COLLECTED ||..|| ROBOT_METRIC_MODEL : references
ROBOT_METRIC_CONFIG ||..|| ROBOT_METRIC_MODEL: references
ASSESSMENT_QUESTION_COLLECTED ||..|| ASSESSMENT_QUESTION_MODEL : references
%% SESSION_MODEL ||..|| ASSESSMENT_QUESTION_MODEL : uses
%% add metrics embedded and grades embeddedGeneral
Both backends rely on migrations for keeping the database schemas updated, and on versioning to ensure synchronization and release-management.