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>
This commit is contained in:
michael 2026-02-27 13:42:59 -06:00
parent da32e0f888
commit b6078cf534
19 changed files with 2882 additions and 0 deletions

File diff suppressed because one or more lines are too long

View 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

View 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

View 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"}

View 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
}

View file

@ -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

View 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

View file

@ -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

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 750 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,015 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 KiB

View 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>&bull;</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 &rarr; 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 &rarr; Client", "agent_started",
"agent_name",
"Agent has begun processing"],
["Server &rarr; Client", "agent_completed",
"agent_name, review (ragStatus, feedback, issues, resolvedIssues, outstandingIssues, newIssues)",
"Agent finished with results"],
["Server &rarr; 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 &rarr; 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 &rarr; Sub-Channel "
"&rarr; 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> &mdash; Verifies JWT and returns claims dict"))
elements.append(bullet(
"<font name='Courier'>get_current_db_user()</font> &mdash; Resolves claims to a User ORM object with agency"))
elements.append(bullet(
"<font name='Courier'>require_role(*roles)</font> &mdash; Factory that restricts endpoints to specific roles"))
elements.append(bullet(
"<font name='Courier'>require_write_access()</font> &mdash; 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 &rarr; parsing &rarr; distilling &rarr; 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> &mdash; 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()

View 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}/"