Tesvia commited on
Commit
6e06cc8
·
verified ·
1 Parent(s): ac3e234

Upload 3 files

Browse files
Files changed (3) hide show
  1. agent.py +51 -27
  2. app.py +54 -43
  3. tools.py +4 -38
agent.py CHANGED
@@ -6,9 +6,9 @@ from __future__ import annotations
6
 
7
  import asyncio
8
  import os
9
- import time
10
- import datetime
11
- from typing import Any, Sequence, Callable, List, Optional
12
 
13
  from dotenv import load_dotenv
14
  from agents import Agent, Runner, FunctionTool, Tool
@@ -23,12 +23,6 @@ from tools import (
23
  duckduckgo_search,
24
  )
25
 
26
- # ---------------------------------------------------------------------------
27
- # Logging Utility
28
- # ---------------------------------------------------------------------------
29
- def log(msg):
30
- print(f"[{datetime.datetime.now():%Y-%m-%d %H:%M:%S}] {msg}")
31
-
32
  # ---------------------------------------------------------------------------
33
  # Load the added system prompt
34
  # ---------------------------------------------------------------------------
@@ -85,37 +79,67 @@ def _build_agent(extra_tools: Sequence[FunctionTool] | None = None) -> Agent:
85
  )
86
 
87
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  class GAIAAgent:
89
  """Thin synchronous wrapper around an asynchronous Agents SDK agent."""
90
 
91
  def __init__(self, *, extra_tools: Sequence[FunctionTool] | None = None):
92
  self._agent = _build_agent(extra_tools=extra_tools)
93
- # Store the model id for logging
94
- self.model_id = _select_model()
95
-
96
- async def _arun(self, question: str, q_index: Optional[int] = None) -> str:
97
- q_num = q_index + 1 if q_index is not None else "?"
98
- log(f"Answering question {q_num}:")
99
- log(f" Question: {question!r}")
100
- log(f" Model: {self.model_id}")
101
 
102
- t0 = time.time()
103
- try:
 
 
 
 
 
 
 
 
104
  result = await Runner.run(self._agent, question)
105
- duration = time.time() - t0
106
- log(f" Total duration: {duration:.2f} seconds.")
107
- except Exception as e:
108
- log(f" Error during answer: {e}")
109
- raise
110
  return str(result.final_output).strip()
111
 
112
- def __call__(self, question: str, q_index: Optional[int] = None, **kwargs: Any) -> str:
 
 
 
 
 
 
 
 
 
 
113
  try:
114
  loop = asyncio.get_running_loop()
115
  except RuntimeError:
116
- return asyncio.run(self._arun(question, q_index=q_index))
 
117
  else:
118
- return loop.run_until_complete(self._arun(question, q_index=q_index))
119
 
120
 
121
  def gaia_agent(*, extra_tools: Sequence[FunctionTool] | None = None) -> GAIAAgent:
 
6
 
7
  import asyncio
8
  import os
9
+ from typing import Any, Sequence, Callable, List
10
+ from datetime import datetime
11
+ from agents import RunHooks # for lifecycle hooks
12
 
13
  from dotenv import load_dotenv
14
  from agents import Agent, Runner, FunctionTool, Tool
 
23
  duckduckgo_search,
24
  )
25
 
 
 
 
 
 
 
26
  # ---------------------------------------------------------------------------
27
  # Load the added system prompt
28
  # ---------------------------------------------------------------------------
 
79
  )
80
 
81
 
82
+ class LoggingHooks(RunHooks):
83
+ """RunHooks to log question start, model used, and each tool‐call step."""
84
+ def __init__(self):
85
+ self.step_counter = 0
86
+
87
+ async def on_agent_start(self, context, agent):
88
+ qnum = context.context.get("question_number")
89
+ qtext = context.context.get("question_text")
90
+ model = agent.model
91
+ ts = datetime.now().isoformat()
92
+ print(f"[{ts}] [Question {qnum}] Starting agent (model={model}) for question: '{qtext}'")
93
+
94
+ async def on_tool_start(self, context, agent, tool):
95
+ self.step_counter += 1
96
+ qnum = context.context.get("question_number")
97
+ ts = datetime.now().isoformat()
98
+ print(f"[{ts}] [Question {qnum}] Step {self.step_counter}: Invoking tool '{tool.name}'")
99
+
100
+ async def on_tool_end(self, context, agent, tool, result):
101
+ qnum = context.context.get("question_number")
102
+ ts = datetime.now().isoformat()
103
+ print(f"[{ts}] [Question {qnum}] Step {self.step_counter}: Tool '{tool.name}' completed")
104
+
105
+
106
  class GAIAAgent:
107
  """Thin synchronous wrapper around an asynchronous Agents SDK agent."""
108
 
109
  def __init__(self, *, extra_tools: Sequence[FunctionTool] | None = None):
110
  self._agent = _build_agent(extra_tools=extra_tools)
 
 
 
 
 
 
 
 
111
 
112
+ async def _arun(self, question: str, context_data=None, hooks=None) -> str:
113
+ # Pass context and hooks to Runner.run if provided
114
+ if context_data is not None and hooks is not None:
115
+ result = await Runner.run(
116
+ self._agent,
117
+ question,
118
+ context=context_data,
119
+ hooks=hooks
120
+ )
121
+ else:
122
  result = await Runner.run(self._agent, question)
 
 
 
 
 
123
  return str(result.final_output).strip()
124
 
125
+ def __call__(self, question: str, question_number: int | None = None, **_kwargs) -> str:
126
+ # Prepare logging context if a question_number is given
127
+ context_data = None
128
+ hooks = None
129
+ if question_number is not None:
130
+ context_data = {
131
+ "question_number": question_number,
132
+ "question_text": question
133
+ }
134
+ hooks = LoggingHooks()
135
+
136
  try:
137
  loop = asyncio.get_running_loop()
138
  except RuntimeError:
139
+ # No running loop: use asyncio.run
140
+ return asyncio.run(self._arun(question, context_data, hooks))
141
  else:
142
+ return loop.run_until_complete(self._arun(question, context_data, hooks))
143
 
144
 
145
  def gaia_agent(*, extra_tools: Sequence[FunctionTool] | None = None) -> GAIAAgent:
app.py CHANGED
@@ -2,34 +2,30 @@ import os
2
  import gradio as gr
3
  import requests
4
  import pandas as pd
5
- import datetime
6
 
7
  # --- Our Agent ---
8
  from agent import gaia_agent
9
 
10
- # Logging utility
11
- def log(msg):
12
- print(f"[{datetime.datetime.now():%Y-%m-%d %H:%M:%S}] {msg}")
13
-
14
  # Debugging level. If DEBUG=0 then DEBUG will be False. If DEBUG=1 then DEBUG will be True.
15
  DEBUG = os.getenv("DEBUG", "0") == "1"
16
 
 
17
  # --- Constants ---
18
  DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
19
 
20
- def run_and_submit_all(profile: gr.OAuthProfile | None):
21
  """
22
  Fetches all questions, runs the BasicAgent on them, submits all answers,
23
  and displays the results.
24
  """
25
  # --- Determine HF Space Runtime URL and Repo URL ---
26
- space_id = os.getenv("SPACE_ID") # Get the SPACE_ID for sending link to the code
27
 
28
  if profile:
29
- username = f"{profile.username}"
30
- log(f"User logged in: {username}")
31
  else:
32
- log("User not logged in.")
33
  return "Please Login to Hugging Face with the button.", None
34
 
35
  api_url = DEFAULT_API_URL
@@ -39,16 +35,16 @@ def run_and_submit_all(profile: gr.OAuthProfile | None):
39
  # 1. Instantiate Agent (now using OpenAI Agents SDK)
40
  try:
41
  agent = gaia_agent()
42
- log("OpenAI Agent instantiated successfully.")
43
  except Exception as e:
44
- log(f"Error instantiating agent: {e}")
45
  return f"Error initializing agent: {e}", None
46
-
47
  agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
48
- log(agent_code)
49
 
50
  # 2. Fetch Questions
51
- log(f"Fetching questions from: {questions_url}")
52
  import json
53
 
54
  try:
@@ -56,55 +52,70 @@ def run_and_submit_all(profile: gr.OAuthProfile | None):
56
  response.raise_for_status()
57
  questions_data = response.json()
58
  if not questions_data:
59
- log("Fetched questions list is empty.")
60
  return "Fetched questions list is empty or invalid format.", None
61
- log(f"Fetched {len(questions_data)} GAIA questions.")
62
  except json.JSONDecodeError as e:
63
- log(f"Error decoding JSON response from questions endpoint: {e}")
64
- log(f"Response text: {response.text[:500]}")
65
  return f"Error decoding server response for questions: {e}", None
66
  except requests.exceptions.RequestException as e:
67
- log(f"Error fetching questions: {e}")
68
  return f"Error fetching questions: {e}", None
69
  except Exception as e:
70
- log(f"An unexpected error occurred fetching questions: {e}")
71
  return f"An unexpected error occurred fetching questions: {e}", None
72
 
73
  # 3. Run the Agent
74
  results_log = []
75
  answers_payload = []
76
- log(f"Running agent on {len(questions_data)} questions...")
77
- for idx, item in enumerate(questions_data):
78
  task_id = item.get("task_id")
79
  question_text = item.get("question")
80
  if not task_id or question_text is None:
81
- log(f"Skipping item with missing task_id or question: {item}")
82
  continue
83
  try:
84
- submitted_answer = agent(question_text, q_index=idx)
 
 
 
85
  if DEBUG:
86
- log(f"[DEBUG] Task {task_id}: Answer type: {type(submitted_answer)}, Value: {repr(submitted_answer)}")
87
  else:
88
- log(f"[{task_id}] {question_text[:50]}... → {submitted_answer[:40]}")
89
 
 
90
  submitted_answer = str(submitted_answer).strip()
91
- answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer})
92
- results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer})
 
 
 
 
 
 
 
93
  except Exception as e:
94
- log(f"Error running agent on task {task_id}: {e}")
95
- results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": f"AGENT ERROR: {e}"})
 
 
 
 
96
 
97
  if not answers_payload:
98
- log("Agent did not produce any answers to submit.")
99
  return "Agent did not produce any answers to submit.", pd.DataFrame(results_log)
100
 
101
- # 4. Prepare Submission
102
  submission_data = {"username": username.strip(), "agent_code": agent_code, "answers": answers_payload}
103
  status_update = f"Agent finished. Submitting {len(answers_payload)} answers for user '{username}'..."
104
- log(status_update)
105
 
106
  # 5. Submit
107
- log(f"Submitting {len(answers_payload)} answers to: {submit_url}")
108
  try:
109
  response = requests.post(submit_url, json=submission_data, timeout=60)
110
  response.raise_for_status()
@@ -116,7 +127,7 @@ def run_and_submit_all(profile: gr.OAuthProfile | None):
116
  f"({result_data.get('correct_count', '?')}/{result_data.get('total_attempted', '?')} correct)\n"
117
  f"Message: {result_data.get('message', 'No message received.')}"
118
  )
119
- log("Submission successful.")
120
  results_df = pd.DataFrame(results_log)
121
  return final_status, results_df
122
  except requests.exceptions.HTTPError as e:
@@ -127,22 +138,22 @@ def run_and_submit_all(profile: gr.OAuthProfile | None):
127
  except requests.exceptions.JSONDecodeError:
128
  error_detail += f" Response: {e.response.text[:500]}"
129
  status_message = f"Submission Failed: {error_detail}"
130
- log(status_message)
131
  results_df = pd.DataFrame(results_log)
132
  return status_message, results_df
133
  except requests.exceptions.Timeout:
134
  status_message = "Submission Failed: The request timed out."
135
- log(status_message)
136
  results_df = pd.DataFrame(results_log)
137
  return status_message, results_df
138
  except requests.exceptions.RequestException as e:
139
  status_message = f"Submission Failed: Network error - {e}"
140
- log(status_message)
141
  results_df = pd.DataFrame(results_log)
142
  return status_message, results_df
143
  except Exception as e:
144
  status_message = f"An unexpected error occurred during submission: {e}"
145
- log(status_message)
146
  results_df = pd.DataFrame(results_log)
147
  return status_message, results_df
148
 
@@ -180,7 +191,7 @@ with gr.Blocks() as demo:
180
  if __name__ == "__main__":
181
  print("\n" + "-"*30 + " App Starting " + "-"*30)
182
  space_host_startup = os.getenv("SPACE_HOST")
183
- space_id_startup = os.getenv("SPACE_ID") # Get SPACE_ID at startup
184
 
185
  if space_host_startup:
186
  print(f"✅ SPACE_HOST found: {space_host_startup}")
@@ -188,14 +199,14 @@ if __name__ == "__main__":
188
  else:
189
  print("ℹ️ SPACE_HOST environment variable not found (running locally?).")
190
 
191
- if space_id_startup: # Print repo URLs if SPACE_ID is found
192
  print(f"✅ SPACE_ID found: {space_id_startup}")
193
  print(f" Repo URL: https://huggingface.co/spaces/{space_id_startup}")
194
  print(f" Repo Tree URL: https://huggingface.co/spaces/{space_id_startup}/tree/main")
195
  else:
196
  print("ℹ️ SPACE_ID environment variable not found (running locally?). Repo URL cannot be determined.")
197
 
198
- print("-" * (60 + len(" App Starting ")) + "\n")
199
 
200
  print("Launching Gradio Interface for Agent Evaluation…")
201
  demo.launch(debug=True, share=False)
 
2
  import gradio as gr
3
  import requests
4
  import pandas as pd
 
5
 
6
  # --- Our Agent ---
7
  from agent import gaia_agent
8
 
 
 
 
 
9
  # Debugging level. If DEBUG=0 then DEBUG will be False. If DEBUG=1 then DEBUG will be True.
10
  DEBUG = os.getenv("DEBUG", "0") == "1"
11
 
12
+ # (Keep Constants as is)
13
  # --- Constants ---
14
  DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
15
 
16
+ def run_and_submit_all( profile: gr.OAuthProfile | None):
17
  """
18
  Fetches all questions, runs the BasicAgent on them, submits all answers,
19
  and displays the results.
20
  """
21
  # --- Determine HF Space Runtime URL and Repo URL ---
22
+ space_id = os.getenv("SPACE_ID") # Get the SPACE_ID for sending link to the code
23
 
24
  if profile:
25
+ username= f"{profile.username}"
26
+ print(f"User logged in: {username}")
27
  else:
28
+ print("User not logged in.")
29
  return "Please Login to Hugging Face with the button.", None
30
 
31
  api_url = DEFAULT_API_URL
 
35
  # 1. Instantiate Agent (now using OpenAI Agents SDK)
36
  try:
37
  agent = gaia_agent()
38
+ print("OpenAI Agent instantiated successfully.")
39
  except Exception as e:
40
+ print(f"Error instantiating agent: {e}")
41
  return f"Error initializing agent: {e}", None
42
+ # In the case of an app running as a hugging Face space, this link points toward your codebase ( useful for others so please keep it public)
43
  agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
44
+ print(agent_code)
45
 
46
  # 2. Fetch Questions
47
+ print(f"Fetching questions from: {questions_url}")
48
  import json
49
 
50
  try:
 
52
  response.raise_for_status()
53
  questions_data = response.json()
54
  if not questions_data:
55
+ print("Fetched questions list is empty.")
56
  return "Fetched questions list is empty or invalid format.", None
57
+ print(f"Fetched {len(questions_data)} questions.")
58
  except json.JSONDecodeError as e:
59
+ print(f"Error decoding JSON response from questions endpoint: {e}")
60
+ print(f"Response text: {response.text[:500]}")
61
  return f"Error decoding server response for questions: {e}", None
62
  except requests.exceptions.RequestException as e:
63
+ print(f"Error fetching questions: {e}")
64
  return f"Error fetching questions: {e}", None
65
  except Exception as e:
66
+ print(f"An unexpected error occurred fetching questions: {e}")
67
  return f"An unexpected error occurred fetching questions: {e}", None
68
 
69
  # 3. Run the Agent
70
  results_log = []
71
  answers_payload = []
72
+ print(f"Running agent on {len(questions_data)} questions...")
73
+ for idx, item in enumerate(questions_data, start=1):
74
  task_id = item.get("task_id")
75
  question_text = item.get("question")
76
  if not task_id or question_text is None:
77
+ print(f"Skipping item with missing task_id or question: {item}")
78
  continue
79
  try:
80
+ # pass in question_number for logging hooks
81
+ submitted_answer = agent(question_text, question_number=idx)
82
+
83
+ # --- DEBUG LOGGING ---
84
  if DEBUG:
85
+ print(f"[DEBUG] Task {task_id}: Answer type: {type(submitted_answer)}, Value: {repr(submitted_answer)}")
86
  else:
87
+ print(f"[{task_id}] {question_text[:50]}... → {submitted_answer[:40]}")
88
 
89
+ # Force string type here just in case (defensive)
90
  submitted_answer = str(submitted_answer).strip()
91
+ answers_payload.append({
92
+ "task_id": task_id,
93
+ "submitted_answer": submitted_answer
94
+ })
95
+ results_log.append({
96
+ "Task ID": task_id,
97
+ "Question": question_text,
98
+ "Submitted Answer": submitted_answer
99
+ })
100
  except Exception as e:
101
+ print(f"Error running agent on task {task_id}: {e}")
102
+ results_log.append({
103
+ "Task ID": task_id,
104
+ "Question": question_text,
105
+ "Submitted Answer": f"AGENT ERROR: {e}"
106
+ })
107
 
108
  if not answers_payload:
109
+ print("Agent did not produce any answers to submit.")
110
  return "Agent did not produce any answers to submit.", pd.DataFrame(results_log)
111
 
112
+ # 4. Prepare Submission
113
  submission_data = {"username": username.strip(), "agent_code": agent_code, "answers": answers_payload}
114
  status_update = f"Agent finished. Submitting {len(answers_payload)} answers for user '{username}'..."
115
+ print(status_update)
116
 
117
  # 5. Submit
118
+ print(f"Submitting {len(answers_payload)} answers to: {submit_url}")
119
  try:
120
  response = requests.post(submit_url, json=submission_data, timeout=60)
121
  response.raise_for_status()
 
127
  f"({result_data.get('correct_count', '?')}/{result_data.get('total_attempted', '?')} correct)\n"
128
  f"Message: {result_data.get('message', 'No message received.')}"
129
  )
130
+ print("Submission successful.")
131
  results_df = pd.DataFrame(results_log)
132
  return final_status, results_df
133
  except requests.exceptions.HTTPError as e:
 
138
  except requests.exceptions.JSONDecodeError:
139
  error_detail += f" Response: {e.response.text[:500]}"
140
  status_message = f"Submission Failed: {error_detail}"
141
+ print(status_message)
142
  results_df = pd.DataFrame(results_log)
143
  return status_message, results_df
144
  except requests.exceptions.Timeout:
145
  status_message = "Submission Failed: The request timed out."
146
+ print(status_message)
147
  results_df = pd.DataFrame(results_log)
148
  return status_message, results_df
149
  except requests.exceptions.RequestException as e:
150
  status_message = f"Submission Failed: Network error - {e}"
151
+ print(status_message)
152
  results_df = pd.DataFrame(results_log)
153
  return status_message, results_df
154
  except Exception as e:
155
  status_message = f"An unexpected error occurred during submission: {e}"
156
+ print(status_message)
157
  results_df = pd.DataFrame(results_log)
158
  return status_message, results_df
159
 
 
191
  if __name__ == "__main__":
192
  print("\n" + "-"*30 + " App Starting " + "-"*30)
193
  space_host_startup = os.getenv("SPACE_HOST")
194
+ space_id_startup = os.getenv("SPACE_ID") # Get SPACE_ID at startup
195
 
196
  if space_host_startup:
197
  print(f"✅ SPACE_HOST found: {space_host_startup}")
 
199
  else:
200
  print("ℹ️ SPACE_HOST environment variable not found (running locally?).")
201
 
202
+ if space_id_startup: # Print repo URLs if SPACE_ID is found
203
  print(f"✅ SPACE_ID found: {space_id_startup}")
204
  print(f" Repo URL: https://huggingface.co/spaces/{space_id_startup}")
205
  print(f" Repo Tree URL: https://huggingface.co/spaces/{space_id_startup}/tree/main")
206
  else:
207
  print("ℹ️ SPACE_ID environment variable not found (running locally?). Repo URL cannot be determined.")
208
 
209
+ print("-"*(60 + len(" App Starting ")) + "\n")
210
 
211
  print("Launching Gradio Interface for Agent Evaluation…")
212
  demo.launch(debug=True, share=False)
tools.py CHANGED
@@ -7,41 +7,12 @@ from __future__ import annotations
7
  import contextlib
8
  import io
9
  import os
10
- import time
11
- import datetime
12
- from typing import TypedDict, List, Union
13
 
14
  from agents import function_tool
15
 
16
- class DuckDuckGoResult(TypedDict):
17
- title: str
18
- href: str
19
- body: str
20
-
21
- class SpreadsheetRow(TypedDict):
22
- # If you don't know the columns, leave this empty,
23
- # but ideally, define them.
24
- pass
25
-
26
- def log(msg):
27
- print(f"[{datetime.datetime.now():%Y-%m-%d %H:%M:%S}] {msg}")
28
-
29
- def log_tool_call(func):
30
- def wrapper(*args, **kwargs):
31
- t0 = time.time()
32
- log(f"Step: {func.__name__} started.")
33
- try:
34
- result = func(*args, **kwargs)
35
- log(f"Step: {func.__name__} completed in {time.time() - t0:.2f}s.")
36
- return result
37
- except Exception as e:
38
- log(f"Step: {func.__name__} error: {e}")
39
- raise
40
- return wrapper
41
-
42
  # 1. --------------------------------------------------------------------
43
  @function_tool
44
- @log_tool_call
45
  def python_run(code: str) -> str:
46
  """Execute trusted Python code and return the captured stdout together with
47
  the repr() of the last expression (or `_result` variable if set).
@@ -65,8 +36,7 @@ def python_run(code: str) -> str:
65
 
66
  # 2. --------------------------------------------------------------------
67
  @function_tool
68
- @log_tool_call
69
- def load_spreadsheet(path: str, sheet: Union[str, int, None] = None) -> List[SpreadsheetRow]:
70
  """Read .csv, .xls or .xlsx from disk and return rows as list of dictionaries.
71
 
72
  Args:
@@ -95,7 +65,6 @@ def load_spreadsheet(path: str, sheet: Union[str, int, None] = None) -> List[Spr
95
 
96
  # 3. --------------------------------------------------------------------
97
  @function_tool
98
- @log_tool_call
99
  def youtube_transcript(url: str, lang: str = "en") -> str:
100
  """Fetch the subtitles of a YouTube video.
101
 
@@ -115,7 +84,6 @@ def youtube_transcript(url: str, lang: str = "en") -> str:
115
 
116
  # 4. --------------------------------------------------------------------
117
  @function_tool
118
- @log_tool_call
119
  def transcribe_audio(path: str, model: str = "whisper-1") -> str:
120
  """Transcribe an audio file using OpenAI Whisper.
121
 
@@ -136,7 +104,6 @@ def transcribe_audio(path: str, model: str = "whisper-1") -> str:
136
 
137
  # 5. --------------------------------------------------------------------
138
  @function_tool
139
- @log_tool_call
140
  def image_ocr(path: str) -> str:
141
  """Perform OCR on an image using Tesseract.
142
 
@@ -153,8 +120,7 @@ def image_ocr(path: str) -> str:
153
 
154
  # 6. --------------------------------------------------------------------
155
  @function_tool
156
- @log_tool_call
157
- def duckduckgo_search(query: str, max_results: int = 5) -> List[DuckDuckGoResult]:
158
  """Search DuckDuckGo and return a list of result dicts with title, href and body.
159
 
160
  Args:
@@ -173,4 +139,4 @@ def duckduckgo_search(query: str, max_results: int = 5) -> List[DuckDuckGoResult
173
  "body": r.get("body", ""),
174
  }
175
  )
176
- return results
 
7
  import contextlib
8
  import io
9
  import os
10
+ from typing import List, Dict
 
 
11
 
12
  from agents import function_tool
13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  # 1. --------------------------------------------------------------------
15
  @function_tool
 
16
  def python_run(code: str) -> str:
17
  """Execute trusted Python code and return the captured stdout together with
18
  the repr() of the last expression (or `_result` variable if set).
 
36
 
37
  # 2. --------------------------------------------------------------------
38
  @function_tool
39
+ def load_spreadsheet(path: str, sheet: str | int | None = None) -> list[Dict[str, str]]:
 
40
  """Read .csv, .xls or .xlsx from disk and return rows as list of dictionaries.
41
 
42
  Args:
 
65
 
66
  # 3. --------------------------------------------------------------------
67
  @function_tool
 
68
  def youtube_transcript(url: str, lang: str = "en") -> str:
69
  """Fetch the subtitles of a YouTube video.
70
 
 
84
 
85
  # 4. --------------------------------------------------------------------
86
  @function_tool
 
87
  def transcribe_audio(path: str, model: str = "whisper-1") -> str:
88
  """Transcribe an audio file using OpenAI Whisper.
89
 
 
104
 
105
  # 5. --------------------------------------------------------------------
106
  @function_tool
 
107
  def image_ocr(path: str) -> str:
108
  """Perform OCR on an image using Tesseract.
109
 
 
120
 
121
  # 6. --------------------------------------------------------------------
122
  @function_tool
123
+ def duckduckgo_search(query: str, max_results: int = 5) -> List[Dict[str, str]]:
 
124
  """Search DuckDuckGo and return a list of result dicts with title, href and body.
125
 
126
  Args:
 
139
  "body": r.get("body", ""),
140
  }
141
  )
142
+ return results