Tesvia commited on
Commit
9623335
·
verified ·
1 Parent(s): 54bbd14

Upload 5 files

Browse files
Files changed (4) hide show
  1. agent.py +61 -101
  2. app.py +10 -17
  3. requirements.txt +4 -2
  4. tools.py +84 -73
agent.py CHANGED
@@ -1,144 +1,104 @@
1
- """GAIA benchmark agent using OpenAI Agents SDK.
2
-
3
- This module exposes:
4
-
5
- * ``gaia_agent()`` – factory returning a ready‑to‑use agent instance.
6
- * ``GAIAAgent`` – a class that wraps ``openai_agents.Agent``.
7
-
8
- The LLM backend is chosen at runtime via the ``MODEL_PROVIDER``
9
- environment variable (``hf`` or ``openai``).
10
  """
 
 
 
 
11
 
 
12
  import os
13
- import asyncio # Added for potential direct asyncio.run if needed, and for async def
14
- from typing import Any, Sequence, Callable, Union # Added Callable and Union
15
 
16
  from dotenv import load_dotenv
 
17
 
18
- # OpenAI Agents SDK imports
19
- from openai_agents import Agent, Runner
20
- from openai_agents.models.openai_chat_completions import OpenAIChatCompletionsModel
21
- from openai_agents.extensions.models.litellm_model import LitellmModel
22
- # FunctionToolType could be imported if it's a public type, for now using Callable
23
- # from openai_agents import FunctionToolType # Example if such type exists
24
-
25
- # Custom Tools from tools.py (now functions)
26
  from tools import (
27
  python_run,
28
  load_spreadsheet,
29
  youtube_transcript,
30
  transcribe_audio,
31
  image_ocr,
32
- duckduckgo_search, # Added the new tool
33
  )
34
 
35
  # ---------------------------------------------------------------------------
36
- # Load the added system prompt from system_prompt.txt (located in the same directory)
37
  # ---------------------------------------------------------------------------
38
  ADDED_PROMPT_PATH = os.path.join(os.path.dirname(__file__), "added_prompt.txt")
39
  with open(ADDED_PROMPT_PATH, "r", encoding="utf-8") as f:
40
  ADDED_PROMPT = f.read().strip()
41
 
42
- # ---------------------------------------------------------------------------
43
- # Model selection helper
44
- # ---------------------------------------------------------------------------
45
 
46
- load_dotenv() # Make sure we read credentials from .env
47
-
48
- def _select_model() -> Union[OpenAIChatCompletionsModel, LitellmModel]:
49
- """Return an OpenAI Agents SDK model instance as configured by env variables."""
50
 
 
 
51
  provider = os.getenv("MODEL_PROVIDER", "hf").lower()
52
- # Ensure API keys are loaded if not directly passed to model constructors
53
- # OpenAI API key is typically read by the library from OPENAI_API_KEY env var
54
- # LiteLLM also often relies on environment variables for keys
55
-
56
- if provider == "hf":
57
- hf_model_id = os.getenv("HF_MODEL", "NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO") # Example, ensure this is a valid LiteLLM model ID
58
- # LiteLLM typically requires a prefix for HuggingFace models
59
- if not hf_model_id.startswith("huggingface/"):
60
- hf_model_id = f"huggingface/{hf_model_id}"
61
- hf_token = os.getenv("HF_API_KEY") # LiteLLM might use this or HUGGINGFACE_API_KEY
62
- # For LiteLLM, api_key parameter might be used for specific providers,
63
- # but often it relies on env vars like HUGGINGFACE_API_KEY.
64
- # Passing token explicitly if LitellmModel supports it, or ensuring env var is set.
65
- return LitellmModel(model=hf_model_id, api_key=hf_token if hf_token else None)
66
-
67
 
68
  if provider == "openai":
69
- openai_model_id = os.getenv("OPENAI_MODEL", "gpt-3.5-turbo")
70
- openai_token = os.getenv("OPENAI_API_KEY") # OpenAIChatCompletionsModel will use this by default if set in env
71
- return OpenAIChatCompletionsModel(
72
- model=openai_model_id,
73
- api_key=openai_token # Explicitly passing, though often picked from env
74
- )
75
 
76
  raise ValueError(
77
- f"Unsupported MODEL_PROVIDER: {provider!r}. "
78
- "Use 'hf' (default) or 'openai'."
79
  )
80
 
81
- # ---------------------------------------------------------------------------
82
- # Core Agent implementation
83
- # ---------------------------------------------------------------------------
84
 
85
- DEFAULT_TOOLS: Sequence[Callable] = [
86
- duckduckgo_search,
87
  python_run,
88
  load_spreadsheet,
89
  youtube_transcript,
90
  transcribe_audio,
91
  image_ocr,
 
92
  ]
93
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  class GAIAAgent:
95
- def __init__(
96
- self,
97
- tools: Sequence[Callable] | None = None
98
- ):
99
- self.model = _select_model()
100
- self.tools = tools or DEFAULT_TOOLS
101
-
102
- base_system_prompt = "You are a helpful assistant designed to answer questions and complete tasks. You have access to a variety of tools to help you."
103
- full_system_prompt = f"{base_system_prompt}\n\n{ADDED_PROMPT}"
104
-
105
- self.agent = Agent(
106
- model=self.model,
107
- tools=self.tools,
108
- instructions=full_system_prompt,
109
- name="GAIAAgent"
110
- )
111
-
112
- async def __call__(self, question: str, **kwargs: Any) -> str:
113
- """
114
- Asynchronously processes a question using the agent and returns the final answer.
115
- kwargs are passed to Runner.run if supported, currently ignored as per plan.
116
- """
117
- # As per plan, Runner.run(self.agent, question) is used.
118
- # If session_id or other kwargs are needed by Runner.run, this might need adjustment.
119
- response = await Runner.run(self.agent, question)
120
-
121
- # Extract the final output. Assuming response.final_output is the way.
122
- # The type of final_output needs to be handled (e.g. if it's a message object or just text)
123
- final_answer = response.final_output
124
- if hasattr(final_answer, 'content'): # Example if final_output is a message object
125
- final_answer_text = str(final_answer.content)
126
  else:
127
- final_answer_text = str(final_answer)
128
-
129
- return final_answer_text.strip()
130
 
131
- # ---------------------------------------------------------------------------
132
- # Factory helpers expected by app.py
133
- # ---------------------------------------------------------------------------
134
 
135
- def gaia_agent(*, extra_tools: Sequence[Callable] | None = None) -> GAIAAgent:
136
- """
137
- Factory function to create a GAIAAgent instance with default and optional extra tools.
138
- """
139
- toolset = list(DEFAULT_TOOLS)
140
- if extra_tools:
141
- toolset.extend(extra_tools)
142
- return GAIAAgent(tools=toolset)
143
 
144
  __all__ = ["GAIAAgent", "gaia_agent"]
 
 
 
 
 
 
 
 
 
 
1
  """
2
+ GAIA benchmark agent using the OpenAI Agents SDK.
3
+ """
4
+
5
+ from __future__ import annotations
6
 
7
+ import asyncio
8
  import os
9
+ from typing import Any, Sequence, Callable, List
 
10
 
11
  from dotenv import load_dotenv
12
+ from agents import Agent, Runner, FunctionTool, Tool
13
 
14
+ # Import all function tools
 
 
 
 
 
 
 
15
  from tools import (
16
  python_run,
17
  load_spreadsheet,
18
  youtube_transcript,
19
  transcribe_audio,
20
  image_ocr,
21
+ duckduckgo_search,
22
  )
23
 
24
  # ---------------------------------------------------------------------------
25
+ # Load the added system prompt
26
  # ---------------------------------------------------------------------------
27
  ADDED_PROMPT_PATH = os.path.join(os.path.dirname(__file__), "added_prompt.txt")
28
  with open(ADDED_PROMPT_PATH, "r", encoding="utf-8") as f:
29
  ADDED_PROMPT = f.read().strip()
30
 
31
+ load_dotenv()
 
 
32
 
 
 
 
 
33
 
34
+ def _select_model() -> str:
35
+ """Return a model identifier appropriate for the Agents SDK based on environment settings."""
36
  provider = os.getenv("MODEL_PROVIDER", "hf").lower()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
  if provider == "openai":
39
+ model_name = os.getenv("OPENAI_MODEL", "gpt-4o-mini")
40
+ return f"openai/{model_name}"
41
+
42
+ if provider == "hf":
43
+ hf_model_id = os.getenv("HF_MODEL", "Qwen/Qwen2.5-Coder-32B-Instruct")
44
+ return f"litellm/huggingface/{hf_model_id}"
45
 
46
  raise ValueError(
47
+ f"Unsupported MODEL_PROVIDER: {provider!r}. Expected 'openai' or 'hf'."
 
48
  )
49
 
 
 
 
50
 
51
+ DEFAULT_TOOLS: List[FunctionTool] = [
 
52
  python_run,
53
  load_spreadsheet,
54
  youtube_transcript,
55
  transcribe_audio,
56
  image_ocr,
57
+ duckduckgo_search,
58
  ]
59
 
60
+
61
+ def _build_agent(extra_tools: Sequence[FunctionTool] | None = None) -> Agent:
62
+ """Construct the underlying Agents SDK `Agent` instance."""
63
+ instructions = (
64
+ "You are a helpful assistant tasked with answering questions using the available tools.\n\n"
65
+ + ADDED_PROMPT
66
+ )
67
+
68
+ tools: Sequence[Tool] = list(DEFAULT_TOOLS)
69
+ if extra_tools:
70
+ tools = list(tools) + list(extra_tools)
71
+
72
+ return Agent(
73
+ name="GAIA Agent",
74
+ instructions=instructions,
75
+ tools=tools,
76
+ model=_select_model(),
77
+ )
78
+
79
+
80
  class GAIAAgent:
81
+ """Thin synchronous wrapper around an asynchronous Agents SDK agent."""
82
+
83
+ def __init__(self, *, extra_tools: Sequence[FunctionTool] | None = None):
84
+ self._agent = _build_agent(extra_tools=extra_tools)
85
+
86
+ async def _arun(self, question: str) -> str:
87
+ result = await Runner.run(self._agent, question)
88
+ return str(result.final_output).strip()
89
+
90
+ def __call__(self, question: str, **kwargs: Any) -> str:
91
+ try:
92
+ loop = asyncio.get_running_loop()
93
+ except RuntimeError:
94
+ return asyncio.run(self._arun(question))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  else:
96
+ return loop.run_until_complete(self._arun(question))
 
 
97
 
 
 
 
98
 
99
+ def gaia_agent(*, extra_tools: Sequence[FunctionTool] | None = None) -> GAIAAgent:
100
+ """Factory returning a ready‑to‑use GAIAAgent instance."""
101
+ return GAIAAgent(extra_tools=extra_tools)
102
+
 
 
 
 
103
 
104
  __all__ = ["GAIAAgent", "gaia_agent"]
app.py CHANGED
@@ -2,7 +2,6 @@ import os
2
  import gradio as gr
3
  import requests
4
  import pandas as pd
5
- import asyncio
6
 
7
  # --- Our Agent ---
8
  from agent import gaia_agent
@@ -14,11 +13,10 @@ DEBUG = os.getenv("DEBUG", "0") == "1"
14
  # --- Constants ---
15
  DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
16
 
17
- # 2. Modified function definition to be async def
18
- async def run_and_submit_all( profile: gr.OAuthProfile | None):
19
  """
20
- Fetches all questions, runs the GAIAAgent on them, submits all answers,
21
- and displays the results. Now an async function.
22
  """
23
  # --- Determine HF Space Runtime URL and Repo URL ---
24
  space_id = os.getenv("SPACE_ID") # Get the SPACE_ID for sending link to the code
@@ -34,10 +32,10 @@ async def run_and_submit_all( profile: gr.OAuthProfile | None):
34
  questions_url = f"{api_url}/questions"
35
  submit_url = f"{api_url}/submit"
36
 
37
- # 1. Instantiate Agent
38
  try:
39
  agent = gaia_agent()
40
- print("GAIAAgent instantiated successfully.")
41
  except Exception as e:
42
  print(f"Error instantiating agent: {e}")
43
  return f"Error initializing agent: {e}", None
@@ -50,9 +48,7 @@ async def run_and_submit_all( profile: gr.OAuthProfile | None):
50
  import json
51
 
52
  try:
53
- # Using asyncio.to_thread to run synchronous requests.get in a separate thread
54
- # to avoid blocking the asyncio event loop.
55
- response = await asyncio.to_thread(requests.get, questions_url, timeout=15)
56
  response.raise_for_status()
57
  questions_data = response.json()
58
  if not questions_data:
@@ -61,7 +57,7 @@ async def run_and_submit_all( profile: gr.OAuthProfile | None):
61
  print(f"Fetched {len(questions_data)} questions.")
62
  except json.JSONDecodeError as e:
63
  print(f"Error decoding JSON response from questions endpoint: {e}")
64
- print(f"Response text: {response.text[:500]}") # type: ignore
65
  return f"Error decoding server response for questions: {e}", None
66
  except requests.exceptions.RequestException as e:
67
  print(f"Error fetching questions: {e}")
@@ -81,8 +77,7 @@ async def run_and_submit_all( profile: gr.OAuthProfile | None):
81
  print(f"Skipping item with missing task_id or question: {item}")
82
  continue
83
  try:
84
- # 3. Changed agent invocation to await agent call
85
- submitted_answer = await agent(question_text)
86
  # --- DEBUG LOGGING ---
87
  if DEBUG:
88
  print(f"[DEBUG] Task {task_id}: Answer type: {type(submitted_answer)}, Value: {repr(submitted_answer)}")
@@ -109,8 +104,7 @@ async def run_and_submit_all( profile: gr.OAuthProfile | None):
109
  # 5. Submit
110
  print(f"Submitting {len(answers_payload)} answers to: {submit_url}")
111
  try:
112
- # Using asyncio.to_thread for synchronous requests.post
113
- response = await asyncio.to_thread(requests.post, submit_url, json=submission_data, timeout=60)
114
  response.raise_for_status()
115
  result_data = response.json()
116
  final_status = (
@@ -128,7 +122,7 @@ async def run_and_submit_all( profile: gr.OAuthProfile | None):
128
  try:
129
  error_json = e.response.json()
130
  error_detail += f" Detail: {error_json.get('detail', e.response.text)}"
131
- except requests.exceptions.JSONDecodeError: # Changed from requests.JSONDecodeError
132
  error_detail += f" Response: {e.response.text[:500]}"
133
  status_message = f"Submission Failed: {error_detail}"
134
  print(status_message)
@@ -176,7 +170,6 @@ with gr.Blocks() as demo:
176
  status_output = gr.Textbox(label="Run Status / Submission Result", lines=5, interactive=False)
177
  results_table = gr.DataFrame(label="Questions and Agent Answers", wrap=True)
178
 
179
- # 5. Gradio's click call remains the same, it should handle async functions.
180
  run_button.click(
181
  fn=run_and_submit_all,
182
  outputs=[status_output, results_table]
 
2
  import gradio as gr
3
  import requests
4
  import pandas as pd
 
5
 
6
  # --- Our Agent ---
7
  from agent import gaia_agent
 
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
 
32
  questions_url = f"{api_url}/questions"
33
  submit_url = f"{api_url}/submit"
34
 
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
 
48
  import json
49
 
50
  try:
51
+ response = requests.get(questions_url, timeout=15)
 
 
52
  response.raise_for_status()
53
  questions_data = response.json()
54
  if not questions_data:
 
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}")
 
77
  print(f"Skipping item with missing task_id or question: {item}")
78
  continue
79
  try:
80
+ submitted_answer = agent(question_text)
 
81
  # --- DEBUG LOGGING ---
82
  if DEBUG:
83
  print(f"[DEBUG] Task {task_id}: Answer type: {type(submitted_answer)}, Value: {repr(submitted_answer)}")
 
104
  # 5. Submit
105
  print(f"Submitting {len(answers_payload)} answers to: {submit_url}")
106
  try:
107
+ response = requests.post(submit_url, json=submission_data, timeout=60)
 
108
  response.raise_for_status()
109
  result_data = response.json()
110
  final_status = (
 
122
  try:
123
  error_json = e.response.json()
124
  error_detail += f" Detail: {error_json.get('detail', e.response.text)}"
125
+ except requests.exceptions.JSONDecodeError:
126
  error_detail += f" Response: {e.response.text[:500]}"
127
  status_message = f"Submission Failed: {error_detail}"
128
  print(status_message)
 
170
  status_output = gr.Textbox(label="Run Status / Submission Result", lines=5, interactive=False)
171
  results_table = gr.DataFrame(label="Questions and Agent Answers", wrap=True)
172
 
 
173
  run_button.click(
174
  fn=run_and_submit_all,
175
  outputs=[status_output, results_table]
requirements.txt CHANGED
@@ -1,8 +1,10 @@
1
  gradio
2
  requests
3
  pandas
4
- openai-agents
 
5
  duckduckgo-search
6
  youtube-transcript-api
7
  pytesseract
8
- pillow
 
 
1
  gradio
2
  requests
3
  pandas
4
+ openai-agents[litellm]
5
+ openai>=1.3
6
  duckduckgo-search
7
  youtube-transcript-api
8
  pytesseract
9
+ pillow
10
+ python-dotenv
tools.py CHANGED
@@ -1,131 +1,142 @@
1
- # Custom tools for OpenAI Agents
 
 
 
2
  from __future__ import annotations
3
 
4
  import contextlib
5
  import io
6
  import os
7
- from typing import Any, List, Union
8
-
9
- from openai_agents import function_tool # Using openai_agents
10
- import pandas as pd
11
- import openai
12
- from PIL import Image
13
- import pytesseract
14
- from duckduckgo_search import DDGS
15
- from urllib.parse import urlparse, parse_qs # For youtube_transcript
16
- from youtube_transcript_api import YouTubeTranscriptApi # For youtube_transcript, corrected import
17
-
18
- # ---- 1. PythonRunTool -> python_run function ----------------------------------
19
  @function_tool
20
  def python_run(code: str) -> str:
21
- """
22
- Execute trusted Python code and return printed output + repr() of the last expression (or _result variable).
23
 
24
  Args:
25
- code (str): Python code to execute.
26
  """
27
- buf, ns = io.StringIO(), {}
 
28
  last = None
29
  try:
30
  with contextlib.redirect_stdout(buf):
31
  exec(compile(code, "<agent-python>", "exec"), {}, ns)
32
- last = ns.get("_result", None)
33
  except Exception as e:
34
- raise RuntimeError(f"PythonRunTool error: {e}") from e
 
35
  out = buf.getvalue()
36
- # Always return a string
37
- result = (out + (repr(last) if last is not None else "")).strip()
38
- return str(result)
39
 
40
- # ---- 2. ExcelLoaderTool -> load_spreadsheet function --------------------------
41
  @function_tool
42
- def load_spreadsheet(path: str, sheet: Union[str, int, None] = None) -> str:
43
- """
44
- Read .xlsx/.xls/.csv from disk and return rows as a list of dictionaries with string keys.
45
 
46
  Args:
47
- path (str): Path to .csv/.xls/.xlsx file.
48
- sheet (Union[str, int, None], optional): Sheet name or index (optional, required for Excel files only). Defaults to None.
49
  """
 
 
50
  if not os.path.isfile(path):
51
  raise FileNotFoundError(path)
52
  ext = os.path.splitext(path)[1].lower()
53
- if sheet == "": # Treat empty string as None for sheet name
54
- sheet = None
55
  if ext == ".csv":
56
  df = pd.read_csv(path)
 
57
  else:
58
- df = pd.read_excel(path, sheet_name=sheet)
59
- records = [{str(k): v for k, v in row.items()} for row in df.to_dict(orient="records")]
60
- # Always return a string
61
- return str(records)
 
 
 
 
 
 
62
 
63
- # ---- 3. YouTubeTranscriptTool -> youtube_transcript function ------------------
64
  @function_tool
65
  def youtube_transcript(url: str, lang: str = "en") -> str:
66
- """
67
- Return the subtitles of a YouTube URL using youtube-transcript-api.
68
 
69
  Args:
70
- url (str): YouTube URL.
71
- lang (str, optional): Transcript language. Defaults to "en".
72
  """
 
 
 
73
  vid = parse_qs(urlparse(url).query).get("v", [None])[0] or url.split("/")[-1]
74
- # Corrected import: from youtube_transcript_api import YouTubeTranscriptApi
75
- data = YouTubeTranscriptApi.get_transcript(vid, languages=[lang, "en", "en-US", "en-GB"])
76
- text = " ".join(d["text"] for d in data).strip()
77
- return str(text)
78
 
79
- # ---- 4. AudioTranscriptionTool -> transcribe_audio function -------------------
 
80
  @function_tool
81
  def transcribe_audio(path: str, model: str = "whisper-1") -> str:
82
- """
83
- Transcribe an audio file with OpenAI Whisper, returns plain text.
84
 
85
  Args:
86
- path (str): Path to audio file.
87
- model (str, optional): Model name for transcription. Defaults to "whisper-1".
88
  """
 
 
89
  if not os.path.isfile(path):
90
  raise FileNotFoundError(path)
 
91
  client = openai.OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
92
  with open(path, "rb") as fp:
93
- transcript_data = client.audio.transcriptions.create(model=model, file=fp) # Renamed to transcript_data
94
- return str(transcript_data.text.strip())
 
95
 
96
- # ---- 5. SimpleOCRTool -> image_ocr function ------------------------------------
97
  @function_tool
98
  def image_ocr(path: str) -> str:
99
- """
100
- Return any text spotted in an image via pytesseract OCR.
101
 
102
  Args:
103
- path (str): Path to image file.
104
  """
 
 
 
105
  if not os.path.isfile(path):
106
  raise FileNotFoundError(path)
107
- return str(pytesseract.image_to_string(Image.open(path)).strip())
 
108
 
109
- # ---- 6. New DuckDuckGo Search Tool ---------------------------------------------
110
  @function_tool
111
- def duckduckgo_search(query: str) -> str:
112
- """
113
- Searches the web using DuckDuckGo and returns a summary of results.
114
 
115
  Args:
116
- query (str): The search query.
 
117
  """
 
 
 
118
  with DDGS() as ddgs:
119
- results = ddgs.text(query, max_results=5) # Get top 5 results
120
- summary = "\n".join([f"{r['title']}: {r['body']}" for r in results]) if results else "No results found."
121
- return summary
122
-
123
- # ---------------------------------------------------------------------------
124
- __all__ = [
125
- "python_run",
126
- "load_spreadsheet",
127
- "youtube_transcript",
128
- "transcribe_audio",
129
- "image_ocr",
130
- "duckduckgo_search",
131
- ]
 
1
+ """
2
+ Custom function tools for OpenAI Agents SDK GAIA agent.
3
+ """
4
+
5
  from __future__ import annotations
6
 
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).
19
 
20
  Args:
21
+ code: Python code to execute.
22
  """
23
+ buf = io.StringIO()
24
+ ns: dict = {}
25
  last = None
26
  try:
27
  with contextlib.redirect_stdout(buf):
28
  exec(compile(code, "<agent-python>", "exec"), {}, ns)
29
+ last = ns.get("_result")
30
  except Exception as e:
31
+ raise RuntimeError(f"python_run error: {e}") from e
32
+
33
  out = buf.getvalue()
34
+ return (out + (repr(last) if last is not None else "")).strip()
35
+
 
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:
43
+ path: Path to spreadsheet file.
44
+ sheet: Sheet name or index (for Excel files only).
45
  """
46
+ import pandas as pd
47
+
48
  if not os.path.isfile(path):
49
  raise FileNotFoundError(path)
50
  ext = os.path.splitext(path)[1].lower()
 
 
51
  if ext == ".csv":
52
  df = pd.read_csv(path)
53
+ dfs = [df]
54
  else:
55
+ sheets = pd.read_excel(path, sheet_name=sheet if sheet not in ("", None) else None)
56
+ if isinstance(sheets, dict):
57
+ dfs = sheets.values()
58
+ else:
59
+ dfs = [sheets]
60
+ results = []
61
+ for df in dfs:
62
+ results.extend([{str(k): v for k, v in row.items()} for row in df.to_dict(orient="records")])
63
+ return results
64
+
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
 
71
  Args:
72
+ url: YouTube video URL.
73
+ lang: Preferred transcript language code (default "en").
74
  """
75
+ from urllib.parse import urlparse, parse_qs
76
+ from youtube_transcript_api._api import YouTubeTranscriptApi
77
+
78
  vid = parse_qs(urlparse(url).query).get("v", [None])[0] or url.split("/")[-1]
79
+ data = YouTubeTranscriptApi.get_transcript(
80
+ vid, languages=[lang, "en", "en-US", "en-GB"]
81
+ )
82
+ return " ".join(chunk["text"] for chunk in data).strip()
83
 
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
 
90
  Args:
91
+ path: Path to audio file (wav / mp3 / m4a / etc.).
92
+ model: Whisper model name (default "whisper-1").
93
  """
94
+ import openai
95
+
96
  if not os.path.isfile(path):
97
  raise FileNotFoundError(path)
98
+
99
  client = openai.OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
100
  with open(path, "rb") as fp:
101
+ transcript = client.audio.transcriptions.create(model=model, file=fp)
102
+ return transcript.text.strip()
103
+
104
 
105
+ # 5. --------------------------------------------------------------------
106
  @function_tool
107
  def image_ocr(path: str) -> str:
108
+ """Perform OCR on an image using Tesseract.
 
109
 
110
  Args:
111
+ path: Path to image file.
112
  """
113
+ from PIL import Image
114
+ import pytesseract
115
+
116
  if not os.path.isfile(path):
117
  raise FileNotFoundError(path)
118
+ return pytesseract.image_to_string(Image.open(path)).strip()
119
+
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:
127
+ query: The search query.
128
+ max_results: Maximum results to return (default 5).
129
  """
130
+ from duckduckgo_search import DDGS
131
+
132
+ results = []
133
  with DDGS() as ddgs:
134
+ for r in ddgs.text(query, max_results=max_results):
135
+ results.append(
136
+ {
137
+ "title": r.get("title", ""),
138
+ "href": r.get("href", ""),
139
+ "body": r.get("body", ""),
140
+ }
141
+ )
142
+ return results