chinmayjha commited on
Commit
0711be9
·
1 Parent(s): 8099e26

Improve source parsing and answer formatting

Browse files

- Fix source deduplication to use document ID instead of title+date
- Extract all 5 sources instead of just 1 when multiple documents have same title/date
- Add rich source information including key findings, marketing insights, and quotes
- Improve answer formatting to properly parse JSON and convert \n to line breaks
- Fix import error in tools/app.py
- Enhanced UI display with better source cards and formatting

configs/compute_rag_vector_index_openai_contextual_simple.yaml CHANGED
@@ -1,7 +1,7 @@
1
  parameters:
2
  extract_collection_name: raw
3
  fetch_limit: 200
4
- load_collection_name: rag
5
  content_quality_score_threshold: 0.6
6
  retriever_type: contextual
7
  embedding_model_id: text-embedding-3-small
 
1
  parameters:
2
  extract_collection_name: raw
3
  fetch_limit: 200
4
+ load_collection_name: rag_insights_test
5
  content_quality_score_threshold: 0.6
6
  retriever_type: contextual
7
  embedding_model_id: text-embedding-3-small
src/second_brain_online/application/ui/custom_gradio_ui.py CHANGED
@@ -57,15 +57,13 @@ class CustomGradioUI:
57
  gr.Markdown("# 🧠 Second Brain AI Assistant")
58
  gr.Markdown("Ask questions about your documents and get AI-powered insights with source attribution.")
59
 
60
- with gr.Row():
61
- with gr.Column(scale=4):
62
- self.query_input = gr.Textbox(
63
- label="Ask a question",
64
- placeholder="What pricing objections were raised in the meetings?",
65
- lines=2
66
- )
67
- with gr.Column(scale=1):
68
- self.submit_btn = gr.Button("Ask", variant="primary", size="lg")
69
 
70
  with gr.Row():
71
  with gr.Column():
@@ -103,14 +101,25 @@ class CustomGradioUI:
103
  # Run the agent
104
  result = self.agent.run(query)
105
 
106
- # Parse the result
107
- answer, sources, tools_used = self.parse_agent_response(result)
 
108
 
109
  # Debug information
110
- print(f"DEBUG - Raw result: {str(result)[:200]}...")
111
- print(f"DEBUG - Parsed answer: {answer[:100]}...")
112
- print(f"DEBUG - Sources found: {len(sources)}")
113
- print(f"DEBUG - Tools found: {tools_used}")
 
 
 
 
 
 
 
 
 
 
114
 
115
  # Format outputs
116
  answer_html = self.format_answer(answer)
@@ -124,7 +133,7 @@ class CustomGradioUI:
124
  error_msg = f"<div style='color: #dc3545; padding: 12px; border: 1px solid #f5c6cb; border-radius: 4px; background-color: #f8d7da;'>Error: {str(e)}</div>"
125
  return error_msg, "", "", str(e)
126
 
127
- def parse_agent_response(self, result: Any) -> Tuple[str, List[Dict], List[str]]:
128
  """Parse the agent response to extract answer, sources, and tools used."""
129
  answer = ""
130
  sources = []
@@ -133,20 +142,7 @@ class CustomGradioUI:
133
  # Convert result to string if it's not already
134
  result_str = str(result)
135
 
136
- # Extract tool usage from the result first
137
- # Pattern 1: 🛠️ Used tool toolname
138
- tool_pattern1 = r'🛠️ Used tool (\w+)'
139
- tool_matches1 = re.findall(tool_pattern1, result_str)
140
-
141
- # Pattern 2: Calling tool: 'toolname'
142
- tool_pattern2 = r"Calling tool:\s*'([^']+)'"
143
- tool_matches2 = re.findall(tool_pattern2, result_str)
144
-
145
- # Combine both patterns
146
- all_tool_matches = tool_matches1 + tool_matches2
147
- tools_used = list(set(all_tool_matches)) # Remove duplicates
148
-
149
- # Try multiple patterns to extract the answer
150
  # Pattern 1: JSON format with "answer" key
151
  json_match = re.search(r'{"answer":\s*"([^"]+)"}', result_str)
152
  if json_match:
@@ -166,53 +162,106 @@ class CustomGradioUI:
166
  # Pattern 3: Use the entire result as answer if no specific pattern matches
167
  answer = result_str
168
 
169
- # Extract sources from the answer text using multiple patterns
170
- # Pattern 1: (Document: "Title", Date)
171
- source_pattern1 = r'\(Document:\s*"([^"]+)",\s*([^)]+)\)'
172
- source_matches1 = re.findall(source_pattern1, answer)
173
-
174
- # Pattern 2: (Document: Title, Date) - without quotes
175
- source_pattern2 = r'\(Document:\s*([^,]+),\s*([^)]+)\)'
176
- source_matches2 = re.findall(source_pattern2, answer)
177
-
178
- # Pattern 3: (Document 1, Date) - numbered format
179
- source_pattern3 = r'\(Document\s+(\d+),\s*([^)]+)\)'
180
- source_matches3 = re.findall(source_pattern3, answer)
181
-
182
- # Pattern 4: (from "Title" on Date) - new format seen in output
183
- source_pattern4 = r'\(from\s+"([^"]+)"\s+on\s+([^)]+)\)'
184
- source_matches4 = re.findall(source_pattern4, answer)
185
-
186
- # Pattern 5: (from "Title" on Date) - without quotes
187
- source_pattern5 = r'\(from\s+([^"]+)\s+on\s+([^)]+)\)'
188
- source_matches5 = re.findall(source_pattern5, answer)
189
-
190
- # Combine all patterns
191
- all_source_matches = source_matches1 + source_matches2 + source_matches3 + source_matches4 + source_matches5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
 
193
- for doc_title, doc_date in all_source_matches:
194
- # Clean up the title and date
195
- clean_title = doc_title.strip().strip('"')
196
- clean_date = doc_date.strip()
 
 
197
 
198
- # Handle numbered documents (Document 1, Document 2, etc.)
199
- if clean_title.isdigit():
200
- clean_title = f"Document {clean_title}"
201
 
202
- sources.append({
203
- "title": clean_title,
204
- "date": clean_date
205
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
 
207
- # Remove duplicates based on title and date
208
  unique_sources = []
209
  seen = set()
210
  for source in sources:
211
- key = (source["title"], source["date"])
 
212
  if key not in seen:
213
  seen.add(key)
214
  unique_sources.append(source)
215
 
 
 
 
216
  return answer, unique_sources, tools_used
217
 
218
  def format_answer(self, answer: str) -> str:
@@ -220,21 +269,33 @@ class CustomGradioUI:
220
  if not answer:
221
  return "<div class='answer-section'><p>No answer provided.</p></div>"
222
 
 
 
 
 
 
 
 
 
 
 
 
223
  # Remove source references from the answer text for cleaner display
224
  answer = re.sub(r'\(Document:[^)]+\)', '', answer)
225
 
226
- # Clean up extra whitespace
227
- answer = re.sub(r'\s+', ' ', answer).strip()
 
228
 
229
  # Format numbered lists and bullet points
230
- answer = re.sub(r'\n\s*\d+\.\s*', '<br><br><strong>', answer) # Numbered lists
231
- answer = re.sub(r'\n\s*•\s*', '<br>• ', answer) # Bullet points
232
- answer = re.sub(r'\n\s*-\s*', '<br>• ', answer) # Dash points
233
 
234
  # Format bold text (markdown style)
235
  answer = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', answer)
236
 
237
- # Format line breaks
238
  answer = answer.replace('\n', '<br>')
239
 
240
  # Clean up multiple line breaks
@@ -248,19 +309,59 @@ class CustomGradioUI:
248
  """
249
 
250
  def format_sources(self, sources: List[Dict]) -> str:
251
- """Format the sources with proper HTML structure."""
252
  if not sources:
253
  return "<div><h3>📚 Sources</h3><p>No sources found.</p></div>"
254
 
255
  sources_html = "<div><h3>📚 Sources</h3>"
256
 
257
  for i, source in enumerate(sources, 1):
 
 
 
 
 
 
 
258
  sources_html += f"""
259
- <div class='source-card'>
260
- <div class='source-title'>{i}. {source['title']}</div>
261
- <div class='source-date'>📅 {source['date']}</div>
262
- </div>
 
 
263
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
 
265
  sources_html += "</div>"
266
  return sources_html
 
57
  gr.Markdown("# 🧠 Second Brain AI Assistant")
58
  gr.Markdown("Ask questions about your documents and get AI-powered insights with source attribution.")
59
 
60
+ self.query_input = gr.Textbox(
61
+ label="Ask a question",
62
+ placeholder="What pricing objections were raised in the meetings?",
63
+ lines=2
64
+ )
65
+
66
+ self.submit_btn = gr.Button("Ask", variant="primary", size="lg")
 
 
67
 
68
  with gr.Row():
69
  with gr.Column():
 
101
  # Run the agent
102
  result = self.agent.run(query)
103
 
104
+ # Parse the result with agent logs
105
+ agent_logs = getattr(self.agent, 'logs', []) if hasattr(self.agent, 'logs') else []
106
+ answer, sources, tools_used = self.parse_agent_response(result, agent_logs)
107
 
108
  # Debug information
109
+ print("\n" + "="*80)
110
+ print("DEBUG: RAW AGENT RESULT")
111
+ print("="*80)
112
+ print(f"Type: {type(result)}")
113
+ print(f"Full Content:\n{result}")
114
+ print("="*80)
115
+
116
+ print("\n" + "="*80)
117
+ print("DEBUG: PARSED RESULTS")
118
+ print("="*80)
119
+ print(f"Answer: {answer}")
120
+ print(f"Sources ({len(sources)}): {sources}")
121
+ print(f"Tools Used: {tools_used}")
122
+ print("="*80)
123
 
124
  # Format outputs
125
  answer_html = self.format_answer(answer)
 
133
  error_msg = f"<div style='color: #dc3545; padding: 12px; border: 1px solid #f5c6cb; border-radius: 4px; background-color: #f8d7da;'>Error: {str(e)}</div>"
134
  return error_msg, "", "", str(e)
135
 
136
+ def parse_agent_response(self, result: Any, agent_logs: List = None) -> Tuple[str, List[Dict], List[str]]:
137
  """Parse the agent response to extract answer, sources, and tools used."""
138
  answer = ""
139
  sources = []
 
142
  # Convert result to string if it's not already
143
  result_str = str(result)
144
 
145
+ # Extract the answer from the result
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  # Pattern 1: JSON format with "answer" key
147
  json_match = re.search(r'{"answer":\s*"([^"]+)"}', result_str)
148
  if json_match:
 
162
  # Pattern 3: Use the entire result as answer if no specific pattern matches
163
  answer = result_str
164
 
165
+ # If we have agent logs, extract tools and sources from them
166
+ if agent_logs:
167
+ for step in agent_logs:
168
+ # Extract tool calls
169
+ if hasattr(step, 'tool_calls') and step.tool_calls:
170
+ for tool_call in step.tool_calls:
171
+ if hasattr(tool_call, 'name'):
172
+ tools_used.append(tool_call.name)
173
+
174
+ # Extract sources from observations
175
+ if hasattr(step, 'observations') and step.observations:
176
+ # Look for complete document blocks with all content
177
+ document_pattern = r'<document id="(\d+)">\s*<title>(.*?)</title>\s*<date>(.*?)</date>\s*<contextual_summary>(.*?)</contextual_summary>\s*<marketing_insights>(.*?)</marketing_insights>\s*<content>(.*?)</content>'
178
+ document_matches = re.findall(document_pattern, step.observations, re.DOTALL)
179
+
180
+ for doc_id, doc_title, doc_date, contextual_summary, marketing_insights, content in document_matches:
181
+ # Clean up the basic fields
182
+ clean_title = doc_title.strip()
183
+ clean_date = doc_date.strip()
184
+ clean_summary = contextual_summary.strip()
185
+
186
+ # Extract key findings from marketing insights
187
+ key_findings = []
188
+ key_findings_pattern = r'<key_findings>(.*?)</key_findings>'
189
+ key_findings_match = re.search(key_findings_pattern, marketing_insights, re.DOTALL)
190
+ if key_findings_match:
191
+ key_findings_text = key_findings_match.group(1).strip()
192
+ # Split by lines and clean up
193
+ key_findings = [line.strip() for line in key_findings_text.split('\n') if line.strip() and line.strip().startswith('-')]
194
+
195
+ # Extract quotes from marketing insights
196
+ quotes = []
197
+ quotes_pattern = r'<quotes>(.*?)</quotes>'
198
+ quotes_match = re.search(quotes_pattern, marketing_insights, re.DOTALL)
199
+ if quotes_match:
200
+ quotes_text = quotes_match.group(1).strip()
201
+ # Split by lines and clean up
202
+ quotes = [line.strip() for line in quotes_text.split('\n') if line.strip() and line.strip().startswith('-')]
203
+
204
+ sources.append({
205
+ "id": doc_id,
206
+ "title": clean_title,
207
+ "date": clean_date,
208
+ "summary": clean_summary,
209
+ "key_findings": key_findings,
210
+ "quotes": quotes
211
+ })
212
 
213
+ # Fallback: Try to extract from result string if no logs provided
214
+ if not agent_logs:
215
+ # Extract tool usage from the result first
216
+ # Pattern 1: 🛠️ Used tool toolname
217
+ tool_pattern1 = r'🛠️ Used tool (\w+)'
218
+ tool_matches1 = re.findall(tool_pattern1, result_str)
219
 
220
+ # Pattern 2: Calling tool: 'toolname' (with single quotes)
221
+ tool_pattern2 = r"Calling tool:\s*'([^']+)'"
222
+ tool_matches2 = re.findall(tool_pattern2, result_str)
223
 
224
+ # Pattern 3: Calling tool: 'toolname' (with double quotes)
225
+ tool_pattern3 = r'Calling tool:\s*"([^"]+)"'
226
+ tool_matches3 = re.findall(tool_pattern3, result_str)
227
+
228
+ # Pattern 4: Calling tool: toolname (without quotes)
229
+ tool_pattern4 = r'Calling tool:\s*([a-zA-Z_][a-zA-Z0-9_]*)'
230
+ tool_matches4 = re.findall(tool_pattern4, result_str)
231
+
232
+ # Combine all patterns
233
+ all_tool_matches = tool_matches1 + tool_matches2 + tool_matches3 + tool_matches4
234
+ tools_used = list(set(all_tool_matches)) # Remove duplicates
235
+
236
+ # Extract sources from the structured search_results format
237
+ # Look for <document> tags in the search results
238
+ document_pattern = r'<document id="(\d+)">\s*<title>(.*?)</title>\s*<date>(.*?)</date>'
239
+ document_matches = re.findall(document_pattern, result_str, re.DOTALL)
240
+
241
+ for doc_id, doc_title, doc_date in document_matches:
242
+ # Clean up the title and date
243
+ clean_title = doc_title.strip()
244
+ clean_date = doc_date.strip()
245
+
246
+ sources.append({
247
+ "id": doc_id,
248
+ "title": clean_title,
249
+ "date": clean_date
250
+ })
251
 
252
+ # Remove duplicates based on document ID (keep all unique documents)
253
  unique_sources = []
254
  seen = set()
255
  for source in sources:
256
+ # Use document ID as the unique key, fallback to title+date if no ID
257
+ key = source.get("id", f"{source['title']}_{source['date']}")
258
  if key not in seen:
259
  seen.add(key)
260
  unique_sources.append(source)
261
 
262
+ # Remove duplicate tools
263
+ tools_used = list(set(tools_used))
264
+
265
  return answer, unique_sources, tools_used
266
 
267
  def format_answer(self, answer: str) -> str:
 
269
  if not answer:
270
  return "<div class='answer-section'><p>No answer provided.</p></div>"
271
 
272
+ # Check if the answer is a JSON string and extract the actual answer
273
+ if answer.strip().startswith('{"answer":') and answer.strip().endswith('}'):
274
+ try:
275
+ import json
276
+ answer_data = json.loads(answer)
277
+ if isinstance(answer_data, dict) and 'answer' in answer_data:
278
+ answer = answer_data['answer']
279
+ except (json.JSONDecodeError, KeyError):
280
+ # If JSON parsing fails, use the original answer
281
+ pass
282
+
283
  # Remove source references from the answer text for cleaner display
284
  answer = re.sub(r'\(Document:[^)]+\)', '', answer)
285
 
286
+ # Clean up extra whitespace but preserve intentional line breaks
287
+ answer = re.sub(r'[ \t]+', ' ', answer) # Replace multiple spaces/tabs with single space
288
+ answer = re.sub(r' *\n *', '\n', answer) # Clean up spaces around newlines
289
 
290
  # Format numbered lists and bullet points
291
+ answer = re.sub(r'\n\s*\d+\.\s*', '\n\n<strong>', answer) # Numbered lists
292
+ answer = re.sub(r'\n\s*•\s*', '\n• ', answer) # Bullet points
293
+ answer = re.sub(r'\n\s*-\s*', '\n• ', answer) # Dash points
294
 
295
  # Format bold text (markdown style)
296
  answer = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', answer)
297
 
298
+ # Convert line breaks to HTML
299
  answer = answer.replace('\n', '<br>')
300
 
301
  # Clean up multiple line breaks
 
309
  """
310
 
311
  def format_sources(self, sources: List[Dict]) -> str:
312
+ """Format the sources with rich information including key findings and marketing insights."""
313
  if not sources:
314
  return "<div><h3>📚 Sources</h3><p>No sources found.</p></div>"
315
 
316
  sources_html = "<div><h3>📚 Sources</h3>"
317
 
318
  for i, source in enumerate(sources, 1):
319
+ title = source.get("title", "Unknown")
320
+ date = source.get("date", "Unknown")
321
+ doc_id = source.get("id", "")
322
+ summary = source.get("summary", "")
323
+ key_findings = source.get("key_findings", [])
324
+ quotes = source.get("quotes", [])
325
+
326
  sources_html += f"""
327
+ <div class='source-card' style='margin-bottom: 20px; padding: 15px; border: 1px solid #e0e0e0; border-radius: 8px; background-color: #f9f9f9;'>
328
+ <div class='source-title' style='font-weight: bold; font-size: 16px; margin-bottom: 8px;'>{i}. {title}</div>
329
+ <div class='source-meta' style='color: #666; margin-bottom: 10px;'>
330
+ 📅 {date}
331
+ {f" | ID: {doc_id}" if doc_id else ""}
332
+ </div>
333
  """
334
+
335
+ if summary:
336
+ sources_html += f"""
337
+ <div class='source-summary' style='margin-bottom: 10px;'>
338
+ <strong>Summary:</strong> {summary}
339
+ </div>
340
+ """
341
+
342
+ if key_findings:
343
+ sources_html += """
344
+ <div class='source-findings' style='margin-bottom: 10px;'>
345
+ <strong>Key Findings:</strong>
346
+ <ul style='margin: 5px 0; padding-left: 20px;'>
347
+ """
348
+ for finding in key_findings:
349
+ clean_finding = finding.lstrip('- ').strip()
350
+ sources_html += f"<li style='margin-bottom: 3px;'>{clean_finding}</li>"
351
+ sources_html += "</ul></div>"
352
+
353
+ if quotes:
354
+ sources_html += """
355
+ <div class='source-quotes' style='margin-bottom: 10px;'>
356
+ <strong>Key Quotes:</strong>
357
+ <ul style='margin: 5px 0; padding-left: 20px;'>
358
+ """
359
+ for quote in quotes:
360
+ clean_quote = quote.lstrip('- ').strip()
361
+ sources_html += f"<li style='margin-bottom: 3px; font-style: italic; color: #555;'>{clean_quote}</li>"
362
+ sources_html += "</ul></div>"
363
+
364
+ sources_html += "</div>"
365
 
366
  sources_html += "</div>"
367
  return sources_html
src/second_brain_online/config.py CHANGED
@@ -15,7 +15,7 @@ class Settings(BaseSettings):
15
 
16
  # --- Comet ML & Opik Configuration ---
17
  COMET_API_KEY: str | None = Field(
18
- default=None, description="API key for Comet ML and Opik services."
19
  )
20
  COMET_PROJECT: str = Field(
21
  default="second_brain_course",
@@ -44,11 +44,11 @@ class Settings(BaseSettings):
44
  description="Name of the MongoDB database.",
45
  )
46
  MONGODB_COLLECTION_NAME: str = Field(
47
- default="rag",
48
  description="Name of the MongoDB collection for RAG documents.",
49
  )
50
  MONGODB_URI: str = Field(
51
- default="mongodb+srv://contextdb:HOqIgSH01CoEiMb1@cluster0.d9cmff.mongodb.net/",
52
  description="Connection URI for the MongoDB Atlas instance.",
53
  )
54
 
 
15
 
16
  # --- Comet ML & Opik Configuration ---
17
  COMET_API_KEY: str | None = Field(
18
+ default="yPmLa7W6QyBODw1Pnfg9jqr7E", description="API key for Comet ML and Opik services."
19
  )
20
  COMET_PROJECT: str = Field(
21
  default="second_brain_course",
 
44
  description="Name of the MongoDB database.",
45
  )
46
  MONGODB_COLLECTION_NAME: str = Field(
47
+ default="rag_insights_test",
48
  description="Name of the MongoDB collection for RAG documents.",
49
  )
50
  MONGODB_URI: str = Field(
51
+ default="mongodb+srv://keshavchhaparia:bUSBXeVCGWDyQhDG@saaslabs.awtivxf.mongodb.net/?retryWrites=true&w=majority&appName=saaslabs",
52
  description="Connection URI for the MongoDB Atlas instance.",
53
  )
54
 
tools/app.py CHANGED
@@ -3,7 +3,7 @@ from pathlib import Path
3
  import click
4
 
5
  from second_brain_online.application.agents import get_agent
6
- from second_brain_online.application.ui import CustomGradioUI
7
 
8
 
9
  @click.command()
@@ -43,7 +43,61 @@ def main(retriever_config_path: Path, ui: bool, query: str) -> None:
43
 
44
  result = agent.run(query)
45
 
46
- print(result)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
 
49
  if __name__ == "__main__":
 
3
  import click
4
 
5
  from second_brain_online.application.agents import get_agent
6
+ from second_brain_online.application.ui.custom_gradio_ui import CustomGradioUI
7
 
8
 
9
  @click.command()
 
43
 
44
  result = agent.run(query)
45
 
46
+ # DEBUG: Print raw result
47
+ print("\n" + "="*80)
48
+ print("DEBUG: RAW AGENT RESULT")
49
+ print("="*80)
50
+ print(f"Type: {type(result)}")
51
+ print(f"Full Content:\n{result}")
52
+ print("="*80)
53
+
54
+ # DEBUG: Check agent object attributes
55
+ print("\n" + "="*80)
56
+ print("DEBUG: AGENT OBJECT ATTRIBUTES")
57
+ print("="*80)
58
+ print(f"Agent type: {type(agent)}")
59
+ print(f"Agent attributes: {dir(agent)}")
60
+ if hasattr(agent, '_AgentWrapper__agent'):
61
+ actual_agent = agent._AgentWrapper__agent
62
+ print(f"Actual agent type: {type(actual_agent)}")
63
+ print(f"Actual agent attributes: {dir(actual_agent)}")
64
+ if hasattr(actual_agent, 'conversation_history'):
65
+ print(f"Conversation history: {actual_agent.conversation_history}")
66
+ if hasattr(actual_agent, 'messages'):
67
+ print(f"Messages: {actual_agent.messages}")
68
+ if hasattr(actual_agent, 'logs'):
69
+ print(f"Logs: {actual_agent.logs}")
70
+ if hasattr(actual_agent, 'state'):
71
+ print(f"State: {actual_agent.state}")
72
+ print("="*80)
73
+
74
+ # Parse the result using the same logic as the UI
75
+ ui_instance = CustomGradioUI(None) # We don't need the agent for parsing
76
+
77
+ # Get agent logs if available
78
+ agent_logs = []
79
+ if hasattr(agent, '_AgentWrapper__agent'):
80
+ actual_agent = agent._AgentWrapper__agent
81
+ if hasattr(actual_agent, 'logs'):
82
+ agent_logs = actual_agent.logs
83
+
84
+ answer, sources, tools_used = ui_instance.parse_agent_response(result, agent_logs)
85
+
86
+ print("\n" + "="*80)
87
+ print("DEBUG: PARSED RESULTS")
88
+ print("="*80)
89
+ print(f"Answer: {answer}")
90
+ print(f"Sources ({len(sources)}): {sources}")
91
+ print(f"Tools Used: {tools_used}")
92
+ print("="*80)
93
+
94
+ print("\n" + "="*80)
95
+ print("FINAL OUTPUT")
96
+ print("="*80)
97
+
98
+ # Format the answer for better display
99
+ formatted_answer = ui_instance.format_answer(answer)
100
+ print(formatted_answer)
101
 
102
 
103
  if __name__ == "__main__":