Paramjit Singh commited on
Commit
6fffc51
·
unverified ·
2 Parent(s): 12a3a307939ee2

Merge pull request #303 from saurabhhhcodes/docs/architecture-swagger-294

Browse files
README.md CHANGED
@@ -99,6 +99,10 @@ The system uses **semantic search + cross-encoder reranking** to find the most r
99
 
100
  ## 🏗️ Architecture
101
 
 
 
 
 
102
  ```mermaid
103
  graph TD
104
  subgraph Frontend["Frontend (Next.js 16)"]
 
99
 
100
  ## 🏗️ Architecture
101
 
102
+ > Contributor note: see [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for a
103
+ > route-by-route system map, request-flow diagrams, ownership boundaries, and
104
+ > Swagger/OpenAPI documentation guidance.
105
+
106
  ```mermaid
107
  graph TD
108
  subgraph Frontend["Frontend (Next.js 16)"]
backend/app/routes/admin.py CHANGED
@@ -34,12 +34,25 @@ def _directory_size(path: Path) -> int:
34
  return total
35
 
36
 
37
- @router.get("/stats", response_model=AdminStatsResponse)
 
 
 
 
 
 
 
 
38
  def get_admin_stats(
39
  db: Session = Depends(get_db),
40
  _admin: User = Depends(get_current_admin),
41
  ):
42
- """Return aggregate system statistics for administrators."""
 
 
 
 
 
43
  upload_dir = Path(settings.UPLOAD_DIR).resolve()
44
  upload_dir.mkdir(parents=True, exist_ok=True)
45
 
@@ -77,10 +90,19 @@ def get_admin_stats(
77
  )
78
 
79
 
80
- @router.get("/users", response_model=List[UserResponse])
 
 
 
 
 
81
  def list_all_users(
82
  db: Session = Depends(get_db),
83
  _admin: User = Depends(get_current_admin),
84
  ):
85
- """List all registered users (admin-only)."""
 
 
 
 
86
  return db.query(User).all()
 
34
  return total
35
 
36
 
37
+ @router.get(
38
+ "/stats",
39
+ response_model=AdminStatsResponse,
40
+ summary="Get admin dashboard statistics",
41
+ description=(
42
+ "Returns aggregate user, document, message, query-latency, and disk "
43
+ "usage metrics for authenticated administrators."
44
+ ),
45
+ )
46
  def get_admin_stats(
47
  db: Session = Depends(get_db),
48
  _admin: User = Depends(get_current_admin),
49
  ):
50
+ """Return aggregate operational statistics for the admin dashboard.
51
+
52
+ The response includes counts for users, uploaded PDFs, all documents, chat
53
+ messages, average RAG query latency, and upload-directory disk usage.
54
+ Access is restricted by the `get_current_admin` dependency.
55
+ """
56
  upload_dir = Path(settings.UPLOAD_DIR).resolve()
57
  upload_dir.mkdir(parents=True, exist_ok=True)
58
 
 
90
  )
91
 
92
 
93
+ @router.get(
94
+ "/users",
95
+ response_model=List[UserResponse],
96
+ summary="List all registered users",
97
+ description="Returns the registered user inventory for authenticated administrators.",
98
+ )
99
  def list_all_users(
100
  db: Session = Depends(get_db),
101
  _admin: User = Depends(get_current_admin),
102
  ):
103
+ """List all registered users.
104
+
105
+ Access is restricted to administrators and the response is serialized
106
+ through `UserResponse` so token fields and secrets are not exposed.
107
+ """
108
  return db.query(User).all()
backend/app/routes/chat.py CHANGED
@@ -35,11 +35,25 @@ logger = logging.getLogger(__name__)
35
  router = APIRouter(prefix="/chat", tags=["Chat"])
36
 
37
 
38
- @router.get("/share/{message_id}", response_model=ShareAnswerResponse)
 
 
 
 
 
 
 
 
39
  def get_shared_answer(
40
  message_id: str,
41
  db: Session = Depends(get_db),
42
  ):
 
 
 
 
 
 
43
  message = db.query(ChatMessage).filter(
44
  ChatMessage.id == message_id,
45
  ChatMessage.role == "assistant",
@@ -51,12 +65,25 @@ def get_shared_answer(
51
  return _share_answer_response(message)
52
 
53
 
54
- @router.post("/share/{message_id}", response_model=ShareLinkResponse)
 
 
 
 
 
 
 
 
55
  def create_share_link(
56
  message_id: str,
57
  user: User = Depends(get_current_user),
58
  db: Session = Depends(get_db),
59
  ):
 
 
 
 
 
60
  message = db.query(ChatMessage).filter(
61
  ChatMessage.id == message_id,
62
  ChatMessage.user_id == user.id,
@@ -80,7 +107,12 @@ def create_share_link(
80
  )
81
 
82
 
83
- @router.get("/sessions", response_model=List[ChatSessionResponse])
 
 
 
 
 
84
  def get_chat_sessions(
85
  user: User = Depends(get_current_user),
86
  db: Session = Depends(get_db),
@@ -95,13 +127,19 @@ def get_chat_sessions(
95
  return sessions
96
 
97
 
98
- @router.post("/sessions", response_model=ChatSessionResponse, status_code=201)
 
 
 
 
 
 
99
  def create_chat_session(
100
  payload: ChatSessionCreate,
101
  user: User = Depends(get_current_user),
102
  db: Session = Depends(get_db),
103
  ):
104
- """Create a new chat session."""
105
  session = ChatSession(
106
  user_id=user.id,
107
  title=payload.title,
@@ -112,14 +150,19 @@ def create_chat_session(
112
  return session
113
 
114
 
115
- @router.put("/sessions/{session_id}", response_model=ChatSessionResponse)
 
 
 
 
 
116
  def rename_chat_session(
117
  session_id: str,
118
  payload: ChatSessionCreate,
119
  user: User = Depends(get_current_user),
120
  db: Session = Depends(get_db),
121
  ):
122
- """Rename an existing chat session."""
123
  session = (
124
  db.query(ChatSession)
125
  .filter(
@@ -136,13 +179,17 @@ def rename_chat_session(
136
  return session
137
 
138
 
139
- @router.delete("/sessions/{session_id}")
 
 
 
 
140
  def delete_chat_session(
141
  session_id: str,
142
  user: User = Depends(get_current_user),
143
  db: Session = Depends(get_db),
144
  ):
145
- """Delete a chat session and all its messages."""
146
  session = (
147
  db.query(ChatSession)
148
  .filter(
@@ -158,13 +205,18 @@ def delete_chat_session(
158
  return Response(status_code=204)
159
 
160
 
161
- @router.get("/history/session/{session_id}", response_model=ChatHistoryResponse)
 
 
 
 
 
162
  def get_session_history(
163
  session_id: str,
164
  user: User = Depends(get_current_user),
165
  db: Session = Depends(get_db),
166
  ):
167
- """Retrieve chat history for a specific chat session."""
168
  session = (
169
  db.query(ChatSession)
170
  .filter(
@@ -220,7 +272,15 @@ def generate_answer_stream(question: str, user_id: str, document_id: Optional[st
220
  return _generate_answer_stream(question=question, user_id=user_id, document_id=document_id, hf_token=hf_token)
221
 
222
 
223
- @router.post("/ask", response_model=ChatResponse)
 
 
 
 
 
 
 
 
224
  @limiter.limit(CHAT_QUERY_RATE_LIMIT)
225
  def ask_question(
226
  request: Request,
@@ -228,7 +288,7 @@ def ask_question(
228
  user: User = Depends(get_current_user),
229
  db: Session = Depends(get_db),
230
  ):
231
- """Ask a question with RAG retrieval (non-streaming)."""
232
  started_at = time.perf_counter()
233
  try:
234
  # Validate document exists if specified
@@ -283,7 +343,14 @@ def ask_question(
283
  record_query_response_time(time.perf_counter() - started_at)
284
 
285
 
286
- @router.post("/ask/stream")
 
 
 
 
 
 
 
287
  @limiter.limit(CHAT_QUERY_RATE_LIMIT)
288
  def ask_question_stream(
289
  request: Request,
@@ -291,7 +358,7 @@ def ask_question_stream(
291
  user: User = Depends(get_current_user),
292
  db: Session = Depends(get_db),
293
  ):
294
- """Ask a question with Server-Sent Events (SSE) streaming response."""
295
  # Validate document
296
  if payload.document_id:
297
  doc = db.query(Document).filter(
@@ -375,7 +442,12 @@ def ask_question_stream(
375
  )
376
 
377
 
378
- @router.get("/history/{document_id}", response_model=ChatHistoryResponse)
 
 
 
 
 
379
  def get_chat_history(
380
  document_id: str,
381
  user: User = Depends(get_current_user),
@@ -412,7 +484,14 @@ def get_chat_history(
412
  return ChatHistoryResponse(messages=formatted, document_id=document_id)
413
 
414
 
415
- @router.get("/export/{document_id}")
 
 
 
 
 
 
 
416
  def export_chat_history(
417
  document_id: str,
418
  format: str = "md",
@@ -484,7 +563,11 @@ def export_chat_history(
484
  )
485
 
486
 
487
- @router.delete("/history/{document_id}")
 
 
 
 
488
  def clear_chat_history(
489
  document_id: str,
490
  user: User = Depends(get_current_user),
 
35
  router = APIRouter(prefix="/chat", tags=["Chat"])
36
 
37
 
38
+ @router.get(
39
+ "/share/{message_id}",
40
+ response_model=ShareAnswerResponse,
41
+ summary="Read a public shared answer",
42
+ description=(
43
+ "Returns a previously shared assistant answer and its safe citation "
44
+ "metadata without requiring authentication."
45
+ ),
46
+ )
47
  def get_shared_answer(
48
  message_id: str,
49
  db: Session = Depends(get_db),
50
  ):
51
+ """Return a public shared assistant answer by message ID.
52
+
53
+ Only assistant messages that already have a `SharedMessage` record are
54
+ exposed. User prompts, private chat history, and unshared answers remain
55
+ protected.
56
+ """
57
  message = db.query(ChatMessage).filter(
58
  ChatMessage.id == message_id,
59
  ChatMessage.role == "assistant",
 
65
  return _share_answer_response(message)
66
 
67
 
68
+ @router.post(
69
+ "/share/{message_id}",
70
+ response_model=ShareLinkResponse,
71
+ summary="Create a public share link for an assistant answer",
72
+ description=(
73
+ "Marks one authenticated user's assistant message as shareable and "
74
+ "returns the frontend share URL."
75
+ ),
76
+ )
77
  def create_share_link(
78
  message_id: str,
79
  user: User = Depends(get_current_user),
80
  db: Session = Depends(get_db),
81
  ):
82
+ """Create or reuse a public share record for an assistant answer.
83
+
84
+ The message must belong to the authenticated user and must have the
85
+ assistant role. User-authored messages cannot be shared through this route.
86
+ """
87
  message = db.query(ChatMessage).filter(
88
  ChatMessage.id == message_id,
89
  ChatMessage.user_id == user.id,
 
107
  )
108
 
109
 
110
+ @router.get(
111
+ "/sessions",
112
+ response_model=List[ChatSessionResponse],
113
+ summary="List chat sessions",
114
+ description="Returns all chat sessions owned by the authenticated user, newest first.",
115
+ )
116
  def get_chat_sessions(
117
  user: User = Depends(get_current_user),
118
  db: Session = Depends(get_db),
 
127
  return sessions
128
 
129
 
130
+ @router.post(
131
+ "/sessions",
132
+ response_model=ChatSessionResponse,
133
+ status_code=201,
134
+ summary="Create a chat session",
135
+ description="Creates a named chat session owned by the authenticated user.",
136
+ )
137
  def create_chat_session(
138
  payload: ChatSessionCreate,
139
  user: User = Depends(get_current_user),
140
  db: Session = Depends(get_db),
141
  ):
142
+ """Create a new chat session for the authenticated user."""
143
  session = ChatSession(
144
  user_id=user.id,
145
  title=payload.title,
 
150
  return session
151
 
152
 
153
+ @router.put(
154
+ "/sessions/{session_id}",
155
+ response_model=ChatSessionResponse,
156
+ summary="Rename a chat session",
157
+ description="Renames one chat session after verifying it belongs to the authenticated user.",
158
+ )
159
  def rename_chat_session(
160
  session_id: str,
161
  payload: ChatSessionCreate,
162
  user: User = Depends(get_current_user),
163
  db: Session = Depends(get_db),
164
  ):
165
+ """Rename an existing chat session owned by the authenticated user."""
166
  session = (
167
  db.query(ChatSession)
168
  .filter(
 
179
  return session
180
 
181
 
182
+ @router.delete(
183
+ "/sessions/{session_id}",
184
+ summary="Delete a chat session",
185
+ description="Deletes one owned chat session and cascades its messages through the database relationship.",
186
+ )
187
  def delete_chat_session(
188
  session_id: str,
189
  user: User = Depends(get_current_user),
190
  db: Session = Depends(get_db),
191
  ):
192
+ """Delete a chat session owned by the authenticated user."""
193
  session = (
194
  db.query(ChatSession)
195
  .filter(
 
205
  return Response(status_code=204)
206
 
207
 
208
+ @router.get(
209
+ "/history/session/{session_id}",
210
+ response_model=ChatHistoryResponse,
211
+ summary="Get chat history for a session",
212
+ description="Returns ordered user and assistant messages for one owned chat session.",
213
+ )
214
  def get_session_history(
215
  session_id: str,
216
  user: User = Depends(get_current_user),
217
  db: Session = Depends(get_db),
218
  ):
219
+ """Retrieve ordered chat history for a specific owned chat session."""
220
  session = (
221
  db.query(ChatSession)
222
  .filter(
 
272
  return _generate_answer_stream(question=question, user_id=user_id, document_id=document_id, hf_token=hf_token)
273
 
274
 
275
+ @router.post(
276
+ "/ask",
277
+ response_model=ChatResponse,
278
+ summary="Ask a RAG question",
279
+ description=(
280
+ "Runs non-streaming retrieval-augmented generation for the authenticated "
281
+ "user, optionally scoped to one ready document."
282
+ ),
283
+ )
284
  @limiter.limit(CHAT_QUERY_RATE_LIMIT)
285
  def ask_question(
286
  request: Request,
 
288
  user: User = Depends(get_current_user),
289
  db: Session = Depends(get_db),
290
  ):
291
+ """Ask a question with RAG retrieval and return the complete answer."""
292
  started_at = time.perf_counter()
293
  try:
294
  # Validate document exists if specified
 
343
  record_query_response_time(time.perf_counter() - started_at)
344
 
345
 
346
+ @router.post(
347
+ "/ask/stream",
348
+ summary="Stream a RAG answer",
349
+ description=(
350
+ "Runs retrieval-augmented generation and streams answer tokens as "
351
+ "server-sent events. The final assistant response is saved to history."
352
+ ),
353
+ )
354
  @limiter.limit(CHAT_QUERY_RATE_LIMIT)
355
  def ask_question_stream(
356
  request: Request,
 
358
  user: User = Depends(get_current_user),
359
  db: Session = Depends(get_db),
360
  ):
361
+ """Ask a question and stream the answer using Server-Sent Events."""
362
  # Validate document
363
  if payload.document_id:
364
  doc = db.query(Document).filter(
 
442
  )
443
 
444
 
445
+ @router.get(
446
+ "/history/{document_id}",
447
+ response_model=ChatHistoryResponse,
448
+ summary="Get document chat history",
449
+ description="Returns ordered chat messages for one document owned by the authenticated user.",
450
+ )
451
  def get_chat_history(
452
  document_id: str,
453
  user: User = Depends(get_current_user),
 
484
  return ChatHistoryResponse(messages=formatted, document_id=document_id)
485
 
486
 
487
+ @router.get(
488
+ "/export/{document_id}",
489
+ summary="Export document chat history",
490
+ description=(
491
+ "Downloads one document's chat history as Markdown, plain text, or PDF. "
492
+ "The browser download flow authenticates with a query token."
493
+ ),
494
+ )
495
  def export_chat_history(
496
  document_id: str,
497
  format: str = "md",
 
563
  )
564
 
565
 
566
+ @router.delete(
567
+ "/history/{document_id}",
568
+ summary="Clear document chat history",
569
+ description="Deletes all chat messages for one document owned by the authenticated user.",
570
+ )
571
  def clear_chat_history(
572
  document_id: str,
573
  user: User = Depends(get_current_user),
backend/app/routes/github.py CHANGED
@@ -4,7 +4,7 @@ import urllib.request
4
  from urllib.error import URLError, HTTPError
5
  from fastapi import APIRouter, HTTPException
6
 
7
- router = APIRouter()
8
 
9
  CACHE = {
10
  "contribs": {"data": None, "timestamp": 0},
@@ -14,16 +14,30 @@ TTL = 3600 # 1 hour cache to avoid 403 Rate Limit
14
 
15
  REPO = "param20h/PDF-Assistant-RAG"
16
 
 
17
  def fetch_github(url: str, cache_key: str):
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  now = time.time()
19
  if CACHE[cache_key]["data"] is not None and now - CACHE[cache_key]["timestamp"] < TTL:
20
  return CACHE[cache_key]["data"]
21
-
22
  req = urllib.request.Request(url, headers={
23
  "Accept": "application/vnd.github.v3+json",
24
  "User-Agent": "PDF-Assistant-RAG"
25
  })
26
-
27
  try:
28
  with urllib.request.urlopen(req) as response:
29
  data = json.loads(response.read().decode())
@@ -40,11 +54,21 @@ def fetch_github(url: str, cache_key: str):
40
  return CACHE[cache_key]["data"]
41
  raise HTTPException(status_code=500, detail="Failed to connect to GitHub")
42
 
43
- @router.get("/github/stats")
 
 
 
 
 
 
 
 
 
44
  def get_github_stats():
 
45
  contribs = fetch_github(f"https://api.github.com/repos/{REPO}/contributors?per_page=30", "contribs")
46
  repo = fetch_github(f"https://api.github.com/repos/{REPO}", "repo")
47
-
48
  return {
49
  "contributors": contribs if isinstance(contribs, list) else [],
50
  "stats": {
 
4
  from urllib.error import URLError, HTTPError
5
  from fastapi import APIRouter, HTTPException
6
 
7
+ router = APIRouter(tags=["GitHub"])
8
 
9
  CACHE = {
10
  "contribs": {"data": None, "timestamp": 0},
 
14
 
15
  REPO = "param20h/PDF-Assistant-RAG"
16
 
17
+
18
  def fetch_github(url: str, cache_key: str):
19
+ """Fetch a GitHub API resource with a short in-memory fallback cache.
20
+
21
+ Args:
22
+ url: GitHub REST API URL to request.
23
+ cache_key: Key in the module-level cache that stores the response.
24
+
25
+ Returns:
26
+ Parsed JSON data from GitHub, or the cached response when GitHub is
27
+ rate-limited or temporarily unreachable.
28
+
29
+ Raises:
30
+ HTTPException: If GitHub fails and no cached data is available.
31
+ """
32
  now = time.time()
33
  if CACHE[cache_key]["data"] is not None and now - CACHE[cache_key]["timestamp"] < TTL:
34
  return CACHE[cache_key]["data"]
35
+
36
  req = urllib.request.Request(url, headers={
37
  "Accept": "application/vnd.github.v3+json",
38
  "User-Agent": "PDF-Assistant-RAG"
39
  })
40
+
41
  try:
42
  with urllib.request.urlopen(req) as response:
43
  data = json.loads(response.read().decode())
 
54
  return CACHE[cache_key]["data"]
55
  raise HTTPException(status_code=500, detail="Failed to connect to GitHub")
56
 
57
+
58
+ @router.get(
59
+ "/github/stats",
60
+ summary="Get public GitHub repository statistics",
61
+ description=(
62
+ "Returns cached contributor and repository counters for the public "
63
+ "PDF-Assistant-RAG repository. The endpoint does not require user "
64
+ "authentication because it only exposes public GitHub metadata."
65
+ ),
66
+ )
67
  def get_github_stats():
68
+ """Return public contributor and repository statistics for the landing page."""
69
  contribs = fetch_github(f"https://api.github.com/repos/{REPO}/contributors?per_page=30", "contribs")
70
  repo = fetch_github(f"https://api.github.com/repos/{REPO}", "repo")
71
+
72
  return {
73
  "contributors": contribs if isinstance(contribs, list) else [],
74
  "stats": {
docs/ARCHITECTURE.md ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Architecture Guide
2
+
3
+ This guide gives contributors a map of the PDF-Assistant-RAG runtime before
4
+ they change an endpoint, storage model, or RAG step. The README keeps the
5
+ product overview; this page focuses on how requests move through the system.
6
+
7
+ ## Runtime Topology
8
+
9
+ ```mermaid
10
+ flowchart LR
11
+ Browser["Next.js frontend<br/>dashboard, chat, PDF viewer"]
12
+ API["FastAPI API<br/>/api/v1 routes"]
13
+ SQL["SQL database<br/>users, documents, chats"]
14
+ Uploads["Upload directory<br/>original files"]
15
+ Chroma["ChromaDB<br/>per-user document chunks"]
16
+ RAG["RAG services<br/>chunking, embeddings, reranking"]
17
+ LLM["HuggingFace inference<br/>answer generation"]
18
+ GitHub["GitHub API<br/>public repo stats"]
19
+
20
+ Browser -->|"JWT + REST"| API
21
+ Browser -->|"SSE chat stream"| API
22
+ API --> SQL
23
+ API --> Uploads
24
+ API --> Chroma
25
+ API --> RAG
26
+ RAG --> Chroma
27
+ RAG --> LLM
28
+ API --> GitHub
29
+ ```
30
+
31
+ The frontend is a Next.js application that talks to the FastAPI backend. In
32
+ development it usually runs on `http://localhost:3000`; the backend runs on
33
+ `http://localhost:8000` and exposes Swagger at `http://localhost:8000/docs`.
34
+ In production the backend can also serve the exported frontend from
35
+ `frontend/out` when that directory exists.
36
+
37
+ ## Backend Route Groups
38
+
39
+ | Route group | Prefix | Responsibility |
40
+ | --- | --- | --- |
41
+ | Auth | `/api/v1/auth` | Registration, login, Google sign-in, JWT refresh, and profile state. |
42
+ | Documents | `/api/v1/documents` | File validation, upload records, background ingestion, status polling, file serving, deletion, and metadata updates. |
43
+ | Chat | `/api/v1/chat` | RAG questions, SSE streaming, chat sessions, history, exports, and shared answer links. |
44
+ | Admin | `/api/v1/admin` | Admin-only operational stats and user inventory. |
45
+ | GitHub | `/api/v1/github/stats` | Cached public repository statistics for the landing page. |
46
+ | Health | `/health`, `/api/health` | Lightweight service health checks for API, SQL, and Chroma availability. |
47
+
48
+ ## Document Ingestion Flow
49
+
50
+ ```mermaid
51
+ sequenceDiagram
52
+ participant UI as Frontend
53
+ participant API as FastAPI documents route
54
+ participant DB as SQL metadata
55
+ participant Worker as Background task
56
+ participant Files as Upload storage
57
+ participant Vector as ChromaDB
58
+
59
+ UI->>API: POST /api/v1/documents/upload
60
+ API->>API: Validate filename, extension, size, MIME, and parser readability
61
+ API->>Files: Persist original file under the user's upload directory
62
+ API->>DB: Create document row with processing status
63
+ API-->>UI: 202 Accepted with document metadata
64
+ API->>Worker: Queue ingestion task
65
+ Worker->>Files: Read saved document
66
+ Worker->>Worker: Extract pages, chunk text, build graph summary data
67
+ Worker->>Vector: Store chunks with document and user metadata
68
+ Worker->>DB: Save page count, chunk count, summary, and ready/failed status
69
+ ```
70
+
71
+ The upload route is intentionally strict before it writes long-lived state:
72
+ extension checks, size checks, MIME checks, and parser checks happen before the
73
+ file is moved into permanent storage. The background task owns expensive work
74
+ such as text extraction, chunking, embedding, graph building, and summary
75
+ generation.
76
+
77
+ ## Chat And Retrieval Flow
78
+
79
+ ```mermaid
80
+ sequenceDiagram
81
+ participant UI as Frontend chat panel
82
+ participant API as FastAPI chat route
83
+ participant DB as SQL chat/session rows
84
+ participant Retriever as Retriever and reranker
85
+ participant Vector as ChromaDB
86
+ participant LLM as HuggingFace model
87
+
88
+ UI->>API: POST /api/v1/chat/ask or /ask/stream
89
+ API->>DB: Validate user, optional document, and chat session
90
+ API->>DB: Save user message
91
+ API->>Retriever: Generate answer for question and optional document scope
92
+ Retriever->>Vector: Semantic search by user and document metadata
93
+ Retriever->>Retriever: Rerank candidate chunks
94
+ Retriever->>LLM: Send prompt with selected context
95
+ LLM-->>API: Answer tokens or complete answer
96
+ API->>DB: Save assistant response and source citations
97
+ API-->>UI: JSON response or server-sent events
98
+ ```
99
+
100
+ Non-streaming chat returns a complete `ChatResponse`. Streaming chat uses
101
+ server-sent events so the frontend can render tokens as they arrive, then saves
102
+ the final assistant message after generation finishes.
103
+
104
+ ## Data Ownership And Boundaries
105
+
106
+ ```mermaid
107
+ flowchart TD
108
+ User["Authenticated user"]
109
+ JWT["JWT identity"]
110
+ Docs["Document rows"]
111
+ Files["Uploaded files"]
112
+ Chunks["Vector chunks"]
113
+ Chats["Chat sessions and messages"]
114
+ Admin["Admin-only routes"]
115
+
116
+ User --> JWT
117
+ JWT --> Docs
118
+ JWT --> Files
119
+ JWT --> Chunks
120
+ JWT --> Chats
121
+ Admin -. "requires admin dependency" .-> Docs
122
+ Admin -. "aggregate only" .-> Chats
123
+ ```
124
+
125
+ User-facing routes must filter by `user.id` before reading or mutating
126
+ documents, chat sessions, messages, uploaded files, or vector chunks. Admin
127
+ routes use `get_current_admin` and should avoid returning secrets, tokens, file
128
+ contents, or raw vector payloads.
129
+
130
+ ## Swagger And OpenAPI Notes
131
+
132
+ FastAPI builds the OpenAPI schema from route decorators, response models,
133
+ function names, parameter annotations, and docstrings. When adding or changing
134
+ an endpoint:
135
+
136
+ - Add a concise `summary` when the function name is not enough for Swagger.
137
+ - Use a docstring to describe ownership rules, side effects, and response shape.
138
+ - Keep `response_model` accurate so generated examples match real responses.
139
+ - Prefer typed query/body models over loosely shaped dictionaries.
140
+ - Mention asynchronous side effects, such as background ingestion or SSE
141
+ streaming, in the route description.
142
+
143
+ ## Local Contributor Checklist
144
+
145
+ Before opening a backend documentation or route metadata PR:
146
+
147
+ 1. Run Python compilation for touched route files.
148
+ 2. Run the fatal-error flake8 selection used by CI.
149
+ 3. Check Markdown fences and Mermaid blocks render as plain GitHub Markdown.
150
+ 4. Confirm the README links to any new contributor-facing docs.