infra: reorganized repo
This commit is contained in:
402
docs/architecture_diagram.md
Normal file
402
docs/architecture_diagram.md
Normal file
@@ -0,0 +1,402 @@
|
||||
# Architecture Diagram - Agent Media
|
||||
|
||||
## System Overview
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph Client["👤 Client"]
|
||||
CHAT[Chat Interface<br/>OpenWebUI / CLI / Custom]
|
||||
end
|
||||
|
||||
subgraph AgentMedia["🎬 Agent Media"]
|
||||
subgraph API["API Layer"]
|
||||
FASTAPI[FastAPI Server<br/>:8000]
|
||||
end
|
||||
|
||||
subgraph Core["Core"]
|
||||
AGENT[🤖 Agent<br/>Orchestrator]
|
||||
MEMORY[🧠 Memory<br/>LTM + STM + Episodic]
|
||||
end
|
||||
|
||||
subgraph Tools["Tools"]
|
||||
T1[📁 Filesystem]
|
||||
T2[🔍 Search]
|
||||
T3[⬇️ Download]
|
||||
end
|
||||
end
|
||||
|
||||
subgraph LLM["🧠 LLM Provider"]
|
||||
DEEPSEEK[DeepSeek API]
|
||||
OLLAMA[Ollama<br/>Local]
|
||||
end
|
||||
|
||||
subgraph External["☁️ External Services"]
|
||||
TMDB[(TMDB<br/>Movie Database)]
|
||||
KNABEN[(Knaben<br/>Torrent Search)]
|
||||
QBIT[qBittorrent<br/>Download Client]
|
||||
end
|
||||
|
||||
subgraph Storage["💾 Storage"]
|
||||
JSON[(memory_data/<br/>ltm.json)]
|
||||
MEDIA[(Media Folders<br/>/movies /tvshows)]
|
||||
end
|
||||
|
||||
CHAT <-->|OpenAI API| FASTAPI
|
||||
FASTAPI <--> AGENT
|
||||
AGENT <--> MEMORY
|
||||
AGENT <--> Tools
|
||||
AGENT <-->|Chat Completion| LLM
|
||||
|
||||
T1 <--> MEDIA
|
||||
T2 --> TMDB
|
||||
T2 --> KNABEN
|
||||
T3 --> QBIT
|
||||
|
||||
MEMORY <--> JSON
|
||||
QBIT --> MEDIA
|
||||
|
||||
style AgentMedia fill:#1a1a2e,color:#fff
|
||||
style AGENT fill:#ff6b6b,color:#fff
|
||||
style MEMORY fill:#4ecdc4,color:#fff
|
||||
```
|
||||
|
||||
## Detailed Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph Clients["Clients"]
|
||||
direction LR
|
||||
OWU[OpenWebUI]
|
||||
CLI[CLI Client]
|
||||
CURL[cURL / HTTP]
|
||||
end
|
||||
|
||||
subgraph LoadBalancer["Entry Point"]
|
||||
NGINX[Nginx / Reverse Proxy<br/>Optional]
|
||||
end
|
||||
|
||||
subgraph Application["Agent Media Application"]
|
||||
direction TB
|
||||
|
||||
subgraph Presentation["Presentation Layer"]
|
||||
EP1["/v1/chat/completions"]
|
||||
EP2["/v1/models"]
|
||||
EP3["/health"]
|
||||
EP4["/memory/state"]
|
||||
end
|
||||
|
||||
subgraph AgentLayer["Agent Layer"]
|
||||
direction LR
|
||||
AG[Agent]
|
||||
PB[PromptBuilder]
|
||||
REG[Registry]
|
||||
end
|
||||
|
||||
subgraph ToolsLayer["Tools Layer"]
|
||||
direction LR
|
||||
FS_TOOL[Filesystem Tools<br/>set_path, list_folder]
|
||||
API_TOOL[API Tools<br/>find_torrent, add_torrent]
|
||||
end
|
||||
|
||||
subgraph AppLayer["Application Layer"]
|
||||
direction LR
|
||||
UC1[SearchMovie<br/>UseCase]
|
||||
UC2[SearchTorrents<br/>UseCase]
|
||||
UC3[AddTorrent<br/>UseCase]
|
||||
UC4[SetFolderPath<br/>UseCase]
|
||||
end
|
||||
|
||||
subgraph DomainLayer["Domain Layer"]
|
||||
direction LR
|
||||
ENT[Entities<br/>Movie, TVShow, Subtitle]
|
||||
VO[Value Objects<br/>ImdbId, Quality, FilePath]
|
||||
REPO_INT[Repository<br/>Interfaces]
|
||||
end
|
||||
|
||||
subgraph InfraLayer["Infrastructure Layer"]
|
||||
direction TB
|
||||
|
||||
subgraph Persistence["Persistence"]
|
||||
MEM[Memory Manager]
|
||||
REPO_IMPL[JSON Repositories]
|
||||
end
|
||||
|
||||
subgraph APIClients["API Clients"]
|
||||
TMDB_C[TMDB Client]
|
||||
KNAB_C[Knaben Client]
|
||||
QBIT_C[qBittorrent Client]
|
||||
end
|
||||
|
||||
subgraph FSManager["Filesystem"]
|
||||
FM[FileManager]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
subgraph LLMProviders["LLM Providers"]
|
||||
direction LR
|
||||
DS[DeepSeek<br/>api.deepseek.com]
|
||||
OL[Ollama<br/>localhost:11434]
|
||||
end
|
||||
|
||||
subgraph ExternalAPIs["External APIs"]
|
||||
direction LR
|
||||
TMDB_API[TMDB API<br/>api.themoviedb.org]
|
||||
KNAB_API[Knaben API<br/>knaben.eu]
|
||||
QBIT_API[qBittorrent WebUI<br/>localhost:8080]
|
||||
end
|
||||
|
||||
subgraph DataStores["Data Stores"]
|
||||
direction LR
|
||||
LTM_FILE[(ltm.json<br/>Persistent Config)]
|
||||
MEDIA_DIR[(Media Directories<br/>/downloads /movies /tvshows)]
|
||||
end
|
||||
|
||||
%% Client connections
|
||||
Clients --> LoadBalancer
|
||||
LoadBalancer --> Presentation
|
||||
|
||||
%% Internal flow
|
||||
Presentation --> AgentLayer
|
||||
AgentLayer --> ToolsLayer
|
||||
ToolsLayer --> AppLayer
|
||||
AppLayer --> DomainLayer
|
||||
AppLayer --> InfraLayer
|
||||
InfraLayer -.->|implements| DomainLayer
|
||||
|
||||
%% Agent to LLM
|
||||
AgentLayer <-->|HTTP| LLMProviders
|
||||
|
||||
%% Infrastructure to External
|
||||
TMDB_C -->|HTTP| TMDB_API
|
||||
KNAB_C -->|HTTP| KNAB_API
|
||||
QBIT_C -->|HTTP| QBIT_API
|
||||
|
||||
%% Persistence
|
||||
MEM <--> LTM_FILE
|
||||
FM <--> MEDIA_DIR
|
||||
QBIT_API --> MEDIA_DIR
|
||||
```
|
||||
|
||||
## Memory System Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph MemoryManager["Memory Manager"]
|
||||
direction TB
|
||||
|
||||
subgraph LTM["💾 Long-Term Memory"]
|
||||
direction LR
|
||||
LTM_DESC["Persistent across restarts<br/>Stored in ltm.json"]
|
||||
|
||||
subgraph LTM_DATA["Data"]
|
||||
CONFIG["config{}<br/>folder paths, API keys"]
|
||||
PREFS["preferences{}<br/>quality, languages"]
|
||||
LIBRARY["library{}<br/>movies[], tv_shows[]"]
|
||||
FOLLOWING["following[]<br/>watchlist"]
|
||||
end
|
||||
end
|
||||
|
||||
subgraph STM["🧠 Short-Term Memory"]
|
||||
direction LR
|
||||
STM_DESC["Session-based<br/>Cleared on restart"]
|
||||
|
||||
subgraph STM_DATA["Data"]
|
||||
HISTORY["conversation_history[]<br/>last 20 messages"]
|
||||
WORKFLOW["current_workflow{}<br/>type, stage, target"]
|
||||
ENTITIES["extracted_entities{}<br/>title, year, quality"]
|
||||
TOPIC["current_topic<br/>searching, downloading"]
|
||||
end
|
||||
end
|
||||
|
||||
subgraph EPISODIC["⚡ Episodic Memory"]
|
||||
direction LR
|
||||
EPIS_DESC["Transient state<br/>Cleared on restart"]
|
||||
|
||||
subgraph EPIS_DATA["Data"]
|
||||
SEARCH["last_search_results{}<br/>indexed torrents"]
|
||||
DOWNLOADS["active_downloads[]<br/>in-progress"]
|
||||
ERRORS["recent_errors[]<br/>last 5 errors"]
|
||||
PENDING["pending_question{}<br/>awaiting user input"]
|
||||
EVENTS["background_events[]<br/>notifications"]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
subgraph Storage["Storage"]
|
||||
FILE[(memory_data/ltm.json)]
|
||||
end
|
||||
|
||||
subgraph Lifecycle["Lifecycle"]
|
||||
SAVE[save()]
|
||||
LOAD[load()]
|
||||
CLEAR[clear_session()]
|
||||
end
|
||||
|
||||
LTM <-->|read/write| FILE
|
||||
SAVE --> LTM
|
||||
LOAD --> LTM
|
||||
CLEAR --> STM
|
||||
CLEAR --> EPISODIC
|
||||
|
||||
style LTM fill:#4caf50,color:#fff
|
||||
style STM fill:#2196f3,color:#fff
|
||||
style EPISODIC fill:#ff9800,color:#fff
|
||||
```
|
||||
|
||||
## Request Flow
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Request["1️⃣ Request"]
|
||||
USER[User Message]
|
||||
end
|
||||
|
||||
subgraph Parse["2️⃣ Parse"]
|
||||
FASTAPI[FastAPI<br/>Extract message]
|
||||
end
|
||||
|
||||
subgraph Context["3️⃣ Build Context"]
|
||||
PROMPT[PromptBuilder<br/>+ Memory context<br/>+ Tool descriptions]
|
||||
end
|
||||
|
||||
subgraph Think["4️⃣ Think"]
|
||||
LLM[LLM<br/>Decide action]
|
||||
end
|
||||
|
||||
subgraph Act["5️⃣ Act"]
|
||||
TOOL[Execute Tool<br/>or respond]
|
||||
end
|
||||
|
||||
subgraph Store["6️⃣ Store"]
|
||||
MEM[Update Memory<br/>STM + Episodic]
|
||||
end
|
||||
|
||||
subgraph Response["7️⃣ Response"]
|
||||
RESP[JSON Response]
|
||||
end
|
||||
|
||||
USER --> FASTAPI --> PROMPT --> LLM
|
||||
LLM -->|Tool call| TOOL --> MEM --> LLM
|
||||
LLM -->|Text response| MEM --> RESP
|
||||
|
||||
style Think fill:#ff6b6b,color:#fff
|
||||
style Act fill:#4ecdc4,color:#fff
|
||||
style Store fill:#45b7d1,color:#fff
|
||||
```
|
||||
|
||||
## Deployment Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph Host["Host Machine"]
|
||||
subgraph Docker["Docker (Optional)"]
|
||||
AGENT_CONTAINER[Agent Media<br/>Container]
|
||||
end
|
||||
|
||||
subgraph Native["Native Services"]
|
||||
QBIT_SERVICE[qBittorrent<br/>:8080]
|
||||
OLLAMA_SERVICE[Ollama<br/>:11434]
|
||||
end
|
||||
|
||||
subgraph Storage["Local Storage"]
|
||||
CONFIG_DIR[/config<br/>memory_data/]
|
||||
MEDIA_DIR[/media<br/>downloads, movies, tvshows]
|
||||
end
|
||||
end
|
||||
|
||||
subgraph Cloud["Cloud Services"]
|
||||
DEEPSEEK[DeepSeek API]
|
||||
TMDB[TMDB API]
|
||||
KNABEN[Knaben API]
|
||||
end
|
||||
|
||||
subgraph Client["Client"]
|
||||
BROWSER[Browser<br/>OpenWebUI]
|
||||
end
|
||||
|
||||
BROWSER <-->|:8000| AGENT_CONTAINER
|
||||
AGENT_CONTAINER <-->|:8080| QBIT_SERVICE
|
||||
AGENT_CONTAINER <-->|:11434| OLLAMA_SERVICE
|
||||
AGENT_CONTAINER <--> CONFIG_DIR
|
||||
AGENT_CONTAINER <--> MEDIA_DIR
|
||||
QBIT_SERVICE --> MEDIA_DIR
|
||||
|
||||
AGENT_CONTAINER <-->|HTTPS| Cloud
|
||||
```
|
||||
|
||||
## Technology Stack
|
||||
|
||||
```mermaid
|
||||
mindmap
|
||||
root((Agent Media))
|
||||
API
|
||||
FastAPI
|
||||
Uvicorn
|
||||
OpenAI Compatible
|
||||
Agent
|
||||
Python 3.11+
|
||||
Dataclasses
|
||||
Protocol typing
|
||||
LLM
|
||||
DeepSeek
|
||||
Ollama
|
||||
OpenAI compatible
|
||||
Storage
|
||||
JSON files
|
||||
Filesystem
|
||||
External APIs
|
||||
TMDB
|
||||
Knaben
|
||||
qBittorrent WebUI
|
||||
Architecture
|
||||
DDD
|
||||
Clean Architecture
|
||||
Hexagonal
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph Security["Security Layers"]
|
||||
direction TB
|
||||
|
||||
subgraph Input["Input Validation"]
|
||||
PATH_VAL[Path Traversal Protection<br/>FileManager._sanitize_path]
|
||||
INPUT_VAL[Input Sanitization<br/>Tool parameters]
|
||||
end
|
||||
|
||||
subgraph Auth["Authentication"]
|
||||
API_KEYS[API Keys<br/>Environment variables]
|
||||
QBIT_AUTH[qBittorrent Auth<br/>Username/Password]
|
||||
end
|
||||
|
||||
subgraph Access["Access Control"]
|
||||
FOLDER_RESTRICT[Folder Restrictions<br/>Configured paths only]
|
||||
SAFE_PATH[Safe Path Checks<br/>_is_safe_path()]
|
||||
end
|
||||
end
|
||||
|
||||
subgraph Env["Environment"]
|
||||
ENV_FILE[.env file<br/>DEEPSEEK_API_KEY<br/>TMDB_API_KEY<br/>QBITTORRENT_*]
|
||||
end
|
||||
|
||||
ENV_FILE --> Auth
|
||||
Input --> Access
|
||||
```
|
||||
|
||||
## Legend
|
||||
|
||||
| Icon | Meaning |
|
||||
|------|---------|
|
||||
| 🎬 | Agent Media System |
|
||||
| 🤖 | AI Agent |
|
||||
| 🧠 | Memory / LLM |
|
||||
| 💾 | Persistent Storage |
|
||||
| ⚡ | Transient / Fast |
|
||||
| 📁 | Filesystem |
|
||||
| 🔍 | Search |
|
||||
| ⬇️ | Download |
|
||||
| ☁️ | Cloud / External |
|
||||
| 👤 | User / Client |
|
||||
367
docs/class_diagram.md
Normal file
367
docs/class_diagram.md
Normal file
@@ -0,0 +1,367 @@
|
||||
# Class Diagram - Agent Media
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
direction TB
|
||||
|
||||
%% ===========================================
|
||||
%% MEMORY SYSTEM
|
||||
%% ===========================================
|
||||
|
||||
class Memory {
|
||||
+Path storage_dir
|
||||
+Path ltm_file
|
||||
+LongTermMemory ltm
|
||||
+ShortTermMemory stm
|
||||
+EpisodicMemory episodic
|
||||
+__init__(storage_dir: str)
|
||||
+save() void
|
||||
+get_context_for_prompt() Dict
|
||||
+get_full_state() Dict
|
||||
+clear_session() void
|
||||
}
|
||||
|
||||
class LongTermMemory {
|
||||
+Dict config
|
||||
+Dict preferences
|
||||
+Dict~str, List~ library
|
||||
+List~Dict~ following
|
||||
+get_config(key: str) Any
|
||||
+set_config(key: str, value: Any) void
|
||||
+has_config(key: str) bool
|
||||
+add_to_library(media_type: str, media: Dict) void
|
||||
+get_library(media_type: str) List
|
||||
+follow_show(show: Dict) void
|
||||
+to_dict() Dict
|
||||
+from_dict(data: Dict)$ LongTermMemory
|
||||
}
|
||||
|
||||
class ShortTermMemory {
|
||||
+List~Dict~ conversation_history
|
||||
+Dict current_workflow
|
||||
+Dict extracted_entities
|
||||
+str current_topic
|
||||
+int max_history
|
||||
+add_message(role: str, content: str) void
|
||||
+get_recent_history(n: int) List
|
||||
+start_workflow(type: str, target: Dict) void
|
||||
+update_workflow_stage(stage: str) void
|
||||
+end_workflow() void
|
||||
+set_entity(key: str, value: Any) void
|
||||
+get_entity(key: str) Any
|
||||
+clear() void
|
||||
+to_dict() Dict
|
||||
}
|
||||
|
||||
class EpisodicMemory {
|
||||
+Dict last_search_results
|
||||
+List~Dict~ active_downloads
|
||||
+List~Dict~ recent_errors
|
||||
+Dict pending_question
|
||||
+List~Dict~ background_events
|
||||
+store_search_results(query: str, results: List) void
|
||||
+get_result_by_index(index: int) Dict
|
||||
+get_search_results() Dict
|
||||
+add_active_download(download: Dict) void
|
||||
+complete_download(task_id: str, path: str) Dict
|
||||
+add_error(action: str, error: str) void
|
||||
+set_pending_question(question: str, options: List) void
|
||||
+resolve_pending_question(index: int) Dict
|
||||
+add_background_event(type: str, data: Dict) void
|
||||
+get_unread_events() List
|
||||
+clear() void
|
||||
+to_dict() Dict
|
||||
}
|
||||
|
||||
Memory *-- LongTermMemory : ltm
|
||||
Memory *-- ShortTermMemory : stm
|
||||
Memory *-- EpisodicMemory : episodic
|
||||
|
||||
%% ===========================================
|
||||
%% AGENT SYSTEM
|
||||
%% ===========================================
|
||||
|
||||
class Agent {
|
||||
+LLMClient llm
|
||||
+Memory memory
|
||||
+Dict~str, Tool~ tools
|
||||
+PromptBuilder prompt_builder
|
||||
+int max_tool_iterations
|
||||
+__init__(llm: LLMClient, memory: Memory)
|
||||
+step(user_input: str) str
|
||||
-_parse_intent(text: str) Dict
|
||||
-_execute_action(intent: Dict) Dict
|
||||
-_check_unread_events() str
|
||||
}
|
||||
|
||||
class LLMClient {
|
||||
<<Protocol>>
|
||||
+complete(messages: List) str
|
||||
}
|
||||
|
||||
class DeepSeekClient {
|
||||
+str api_key
|
||||
+str model
|
||||
+str base_url
|
||||
+complete(messages: List) str
|
||||
}
|
||||
|
||||
class OllamaClient {
|
||||
+str base_url
|
||||
+str model
|
||||
+complete(messages: List) str
|
||||
}
|
||||
|
||||
class PromptBuilder {
|
||||
+Dict~str, Tool~ tools
|
||||
+__init__(tools: Dict)
|
||||
+build_system_prompt(memory: Memory) str
|
||||
-_format_tools_description() str
|
||||
-_format_episodic_context(memory: Memory) str
|
||||
-_format_stm_context(memory: Memory) str
|
||||
}
|
||||
|
||||
class Tool {
|
||||
<<dataclass>>
|
||||
+str name
|
||||
+str description
|
||||
+Callable func
|
||||
+Dict parameters
|
||||
}
|
||||
|
||||
Agent --> LLMClient : uses
|
||||
Agent --> Memory : uses
|
||||
Agent --> PromptBuilder : uses
|
||||
Agent --> Tool : executes
|
||||
DeepSeekClient ..|> LLMClient
|
||||
OllamaClient ..|> LLMClient
|
||||
PromptBuilder --> Tool : formats
|
||||
|
||||
%% ===========================================
|
||||
%% DOMAIN - MOVIES
|
||||
%% ===========================================
|
||||
|
||||
class Movie {
|
||||
<<Entity>>
|
||||
+ImdbId imdb_id
|
||||
+MovieTitle title
|
||||
+ReleaseYear release_year
|
||||
+Quality quality
|
||||
+FilePath file_path
|
||||
+FileSize file_size
|
||||
+int tmdb_id
|
||||
+datetime added_at
|
||||
}
|
||||
|
||||
class MovieTitle {
|
||||
<<ValueObject>>
|
||||
+str value
|
||||
+__init__(value: str)
|
||||
}
|
||||
|
||||
class ReleaseYear {
|
||||
<<ValueObject>>
|
||||
+int value
|
||||
+__init__(value: int)
|
||||
}
|
||||
|
||||
class Quality {
|
||||
<<ValueObject>>
|
||||
+str value
|
||||
+__init__(value: str)
|
||||
}
|
||||
|
||||
class MovieRepository {
|
||||
<<Interface>>
|
||||
+save(movie: Movie) void
|
||||
+find_by_imdb_id(imdb_id: ImdbId) Movie
|
||||
+find_all() List~Movie~
|
||||
+delete(imdb_id: ImdbId) bool
|
||||
+exists(imdb_id: ImdbId) bool
|
||||
}
|
||||
|
||||
Movie --> MovieTitle
|
||||
Movie --> ReleaseYear
|
||||
Movie --> Quality
|
||||
Movie --> ImdbId
|
||||
|
||||
%% ===========================================
|
||||
%% DOMAIN - TV SHOWS
|
||||
%% ===========================================
|
||||
|
||||
class TVShow {
|
||||
<<Entity>>
|
||||
+ImdbId imdb_id
|
||||
+str title
|
||||
+int seasons_count
|
||||
+ShowStatus status
|
||||
+int tmdb_id
|
||||
+str first_air_date
|
||||
+datetime added_at
|
||||
}
|
||||
|
||||
class ShowStatus {
|
||||
<<Enum>>
|
||||
CONTINUING
|
||||
ENDED
|
||||
UNKNOWN
|
||||
+from_string(value: str)$ ShowStatus
|
||||
}
|
||||
|
||||
class TVShowRepository {
|
||||
<<Interface>>
|
||||
+save(show: TVShow) void
|
||||
+find_by_imdb_id(imdb_id: ImdbId) TVShow
|
||||
+find_all() List~TVShow~
|
||||
+delete(imdb_id: ImdbId) bool
|
||||
}
|
||||
|
||||
TVShow --> ShowStatus
|
||||
TVShow --> ImdbId
|
||||
|
||||
%% ===========================================
|
||||
%% DOMAIN - SHARED
|
||||
%% ===========================================
|
||||
|
||||
class ImdbId {
|
||||
<<ValueObject>>
|
||||
+str value
|
||||
+__init__(value: str)
|
||||
+__str__() str
|
||||
}
|
||||
|
||||
class FilePath {
|
||||
<<ValueObject>>
|
||||
+str value
|
||||
+__init__(value: str)
|
||||
}
|
||||
|
||||
class FileSize {
|
||||
<<ValueObject>>
|
||||
+int bytes
|
||||
+__init__(bytes: int)
|
||||
+to_human_readable() str
|
||||
}
|
||||
|
||||
%% ===========================================
|
||||
%% INFRASTRUCTURE - PERSISTENCE
|
||||
%% ===========================================
|
||||
|
||||
class JsonMovieRepository {
|
||||
+Memory memory
|
||||
+__init__(memory: Memory)
|
||||
+save(movie: Movie) void
|
||||
+find_by_imdb_id(imdb_id: ImdbId) Movie
|
||||
+find_all() List~Movie~
|
||||
+delete(imdb_id: ImdbId) bool
|
||||
}
|
||||
|
||||
class JsonTVShowRepository {
|
||||
+Memory memory
|
||||
+__init__(memory: Memory)
|
||||
+save(show: TVShow) void
|
||||
+find_by_imdb_id(imdb_id: ImdbId) TVShow
|
||||
+find_all() List~TVShow~
|
||||
+delete(imdb_id: ImdbId) bool
|
||||
}
|
||||
|
||||
JsonMovieRepository ..|> MovieRepository
|
||||
JsonTVShowRepository ..|> TVShowRepository
|
||||
JsonMovieRepository --> Memory
|
||||
JsonTVShowRepository --> Memory
|
||||
|
||||
%% ===========================================
|
||||
%% INFRASTRUCTURE - API CLIENTS
|
||||
%% ===========================================
|
||||
|
||||
class TMDBClient {
|
||||
+str api_key
|
||||
+str base_url
|
||||
+search_movie(title: str) TMDBSearchResult
|
||||
+search_tv(title: str) TMDBSearchResult
|
||||
+get_external_ids(tmdb_id: int) Dict
|
||||
}
|
||||
|
||||
class KnabenClient {
|
||||
+str base_url
|
||||
+search(query: str, limit: int) List~TorrentResult~
|
||||
}
|
||||
|
||||
class QBittorrentClient {
|
||||
+str host
|
||||
+str username
|
||||
+str password
|
||||
+add_torrent(magnet: str) bool
|
||||
+get_torrents() List
|
||||
}
|
||||
|
||||
%% ===========================================
|
||||
%% INFRASTRUCTURE - FILESYSTEM
|
||||
%% ===========================================
|
||||
|
||||
class FileManager {
|
||||
+Memory memory
|
||||
+__init__(memory: Memory)
|
||||
+set_folder_path(name: str, path: str) Dict
|
||||
+list_folder(type: str, path: str) Dict
|
||||
+move_file(source: str, dest: str) Dict
|
||||
}
|
||||
|
||||
FileManager --> Memory
|
||||
|
||||
%% ===========================================
|
||||
%% APPLICATION - USE CASES
|
||||
%% ===========================================
|
||||
|
||||
class SearchMovieUseCase {
|
||||
+TMDBClient tmdb_client
|
||||
+execute(title: str) SearchMovieResponse
|
||||
}
|
||||
|
||||
class SearchTorrentsUseCase {
|
||||
+KnabenClient knaben_client
|
||||
+execute(title: str, limit: int) SearchTorrentsResponse
|
||||
}
|
||||
|
||||
class AddTorrentUseCase {
|
||||
+QBittorrentClient qbittorrent_client
|
||||
+execute(magnet: str) AddTorrentResponse
|
||||
}
|
||||
|
||||
class SetFolderPathUseCase {
|
||||
+FileManager file_manager
|
||||
+execute(folder_name: str, path: str) SetFolderPathResponse
|
||||
}
|
||||
|
||||
class ListFolderUseCase {
|
||||
+FileManager file_manager
|
||||
+execute(folder_type: str, path: str) ListFolderResponse
|
||||
}
|
||||
|
||||
SearchMovieUseCase --> TMDBClient
|
||||
SearchTorrentsUseCase --> KnabenClient
|
||||
AddTorrentUseCase --> QBittorrentClient
|
||||
SetFolderPathUseCase --> FileManager
|
||||
ListFolderUseCase --> FileManager
|
||||
```
|
||||
|
||||
## Legend
|
||||
|
||||
| Symbol | Meaning |
|
||||
|--------|---------|
|
||||
| `<<Entity>>` | Domain entity with identity |
|
||||
| `<<ValueObject>>` | Immutable value object |
|
||||
| `<<Interface>>` | Abstract interface/protocol |
|
||||
| `<<Enum>>` | Enumeration |
|
||||
| `<<dataclass>>` | Python dataclass |
|
||||
| `<<Protocol>>` | Python Protocol (structural typing) |
|
||||
| `*--` | Composition (owns) |
|
||||
| `-->` | Association (uses) |
|
||||
| `..\|>` | Implementation |
|
||||
|
||||
## Architecture Layers
|
||||
|
||||
1. **Domain Layer** - Business entities and rules (Movie, TVShow, ValueObjects)
|
||||
2. **Application Layer** - Use cases orchestrating business logic
|
||||
3. **Infrastructure Layer** - External services (APIs, filesystem, persistence)
|
||||
4. **Agent Layer** - AI agent, LLM clients, tools, prompts
|
||||
311
docs/component_diagram.md
Normal file
311
docs/component_diagram.md
Normal file
@@ -0,0 +1,311 @@
|
||||
# Component Diagram - Agent Media (DDD Architecture)
|
||||
|
||||
```mermaid
|
||||
C4Component
|
||||
title Component Diagram - Agent Media
|
||||
|
||||
Container_Boundary(agent_layer, "Agent Layer") {
|
||||
Component(agent, "Agent", "Python", "Orchestrates LLM and tools")
|
||||
Component(prompt_builder, "PromptBuilder", "Python", "Builds system prompts with context")
|
||||
Component(registry, "Tool Registry", "Python", "Registers and binds tools")
|
||||
|
||||
Component_Boundary(llm_clients, "LLM Clients") {
|
||||
Component(deepseek, "DeepSeekClient", "Python", "DeepSeek API client")
|
||||
Component(ollama, "OllamaClient", "Python", "Ollama local client")
|
||||
}
|
||||
|
||||
Component_Boundary(tools, "Tools") {
|
||||
Component(api_tools, "API Tools", "Python", "find_torrent, add_torrent, etc.")
|
||||
Component(fs_tools, "Filesystem Tools", "Python", "set_path, list_folder")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Layered Architecture (DDD)
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph Presentation["🌐 Presentation Layer"]
|
||||
API["FastAPI Server<br/>/v1/chat/completions"]
|
||||
end
|
||||
|
||||
subgraph Agent["🤖 Agent Layer"]
|
||||
AG[Agent]
|
||||
PB[PromptBuilder]
|
||||
TR[Tool Registry]
|
||||
|
||||
subgraph LLM["LLM Clients"]
|
||||
DS[DeepSeek]
|
||||
OL[Ollama]
|
||||
end
|
||||
|
||||
subgraph Tools["Tools"]
|
||||
AT[API Tools]
|
||||
FT[Filesystem Tools]
|
||||
end
|
||||
end
|
||||
|
||||
subgraph Application["⚙️ Application Layer"]
|
||||
subgraph UseCases["Use Cases"]
|
||||
UC1[SearchMovieUseCase]
|
||||
UC2[SearchTorrentsUseCase]
|
||||
UC3[AddTorrentUseCase]
|
||||
UC4[SetFolderPathUseCase]
|
||||
UC5[ListFolderUseCase]
|
||||
end
|
||||
|
||||
subgraph DTOs["DTOs"]
|
||||
DTO1[SearchMovieResponse]
|
||||
DTO2[SearchTorrentsResponse]
|
||||
DTO3[AddTorrentResponse]
|
||||
end
|
||||
end
|
||||
|
||||
subgraph Domain["📦 Domain Layer"]
|
||||
subgraph Movies["movies/"]
|
||||
ME[Movie Entity]
|
||||
MVO[MovieTitle, Quality, ReleaseYear]
|
||||
MR[MovieRepository Interface]
|
||||
end
|
||||
|
||||
subgraph TVShows["tv_shows/"]
|
||||
TE[TVShow Entity]
|
||||
TVO[ShowStatus]
|
||||
TR2[TVShowRepository Interface]
|
||||
end
|
||||
|
||||
subgraph Subtitles["subtitles/"]
|
||||
SE[Subtitle Entity]
|
||||
SVO[Language, SubtitleFormat]
|
||||
SR[SubtitleRepository Interface]
|
||||
end
|
||||
|
||||
subgraph Shared["shared/"]
|
||||
SH[ImdbId, FilePath, FileSize]
|
||||
end
|
||||
end
|
||||
|
||||
subgraph Infrastructure["🔧 Infrastructure Layer"]
|
||||
subgraph Persistence["persistence/"]
|
||||
MEM[Memory<br/>LTM + STM + Episodic]
|
||||
JMR[JsonMovieRepository]
|
||||
JTR[JsonTVShowRepository]
|
||||
JSR[JsonSubtitleRepository]
|
||||
end
|
||||
|
||||
subgraph APIs["api/"]
|
||||
TMDB[TMDBClient]
|
||||
KNAB[KnabenClient]
|
||||
QBIT[QBittorrentClient]
|
||||
end
|
||||
|
||||
subgraph FS["filesystem/"]
|
||||
FM[FileManager]
|
||||
end
|
||||
end
|
||||
|
||||
subgraph External["☁️ External Services"]
|
||||
TMDB_API[(TMDB API)]
|
||||
KNAB_API[(Knaben API)]
|
||||
QBIT_API[(qBittorrent)]
|
||||
DISK[(Filesystem)]
|
||||
end
|
||||
|
||||
%% Connections
|
||||
API --> AG
|
||||
AG --> PB
|
||||
AG --> TR
|
||||
AG --> LLM
|
||||
TR --> Tools
|
||||
|
||||
AT --> UC1
|
||||
AT --> UC2
|
||||
AT --> UC3
|
||||
FT --> UC4
|
||||
FT --> UC5
|
||||
|
||||
UC1 --> TMDB
|
||||
UC2 --> KNAB
|
||||
UC3 --> QBIT
|
||||
UC4 --> FM
|
||||
UC5 --> FM
|
||||
|
||||
JMR --> MEM
|
||||
JTR --> MEM
|
||||
JSR --> MEM
|
||||
FM --> MEM
|
||||
|
||||
JMR -.->|implements| MR
|
||||
JTR -.->|implements| TR2
|
||||
JSR -.->|implements| SR
|
||||
|
||||
TMDB --> TMDB_API
|
||||
KNAB --> KNAB_API
|
||||
QBIT --> QBIT_API
|
||||
FM --> DISK
|
||||
MEM --> DISK
|
||||
|
||||
%% Styling
|
||||
classDef presentation fill:#e1f5fe
|
||||
classDef agent fill:#fff3e0
|
||||
classDef application fill:#f3e5f5
|
||||
classDef domain fill:#e8f5e9
|
||||
classDef infrastructure fill:#fce4ec
|
||||
classDef external fill:#f5f5f5
|
||||
|
||||
class API presentation
|
||||
class AG,PB,TR,DS,OL,AT,FT agent
|
||||
class UC1,UC2,UC3,UC4,UC5,DTO1,DTO2,DTO3 application
|
||||
class ME,MVO,MR,TE,TVO,TR2,SE,SVO,SR,SH domain
|
||||
class MEM,JMR,JTR,JSR,TMDB,KNAB,QBIT,FM infrastructure
|
||||
class TMDB_API,KNAB_API,QBIT_API,DISK external
|
||||
```
|
||||
|
||||
## Memory Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Memory["Memory System"]
|
||||
direction TB
|
||||
|
||||
subgraph LTM["💾 Long-Term Memory<br/>(Persistent - JSON)"]
|
||||
CONFIG[config<br/>download_folder, tvshow_folder...]
|
||||
PREFS[preferences<br/>quality, languages...]
|
||||
LIB[library<br/>movies[], tv_shows[]]
|
||||
FOLLOW[following<br/>watchlist]
|
||||
end
|
||||
|
||||
subgraph STM["🧠 Short-Term Memory<br/>(Session - RAM)"]
|
||||
HIST[conversation_history]
|
||||
WORKFLOW[current_workflow]
|
||||
ENTITIES[extracted_entities]
|
||||
TOPIC[current_topic]
|
||||
end
|
||||
|
||||
subgraph EPISODIC["⚡ Episodic Memory<br/>(Transient - RAM)"]
|
||||
SEARCH[last_search_results<br/>indexed torrents]
|
||||
DOWNLOADS[active_downloads]
|
||||
ERRORS[recent_errors]
|
||||
PENDING[pending_question]
|
||||
EVENTS[background_events]
|
||||
end
|
||||
end
|
||||
|
||||
subgraph Storage["Storage"]
|
||||
JSON[(ltm.json)]
|
||||
end
|
||||
|
||||
LTM -->|save| JSON
|
||||
JSON -->|load| LTM
|
||||
|
||||
STM -.->|cleared on| RESTART[Server Restart]
|
||||
EPISODIC -.->|cleared on| RESTART
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Input
|
||||
USER[User Request]
|
||||
end
|
||||
|
||||
subgraph Processing
|
||||
direction TB
|
||||
FASTAPI[FastAPI]
|
||||
AGENT[Agent]
|
||||
TOOLS[Tools]
|
||||
USECASES[Use Cases]
|
||||
end
|
||||
|
||||
subgraph External
|
||||
direction TB
|
||||
TMDB[(TMDB)]
|
||||
KNABEN[(Knaben)]
|
||||
QBIT[(qBittorrent)]
|
||||
end
|
||||
|
||||
subgraph Memory
|
||||
direction TB
|
||||
LTM[(LTM)]
|
||||
STM[(STM)]
|
||||
EPIS[(Episodic)]
|
||||
end
|
||||
|
||||
USER -->|HTTP POST| FASTAPI
|
||||
FASTAPI -->|step()| AGENT
|
||||
AGENT -->|execute| TOOLS
|
||||
TOOLS -->|call| USECASES
|
||||
|
||||
USECASES -->|search| TMDB
|
||||
USECASES -->|search| KNABEN
|
||||
USECASES -->|add| QBIT
|
||||
|
||||
AGENT <-->|read/write| LTM
|
||||
AGENT <-->|read/write| STM
|
||||
TOOLS <-->|read/write| EPIS
|
||||
|
||||
AGENT -->|response| FASTAPI
|
||||
FASTAPI -->|JSON| USER
|
||||
```
|
||||
|
||||
## Dependency Direction
|
||||
|
||||
```mermaid
|
||||
flowchart BT
|
||||
subgraph External["External"]
|
||||
EXT[APIs, Filesystem]
|
||||
end
|
||||
|
||||
subgraph Infra["Infrastructure"]
|
||||
INF[Clients, Repositories, Memory]
|
||||
end
|
||||
|
||||
subgraph App["Application"]
|
||||
APP[Use Cases, DTOs]
|
||||
end
|
||||
|
||||
subgraph Dom["Domain"]
|
||||
DOM[Entities, Value Objects, Interfaces]
|
||||
end
|
||||
|
||||
subgraph Agent["Agent"]
|
||||
AGT[Agent, Tools, Prompts]
|
||||
end
|
||||
|
||||
subgraph Pres["Presentation"]
|
||||
PRES[FastAPI]
|
||||
end
|
||||
|
||||
EXT --> Infra
|
||||
Infra --> App
|
||||
Infra -.->|implements| Dom
|
||||
App --> Dom
|
||||
Agent --> App
|
||||
Agent --> Infra
|
||||
Pres --> Agent
|
||||
|
||||
style Dom fill:#e8f5e9,stroke:#4caf50,stroke-width:3px
|
||||
style Infra fill:#fce4ec,stroke:#e91e63
|
||||
style App fill:#f3e5f5,stroke:#9c27b0
|
||||
style Agent fill:#fff3e0,stroke:#ff9800
|
||||
style Pres fill:#e1f5fe,stroke:#03a9f4
|
||||
```
|
||||
|
||||
## Legend
|
||||
|
||||
| Layer | Responsibility | Examples |
|
||||
|-------|---------------|----------|
|
||||
| 🌐 **Presentation** | HTTP interface, request/response handling | FastAPI endpoints |
|
||||
| 🤖 **Agent** | AI orchestration, LLM interaction, tools | Agent, PromptBuilder, Tools |
|
||||
| ⚙️ **Application** | Use case orchestration, DTOs | SearchMovieUseCase, SearchTorrentsResponse |
|
||||
| 📦 **Domain** | Business entities, rules, interfaces | Movie, TVShow, ImdbId, MovieRepository |
|
||||
| 🔧 **Infrastructure** | External service implementations | TMDBClient, JsonMovieRepository, Memory |
|
||||
| ☁️ **External** | Third-party services | TMDB API, qBittorrent, Filesystem |
|
||||
|
||||
## Key Principles
|
||||
|
||||
1. **Dependency Inversion**: Domain defines interfaces, Infrastructure implements them
|
||||
2. **Clean Architecture**: Dependencies point inward (toward Domain)
|
||||
3. **Separation of Concerns**: Each layer has a single responsibility
|
||||
4. **Memory Segregation**: LTM (persistent), STM (session), Episodic (transient)
|
||||
366
docs/flowchart.md
Normal file
366
docs/flowchart.md
Normal file
@@ -0,0 +1,366 @@
|
||||
# Flowcharts - Agent Media
|
||||
|
||||
## 1. Main Application Flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
START([Application Start]) --> INIT_MEM[Initialize Memory Context<br/>init_memory]
|
||||
INIT_MEM --> INIT_LLM{LLM Provider?}
|
||||
|
||||
INIT_LLM -->|OLLAMA| OLLAMA[Create OllamaClient]
|
||||
INIT_LLM -->|DEEPSEEK| DEEPSEEK[Create DeepSeekClient]
|
||||
|
||||
OLLAMA --> INIT_AGENT[Create Agent]
|
||||
DEEPSEEK --> INIT_AGENT
|
||||
|
||||
INIT_AGENT --> INIT_TOOLS[Register Tools<br/>make_tools]
|
||||
INIT_TOOLS --> START_SERVER[Start FastAPI Server<br/>:8000]
|
||||
|
||||
START_SERVER --> WAIT_REQ[/Wait for Request/]
|
||||
|
||||
WAIT_REQ --> REQ_TYPE{Request Type?}
|
||||
|
||||
REQ_TYPE -->|GET /health| HEALTH[Return health status]
|
||||
REQ_TYPE -->|GET /v1/models| MODELS[Return model list]
|
||||
REQ_TYPE -->|GET /memory/state| MEM_STATE[Return memory state]
|
||||
REQ_TYPE -->|POST /memory/clear-session| CLEAR_SESSION[Clear STM + Episodic]
|
||||
REQ_TYPE -->|POST /v1/chat/completions| CHAT[Process Chat Request]
|
||||
|
||||
HEALTH --> WAIT_REQ
|
||||
MODELS --> WAIT_REQ
|
||||
MEM_STATE --> WAIT_REQ
|
||||
CLEAR_SESSION --> WAIT_REQ
|
||||
CHAT --> AGENT_STEP[agent.step]
|
||||
AGENT_STEP --> RETURN_RESP[Return Response]
|
||||
RETURN_RESP --> WAIT_REQ
|
||||
```
|
||||
|
||||
## 2. Agent Step Flow (Core Logic)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
START([agent.step called]) --> GET_MEM[Get Memory from Context]
|
||||
GET_MEM --> CHECK_EVENTS[Check Unread Events]
|
||||
|
||||
CHECK_EVENTS --> HAS_EVENTS{Has Events?}
|
||||
HAS_EVENTS -->|Yes| FORMAT_EVENTS[Format Event Notifications]
|
||||
HAS_EVENTS -->|No| BUILD_PROMPT
|
||||
FORMAT_EVENTS --> BUILD_PROMPT
|
||||
|
||||
BUILD_PROMPT[Build System Prompt<br/>with Memory Context]
|
||||
BUILD_PROMPT --> INIT_MSGS[Initialize Messages Array]
|
||||
|
||||
INIT_MSGS --> ADD_SYSTEM[Add System Prompt]
|
||||
ADD_SYSTEM --> GET_HISTORY[Get STM History]
|
||||
GET_HISTORY --> ADD_HISTORY[Add History Messages]
|
||||
ADD_HISTORY --> ADD_NOTIF{Has Notifications?}
|
||||
|
||||
ADD_NOTIF -->|Yes| ADD_NOTIF_MSG[Add Notification Message]
|
||||
ADD_NOTIF -->|No| ADD_USER
|
||||
ADD_NOTIF_MSG --> ADD_USER[Add User Input]
|
||||
|
||||
ADD_USER --> LOOP_START[/Tool Execution Loop/]
|
||||
|
||||
LOOP_START --> CHECK_ITER{iteration < max?}
|
||||
CHECK_ITER -->|No| MAX_REACHED[Request Final Response]
|
||||
CHECK_ITER -->|Yes| CALL_LLM[Call LLM.complete]
|
||||
|
||||
MAX_REACHED --> FINAL_LLM[Call LLM.complete]
|
||||
FINAL_LLM --> SAVE_FINAL[Save to STM History]
|
||||
SAVE_FINAL --> RETURN_FINAL([Return Response])
|
||||
|
||||
CALL_LLM --> PARSE_INTENT[Parse Intent from Response]
|
||||
PARSE_INTENT --> IS_TOOL{Is Tool Call?}
|
||||
|
||||
IS_TOOL -->|No| SAVE_HISTORY[Save to STM History]
|
||||
SAVE_HISTORY --> SAVE_LTM[Save LTM]
|
||||
SAVE_LTM --> RETURN_TEXT([Return Text Response])
|
||||
|
||||
IS_TOOL -->|Yes| EXEC_TOOL[Execute Tool]
|
||||
EXEC_TOOL --> ADD_RESULT[Add Tool Result to Messages]
|
||||
ADD_RESULT --> INC_ITER[iteration++]
|
||||
INC_ITER --> LOOP_START
|
||||
```
|
||||
|
||||
## 3. Tool Execution Flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
START([_execute_action called]) --> GET_ACTION[Extract action name & args]
|
||||
GET_ACTION --> FIND_TOOL{Tool exists?}
|
||||
|
||||
FIND_TOOL -->|No| UNKNOWN[Return unknown_tool error]
|
||||
UNKNOWN --> END_ERR([Return Error])
|
||||
|
||||
FIND_TOOL -->|Yes| CALL_FUNC[Call tool.func with args]
|
||||
|
||||
CALL_FUNC --> EXEC_OK{Execution OK?}
|
||||
|
||||
EXEC_OK -->|TypeError| BAD_ARGS[Log bad arguments error]
|
||||
EXEC_OK -->|Exception| EXEC_ERR[Log execution error]
|
||||
EXEC_OK -->|Success| CHECK_RESULT{Result has error?}
|
||||
|
||||
BAD_ARGS --> ADD_ERR_MEM[Add error to Episodic Memory]
|
||||
EXEC_ERR --> ADD_ERR_MEM
|
||||
ADD_ERR_MEM --> END_ERR
|
||||
|
||||
CHECK_RESULT -->|Yes| ADD_ERR_MEM2[Add error to Episodic Memory]
|
||||
ADD_ERR_MEM2 --> RETURN_RESULT
|
||||
CHECK_RESULT -->|No| RETURN_RESULT([Return Result])
|
||||
```
|
||||
|
||||
## 4. Prompt Building Flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
START([build_system_prompt called]) --> GET_MEM[Get Memory from Context]
|
||||
|
||||
GET_MEM --> FORMAT_TOOLS[Format Tools Description]
|
||||
FORMAT_TOOLS --> FORMAT_PARAMS[Format Parameters Description]
|
||||
|
||||
FORMAT_PARAMS --> CHECK_MISSING[Check Missing Required Params]
|
||||
CHECK_MISSING --> HAS_MISSING{Has Missing?}
|
||||
|
||||
HAS_MISSING -->|Yes| FORMAT_MISSING[Format Missing Params Info]
|
||||
HAS_MISSING -->|No| FORMAT_EPISODIC
|
||||
FORMAT_MISSING --> FORMAT_EPISODIC
|
||||
|
||||
FORMAT_EPISODIC[Format Episodic Context]
|
||||
FORMAT_EPISODIC --> HAS_SEARCH{Has Search Results?}
|
||||
|
||||
HAS_SEARCH -->|Yes| ADD_SEARCH[Add Search Results Summary]
|
||||
HAS_SEARCH -->|No| CHECK_PENDING
|
||||
ADD_SEARCH --> CHECK_PENDING
|
||||
|
||||
CHECK_PENDING{Has Pending Question?}
|
||||
CHECK_PENDING -->|Yes| ADD_PENDING[Add Pending Question]
|
||||
CHECK_PENDING -->|No| CHECK_DOWNLOADS
|
||||
ADD_PENDING --> CHECK_DOWNLOADS
|
||||
|
||||
CHECK_DOWNLOADS{Has Active Downloads?}
|
||||
CHECK_DOWNLOADS -->|Yes| ADD_DOWNLOADS[Add Downloads Status]
|
||||
CHECK_DOWNLOADS -->|No| CHECK_ERRORS
|
||||
ADD_DOWNLOADS --> CHECK_ERRORS
|
||||
|
||||
CHECK_ERRORS{Has Recent Errors?}
|
||||
CHECK_ERRORS -->|Yes| ADD_ERRORS[Add Last Error]
|
||||
CHECK_ERRORS -->|No| FORMAT_STM
|
||||
ADD_ERRORS --> FORMAT_STM
|
||||
|
||||
FORMAT_STM[Format STM Context]
|
||||
FORMAT_STM --> HAS_WORKFLOW{Has Workflow?}
|
||||
|
||||
HAS_WORKFLOW -->|Yes| ADD_WORKFLOW[Add Workflow Info]
|
||||
HAS_WORKFLOW -->|No| CHECK_TOPIC
|
||||
ADD_WORKFLOW --> CHECK_TOPIC
|
||||
|
||||
CHECK_TOPIC{Has Topic?}
|
||||
CHECK_TOPIC -->|Yes| ADD_TOPIC[Add Current Topic]
|
||||
CHECK_TOPIC -->|No| CHECK_ENTITIES
|
||||
ADD_TOPIC --> CHECK_ENTITIES
|
||||
|
||||
CHECK_ENTITIES{Has Entities?}
|
||||
CHECK_ENTITIES -->|Yes| ADD_ENTITIES[Add Extracted Entities]
|
||||
CHECK_ENTITIES -->|No| BUILD_FINAL
|
||||
ADD_ENTITIES --> BUILD_FINAL
|
||||
|
||||
BUILD_FINAL[Assemble Final Prompt]
|
||||
BUILD_FINAL --> RETURN([Return System Prompt])
|
||||
```
|
||||
|
||||
## 5. Memory System Flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph Initialization
|
||||
INIT([init_memory called]) --> CREATE_MEM[Create Memory Instance]
|
||||
CREATE_MEM --> LOAD_LTM{LTM file exists?}
|
||||
LOAD_LTM -->|Yes| READ_FILE[Read ltm.json]
|
||||
LOAD_LTM -->|No| CREATE_DEFAULT[Create Default LTM]
|
||||
READ_FILE --> PARSE_JSON{Parse OK?}
|
||||
PARSE_JSON -->|Yes| RESTORE_LTM[Restore LTM from Dict]
|
||||
PARSE_JSON -->|No| CREATE_DEFAULT
|
||||
CREATE_DEFAULT --> CREATE_STM[Create Empty STM]
|
||||
RESTORE_LTM --> CREATE_STM
|
||||
CREATE_STM --> CREATE_EPIS[Create Empty Episodic]
|
||||
CREATE_EPIS --> SET_CTX[Set in Context Variable]
|
||||
SET_CTX --> RETURN_MEM([Return Memory])
|
||||
end
|
||||
|
||||
subgraph Access
|
||||
GET([get_memory called]) --> CHECK_CTX{Context has Memory?}
|
||||
CHECK_CTX -->|Yes| RETURN_CTX([Return Memory])
|
||||
CHECK_CTX -->|No| RAISE_ERR[Raise RuntimeError]
|
||||
end
|
||||
|
||||
subgraph Save
|
||||
SAVE([memory.save called]) --> SERIALIZE[Serialize LTM to Dict]
|
||||
SERIALIZE --> WRITE_JSON[Write to ltm.json]
|
||||
WRITE_JSON --> SAVE_OK{Write OK?}
|
||||
SAVE_OK -->|Yes| DONE([Done])
|
||||
SAVE_OK -->|No| LOG_ERR[Log Error & Raise]
|
||||
end
|
||||
```
|
||||
|
||||
## 6. Torrent Search & Download Flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph Search
|
||||
SEARCH_START([find_torrent called]) --> CREATE_UC[Create SearchTorrentsUseCase]
|
||||
CREATE_UC --> EXEC_SEARCH[Execute Search via Knaben API]
|
||||
EXEC_SEARCH --> SEARCH_OK{Results Found?}
|
||||
|
||||
SEARCH_OK -->|No| RETURN_ERR([Return Error])
|
||||
SEARCH_OK -->|Yes| GET_MEM[Get Memory]
|
||||
GET_MEM --> STORE_RESULTS[Store in Episodic Memory<br/>with indexes 1,2,3...]
|
||||
STORE_RESULTS --> SET_TOPIC[Set Topic: selecting_torrent]
|
||||
SET_TOPIC --> RETURN_RESULTS([Return Results])
|
||||
end
|
||||
|
||||
subgraph "Get by Index"
|
||||
GET_START([get_torrent_by_index called]) --> GET_MEM2[Get Memory]
|
||||
GET_MEM2 --> HAS_RESULTS{Has Search Results?}
|
||||
|
||||
HAS_RESULTS -->|No| NO_RESULTS([Return not_found Error])
|
||||
HAS_RESULTS -->|Yes| FIND_INDEX[Find Result by Index]
|
||||
FIND_INDEX --> FOUND{Found?}
|
||||
|
||||
FOUND -->|No| NOT_FOUND([Return not_found Error])
|
||||
FOUND -->|Yes| RETURN_TORRENT([Return Torrent Data])
|
||||
end
|
||||
|
||||
subgraph "Add by Index"
|
||||
ADD_START([add_torrent_by_index called]) --> CALL_GET[Call get_torrent_by_index]
|
||||
CALL_GET --> GET_OK{Got Torrent?}
|
||||
|
||||
GET_OK -->|No| RETURN_GET_ERR([Return Error])
|
||||
GET_OK -->|Yes| HAS_MAGNET{Has Magnet Link?}
|
||||
|
||||
HAS_MAGNET -->|No| NO_MAGNET([Return no_magnet Error])
|
||||
HAS_MAGNET -->|Yes| CALL_ADD[Call add_torrent_to_qbittorrent]
|
||||
CALL_ADD --> ADD_OK{Added OK?}
|
||||
|
||||
ADD_OK -->|No| RETURN_ADD_ERR([Return Error])
|
||||
ADD_OK -->|Yes| ADD_NAME[Add torrent_name to Result]
|
||||
ADD_NAME --> RETURN_SUCCESS([Return Success])
|
||||
end
|
||||
|
||||
subgraph "Add to qBittorrent"
|
||||
QB_START([add_torrent_to_qbittorrent called]) --> CREATE_UC2[Create AddTorrentUseCase]
|
||||
CREATE_UC2 --> EXEC_ADD[Execute Add via qBittorrent API]
|
||||
EXEC_ADD --> QB_OK{Added OK?}
|
||||
|
||||
QB_OK -->|No| QB_ERR([Return Error])
|
||||
QB_OK -->|Yes| GET_MEM3[Get Memory]
|
||||
GET_MEM3 --> FIND_NAME[Find Torrent Name from Search]
|
||||
FIND_NAME --> ADD_DOWNLOAD[Add to Active Downloads]
|
||||
ADD_DOWNLOAD --> SET_TOPIC2[Set Topic: downloading]
|
||||
SET_TOPIC2 --> END_WORKFLOW[End Current Workflow]
|
||||
END_WORKFLOW --> QB_SUCCESS([Return Success])
|
||||
end
|
||||
```
|
||||
|
||||
## 7. Filesystem Operations Flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph "Set Folder Path"
|
||||
SET_START([set_path_for_folder called]) --> VALIDATE_NAME[Validate Folder Name]
|
||||
VALIDATE_NAME --> NAME_OK{Valid Name?}
|
||||
|
||||
NAME_OK -->|No| INVALID_NAME([Return validation_failed])
|
||||
NAME_OK -->|Yes| RESOLVE_PATH[Resolve Path]
|
||||
|
||||
RESOLVE_PATH --> PATH_EXISTS{Path Exists?}
|
||||
PATH_EXISTS -->|No| NOT_EXISTS([Return invalid_path])
|
||||
PATH_EXISTS -->|Yes| IS_DIR{Is Directory?}
|
||||
|
||||
IS_DIR -->|No| NOT_DIR([Return invalid_path])
|
||||
IS_DIR -->|Yes| IS_READABLE{Is Readable?}
|
||||
|
||||
IS_READABLE -->|No| NO_READ([Return permission_denied])
|
||||
IS_READABLE -->|Yes| GET_MEM[Get Memory]
|
||||
|
||||
GET_MEM --> SET_CONFIG[Set in LTM Config]
|
||||
SET_CONFIG --> SAVE_MEM[Save Memory]
|
||||
SAVE_MEM --> SET_SUCCESS([Return Success])
|
||||
end
|
||||
|
||||
subgraph "List Folder"
|
||||
LIST_START([list_folder called]) --> VALIDATE_TYPE[Validate Folder Type]
|
||||
VALIDATE_TYPE --> TYPE_OK{Valid Type?}
|
||||
|
||||
TYPE_OK -->|No| INVALID_TYPE([Return validation_failed])
|
||||
TYPE_OK -->|Yes| SANITIZE[Sanitize Path]
|
||||
|
||||
SANITIZE --> SAFE{Path Safe?}
|
||||
SAFE -->|No| TRAVERSAL([Return forbidden])
|
||||
SAFE -->|Yes| GET_MEM2[Get Memory]
|
||||
|
||||
GET_MEM2 --> GET_CONFIG[Get Folder from Config]
|
||||
GET_CONFIG --> CONFIGURED{Folder Configured?}
|
||||
|
||||
CONFIGURED -->|No| NOT_SET([Return folder_not_set])
|
||||
CONFIGURED -->|Yes| BUILD_TARGET[Build Target Path]
|
||||
|
||||
BUILD_TARGET --> CHECK_SAFE[Check Path is Safe]
|
||||
CHECK_SAFE --> SAFE2{Within Base?}
|
||||
|
||||
SAFE2 -->|No| FORBIDDEN([Return forbidden])
|
||||
SAFE2 -->|Yes| TARGET_EXISTS{Target Exists?}
|
||||
|
||||
TARGET_EXISTS -->|No| NOT_FOUND([Return not_found])
|
||||
TARGET_EXISTS -->|Yes| TARGET_DIR{Is Directory?}
|
||||
|
||||
TARGET_DIR -->|No| NOT_A_DIR([Return not_a_directory])
|
||||
TARGET_DIR -->|Yes| LIST_DIR[List Directory Contents]
|
||||
|
||||
LIST_DIR --> LIST_OK{Permission OK?}
|
||||
LIST_OK -->|No| PERM_DENIED([Return permission_denied])
|
||||
LIST_OK -->|Yes| LIST_SUCCESS([Return Entries])
|
||||
end
|
||||
```
|
||||
|
||||
## 8. LLM Communication Flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph "DeepSeek Client"
|
||||
DS_START([complete called]) --> DS_BUILD[Build Request Body]
|
||||
DS_BUILD --> DS_HEADERS[Set Headers with API Key]
|
||||
DS_HEADERS --> DS_POST[POST to DeepSeek API]
|
||||
DS_POST --> DS_OK{Response OK?}
|
||||
|
||||
DS_OK -->|No| DS_ERR{Error Type?}
|
||||
DS_ERR -->|401| DS_AUTH([Raise LLMAuthenticationError])
|
||||
DS_ERR -->|429| DS_RATE([Raise LLMRateLimitError])
|
||||
DS_ERR -->|Other| DS_API([Raise LLMAPIError])
|
||||
|
||||
DS_OK -->|Yes| DS_PARSE[Parse JSON Response]
|
||||
DS_PARSE --> DS_EXTRACT[Extract Content]
|
||||
DS_EXTRACT --> DS_RETURN([Return Content String])
|
||||
end
|
||||
|
||||
subgraph "Ollama Client"
|
||||
OL_START([complete called]) --> OL_BUILD[Build Request Body]
|
||||
OL_BUILD --> OL_POST[POST to Ollama API]
|
||||
OL_POST --> OL_OK{Response OK?}
|
||||
|
||||
OL_OK -->|No| OL_ERR([Raise LLMAPIError])
|
||||
OL_OK -->|Yes| OL_PARSE[Parse JSON Response]
|
||||
OL_PARSE --> OL_EXTRACT[Extract Message Content]
|
||||
OL_EXTRACT --> OL_RETURN([Return Content String])
|
||||
end
|
||||
```
|
||||
|
||||
## Legend
|
||||
|
||||
| Symbol | Meaning |
|
||||
|--------|---------|
|
||||
| `([text])` | Start/End (Terminal) |
|
||||
| `[text]` | Process |
|
||||
| `{text}` | Decision |
|
||||
| `/text/` | Input/Output |
|
||||
| `-->` | Flow direction |
|
||||
| `-->\|label\|` | Conditional flow |
|
||||
264
docs/sequence_diagram.md
Normal file
264
docs/sequence_diagram.md
Normal file
@@ -0,0 +1,264 @@
|
||||
# Sequence Diagrams - Agent Media
|
||||
|
||||
## 1. Torrent Search and Download Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant User
|
||||
participant FastAPI as FastAPI Server
|
||||
participant Agent
|
||||
participant PromptBuilder
|
||||
participant LLM as LLM (DeepSeek/Ollama)
|
||||
participant Tools as Tool Registry
|
||||
participant Memory
|
||||
participant Knaben as Knaben API
|
||||
participant qBit as qBittorrent
|
||||
|
||||
User->>FastAPI: POST /v1/chat/completions<br/>"Find torrents for Inception 1080p"
|
||||
FastAPI->>Agent: step(user_input)
|
||||
|
||||
Agent->>Memory: stm.get_recent_history()
|
||||
Memory-->>Agent: conversation history
|
||||
|
||||
Agent->>PromptBuilder: build_system_prompt(memory)
|
||||
PromptBuilder->>Memory: ltm.config, episodic state
|
||||
Memory-->>PromptBuilder: context data
|
||||
PromptBuilder-->>Agent: system prompt with context
|
||||
|
||||
Agent->>LLM: complete(messages)
|
||||
LLM-->>Agent: {"action": {"name": "find_torrents", "args": {...}}}
|
||||
|
||||
Agent->>Agent: _parse_intent(response)
|
||||
Agent->>Tools: execute find_torrents
|
||||
Tools->>Knaben: search("Inception 1080p")
|
||||
Knaben-->>Tools: torrent results
|
||||
|
||||
Tools->>Memory: episodic.store_search_results()
|
||||
Memory-->>Tools: stored with indexes (1, 2, 3...)
|
||||
|
||||
Tools-->>Agent: {"status": "ok", "torrents": [...]}
|
||||
|
||||
Agent->>LLM: complete(messages + tool_result)
|
||||
LLM-->>Agent: "I found 5 torrents for Inception..."
|
||||
|
||||
Agent->>Memory: stm.add_message("user", input)
|
||||
Agent->>Memory: stm.add_message("assistant", response)
|
||||
Agent->>Memory: save()
|
||||
|
||||
Agent-->>FastAPI: final response
|
||||
FastAPI-->>User: JSON response
|
||||
|
||||
Note over User,qBit: User selects a torrent
|
||||
|
||||
User->>FastAPI: POST /v1/chat/completions<br/>"Download the 2nd one"
|
||||
FastAPI->>Agent: step(user_input)
|
||||
|
||||
Agent->>PromptBuilder: build_system_prompt(memory)
|
||||
PromptBuilder->>Memory: episodic.last_search_results
|
||||
Note right of Memory: Results still in memory:<br/>1. Inception.2010.1080p...<br/>2. Inception.1080p.BluRay...
|
||||
Memory-->>PromptBuilder: context with search results
|
||||
PromptBuilder-->>Agent: prompt showing available results
|
||||
|
||||
Agent->>LLM: complete(messages)
|
||||
LLM-->>Agent: {"action": {"name": "add_torrent_by_index", "args": {"index": 2}}}
|
||||
|
||||
Agent->>Tools: execute add_torrent_by_index(index=2)
|
||||
Tools->>Memory: episodic.get_result_by_index(2)
|
||||
Memory-->>Tools: torrent data with magnet link
|
||||
|
||||
Tools->>qBit: add_torrent(magnet_link)
|
||||
qBit-->>Tools: success
|
||||
|
||||
Tools->>Memory: episodic.add_active_download()
|
||||
Tools-->>Agent: {"status": "ok", "torrent_name": "Inception.1080p.BluRay"}
|
||||
|
||||
Agent->>LLM: complete(messages + tool_result)
|
||||
LLM-->>Agent: "I've added Inception to qBittorrent!"
|
||||
|
||||
Agent-->>FastAPI: final response
|
||||
FastAPI-->>User: JSON response
|
||||
```
|
||||
|
||||
## 2. Folder Configuration Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant User
|
||||
participant FastAPI as FastAPI Server
|
||||
participant Agent
|
||||
participant LLM as LLM
|
||||
participant Tools as Tool Registry
|
||||
participant FileManager
|
||||
participant Memory
|
||||
participant FS as Filesystem
|
||||
|
||||
User->>FastAPI: POST /v1/chat/completions<br/>"Set download folder to /mnt/media/downloads"
|
||||
FastAPI->>Agent: step(user_input)
|
||||
|
||||
Agent->>LLM: complete(messages)
|
||||
LLM-->>Agent: {"action": {"name": "set_path_for_folder", "args": {...}}}
|
||||
|
||||
Agent->>Tools: execute set_path_for_folder
|
||||
Tools->>FileManager: set_folder_path("download", "/mnt/media/downloads")
|
||||
|
||||
FileManager->>FS: Path.exists()?
|
||||
FS-->>FileManager: true
|
||||
FileManager->>FS: Path.is_dir()?
|
||||
FS-->>FileManager: true
|
||||
FileManager->>FS: os.access(R_OK)?
|
||||
FS-->>FileManager: true
|
||||
|
||||
FileManager->>Memory: ltm.set_config("download_folder", path)
|
||||
FileManager->>Memory: save()
|
||||
Memory->>FS: write ltm.json
|
||||
|
||||
FileManager-->>Tools: {"status": "ok", "path": "/mnt/media/downloads"}
|
||||
Tools-->>Agent: result
|
||||
|
||||
Agent->>LLM: complete(messages + tool_result)
|
||||
LLM-->>Agent: "Download folder set to /mnt/media/downloads"
|
||||
|
||||
Agent-->>FastAPI: final response
|
||||
FastAPI-->>User: JSON response
|
||||
```
|
||||
|
||||
## 3. Multi-Tool Workflow (Search Movie → Find Torrents → Download)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant User
|
||||
participant Agent
|
||||
participant LLM as LLM
|
||||
participant TMDB as TMDB API
|
||||
participant Knaben as Knaben API
|
||||
participant qBit as qBittorrent
|
||||
participant Memory
|
||||
|
||||
User->>Agent: "Download Dune 2 in 4K"
|
||||
|
||||
rect rgb(240, 248, 255)
|
||||
Note over Agent,TMDB: Step 1: Identify the movie
|
||||
Agent->>LLM: complete(messages)
|
||||
LLM-->>Agent: {"action": "find_media_imdb_id", "args": {"media_title": "Dune 2"}}
|
||||
Agent->>TMDB: search_movie("Dune 2")
|
||||
TMDB-->>Agent: {title: "Dune: Part Two", imdb_id: "tt15239678", year: 2024}
|
||||
Agent->>Memory: stm.set_entity("last_media_search", {...})
|
||||
end
|
||||
|
||||
rect rgb(255, 248, 240)
|
||||
Note over Agent,Knaben: Step 2: Search for torrents
|
||||
Agent->>LLM: complete(messages + movie_info)
|
||||
LLM-->>Agent: {"action": "find_torrents", "args": {"media_title": "Dune Part Two 2024 4K"}}
|
||||
Agent->>Knaben: search("Dune Part Two 2024 4K")
|
||||
Knaben-->>Agent: [torrent1, torrent2, torrent3...]
|
||||
Agent->>Memory: episodic.store_search_results()
|
||||
end
|
||||
|
||||
rect rgb(240, 255, 240)
|
||||
Note over Agent,qBit: Step 3: Add best torrent
|
||||
Agent->>LLM: complete(messages + torrents)
|
||||
LLM-->>Agent: {"action": "add_torrent_by_index", "args": {"index": 1}}
|
||||
Agent->>Memory: episodic.get_result_by_index(1)
|
||||
Memory-->>Agent: torrent with magnet
|
||||
Agent->>qBit: add_torrent(magnet)
|
||||
qBit-->>Agent: success
|
||||
Agent->>Memory: episodic.add_active_download()
|
||||
end
|
||||
|
||||
Agent->>LLM: complete(messages + all_results)
|
||||
LLM-->>Agent: "I found Dune: Part Two (2024) and added the 4K torrent to qBittorrent!"
|
||||
Agent-->>User: Final response
|
||||
```
|
||||
|
||||
## 4. Error Handling Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant User
|
||||
participant Agent
|
||||
participant LLM as LLM
|
||||
participant Tools as Tool Registry
|
||||
participant Memory
|
||||
participant API as External API
|
||||
|
||||
User->>Agent: "Download the 5th torrent"
|
||||
|
||||
Agent->>LLM: complete(messages)
|
||||
LLM-->>Agent: {"action": "add_torrent_by_index", "args": {"index": 5}}
|
||||
|
||||
Agent->>Tools: execute add_torrent_by_index(5)
|
||||
Tools->>Memory: episodic.get_result_by_index(5)
|
||||
|
||||
alt No search results
|
||||
Memory-->>Tools: None (no previous search)
|
||||
Tools-->>Agent: {"status": "error", "error": "not_found"}
|
||||
Agent->>Memory: episodic.add_error("add_torrent_by_index", "not_found")
|
||||
else Index out of range
|
||||
Memory-->>Tools: None (only 3 results)
|
||||
Tools-->>Agent: {"status": "error", "error": "not_found"}
|
||||
Agent->>Memory: episodic.add_error("add_torrent_by_index", "not_found")
|
||||
end
|
||||
|
||||
Agent->>LLM: complete(messages + error)
|
||||
LLM-->>Agent: "I couldn't find torrent #5. Please search for torrents first."
|
||||
|
||||
Agent-->>User: Error explanation
|
||||
|
||||
Note over User,API: User searches first
|
||||
|
||||
User->>Agent: "Search for Matrix 1999"
|
||||
Agent->>API: search("Matrix 1999")
|
||||
API-->>Agent: [3 results]
|
||||
Agent->>Memory: episodic.store_search_results()
|
||||
Agent-->>User: "Found 3 torrents..."
|
||||
|
||||
User->>Agent: "Download the 2nd one"
|
||||
Agent->>Memory: episodic.get_result_by_index(2)
|
||||
Memory-->>Agent: torrent data ✓
|
||||
Agent-->>User: "Added to qBittorrent!"
|
||||
```
|
||||
|
||||
## 5. Background Events Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant User
|
||||
participant Agent
|
||||
participant Memory
|
||||
participant qBit as qBittorrent
|
||||
participant LLM as LLM
|
||||
|
||||
Note over qBit,Memory: Background: Download completes
|
||||
qBit--)Memory: episodic.complete_download(task_id, file_path)
|
||||
Memory->>Memory: add_background_event("download_complete", {...})
|
||||
|
||||
Note over User,LLM: Later: User sends a message
|
||||
User->>Agent: "What's new?"
|
||||
|
||||
Agent->>Memory: episodic.get_unread_events()
|
||||
Memory-->>Agent: [{type: "download_complete", data: {name: "Inception.1080p"}}]
|
||||
|
||||
Agent->>Agent: _check_unread_events()
|
||||
Note right of Agent: Formats notification:<br/>"Download completed: Inception.1080p"
|
||||
|
||||
Agent->>LLM: complete(messages + notification)
|
||||
LLM-->>Agent: "Good news! Inception.1080p has finished downloading."
|
||||
|
||||
Agent-->>User: Response with notification
|
||||
```
|
||||
|
||||
## Legend
|
||||
|
||||
| Element | Description |
|
||||
|---------|-------------|
|
||||
| `rect rgb(...)` | Grouped steps in a workflow |
|
||||
| `alt/else` | Conditional branches |
|
||||
| `Note` | Explanatory notes |
|
||||
| `-->>` | Response/return |
|
||||
| `->>` | Request/call |
|
||||
| `--))` | Async event |
|
||||
Reference in New Issue
Block a user