Isateles commited on
Commit
e01c471
·
1 Parent(s): 7adb281

Updated agent

Browse files
Files changed (6) hide show
  1. README.md +44 -6
  2. app.py +195 -523
  3. requirements.txt +12 -84
  4. retriever.py +243 -428
  5. test_hf_space.py +297 -0
  6. tools.py +189 -509
README.md CHANGED
@@ -1,13 +1,51 @@
1
  ---
2
- title: Isadora Final Assignment
3
- emoji: 🕵🏻‍♂️
4
- colorFrom: indigo
5
- colorTo: indigo
6
  sdk: gradio
7
  sdk_version: 5.25.2
8
  app_file: app.py
9
  pinned: false
10
  hf_oauth: true
11
- # optional, default duration is 8 hours/480 minutes. Max duration is 30 days/43200 minutes.
12
  hf_oauth_expiration_minutes: 480
13
- ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: My GAIA Agent - Final Project
3
+ emoji: 🤖
4
+ colorFrom: blue
5
+ colorTo: green
6
  sdk: gradio
7
  sdk_version: 5.25.2
8
  app_file: app.py
9
  pinned: false
10
  hf_oauth: true
 
11
  hf_oauth_expiration_minutes: 480
12
+ ---
13
+
14
+ # My GAIA Agent - Final Course Project
15
+
16
+ This is my submission for the AI Agents course. I built an agent that can hopefully pass the GAIA benchmark with 30%+ score to get my certificate!
17
+
18
+ ## What My Agent Does
19
+
20
+ My agent combines everything I learned in the course:
21
+
22
+ - **🔍 Web Search**: Uses DuckDuckGo to find current information
23
+ - **🧮 Calculator**: Does math calculations (super important for GAIA!)
24
+ - **📊 File Analysis**: Can analyze CSV files and other data
25
+ - **👥 Persona Database**: RAG system with vector search over persona descriptions
26
+ - **🤖 Agent Workflow**: Uses LlamaIndex AgentWorkflow like we learned in class
27
+
28
+ ## How to Use
29
+
30
+ 1. **Login** with your HuggingFace account using the button below
31
+ 2. **Click "Run GAIA Evaluation"** and wait (takes 5-10 minutes)
32
+ 3. **See your results** and hopefully pass with 30%+!
33
+
34
+ ## Technical Details
35
+
36
+ - **LLM**: OpenAI GPT-4o-mini (primary) or HuggingFace Qwen2.5 (fallback)
37
+ - **Vector DB**: ChromaDB with in-memory storage for HF Spaces
38
+ - **Embeddings**: BAAI/bge-small-en-v1.5
39
+ - **Agent**: LlamaIndex AgentWorkflow
40
+ - **Interface**: Gradio web app
41
+
42
+ ## Setup
43
+
44
+ The Space needs either:
45
+ - `OPENAI_API_KEY` (recommended for better performance)
46
+ - `HF_TOKEN` (free fallback option)
47
+
48
+ Set these in the Space's Repository secrets.
49
+
50
+ ---
51
+
app.py CHANGED
@@ -1,27 +1,14 @@
1
  """
2
- app.py - GAIA Benchmark Agent Application
3
 
4
- This is the main application file that brings together:
5
- 1. Tools from tools.py (web search, calculator, file analysis)
6
- 2. RAG system from retriever.py (guest database)
7
- 3. LLM integration with fallback options
8
- 4. Agent workflow for handling GAIA questions
9
- 5. Gradio interface for submission to the GAIA benchmark
10
 
11
- The goal is to achieve 30%+ score on GAIA benchmark questions to earn the course certificate.
12
-
13
- How it works:
14
- 1. User logs in with HuggingFace account
15
- 2. System fetches GAIA questions from the evaluation API
16
- 3. Our agent processes each question using its tools
17
- 4. Answers are submitted and scored
18
- 5. Results are displayed with pass/fail status
19
-
20
- Key design decisions:
21
- - Modular architecture: tools and retriever in separate files
22
- - Robust error handling: graceful failures with logging
23
- - API key flexibility: OpenAI (best) or HuggingFace (fallback)
24
- - GAIA-optimized: focused on accuracy over speed
25
  """
26
 
27
  import os
@@ -30,679 +17,364 @@ import requests
30
  import pandas as pd
31
  import asyncio
32
  import logging
33
- from typing import List, Dict, Any, Optional
34
 
35
- # Setup comprehensive logging
36
- logging.basicConfig(
37
- level=logging.INFO,
38
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
39
- )
40
  logger = logging.getLogger(__name__)
41
 
42
- # ============================================================================
43
- # CONSTANTS AND CONFIGURATION
44
- # ============================================================================
45
-
46
- # GAIA evaluation API endpoint
47
- DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
48
-
49
- # Required score to pass the course
50
- PASSING_SCORE = 30 # 30% minimum to earn certificate
51
 
52
- # ============================================================================
53
- # LLM SETUP WITH FALLBACK OPTIONS
54
- # ============================================================================
55
-
56
- def create_llm():
57
  """
58
- Create an LLM (Large Language Model) with fallback options.
59
-
60
- Priority order:
61
- 1. OpenAI GPT-4 (best performance for GAIA)
62
- 2. HuggingFace Qwen model (free alternative)
63
-
64
- Why this order:
65
- - OpenAI models generally perform better on GAIA benchmark
66
- - HuggingFace provides free alternative for those without OpenAI credits
67
- - Fallback ensures the agent works regardless of available keys
68
-
69
- API Keys Setup:
70
- - Go to your HuggingFace Space settings
71
- - Add "Repository secrets"
72
- - Set OPENAI_API_KEY (recommended) and/or HF_TOKEN
73
-
74
- Returns:
75
- LLM: Configured language model ready for use
76
-
77
- Raises:
78
- RuntimeError: If no API keys are available
79
  """
80
- logger.info("Initializing LLM with fallback options...")
81
 
82
- # Try OpenAI first (recommended for GAIA performance)
83
  openai_key = os.getenv("OPENAI_API_KEY")
84
  if openai_key:
85
  try:
86
  from llama_index.llms.openai import OpenAI
87
-
88
  llm = OpenAI(
89
  api_key=openai_key,
90
- model="gpt-4o-mini", # Good balance of cost and performance
91
- max_tokens=1024, # Reasonable limit for GAIA answers
92
- temperature=0.1 # Low temperature for more consistent, factual responses
93
  )
94
-
95
- logger.info("✅ Successfully initialized OpenAI LLM")
96
  return llm
97
-
98
- except ImportError:
99
- logger.warning("❌ OpenAI library not available, trying HuggingFace...")
100
  except Exception as e:
101
- logger.warning(f"OpenAI initialization failed: {e}, trying HuggingFace...")
102
- else:
103
- logger.info("ℹ️ No OPENAI_API_KEY found, trying HuggingFace...")
104
 
105
- # Fallback to HuggingFace
106
  hf_token = os.getenv("HF_TOKEN")
107
  if hf_token:
108
  try:
109
  from llama_index.llms.huggingface_api import HuggingFaceInferenceAPI
110
-
111
  llm = HuggingFaceInferenceAPI(
112
- model_name="Qwen/Qwen2.5-Coder-32B-Instruct", # Good open-source model
113
  token=hf_token,
114
- max_new_tokens=512, # Limit for response length
115
- temperature=0.1, # Low temperature for consistency
116
- context_window=8192 # Context window size
117
  )
118
-
119
- logger.info("✅ Successfully initialized HuggingFace LLM")
120
  return llm
121
-
122
- except ImportError:
123
- logger.error("❌ HuggingFace library not available")
124
  except Exception as e:
125
- logger.error(f"HuggingFace initialization failed: {e}")
126
- else:
127
- logger.info("ℹ️ No HF_TOKEN found")
128
 
129
- # If we get here, no LLM could be initialized
130
- error_msg = (
131
- "No LLM could be initialized. Please set either:\n"
132
- "- OPENAI_API_KEY (recommended for better GAIA performance)\n"
133
- "- HF_TOKEN (free alternative)\n"
134
- "In your HuggingFace Space settings → Repository secrets"
135
- )
136
- logger.error(error_msg)
137
- raise RuntimeError(error_msg)
138
-
139
-
140
- # ============================================================================
141
- # GAIA AGENT CLASS - Main Agent Implementation
142
- # ============================================================================
143
 
144
- class GAIAAgent:
145
  """
146
- GAIA Benchmark Agent that combines course learning with benchmark capabilities.
147
-
148
- This agent demonstrates:
149
- 1. Multi-tool usage (web search, calculator, file analysis)
150
- 2. RAG implementation (guest database from course)
151
- 3. LLM integration with robust error handling
152
- 4. GAIA-optimized prompting for accurate answers
153
-
154
- The agent is designed to handle various types of GAIA questions:
155
- - Factual questions requiring web search
156
- - Mathematical problems requiring calculations
157
- - Data analysis questions requiring file processing
158
- - Questions about the guest database (demonstrating RAG)
159
  """
160
 
161
  def __init__(self):
162
- """
163
- Initialize the GAIA agent with LLM and tools.
164
 
165
- This sets up:
166
- 1. The language model (with fallback options)
167
- 2. All available tools (web search, calculator, etc.)
168
- 3. The agent workflow that orchestrates everything
169
- """
170
- logger.info("🚀 Initializing GAIA Agent...")
171
 
172
- # Step 1: Initialize the LLM
173
- try:
174
- self.llm = create_llm()
175
- logger.info("✅ LLM initialized successfully")
176
- except Exception as e:
177
- logger.error(f"❌ Failed to initialize LLM: {e}")
178
- raise
179
 
180
- # Step 2: Import and create tools
181
- tools = []
182
 
183
- # Import tools from our tools.py file
184
- try:
185
- from tools import get_all_tools
186
- tool_list = get_all_tools()
187
- tools.extend(tool_list)
188
- logger.info(f"✅ Loaded {len(tool_list)} tools from tools.py")
189
- except ImportError as e:
190
- logger.error(f"❌ Could not import tools.py: {e}")
191
- except Exception as e:
192
- logger.warning(f"⚠️ Error loading tools from tools.py: {e}")
193
-
194
- # Check if we have any tools
195
- if not tools:
196
- error_msg = "❌ No tools available! Check tools.py and retriever.py"
197
- logger.error(error_msg)
198
- raise RuntimeError(error_msg)
199
 
200
- logger.info(f"✅ Total tools available: {len(tools)}")
201
- for tool in tools:
202
- logger.info(f" - {tool.metadata.name}: {tool.metadata.description[:50]}...")
203
 
204
- # Step 3: Create the agent using AgentWorkflow (course approach)
205
- try:
206
- from llama_index.core.agent.workflow import AgentWorkflow, ToolCallResult, AgentStream
207
-
208
- # Create the agent with a GAIA-optimized system prompt (exactly like course)
209
- self.agent = AgentWorkflow.from_tools_or_functions(
210
- tools_or_functions=tools,
211
- llm=self.llm,
212
- system_prompt=self._create_system_prompt()
213
- )
214
-
215
- logger.info("✅ AgentWorkflow created successfully")
216
-
217
- except ImportError as e:
218
- error_msg = f"❌ Could not import AgentWorkflow: {e}"
219
- logger.error(error_msg)
220
- raise RuntimeError(error_msg)
221
- except Exception as e:
222
- error_msg = f"❌ Failed to create agent workflow: {e}"
223
- logger.error(error_msg)
224
- raise RuntimeError(error_msg)
225
 
226
- logger.info("🎉 GAIA Agent initialization complete!")
227
 
228
- def _create_system_prompt(self) -> str:
229
  """
230
- Create a system prompt optimized for GAIA benchmark performance.
231
-
232
- The prompt is designed to:
233
- 1. Encourage accuracy over creativity
234
- 2. Guide proper tool usage
235
- 3. Ensure concise, direct answers
236
- 4. Handle various question types
237
-
238
- Returns:
239
- str: Optimized system prompt for GAIA questions
240
  """
241
- return """You are a helpful AI assistant specialized in answering questions accurately and concisely.
242
-
243
- IMPORTANT - GAIA BENCHMARK GUIDELINES:
244
- - Provide direct, factual answers without extra explanations
245
- - Use your tools when you need specific information or calculations
246
- - Be precise and accurate - exact matches are required for scoring
247
- - If you're not certain about an answer, use available tools to verify
248
-
249
- AVAILABLE TOOLS AND WHEN TO USE THEM:
250
- 1. web_search: Use for current information, recent events, facts not in your training data
251
- 2. calculator: Use for ANY mathematical calculations to ensure accuracy
252
- 3. file_analyzer: Use when questions involve analyzing data files or documents
253
- 4. persona_database: Use for questions about people, characteristics, interests, professions
254
- (Database contains 5000 diverse personas with various backgrounds and interests)
255
-
256
- RESPONSE GUIDELINES:
257
- - Give direct answers without phrases like "Based on my search..." or "According to..."
258
- - For numerical answers, provide just the number or value
259
- - For factual questions, provide just the fact
260
- - For yes/no questions, answer yes or no clearly
261
- - Always use tools for calculations rather than doing math in your head
262
 
263
- EXAMPLES:
264
- Question: "What is 15% of 847?"
265
- Good: Use calculator tool, then respond with just the number
266
- Bad: Try to calculate mentally and risk errors
 
 
267
 
268
- Question: "Who is the current president of France?"
269
- Good: Use web search to get current information
270
- Bad: Guess based on training data that might be outdated
 
 
271
 
272
- Remember: Accuracy is more important than speed. Use your tools to ensure correct answers."""
273
 
274
- def __call__(self, question: str) -> str:
275
  """
276
- Process a GAIA question and return an answer using course approach.
277
-
278
- This follows the exact pattern from the course notebook:
279
- 1. Run the agent to get a handler
280
- 2. Stream events asynchronously
281
- 3. Extract the final response
282
- 4. Clean and return the answer
283
-
284
- Args:
285
- question (str): The GAIA question to answer
286
-
287
- Returns:
288
- str: The agent's answer to the question
289
  """
290
- logger.info(f"📝 Processing GAIA question: {question[:100]}...")
291
 
292
  try:
293
- # Import event types for processing
294
  from llama_index.core.agent.workflow import ToolCallResult, AgentStream
295
 
296
- # Run the agent asynchronously following course pattern
297
  loop = asyncio.new_event_loop()
298
  asyncio.set_event_loop(loop)
299
 
300
  try:
301
  async def run_agent():
302
- # Start the agent run (course pattern)
303
  handler = self.agent.run(user_msg=question)
304
 
305
- # Stream events and collect reasoning (optional for debugging)
306
- reasoning_steps = []
307
- async for ev in handler.stream_events():
308
- if isinstance(ev, ToolCallResult):
309
- step = f"Tool: {ev.tool_name}({ev.tool_kwargs}) => {ev.tool_output}"
310
- reasoning_steps.append(step)
311
- logger.info(f"🔧 {step}")
312
- elif isinstance(ev, AgentStream):
313
- # This is the agent's thought process
314
- pass # We could log this for debugging
315
 
316
- # Get the final response
317
- resp = await handler
318
- return resp
319
 
320
- # Execute the agent
321
  result = loop.run_until_complete(run_agent())
322
 
323
- # Extract the response from the result object
324
- answer = self._extract_response(result)
 
325
 
326
- # Clean and format the answer for GAIA submission
327
- cleaned_answer = self._clean_answer(answer)
328
-
329
- logger.info(f"✅ Generated answer: {cleaned_answer[:100]}...")
330
- return cleaned_answer
331
 
332
  finally:
333
- # Always close the event loop to prevent memory leaks
334
  loop.close()
335
 
336
  except Exception as e:
337
- # If anything goes wrong, return a helpful error message
338
- error_msg = f"I encountered an error processing this question: {str(e)}"
339
- logger.error(f"❌ Error processing question: {e}")
340
  return error_msg
341
 
342
- def _extract_response(self, result: Any) -> str:
343
  """
344
- Extract the text response from the AgentWorkflow result.
345
-
346
- Based on the course notebook, AgentWorkflow returns an AgentOutput with this structure:
347
- AgentOutput(response=ChatMessage(...), tool_calls=[], raw={...})
348
-
349
- Args:
350
- result: The result object from the agent workflow
351
-
352
- Returns:
353
- str: Extracted response text
354
  """
355
  try:
356
- # Handle AgentOutput format from course (most likely)
357
- if hasattr(result, 'response'):
358
- chat_message = result.response
359
- if hasattr(chat_message, 'blocks'):
360
- # Extract text from TextBlock(s)
361
- for block in chat_message.blocks:
362
- if hasattr(block, 'text'):
363
- return str(block.text)
364
- elif hasattr(chat_message, 'content'):
365
- return str(chat_message.content)
366
- else:
367
- return str(chat_message)
368
 
369
- # Fallback to other common formats
 
 
370
  elif hasattr(result, 'content'):
371
  return str(result.content)
372
- elif hasattr(result, 'message'):
373
- if hasattr(result.message, 'content'):
374
- return str(result.message.content)
375
- else:
376
- return str(result.message)
377
  else:
378
- # Final fallback: convert whatever we got to string
379
  return str(result)
380
-
381
- except Exception as e:
382
- logger.warning(f"⚠️ Error extracting response: {e}")
383
- # If extraction fails, try simple string conversion
384
  return str(result)
385
 
386
- def _clean_answer(self, answer: str) -> str:
387
  """
388
- Clean and format the answer for GAIA submission.
389
-
390
- GAIA requires exact matches, so we need to:
391
- 1. Remove common prefixes that agents add
392
- 2. Strip whitespace
393
- 3. Ensure clean, direct responses
394
-
395
- Args:
396
- answer (str): Raw answer from the agent
397
-
398
- Returns:
399
- str: Cleaned answer ready for submission
400
  """
401
- # Remove common agent response prefixes
402
  prefixes_to_remove = [
403
- "assistant:",
404
- "Assistant:",
405
- "Based on my search,",
406
- "According to the search results,",
407
- "The answer is:",
408
- "Answer:"
409
  ]
410
 
411
  cleaned = answer.strip()
412
-
413
  for prefix in prefixes_to_remove:
414
  if cleaned.startswith(prefix):
415
  cleaned = cleaned[len(prefix):].strip()
416
 
417
  return cleaned
418
 
419
-
420
- # ============================================================================
421
- # EVALUATION AND SUBMISSION LOGIC
422
- # ============================================================================
423
-
424
- def run_and_submit_all(profile: gr.OAuthProfile | None) -> tuple[str, pd.DataFrame]:
425
  """
426
- Main function that handles the entire GAIA evaluation process.
427
-
428
- This function:
429
- 1. Validates user authentication
430
- 2. Fetches questions from GAIA API
431
- 3. Runs the agent on all questions
432
- 4. Submits answers for scoring
433
- 5. Returns results and status
434
-
435
- Args:
436
- profile: Gradio OAuth profile (None if not logged in)
437
-
438
- Returns:
439
- tuple: (status_message, results_dataframe)
440
  """
441
- # Step 1: Check authentication
442
  if not profile:
443
- logger.warning(" User not logged in")
444
- return "Please log in to HuggingFace using the button above.", None
445
 
446
  username = profile.username
447
- logger.info(f"👤 User logged in: {username}")
448
 
449
- # Step 2: Get space information for code link
450
  space_id = os.getenv("SPACE_ID")
451
- agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main" if space_id else "No space ID available"
452
 
453
- # Step 3: Set up API endpoints
454
- api_url = DEFAULT_API_URL
455
- questions_url = f"{api_url}/questions"
456
- submit_url = f"{api_url}/submit"
457
-
458
- # Step 4: Initialize the agent
459
- logger.info("🤖 Initializing GAIA Agent...")
460
  try:
461
- agent = GAIAAgent()
462
- logger.info("✅ GAIA Agent ready for evaluation")
463
  except Exception as e:
464
- error_msg = f"Failed to initialize agent: {str(e)}"
465
- logger.error(error_msg)
466
- return error_msg, None
467
 
468
- # Step 5: Fetch GAIA questions
469
- logger.info(f"📥 Fetching questions from: {questions_url}")
470
  try:
471
- response = requests.get(questions_url, timeout=15)
 
472
  response.raise_for_status()
473
- questions_data = response.json()
474
-
475
- if not questions_data:
476
- return "❌ No questions received from GAIA API", None
477
 
478
- logger.info(f"✅ Fetched {len(questions_data)} GAIA questions")
 
 
 
479
 
480
- except requests.exceptions.RequestException as e:
481
- error_msg = f"❌ Network error fetching questions: {str(e)}"
482
- logger.error(error_msg)
483
- return error_msg, None
484
  except Exception as e:
485
- error_msg = f" Error processing questions: {str(e)}"
486
- logger.error(error_msg)
487
- return error_msg, None
488
 
489
- # Step 6: Process all questions
490
- logger.info(f"🧠 Running agent on {len(questions_data)} questions...")
491
- results_log = []
492
- answers_payload = []
493
 
494
- for i, item in enumerate(questions_data, 1):
495
  task_id = item.get("task_id")
496
  question_text = item.get("question")
497
 
498
- if not task_id or question_text is None:
499
- logger.warning(f"⚠️ Skipping invalid question item: {item}")
500
  continue
501
-
502
- logger.info(f"📝 Processing question {i}/{len(questions_data)}: {task_id}")
503
 
504
  try:
505
- # Run the agent on this question
506
- submitted_answer = agent(question_text)
507
 
508
  # Store for submission
509
- answers_payload.append({
510
  "task_id": task_id,
511
- "submitted_answer": submitted_answer
512
  })
513
 
514
- # Store for display (truncated for readability)
515
- results_log.append({
516
  "Task ID": task_id,
517
  "Question": question_text[:100] + "..." if len(question_text) > 100 else question_text,
518
- "Answer": submitted_answer[:150] + "..." if len(submitted_answer) > 150 else submitted_answer
519
  })
520
 
521
- logger.info(f"✅ Question {i} completed")
522
-
523
  except Exception as e:
524
  error_answer = f"ERROR: {str(e)}"
525
- logger.error(f"❌ Error on question {i}: {e}")
526
-
527
- answers_payload.append({
528
  "task_id": task_id,
529
  "submitted_answer": error_answer
530
  })
531
-
532
- results_log.append({
533
  "Task ID": task_id,
534
  "Question": question_text[:100] + "..." if len(question_text) > 100 else question_text,
535
- "Answer": error_answer
536
  })
537
 
538
- if not answers_payload:
539
- return "❌ No answers generated for submission", pd.DataFrame(results_log)
540
-
541
- # Step 7: Submit answers to GAIA API
542
- logger.info(f"📤 Submitting {len(answers_payload)} answers...")
543
- submission_data = {
544
- "username": username.strip(),
545
- "agent_code": agent_code,
546
- "answers": answers_payload
547
- }
548
-
549
  try:
550
- response = requests.post(submit_url, json=submission_data, timeout=60)
 
 
 
 
 
 
 
 
551
  response.raise_for_status()
552
  result_data = response.json()
553
 
554
- # Extract results
555
  score = result_data.get('score', 0)
556
- correct_count = result_data.get('correct_count', 0)
557
- total_attempted = result_data.get('total_attempted', len(answers_payload))
558
 
559
- # Determine pass/fail status
560
  passed = score >= PASSING_SCORE
561
- status_emoji = "🎉" if passed else "📊"
562
 
563
- # Create status message
564
- final_status = (
565
- f"{status_emoji} GAIA Evaluation Results\n"
566
- f"User: {username}\n"
567
- f"Score: {score}% ({correct_count}/{total_attempted} correct)\n"
568
- f"Required: {PASSING_SCORE}% to pass\n"
569
- f"Status: {'✅ PASSED - Certificate Earned!' if passed else '❌ Not passed - Try again!'}\n"
570
- f"Message: {result_data.get('message', 'Evaluation completed')}"
571
- )
572
 
573
- logger.info(f" Submission successful - Score: {score}%")
574
- return final_status, pd.DataFrame(results_log)
575
 
576
- except requests.exceptions.RequestException as e:
577
- error_msg = f"❌ Submission failed: {str(e)}"
578
- logger.error(error_msg)
579
- return error_msg, pd.DataFrame(results_log)
580
  except Exception as e:
581
- error_msg = f" Unexpected error during submission: {str(e)}"
582
- logger.error(error_msg)
583
- return error_msg, pd.DataFrame(results_log)
584
-
585
-
586
- # ============================================================================
587
- # GRADIO INTERFACE
588
- # ============================================================================
589
 
590
  # Create the Gradio interface
591
- with gr.Blocks(title="GAIA Benchmark Agent") as demo:
592
- # Header and instructions
593
- gr.Markdown("# 🎯 GAIA Benchmark Agent - Course Final Project")
594
-
595
  gr.Markdown("""
596
- ## 🚀 Welcome to Your Final Challenge!
597
-
598
- This agent combines everything you've learned in the course:
599
- - **🔧 Multi-Tool Integration**: Web search, calculator, file analysis
600
- - **📚 RAG Implementation**: Persona database with 5K diverse individuals
601
- - **🤖 Agent Workflows**: LlamaIndex agent orchestration
602
- - **🎯 GAIA Optimization**: Designed for benchmark performance
603
-
604
- ### 📋 Setup Checklist:
605
- 1. **🔑 API Keys**: Set `OPENAI_API_KEY` or `HF_TOKEN` in Space secrets
606
- 2. **🔓 Public Space**: Keep your space public for verification
607
- 3. **👤 Login**: Use the HuggingFace login button below
608
- 4. **▶️ Run**: Click the evaluation button and wait for results
609
 
610
- ### 🏆 Goal: Score 30%+ to earn your certificate!
 
 
 
 
611
 
612
- ---
613
  """)
614
 
615
- # Login section
616
- gr.Markdown("### Step 1: Login to HuggingFace")
617
  gr.LoginButton()
618
 
619
- # Evaluation section
620
- gr.Markdown("### Step 2: Run GAIA Evaluation")
621
- gr.Markdown("⚠️ **Note**: This may take 5-10 minutes to complete all questions. Please be patient!")
622
 
623
- run_button = gr.Button(
624
- "🚀 Run GAIA Evaluation & Submit Results",
625
- variant="primary",
626
- size="lg"
627
- )
628
 
629
- # Results section
630
- gr.Markdown("### Step 3: View Results")
631
 
632
- status_output = gr.Textbox(
633
- label="📊 Evaluation Status & Results",
634
- lines=8,
635
  interactive=False,
636
- placeholder="Results will appear here after evaluation..."
637
- )
638
-
639
- results_table = gr.DataFrame(
640
- label="📝 Question-by-Question Results",
641
- wrap=True
642
  )
643
 
644
- # Wire up the interface
645
- run_button.click(
646
- fn=run_and_submit_all,
647
- outputs=[status_output, results_table]
648
- )
649
 
650
- # Footer
651
- gr.Markdown("""
652
- ---
653
- ### 🔧 Troubleshooting:
654
- - **No API Key Error**: Add `OPENAI_API_KEY` or `HF_TOKEN` to your Space secrets
655
- - **Import Errors**: Check that all dependencies are installed
656
- - **Low Score**: GAIA requires exact answers - the agent uses tools for accuracy
657
 
658
- ### 🏅 Good luck earning your certificate!
659
- """)
660
-
661
-
662
- # ============================================================================
663
- # MAIN EXECUTION
664
- # ============================================================================
665
 
666
  if __name__ == "__main__":
667
- print("\n" + "="*60)
668
- print("🎯 GAIA BENCHMARK AGENT - Course Final Project")
669
- print("="*60)
670
-
671
- # Check environment setup
672
- print("\n🔍 Environment Check:")
673
 
674
- space_host = os.getenv("SPACE_HOST")
675
- space_id = os.getenv("SPACE_ID")
676
  openai_key = os.getenv("OPENAI_API_KEY")
677
  hf_token = os.getenv("HF_TOKEN")
678
 
679
- if space_host:
680
- print(f"✅ SPACE_HOST: {space_host}")
681
- if space_id:
682
- print(f"✅ SPACE_ID: {space_id}")
683
  if openai_key:
684
- print("✅ OPENAI_API_KEY: Set")
685
  if hf_token:
686
- print("✅ HF_TOKEN: Set")
687
-
688
  if not openai_key and not hf_token:
689
- print("⚠️ WARNING: No API keys found!")
690
- print(" Please set OPENAI_API_KEY or HF_TOKEN in Space secrets")
691
-
692
- print(f"\n🎯 Target Score: {PASSING_SCORE}% (to earn certificate)")
693
- print("🚀 Agent Features:")
694
- print(" - Web Search (DuckDuckGo)")
695
- print(" - Calculator (Math operations)")
696
- print(" - Guest Database RAG (Course demo)")
697
- print(" - File Analysis (Data processing)")
698
 
699
- print("\n" + "="*60)
700
- print("🌐 Launching Gradio Interface...")
701
- print("="*60 + "\n")
702
 
703
- # Launch the Gradio app
704
- demo.launch(
705
- debug=True,
706
- share=False,
707
- show_error=True
708
- )
 
1
  """
2
+ My GAIA Benchmark Agent - Final Course Project
3
 
4
+ This is my attempt at building an agent that can pass the GAIA benchmark.
5
+ I'm combining everything I learned in the course:
6
+ - Tools (web search, calculator, file processing)
7
+ - RAG with a persona database
8
+ - Agent workflows from LlamaIndex
9
+ - Gradio interface
10
 
11
+ Goal: Get 30%+ score to pass the course!
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  """
13
 
14
  import os
 
17
  import pandas as pd
18
  import asyncio
19
  import logging
 
20
 
21
+ # Set up logging so I can debug issues
22
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 
 
 
23
  logger = logging.getLogger(__name__)
24
 
25
+ # Config stuff
26
+ GAIA_API_URL = "https://agents-course-unit4-scoring.hf.space"
27
+ PASSING_SCORE = 30 # Need this to get my certificate!
 
 
 
 
 
 
28
 
29
+ def setup_llm():
 
 
 
 
30
  """
31
+ Setting up the LLM - trying OpenAI first since it usually works better,
32
+ but falling back to HuggingFace if I don't have OpenAI credits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  """
34
+ logger.info("Setting up LLM...")
35
 
36
+ # Try OpenAI first (better performance but costs money)
37
  openai_key = os.getenv("OPENAI_API_KEY")
38
  if openai_key:
39
  try:
40
  from llama_index.llms.openai import OpenAI
 
41
  llm = OpenAI(
42
  api_key=openai_key,
43
+ model="gpt-4o-mini", # Good balance of performance and cost
44
+ max_tokens=1024,
45
+ temperature=0.1 # Low temp for more consistent answers
46
  )
47
+ logger.info("Got OpenAI working!")
 
48
  return llm
 
 
 
49
  except Exception as e:
50
+ logger.warning(f"OpenAI didn't work: {e}")
 
 
51
 
52
+ # Fallback to HuggingFace (free but maybe not as good)
53
  hf_token = os.getenv("HF_TOKEN")
54
  if hf_token:
55
  try:
56
  from llama_index.llms.huggingface_api import HuggingFaceInferenceAPI
 
57
  llm = HuggingFaceInferenceAPI(
58
+ model_name="Qwen/Qwen2.5-Coder-32B-Instruct",
59
  token=hf_token,
60
+ max_new_tokens=512,
61
+ temperature=0.1
 
62
  )
63
+ logger.info("Using HuggingFace LLM")
 
64
  return llm
 
 
 
65
  except Exception as e:
66
+ logger.error(f"HuggingFace also failed: {e}")
 
 
67
 
68
+ # If we get here, nothing worked
69
+ raise RuntimeError("No LLM available! Need either OPENAI_API_KEY or HF_TOKEN")
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
+ class MyGAIAAgent:
72
  """
73
+ This is my main agent class. It brings together the LLM, tools, and
74
+ the agent workflow from the course.
 
 
 
 
 
 
 
 
 
 
 
75
  """
76
 
77
  def __init__(self):
78
+ logger.info("Building my GAIA agent...")
 
79
 
80
+ # Step 1: Get the LLM working
81
+ self.llm = setup_llm()
 
 
 
 
82
 
83
+ # Step 2: Load my tools
84
+ from tools import get_my_tools
85
+ self.tools = get_my_tools(self.llm) # Pass LLM so all tools use same one
 
 
 
 
86
 
87
+ if not self.tools:
88
+ raise RuntimeError("No tools loaded! Check tools.py")
89
 
90
+ logger.info(f"Loaded {len(self.tools)} tools:")
91
+ for tool in self.tools:
92
+ logger.info(f" - {tool.metadata.name}")
 
 
 
 
 
 
 
 
 
 
 
 
 
93
 
94
+ # Step 3: Create the agent using the workflow pattern from class
95
+ from llama_index.core.agent.workflow import AgentWorkflow
 
96
 
97
+ self.agent = AgentWorkflow.from_tools_or_functions(
98
+ tools_or_functions=self.tools,
99
+ llm=self.llm,
100
+ system_prompt=self._get_system_prompt()
101
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
 
103
+ logger.info("Agent ready to go!")
104
 
105
+ def _get_system_prompt(self):
106
  """
107
+ My system prompt - trying to make it good for GAIA questions
 
 
 
 
 
 
 
 
 
108
  """
109
+ return """You are my AI assistant for answering GAIA benchmark questions accurately.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
+ Key rules:
112
+ - Give direct, precise answers (GAIA needs exact matches)
113
+ - Use tools when you need current info or calculations
114
+ - Don't add extra explanations unless asked
115
+ - For math problems, always use the calculator tool
116
+ - For current events, use web search
117
 
118
+ Available tools:
119
+ - web_search: for current information and facts
120
+ - calculator: for any math calculations
121
+ - file_analyzer: for processing data files
122
+ - persona_database: database of different people and their interests
123
 
124
+ Be accurate above all else - that's how I pass this course!"""
125
 
126
+ def answer_question(self, question):
127
  """
128
+ Main function to answer a GAIA question
 
 
 
 
 
 
 
 
 
 
 
 
129
  """
130
+ logger.info(f"Got question: {question[:100]}...")
131
 
132
  try:
133
+ # Import the event types for processing
134
  from llama_index.core.agent.workflow import ToolCallResult, AgentStream
135
 
136
+ # Run the agent (this is the async pattern from the course)
137
  loop = asyncio.new_event_loop()
138
  asyncio.set_event_loop(loop)
139
 
140
  try:
141
  async def run_agent():
 
142
  handler = self.agent.run(user_msg=question)
143
 
144
+ # Watch what the agent does (helpful for debugging)
145
+ async for event in handler.stream_events():
146
+ if isinstance(event, ToolCallResult):
147
+ logger.info(f"Used tool: {event.tool_name} -> {str(event.tool_output)[:100]}...")
 
 
 
 
 
 
148
 
149
+ result = await handler
150
+ return result
 
151
 
 
152
  result = loop.run_until_complete(run_agent())
153
 
154
+ # Extract the actual answer from the result
155
+ answer = self._extract_answer(result)
156
+ answer = self._clean_answer(answer)
157
 
158
+ logger.info(f"My answer: {answer[:100]}...")
159
+ return answer
 
 
 
160
 
161
  finally:
 
162
  loop.close()
163
 
164
  except Exception as e:
165
+ error_msg = f"Something went wrong: {str(e)}"
166
+ logger.error(error_msg)
 
167
  return error_msg
168
 
169
+ def _extract_answer(self, result):
170
  """
171
+ Extract the text from the agent result - this took me a while to figure out
 
 
 
 
 
 
 
 
 
172
  """
173
  try:
174
+ # The result has a response with blocks containing text
175
+ if hasattr(result, 'response') and hasattr(result.response, 'blocks'):
176
+ for block in result.response.blocks:
177
+ if hasattr(block, 'text'):
178
+ return str(block.text)
 
 
 
 
 
 
 
179
 
180
+ # Fallback methods if the structure is different
181
+ if hasattr(result, 'response'):
182
+ return str(result.response)
183
  elif hasattr(result, 'content'):
184
  return str(result.content)
 
 
 
 
 
185
  else:
 
186
  return str(result)
187
+ except:
 
 
 
188
  return str(result)
189
 
190
+ def _clean_answer(self, answer):
191
  """
192
+ Clean up the answer - remove common prefixes that agents add
 
 
 
 
 
 
 
 
 
 
 
193
  """
194
+ # Remove stuff like "Based on my search" etc.
195
  prefixes_to_remove = [
196
+ "assistant:", "Assistant:", "Based on my search,",
197
+ "According to the search results,", "The answer is:", "Answer:"
 
 
 
 
198
  ]
199
 
200
  cleaned = answer.strip()
 
201
  for prefix in prefixes_to_remove:
202
  if cleaned.startswith(prefix):
203
  cleaned = cleaned[len(prefix):].strip()
204
 
205
  return cleaned
206
 
207
+ def run_gaia_evaluation(profile):
 
 
 
 
 
208
  """
209
+ This is the main function that runs when someone clicks the button.
210
+ It fetches questions from GAIA, runs my agent on them, and submits results.
 
 
 
 
 
 
 
 
 
 
 
 
211
  """
 
212
  if not profile:
213
+ return "Need to log in with HuggingFace first!", None
 
214
 
215
  username = profile.username
216
+ logger.info(f"Running evaluation for {username}")
217
 
218
+ # Get the space info for submission
219
  space_id = os.getenv("SPACE_ID")
220
+ code_link = f"https://huggingface.co/spaces/{space_id}/tree/main" if space_id else "No space ID"
221
 
222
+ # Initialize my agent
 
 
 
 
 
 
223
  try:
224
+ agent = MyGAIAAgent()
 
225
  except Exception as e:
226
+ return f"Failed to create agent: {e}", None
 
 
227
 
228
+ # Fetch the questions
 
229
  try:
230
+ logger.info("Getting questions from GAIA...")
231
+ response = requests.get(f"{GAIA_API_URL}/questions", timeout=15)
232
  response.raise_for_status()
233
+ questions = response.json()
 
 
 
234
 
235
+ if not questions:
236
+ return "No questions received!", None
237
+
238
+ logger.info(f"Got {len(questions)} questions to answer")
239
 
 
 
 
 
240
  except Exception as e:
241
+ return f"Failed to get questions: {e}", None
 
 
242
 
243
+ # Answer all the questions
244
+ results = []
245
+ answers_for_submission = []
 
246
 
247
+ for i, item in enumerate(questions, 1):
248
  task_id = item.get("task_id")
249
  question_text = item.get("question")
250
 
251
+ if not task_id or not question_text:
 
252
  continue
253
+
254
+ logger.info(f"Question {i}/{len(questions)}: {task_id}")
255
 
256
  try:
257
+ answer = agent.answer_question(question_text)
 
258
 
259
  # Store for submission
260
+ answers_for_submission.append({
261
  "task_id": task_id,
262
+ "submitted_answer": answer
263
  })
264
 
265
+ # Store for display (truncated)
266
+ results.append({
267
  "Task ID": task_id,
268
  "Question": question_text[:100] + "..." if len(question_text) > 100 else question_text,
269
+ "My Answer": answer[:150] + "..." if len(answer) > 150 else answer
270
  })
271
 
 
 
272
  except Exception as e:
273
  error_answer = f"ERROR: {str(e)}"
274
+ answers_for_submission.append({
 
 
275
  "task_id": task_id,
276
  "submitted_answer": error_answer
277
  })
278
+ results.append({
 
279
  "Task ID": task_id,
280
  "Question": question_text[:100] + "..." if len(question_text) > 100 else question_text,
281
+ "My Answer": error_answer
282
  })
283
 
284
+ # Submit my answers
 
 
 
 
 
 
 
 
 
 
285
  try:
286
+ logger.info(f"Submitting {len(answers_for_submission)} answers...")
287
+
288
+ submission = {
289
+ "username": username,
290
+ "agent_code": code_link,
291
+ "answers": answers_for_submission
292
+ }
293
+
294
+ response = requests.post(f"{GAIA_API_URL}/submit", json=submission, timeout=60)
295
  response.raise_for_status()
296
  result_data = response.json()
297
 
298
+ # Get my score!
299
  score = result_data.get('score', 0)
300
+ correct = result_data.get('correct_count', 0)
301
+ total = result_data.get('total_attempted', len(answers_for_submission))
302
 
303
+ # Did I pass?
304
  passed = score >= PASSING_SCORE
305
+ emoji = "🎉" if passed else "😔"
306
 
307
+ status_message = f"""{emoji} GAIA Results for {username}
308
+
309
+ Score: {score}% ({correct}/{total} correct)
310
+ Required to pass: {PASSING_SCORE}%
311
+
312
+ {'🎊 PASSED! I got my certificate!' if passed else '😞 Not quite... need to try again'}
313
+
314
+ {result_data.get('message', 'Evaluation complete')}"""
 
315
 
316
+ logger.info(f"Final score: {score}%")
317
+ return status_message, pd.DataFrame(results)
318
 
 
 
 
 
319
  except Exception as e:
320
+ return f"Submission failed: {e}", pd.DataFrame(results)
 
 
 
 
 
 
 
321
 
322
  # Create the Gradio interface
323
+ with gr.Blocks(title="My GAIA Agent") as demo:
324
+ gr.Markdown("# 🤖 My GAIA Benchmark Agent")
 
 
325
  gr.Markdown("""
326
+ This is my final project for the AI Agents course!
 
 
 
 
 
 
 
 
 
 
 
 
327
 
328
+ My agent can:
329
+ - 🔍 Search the web for current information
330
+ - 🧮 Do mathematical calculations
331
+ - 📊 Analyze data files
332
+ - 👥 Query a database of personas
333
 
334
+ **Goal:** Score 30%+ on GAIA benchmark to pass the course!
335
  """)
336
 
337
+ gr.Markdown("### Step 1: Login")
 
338
  gr.LoginButton()
339
 
340
+ gr.Markdown("### Step 2: Run the Evaluation")
341
+ gr.Markdown(" This might take 5-10 minutes...")
 
342
 
343
+ run_btn = gr.Button("🚀 Run GAIA Evaluation", variant="primary", size="lg")
 
 
 
 
344
 
345
+ gr.Markdown("### Step 3: Results")
 
346
 
347
+ status_text = gr.Textbox(
348
+ label="📊 My Results",
349
+ lines=10,
350
  interactive=False,
351
+ placeholder="Results will show here..."
 
 
 
 
 
352
  )
353
 
354
+ results_df = gr.DataFrame(label="📝 Question by Question Results")
 
 
 
 
355
 
356
+ # Connect the button
357
+ run_btn.click(fn=run_gaia_evaluation, outputs=[status_text, results_df])
 
 
 
 
 
358
 
359
+ gr.Markdown("---")
360
+ gr.Markdown("🤞 Fingers crossed I pass this course!")
 
 
 
 
 
361
 
362
  if __name__ == "__main__":
363
+ print("🎯 My GAIA Agent - Final Course Project")
364
+ print("=" * 50)
 
 
 
 
365
 
366
+ # Check my environment
 
367
  openai_key = os.getenv("OPENAI_API_KEY")
368
  hf_token = os.getenv("HF_TOKEN")
369
 
 
 
 
 
370
  if openai_key:
371
+ print("✅ OpenAI key found")
372
  if hf_token:
373
+ print("✅ HuggingFace token found")
 
374
  if not openai_key and not hf_token:
375
+ print("⚠️ No API keys! Add OPENAI_API_KEY or HF_TOKEN to secrets")
 
 
 
 
 
 
 
 
376
 
377
+ print(f"🎯 Need {PASSING_SCORE}% to pass the course")
378
+ print("🚀 Starting my agent...")
 
379
 
380
+ demo.launch(debug=True, share=False, show_error=True)
 
 
 
 
 
requirements.txt CHANGED
@@ -1,104 +1,32 @@
1
- # ============================================================================
2
- # GAIA Benchmark Agent - Requirements
3
- # ============================================================================
4
- # This file lists all the Python packages needed for the GAIA agent to work.
5
- # Each section explains what the packages are used for.
6
-
7
- # ============================================================================
8
- # CORE INTERFACE AND API DEPENDENCIES
9
- # ============================================================================
10
- # These are essential for the app to run and communicate with GAIA API
11
 
 
12
  gradio>=4.0.0
13
- # Web interface for the agent - provides the UI where users interact
14
- # Includes login functionality and result display
15
-
16
- requests>=2.28.0
17
- # For HTTP requests to the GAIA evaluation API
18
- # Used to fetch questions and submit answers
19
-
20
  pandas>=1.5.0
21
- # Data manipulation and display of results in tables
22
- # Used to show question-answer pairs in a nice format
23
-
24
- # ============================================================================
25
- # LLAMAINDEX CORE - The Foundation
26
- # ============================================================================
27
- # LlamaIndex is the main framework from the course
28
 
 
29
  llama-index-core>=0.10.0
30
- # Core LlamaIndex functionality - documents, nodes, retrievers, etc.
31
- # This is the foundation that everything else builds on
32
-
33
- # ============================================================================
34
- # LLM (Language Model) INTEGRATIONS
35
- # ============================================================================
36
- # These allow us to use different LLMs with fallback options
37
 
 
38
  llama-index-llms-openai
39
- # OpenAI integration (GPT-4, GPT-3.5) - recommended for best GAIA performance
40
- # Requires OPENAI_API_KEY in your Space secrets
41
-
42
  llama-index-llms-huggingface-api
43
- # HuggingFace Inference API integration - free alternative
44
- # Uses models like Qwen/Qwen2.5-Coder-32B-Instruct
45
- # Requires HF_TOKEN in your Space secrets
46
-
47
- # ============================================================================
48
- # AGENT SYSTEM - Course Approach
49
- # ============================================================================
50
- # AgentWorkflow is part of llama-index-core, no separate package needed
51
- # This matches exactly what the course notebook uses
52
-
53
- # ============================================================================
54
- # RETRIEVAL SYSTEMS (RAG) - Enhanced with Vector Embeddings
55
- # ============================================================================
56
- # These are for the advanced RAG (Retrieval-Augmented Generation) functionality
57
 
 
58
  llama-index-retrievers-bm25
59
- # BM25 retriever for keyword-based search (still useful as fallback)
60
- # Great for finding exact matches and proper nouns
61
-
62
  llama-index-embeddings-huggingface
63
- # HuggingFace embedding models for semantic search
64
- # Converts text to vectors that capture meaning and context
65
- # Used with BAAI/bge-small-en-v1.5 model
66
-
67
  llama-index-vector-stores-chroma
68
- # ChromaDB vector store integration
69
- # Provides persistent storage for vector embeddings
70
- # Fast similarity search for semantic retrieval
71
 
 
72
  chromadb>=0.4.0
73
- # ChromaDB database for vector storage
74
- # Self-contained vector database with no external dependencies
75
- # Stores embeddings locally for fast retrieval
76
 
 
77
  datasets>=2.0.0
78
- # HuggingFace datasets library
79
- # Used to load the finepersonas dataset
80
- # Provides easy access to thousands of datasets
81
-
82
- # ============================================================================
83
- # TOOLS AND EXTERNAL SERVICES
84
- # ============================================================================
85
- # These packages enable the agent's tools
86
 
 
87
  duckduckgo-search>=6.0.0
88
- # Web search functionality using DuckDuckGo
89
- # Essential for GAIA questions requiring current information
90
- # Free alternative to Google Search API
91
-
92
- # ============================================================================
93
- # UTILITIES AND ENVIRONMENT
94
- # ============================================================================
95
- # Supporting packages for configuration and development
96
 
 
97
  python-dotenv
98
- # For loading environment variables from .env files
99
- # Useful for local development and testing
100
-
101
- nest-asyncio
102
- # Allows running async code in environments that already have an event loop
103
- # Required for running LlamaIndex query engines in Jupyter/Gradio
104
- # Fixes "RuntimeError: This event loop is already running" errors
 
1
+ # My GAIA Agent Requirements
2
+ # These are all the packages I need for my final project
 
 
 
 
 
 
 
 
3
 
4
+ # Basic stuff for the web interface
5
  gradio>=4.0.0
6
+ requests>=2.28.0
 
 
 
 
 
 
7
  pandas>=1.5.0
 
 
 
 
 
 
 
8
 
9
+ # Main LlamaIndex stuff - this is the core framework we learned about
10
  llama-index-core>=0.10.0
 
 
 
 
 
 
 
11
 
12
+ # Different LLM options - trying both OpenAI and HuggingFace
13
  llama-index-llms-openai
 
 
 
14
  llama-index-llms-huggingface-api
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
+ # For the RAG part with embeddings and vector search
17
  llama-index-retrievers-bm25
 
 
 
18
  llama-index-embeddings-huggingface
 
 
 
 
19
  llama-index-vector-stores-chroma
 
 
 
20
 
21
+ # Vector database - using ChromaDB like in the course
22
  chromadb>=0.4.0
 
 
 
23
 
24
+ # To load the persona dataset from HuggingFace
25
  datasets>=2.0.0
 
 
 
 
 
 
 
 
26
 
27
+ # Web search tool
28
  duckduckgo-search>=6.0.0
 
 
 
 
 
 
 
 
29
 
30
+ # Helper packages
31
  python-dotenv
32
+ nest-asyncio
 
 
 
 
 
 
retriever.py CHANGED
@@ -1,526 +1,341 @@
1
  """
2
- retriever.py - Advanced RAG Implementation with Personas Database
3
 
4
- This file implements an advanced RAG system using:
5
- 1. Real dataset from HuggingFace (dvilasuero/finepersonas-v0.1-tiny)
6
- 2. Vector embeddings for semantic search
7
- 3. ChromaDB for persistent vector storage
8
- 4. LlamaIndex IngestionPipeline for processing
 
9
 
10
- This demonstrates advanced course concepts:
11
- - Dataset integration from HuggingFace
12
- - Vector embeddings vs keyword search
13
- - Persistent storage with ChromaDB
14
- - Ingestion pipelines for data processing
15
 
16
- Why this approach:
17
- - 5K personas provide rich, diverse data
18
- - Vector embeddings capture semantic meaning
19
- - ChromaDB provides fast, persistent storage
20
- - More realistic than simple guest database
21
-
22
- download_and_prepare_personas() # Download 5K personas
23
- load_persona_documents() # Load into documents
24
- create_persona_index() # Create vector index
25
- get_persona_query_engine() # For tools.py to use
26
  """
27
 
28
  import logging
29
  import os
30
- from typing import List, Dict, Any
31
  from pathlib import Path
32
 
33
- # LlamaIndex core components
34
  from llama_index.core.schema import Document
35
- from llama_index.core.tools import FunctionTool, QueryEngineTool
36
- from llama_index.core import SimpleDirectoryReader, VectorStoreIndex
37
  from llama_index.core.node_parser import SentenceSplitter
38
- from llama_index.core.ingestion import IngestionPipeline
39
 
40
- # Embeddings and vector store
41
  from llama_index.embeddings.huggingface import HuggingFaceEmbedding
42
  from llama_index.vector_stores.chroma import ChromaVectorStore
43
 
44
- # External libraries
45
- from datasets import load_dataset
46
- import chromadb
47
-
48
- # Setup logging
49
- logger = logging.getLogger(__name__)
50
-
51
- # ============================================================================
52
- # CONFIGURATION AND CONSTANTS
53
- # ============================================================================
54
 
55
- # Dataset configuration
56
- DATASET_NAME = "dvilasuero/finepersonas-v0.1-tiny"
57
- DATA_DIR = Path("data")
58
- CHROMA_DB_PATH = "./alfred_chroma_db"
59
- COLLECTION_NAME = "alfred"
60
 
61
- # Embedding model - good balance of performance and speed
62
- EMBEDDING_MODEL = "BAAI/bge-small-en-v1.5"
63
 
64
- # Chunk size for text splitting - optimal for personas
65
- CHUNK_SIZE = 1024
66
- CHUNK_OVERLAP = 20
 
 
67
 
68
- # ============================================================================
69
- # DATA PREPARATION - Loading Personas from HuggingFace
70
- # ============================================================================
71
 
72
- def download_and_prepare_personas() -> int:
73
  """
74
- Download personas from HuggingFace and save as individual text files.
75
-
76
- This approach demonstrates:
77
- 1. Dataset integration from HuggingFace Hub
78
- 2. Local file preparation for SimpleDirectoryReader
79
- 3. Data persistence for repeated runs
80
-
81
- Why save as files:
82
- - SimpleDirectoryReader expects file-based input
83
- - Allows for easy inspection and debugging
84
- - Caches data locally to avoid repeated downloads
85
- - Mimics real-world scenario where you have document files
86
-
87
- Returns:
88
- int: Number of persona files created
89
  """
90
- logger.info(f"Starting persona data preparation...")
91
-
92
- # Create data directory if it doesn't exist
93
- DATA_DIR.mkdir(parents=True, exist_ok=True)
94
-
95
- # Check if we already have data (avoid re-downloading)
96
- existing_files = list(DATA_DIR.glob("persona_*.txt"))
97
- if existing_files:
98
- logger.info(f"Found {len(existing_files)} existing persona files, skipping download")
99
- return len(existing_files)
100
-
101
- try:
102
- # Load the dataset from HuggingFace
103
- logger.info(f"Loading dataset: {DATASET_NAME}")
104
- dataset = load_dataset(path=DATASET_NAME, split="train")
105
- logger.info(f"Dataset loaded successfully with {len(dataset)} personas")
106
-
107
- # Save each persona as a separate text file
108
- personas_created = 0
109
- for i, persona_data in enumerate(dataset):
110
- persona_file = DATA_DIR / f"persona_{i}.txt"
111
-
112
- # Extract the persona text
113
- persona_text = persona_data["persona"]
114
-
115
- # Add some metadata to make the persona more searchable
116
- enhanced_text = f"Persona {i}:\n{persona_text}"
117
-
118
- # Write to file
119
- with open(persona_file, "w", encoding="utf-8") as f:
120
- f.write(enhanced_text)
121
-
122
- personas_created += 1
123
-
124
- # Log progress for large datasets
125
- if personas_created % 1000 == 0:
126
- logger.info(f"Created {personas_created} persona files...")
127
-
128
- logger.info(f"✅ Successfully created {personas_created} persona files")
129
- return personas_created
130
 
131
- except Exception as e:
132
- logger.error(f"❌ Error downloading personas: {e}")
133
- raise RuntimeError(f"Failed to download personas: {e}")
134
-
135
-
136
- # ============================================================================
137
- # DOCUMENT LOADING - Converting Files to LlamaIndex Documents
138
- # ============================================================================
139
-
140
- def load_persona_documents() -> List[Document]:
141
- """
142
- Load persona files into LlamaIndex Document objects.
143
-
144
- This demonstrates:
145
- 1. SimpleDirectoryReader usage for file loading
146
- 2. Document object creation and metadata handling
147
- 3. Error handling for file operations
148
-
149
- Why SimpleDirectoryReader:
150
- - Handles multiple file formats automatically
151
- - Preserves file metadata (filename, path, etc.)
152
- - Integrates seamlessly with LlamaIndex pipeline
153
- - Scales well for large document collections
154
-
155
- Returns:
156
- List[Document]: List of loaded persona documents
157
- """
158
- logger.info("Loading persona documents...")
159
-
160
- # Ensure we have persona data
161
- if not DATA_DIR.exists() or not list(DATA_DIR.glob("persona_*.txt")):
162
- logger.info("No persona files found, downloading...")
163
- download_and_prepare_personas()
164
-
165
- try:
166
- # Use SimpleDirectoryReader to load all text files
167
- reader = SimpleDirectoryReader(input_dir=str(DATA_DIR))
168
- documents = reader.load_data()
169
 
170
- logger.info(f" Loaded {len(documents)} persona documents")
171
 
172
- # Log some statistics about the documents
173
- if documents:
174
- total_chars = sum(len(doc.text) for doc in documents)
175
- avg_chars = total_chars / len(documents)
176
- logger.info(f"Average document length: {avg_chars:.0f} characters")
177
 
178
- return documents
179
 
180
- except Exception as e:
181
- logger.error(f"❌ Error loading documents: {e}")
182
- raise RuntimeError(f"Failed to load persona documents: {e}")
183
-
184
-
185
- # ============================================================================
186
- # VECTOR STORE SETUP - ChromaDB Configuration
187
- # ============================================================================
 
 
 
 
 
188
 
189
- def setup_chroma_vector_store():
190
  """
191
- Set up ChromaDB vector store for persistent storage.
192
-
193
- This demonstrates:
194
- 1. Persistent vector database configuration
195
- 2. Collection management
196
- 3. Integration with LlamaIndex vector stores
197
-
198
- Why ChromaDB:
199
- - Persistent storage (survives application restarts)
200
- - Fast vector similarity search
201
- - Easy integration with LlamaIndex
202
- - Good for development and production
203
- - No external dependencies (self-contained)
204
-
205
- Returns:
206
- ChromaVectorStore: Configured vector store ready for use
207
  """
208
- logger.info("Setting up ChromaDB vector store...")
 
 
 
 
209
 
210
  try:
211
- # Create persistent ChromaDB client
212
- # This creates a local database that persists between runs
213
- db = chromadb.PersistentClient(path=CHROMA_DB_PATH)
214
- logger.info(f"ChromaDB client created at: {CHROMA_DB_PATH}")
215
-
216
- # Get or create collection for our personas
217
- # Collections are like tables in a traditional database
218
- chroma_collection = db.get_or_create_collection(name=COLLECTION_NAME)
219
- logger.info(f"Using collection: {COLLECTION_NAME}")
220
 
221
- # Wrap ChromaDB collection in LlamaIndex vector store
222
- vector_store = ChromaVectorStore(chroma_collection=chroma_collection)
 
 
 
 
 
 
 
 
 
223
 
224
- logger.info(" ChromaDB vector store configured successfully")
225
- return vector_store
226
 
227
  except Exception as e:
228
- logger.error(f" Error setting up ChromaDB: {e}")
229
- raise RuntimeError(f"Failed to setup ChromaDB: {e}")
230
-
231
-
232
- # ============================================================================
233
- # INGESTION PIPELINE - Document Processing with Embeddings
234
- # ============================================================================
235
 
236
- def create_ingestion_pipeline(vector_store) -> IngestionPipeline:
237
  """
238
- Create an ingestion pipeline for processing persona documents.
239
-
240
- This demonstrates:
241
- 1. Text chunking with SentenceSplitter
242
- 2. Embedding generation with HuggingFace models
243
- 3. Pipeline composition for complex processing
244
-
245
- The pipeline does:
246
- 1. Split documents into smaller chunks (better for retrieval)
247
- 2. Generate vector embeddings for each chunk
248
- 3. Store embeddings in the vector database
249
-
250
- Why this approach:
251
- - Chunking improves retrieval precision
252
- - Embeddings capture semantic meaning
253
- - Pipeline caches results for efficiency
254
- - Modular design allows easy modification
255
-
256
- Args:
257
- vector_store: ChromaDB vector store for persistence
258
-
259
- Returns:
260
- IngestionPipeline: Configured pipeline ready for document processing
261
  """
262
- logger.info("Creating ingestion pipeline...")
 
 
 
 
 
 
 
 
 
 
 
 
263
 
 
 
 
 
 
 
 
 
 
 
 
 
264
  try:
265
- # Create text splitter
266
- # SentenceSplitter respects sentence boundaries for better coherence
267
- text_splitter = SentenceSplitter(
268
- chunk_size=CHUNK_SIZE, # Max characters per chunk
269
- chunk_overlap=CHUNK_OVERLAP # Overlap to maintain context
270
- )
271
- logger.info(f"Text splitter configured: {CHUNK_SIZE} chars, {CHUNK_OVERLAP} overlap")
272
 
273
- # Create embedding model
274
- # This model converts text to numerical vectors that capture meaning
275
- embed_model = HuggingFaceEmbedding(model_name=EMBEDDING_MODEL)
276
- logger.info(f"Embedding model configured: {EMBEDDING_MODEL}")
277
-
278
- # Create the ingestion pipeline
279
- # This processes documents through the transformations in order
280
- pipeline = IngestionPipeline(
281
- transformations=[
282
- text_splitter, # First: split into chunks
283
- embed_model, # Second: create embeddings
284
- ],
285
- vector_store=vector_store # Third: store in database
286
- )
287
 
288
- logger.info("✅ Ingestion pipeline created successfully")
289
- return pipeline
 
 
 
290
 
291
  except Exception as e:
292
- logger.error(f" Error creating ingestion pipeline: {e}")
293
- raise RuntimeError(f"Failed to create ingestion pipeline: {e}")
294
-
295
 
296
- # ============================================================================
297
- # INDEX CREATION - Vector Search Index
298
- # ============================================================================
299
-
300
- def create_persona_index():
301
  """
302
- Create or load the persona vector index.
303
-
304
- This is the main function that orchestrates the entire RAG setup:
305
- 1. Load documents from files
306
- 2. Set up vector storage
307
- 3. Process documents through pipeline
308
- 4. Create searchable index
309
-
310
- The index enables semantic search where:
311
- - Similar meanings are found even with different words
312
- - Context and relationships are preserved
313
- - Fast retrieval from thousands of personas
314
-
315
- Returns:
316
- VectorStoreIndex: Ready-to-use search index
317
  """
318
- logger.info("Creating persona search index...")
319
 
320
  try:
321
- # Step 1: Load persona documents
322
- documents = load_persona_documents()
323
- if not documents:
324
- raise RuntimeError("No documents loaded")
 
 
 
 
325
 
326
- # Step 2: Set up vector store
327
- vector_store = setup_chroma_vector_store()
 
 
 
328
 
329
- # Step 3: Check if we already have processed data
330
- # This saves time on repeated runs
331
  try:
332
- # Try to create index from existing vector store
333
  embed_model = HuggingFaceEmbedding(model_name=EMBEDDING_MODEL)
334
- existing_index = VectorStoreIndex.from_vector_store(
335
- vector_store=vector_store,
336
- embed_model=embed_model
337
- )
338
-
339
- # Test if the index has data
340
- test_retriever = existing_index.as_retriever(similarity_top_k=1)
341
- test_results = test_retriever.retrieve("test query")
342
-
343
- if test_results:
344
- logger.info("✅ Found existing persona index with data")
345
- return existing_index
346
- else:
347
- logger.info("Existing index is empty, rebuilding...")
348
-
349
- except Exception:
350
- logger.info("No existing index found, creating new one...")
351
 
352
- # Step 4: Process documents through ingestion pipeline
353
- pipeline = create_ingestion_pipeline(vector_store)
354
 
355
- logger.info(f"Processing {len(documents)} documents through pipeline...")
356
- # This may take a while for large datasets as it generates embeddings
357
- nodes = pipeline.run(documents=documents)
358
- logger.info(f"✅ Processed {len(nodes)} document chunks")
359
-
360
- # Step 5: Create the final index
361
- embed_model = HuggingFaceEmbedding(model_name=EMBEDDING_MODEL)
362
- index = VectorStoreIndex.from_vector_store(
363
  vector_store=vector_store,
364
- embed_model=embed_model
 
365
  )
366
 
367
- logger.info("Persona index created successfully")
368
  return index
369
 
370
  except Exception as e:
371
- logger.error(f" Error creating persona index: {e}")
372
- raise RuntimeError(f"Failed to create persona index: {e}")
373
-
374
-
375
- # ============================================================================
376
- # MAIN FUNCTIONS USED BY TOOLS.PY
377
- # ============================================================================
378
- # These are the core functions that tools.py uses to access the persona database.
379
- # Tool creation is handled in tools.py following the course structure.
380
 
381
  def get_persona_index():
382
  """
383
- Get the persona index for use by tools.py.
 
 
384
 
385
- This is a simple wrapper function that tools.py can import and use.
386
- It ensures the index is created and ready for use.
 
 
 
387
 
388
- Returns:
389
- VectorStoreIndex: The persona database index
390
- """
391
- return create_persona_index()
392
 
393
-
394
- def get_persona_query_engine():
395
  """
396
- Get a configured query engine for the persona database.
397
-
398
- This creates a query engine ready for use in QueryEngineTool.
399
- Tools.py can import this to create the persona database tool.
400
-
401
- Returns:
402
- QueryEngine: Configured query engine for persona database
403
  """
404
  try:
405
- # Get the index
406
- index = create_persona_index()
 
 
407
 
408
- # Configure embedding model (same as indexing)
409
- embed_model = HuggingFaceEmbedding(model_name=EMBEDDING_MODEL)
410
-
411
- # Create query engine with optimal settings
412
  query_engine = index.as_query_engine(
413
- response_mode="tree_summarize", # Good for combining multiple sources
414
- similarity_top_k=5, # Retrieve top 5 most relevant personas
415
- streaming=False # Disable streaming for stability
 
416
  )
417
 
418
- logger.info("Persona query engine ready for tools.py")
419
  return query_engine
420
 
421
  except Exception as e:
422
- logger.error(f" Error creating query engine for tools.py: {e}")
423
- raise
424
-
425
 
426
- # ============================================================================
427
- # TESTING AND DEBUGGING FUNCTIONS
428
- # ============================================================================
429
-
430
- def test_persona_system():
431
  """
432
- Test the persona system components available in retriever.py.
433
- This helps verify that the database setup is working correctly.
434
-
435
- Note: Tool creation testing is now in tools.py since that's where tools are created.
436
  """
437
- print("\n=== Testing Persona Database System ===")
438
 
439
- # Test data preparation
440
- print("\n--- Testing Data Preparation ---")
441
- try:
442
- count = download_and_prepare_personas()
443
- print(f"✅ Data preparation successful: {count} personas")
444
- except Exception as e:
445
- print(f"❌ Data preparation failed: {e}")
446
- return
447
 
448
- # Test document loading
449
- print("\n--- Testing Document Loading ---")
450
- try:
451
- docs = load_persona_documents()
452
- print(f"✅ Document loading successful: {len(docs)} documents")
453
- except Exception as e:
454
- print(f"❌ Document loading failed: {e}")
455
- return
456
 
457
- # Test index creation
458
- print("\n--- Testing Index Creation ---")
459
  try:
460
- index = create_persona_index()
461
- print("✅ Index creation successful")
 
 
462
  except Exception as e:
463
- print(f"❌ Index creation failed: {e}")
464
- return
465
-
466
- # Test basic retrieval (without tool wrapper)
467
- print("\n--- Testing Basic Retrieval ---")
468
- test_queries = [
469
- "writers and authors",
470
- "people interested in travel",
471
- "scientists and researchers"
472
- ]
473
 
 
 
474
  try:
475
- retriever = index.as_retriever(similarity_top_k=2)
476
-
477
- for query in test_queries:
478
- print(f"\nQuery: {query}")
479
- try:
480
- results = retriever.retrieve(query)
481
- if results:
482
- print(f"✅ Found {len(results)} results")
483
- print(f"Sample: {results[0].text[:100]}...")
484
- else:
485
- print("No results found")
486
- except Exception as e:
487
- print(f"❌ Query failed: {e}")
488
-
489
  except Exception as e:
490
- print(f"❌ Retriever creation failed: {e}")
 
491
 
492
- # Test query engine creation (for tools.py)
493
- print("\n--- Testing Query Engine Creation ---")
494
  try:
495
- query_engine = get_persona_query_engine()
496
- print("✅ Query engine creation successful")
497
- print(" (This query engine can be used by tools.py)")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
498
  except Exception as e:
499
- print(f"❌ Query engine creation failed: {e}")
500
-
501
- print("\n=== Database System Testing Complete ===")
502
- print("\nNote: For tool testing, run tools.py or usage_example.py")
503
-
504
-
505
- # ============================================================================
506
- # MAIN EXECUTION
507
- # ============================================================================
508
 
509
  if __name__ == "__main__":
510
- # If this file is run directly, run tests
511
- print("Persona Database System Testing")
512
- print("=" * 50)
513
-
514
- # Set up logging for testing
515
  logging.basicConfig(level=logging.INFO)
516
 
517
- # Run database system tests
518
- test_persona_system()
 
 
 
 
 
 
 
519
 
520
- print("\n" + "=" * 50)
521
- print("Database testing complete!")
522
- print("\nFor tool testing, run:")
523
- print(" python tools.py")
524
- print(" python usage_example.py")
525
- print("\nFor full agent testing, run:")
526
- print(" python app.py")
 
1
  """
2
+ My Persona Database - RAG Implementation
3
 
4
+ This is where I build my persona database using what I learned about RAG.
5
+ I'm using:
6
+ - HuggingFace dataset with persona descriptions
7
+ - ChromaDB for vector storage (learned this is good for small projects)
8
+ - Embeddings to find similar personas
9
+ - LlamaIndex to tie it all together
10
 
11
+ The goal is to have a database I can query like "find me creative people"
12
+ and get back actual persona descriptions.
 
 
 
13
 
14
+ Note: I made this work in HuggingFace Spaces by keeping everything in memory
15
+ and using a smaller dataset so it doesn't crash.
 
 
 
 
 
 
 
 
16
  """
17
 
18
  import logging
19
  import os
20
+ from typing import List, Optional
21
  from pathlib import Path
22
 
23
+ # Core LlamaIndex stuff
24
  from llama_index.core.schema import Document
25
+ from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
 
26
  from llama_index.core.node_parser import SentenceSplitter
 
27
 
28
+ # For embeddings and vector storage
29
  from llama_index.embeddings.huggingface import HuggingFaceEmbedding
30
  from llama_index.vector_stores.chroma import ChromaVectorStore
31
 
32
+ # External stuff
33
+ try:
34
+ from datasets import load_dataset
35
+ CAN_LOAD_DATASETS = True
36
+ except ImportError:
37
+ CAN_LOAD_DATASETS = False
 
 
 
 
38
 
39
+ try:
40
+ import chromadb
41
+ CHROMADB_WORKS = True
42
+ except ImportError:
43
+ CHROMADB_WORKS = False
44
 
45
+ logger = logging.getLogger(__name__)
 
46
 
47
+ # My settings
48
+ PERSONA_DATASET = "dvilasuero/finepersonas-v0.1-tiny"
49
+ MAX_PERSONAS = 300 # Keep it small for HF Spaces
50
+ EMBEDDING_MODEL = "BAAI/bge-small-en-v1.5" # This one works well
51
+ CHUNK_SIZE = 400 # Smaller chunks work better
52
 
53
+ # Cache so I don't rebuild this every time
54
+ _my_persona_index = None
 
55
 
56
+ def make_sample_personas():
57
  """
58
+ Backup personas in case I can't download the real dataset
59
+ These are just examples but at least my agent will work
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  """
61
+ samples = [
62
+ "I'm a 28-year-old software developer from Seattle. I love hiking on weekends, coding in Python, and playing indie video games. I work at a tech startup and dream of building my own app someday.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
 
64
+ "I'm a 35-year-old high school teacher in Boston. I teach English literature and spend my free time writing poetry. I volunteer at the local animal shelter and love mystery novels.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
 
66
+ "I'm a 42-year-old chef who owns a small Italian restaurant in Chicago. I learned to cook from my grandmother and love experimenting with fusion cuisine. I teach cooking classes on Sundays.",
67
 
68
+ "I'm a 24-year-old graphic designer in Los Angeles. I freelance for indie game studios and love creating digital art. My hobbies include skateboarding and visiting coffee shops for inspiration.",
 
 
 
 
69
 
70
+ "I'm a 39-year-old veterinarian in Denver. I specialize in wildlife rehabilitation and spend weekends hiking in the mountains. I volunteer at the local zoo and love photography.",
71
 
72
+ "I'm a 31-year-old journalist in New York covering tech trends. I write a weekly newsletter about AI and automation. I practice yoga daily and love exploring the city's food scene.",
73
+
74
+ "I'm a 45-year-old musician who plays guitar in a blues band. I teach music lessons during the day and perform at local venues on weekends. I collect vintage vinyl records.",
75
+
76
+ "I'm a 27-year-old marine biologist studying coral reefs in San Diego. I love scuba diving and underwater photography. I'm passionate about ocean conservation and climate change.",
77
+
78
+ "I'm a 33-year-old architect designing sustainable buildings in Portland. I believe in green construction and volunteer for Habitat for Humanity. I enjoy urban sketching.",
79
+
80
+ "I'm a 29-year-old data scientist working in healthcare analytics in Austin. I love solving puzzles and play chess competitively. I brew craft beer as a hobby."
81
+ ]
82
+
83
+ logger.info(f"Created {len(samples)} backup personas")
84
+ return samples
85
 
86
+ def download_personas():
87
  """
88
+ Try to get the real persona dataset from HuggingFace
89
+ If that fails, use my backup personas
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  """
91
+ logger.info("Trying to download persona dataset...")
92
+
93
+ if not CAN_LOAD_DATASETS:
94
+ logger.warning("Can't load datasets library, using backups")
95
+ return make_sample_personas()
96
 
97
  try:
98
+ # Load the dataset (streaming to save memory)
99
+ dataset = load_dataset(PERSONA_DATASET, split="train", streaming=True)
 
 
 
 
 
 
 
100
 
101
+ personas = []
102
+ for i, item in enumerate(dataset):
103
+ if i >= MAX_PERSONAS: # Don't go over my limit
104
+ break
105
+
106
+ persona_text = item.get("persona", "")
107
+ if persona_text.strip():
108
+ personas.append(f"Person {i+1}: {persona_text}")
109
+
110
+ if (i + 1) % 50 == 0:
111
+ logger.info(f"Downloaded {i+1} personas...")
112
 
113
+ logger.info(f"Got {len(personas)} personas from HuggingFace!")
114
+ return personas
115
 
116
  except Exception as e:
117
+ logger.warning(f"Download failed: {e}, using backups")
118
+ return make_sample_personas()
 
 
 
 
 
119
 
120
+ def make_documents(personas):
121
  """
122
+ Turn my persona strings into LlamaIndex documents
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  """
124
+ logger.info(f"Making documents from {len(personas)} personas...")
125
+
126
+ docs = []
127
+ for i, persona_text in enumerate(personas):
128
+ doc = Document(
129
+ text=persona_text,
130
+ metadata={
131
+ "source": f"persona_{i}",
132
+ "persona_id": i,
133
+ "type": "persona_description"
134
+ }
135
+ )
136
+ docs.append(doc)
137
 
138
+ logger.info(f"Created {len(docs)} documents")
139
+ return docs
140
+
141
+ def setup_vector_store():
142
+ """
143
+ Set up ChromaDB for storing my vectors
144
+ Using in-memory so it works in HuggingFace Spaces
145
+ """
146
+ if not CHROMADB_WORKS:
147
+ logger.error("ChromaDB not available!")
148
+ return None
149
+
150
  try:
151
+ logger.info("Setting up in-memory vector store...")
 
 
 
 
 
 
152
 
153
+ # In-memory client (no files to worry about)
154
+ client = chromadb.Client()
155
+ collection = client.get_or_create_collection("my_personas")
 
 
 
 
 
 
 
 
 
 
 
156
 
157
+ # Wrap it for LlamaIndex
158
+ vector_store = ChromaVectorStore(chroma_collection=collection)
159
+
160
+ logger.info("Vector store ready!")
161
+ return vector_store
162
 
163
  except Exception as e:
164
+ logger.error(f"Vector store setup failed: {e}")
165
+ return None
 
166
 
167
+ def build_persona_index():
 
 
 
 
168
  """
169
+ Build my persona index from scratch
170
+ This might take a minute the first time
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  """
172
+ logger.info("Building persona index...")
173
 
174
  try:
175
+ # Step 1: Get the persona data
176
+ personas = download_personas()
177
+ if not personas:
178
+ logger.error("No persona data available")
179
+ return None
180
+
181
+ # Step 2: Make documents
182
+ documents = make_documents(personas)
183
 
184
+ # Step 3: Set up vector storage
185
+ vector_store = setup_vector_store()
186
+ if not vector_store:
187
+ logger.error("Can't create vector store")
188
+ return None
189
 
190
+ # Step 4: Set up embeddings
 
191
  try:
 
192
  embed_model = HuggingFaceEmbedding(model_name=EMBEDDING_MODEL)
193
+ logger.info(f"Loaded embedding model: {EMBEDDING_MODEL}")
194
+ except Exception as e:
195
+ logger.error(f"Can't load embeddings: {e}")
196
+ return None
 
 
 
 
 
 
 
 
 
 
 
 
 
197
 
198
+ # Step 5: Build the index
199
+ logger.info("Creating vector index... this might take a moment")
200
 
201
+ index = VectorStoreIndex.from_documents(
202
+ documents=documents,
 
 
 
 
 
 
203
  vector_store=vector_store,
204
+ embed_model=embed_model,
205
+ show_progress=True
206
  )
207
 
208
+ logger.info("Persona index built successfully!")
209
  return index
210
 
211
  except Exception as e:
212
+ logger.error(f"Index building failed: {e}")
213
+ return None
 
 
 
 
 
 
 
214
 
215
  def get_persona_index():
216
  """
217
+ Get my persona index (builds it if needed, caches it if possible)
218
+ """
219
+ global _my_persona_index
220
 
221
+ if _my_persona_index is None:
222
+ logger.info("Building persona index for the first time...")
223
+ _my_persona_index = build_persona_index()
224
+ else:
225
+ logger.info("Using cached persona index")
226
 
227
+ return _my_persona_index
 
 
 
228
 
229
+ def get_persona_query_engine(llm=None):
 
230
  """
231
+ Get a query engine I can use to search my personas
232
+ This is what gets called from my tools
 
 
 
 
 
233
  """
234
  try:
235
+ index = get_persona_index()
236
+ if index is None:
237
+ logger.warning("No persona index available")
238
+ return None
239
 
240
+ # Make the query engine
 
 
 
241
  query_engine = index.as_query_engine(
242
+ llm=llm, # Use the LLM from my agent
243
+ response_mode="tree_summarize", # Good for combining multiple results
244
+ similarity_top_k=3, # Get top 3 matches
245
+ streaming=False
246
  )
247
 
248
+ logger.info("Persona query engine ready")
249
  return query_engine
250
 
251
  except Exception as e:
252
+ logger.error(f"Query engine creation failed: {e}")
253
+ return None
 
254
 
255
+ def test_my_personas():
 
 
 
 
256
  """
257
+ Test that my persona system works
 
 
 
258
  """
259
+ print("\n=== Testing My Persona Database ===")
260
 
261
+ # Check dependencies
262
+ print(f"Datasets available: {CAN_LOAD_DATASETS}")
263
+ print(f"ChromaDB available: {CHROMADB_WORKS}")
 
 
 
 
 
264
 
265
+ if not CHROMADB_WORKS:
266
+ print(" ChromaDB missing - persona database won't work")
267
+ return False
 
 
 
 
 
268
 
269
+ # Test data loading
270
+ print("\nTesting persona loading...")
271
  try:
272
+ personas = download_personas()
273
+ print(f"✅ Got {len(personas)} personas")
274
+ if personas:
275
+ print(f"Sample: {personas[0][:100]}...")
276
  except Exception as e:
277
+ print(f"❌ Persona loading failed: {e}")
278
+ return False
 
 
 
 
 
 
 
 
279
 
280
+ # Test vector store
281
+ print("\nTesting vector store...")
282
  try:
283
+ vector_store = setup_vector_store()
284
+ if vector_store:
285
+ print("✅ Vector store created")
286
+ else:
287
+ print("❌ Vector store failed")
288
+ return False
 
 
 
 
 
 
 
 
289
  except Exception as e:
290
+ print(f"❌ Vector store error: {e}")
291
+ return False
292
 
293
+ # Test index building (small test)
294
+ print("\nTesting index building...")
295
  try:
296
+ # Use just a few personas for testing
297
+ test_personas = make_sample_personas()[:3]
298
+ test_docs = make_documents(test_personas)
299
+
300
+ vector_store = setup_vector_store()
301
+ embed_model = HuggingFaceEmbedding(model_name=EMBEDDING_MODEL)
302
+
303
+ index = VectorStoreIndex.from_documents(
304
+ documents=test_docs,
305
+ vector_store=vector_store,
306
+ embed_model=embed_model
307
+ )
308
+
309
+ print("✅ Index building works")
310
+
311
+ # Test a simple query
312
+ query_engine = index.as_query_engine(similarity_top_k=1)
313
+ results = query_engine.query("software developer")
314
+ print("✅ Query test passed")
315
+
316
+ return True
317
+
318
  except Exception as e:
319
+ print(f"❌ Index test failed: {e}")
320
+ return False
 
 
 
 
 
 
 
321
 
322
  if __name__ == "__main__":
323
+ # Test my persona system
324
+ import logging
 
 
 
325
  logging.basicConfig(level=logging.INFO)
326
 
327
+ print("Testing My Persona Database System")
328
+ print("=" * 40)
329
+
330
+ success = test_my_personas()
331
+
332
+ if success:
333
+ print("\n✅ Persona database is working!")
334
+ else:
335
+ print("\n❌ Persona database has issues")
336
 
337
+ print("\nThis system is optimized for HuggingFace Spaces:")
338
+ print("- Uses in-memory storage (no files)")
339
+ print("- Limited personas (saves memory)")
340
+ print("- Fallback data (works offline)")
341
+ print("- Fast startup (cached building)")
 
 
test_hf_space.py ADDED
@@ -0,0 +1,297 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Test Everything - Making Sure My GAIA Agent Works
3
+
4
+ I'm nervous about submitting my final project, so I made this test script
5
+ to check that everything works properly before I deploy to HuggingFace Spaces.
6
+
7
+ This tests:
8
+ - All my dependencies are installed
9
+ - My tools work correctly
10
+ - My persona database loads
11
+ - My agent can be created
12
+ - Everything runs in HF Space environment
13
+
14
+ If this passes, I should be good to go for the GAIA evaluation!
15
+ """
16
+
17
+ import sys
18
+ import os
19
+ import logging
20
+ import traceback
21
+
22
+ # Setup logging so I can see what's happening
23
+ logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
24
+ logger = logging.getLogger(__name__)
25
+
26
+ def check_my_dependencies():
27
+ """
28
+ Make sure I have all the packages I need
29
+ """
30
+ print("\n📦 Checking My Dependencies...")
31
+
32
+ required = [
33
+ "gradio", "requests", "pandas",
34
+ "llama_index.core", "llama_index.llms.huggingface_api",
35
+ "llama_index.embeddings.huggingface", "llama_index.vector_stores.chroma"
36
+ ]
37
+
38
+ results = {}
39
+
40
+ for package in required:
41
+ try:
42
+ __import__(package)
43
+ print(f"✅ {package}")
44
+ results[package] = True
45
+ except ImportError as e:
46
+ print(f"❌ {package}: {e}")
47
+ results[package] = False
48
+
49
+ # Check optional ones
50
+ optional = ["chromadb", "datasets", "duckduckgo_search"]
51
+
52
+ for package in optional:
53
+ try:
54
+ __import__(package)
55
+ print(f"✅ {package} (optional)")
56
+ results[package] = True
57
+ except ImportError:
58
+ print(f"⚠️ {package} (optional) - missing")
59
+ results[package] = False
60
+
61
+ return results
62
+
63
+ def check_my_environment():
64
+ """
65
+ Check if I'm in the right environment and have API keys
66
+ """
67
+ print("\n🌍 Checking My Environment...")
68
+
69
+ env = {
70
+ "python_version": sys.version.split()[0],
71
+ "platform": sys.platform,
72
+ "working_dir": os.getcwd(),
73
+ "is_hf_space": bool(os.getenv("SPACE_HOST")),
74
+ "has_hf_token": bool(os.getenv("HF_TOKEN")),
75
+ "has_openai_key": bool(os.getenv("OPENAI_API_KEY"))
76
+ }
77
+
78
+ print(f"✅ Python {env['python_version']}")
79
+ print(f"✅ Platform: {env['platform']}")
80
+ print(f"✅ Working in: {env['working_dir']}")
81
+
82
+ if env['is_hf_space']:
83
+ print("✅ Running in HuggingFace Space")
84
+ else:
85
+ print("ℹ️ Running locally (not in HF Space)")
86
+
87
+ if env['has_openai_key'] or env['has_hf_token']:
88
+ print("✅ Have at least one API key")
89
+ else:
90
+ print("⚠️ No API keys found - might not work")
91
+
92
+ return env
93
+
94
+ def test_my_tools():
95
+ """
96
+ Test that all my tools work properly
97
+ """
98
+ print("\n🔧 Testing My Tools...")
99
+
100
+ try:
101
+ from tools import get_my_tools
102
+
103
+ # Test creating tools without LLM first
104
+ tools = get_my_tools()
105
+ print(f"✅ Created {len(tools)} tools")
106
+
107
+ # List what I got
108
+ for tool in tools:
109
+ tool_name = tool.metadata.name
110
+ print(f" - {tool_name}")
111
+
112
+ # Test some basic functions
113
+ print("\nTesting basic functions...")
114
+
115
+ from tools import do_math, analyze_file
116
+
117
+ # Test calculator
118
+ result = do_math("10 + 5 * 2")
119
+ print(f"✅ Calculator: 10 + 5 * 2 = {result}")
120
+
121
+ # Test file analyzer
122
+ test_csv = "name,age\nAlice,25\nBob,30"
123
+ result = analyze_file(test_csv, "csv")
124
+ print(f"✅ File analyzer works")
125
+
126
+ return True
127
+
128
+ except Exception as e:
129
+ print(f"❌ Tool testing failed: {e}")
130
+ traceback.print_exc()
131
+ return False
132
+
133
+ def test_my_persona_database():
134
+ """
135
+ Test my persona database system
136
+ """
137
+ print("\n👥 Testing My Persona Database...")
138
+
139
+ try:
140
+ from my_retriever import test_my_personas
141
+
142
+ # Run the built-in test
143
+ success = test_my_personas()
144
+
145
+ if success:
146
+ print("✅ Persona database works!")
147
+ else:
148
+ print("⚠️ Persona database issues (agent will still work)")
149
+
150
+ return success
151
+
152
+ except Exception as e:
153
+ print(f"⚠️ Persona database test failed: {e}")
154
+ print(" This is OK - agent can work without it")
155
+ return False
156
+
157
+ def test_my_agent():
158
+ """
159
+ Test that I can create my agent and it works
160
+ """
161
+ print("\n🤖 Testing My Agent...")
162
+
163
+ try:
164
+ # Import what I need
165
+ from llama_index.core.agent.workflow import AgentWorkflow
166
+ from tools import get_my_tools
167
+
168
+ print("Testing LLM setup...")
169
+
170
+ # Try to create an LLM
171
+ llm = None
172
+ openai_key = os.getenv("OPENAI_API_KEY")
173
+ hf_token = os.getenv("HF_TOKEN")
174
+
175
+ if openai_key:
176
+ try:
177
+ from llama_index.llms.openai import OpenAI
178
+ llm = OpenAI(api_key=openai_key, model="gpt-4o-mini", max_tokens=50)
179
+ print("✅ OpenAI LLM works")
180
+ except Exception as e:
181
+ print(f"⚠️ OpenAI failed: {e}")
182
+
183
+ if llm is None and hf_token:
184
+ try:
185
+ from llama_index.llms.huggingface_api import HuggingFaceInferenceAPI
186
+ llm = HuggingFaceInferenceAPI(
187
+ model_name="Qwen/Qwen2.5-Coder-32B-Instruct",
188
+ token=hf_token,
189
+ max_new_tokens=50
190
+ )
191
+ print("✅ HuggingFace LLM works")
192
+ except Exception as e:
193
+ print(f"⚠️ HuggingFace failed: {e}")
194
+
195
+ if llm is None:
196
+ print("❌ No LLM available - can't test agent")
197
+ return False
198
+
199
+ # Test creating tools with LLM
200
+ tools = get_my_tools(llm)
201
+ print(f"✅ Got {len(tools)} tools with LLM")
202
+
203
+ # Create the agent
204
+ agent = AgentWorkflow.from_tools_or_functions(
205
+ tools_or_functions=tools,
206
+ llm=llm,
207
+ system_prompt="You are my test assistant."
208
+ )
209
+ print("✅ Agent created successfully")
210
+
211
+ # Test a simple question
212
+ import asyncio
213
+
214
+ async def test_simple_question():
215
+ try:
216
+ handler = agent.run(user_msg="What is 3 + 4?")
217
+ result = await handler
218
+ return str(result)
219
+ except Exception as e:
220
+ return f"Error: {e}"
221
+
222
+ # Run the test
223
+ loop = asyncio.new_event_loop()
224
+ asyncio.set_event_loop(loop)
225
+ try:
226
+ answer = loop.run_until_complete(test_simple_question())
227
+ print(f"✅ Agent answered: {answer[:100]}...")
228
+ finally:
229
+ loop.close()
230
+
231
+ print("✅ My agent is fully working!")
232
+ return True
233
+
234
+ except Exception as e:
235
+ print(f"❌ Agent test failed: {e}")
236
+ traceback.print_exc()
237
+ return False
238
+
239
+ def run_all_my_tests():
240
+ """
241
+ Run every test I can think of
242
+ """
243
+ print("🎯 Testing My GAIA Agent - Final Project Check")
244
+ print("=" * 50)
245
+
246
+ # Run all the tests
247
+ deps_ok = check_my_dependencies()
248
+ env_info = check_my_environment()
249
+ tools_ok = test_my_tools()
250
+ personas_ok = test_my_persona_database()
251
+ agent_ok = test_my_agent()
252
+
253
+ # Check critical dependencies
254
+ critical = ["llama_index.core", "gradio", "requests"]
255
+ critical_ok = all(deps_ok.get(dep, False) for dep in critical)
256
+
257
+ # Summary
258
+ print("\n" + "=" * 50)
259
+ print("📊 MY TEST RESULTS")
260
+ print("=" * 50)
261
+
262
+ print(f"Critical Dependencies: {'✅ GOOD' if critical_ok else '❌ BAD'}")
263
+ print(f"My Tools: {'✅ GOOD' if tools_ok else '❌ BAD'}")
264
+ print(f"Persona Database: {'✅ GOOD' if personas_ok else '⚠️ OPTIONAL'}")
265
+ print(f"My Agent: {'✅ GOOD' if agent_ok else '❌ BAD'}")
266
+
267
+ # Final verdict
268
+ ready_for_gaia = critical_ok and tools_ok and agent_ok
269
+
270
+ print("\n" + "=" * 50)
271
+ if ready_for_gaia:
272
+ print("🎉 I'M READY FOR GAIA!")
273
+ print("My agent should work properly in HuggingFace Spaces.")
274
+ print("Time to deploy and hope I get 30%+ to pass! 🤞")
275
+
276
+ if not personas_ok:
277
+ print("\nNote: Persona database might not work, but that's OK.")
278
+ else:
279
+ print("😰 NOT READY YET")
280
+ print("I need to fix the issues above before submitting.")
281
+ print("Don't want to fail the course!")
282
+
283
+ print("=" * 50)
284
+
285
+ return ready_for_gaia
286
+
287
+ if __name__ == "__main__":
288
+ # Run all my tests
289
+ success = run_all_my_tests()
290
+
291
+ # Exit with appropriate code
292
+ if success:
293
+ print("\n🚀 All systems go! Ready to deploy!")
294
+ sys.exit(0)
295
+ else:
296
+ print("\n🛑 Need to fix issues first!")
297
+ sys.exit(1)
tools.py CHANGED
@@ -1,15 +1,15 @@
1
  """
2
- tools.py - Agent Tools for GAIA Benchmark (Course Didactic Structure)
3
 
4
- This file follows the course approach of separating:
5
- 1. Raw functions (the actual functionality)
6
- 2. Tool wrappers (FunctionTool and QueryEngineTool creation)
7
 
8
- This makes it easier to understand and debug each component separately.
9
- Each tool addresses specific GAIA benchmark needs while demonstrating course concepts.
10
-
11
- create_persona_database_tool() # QueryEngineTool creation
12
- get_all_tools() # All tools collection
 
13
  """
14
 
15
  import logging
@@ -19,639 +19,319 @@ import random
19
  from typing import List
20
  import chromadb
21
 
22
- # LlamaIndex imports
23
  from llama_index.core.tools import FunctionTool, QueryEngineTool
24
  from llama_index.core import VectorStoreIndex
25
  from llama_index.embeddings.huggingface import HuggingFaceEmbedding
26
  from llama_index.vector_stores.chroma import ChromaVectorStore
27
  from llama_index.llms.huggingface_api import HuggingFaceInferenceAPI
28
 
29
- # Setup logging
30
  logger = logging.getLogger(__name__)
31
 
32
- # ============================================================================
33
- # PART 1: RAW FUNCTIONS (The actual functionality)
34
- # ============================================================================
35
- # These are the core functions that do the actual work.
36
- # They can be tested independently and are easy to understand.
37
 
38
- def web_search(query: str) -> str:
39
  """
40
- Search the web for information using DuckDuckGo.
41
-
42
- This function handles the actual web searching logic.
43
- Critical for GAIA questions requiring current information.
44
-
45
- Args:
46
- query (str): The search query/question
47
-
48
- Returns:
49
- str: Formatted search results with titles, content, and URLs
50
-
51
- Why this is essential for GAIA:
52
- - Many GAIA questions need current information (news, prices, events)
53
- - LLMs have knowledge cutoffs and may not know recent facts
54
- - Web search provides access to the latest information
55
  """
56
- logger.info(f"🔍 Web search requested: {query}")
57
 
58
  try:
59
- # Import DuckDuckGo search - free search API
60
  from duckduckgo_search import DDGS
61
 
62
- # Perform the search with a reasonable limit
63
  with DDGS() as ddgs:
64
- # Get top 3 results to avoid overwhelming the LLM
65
  results = list(ddgs.text(query, max_results=3))
66
 
67
  if not results:
68
- logger.warning("No search results found")
69
- return "No search results found for this query."
70
 
71
- # Format results in a clean, readable way
72
- formatted_results = []
73
  for i, result in enumerate(results, 1):
74
- formatted_result = (
75
- f"Result {i}:\n"
76
- f"Title: {result['title']}\n"
77
- f"Content: {result['body']}\n"
78
- f"URL: {result['href']}\n"
79
- )
80
- formatted_results.append(formatted_result)
81
 
82
- final_result = "\n".join(formatted_results)
83
- logger.info(f"✅ Web search completed: {len(results)} results found")
84
- return final_result
85
 
86
  except ImportError:
87
- error_msg = "DuckDuckGo search library not available. Please install duckduckgo-search."
88
- logger.error(error_msg)
89
- return error_msg
90
  except Exception as e:
91
- error_msg = f"Search error: {str(e)}"
92
- logger.error(f"Web search failed: {e}")
93
- return error_msg
94
-
95
 
96
- def calculate(expression: str) -> str:
97
  """
98
- Safely evaluate mathematical expressions.
99
-
100
- This function handles mathematical calculations with safety measures.
101
- CRITICAL for GAIA because many questions involve precise calculations.
102
-
103
- Args:
104
- expression (str): Mathematical expression (e.g., "2 + 2", "sqrt(16)", "sin(pi/2)")
105
-
106
- Returns:
107
- str: The result of the calculation or an error message
108
-
109
- Why this is essential for GAIA:
110
- - GAIA has many mathematical questions (percentages, conversions, etc.)
111
- - LLMs can make arithmetic errors, especially with complex math
112
- - Exact numerical accuracy is required (GAIA uses exact match scoring)
113
-
114
- Examples:
115
- calculate("2 + 2") → "4"
116
- calculate("15% of 847") → calculate("0.15 * 847") → "127.05"
117
- calculate("sqrt(16)") → "4.0"
118
  """
119
- logger.info(f"🧮 Calculation requested: {expression}")
120
 
121
  try:
122
- # Create a safe environment for evaluation
123
- # Only allow mathematical functions, no dangerous operations
124
- allowed_names = {
125
- # Include all math module functions (sin, cos, sqrt, log, etc.)
126
- k: v for k, v in math.__dict__.items() if not k.startswith("__")
 
 
 
127
  }
128
 
129
- # Add safe Python functions
130
- allowed_names.update({
131
- "abs": abs, # Absolute value
132
- "round": round, # Rounding
133
- "min": min, # Minimum
134
- "max": max, # Maximum
135
- "sum": sum, # Sum of iterables
136
- "pow": pow, # Power function
137
- })
138
-
139
- # Add mathematical constants
140
- allowed_names.update({
141
- "pi": math.pi, # π
142
- "e": math.e, # Euler's number
143
- })
144
-
145
- # Evaluate the expression safely
146
- # __builtins__ = {} prevents dangerous functions like open(), exec()
147
- result = eval(expression, {"__builtins__": {}}, allowed_names)
148
-
149
- result_str = str(result)
150
- logger.info(f"✅ Calculation result: {expression} = {result_str}")
151
- return result_str
152
 
153
  except ZeroDivisionError:
154
- error_msg = "Error: Division by zero"
155
- logger.error(error_msg)
156
- return error_msg
157
- except ValueError as e:
158
- error_msg = f"Error: Invalid mathematical operation - {str(e)}"
159
- logger.error(error_msg)
160
- return error_msg
161
- except SyntaxError:
162
- error_msg = "Error: Invalid mathematical expression syntax"
163
- logger.error(error_msg)
164
- return error_msg
165
  except Exception as e:
166
- error_msg = f"Calculation error: {str(e)}"
167
- logger.error(f"Unexpected calculation error: {e}")
168
- return error_msg
169
-
170
 
171
- def analyze_file(file_content: str, file_type: str = "text") -> str:
172
  """
173
- Analyze file content and extract relevant information.
174
-
175
- This function processes different file types for analysis.
176
- Useful for GAIA questions that include file attachments.
177
-
178
- Args:
179
- file_content (str): The content of the file
180
- file_type (str): Type of file ("text", "csv", "json", etc.)
181
-
182
- Returns:
183
- str: Analysis results or extracted information
184
-
185
- Why this helps with GAIA:
186
- - Some GAIA questions include data files to analyze
187
- - Questions might ask for statistics, summaries, or specific data extraction
188
- - File processing shows practical data analysis skills
189
  """
190
- logger.info(f"📊 File analysis requested for {file_type} file")
191
 
192
  try:
193
  if file_type.lower() == "csv":
194
- # For CSV files, provide basic statistics
195
- lines = file_content.strip().split('\n')
196
  if not lines:
197
  return "Empty file"
198
 
199
- # Count rows and columns (assuming first row is header)
200
- num_rows = len(lines) - 1 # Subtract header
201
- if lines:
202
- num_cols = len(lines[0].split(','))
203
- analysis = (
204
- f"CSV Analysis:\n"
205
- f"- Rows: {num_rows}\n"
206
- f"- Columns: {num_cols}\n"
207
- f"- Headers: {lines[0]}"
208
- )
209
- if num_rows > 0:
210
- analysis += f"\n- First data row: {lines[1] if len(lines) > 1 else 'None'}"
211
- return analysis
212
-
213
- elif file_type.lower() in ["txt", "text"]:
214
- # For text files, provide basic statistics
215
- lines = file_content.split('\n')
216
- words = file_content.split()
217
- chars = len(file_content)
218
 
219
- return (
220
- f"Text Analysis:\n"
221
- f"- Lines: {len(lines)}\n"
222
- f"- Words: {len(words)}\n"
223
- f"- Characters: {chars}"
224
- )
225
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
  else:
227
- # For other file types, return content with basic info
228
- preview = file_content[:1000] + '...' if len(file_content) > 1000 else file_content
229
  return f"File content ({file_type}):\n{preview}"
230
 
231
  except Exception as e:
232
- error_msg = f"File analysis error: {str(e)}"
233
- logger.error(error_msg)
234
- return error_msg
235
-
236
 
237
  def get_weather(location: str) -> str:
238
  """
239
- Get dummy weather information for a location.
240
-
241
- This is a simplified weather function for demonstration.
242
- In a real implementation, you'd connect to a weather API like OpenWeatherMap.
243
-
244
- Args:
245
- location (str): City or location name
246
-
247
- Returns:
248
- str: Weather description with temperature
249
-
250
- Note: This is a dummy implementation for course purposes.
251
- Real weather data would require an API key and actual weather service.
252
  """
253
- logger.info(f"🌤️ Weather requested for: {location}")
254
-
255
- # Dummy weather data for demonstration
256
- weather_conditions = [
257
- {"condition": "Sunny", "temp_c": 25, "humidity": 60},
258
- {"condition": "Cloudy", "temp_c": 20, "humidity": 70},
259
- {"condition": "Rainy", "temp_c": 15, "humidity": 85},
260
- {"condition": "Windy", "temp_c": 22, "humidity": 55},
261
- {"condition": "Clear", "temp_c": 28, "humidity": 45}
262
  ]
263
 
264
- # Randomly select weather (in real implementation, this would be API call)
265
- weather = random.choice(weather_conditions)
266
-
267
- result = (
268
- f"Weather in {location.title()}:\n"
269
- f"Condition: {weather['condition']}\n"
270
- f"Temperature: {weather['temp_c']}°C\n"
271
- f"Humidity: {weather['humidity']}%"
272
- )
273
 
274
- logger.info(f"Weather result: {weather['condition']}, {weather['temp_c']}°C")
275
- return result
 
 
276
 
 
 
 
277
 
278
- # ============================================================================
279
- # PART 2: PERSONA DATABASE SETUP (QueryEngine creation)
280
- # ============================================================================
281
- # This sets up the persona database query engine following the course pattern.
282
-
283
- def create_persona_query_engine():
284
  """
285
- Create a query engine for the persona database following course pattern.
286
-
287
- This demonstrates the exact approach from the course:
288
- 1. Connect to existing ChromaDB database
289
- 2. Create VectorStoreIndex from the stored vectors
290
- 3. Configure LLM for response generation
291
- 4. Create QueryEngine with specific settings
292
-
293
- Returns:
294
- QueryEngine: Ready-to-use query engine for persona database
295
-
296
- Why QueryEngine vs simple retrieval:
297
- - QueryEngine combines retrieval + LLM generation
298
- - Provides natural, conversational responses
299
- - Can synthesize information from multiple personas
300
- - Better for complex questions requiring reasoning
301
  """
302
- logger.info("🏗️ Creating persona database query engine...")
303
 
304
  try:
305
- # Step 1: Connect to existing ChromaDB (created by retriever.py)
306
- db = chromadb.PersistentClient(path="./alfred_chroma_db")
307
- chroma_collection = db.get_or_create_collection("alfred")
308
- vector_store = ChromaVectorStore(chroma_collection=chroma_collection)
309
- logger.info("✅ Connected to ChromaDB")
310
 
311
- # Step 2: Set up embedding model (same as used during indexing)
312
  embed_model = HuggingFaceEmbedding(model_name="BAAI/bge-small-en-v1.5")
313
- logger.info("✅ Embedding model configured")
314
 
315
- # Step 3: Create VectorStoreIndex from existing data
316
  index = VectorStoreIndex.from_vector_store(
317
- vector_store=vector_store,
318
  embed_model=embed_model
319
  )
320
- logger.info("✅ Vector index created")
321
-
322
- # Step 4: Configure LLM for response generation
323
- # Try to get LLM from settings first, then fallback
324
- try:
325
- from llama_index.core import Settings
326
- llm = Settings.llm
327
-
328
- if llm is None:
329
- # Fallback to HuggingFace LLM
330
- hf_token = os.getenv("HF_TOKEN")
331
- if hf_token:
332
- llm = HuggingFaceInferenceAPI(
333
- model_name="Qwen/Qwen2.5-Coder-32B-Instruct",
334
- token=hf_token,
335
- max_new_tokens=512,
336
- temperature=0.1
337
- )
338
- logger.info("✅ Using HuggingFace LLM")
339
- else:
340
- logger.warning("⚠️ No LLM available, query engine will use default")
341
- llm = None
342
- except Exception:
343
- logger.warning("⚠️ Could not configure LLM, using default")
344
- llm = None
345
 
346
- # Step 5: Create QueryEngine with optimized settings
347
  query_engine = index.as_query_engine(
348
- llm=llm,
349
- response_mode="tree_summarize", # Good for combining multiple sources
350
- similarity_top_k=5, # Retrieve top 5 most relevant personas
351
- streaming=False # Disable streaming for stability
352
  )
353
 
354
- logger.info("Persona query engine created successfully")
355
  return query_engine
356
 
357
  except Exception as e:
358
- logger.error(f" Error creating persona query engine: {e}")
359
- raise RuntimeError(f"Failed to create persona query engine: {e}")
360
-
361
 
362
- # ============================================================================
363
- # PART 3: TOOL WRAPPERS (Converting functions to tools)
364
- # ============================================================================
365
- # This section creates the actual tools that the agent can use.
366
- # Each tool wraps a function with metadata for the LLM to understand.
367
 
368
- # Web Search Tool
369
- web_search_tool = FunctionTool.from_defaults(
370
- fn=web_search,
371
  name="web_search",
372
- description=(
373
- "Search the web for current information, recent events, statistics, "
374
- "facts, or any information not in the LLM's training data. "
375
- "Use this when you need up-to-date or specific factual information. "
376
- "Essential for GAIA questions about current events, prices, or recent developments."
377
- )
378
  )
379
 
380
- # Calculator Tool
381
- calculator_tool = FunctionTool.from_defaults(
382
- fn=calculate,
383
- name="calculator",
384
- description=(
385
- "Perform mathematical calculations and evaluate mathematical expressions. "
386
- "Supports basic arithmetic (+, -, *, /), advanced math functions (sqrt, sin, cos, log), "
387
- "and mathematical constants (pi, e). Use this for any numerical computations, "
388
- "percentage calculations, unit conversions, or statistical operations. "
389
- "CRITICAL for GAIA mathematical questions to ensure accuracy."
390
- )
391
  )
392
 
393
- # File Analysis Tool
394
- file_analysis_tool = FunctionTool.from_defaults(
395
  fn=analyze_file,
396
- name="file_analyzer",
397
- description=(
398
- "Analyze file contents including CSV files, text files, and other data files. "
399
- "Can extract statistics, summarize content, and process structured data. "
400
- "Use this when GAIA questions involve analyzing attached files or datasets."
401
- )
402
  )
403
 
404
- # Weather Tool (demonstration)
405
  weather_tool = FunctionTool.from_defaults(
406
  fn=get_weather,
407
  name="weather_tool",
408
- description=(
409
- "Get weather information for a specific location. "
410
- "Note: This is a demo implementation with dummy data. "
411
- "Use when questions ask about weather conditions."
412
- )
413
  )
414
 
415
- # Persona Database Query Engine Tool
416
- def create_persona_database_tool():
417
  """
418
- Create the persona database tool using QueryEngineTool.
419
-
420
- This follows the exact course pattern for creating QueryEngineTool.
421
- The tool combines retrieval with LLM generation for natural responses.
422
-
423
- Returns:
424
- QueryEngineTool: Tool for querying the persona database
425
  """
426
- logger.info("🛠️ Creating persona database tool...")
427
 
428
  try:
429
- # First ensure we have the persona data (this will create it if needed)
430
  try:
431
- from retriever import create_persona_index
432
- # This creates the index if it doesn't exist
433
- create_persona_index()
434
- logger.info("✅ Persona index ready")
435
- except Exception as e:
436
- logger.warning(f"⚠️ Could not ensure persona index: {e}")
437
 
438
- # Create the query engine
439
- query_engine = create_persona_query_engine()
 
440
 
441
- # Create the QueryEngineTool following course pattern
442
  persona_tool = QueryEngineTool.from_defaults(
443
  query_engine=query_engine,
444
  name="persona_database",
445
  description=(
446
- "Search and query a database of 5000 diverse personas with various backgrounds, "
447
- "interests, and professions. Use this to find people with specific characteristics, "
448
- "skills, or interests. Can answer questions like 'find writers', 'who likes travel', "
449
- "'scientists in the group', 'creative professionals', or 'people interested in technology'. "
450
- "Returns detailed information about matching personas with their backgrounds and interests."
451
  )
452
  )
453
 
454
- logger.info("Persona database tool created successfully")
455
  return persona_tool
456
 
457
  except Exception as e:
458
- logger.error(f" Error creating persona database tool: {e}")
459
- # Return None so the agent can still work without this tool
460
  return None
461
 
462
-
463
- # ============================================================================
464
- # PART 4: TOOL COLLECTION (Getting all tools together)
465
- # ============================================================================
466
-
467
- def get_all_tools() -> List:
468
  """
469
- Get all available tools for the GAIA agent.
470
-
471
- This function collects all tools and handles any creation errors gracefully.
472
- The agent will work with whatever tools are successfully created.
473
-
474
- Returns:
475
- List: All successfully created tools
476
  """
477
- logger.info("🔧 Collecting all tools...")
478
 
479
  tools = []
480
 
481
- # Add function-based tools (these should always work)
482
- try:
483
- tools.extend([
484
- web_search_tool,
485
- calculator_tool,
486
- file_analysis_tool,
487
- weather_tool
488
- ])
489
- logger.info(f"✅ Added {len(tools)} function-based tools")
490
- except Exception as e:
491
- logger.error(f"❌ Error adding function tools: {e}")
492
 
493
- # Add persona database tool (this might fail if database isn't ready)
494
- try:
495
- persona_tool = create_persona_database_tool()
496
- if persona_tool:
497
- tools.append(persona_tool)
498
- logger.info("✅ Added persona database tool")
499
- else:
500
- logger.warning("⚠️ Persona database tool not available")
501
- except Exception as e:
502
- logger.warning(f"⚠️ Could not create persona database tool: {e}")
503
 
504
- logger.info(f"🎯 Total tools available: {len(tools)}")
505
  for tool in tools:
506
- tool_name = getattr(tool.metadata, 'name', 'Unknown')
507
- logger.info(f" - {tool_name}")
508
 
509
  return tools
510
 
 
 
 
511
 
512
- # ============================================================================
513
- # PART 5: TESTING FUNCTIONS (For development and debugging)
514
- # ============================================================================
515
-
516
- def test_individual_functions():
517
  """
518
- Test each function individually to make sure they work.
519
- This helps with debugging and understanding what each function does.
520
  """
521
- print("\n=== Testing Individual Functions ===")
522
-
523
- # Test web search
524
- print("\n--- Testing Web Search Function ---")
525
- try:
526
- result = web_search("current year")
527
- print(f"Web search result: {result[:150]}...")
528
- print("✅ Web search function works")
529
- except Exception as e:
530
- print(f"❌ Web search failed: {e}")
531
 
532
  # Test calculator
533
- print("\n--- Testing Calculator Function ---")
534
- try:
535
- result = calculate("2 + 2 * 3")
536
- print(f"Calculator result (2 + 2 * 3): {result}")
537
- result = calculate("sqrt(16)")
538
- print(f"Calculator result (sqrt(16)): {result}")
539
- print("✅ Calculator function works")
540
- except Exception as e:
541
- print(f"❌ Calculator failed: {e}")
542
 
543
  # Test file analyzer
544
- print("\n--- Testing File Analysis Function ---")
545
- try:
546
- sample_csv = "name,age,city\nJohn,25,NYC\nJane,30,LA\nBob,35,SF"
547
- result = analyze_file(sample_csv, "csv")
548
- print(f"File analysis result: {result}")
549
- print("✅ File analysis function works")
550
- except Exception as e:
551
- print(f"❌ File analysis failed: {e}")
552
 
553
  # Test weather
554
- print("\n--- Testing Weather Function ---")
555
- try:
556
- result = get_weather("Paris")
557
- print(f"Weather result: {result}")
558
- print("✅ Weather function works")
559
- except Exception as e:
560
- print(f"❌ Weather failed: {e}")
561
-
562
-
563
- def test_tool_creation():
564
- """
565
- Test that all tools can be created successfully.
566
- """
567
- print("\n=== Testing Tool Creation ===")
568
 
569
- try:
570
- tools = get_all_tools()
571
- print(f"✅ Successfully created {len(tools)} tools")
572
-
573
- for tool in tools:
574
- tool_name = getattr(tool.metadata, 'name', 'Unknown')
575
- tool_desc = getattr(tool.metadata, 'description', 'No description')[:100]
576
- print(f" - {tool_name}: {tool_desc}...")
577
-
578
- except Exception as e:
579
- print(f"❌ Tool creation failed: {e}")
580
-
581
-
582
- def test_tool_functionality():
583
- """
584
- Test that tools can actually be called and return results.
585
- """
586
- print("\n=== Testing Tool Functionality ===")
587
-
588
- tools = get_all_tools()
589
 
590
- for tool in tools:
591
- tool_name = getattr(tool.metadata, 'name', 'Unknown')
592
- print(f"\n--- Testing {tool_name} ---")
593
-
594
- try:
595
- if tool_name == "calculator":
596
- # Test calculator tool
597
- result = tool.func("5 * 8")
598
- print(f"Calculator test (5 * 8): {result}")
599
-
600
- elif tool_name == "web_search":
601
- # Test web search (might be slow)
602
- print("Testing web search (this might take a moment)...")
603
- result = tool.func("Python programming")
604
- print(f"Web search test: {result[:100]}...")
605
-
606
- elif tool_name == "file_analyzer":
607
- # Test file analyzer
608
- test_data = "col1,col2\nval1,val2\nval3,val4"
609
- result = tool.func(test_data, "csv")
610
- print(f"File analyzer test: {result}")
611
-
612
- elif tool_name == "weather_tool":
613
- # Test weather tool
614
- result = tool.func("London")
615
- print(f"Weather test: {result}")
616
-
617
- elif tool_name == "persona_database":
618
- # Test persona database (might be slow on first run)
619
- print("Testing persona database (this might take a moment)...")
620
- # This would be an async call in real usage
621
- print("Persona database test skipped (requires async)")
622
-
623
- print(f"✅ {tool_name} test completed")
624
-
625
- except Exception as e:
626
- print(f"❌ {tool_name} test failed: {e}")
627
-
628
-
629
- # ============================================================================
630
- # MAIN EXECUTION (For testing when file is run directly)
631
- # ============================================================================
632
 
633
  if __name__ == "__main__":
634
- print("GAIA Agent Tools Testing")
635
- print("=" * 50)
636
-
637
- # Set up logging for testing
638
  logging.basicConfig(level=logging.INFO)
639
 
640
- # Test individual functions first
641
- test_individual_functions()
642
-
643
- # Test tool creation
644
- test_tool_creation()
645
-
646
- # Test tool functionality (optional - can be slow)
647
- response = input("\nRun tool functionality tests? (y/n): ")
648
- if response.lower() == 'y':
649
- test_tool_functionality()
650
- else:
651
- print("Skipping functionality tests")
652
-
653
- print("\n=== Tools Testing Complete ===")
654
- print("\nTo use these tools in your agent:")
655
- print("from tools import get_all_tools")
656
- print("tools = get_all_tools()")
657
-
 
1
  """
2
+ My Agent Tools
3
 
4
+ These are all the tools I'm giving my agent. I learned in the course that you need
5
+ to separate the actual functions from the tool wrappers.
 
6
 
7
+ Tools I'm building:
8
+ 1. Web search (for current info)
9
+ 2. Calculator (for math - super important for GAIA)
10
+ 3. File analyzer (for data questions)
11
+ 4. Weather tool (just for demo)
12
+ 5. Persona database (RAG with vector search)
13
  """
14
 
15
  import logging
 
19
  from typing import List
20
  import chromadb
21
 
22
+ # LlamaIndex stuff for creating tools
23
  from llama_index.core.tools import FunctionTool, QueryEngineTool
24
  from llama_index.core import VectorStoreIndex
25
  from llama_index.embeddings.huggingface import HuggingFaceEmbedding
26
  from llama_index.vector_stores.chroma import ChromaVectorStore
27
  from llama_index.llms.huggingface_api import HuggingFaceInferenceAPI
28
 
 
29
  logger = logging.getLogger(__name__)
30
 
31
+ # ========================================
32
+ # THE ACTUAL FUNCTIONS
33
+ # ========================================
 
 
34
 
35
+ def search_web(query: str) -> str:
36
  """
37
+ Search the web using DuckDuckGo
38
+ I'm using this instead of Google because it's free
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  """
40
+ logger.info(f"Searching for: {query}")
41
 
42
  try:
 
43
  from duckduckgo_search import DDGS
44
 
 
45
  with DDGS() as ddgs:
46
+ # Get top 3 results so I don't overwhelm the LLM
47
  results = list(ddgs.text(query, max_results=3))
48
 
49
  if not results:
50
+ return "No search results found."
 
51
 
52
+ # Format the results nicely
53
+ formatted = []
54
  for i, result in enumerate(results, 1):
55
+ formatted.append(f"""Result {i}:
56
+ Title: {result['title']}
57
+ Content: {result['body']}
58
+ URL: {result['href']}
59
+ """)
 
 
60
 
61
+ return "\n".join(formatted)
 
 
62
 
63
  except ImportError:
64
+ return "Search not available - duckduckgo_search not installed"
 
 
65
  except Exception as e:
66
+ return f"Search failed: {e}"
 
 
 
67
 
68
+ def do_math(expression: str) -> str:
69
  """
70
+ Calculate math expressions safely
71
+ This is super important for GAIA - lots of math questions!
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  """
73
+ logger.info(f"Calculating: {expression}")
74
 
75
  try:
76
+ # Only allow safe math operations - learned this the hard way
77
+ safe_functions = {
78
+ # Basic math
79
+ 'abs': abs, 'round': round, 'min': min, 'max': max, 'sum': sum, 'pow': pow,
80
+ # Math module functions
81
+ **{k: v for k, v in math.__dict__.items() if not k.startswith("__")},
82
+ # Constants
83
+ 'pi': math.pi, 'e': math.e,
84
  }
85
 
86
+ # eval is dangerous but this is safe with limited scope
87
+ result = eval(expression, {"__builtins__": {}}, safe_functions)
88
+ return str(result)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
  except ZeroDivisionError:
91
+ return "Error: Division by zero"
 
 
 
 
 
 
 
 
 
 
92
  except Exception as e:
93
+ return f"Math error: {e}"
 
 
 
94
 
95
+ def analyze_file(content: str, file_type: str = "text") -> str:
96
  """
97
+ Analyze file contents - useful for GAIA questions with data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  """
99
+ logger.info(f"Analyzing {file_type} file")
100
 
101
  try:
102
  if file_type.lower() == "csv":
103
+ lines = content.strip().split('\n')
 
104
  if not lines:
105
  return "Empty file"
106
 
107
+ rows = len(lines) - 1 # minus header
108
+ cols = len(lines[0].split(',')) if lines else 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
 
110
+ analysis = f"""CSV Analysis:
111
+ Rows: {rows}
112
+ Columns: {cols}
113
+ Headers: {lines[0]}"""
 
 
114
 
115
+ if rows > 0 and len(lines) > 1:
116
+ analysis += f"\nFirst row: {lines[1]}"
117
+
118
+ return analysis
119
+
120
+ elif file_type.lower() in ["txt", "text"]:
121
+ lines = content.split('\n')
122
+ words = content.split()
123
+
124
+ return f"""Text Analysis:
125
+ Lines: {len(lines)}
126
+ Words: {len(words)}
127
+ Characters: {len(content)}"""
128
+
129
  else:
130
+ # Just show a preview
131
+ preview = content[:500] + '...' if len(content) > 500 else content
132
  return f"File content ({file_type}):\n{preview}"
133
 
134
  except Exception as e:
135
+ return f"File analysis error: {e}"
 
 
 
136
 
137
  def get_weather(location: str) -> str:
138
  """
139
+ Dummy weather function - just for demonstration
140
+ In a real app I'd use an actual weather API
 
 
 
 
 
 
 
 
 
 
 
141
  """
142
+ logger.info(f"Getting weather for {location}")
143
+
144
+ # Fake weather data
145
+ weather_options = [
146
+ {"condition": "Sunny", "temp": 25, "humidity": 60},
147
+ {"condition": "Cloudy", "temp": 18, "humidity": 75},
148
+ {"condition": "Rainy", "temp": 15, "humidity": 90},
149
+ {"condition": "Clear", "temp": 28, "humidity": 45}
 
150
  ]
151
 
152
+ weather = random.choice(weather_options)
 
 
 
 
 
 
 
 
153
 
154
+ return f"""Weather in {location}:
155
+ Condition: {weather['condition']}
156
+ Temperature: {weather['temp']}°C
157
+ Humidity: {weather['humidity']}%"""
158
 
159
+ # ========================================
160
+ # PERSONA DATABASE SETUP
161
+ # ========================================
162
 
163
+ def setup_persona_database(llm=None):
 
 
 
 
 
164
  """
165
+ This creates a query engine for my persona database
166
+ Using the patterns I learned in the course
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
  """
168
+ logger.info("Setting up persona database...")
169
 
170
  try:
171
+ # Connect to my ChromaDB database
172
+ db = chromadb.PersistentClient(path="./my_persona_db")
173
+ collection = db.get_or_create_collection("personas")
174
+ vector_store = ChromaVectorStore(chroma_collection=collection)
 
175
 
176
+ # Use the same embedding model as in the course
177
  embed_model = HuggingFaceEmbedding(model_name="BAAI/bge-small-en-v1.5")
 
178
 
179
+ # Create the index
180
  index = VectorStoreIndex.from_vector_store(
181
+ vector_store=vector_store,
182
  embed_model=embed_model
183
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
 
185
+ # Make the query engine
186
  query_engine = index.as_query_engine(
187
+ llm=llm, # Use the same LLM as the agent
188
+ response_mode="tree_summarize",
189
+ similarity_top_k=3, # Get top 3 matches
190
+ streaming=False
191
  )
192
 
193
+ logger.info("Persona database ready")
194
  return query_engine
195
 
196
  except Exception as e:
197
+ logger.warning(f"Persona database failed: {e}")
198
+ return None
 
199
 
200
+ # ========================================
201
+ # CREATING THE TOOLS
202
+ # ========================================
 
 
203
 
204
+ # Make function tools from my functions
205
+ web_tool = FunctionTool.from_defaults(
206
+ fn=search_web,
207
  name="web_search",
208
+ description="Search the web for current information, recent events, or facts"
 
 
 
 
 
209
  )
210
 
211
+ calc_tool = FunctionTool.from_defaults(
212
+ fn=do_math,
213
+ name="calculator",
214
+ description="Calculate mathematical expressions. Use this for ANY math calculations!"
 
 
 
 
 
 
 
215
  )
216
 
217
+ file_tool = FunctionTool.from_defaults(
 
218
  fn=analyze_file,
219
+ name="file_analyzer",
220
+ description="Analyze file contents like CSV files or text files"
 
 
 
 
221
  )
222
 
 
223
  weather_tool = FunctionTool.from_defaults(
224
  fn=get_weather,
225
  name="weather_tool",
226
+ description="Get weather information (demo only - uses fake data)"
 
 
 
 
227
  )
228
 
229
+ def create_persona_tool(llm=None):
 
230
  """
231
+ Create the persona database tool
232
+ This might fail in some environments so I handle errors gracefully
 
 
 
 
 
233
  """
234
+ logger.info("Creating persona database tool...")
235
 
236
  try:
237
+ # Try to load the persona data first
238
  try:
239
+ from my_retriever import get_persona_query_engine
240
+ query_engine = get_persona_query_engine(llm=llm)
241
+ except ImportError:
242
+ # Fallback if my_retriever doesn't exist
243
+ query_engine = setup_persona_database(llm=llm)
 
244
 
245
+ if query_engine is None:
246
+ logger.warning("Couldn't create persona database")
247
+ return None
248
 
249
+ # Make the tool
250
  persona_tool = QueryEngineTool.from_defaults(
251
  query_engine=query_engine,
252
  name="persona_database",
253
  description=(
254
+ "Search a database of people with different backgrounds and interests. "
255
+ "Use this to find people with specific skills, hobbies, or characteristics."
 
 
 
256
  )
257
  )
258
 
259
+ logger.info("Persona tool created")
260
  return persona_tool
261
 
262
  except Exception as e:
263
+ logger.warning(f"Persona tool creation failed: {e}")
 
264
  return None
265
 
266
+ def get_my_tools(llm=None):
 
 
 
 
 
267
  """
268
+ Get all my tools together
269
+ This is what my agent will call
 
 
 
 
 
270
  """
271
+ logger.info("Loading all my tools...")
272
 
273
  tools = []
274
 
275
+ # Add the basic function tools (these should always work)
276
+ basic_tools = [web_tool, calc_tool, file_tool, weather_tool]
277
+ tools.extend(basic_tools)
278
+ logger.info(f"Added {len(basic_tools)} basic tools")
 
 
 
 
 
 
 
279
 
280
+ # Try to add the persona database tool
281
+ persona_tool = create_persona_tool(llm=llm)
282
+ if persona_tool:
283
+ tools.append(persona_tool)
284
+ logger.info("Added persona database tool")
285
+ else:
286
+ logger.info("Persona tool not available (that's ok)")
287
+
288
+ logger.info(f"Total tools ready: {len(tools)}")
 
289
 
290
+ # Log what I have
291
  for tool in tools:
292
+ logger.info(f" - {tool.metadata.name}")
 
293
 
294
  return tools
295
 
296
+ # ========================================
297
+ # TESTING MY TOOLS
298
+ # ========================================
299
 
300
+ def test_my_tools():
 
 
 
 
301
  """
302
+ Quick test to make sure my tools work
 
303
  """
304
+ print("\n=== Testing My Tools ===")
 
 
 
 
 
 
 
 
 
305
 
306
  # Test calculator
307
+ print("Testing calculator...")
308
+ result = do_math("2 + 2 * 3")
309
+ print(f"2 + 2 * 3 = {result}")
310
+
311
+ result = do_math("sqrt(16)")
312
+ print(f"sqrt(16) = {result}")
 
 
 
313
 
314
  # Test file analyzer
315
+ print("\nTesting file analyzer...")
316
+ sample_csv = "name,age,city\nAlice,25,NYC\nBob,30,LA"
317
+ result = analyze_file(sample_csv, "csv")
318
+ print(f"CSV analysis:\n{result}")
 
 
 
 
319
 
320
  # Test weather
321
+ print("\nTesting weather...")
322
+ result = get_weather("Paris")
323
+ print(f"Weather:\n{result}")
 
 
 
 
 
 
 
 
 
 
 
324
 
325
+ # Test tool creation
326
+ print("\nTesting tool creation...")
327
+ tools = get_my_tools()
328
+ print(f"Created {len(tools)} tools successfully!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
329
 
330
+ print("\n=== All Tests Done ===")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
 
332
  if __name__ == "__main__":
333
+ # Run tests if this file is called directly
334
+ import logging
 
 
335
  logging.basicConfig(level=logging.INFO)
336
 
337
+ test_my_tools()