Srini P commited on
Commit
ba8f1ce
·
1 Parent(s): c28eaa9

Feat: Add Azure AI Foundry Tracing and update documentation

Browse files
.gitignore CHANGED
@@ -1,7 +1,8 @@
1
  # Environments
2
  .env
3
- .env*
4
- **/.env*
 
5
 
6
  # Python
7
  __pycache__/
 
1
  # Environments
2
  .env
3
+ !.env.example
4
+ **/.env
5
+ !**/.env.example
6
 
7
  # Python
8
  __pycache__/
COMPLETE_SYSTEM_GUIDE.md CHANGED
@@ -36,10 +36,16 @@ This document provides an overview of the complete FinBot RAG system with both f
36
  │ 5. Output Guards (grounding, citations) │
37
  └────────────────┬─────────────────────────┘
38
 
39
- ┌──────────────────────────────────────────┐
40
- Vector Store (Qdrant) + LLM (Groq)
41
- Document Ingestion Pipeline
42
- └──────────────────────────────────────────┘
 
 
 
 
 
 
43
  ```
44
 
45
  ## Quick Start Options
 
36
  │ 5. Output Guards (grounding, citations) │
37
  └────────────────┬─────────────────────────┘
38
 
39
+ ┌──────────────────────────────────────────
40
+ Observability (Azure OTEL)
41
+ Automatic HTTP request tracing
42
+ │ • Manual Spans for RAG Stages │
43
+ │ • Prompt/Response Content Monitoring │
44
+ └───────────────────────────────────────────┘
45
+
46
+ ┌───────────────────────────────────────────┐
47
+ │ Output to Frontend │
48
+ └───────────────────────────────────────────┘
49
  ```
50
 
51
  ## Quick Start Options
README.md CHANGED
@@ -117,7 +117,9 @@ FinBot solves both problems:
117
 
118
  4. **Guardrails on Both Sides**: Input guards block prompt injection, off-topic queries, and PII. Output guards verify grounding, enforce citations, and detect cross-role leakage.
119
 
120
- 5. **Modular Design**: Each component (routing, retrieval, guardrails, LLM) is independently testable and replaceable.
 
 
121
 
122
  ---
123
 
@@ -239,6 +241,10 @@ Edit `.env` and add your Groq API key:
239
  GROQ_API_KEY=gsk-...your-key-here...
240
  QDRANT_MODE=local
241
  SERVER_PORT=8000
 
 
 
 
242
  ```
243
 
244
  ### 3. Ingest Documents
@@ -768,6 +774,8 @@ Since free-tier hosting uses ephemeral storage, you **must** use Qdrant Cloud to
768
  - `QDRANT_URL`: Your Qdrant Cloud URL (include port :6333)
769
  - `QDRANT_API_KEY`: Your Qdrant Cloud API Key
770
  - `PORT`: Automatically set to 7860 by Hugging Face
 
 
771
 
772
  ### 3. Frontend (Vercel)
773
  1. **Import Repository**: Connect your GitHub repository to [Vercel](https://vercel.com).
 
117
 
118
  4. **Guardrails on Both Sides**: Input guards block prompt injection, off-topic queries, and PII. Output guards verify grounding, enforce citations, and detect cross-role leakage.
119
 
120
+ 5. **Built-in Observability**: Full OpenTelemetry integration with Azure AI Foundry. Every request is traced, and the RAG pipeline is broken down into granular spans (Guardrails, Routing, Retrieval, Generation) for production monitoring.
121
+
122
+ 6. **Modular Design**: Each component (routing, retrieval, guardrails, LLM) is independently testable and replaceable.
123
 
124
  ---
125
 
 
241
  GROQ_API_KEY=gsk-...your-key-here...
242
  QDRANT_MODE=local
243
  SERVER_PORT=8000
244
+
245
+ # Optional: Azure AI Foundry Tracing
246
+ APPLICATIONINSIGHTS_CONNECTION_STRING=...your-connection-string...
247
+ AZURE_TRACING_GEN_AI_CONTENT_RECORDING_ENABLED=true
248
  ```
249
 
250
  ### 3. Ingest Documents
 
774
  - `QDRANT_URL`: Your Qdrant Cloud URL (include port :6333)
775
  - `QDRANT_API_KEY`: Your Qdrant Cloud API Key
776
  - `PORT`: Automatically set to 7860 by Hugging Face
777
+ - `APPLICATIONINSIGHTS_CONNECTION_STRING`: Your Azure trace connection string
778
+ - `AZURE_TRACING_GEN_AI_CONTENT_RECORDING_ENABLED`: `true`
779
 
780
  ### 3. Frontend (Vercel)
781
  1. **Import Repository**: Connect your GitHub repository to [Vercel](https://vercel.com).
app/backend/.env.example ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Environment configuration for FinBot
2
+ # Copy this to .env and fill in your actual values
3
+
4
+ # Groq Configuration (LLM provider)
5
+ GROQ_API_KEY=your_groq_api_key_here
6
+
7
+ # Qdrant Configuration
8
+ # Mode: "local" for local persistent storage, "url" for Qdrant Cloud, "memory" for in-memory
9
+ QDRANT_MODE=local
10
+ # For Qdrant Cloud: set QDRANT_MODE=url and fill in the following:
11
+ QDRANT_URL=https://your-cluster-id.region.aws.cloud.qdrant.io:6333
12
+ QDRANT_API_KEY=your_qdrant_cloud_api_key_here
13
+
14
+ # Server Configuration
15
+ SERVER_HOST=0.0.0.0
16
+ SERVER_PORT=8000
17
+ DEBUG=True
18
+
19
+ # Logging Configuration
20
+ LOG_LEVEL=INFO
app/backend/ARCHITECTURE.md CHANGED
@@ -460,8 +460,14 @@ RBAC Check:
460
 
461
  ### 3. **Why RBAC at Vector Store Level?**
462
  - Cannot be bypassed
463
- - Single source of truth
464
- - Efficient (filters at query time)
 
 
 
 
 
 
465
 
466
  ### 4. **Why Separate Input/Output Guards?**
467
  - Defense in depth
 
460
 
461
  ### 3. **Why RBAC at Vector Store Level?**
462
  - Cannot be bypassed
463
+ - **Retrieval Engine**: RBAC-aware vector search
464
+ - **Generation Engine**: Groq (Llama 3.3 70B)
465
+ - **Observability Layer**: Azure Monitor + OpenTelemetry manual spans
466
+ - **Security Layer**: Input/Output Guardrails
467
+ - Defense in depth
468
+ - Prevents malicious input
469
+ - Ensures answer quality
470
+ - Auditable (logged)
471
 
472
  ### 4. **Why Separate Input/Output Guards?**
473
  - Defense in depth
app/backend/main.py CHANGED
@@ -11,6 +11,8 @@ from typing import Optional
11
  from fastapi.responses import JSONResponse
12
  from fastapi.middleware.cors import CORSMiddleware
13
  from pydantic import BaseModel
 
 
14
  from pipeline.rag_pipeline import get_rag_pipeline
15
  from retrieval.user_auth import get_user_manager
16
  from vector_store import get_vector_store
@@ -24,6 +26,14 @@ logging.basicConfig(
24
  )
25
  logger = logging.getLogger(__name__)
26
 
 
 
 
 
 
 
 
 
27
 
28
  # ====================
29
  # REQUEST/RESPONSE MODELS
@@ -121,6 +131,14 @@ app.add_middleware(
121
  allow_headers=["*"],
122
  )
123
 
 
 
 
 
 
 
 
 
124
 
125
  # ====================
126
  # CHAT ENDPOINT
 
11
  from fastapi.responses import JSONResponse
12
  from fastapi.middleware.cors import CORSMiddleware
13
  from pydantic import BaseModel
14
+ from azure.monitor.opentelemetry import configure_azure_monitor
15
+ from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
16
  from pipeline.rag_pipeline import get_rag_pipeline
17
  from retrieval.user_auth import get_user_manager
18
  from vector_store import get_vector_store
 
26
  )
27
  logger = logging.getLogger(__name__)
28
 
29
+ # Initialize Azure Monitor Tracing (Must be done before app creation)
30
+ if os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING"):
31
+ logger.info("Initializing Azure Monitor Tracing...")
32
+ try:
33
+ configure_azure_monitor()
34
+ except Exception as e:
35
+ logger.error(f"Failed to initialize Azure Monitor Tracing: {str(e)}")
36
+
37
 
38
  # ====================
39
  # REQUEST/RESPONSE MODELS
 
131
  allow_headers=["*"],
132
  )
133
 
134
+ # Instrument FastAPI app
135
+ if os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING"):
136
+ try:
137
+ FastAPIInstrumentor.instrument_app(app)
138
+ logger.info("FastAPI application instrumented with OpenTelemetry")
139
+ except Exception as e:
140
+ logger.error(f"Failed to instrument FastAPI app: {str(e)}")
141
+
142
 
143
  # ====================
144
  # CHAT ENDPOINT
app/backend/pipeline/rag_pipeline.py CHANGED
@@ -14,6 +14,9 @@ from retrieval.user_auth import get_user_manager
14
  from guardrails.input_guards import get_input_guards
15
  from guardrails.output_guards import get_output_guards
16
  from config import LLM_CONFIG, RETRIEVAL_CONFIG
 
 
 
17
 
18
  logger = logging.getLogger(__name__)
19
 
@@ -90,193 +93,210 @@ class RAGPipeline:
90
 
91
  logger.info(f"Processing query from user role '{user_role}': {query_text[:100]}")
92
 
93
- # ====================
94
- # STEP 1: INPUT GUARDS
95
- # ====================
96
- logger.info("STEP 1: Input validation...")
97
-
98
- # Check rate limiting if user_id provided
99
- if user_id:
100
- is_under_limit, rate_warning = self.input_guards.check_rate_limit(user_id)
101
- if not is_under_limit:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  return RAGResponse(
103
- answer=rate_warning or "Rate limit exceeded",
104
  sources=[],
105
- route="rate_limited",
106
  user_role=user_role,
107
  accessible_collections=self.user_manager.get_user_accessible_collections(user_role),
108
- guardrail_flags=["rate_limit_exceeded"],
 
 
109
  )
110
-
111
- # Validate query for injection, off-topic, PII
112
- is_valid, rejection_reason, input_flags = self.input_guards.validate_query(
113
- query_text,
114
- user_role
115
- )
116
-
117
- if not is_valid:
118
- logger.warning(f"Query rejected by input guards: {rejection_reason}")
119
- return RAGResponse(
120
- answer=rejection_reason or "Query validation failed",
121
- sources=[],
122
- route="blocked_by_guardrails",
123
- user_role=user_role,
124
- accessible_collections=self.user_manager.get_user_accessible_collections(user_role),
125
- guardrail_flags=input_flags,
126
- guardrail_warnings=[rejection_reason] if rejection_reason else [],
127
- )
128
-
129
- metadata.guardrail_flags.extend(input_flags)
130
-
131
- # ====================
132
- # STEP 2: QUERY ROUTING
133
- # ====================
134
- logger.info("STEP 2: Semantic routing...")
135
-
136
- route_name, authorized_collections, denial_reason = self.router.route_query(
137
- query_text,
138
- user_role
139
- )
140
-
141
- metadata.route_selected = route_name
142
- metadata.collections_queried = authorized_collections
143
-
144
- # Check if RBAC denied this query
145
- if route_name == "denied":
146
- logger.warning(f"Query denied by RBAC: {denial_reason}")
147
- return RAGResponse(
148
- answer=denial_reason or "You don't have access to the requested information.",
149
- sources=[],
150
- route=route_name,
151
- user_role=user_role,
152
- accessible_collections=self.user_manager.get_user_accessible_collections(user_role),
153
- rbac_denied=True,
154
- rbac_reason=denial_reason,
155
- guardrail_flags=["rbac_denied"],
156
- )
157
-
158
- logger.info(f"Routed to: {route_name} → collections: {authorized_collections}")
159
-
160
- # ====================
161
- # STEP 3: RETRIEVAL
162
- # ====================
163
- logger.info("STEP 3: RBAC-enforced retrieval...")
164
-
165
- retrieval_result = self.retriever.retrieve(
166
- user_role=user_role,
167
- collections=authorized_collections,
168
- query_text=query_text,
169
- top_k=RETRIEVAL_CONFIG.get("top_k", 5),
170
- )
171
-
172
- if not retrieval_result.rbac_passed:
173
- logger.warning(f"Retrieval RBAC check failed: {retrieval_result.reason}")
174
- return RAGResponse(
175
- answer="Unable to retrieve documents due to access restrictions.",
176
- sources=[],
177
- route=route_name,
178
- user_role=user_role,
179
- accessible_collections=self.user_manager.get_user_accessible_collections(user_role),
180
- rbac_denied=True,
181
- rbac_reason=retrieval_result.reason,
182
- )
183
-
184
- chunks = retrieval_result.chunks
185
- metadata.chunks_retrieved = len(chunks)
186
-
187
- if not chunks:
188
- logger.info(f"No relevant documents found")
189
- return RAGResponse(
190
- answer="I couldn't find relevant information to answer your question.",
191
- sources=[],
192
- route=route_name,
193
- user_role=user_role,
194
- accessible_collections=self.user_manager.get_user_accessible_collections(user_role),
195
- guardrail_flags=["no_relevant_context"],
196
- )
197
-
198
- logger.info(f"Retrieved {len(chunks)} chunks")
199
-
200
- # ====================
201
- # STEP 4: LLM GENERATION
202
- # ====================
203
- logger.info("STEP 4: LLM generation...")
204
-
205
- # Build context from chunks
206
- context = self._build_context(chunks)
207
-
208
- # Generate answer
209
- answer = self._generate_answer(query_text, context, user_role)
210
-
211
- if not answer or not answer.strip():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
  return RAGResponse(
213
- answer="I wasn't able to generate a response for your question. Please try rephrasing or ask a different question.",
214
- sources=[],
215
  route=route_name,
216
  user_role=user_role,
217
  accessible_collections=self.user_manager.get_user_accessible_collections(user_role),
218
- guardrail_flags=["generation_failed"],
219
- )
220
-
221
- logger.info(f"Generated answer: {answer[:100]}...")
222
-
223
- # ====================
224
- # STEP 5: OUTPUT GUARDS
225
- # ====================
226
- logger.info("STEP 5: Output validation...")
227
-
228
- is_safe, output_warning, output_flags = self.output_guards.validate_response(
229
- answer,
230
- chunks,
231
- user_role,
232
- authorized_collections
233
- )
234
-
235
- metadata.guardrail_flags.extend(output_flags)
236
-
237
- # Append warning to answer if applicable
238
- if output_warning:
239
- answer = self.output_guards.append_warning_to_response(answer, output_warning)
240
-
241
- # ====================
242
- # BUILD SOURCES
243
- # ====================
244
- sources = []
245
- seen_sources = set()
246
-
247
- for chunk in chunks:
248
- if len(sources) >= 3:
249
- break
250
-
251
- source_key = (
252
- chunk.source_document,
253
- chunk.page_number or 1,
254
- chunk.section_title or ""
255
  )
256
-
257
- if source_key not in seen_sources:
258
- seen_sources.add(source_key)
259
- sources.append({
260
- "document": chunk.source_document,
261
- "page_number": chunk.page_number or 1,
262
- "section_title": chunk.section_title,
263
- })
264
-
265
- metadata.sources = [s["document"] for s in sources]
266
- metadata.answer = answer
267
-
268
- logger.info("Query processing complete")
269
-
270
- # Return final response
271
- return RAGResponse(
272
- answer=answer,
273
- sources=sources,
274
- route=route_name,
275
- user_role=user_role,
276
- accessible_collections=self.user_manager.get_user_accessible_collections(user_role),
277
- guardrail_flags=metadata.guardrail_flags,
278
- guardrail_warnings=[output_warning] if output_warning else [],
279
- )
280
 
281
  def _build_context(self, chunks: List) -> str:
282
  """
@@ -318,30 +338,43 @@ class RAGPipeline:
318
  Generated answer or None if error
319
  """
320
  try:
321
- prompt = self._build_prompt(query, context, user_role)
322
-
323
- response = self.llm_client.chat.completions.create(
324
- model=self.llm_model,
325
- messages=[
326
- {
327
- "role": "system",
328
- "content": (
329
- "You are a helpful assistant for FinSolve Technologies. "
330
- "Answer questions based ONLY on the provided context. "
331
- "If the context doesn't contain the answer, say so. "
332
- "Always cite your sources with document name and page number."
333
- ),
334
- },
335
- {
336
- "role": "user",
337
- "content": prompt,
338
- },
339
- ],
340
- temperature=self.llm_temperature,
341
- max_tokens=self.llm_max_tokens,
342
- )
343
-
344
- return response.choices[0].message.content
 
 
 
 
 
 
 
 
 
 
 
 
 
345
 
346
  except Exception as e:
347
  logger.error(f"Error generating answer with Groq: {str(e)}")
 
14
  from guardrails.input_guards import get_input_guards
15
  from guardrails.output_guards import get_output_guards
16
  from config import LLM_CONFIG, RETRIEVAL_CONFIG
17
+ from opentelemetry import trace
18
+
19
+ tracer = trace.get_tracer(__name__)
20
 
21
  logger = logging.getLogger(__name__)
22
 
 
93
 
94
  logger.info(f"Processing query from user role '{user_role}': {query_text[:100]}")
95
 
96
+ with tracer.start_as_current_span("rag_pipeline_process_query") as span:
97
+ span.set_attribute("user.role", user_role)
98
+ span.set_attribute("query.text", query_text)
99
+
100
+ # ====================
101
+ # STEP 1: INPUT GUARDS
102
+ # ====================
103
+ with tracer.start_as_current_span("stage_1_input_guards"):
104
+ logger.info("STEP 1: Input validation...")
105
+
106
+ # Check rate limiting if user_id provided
107
+ if user_id:
108
+ is_under_limit, rate_warning = self.input_guards.check_rate_limit(user_id)
109
+ if not is_under_limit:
110
+ return RAGResponse(
111
+ answer=rate_warning or "Rate limit exceeded",
112
+ sources=[],
113
+ route="rate_limited",
114
+ user_role=user_role,
115
+ accessible_collections=self.user_manager.get_user_accessible_collections(user_role),
116
+ guardrail_flags=["rate_limit_exceeded"],
117
+ )
118
+
119
+ # Validate query for injection, off-topic, PII
120
+ is_valid, rejection_reason, input_flags = self.input_guards.validate_query(
121
+ query_text,
122
+ user_role
123
+ )
124
+
125
+ if not is_valid:
126
+ logger.warning(f"Query rejected by input guards: {rejection_reason}")
127
+ return RAGResponse(
128
+ answer=rejection_reason or "Query validation failed",
129
+ sources=[],
130
+ route="blocked_by_guardrails",
131
+ user_role=user_role,
132
+ accessible_collections=self.user_manager.get_user_accessible_collections(user_role),
133
+ guardrail_flags=input_flags,
134
+ guardrail_warnings=[rejection_reason] if rejection_reason else [],
135
+ )
136
+
137
+ metadata.guardrail_flags.extend(input_flags)
138
+
139
+ # ====================
140
+ # STEP 2: QUERY ROUTING
141
+ # ====================
142
+ with tracer.start_as_current_span("stage_2_routing") as route_span:
143
+ logger.info("STEP 2: Semantic routing...")
144
+
145
+ route_name, authorized_collections, denial_reason = self.router.route_query(
146
+ query_text,
147
+ user_role
148
+ )
149
+
150
+ route_span.set_attribute("route.name", route_name)
151
+ route_span.set_attribute("route.authorized_collections", str(authorized_collections))
152
+
153
+ metadata.route_selected = route_name
154
+ metadata.collections_queried = authorized_collections
155
+
156
+ # Check if RBAC denied this query
157
+ if route_name == "denied":
158
+ logger.warning(f"Query denied by RBAC: {denial_reason}")
159
  return RAGResponse(
160
+ answer=denial_reason or "You don't have access to the requested information.",
161
  sources=[],
162
+ route=route_name,
163
  user_role=user_role,
164
  accessible_collections=self.user_manager.get_user_accessible_collections(user_role),
165
+ rbac_denied=True,
166
+ rbac_reason=denial_reason,
167
+ guardrail_flags=["rbac_denied"],
168
  )
169
+
170
+ logger.info(f"Routed to: {route_name} collections: {authorized_collections}")
171
+
172
+ # ====================
173
+ # STEP 3: RETRIEVAL
174
+ # ====================
175
+ with tracer.start_as_current_span("stage_3_retrieval") as retr_span:
176
+ logger.info("STEP 3: RBAC-enforced retrieval...")
177
+
178
+ retrieval_result = self.retriever.retrieve(
179
+ user_role=user_role,
180
+ collections=authorized_collections,
181
+ query_text=query_text,
182
+ top_k=RETRIEVAL_CONFIG.get("top_k", 5),
183
+ )
184
+
185
+ retr_span.set_attribute("retrieval.rbac_passed", retrieval_result.rbac_passed)
186
+ retr_span.set_attribute("retrieval.chunks_count", len(retrieval_result.chunks))
187
+
188
+ if not retrieval_result.rbac_passed:
189
+ logger.warning(f"Retrieval RBAC check failed: {retrieval_result.reason}")
190
+ return RAGResponse(
191
+ answer="Unable to retrieve documents due to access restrictions.",
192
+ sources=[],
193
+ route=route_name,
194
+ user_role=user_role,
195
+ accessible_collections=self.user_manager.get_user_accessible_collections(user_role),
196
+ rbac_denied=True,
197
+ rbac_reason=retrieval_result.reason,
198
+ )
199
+
200
+ chunks = retrieval_result.chunks
201
+ metadata.chunks_retrieved = len(chunks)
202
+
203
+ if not chunks:
204
+ logger.info(f"No relevant documents found")
205
+ return RAGResponse(
206
+ answer="I couldn't find relevant information to answer your question.",
207
+ sources=[],
208
+ route=route_name,
209
+ user_role=user_role,
210
+ accessible_collections=self.user_manager.get_user_accessible_collections(user_role),
211
+ guardrail_flags=["no_relevant_context"],
212
+ )
213
+
214
+ logger.info(f"Retrieved {len(chunks)} chunks")
215
+
216
+ # ====================
217
+ # STEP 4: LLM GENERATION
218
+ # ====================
219
+ with tracer.start_as_current_span("stage_4_generation") as gen_span:
220
+ logger.info("STEP 4: LLM generation...")
221
+
222
+ # Build context from chunks
223
+ context = self._build_context(chunks)
224
+
225
+ # Generate answer
226
+ answer = self._generate_answer(query_text, context, user_role)
227
+
228
+ gen_span.set_attribute("generation.successful", bool(answer))
229
+
230
+ if not answer or not answer.strip():
231
+ return RAGResponse(
232
+ answer="I wasn't able to generate a response for your question. Please try rephrasing or ask a different question.",
233
+ sources=[],
234
+ route=route_name,
235
+ user_role=user_role,
236
+ accessible_collections=self.user_manager.get_user_accessible_collections(user_role),
237
+ guardrail_flags=["generation_failed"],
238
+ )
239
+
240
+ logger.info(f"Generated answer: {answer[:100]}...")
241
+
242
+ # ====================
243
+ # STEP 5: OUTPUT GUARDS
244
+ # ====================
245
+ with tracer.start_as_current_span("stage_5_output_guards"):
246
+ logger.info("STEP 5: Output validation...")
247
+
248
+ is_safe, output_warning, output_flags = self.output_guards.validate_response(
249
+ answer,
250
+ chunks,
251
+ user_role,
252
+ authorized_collections
253
+ )
254
+
255
+ metadata.guardrail_flags.extend(output_flags)
256
+
257
+ # Append warning to answer if applicable
258
+ if output_warning:
259
+ answer = self.output_guards.append_warning_to_response(answer, output_warning)
260
+
261
+ # ====================
262
+ # BUILD SOURCES
263
+ # ====================
264
+ sources = []
265
+ seen_sources = set()
266
+
267
+ for chunk in chunks:
268
+ if len(sources) >= 3:
269
+ break
270
+
271
+ source_key = (
272
+ chunk.source_document,
273
+ chunk.page_number or 1,
274
+ chunk.section_title or ""
275
+ )
276
+
277
+ if source_key not in seen_sources:
278
+ seen_sources.add(source_key)
279
+ sources.append({
280
+ "document": chunk.source_document,
281
+ "page_number": chunk.page_number or 1,
282
+ "section_title": chunk.section_title,
283
+ })
284
+
285
+ metadata.sources = [s["document"] for s in sources]
286
+ metadata.answer = answer
287
+
288
+ logger.info("Query processing complete")
289
+
290
+ # Return final response
291
  return RAGResponse(
292
+ answer=answer,
293
+ sources=sources,
294
  route=route_name,
295
  user_role=user_role,
296
  accessible_collections=self.user_manager.get_user_accessible_collections(user_role),
297
+ guardrail_flags=metadata.guardrail_flags,
298
+ guardrail_warnings=[output_warning] if output_warning else [],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
299
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300
 
301
  def _build_context(self, chunks: List) -> str:
302
  """
 
338
  Generated answer or None if error
339
  """
340
  try:
341
+ with tracer.start_as_current_span("llm_generation") as span:
342
+ span.set_attribute("gen_ai.system", "groq")
343
+ span.set_attribute("gen_ai.request.model", self.llm_model)
344
+
345
+ # If enabled, record the prompt content
346
+ if os.getenv("AZURE_TRACING_GEN_AI_CONTENT_RECORDING_ENABLED", "false").lower() == "true":
347
+ span.set_attribute("gen_ai.content.prompt", context[:1000]) # Sample context
348
+
349
+ prompt = self._build_prompt(query, context, user_role)
350
+
351
+ response = self.llm_client.chat.completions.create(
352
+ model=self.llm_model,
353
+ messages=[
354
+ {
355
+ "role": "system",
356
+ "content": (
357
+ "You are a helpful assistant for FinSolve Technologies. "
358
+ "Answer questions based ONLY on the provided context. "
359
+ "If the context doesn't contain the answer, say so. "
360
+ "Always cite your sources with document name and page number."
361
+ ),
362
+ },
363
+ {
364
+ "role": "user",
365
+ "content": prompt,
366
+ },
367
+ ],
368
+ temperature=self.llm_temperature,
369
+ max_tokens=self.llm_max_tokens,
370
+ )
371
+
372
+ answer = response.choices[0].message.content
373
+
374
+ if answer and os.getenv("AZURE_TRACING_GEN_AI_CONTENT_RECORDING_ENABLED", "false").lower() == "true":
375
+ span.set_attribute("gen_ai.content.completion", answer)
376
+
377
+ return answer
378
 
379
  except Exception as e:
380
  logger.error(f"Error generating answer with Groq: {str(e)}")
app/backend/requirements.txt CHANGED
@@ -8,6 +8,16 @@ sentence-transformers>=2.2.0
8
  docling>=2.0.0
9
  qdrant-client>=1.7.0
10
  semantic-router>=0.0.40
 
 
 
 
 
 
 
 
 
 
11
  langchain>=0.1.0
12
  ragas>=0.1.0
13
  python-multipart>=0.0.6
@@ -17,3 +27,6 @@ pytest>=7.0.0
17
  docling-hierarchical-pdf==0.1.6
18
  transformers>=4.40.0
19
  gunicorn
 
 
 
 
8
  docling>=2.0.0
9
  qdrant-client>=1.7.0
10
  semantic-router>=0.0.40
11
+ fastapi>=0.115.0
12
+ uvicorn>=0.30.0
13
+ pydantic>=2.0.0
14
+ pydantic-settings>=2.0.0
15
+ python-dotenv>=1.0.0
16
+ groq>=0.9.0
17
+ sentence-transformers>=2.2.0
18
+ docling>=2.0.0
19
+ qdrant-client>=1.7.0
20
+ semantic-router>=0.0.40
21
  langchain>=0.1.0
22
  ragas>=0.1.0
23
  python-multipart>=0.0.6
 
27
  docling-hierarchical-pdf==0.1.6
28
  transformers>=4.40.0
29
  gunicorn
30
+ azure-monitor-opentelemetry
31
+ opentelemetry-instrumentation-fastapi
32
+ azure-identity