Add comprehensive Technical Architecture PDF documentation
Generate a professional 22-page A4 PDF covering the full ModComms system architecture including: system overview, multi-agent AI pipeline, WebSocket analysis flow, database schema (15 tables), frontend component hierarchy, Azure AD authentication & RBAC, knowledge base processing pipeline, deployment architecture, REST API reference, and appendices. Includes 8 Mermaid diagrams rendered to high-res PNGs, styled tables, and consistent Barclays design tokens throughout. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1403
documentation/architecture/ModComms_Technical_Architecture.pdf
Normal file
49
documentation/architecture/diagrams/01_system_overview.mmd
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
graph TB
|
||||||
|
subgraph "Client Tier"
|
||||||
|
SPA["React SPA<br/>(Vite + TypeScript)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Application Tier"
|
||||||
|
API["FastAPI Backend<br/>(Python 3.11+)"]
|
||||||
|
WS["WebSocket Server<br/>(/ws/analyze)"]
|
||||||
|
REST["REST API<br/>(/api/*)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "AI Services"
|
||||||
|
GEMINI["Google Gemini<br/>2.5 Flash"]
|
||||||
|
LLAMA["LlamaParse<br/>(Document Parsing)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Authentication"
|
||||||
|
AZURE["Azure AD<br/>(MSAL)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Data Tier"
|
||||||
|
PG["PostgreSQL<br/>(asyncpg)"]
|
||||||
|
FS["File Storage<br/>(Local Disk)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
SPA -->|"HTTPS"| REST
|
||||||
|
SPA -->|"WSS"| WS
|
||||||
|
SPA -->|"MSAL Auth"| AZURE
|
||||||
|
API --- WS
|
||||||
|
API --- REST
|
||||||
|
REST -->|"SQLAlchemy Async"| PG
|
||||||
|
WS -->|"SQLAlchemy Async"| PG
|
||||||
|
REST -->|"Read/Write"| FS
|
||||||
|
WS -->|"Store Files"| FS
|
||||||
|
API -->|"Gemini API"| GEMINI
|
||||||
|
API -->|"Parse PDFs"| LLAMA
|
||||||
|
AZURE -.->|"JWT Verify"| API
|
||||||
|
|
||||||
|
classDef client fill:#006DE3,stroke:#1A2142,color:#fff
|
||||||
|
classDef app fill:#1A2142,stroke:#006DE3,color:#fff
|
||||||
|
classDef ai fill:#01A1A2,stroke:#1A2142,color:#fff
|
||||||
|
classDef auth fill:#7A0FF9,stroke:#1A2142,color:#fff
|
||||||
|
classDef data fill:#C3FB5A,stroke:#1A2142,color:#1A2142
|
||||||
|
|
||||||
|
class SPA client
|
||||||
|
class API,WS,REST app
|
||||||
|
class GEMINI,LLAMA ai
|
||||||
|
class AZURE auth
|
||||||
|
class PG,FS data
|
||||||
41
documentation/architecture/diagrams/02_agent_pipeline.mmd
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
graph TB
|
||||||
|
INPUT["Proof Upload<br/>(Image / PDF)"] --> DECODE["Decode & Rasterize"]
|
||||||
|
DECODE --> PARALLEL
|
||||||
|
|
||||||
|
subgraph PARALLEL ["Parallel Agent Execution"]
|
||||||
|
direction LR
|
||||||
|
LEGAL["Legal Agent<br/>Advertising standards,<br/>disclaimers, financial<br/>promotion detection"]
|
||||||
|
BRAND["Brand Agent<br/>Logo usage, colors,<br/>typography, design<br/>principles"]
|
||||||
|
CBP["Channel Best<br/>Practices Agent<br/>Platform guidelines,<br/>accessibility"]
|
||||||
|
CTS["Channel Tech<br/>Specs Agent<br/>Dimensions, file size,<br/>format compliance"]
|
||||||
|
end
|
||||||
|
|
||||||
|
PARALLEL --> LEAD["Lead Agent<br/>(Synthesis)"]
|
||||||
|
|
||||||
|
LEAD --> DECISION{RAG Decision}
|
||||||
|
DECISION -->|"All pass,<br/>max 1 amber/agent,<br/>no Legal amber"| GREEN["GREEN<br/>Passed"]
|
||||||
|
DECISION -->|">1 issue/agent,<br/>or Legal amber"| AMBER["AMBER<br/>Requires Manual<br/>Legal Review"]
|
||||||
|
DECISION -->|"Any Red"| RED["RED<br/>Failed"]
|
||||||
|
|
||||||
|
subgraph "Revision-Aware"
|
||||||
|
PREV["Previous Version<br/>Analysis"] -.->|"Context"| LEGAL
|
||||||
|
PREV -.->|"Context"| BRAND
|
||||||
|
PREV -.->|"Context"| CBP
|
||||||
|
PREV -.->|"Context"| CTS
|
||||||
|
end
|
||||||
|
|
||||||
|
classDef input fill:#006DE3,stroke:#1A2142,color:#fff
|
||||||
|
classDef agent fill:#1A2142,stroke:#006DE3,color:#fff
|
||||||
|
classDef lead fill:#7A0FF9,stroke:#1A2142,color:#fff
|
||||||
|
classDef green fill:#09821F,stroke:#1A2142,color:#fff
|
||||||
|
classDef amber fill:#FFBA00,stroke:#1A2142,color:#1A2142
|
||||||
|
classDef red fill:#E3000F,stroke:#1A2142,color:#fff
|
||||||
|
classDef revision fill:#01A1A2,stroke:#1A2142,color:#fff
|
||||||
|
|
||||||
|
class INPUT,DECODE input
|
||||||
|
class LEGAL,BRAND,CBP,CTS agent
|
||||||
|
class LEAD lead
|
||||||
|
class GREEN green
|
||||||
|
class AMBER amber
|
||||||
|
class RED red
|
||||||
|
class PREV revision
|
||||||
63
documentation/architecture/diagrams/03_websocket_flow.mmd
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
sequenceDiagram
|
||||||
|
participant C as React Client
|
||||||
|
participant WS as WebSocket Server
|
||||||
|
participant AUTH as Auth Service
|
||||||
|
participant DB as PostgreSQL
|
||||||
|
participant AS as Analysis Service
|
||||||
|
participant A1 as Legal Agent
|
||||||
|
participant A2 as Brand Agent
|
||||||
|
participant A3 as Channel BP Agent
|
||||||
|
participant A4 as Channel TS Agent
|
||||||
|
participant LA as Lead Agent
|
||||||
|
participant GEM as Gemini API
|
||||||
|
|
||||||
|
C->>WS: Connect /ws/analyze
|
||||||
|
WS-->>C: Connection established
|
||||||
|
|
||||||
|
C->>WS: {"type":"analyze", "file_data":"base64...", "access_token":"jwt..."}
|
||||||
|
|
||||||
|
WS->>AUTH: verify_access_token(jwt)
|
||||||
|
AUTH-->>WS: claims {oid, name, role}
|
||||||
|
|
||||||
|
WS->>DB: Fetch previous version analysis
|
||||||
|
DB-->>WS: previous_analysis (if revision)
|
||||||
|
|
||||||
|
par Parallel Agent Execution
|
||||||
|
WS-->>C: {"type":"agent_started", "agent_name":"Legal Agent"}
|
||||||
|
AS->>A1: analyze(images, prev_review)
|
||||||
|
A1->>GEM: Gemini API call
|
||||||
|
GEM-->>A1: Response
|
||||||
|
A1-->>AS: SubReview
|
||||||
|
WS-->>C: {"type":"agent_completed", "agent_name":"Legal Agent", "review":{...}}
|
||||||
|
and
|
||||||
|
WS-->>C: {"type":"agent_started", "agent_name":"Brand Agent"}
|
||||||
|
AS->>A2: analyze(images, prev_review, brand)
|
||||||
|
A2->>GEM: Gemini API call
|
||||||
|
GEM-->>A2: Response
|
||||||
|
A2-->>AS: SubReview
|
||||||
|
WS-->>C: {"type":"agent_completed", "agent_name":"Brand Agent", "review":{...}}
|
||||||
|
and
|
||||||
|
WS-->>C: {"type":"agent_started", "agent_name":"Channel BP Agent"}
|
||||||
|
AS->>A3: analyze(images, prev_review)
|
||||||
|
A3->>GEM: Gemini API call
|
||||||
|
GEM-->>A3: Response
|
||||||
|
A3-->>AS: SubReview
|
||||||
|
WS-->>C: {"type":"agent_completed", "agent_name":"Channel BP Agent", "review":{...}}
|
||||||
|
and
|
||||||
|
WS-->>C: {"type":"agent_started", "agent_name":"Channel TS Agent"}
|
||||||
|
AS->>A4: analyze(images, prev_review)
|
||||||
|
A4->>GEM: Gemini API call
|
||||||
|
GEM-->>A4: Response
|
||||||
|
A4-->>AS: SubReview
|
||||||
|
WS-->>C: {"type":"agent_completed", "agent_name":"Channel TS Agent", "review":{...}}
|
||||||
|
end
|
||||||
|
|
||||||
|
AS->>LA: synthesize(all_reviews)
|
||||||
|
LA->>GEM: Gemini API call
|
||||||
|
GEM-->>LA: Summary + RAG status
|
||||||
|
LA-->>AS: overall_status, summary
|
||||||
|
|
||||||
|
AS->>DB: Persist proof + version + agent_review
|
||||||
|
AS->>DB: Store file to disk
|
||||||
|
|
||||||
|
WS-->>C: {"type":"complete", "result":{...}, "proof_id":"uuid", "version_id":"uuid"}
|
||||||
167
documentation/architecture/diagrams/04_database_erd.mmd
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
erDiagram
|
||||||
|
agencies ||--o{ users : "has"
|
||||||
|
agencies ||--o{ campaigns : "owns"
|
||||||
|
users ||--o{ campaigns : "creates"
|
||||||
|
users ||--o{ proofs : "creates"
|
||||||
|
users ||--o{ flagged_items : "submits"
|
||||||
|
users ||--o{ resolved_items : "submits"
|
||||||
|
users ||--o{ user_change_logs : "subject"
|
||||||
|
campaigns ||--o{ proofs : "contains"
|
||||||
|
proofs ||--o{ proof_versions : "has"
|
||||||
|
proof_versions ||--o{ flagged_items : "has"
|
||||||
|
proof_versions ||--o{ resolved_items : "has"
|
||||||
|
proof_versions ||--o{ error_items : "has"
|
||||||
|
knowledge_bases ||--o{ source_documents : "has"
|
||||||
|
knowledge_bases ||--o{ spec_versions : "has"
|
||||||
|
knowledge_bases ||--o{ processing_jobs : "has"
|
||||||
|
dropdown_options ||--o{ dropdown_options : "parent"
|
||||||
|
|
||||||
|
agencies {
|
||||||
|
uuid id PK
|
||||||
|
string name UK
|
||||||
|
timestamp created_at
|
||||||
|
}
|
||||||
|
|
||||||
|
users {
|
||||||
|
uuid id PK
|
||||||
|
string azure_ad_oid UK
|
||||||
|
string email
|
||||||
|
string name
|
||||||
|
string role
|
||||||
|
uuid agency_id FK
|
||||||
|
timestamp created_at
|
||||||
|
}
|
||||||
|
|
||||||
|
user_change_logs {
|
||||||
|
uuid id PK
|
||||||
|
uuid user_id FK
|
||||||
|
uuid changed_by_id FK
|
||||||
|
string change_type
|
||||||
|
string field_changed
|
||||||
|
string old_value
|
||||||
|
string new_value
|
||||||
|
timestamp created_at
|
||||||
|
}
|
||||||
|
|
||||||
|
campaigns {
|
||||||
|
uuid id PK
|
||||||
|
string name
|
||||||
|
string workfront_id
|
||||||
|
string client_lead
|
||||||
|
string agency_lead
|
||||||
|
string brand_guidelines
|
||||||
|
string status
|
||||||
|
uuid agency_id FK
|
||||||
|
uuid created_by FK
|
||||||
|
timestamp created_at
|
||||||
|
timestamp updated_at
|
||||||
|
}
|
||||||
|
|
||||||
|
proofs {
|
||||||
|
uuid id PK
|
||||||
|
uuid campaign_id FK
|
||||||
|
string proof_name
|
||||||
|
string channel
|
||||||
|
string sub_channel
|
||||||
|
string proof_type
|
||||||
|
string workfront_id
|
||||||
|
uuid created_by FK
|
||||||
|
timestamp created_at
|
||||||
|
}
|
||||||
|
|
||||||
|
proof_versions {
|
||||||
|
uuid id PK
|
||||||
|
uuid proof_id FK
|
||||||
|
integer version
|
||||||
|
string file_storage_key
|
||||||
|
text thumbnail_url
|
||||||
|
jsonb agent_review
|
||||||
|
string overall_status
|
||||||
|
string file_hash
|
||||||
|
boolean is_identical_file
|
||||||
|
timestamp created_at
|
||||||
|
}
|
||||||
|
|
||||||
|
flagged_items {
|
||||||
|
uuid id PK
|
||||||
|
uuid proof_version_id FK
|
||||||
|
string agent_flagged
|
||||||
|
text comments
|
||||||
|
uuid submitter_id FK
|
||||||
|
timestamp created_at
|
||||||
|
}
|
||||||
|
|
||||||
|
resolved_items {
|
||||||
|
uuid id PK
|
||||||
|
uuid proof_version_id FK
|
||||||
|
string agent
|
||||||
|
text issue
|
||||||
|
text resolution
|
||||||
|
uuid submitter_id FK
|
||||||
|
timestamp created_at
|
||||||
|
}
|
||||||
|
|
||||||
|
error_items {
|
||||||
|
uuid id PK
|
||||||
|
uuid proof_version_id FK
|
||||||
|
text error_summary
|
||||||
|
timestamp created_at
|
||||||
|
}
|
||||||
|
|
||||||
|
knowledge_bases {
|
||||||
|
uuid id PK
|
||||||
|
string agent_key UK
|
||||||
|
string display_name
|
||||||
|
text description
|
||||||
|
timestamp created_at
|
||||||
|
}
|
||||||
|
|
||||||
|
source_documents {
|
||||||
|
uuid id PK
|
||||||
|
uuid knowledge_base_id FK
|
||||||
|
string filename
|
||||||
|
string file_storage_key
|
||||||
|
integer file_size_bytes
|
||||||
|
string mime_type
|
||||||
|
uuid uploaded_by_id FK
|
||||||
|
string uploaded_by_name
|
||||||
|
text parsed_markdown
|
||||||
|
string parse_status
|
||||||
|
timestamp created_at
|
||||||
|
}
|
||||||
|
|
||||||
|
spec_versions {
|
||||||
|
uuid id PK
|
||||||
|
uuid knowledge_base_id FK
|
||||||
|
integer version_number
|
||||||
|
text content
|
||||||
|
jsonb source_document_ids
|
||||||
|
uuid generated_by_id FK
|
||||||
|
boolean is_active
|
||||||
|
integer char_count
|
||||||
|
timestamp created_at
|
||||||
|
}
|
||||||
|
|
||||||
|
processing_jobs {
|
||||||
|
uuid id PK
|
||||||
|
uuid knowledge_base_id FK
|
||||||
|
string status
|
||||||
|
uuid triggered_by_id FK
|
||||||
|
integer total_documents
|
||||||
|
integer parsed_documents
|
||||||
|
uuid spec_version_id FK
|
||||||
|
text error_message
|
||||||
|
jsonb log
|
||||||
|
timestamp started_at
|
||||||
|
timestamp completed_at
|
||||||
|
timestamp created_at
|
||||||
|
}
|
||||||
|
|
||||||
|
dropdown_options {
|
||||||
|
uuid id PK
|
||||||
|
string option_type
|
||||||
|
uuid parent_id FK
|
||||||
|
string value
|
||||||
|
integer display_order
|
||||||
|
timestamp created_at
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
graph TB
|
||||||
|
INDEX["index.tsx<br/>(MsalProvider)"] --> APP["App.tsx<br/>(Auth Gate + Router)"]
|
||||||
|
APP --> UP["UserProvider<br/>(UserContext)"]
|
||||||
|
UP --> AC["AppContent"]
|
||||||
|
|
||||||
|
AC --> SB["Sidebar"]
|
||||||
|
AC --> AFB["AgencyFilterBar"]
|
||||||
|
|
||||||
|
AC --> HOME["Home View"]
|
||||||
|
AC --> CAMP["Campaigns"]
|
||||||
|
AC --> ANAL["Analytics"]
|
||||||
|
AC --> AUDIT["Auditing"]
|
||||||
|
AC --> WIP["WIPReviewer"]
|
||||||
|
AC --> KB["KnowledgeBase"]
|
||||||
|
AC --> SET["Settings"]
|
||||||
|
AC --> USERMGMT["UserManagement"]
|
||||||
|
AC --> PROF["Profile"]
|
||||||
|
AC --> COPY["CopyGenAI"]
|
||||||
|
AC --> LOGIN["Login"]
|
||||||
|
|
||||||
|
HOME --> HERO["Hero<br/>(Upload / Analyze)"]
|
||||||
|
HOME --> CO["ChecksOverview<br/>(Agent Results)"]
|
||||||
|
HOME --> FR["FeedbackReport<br/>(PDF Export)"]
|
||||||
|
|
||||||
|
CAMP --> CL["Campaign List"]
|
||||||
|
CAMP --> CD["Campaign Detail"]
|
||||||
|
CD --> PL["Proof List"]
|
||||||
|
CD --> PV["Proof Version Viewer"]
|
||||||
|
|
||||||
|
KB --> KBL["KB List"]
|
||||||
|
KB --> KBD["KB Detail"]
|
||||||
|
KBD --> DOCS["Source Documents"]
|
||||||
|
KBD --> SPEC["Spec Versions"]
|
||||||
|
KBD --> DIFF["Version Diff"]
|
||||||
|
|
||||||
|
subgraph "Services"
|
||||||
|
GS["geminiService.ts<br/>(WebSocket Client)"]
|
||||||
|
AS["apiService.ts<br/>(REST Client)"]
|
||||||
|
AUTH["authService.ts<br/>(MSAL Auth)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
HERO --> GS
|
||||||
|
CAMP --> AS
|
||||||
|
KB --> AS
|
||||||
|
APP --> AUTH
|
||||||
|
|
||||||
|
classDef root fill:#006DE3,stroke:#1A2142,color:#fff
|
||||||
|
classDef view fill:#1A2142,stroke:#006DE3,color:#fff
|
||||||
|
classDef component fill:#01A1A2,stroke:#1A2142,color:#fff
|
||||||
|
classDef service fill:#C3FB5A,stroke:#1A2142,color:#1A2142
|
||||||
|
|
||||||
|
class INDEX,APP,UP,AC root
|
||||||
|
class HOME,CAMP,ANAL,AUDIT,WIP,KB,SET,USERMGMT,PROF,COPY,LOGIN view
|
||||||
|
class SB,AFB,HERO,CO,FR,CL,CD,PL,PV,KBL,KBD,DOCS,SPEC,DIFF component
|
||||||
|
class GS,AS,AUTH service
|
||||||
48
documentation/architecture/diagrams/06_auth_rbac_flow.mmd
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
sequenceDiagram
|
||||||
|
participant U as User Browser
|
||||||
|
participant SPA as React SPA
|
||||||
|
participant MSAL as MSAL.js
|
||||||
|
participant AAD as Azure AD
|
||||||
|
participant API as FastAPI Backend
|
||||||
|
participant DB as PostgreSQL
|
||||||
|
|
||||||
|
U->>SPA: Navigate to app
|
||||||
|
SPA->>MSAL: Check authentication
|
||||||
|
MSAL-->>SPA: Not authenticated
|
||||||
|
|
||||||
|
SPA->>MSAL: loginPopup()
|
||||||
|
MSAL->>AAD: OAuth2 Authorization
|
||||||
|
AAD-->>U: Login prompt
|
||||||
|
U->>AAD: Enter credentials
|
||||||
|
AAD-->>MSAL: ID Token + Access Token
|
||||||
|
MSAL-->>SPA: Authentication success
|
||||||
|
|
||||||
|
Note over SPA: User now authenticated
|
||||||
|
|
||||||
|
SPA->>API: GET /api/me (Bearer token)
|
||||||
|
API->>API: verify_access_token(jwt)
|
||||||
|
API->>API: Decode & validate claims
|
||||||
|
API->>DB: get_or_create_from_azure(oid, email, name)
|
||||||
|
|
||||||
|
alt First login
|
||||||
|
DB-->>API: Create user (role: basic_user, agency: null)
|
||||||
|
else Existing user
|
||||||
|
DB-->>API: Return existing user
|
||||||
|
end
|
||||||
|
|
||||||
|
API-->>SPA: {id, email, name, role, agencyId, agencyName}
|
||||||
|
|
||||||
|
Note over SPA: Role-based UI rendering
|
||||||
|
|
||||||
|
rect rgb(9, 130, 31)
|
||||||
|
Note over SPA,API: super_admin: Full access to all features
|
||||||
|
end
|
||||||
|
rect rgb(0, 109, 227)
|
||||||
|
Note over SPA,API: oversight_admin: Read-only access to all campaigns
|
||||||
|
end
|
||||||
|
rect rgb(255, 186, 0)
|
||||||
|
Note over SPA,API: agency_admin: Full access within own agency
|
||||||
|
end
|
||||||
|
rect rgb(227, 0, 15)
|
||||||
|
Note over SPA,API: basic_user: Limited access within own agency
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
graph TB
|
||||||
|
UPLOAD["Admin Uploads<br/>Source Documents<br/>(PDF, DOCX, etc.)"] --> STORE["Store in<br/>File Storage"]
|
||||||
|
STORE --> TRIGGER["Trigger<br/>Processing Job"]
|
||||||
|
|
||||||
|
TRIGGER --> PARSE["Parse Documents<br/>(LlamaParse API)"]
|
||||||
|
PARSE --> MD["Parsed Markdown<br/>per Document"]
|
||||||
|
|
||||||
|
MD --> COMBINE["Combine All<br/>Parsed Markdown"]
|
||||||
|
COMBINE --> DISTIL["Distil via Gemini<br/>(Generate Spec)"]
|
||||||
|
|
||||||
|
DISTIL --> SPEC["New Spec Version<br/>(Versioned Content)"]
|
||||||
|
SPEC --> ACTIVATE["Set as Active<br/>Spec Version"]
|
||||||
|
|
||||||
|
ACTIVATE --> CACHE["Invalidate<br/>Reference Docs Cache"]
|
||||||
|
CACHE --> AGENTS["Agents Use<br/>Updated Specs"]
|
||||||
|
|
||||||
|
subgraph "5 Knowledge Base Types"
|
||||||
|
direction LR
|
||||||
|
KB1["Legal<br/>Compliance"]
|
||||||
|
KB2["Barclays<br/>Brand"]
|
||||||
|
KB3["Barclaycard<br/>Brand"]
|
||||||
|
KB4["Channel<br/>Best Practices"]
|
||||||
|
KB5["Channel<br/>Tech Specs"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Processing Job Tracking"
|
||||||
|
direction LR
|
||||||
|
S1["pending"]
|
||||||
|
S1 --> S2["parsing"]
|
||||||
|
S2 --> S3["distilling"]
|
||||||
|
S3 --> S4["completed"]
|
||||||
|
S2 -.-> S5["failed"]
|
||||||
|
S3 -.-> S5
|
||||||
|
end
|
||||||
|
|
||||||
|
classDef upload fill:#006DE3,stroke:#1A2142,color:#fff
|
||||||
|
classDef process fill:#1A2142,stroke:#006DE3,color:#fff
|
||||||
|
classDef output fill:#C3FB5A,stroke:#1A2142,color:#1A2142
|
||||||
|
classDef cache fill:#01A1A2,stroke:#1A2142,color:#fff
|
||||||
|
classDef kb fill:#7A0FF9,stroke:#1A2142,color:#fff
|
||||||
|
classDef status fill:#FFBA00,stroke:#1A2142,color:#1A2142
|
||||||
|
classDef failed fill:#E3000F,stroke:#1A2142,color:#fff
|
||||||
|
|
||||||
|
class UPLOAD upload
|
||||||
|
class STORE,TRIGGER,PARSE,MD,COMBINE,DISTIL process
|
||||||
|
class SPEC,ACTIVATE output
|
||||||
|
class CACHE,AGENTS cache
|
||||||
|
class KB1,KB2,KB3,KB4,KB5 kb
|
||||||
|
class S1,S2,S3,S4 status
|
||||||
|
class S5 failed
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
graph TB
|
||||||
|
subgraph "User"
|
||||||
|
BROWSER["Web Browser"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Apache Web Server (Port 443)"
|
||||||
|
APACHE["Apache2<br/>+ mod_proxy"]
|
||||||
|
STATIC["Static Files<br/>(React Build)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Docker Compose"
|
||||||
|
subgraph "Backend Container"
|
||||||
|
UVICORN["Uvicorn<br/>(ASGI Server)"]
|
||||||
|
FASTAPI["FastAPI App<br/>(Port 8000)"]
|
||||||
|
end
|
||||||
|
subgraph "Database Container"
|
||||||
|
POSTGRES["PostgreSQL 15<br/>(Port 5432)"]
|
||||||
|
PGDATA["Persistent Volume<br/>(/var/lib/postgresql/data)"]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "File Storage"
|
||||||
|
UPLOADS["uploads/<br/>(Proof Files)"]
|
||||||
|
KB_FILES["kb_storage/<br/>(Knowledge Base Docs)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "External Services"
|
||||||
|
GEMINI["Google Gemini API"]
|
||||||
|
AZURE["Azure AD"]
|
||||||
|
LLAMA["LlamaParse API"]
|
||||||
|
end
|
||||||
|
|
||||||
|
BROWSER -->|"HTTPS"| APACHE
|
||||||
|
APACHE -->|"Serve"| STATIC
|
||||||
|
APACHE -->|"Proxy /api/*<br/>/ws/* /health /info"| UVICORN
|
||||||
|
UVICORN --- FASTAPI
|
||||||
|
FASTAPI -->|"asyncpg"| POSTGRES
|
||||||
|
POSTGRES --- PGDATA
|
||||||
|
FASTAPI -->|"Read/Write"| UPLOADS
|
||||||
|
FASTAPI -->|"Read/Write"| KB_FILES
|
||||||
|
FASTAPI -->|"HTTPS"| GEMINI
|
||||||
|
FASTAPI -->|"HTTPS"| LLAMA
|
||||||
|
BROWSER -->|"OAuth2"| AZURE
|
||||||
|
|
||||||
|
classDef user fill:#006DE3,stroke:#1A2142,color:#fff
|
||||||
|
classDef proxy fill:#C3FB5A,stroke:#1A2142,color:#1A2142
|
||||||
|
classDef backend fill:#1A2142,stroke:#006DE3,color:#fff
|
||||||
|
classDef db fill:#01A1A2,stroke:#1A2142,color:#fff
|
||||||
|
classDef storage fill:#FFBA00,stroke:#1A2142,color:#1A2142
|
||||||
|
classDef external fill:#7A0FF9,stroke:#1A2142,color:#fff
|
||||||
|
|
||||||
|
class BROWSER user
|
||||||
|
class APACHE,STATIC proxy
|
||||||
|
class UVICORN,FASTAPI backend
|
||||||
|
class POSTGRES,PGDATA db
|
||||||
|
class UPLOADS,KB_FILES storage
|
||||||
|
class GEMINI,AZURE,LLAMA external
|
||||||
|
After Width: | Height: | Size: 295 KiB |
|
After Width: | Height: | Size: 334 KiB |
|
After Width: | Height: | Size: 750 KiB |
BIN
documentation/architecture/diagrams/rendered/04_database_erd.png
Normal file
|
After Width: | Height: | Size: 1,015 KiB |
|
After Width: | Height: | Size: 288 KiB |
|
After Width: | Height: | Size: 449 KiB |
|
After Width: | Height: | Size: 360 KiB |
|
After Width: | Height: | Size: 379 KiB |
928
documentation/architecture/generate_architecture_pdf.py
Normal file
|
|
@ -0,0 +1,928 @@
|
||||||
|
"""
|
||||||
|
ModComms Technical Architecture PDF Generator
|
||||||
|
|
||||||
|
Produces a comprehensive A4 PDF document describing the full system architecture,
|
||||||
|
multi-agent AI pipeline, database schema, frontend/backend design, authentication,
|
||||||
|
knowledge base system, and deployment.
|
||||||
|
|
||||||
|
Uses ReportLab Platypus for layout and embeds pre-rendered Mermaid diagram PNGs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from reportlab.lib import colors
|
||||||
|
from reportlab.lib.pagesizes import A4
|
||||||
|
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||||
|
from reportlab.lib.units import inch, mm
|
||||||
|
from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_JUSTIFY, TA_RIGHT
|
||||||
|
from reportlab.platypus import (
|
||||||
|
BaseDocTemplate, PageTemplate, Frame, NextPageTemplate,
|
||||||
|
Paragraph, Spacer, Table, TableStyle, Image, PageBreak,
|
||||||
|
KeepTogether,
|
||||||
|
)
|
||||||
|
from reportlab.platypus.tableofcontents import TableOfContents
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Paths
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||||
|
PROJECT_ROOT = SCRIPT_DIR.parent.parent
|
||||||
|
LOGO_PATH = PROJECT_ROOT / "UI_guidance" / "Barclays-Modcomms.png"
|
||||||
|
DIAGRAMS_DIR = SCRIPT_DIR / "diagrams" / "rendered"
|
||||||
|
OUTPUT_PDF = SCRIPT_DIR / "ModComms_Technical_Architecture.pdf"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Design Tokens (matching existing presentation)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
DARK_NAVY = colors.HexColor("#1A2142")
|
||||||
|
ACTIVE_BLUE = colors.HexColor("#006DE3")
|
||||||
|
LIME = colors.HexColor("#C3FB5A")
|
||||||
|
TEAL = colors.HexColor("#01A1A2")
|
||||||
|
ELECTRIC_VIOLET = colors.HexColor("#7A0FF9")
|
||||||
|
WHITE = colors.HexColor("#FFFFFF")
|
||||||
|
LIGHT_GREY = colors.HexColor("#F6F6F6")
|
||||||
|
MID_GREY = colors.HexColor("#E2E2E2")
|
||||||
|
DARK_GREY = colors.HexColor("#8E8E8E")
|
||||||
|
BLACK_TITLE = colors.HexColor("#272727")
|
||||||
|
|
||||||
|
RAG_GREEN = colors.HexColor("#09821F")
|
||||||
|
RAG_AMBER = colors.HexColor("#FFBA00")
|
||||||
|
RAG_RED = colors.HexColor("#E3000F")
|
||||||
|
|
||||||
|
PAGE_W, PAGE_H = A4
|
||||||
|
MARGIN = 0.75 * inch
|
||||||
|
CONTENT_W = PAGE_W - 2 * MARGIN
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Styles
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
_base = getSampleStyleSheet()
|
||||||
|
|
||||||
|
|
||||||
|
def _style(name, parent="Normal", **kw):
|
||||||
|
"""Create a named ParagraphStyle, merging with parent."""
|
||||||
|
p = _base[parent]
|
||||||
|
return ParagraphStyle(name, parent=p, **kw)
|
||||||
|
|
||||||
|
|
||||||
|
styles = {
|
||||||
|
"body": _style("Body", fontSize=10, leading=14, spaceAfter=6, alignment=TA_JUSTIFY,
|
||||||
|
fontName="Helvetica", textColor=BLACK_TITLE),
|
||||||
|
"h1": _style("H1", fontSize=22, leading=26, spaceAfter=12, spaceBefore=20,
|
||||||
|
fontName="Helvetica-Bold", textColor=DARK_NAVY),
|
||||||
|
"h2": _style("H2", fontSize=16, leading=20, spaceAfter=8, spaceBefore=14,
|
||||||
|
fontName="Helvetica-Bold", textColor=ACTIVE_BLUE),
|
||||||
|
"h3": _style("H3", fontSize=12, leading=16, spaceAfter=6, spaceBefore=10,
|
||||||
|
fontName="Helvetica-Bold", textColor=DARK_NAVY),
|
||||||
|
"bullet": _style("Bullet", fontSize=10, leading=14, spaceAfter=4,
|
||||||
|
fontName="Helvetica", textColor=BLACK_TITLE,
|
||||||
|
bulletIndent=12, leftIndent=24,
|
||||||
|
bulletFontName="Helvetica", bulletFontSize=10),
|
||||||
|
"code": _style("Code", fontSize=8.5, leading=11, spaceAfter=4,
|
||||||
|
fontName="Courier", textColor=BLACK_TITLE,
|
||||||
|
backColor=LIGHT_GREY, borderPadding=4),
|
||||||
|
"toc_h1": _style("TOC_H1", fontSize=14, leading=20, leftIndent=0,
|
||||||
|
fontName="Helvetica-Bold", textColor=DARK_NAVY),
|
||||||
|
"toc_h2": _style("TOC_H2", fontSize=11, leading=16, leftIndent=20,
|
||||||
|
fontName="Helvetica", textColor=BLACK_TITLE),
|
||||||
|
"cover_title": _style("CoverTitle", fontSize=32, leading=38, alignment=TA_LEFT,
|
||||||
|
fontName="Helvetica-Bold", textColor=WHITE),
|
||||||
|
"cover_subtitle": _style("CoverSubtitle", fontSize=16, leading=22, alignment=TA_LEFT,
|
||||||
|
fontName="Helvetica", textColor=colors.HexColor("#B0B8D0")),
|
||||||
|
"cover_meta": _style("CoverMeta", fontSize=11, leading=15, alignment=TA_LEFT,
|
||||||
|
fontName="Helvetica", textColor=LIME),
|
||||||
|
"table_header": _style("TableHeader", fontSize=9, leading=12, alignment=TA_LEFT,
|
||||||
|
fontName="Helvetica-Bold", textColor=WHITE),
|
||||||
|
"table_cell": _style("TableCell", fontSize=9, leading=12, alignment=TA_LEFT,
|
||||||
|
fontName="Helvetica", textColor=BLACK_TITLE),
|
||||||
|
"footer": _style("Footer", fontSize=8, leading=10, alignment=TA_RIGHT,
|
||||||
|
fontName="Helvetica", textColor=DARK_GREY),
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Heading with TOC entry
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TOCHeading(Paragraph):
|
||||||
|
"""A Paragraph that registers itself with the Table of Contents."""
|
||||||
|
|
||||||
|
def __init__(self, text, style, level=0, bookmarkName=None):
|
||||||
|
self._toc_text = text
|
||||||
|
self._toc_level = level
|
||||||
|
self._bookmark = bookmarkName or text.replace(" ", "_").replace("/", "_")
|
||||||
|
# Add bookmark anchor
|
||||||
|
tagged = f'<a name="{self._bookmark}"/>{text}'
|
||||||
|
super().__init__(tagged, style)
|
||||||
|
|
||||||
|
def draw(self):
|
||||||
|
super().draw()
|
||||||
|
key = self._bookmark
|
||||||
|
self.canv.bookmarkPage(key)
|
||||||
|
self.canv.addOutlineEntry(self._toc_text, key, self._toc_level)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Document Template with header/footer
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class ArchDocTemplate(BaseDocTemplate):
|
||||||
|
"""A4 document with header stripe and page number footer."""
|
||||||
|
|
||||||
|
def __init__(self, filename, **kw):
|
||||||
|
super().__init__(filename, pagesize=A4, **kw)
|
||||||
|
frame = Frame(MARGIN, MARGIN + 0.4 * inch, CONTENT_W, PAGE_H - 2 * MARGIN - 0.6 * inch,
|
||||||
|
id="normal")
|
||||||
|
cover_frame = Frame(0, 0, PAGE_W, PAGE_H, id="cover")
|
||||||
|
self.addPageTemplates([
|
||||||
|
PageTemplate(id="cover", frames=[cover_frame], onPage=self._cover_page),
|
||||||
|
PageTemplate(id="content", frames=[frame], onPage=self._content_page),
|
||||||
|
])
|
||||||
|
|
||||||
|
def afterFlowable(self, flowable):
|
||||||
|
"""Register TOC entries."""
|
||||||
|
if isinstance(flowable, TOCHeading):
|
||||||
|
level = flowable._toc_level
|
||||||
|
text = flowable._toc_text
|
||||||
|
key = flowable._bookmark
|
||||||
|
self.notify("TOCEntry", (level, text, self.page, key))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _cover_page(canvas, doc):
|
||||||
|
"""Draw cover page background."""
|
||||||
|
canvas.saveState()
|
||||||
|
canvas.setFillColor(DARK_NAVY)
|
||||||
|
canvas.rect(0, 0, PAGE_W, PAGE_H, fill=1, stroke=0)
|
||||||
|
# Accent stripe
|
||||||
|
canvas.setFillColor(ACTIVE_BLUE)
|
||||||
|
canvas.rect(0, PAGE_H - 8 * mm, PAGE_W, 8 * mm, fill=1, stroke=0)
|
||||||
|
# Bottom lime accent
|
||||||
|
canvas.setFillColor(LIME)
|
||||||
|
canvas.rect(0, 0, PAGE_W, 3 * mm, fill=1, stroke=0)
|
||||||
|
canvas.restoreState()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _content_page(canvas, doc):
|
||||||
|
"""Draw header and footer on content pages."""
|
||||||
|
canvas.saveState()
|
||||||
|
# Header stripe
|
||||||
|
canvas.setFillColor(DARK_NAVY)
|
||||||
|
canvas.rect(0, PAGE_H - 12 * mm, PAGE_W, 12 * mm, fill=1, stroke=0)
|
||||||
|
canvas.setFillColor(WHITE)
|
||||||
|
canvas.setFont("Helvetica-Bold", 8)
|
||||||
|
canvas.drawString(MARGIN, PAGE_H - 9 * mm, "ModComms Technical Architecture")
|
||||||
|
canvas.setFillColor(ACTIVE_BLUE)
|
||||||
|
canvas.rect(0, PAGE_H - 12.8 * mm, PAGE_W, 0.8 * mm, fill=1, stroke=0)
|
||||||
|
|
||||||
|
# Footer
|
||||||
|
canvas.setFillColor(MID_GREY)
|
||||||
|
canvas.rect(0, 0, PAGE_W, 10 * mm, fill=1, stroke=0)
|
||||||
|
canvas.setFillColor(DARK_GREY)
|
||||||
|
canvas.setFont("Helvetica", 7)
|
||||||
|
canvas.drawString(MARGIN, 4 * mm, "Barclays Internal - Confidential")
|
||||||
|
canvas.drawRightString(PAGE_W - MARGIN, 4 * mm, f"Page {doc.page}")
|
||||||
|
canvas.restoreState()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Helper functions
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def h1(text):
|
||||||
|
return TOCHeading(text, styles["h1"], level=0)
|
||||||
|
|
||||||
|
|
||||||
|
def h2(text):
|
||||||
|
return TOCHeading(text, styles["h2"], level=1)
|
||||||
|
|
||||||
|
|
||||||
|
def h3(text):
|
||||||
|
return TOCHeading(text, styles["h3"], level=1)
|
||||||
|
|
||||||
|
|
||||||
|
def p(text):
|
||||||
|
return Paragraph(text, styles["body"])
|
||||||
|
|
||||||
|
|
||||||
|
def bullet(text):
|
||||||
|
return Paragraph(f"<bullet>•</bullet> {text}", styles["bullet"])
|
||||||
|
|
||||||
|
|
||||||
|
def code(text):
|
||||||
|
return Paragraph(text.replace("\n", "<br/>"), styles["code"])
|
||||||
|
|
||||||
|
|
||||||
|
def spacer(h=0.15):
|
||||||
|
return Spacer(1, h * inch)
|
||||||
|
|
||||||
|
|
||||||
|
def diagram(filename, caption=None, max_width=None):
|
||||||
|
"""Embed a rendered diagram PNG, scaled to fit content width."""
|
||||||
|
from PIL import Image as PILImage
|
||||||
|
|
||||||
|
path = DIAGRAMS_DIR / filename
|
||||||
|
if not path.exists():
|
||||||
|
return [p(f"<i>[Diagram not found: {filename}]</i>")]
|
||||||
|
|
||||||
|
# Get actual image dimensions
|
||||||
|
with PILImage.open(str(path)) as pil_img:
|
||||||
|
img_w, img_h = pil_img.size
|
||||||
|
|
||||||
|
# Scale to fit content width
|
||||||
|
target_w = max_width or CONTENT_W
|
||||||
|
scale = target_w / img_w
|
||||||
|
target_h = img_h * scale
|
||||||
|
|
||||||
|
# Cap height at 6 inches
|
||||||
|
if target_h > 6 * inch:
|
||||||
|
target_h = 6 * inch
|
||||||
|
target_w = img_w * (target_h / img_h)
|
||||||
|
|
||||||
|
img = Image(str(path), width=target_w, height=target_h)
|
||||||
|
elements = [spacer(0.1), img, spacer(0.05)]
|
||||||
|
if caption:
|
||||||
|
cap_style = _style("Caption", fontSize=8, leading=10, alignment=TA_CENTER,
|
||||||
|
fontName="Helvetica-Oblique", textColor=DARK_GREY, spaceAfter=8)
|
||||||
|
elements.append(Paragraph(caption, cap_style))
|
||||||
|
return elements
|
||||||
|
|
||||||
|
|
||||||
|
def make_table(headers, rows, col_widths=None):
|
||||||
|
"""Create a styled table with header row."""
|
||||||
|
header_paras = [Paragraph(h, styles["table_header"]) for h in headers]
|
||||||
|
data = [header_paras]
|
||||||
|
for row in rows:
|
||||||
|
data.append([Paragraph(str(c), styles["table_cell"]) for c in row])
|
||||||
|
|
||||||
|
if col_widths is None:
|
||||||
|
col_widths = [CONTENT_W / len(headers)] * len(headers)
|
||||||
|
|
||||||
|
t = Table(data, colWidths=col_widths, repeatRows=1)
|
||||||
|
t.setStyle(TableStyle([
|
||||||
|
("BACKGROUND", (0, 0), (-1, 0), DARK_NAVY),
|
||||||
|
("TEXTCOLOR", (0, 0), (-1, 0), WHITE),
|
||||||
|
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
|
||||||
|
("FONTSIZE", (0, 0), (-1, 0), 9),
|
||||||
|
("BOTTOMPADDING", (0, 0), (-1, 0), 6),
|
||||||
|
("TOPPADDING", (0, 0), (-1, 0), 6),
|
||||||
|
("BACKGROUND", (0, 1), (-1, -1), WHITE),
|
||||||
|
("ROWBACKGROUNDS", (0, 1), (-1, -1), [WHITE, LIGHT_GREY]),
|
||||||
|
("FONTNAME", (0, 1), (-1, -1), "Helvetica"),
|
||||||
|
("FONTSIZE", (0, 1), (-1, -1), 9),
|
||||||
|
("TOPPADDING", (0, 1), (-1, -1), 4),
|
||||||
|
("BOTTOMPADDING", (0, 1), (-1, -1), 4),
|
||||||
|
("LEFTPADDING", (0, 0), (-1, -1), 6),
|
||||||
|
("RIGHTPADDING", (0, 0), (-1, -1), 6),
|
||||||
|
("GRID", (0, 0), (-1, -1), 0.5, MID_GREY),
|
||||||
|
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
||||||
|
]))
|
||||||
|
return t
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Document Sections
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def build_cover():
|
||||||
|
"""Cover page elements."""
|
||||||
|
elements = []
|
||||||
|
# Push content down
|
||||||
|
elements.append(Spacer(1, 2.2 * inch))
|
||||||
|
|
||||||
|
# Logo
|
||||||
|
if LOGO_PATH.exists():
|
||||||
|
from PIL import Image as PILImage
|
||||||
|
with PILImage.open(str(LOGO_PATH)) as pil_img:
|
||||||
|
lw, lh = pil_img.size
|
||||||
|
logo_w = 2.8 * inch
|
||||||
|
logo_h = lh * (logo_w / lw)
|
||||||
|
logo = Image(str(LOGO_PATH), width=logo_w, height=logo_h)
|
||||||
|
elements.append(logo)
|
||||||
|
elements.append(Spacer(1, 0.5 * inch))
|
||||||
|
|
||||||
|
elements.append(Paragraph("Technical Architecture", styles["cover_title"]))
|
||||||
|
elements.append(Spacer(1, 0.15 * inch))
|
||||||
|
elements.append(Paragraph("Document", styles["cover_title"]))
|
||||||
|
elements.append(Spacer(1, 0.4 * inch))
|
||||||
|
elements.append(Paragraph(
|
||||||
|
"Complete system design reference for the AI-powered proof review platform",
|
||||||
|
styles["cover_subtitle"]))
|
||||||
|
elements.append(Spacer(1, 0.6 * inch))
|
||||||
|
elements.append(Paragraph(f"Version 1.0 | {date.today().strftime('%B %Y')} | Internal",
|
||||||
|
styles["cover_meta"]))
|
||||||
|
|
||||||
|
# Switch to content template for next page
|
||||||
|
elements.append(NextPageTemplate("content"))
|
||||||
|
elements.append(PageBreak())
|
||||||
|
return elements
|
||||||
|
|
||||||
|
|
||||||
|
def build_toc():
|
||||||
|
"""Table of Contents page."""
|
||||||
|
elements = []
|
||||||
|
elements.append(TOCHeading("Table of Contents", styles["h1"], level=0))
|
||||||
|
elements.append(spacer(0.2))
|
||||||
|
toc = TableOfContents()
|
||||||
|
toc.levelStyles = [styles["toc_h1"], styles["toc_h2"]]
|
||||||
|
elements.append(toc)
|
||||||
|
elements.append(PageBreak())
|
||||||
|
return elements
|
||||||
|
|
||||||
|
|
||||||
|
def build_executive_summary():
|
||||||
|
elements = []
|
||||||
|
elements.append(h1("Executive Summary"))
|
||||||
|
elements.append(p(
|
||||||
|
"<b>ModComms</b> is an AI-powered proof review tool built for Barclays marketing operations. "
|
||||||
|
"It automates the compliance, brand, tone-of-voice, and channel-suitability review of marketing "
|
||||||
|
"assets (proofs) that would traditionally require manual review by multiple specialist teams."
|
||||||
|
))
|
||||||
|
elements.append(p(
|
||||||
|
"The platform employs a <b>multi-agent AI architecture</b> where four specialist agents "
|
||||||
|
"analyse each proof in parallel, with a lead agent synthesising their findings into a single "
|
||||||
|
"RAG (Red/Amber/Green) status decision. This enables near-instant feedback on uploaded proofs, "
|
||||||
|
"dramatically reducing review cycle times."
|
||||||
|
))
|
||||||
|
elements.append(h2("Key Technology Choices"))
|
||||||
|
elements.append(bullet("<b>Frontend:</b> React 18 SPA with TypeScript, Vite build tool, Tailwind CSS"))
|
||||||
|
elements.append(bullet("<b>Backend:</b> Python FastAPI with async/await, WebSocket real-time communication"))
|
||||||
|
elements.append(bullet("<b>AI Engine:</b> Google Gemini 2.5 Flash for multi-modal image analysis"))
|
||||||
|
elements.append(bullet("<b>Database:</b> PostgreSQL with SQLAlchemy async ORM and Alembic migrations"))
|
||||||
|
elements.append(bullet("<b>Authentication:</b> Azure AD via MSAL with 4-tier RBAC"))
|
||||||
|
elements.append(bullet("<b>Knowledge Base:</b> LlamaParse document parsing + Gemini distillation pipeline"))
|
||||||
|
elements.append(PageBreak())
|
||||||
|
return elements
|
||||||
|
|
||||||
|
|
||||||
|
def build_system_architecture():
|
||||||
|
elements = []
|
||||||
|
elements.append(h1("System Architecture Overview"))
|
||||||
|
elements.append(p(
|
||||||
|
"ModComms follows a <b>three-tier architecture</b> with a React single-page application frontend, "
|
||||||
|
"a Python FastAPI backend, and PostgreSQL for persistence. The backend communicates with external "
|
||||||
|
"services including Google Gemini for AI analysis, Azure AD for authentication, and LlamaParse for "
|
||||||
|
"document processing."
|
||||||
|
))
|
||||||
|
elements.extend(diagram("01_system_overview.png", "Figure 1: High-Level System Architecture"))
|
||||||
|
|
||||||
|
elements.append(h2("Technology Stack"))
|
||||||
|
elements.append(make_table(
|
||||||
|
["Tier", "Technology", "Purpose"],
|
||||||
|
[
|
||||||
|
["Frontend", "React 18 + TypeScript", "Single-page application UI"],
|
||||||
|
["Frontend", "Vite 5", "Build tool and dev server"],
|
||||||
|
["Frontend", "Tailwind CSS", "Utility-first styling"],
|
||||||
|
["Frontend", "@azure/msal-react", "Azure AD authentication"],
|
||||||
|
["Backend", "Python 3.11+ / FastAPI", "Async REST API + WebSocket server"],
|
||||||
|
["Backend", "SQLAlchemy 2.0 (async)", "ORM with asyncpg driver"],
|
||||||
|
["Backend", "Alembic", "Database migration management"],
|
||||||
|
["Backend", "Uvicorn", "ASGI server"],
|
||||||
|
["AI", "Google Gemini 2.5 Flash", "Multi-modal proof analysis"],
|
||||||
|
["AI", "LlamaParse", "PDF/DOCX document parsing"],
|
||||||
|
["Database", "PostgreSQL 15", "Primary data store"],
|
||||||
|
["Auth", "Azure AD / MSAL", "OAuth2 + JWT identity provider"],
|
||||||
|
["Deployment", "Docker Compose", "Container orchestration"],
|
||||||
|
["Deployment", "Apache2 + mod_proxy", "Reverse proxy and static serving"],
|
||||||
|
],
|
||||||
|
col_widths=[1.1 * inch, 2.0 * inch, CONTENT_W - 3.1 * inch],
|
||||||
|
))
|
||||||
|
elements.append(PageBreak())
|
||||||
|
return elements
|
||||||
|
|
||||||
|
|
||||||
|
def build_agent_pipeline():
|
||||||
|
elements = []
|
||||||
|
elements.append(h1("Multi-Agent Analysis Pipeline"))
|
||||||
|
elements.append(p(
|
||||||
|
"The core of ModComms is a multi-agent system where four specialist agents analyse each proof "
|
||||||
|
"in parallel. Each agent has a distinct area of expertise and access to a curated knowledge base "
|
||||||
|
"of reference documents. After all agents complete, a Lead Agent synthesises their findings."
|
||||||
|
))
|
||||||
|
|
||||||
|
elements.append(h2("Specialist Agents"))
|
||||||
|
elements.append(make_table(
|
||||||
|
["Agent", "Focus Area", "Key Checks"],
|
||||||
|
[
|
||||||
|
["Legal Agent", "Advertising standards compliance",
|
||||||
|
"Financial promotion detection, required disclaimers, FCA/ASA rules, risk language"],
|
||||||
|
["Brand Agent", "Brand identity adherence",
|
||||||
|
"Logo usage, colour palette, typography, design language principles (Barclays or Barclaycard)"],
|
||||||
|
["Channel Best Practices Agent", "Platform-specific guidelines",
|
||||||
|
"Content best practices, accessibility, readability, platform conventions"],
|
||||||
|
["Channel Tech Specs Agent", "Technical specifications",
|
||||||
|
"Dimensions, file size limits, format requirements, resolution, safe zones"],
|
||||||
|
],
|
||||||
|
col_widths=[1.6 * inch, 1.6 * inch, CONTENT_W - 3.2 * inch],
|
||||||
|
))
|
||||||
|
elements.append(spacer(0.15))
|
||||||
|
|
||||||
|
elements.append(h2("Lead Agent RAG Decision Logic"))
|
||||||
|
elements.append(p(
|
||||||
|
"The Lead Agent synthesises all four sub-reviews into an overall RAG status and a human-readable summary:"))
|
||||||
|
elements.append(bullet(
|
||||||
|
'<font color="#09821F"><b>GREEN (Passed):</b></font> All agents pass with at most 1 amber-level issue per agent, '
|
||||||
|
"and no Legal agent amber issues."))
|
||||||
|
elements.append(bullet(
|
||||||
|
'<font color="#FFBA00"><b>AMBER (Requires Manual Legal Review):</b></font> More than 1 actionable issue per agent, '
|
||||||
|
"or any Legal agent amber-level issue."))
|
||||||
|
elements.append(bullet(
|
||||||
|
'<font color="#E3000F"><b>RED (Failed):</b></font> Any agent returns a Red status, indicating a critical compliance '
|
||||||
|
"or brand violation that must be resolved."))
|
||||||
|
elements.append(spacer(0.1))
|
||||||
|
|
||||||
|
elements.append(h2("Revision-Aware Analysis"))
|
||||||
|
elements.append(p(
|
||||||
|
"When a proof has been previously analysed (version N > 1), the system automatically fetches the "
|
||||||
|
"prior version's analysis results and passes them as context to each agent. This enables agents to "
|
||||||
|
"identify <b>resolved issues</b>, <b>outstanding issues</b>, and <b>new issues</b> relative to "
|
||||||
|
"the previous version, providing actionable delta feedback."
|
||||||
|
))
|
||||||
|
elements.extend(diagram("02_agent_pipeline.png", "Figure 2: Multi-Agent Analysis Pipeline"))
|
||||||
|
elements.append(PageBreak())
|
||||||
|
return elements
|
||||||
|
|
||||||
|
|
||||||
|
def build_websocket_flow():
|
||||||
|
elements = []
|
||||||
|
elements.append(h1("WebSocket Analysis Flow"))
|
||||||
|
elements.append(p(
|
||||||
|
"Proof analysis uses a WebSocket connection for real-time streaming of agent progress. "
|
||||||
|
"The client sends a single <font name='Courier'>analyze</font> message containing the base64-encoded "
|
||||||
|
"file and metadata, and receives a stream of updates as each agent starts and completes."
|
||||||
|
))
|
||||||
|
|
||||||
|
elements.append(h2("Message Protocol"))
|
||||||
|
elements.append(make_table(
|
||||||
|
["Direction", "Type", "Payload", "Description"],
|
||||||
|
[
|
||||||
|
["Client → Server", "analyze",
|
||||||
|
"file_data, file_type, access_token, brand, campaign_id, proof_name, channel, sub_channel, proof_type",
|
||||||
|
"Initiate analysis with file and metadata"],
|
||||||
|
["Server → Client", "agent_started",
|
||||||
|
"agent_name",
|
||||||
|
"Agent has begun processing"],
|
||||||
|
["Server → Client", "agent_completed",
|
||||||
|
"agent_name, review (ragStatus, feedback, issues, resolvedIssues, outstandingIssues, newIssues)",
|
||||||
|
"Agent finished with results"],
|
||||||
|
["Server → Client", "complete",
|
||||||
|
"result (all agent reviews + lead summary + overall status), proof_id, version_id, is_identical_file, pdf_pages",
|
||||||
|
"Full analysis complete and persisted"],
|
||||||
|
["Server → Client", "error",
|
||||||
|
"message",
|
||||||
|
"Error occurred during processing"],
|
||||||
|
],
|
||||||
|
col_widths=[1.0 * inch, 1.0 * inch, 2.2 * inch, CONTENT_W - 4.2 * inch],
|
||||||
|
))
|
||||||
|
elements.append(spacer(0.15))
|
||||||
|
|
||||||
|
elements.append(h2("Flow Lifecycle"))
|
||||||
|
elements.append(bullet("1. Client establishes WebSocket connection to <font name='Courier'>/ws/analyze</font>"))
|
||||||
|
elements.append(bullet("2. Client sends <font name='Courier'>analyze</font> message with JWT access token"))
|
||||||
|
elements.append(bullet("3. Server verifies JWT token against Azure AD"))
|
||||||
|
elements.append(bullet("4. Server checks user role (oversight_admin blocked from analysis)"))
|
||||||
|
elements.append(bullet("5. Server decodes base64 file data; rasterizes PDF pages if applicable"))
|
||||||
|
elements.append(bullet("6. Server fetches previous version analysis for revision context"))
|
||||||
|
elements.append(bullet("7. Four agents run in parallel via <font name='Courier'>asyncio.gather()</font>"))
|
||||||
|
elements.append(bullet("8. Real-time <font name='Courier'>agent_started</font> / <font name='Courier'>agent_completed</font> messages stream to client"))
|
||||||
|
elements.append(bullet("9. Lead Agent synthesises overall RAG status"))
|
||||||
|
elements.append(bullet("10. Results persisted to database; file stored to disk"))
|
||||||
|
elements.append(bullet("11. <font name='Courier'>complete</font> message sent with full results and IDs"))
|
||||||
|
elements.extend(diagram("03_websocket_flow.png", "Figure 3: WebSocket Analysis Sequence"))
|
||||||
|
elements.append(PageBreak())
|
||||||
|
return elements
|
||||||
|
|
||||||
|
|
||||||
|
def build_database_schema():
|
||||||
|
elements = []
|
||||||
|
elements.append(h1("Database Schema"))
|
||||||
|
elements.append(p(
|
||||||
|
"ModComms uses PostgreSQL with SQLAlchemy 2.0 async ORM. The schema comprises 15 tables "
|
||||||
|
"organised into four logical domains. All primary keys are UUIDs. Alembic manages migrations."
|
||||||
|
))
|
||||||
|
|
||||||
|
elements.append(h2("Domain Overview"))
|
||||||
|
elements.append(make_table(
|
||||||
|
["Domain", "Tables", "Purpose"],
|
||||||
|
[
|
||||||
|
["Identity & Access", "agencies, users, user_change_logs",
|
||||||
|
"User accounts, agency membership, role audit trail"],
|
||||||
|
["Campaign & Proof", "campaigns, proofs, proof_versions",
|
||||||
|
"Marketing campaigns, proof assets, versioned analysis results"],
|
||||||
|
["Audit & Review", "flagged_items, resolved_items, error_items",
|
||||||
|
"Manual flagging, issue resolution tracking, analysis error records"],
|
||||||
|
["Knowledge Base", "knowledge_bases, source_documents, spec_versions, processing_jobs",
|
||||||
|
"Agent reference documentation, document processing pipeline"],
|
||||||
|
["Configuration", "dropdown_options",
|
||||||
|
"Hierarchical channel/sub-channel/proof-type configuration"],
|
||||||
|
],
|
||||||
|
col_widths=[1.3 * inch, 2.3 * inch, CONTENT_W - 3.6 * inch],
|
||||||
|
))
|
||||||
|
elements.append(spacer(0.1))
|
||||||
|
|
||||||
|
elements.append(h2("Key Design Decisions"))
|
||||||
|
elements.append(bullet(
|
||||||
|
"<b>JSONB for agent_review:</b> The <font name='Courier'>proof_versions.agent_review</font> column "
|
||||||
|
"stores the complete multi-agent analysis result as a JSONB document, enabling flexible querying "
|
||||||
|
"while keeping the schema stable as agent output evolves."
|
||||||
|
))
|
||||||
|
elements.append(bullet(
|
||||||
|
"<b>JSONB for processing logs:</b> <font name='Courier'>processing_jobs.log</font> stores "
|
||||||
|
"step-by-step pipeline progress as structured JSON for debugging."
|
||||||
|
))
|
||||||
|
elements.append(bullet(
|
||||||
|
"<b>Self-referential hierarchy:</b> <font name='Courier'>dropdown_options</font> uses a "
|
||||||
|
"<font name='Courier'>parent_id</font> FK to itself, supporting the Channel → Sub-Channel "
|
||||||
|
"→ Proof Type hierarchy in a single table."
|
||||||
|
))
|
||||||
|
elements.append(bullet(
|
||||||
|
"<b>File hash deduplication:</b> <font name='Courier'>proof_versions.file_hash</font> (MD5) "
|
||||||
|
"detects when an identical file is re-uploaded, flagging it to the user."
|
||||||
|
))
|
||||||
|
elements.extend(diagram("04_database_erd.png", "Figure 4: Entity Relationship Diagram"))
|
||||||
|
elements.append(PageBreak())
|
||||||
|
return elements
|
||||||
|
|
||||||
|
|
||||||
|
def build_frontend():
|
||||||
|
elements = []
|
||||||
|
elements.append(h1("Frontend Architecture"))
|
||||||
|
elements.append(p(
|
||||||
|
"The frontend is a React 18 single-page application built with TypeScript and Vite. "
|
||||||
|
"It uses Tailwind CSS for styling with a custom Barclays design system. The application "
|
||||||
|
"is wrapped in an MSAL authentication provider and a custom UserContext for role-based rendering."
|
||||||
|
))
|
||||||
|
|
||||||
|
elements.append(h2("Component Hierarchy"))
|
||||||
|
elements.append(p(
|
||||||
|
"The entry point (<font name='Courier'>index.tsx</font>) wraps the app in <font name='Courier'>MsalProvider</font>. "
|
||||||
|
"<font name='Courier'>App.tsx</font> acts as an authentication gate, rendering <font name='Courier'>Login</font> "
|
||||||
|
"for unauthenticated users and <font name='Courier'>AppContent</font> (inside <font name='Courier'>UserProvider</font>) "
|
||||||
|
"for authenticated users. Views are rendered based on a <font name='Courier'>currentView</font> state variable."
|
||||||
|
))
|
||||||
|
elements.extend(diagram("05_frontend_hierarchy.png", "Figure 5: Frontend Component Hierarchy"))
|
||||||
|
elements.append(spacer(0.1))
|
||||||
|
|
||||||
|
elements.append(h2("Views"))
|
||||||
|
elements.append(make_table(
|
||||||
|
["View", "Component", "Description"],
|
||||||
|
[
|
||||||
|
["Home", "Hero + ChecksOverview + FeedbackReport", "Upload proof, view real-time analysis, export report"],
|
||||||
|
["Campaigns", "Campaigns", "Campaign CRUD, proof list, version history, re-analysis"],
|
||||||
|
["Analytics", "Analytics", "Aggregate statistics, RAG distributions, per-agency breakdowns"],
|
||||||
|
["Auditing", "Auditing", "Flagged items, resolved items, error items with filters"],
|
||||||
|
["WIP Reviewer", "WIPReviewer", "Quick analysis without persisting to a campaign"],
|
||||||
|
["Knowledge Base", "KnowledgeBase", "Manage agent reference docs, trigger processing, view specs"],
|
||||||
|
["Settings", "Settings", "Dropdown options (channels, sub-channels, proof types)"],
|
||||||
|
["User Management", "UserManagement", "Role assignment, agency assignment, change history"],
|
||||||
|
["Profile", "Profile", "Current user info and preferences"],
|
||||||
|
["CopyGenAI", "CopyGenAI", "AI-assisted marketing copy generation"],
|
||||||
|
],
|
||||||
|
col_widths=[1.2 * inch, 2.0 * inch, CONTENT_W - 3.2 * inch],
|
||||||
|
))
|
||||||
|
elements.append(spacer(0.1))
|
||||||
|
|
||||||
|
elements.append(h2("State Management"))
|
||||||
|
elements.append(bullet(
|
||||||
|
"<b>UserContext:</b> Provides authenticated user info, role checks (<font name='Courier'>canWrite</font>, "
|
||||||
|
"<font name='Courier'>canSeeAnalytics</font>, etc.), and agency filtering state."))
|
||||||
|
elements.append(bullet(
|
||||||
|
"<b>URL State:</b> Campaign and proof selections are encoded in the URL hash for deep linking "
|
||||||
|
"and browser history support."))
|
||||||
|
elements.append(bullet(
|
||||||
|
"<b>API Service:</b> Centralized REST client (<font name='Courier'>apiService.ts</font>) that "
|
||||||
|
"auto-attaches MSAL access tokens to all requests."))
|
||||||
|
elements.append(bullet(
|
||||||
|
"<b>WebSocket Service:</b> <font name='Courier'>geminiService.ts</font> manages the WebSocket "
|
||||||
|
"lifecycle for proof analysis, including reconnection and progress callbacks."))
|
||||||
|
elements.append(PageBreak())
|
||||||
|
return elements
|
||||||
|
|
||||||
|
|
||||||
|
def build_auth_rbac():
|
||||||
|
elements = []
|
||||||
|
elements.append(h1("Authentication & RBAC"))
|
||||||
|
elements.append(p(
|
||||||
|
"ModComms uses Azure Active Directory for authentication via the MSAL (Microsoft Authentication Library) "
|
||||||
|
"protocol. The frontend acquires tokens via MSAL.js popup flow, and the backend verifies JWT tokens "
|
||||||
|
"using Azure AD's JWKS endpoint."
|
||||||
|
))
|
||||||
|
|
||||||
|
elements.append(h2("Authentication Flow"))
|
||||||
|
elements.append(bullet("1. User navigates to app; MSAL checks for existing session"))
|
||||||
|
elements.append(bullet("2. If not authenticated, Login component triggers <font name='Courier'>loginPopup()</font>"))
|
||||||
|
elements.append(bullet("3. Azure AD returns ID token + access token"))
|
||||||
|
elements.append(bullet("4. Frontend calls <font name='Courier'>GET /api/me</font> with Bearer token"))
|
||||||
|
elements.append(bullet("5. Backend verifies JWT, extracts claims (oid, name, email)"))
|
||||||
|
elements.append(bullet("6. Backend auto-provisions user on first login as <font name='Courier'>basic_user</font>"))
|
||||||
|
elements.append(bullet("7. UserContext stores profile and computes role-based feature flags"))
|
||||||
|
elements.extend(diagram("06_auth_rbac_flow.png", "Figure 6: Authentication & RBAC Flow"))
|
||||||
|
elements.append(spacer(0.1))
|
||||||
|
|
||||||
|
elements.append(h2("Role Hierarchy"))
|
||||||
|
elements.append(make_table(
|
||||||
|
["Role", "Scope", "Key Permissions"],
|
||||||
|
[
|
||||||
|
["super_admin", "Global",
|
||||||
|
"All features, all campaigns, user management, settings, knowledge base, analytics, auditing"],
|
||||||
|
["oversight_admin", "Global (read-only)",
|
||||||
|
"View all campaigns across agencies, analytics, auditing. Cannot upload, analyse, flag, or resolve."],
|
||||||
|
["agency_admin", "Own agency",
|
||||||
|
"Full CRUD within own agency's campaigns, flagging, resolving"],
|
||||||
|
["basic_user", "Own agency",
|
||||||
|
"Upload and analyse proofs, view own agency's campaigns, flag issues"],
|
||||||
|
],
|
||||||
|
col_widths=[1.3 * inch, 1.0 * inch, CONTENT_W - 2.3 * inch],
|
||||||
|
))
|
||||||
|
elements.append(spacer(0.1))
|
||||||
|
|
||||||
|
elements.append(h2("Backend Enforcement"))
|
||||||
|
elements.append(p(
|
||||||
|
"Role-based access is enforced at the FastAPI dependency level using composable dependencies:"))
|
||||||
|
elements.append(bullet(
|
||||||
|
"<font name='Courier'>get_current_user()</font> — Verifies JWT and returns claims dict"))
|
||||||
|
elements.append(bullet(
|
||||||
|
"<font name='Courier'>get_current_db_user()</font> — Resolves claims to a User ORM object with agency"))
|
||||||
|
elements.append(bullet(
|
||||||
|
"<font name='Courier'>require_role(*roles)</font> — Factory that restricts endpoints to specific roles"))
|
||||||
|
elements.append(bullet(
|
||||||
|
"<font name='Courier'>require_write_access()</font> — Blocks oversight_admin from mutation operations"))
|
||||||
|
elements.append(PageBreak())
|
||||||
|
return elements
|
||||||
|
|
||||||
|
|
||||||
|
def build_knowledge_base():
|
||||||
|
elements = []
|
||||||
|
elements.append(h1("Knowledge Base Pipeline"))
|
||||||
|
elements.append(p(
|
||||||
|
"The Knowledge Base system allows admins to upload reference documents (brand guidelines, "
|
||||||
|
"legal standards, channel specifications) that are parsed, combined, and distilled into "
|
||||||
|
"concise agent specifications. These specs are versioned and serve as the primary context "
|
||||||
|
"for each agent during proof analysis."
|
||||||
|
))
|
||||||
|
|
||||||
|
elements.append(h2("Knowledge Base Types"))
|
||||||
|
elements.append(make_table(
|
||||||
|
["Agent Key", "Display Name", "Description"],
|
||||||
|
[
|
||||||
|
["legal", "Legal Compliance", "Advertising standards, FCA rules, disclaimer requirements"],
|
||||||
|
["brand_barclays", "Barclays Brand", "Barclays brand identity guidelines, design language"],
|
||||||
|
["brand_barclaycard", "Barclaycard Brand", "Barclaycard-specific brand guidelines"],
|
||||||
|
["channel_best_practices", "Channel Best Practices", "Platform content guidelines, accessibility standards"],
|
||||||
|
["channel_tech_specs", "Channel Tech Specs", "Technical dimensions, file formats, resolution specs"],
|
||||||
|
],
|
||||||
|
col_widths=[1.6 * inch, 1.5 * inch, CONTENT_W - 3.1 * inch],
|
||||||
|
))
|
||||||
|
elements.append(spacer(0.1))
|
||||||
|
|
||||||
|
elements.append(h2("Processing Pipeline"))
|
||||||
|
elements.append(bullet("1. <b>Upload:</b> Admin uploads source documents (PDF, DOCX, etc.) via the KB UI"))
|
||||||
|
elements.append(bullet("2. <b>Store:</b> Files saved to <font name='Courier'>kb_storage/</font> with metadata in DB"))
|
||||||
|
elements.append(bullet("3. <b>Parse:</b> LlamaParse API converts documents to clean Markdown"))
|
||||||
|
elements.append(bullet("4. <b>Combine:</b> All parsed Markdown from source documents is concatenated"))
|
||||||
|
elements.append(bullet("5. <b>Distil:</b> Gemini generates a concise, structured agent spec from the combined text"))
|
||||||
|
elements.append(bullet("6. <b>Version:</b> New spec version created with version number, character count, and source doc links"))
|
||||||
|
elements.append(bullet("7. <b>Activate:</b> New version set as active; reference docs cache invalidated"))
|
||||||
|
elements.append(bullet("8. <b>Serve:</b> Agents load the active spec version at analysis time"))
|
||||||
|
|
||||||
|
elements.append(spacer(0.1))
|
||||||
|
elements.append(h2("Version Management"))
|
||||||
|
elements.append(p(
|
||||||
|
"Spec versions are immutable once created. Admins can view the full content of any version, "
|
||||||
|
"compare two versions with a unified diff view, and revert to a previous version by activating it. "
|
||||||
|
"Processing jobs track the full pipeline lifecycle with status progression: "
|
||||||
|
"<font name='Courier'>pending → parsing → distilling → completed</font> (or <font name='Courier'>failed</font>)."
|
||||||
|
))
|
||||||
|
elements.extend(diagram("07_knowledge_base_pipeline.png", "Figure 7: Knowledge Base Processing Pipeline"))
|
||||||
|
elements.append(PageBreak())
|
||||||
|
return elements
|
||||||
|
|
||||||
|
|
||||||
|
def build_deployment():
|
||||||
|
elements = []
|
||||||
|
elements.append(h1("Deployment Architecture"))
|
||||||
|
elements.append(p(
|
||||||
|
"ModComms is deployed using Docker Compose for the backend services and Apache as a "
|
||||||
|
"reverse proxy serving the static frontend build. The deployment script (<font name='Courier'>deploy.sh</font>) "
|
||||||
|
"automates the full build and deployment process."
|
||||||
|
))
|
||||||
|
|
||||||
|
elements.append(h2("Infrastructure Components"))
|
||||||
|
elements.append(make_table(
|
||||||
|
["Component", "Technology", "Configuration"],
|
||||||
|
[
|
||||||
|
["Reverse Proxy", "Apache2 + mod_proxy + mod_proxy_wstunnel",
|
||||||
|
"HTTPS termination, static file serving, proxy to backend (port 8000)"],
|
||||||
|
["Backend", "Docker container (Python + Uvicorn)",
|
||||||
|
"Port 8000, auto-restart, volume mounts for uploads and KB storage"],
|
||||||
|
["Database", "Docker container (PostgreSQL 15)",
|
||||||
|
"Port 5432, persistent volume for data, health checks"],
|
||||||
|
["Frontend", "Static files (Apache DocumentRoot)",
|
||||||
|
"Vite production build served directly by Apache"],
|
||||||
|
["File Storage", "Host filesystem volumes",
|
||||||
|
"uploads/ for proof files, kb_storage/ for knowledge base documents"],
|
||||||
|
],
|
||||||
|
col_widths=[1.2 * inch, 2.0 * inch, CONTENT_W - 3.2 * inch],
|
||||||
|
))
|
||||||
|
elements.append(spacer(0.1))
|
||||||
|
|
||||||
|
elements.append(h2("Deployment Process"))
|
||||||
|
elements.append(bullet("1. <b>Pull:</b> Fetch latest code from Git repository"))
|
||||||
|
elements.append(bullet("2. <b>Frontend build:</b> <font name='Courier'>npm run build</font> produces static assets"))
|
||||||
|
elements.append(bullet("3. <b>Deploy frontend:</b> Copy build output to Apache DocumentRoot"))
|
||||||
|
elements.append(bullet("4. <b>Backend build:</b> <font name='Courier'>docker compose build</font> rebuilds backend image"))
|
||||||
|
elements.append(bullet("5. <b>Database migration:</b> <font name='Courier'>alembic upgrade head</font> inside container"))
|
||||||
|
elements.append(bullet("6. <b>Restart services:</b> <font name='Courier'>docker compose up -d</font> restarts backend + DB"))
|
||||||
|
elements.append(bullet("7. <b>Reload Apache:</b> <font name='Courier'>systemctl reload apache2</font> picks up config changes"))
|
||||||
|
elements.extend(diagram("08_deployment_architecture.png", "Figure 8: Deployment Architecture"))
|
||||||
|
elements.append(PageBreak())
|
||||||
|
return elements
|
||||||
|
|
||||||
|
|
||||||
|
def build_api_reference():
|
||||||
|
elements = []
|
||||||
|
elements.append(h1("API Reference Summary"))
|
||||||
|
|
||||||
|
elements.append(h2("REST Endpoints"))
|
||||||
|
elements.append(make_table(
|
||||||
|
["Method", "Endpoint", "Auth", "Description"],
|
||||||
|
[
|
||||||
|
["GET", "/api/me", "Bearer", "Get authenticated user profile"],
|
||||||
|
["GET", "/api/campaigns", "Bearer", "List campaigns (filtered by role/agency)"],
|
||||||
|
["POST", "/api/campaigns", "Bearer + Write", "Create a new campaign"],
|
||||||
|
["GET", "/api/campaigns/{id}", "Bearer", "Get campaign by ID"],
|
||||||
|
["PUT", "/api/campaigns/{id}", "Bearer + Write", "Update a campaign"],
|
||||||
|
["DELETE", "/api/campaigns/{id}", "Bearer + Write", "Delete campaign and all files"],
|
||||||
|
["GET", "/api/campaigns/{id}/proofs", "Bearer", "List proofs for a campaign"],
|
||||||
|
["GET", "/api/proofs/{id}", "Bearer", "Get proof by ID"],
|
||||||
|
["DELETE", "/api/proofs/{id}", "Bearer + Write", "Delete proof and files"],
|
||||||
|
["POST", "/api/proofs/{id}/versions/{v}/flag", "Bearer + Write", "Flag an issue"],
|
||||||
|
["POST", "/api/proofs/{id}/versions/{v}/resolve", "Bearer + Write", "Resolve an issue"],
|
||||||
|
["GET", "/api/audit/flagged", "Bearer", "List flagged items"],
|
||||||
|
["GET", "/api/audit/resolved", "Bearer", "List resolved items"],
|
||||||
|
["GET", "/api/audit/errors", "Bearer", "List error items"],
|
||||||
|
["GET", "/api/analytics", "Bearer + Admin", "Get analytics data"],
|
||||||
|
["GET", "/api/analytics/by-agency", "Bearer + Admin", "Per-agency analytics"],
|
||||||
|
["GET", "/api/users", "Bearer + Admin", "List all users"],
|
||||||
|
["PUT", "/api/users/{id}", "Bearer + Super", "Update user role/agency"],
|
||||||
|
["GET", "/api/users/{id}/change-history", "Bearer + Admin", "User change audit trail"],
|
||||||
|
["GET", "/api/agencies", "Bearer", "List all agencies"],
|
||||||
|
["POST", "/api/agencies", "Bearer + Super", "Create agency"],
|
||||||
|
["GET", "/api/dropdown-options", "Bearer", "Get channel/sub-channel/type options"],
|
||||||
|
["POST", "/api/dropdown-options/...", "Bearer + Super", "Manage dropdown options"],
|
||||||
|
["GET", "/api/files/{key}", "Bearer", "Retrieve stored file"],
|
||||||
|
["GET", "/api/files/{key}/pages", "Bearer", "Rasterize PDF to page images"],
|
||||||
|
["POST", "/api/support/email", "Public", "Send support email"],
|
||||||
|
],
|
||||||
|
col_widths=[0.6 * inch, 2.3 * inch, 0.9 * inch, CONTENT_W - 3.8 * inch],
|
||||||
|
))
|
||||||
|
elements.append(spacer(0.1))
|
||||||
|
|
||||||
|
elements.append(h2("Knowledge Base Endpoints"))
|
||||||
|
elements.append(make_table(
|
||||||
|
["Method", "Endpoint", "Description"],
|
||||||
|
[
|
||||||
|
["GET", "/api/knowledge-base", "List all knowledge bases"],
|
||||||
|
["GET", "/api/knowledge-base/{id}", "Get KB detail with docs and active spec"],
|
||||||
|
["POST", "/api/knowledge-base/{id}/documents", "Upload source document"],
|
||||||
|
["DELETE", "/api/knowledge-base/{id}/documents/{doc_id}", "Remove source document"],
|
||||||
|
["POST", "/api/knowledge-base/{id}/process", "Trigger processing pipeline"],
|
||||||
|
["GET", "/api/knowledge-base/{id}/jobs/{job_id}", "Get processing job status"],
|
||||||
|
["GET", "/api/knowledge-base/{id}/versions", "List spec versions"],
|
||||||
|
["GET", "/api/knowledge-base/{id}/versions/{v_id}", "Get spec version content"],
|
||||||
|
["GET", "/api/knowledge-base/{id}/versions/{a}/diff/{b}", "Diff two spec versions"],
|
||||||
|
["POST", "/api/knowledge-base/{id}/versions/{v_id}/activate", "Activate a spec version"],
|
||||||
|
],
|
||||||
|
col_widths=[0.6 * inch, 2.8 * inch, CONTENT_W - 3.4 * inch],
|
||||||
|
))
|
||||||
|
elements.append(spacer(0.1))
|
||||||
|
|
||||||
|
elements.append(h2("WebSocket Endpoint"))
|
||||||
|
elements.append(p(
|
||||||
|
"<font name='Courier'>ws://host/ws/analyze</font> — Real-time proof analysis with streaming "
|
||||||
|
"agent progress updates. See the WebSocket Analysis Flow section for full protocol details."
|
||||||
|
))
|
||||||
|
elements.append(PageBreak())
|
||||||
|
return elements
|
||||||
|
|
||||||
|
|
||||||
|
def build_env_vars():
|
||||||
|
elements = []
|
||||||
|
elements.append(h1("Appendix A: Environment Variables"))
|
||||||
|
|
||||||
|
elements.append(h2("Backend (backend/.env)"))
|
||||||
|
elements.append(make_table(
|
||||||
|
["Variable", "Required", "Description"],
|
||||||
|
[
|
||||||
|
["GEMINI_API_KEY", "Yes", "Google Gemini API key for AI analysis"],
|
||||||
|
["DATABASE_URL", "Yes", "PostgreSQL connection string (asyncpg)"],
|
||||||
|
["AZURE_TENANT_ID", "Yes*", "Azure AD tenant ID for JWT verification"],
|
||||||
|
["AZURE_CLIENT_ID", "Yes*", "Azure AD application (client) ID"],
|
||||||
|
["CORS_ORIGINS", "Yes", "Comma-separated allowed CORS origins"],
|
||||||
|
["LLAMA_CLOUD_API_KEY", "No", "LlamaParse API key (enables KB pipeline)"],
|
||||||
|
["DISABLE_AUTH", "No", "Set 'true' for local dev without Azure AD"],
|
||||||
|
["REFERENCE_DOCS_PATH", "No", "Path to reference docs directory (default: reference_docs/)"],
|
||||||
|
],
|
||||||
|
col_widths=[1.8 * inch, 0.7 * inch, CONTENT_W - 2.5 * inch],
|
||||||
|
))
|
||||||
|
elements.append(p("<i>* Required when DISABLE_AUTH is not true</i>"))
|
||||||
|
elements.append(spacer(0.15))
|
||||||
|
|
||||||
|
elements.append(h2("Frontend (frontend/.env.local)"))
|
||||||
|
elements.append(make_table(
|
||||||
|
["Variable", "Required", "Description"],
|
||||||
|
[
|
||||||
|
["VITE_BACKEND_URL", "Yes", "Backend REST API base URL (e.g. http://localhost:8000)"],
|
||||||
|
["VITE_BACKEND_WS_URL", "Yes", "Backend WebSocket URL (e.g. ws://localhost:8000/ws/analyze)"],
|
||||||
|
["VITE_AZURE_CLIENT_ID", "Yes", "Azure AD app client ID for MSAL"],
|
||||||
|
["VITE_AZURE_TENANT_ID", "Yes", "Azure AD tenant ID for MSAL"],
|
||||||
|
["VITE_AZURE_REDIRECT_URI", "Yes", "OAuth2 redirect URI"],
|
||||||
|
],
|
||||||
|
col_widths=[2.0 * inch, 0.7 * inch, CONTENT_W - 2.7 * inch],
|
||||||
|
))
|
||||||
|
elements.append(PageBreak())
|
||||||
|
return elements
|
||||||
|
|
||||||
|
|
||||||
|
def build_tech_stack():
|
||||||
|
elements = []
|
||||||
|
elements.append(h1("Appendix B: Technology Stack"))
|
||||||
|
|
||||||
|
elements.append(h2("Backend Dependencies"))
|
||||||
|
elements.append(make_table(
|
||||||
|
["Package", "Version", "Purpose"],
|
||||||
|
[
|
||||||
|
["fastapi", "0.115+", "Web framework (REST + WebSocket)"],
|
||||||
|
["uvicorn", "0.34+", "ASGI server"],
|
||||||
|
["sqlalchemy", "2.0+", "Async ORM"],
|
||||||
|
["asyncpg", "0.30+", "PostgreSQL async driver"],
|
||||||
|
["alembic", "1.14+", "Database migrations"],
|
||||||
|
["google-genai", "1.x", "Google Gemini API client"],
|
||||||
|
["llama-parse", "0.6+", "Document parsing service"],
|
||||||
|
["pydantic", "2.x", "Data validation and serialisation"],
|
||||||
|
["python-jose", "3.3+", "JWT decoding and verification"],
|
||||||
|
["pillow", "11.x", "Image processing (thumbnails)"],
|
||||||
|
["pymupdf", "1.25+", "PDF rasterisation"],
|
||||||
|
["httpx", "0.28+", "Async HTTP client"],
|
||||||
|
],
|
||||||
|
col_widths=[1.6 * inch, 1.0 * inch, CONTENT_W - 2.6 * inch],
|
||||||
|
))
|
||||||
|
elements.append(spacer(0.15))
|
||||||
|
|
||||||
|
elements.append(h2("Frontend Dependencies"))
|
||||||
|
elements.append(make_table(
|
||||||
|
["Package", "Version", "Purpose"],
|
||||||
|
[
|
||||||
|
["react", "18.x", "UI component library"],
|
||||||
|
["typescript", "5.x", "Type-safe JavaScript"],
|
||||||
|
["vite", "5.x", "Build tool and dev server"],
|
||||||
|
["tailwindcss", "3.x", "Utility-first CSS framework"],
|
||||||
|
["@azure/msal-browser", "3.x", "Azure AD authentication (browser)"],
|
||||||
|
["@azure/msal-react", "2.x", "React bindings for MSAL"],
|
||||||
|
["lucide-react", "latest", "Icon library"],
|
||||||
|
["react-markdown", "latest", "Markdown rendering"],
|
||||||
|
["recharts", "2.x", "Charting library (Analytics)"],
|
||||||
|
],
|
||||||
|
col_widths=[1.8 * inch, 1.0 * inch, CONTENT_W - 2.8 * inch],
|
||||||
|
))
|
||||||
|
return elements
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Main Build
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("Building ModComms Technical Architecture PDF...")
|
||||||
|
doc = ArchDocTemplate(str(OUTPUT_PDF))
|
||||||
|
|
||||||
|
story = []
|
||||||
|
|
||||||
|
# Cover
|
||||||
|
story.extend(build_cover())
|
||||||
|
|
||||||
|
# TOC
|
||||||
|
story.extend(build_toc())
|
||||||
|
|
||||||
|
# Content sections
|
||||||
|
story.extend(build_executive_summary())
|
||||||
|
story.extend(build_system_architecture())
|
||||||
|
story.extend(build_agent_pipeline())
|
||||||
|
story.extend(build_websocket_flow())
|
||||||
|
story.extend(build_database_schema())
|
||||||
|
story.extend(build_frontend())
|
||||||
|
story.extend(build_auth_rbac())
|
||||||
|
story.extend(build_knowledge_base())
|
||||||
|
story.extend(build_deployment())
|
||||||
|
story.extend(build_api_reference())
|
||||||
|
story.extend(build_env_vars())
|
||||||
|
story.extend(build_tech_stack())
|
||||||
|
|
||||||
|
# Build with two passes for TOC page numbers
|
||||||
|
doc.multiBuild(story)
|
||||||
|
print(f"PDF generated: {OUTPUT_PDF}")
|
||||||
|
print(f" File size: {OUTPUT_PDF.stat().st_size / 1024:.0f} KB")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
21
documentation/architecture/render_diagrams.sh
Executable file
|
|
@ -0,0 +1,21 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Render all Mermaid .mmd diagrams to high-resolution PNGs
|
||||||
|
# Usage: bash render_diagrams.sh
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
DIAGRAMS_DIR="${SCRIPT_DIR}/diagrams"
|
||||||
|
OUTPUT_DIR="${DIAGRAMS_DIR}/rendered"
|
||||||
|
|
||||||
|
mkdir -p "${OUTPUT_DIR}"
|
||||||
|
|
||||||
|
echo "Rendering Mermaid diagrams..."
|
||||||
|
for mmd_file in "${DIAGRAMS_DIR}"/*.mmd; do
|
||||||
|
filename="$(basename "${mmd_file}" .mmd)"
|
||||||
|
output_file="${OUTPUT_DIR}/${filename}.png"
|
||||||
|
echo " ${filename}.mmd -> ${filename}.png"
|
||||||
|
mmdc -i "${mmd_file}" -o "${output_file}" -s 3 -w 2400 -b white --quiet
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Done! Rendered $(ls -1 "${OUTPUT_DIR}"/*.png 2>/dev/null | wc -l | tr -d ' ') diagrams to ${OUTPUT_DIR}/"
|
||||||