Markus Schramm commited on
Commit
4565986
·
1 Parent(s): 81917a3

Add updated project files

Browse files
Files changed (5) hide show
  1. agents.py +160 -0
  2. app.py +42 -10
  3. requirements.txt +15 -2
  4. tests.ipynb +0 -0
  5. tools.py +398 -0
agents.py ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # agents.py
2
+
3
+ # agents.py
4
+ import shutil
5
+ import os
6
+ import json
7
+ from smolagents import CodeAgent, LiteLLMModel, DuckDuckGoSearchTool, HfApiModel,VisitWebpageTool, FinalAnswerTool
8
+ # import datetime
9
+
10
+ from smolagents.tools import Tool
11
+ from typing import Any
12
+
13
+ from tools import get_text_from_ascii_file, get_wikipedia_markdown, transcribe_mp3, describe_image_file, read_xls_File, get_youtube_video_transcript
14
+
15
+ # class FinalAnswerTool(Tool):
16
+ # name = "final_answer"
17
+ # description = "Provides a final answer to the given problem."
18
+ # inputs = {"answer": {"type": "any", "description": "The final answer to the problem"}}
19
+ # output_type = "any"
20
+
21
+ # def forward(self, answer: Any) -> Any:
22
+ # if "FINAL ANSWER:" in answer:
23
+ # return answer.split("FINAL ANSWER:")[-1].strip()
24
+ # else:
25
+ # return answer.strip()
26
+
27
+ # helper function based on recommendations:
28
+ # "For the sake of this course, make sure you don’t include the text “FINAL ANSWER” in your submission,
29
+ # just make your agent reply with the answer and nothing else."
30
+ def extract_final_answer(answer: str) -> str:
31
+ # answer = answer.strip()
32
+ # Check if the answer contains "FINAL ANSWER:" and elemint it
33
+ if "</think>" in answer:
34
+ answer = answer.split("</think>")[-1].strip()
35
+ while "final answer:" in answer.lower():
36
+ answer = answer.replace("FINAL ANSWER:", "").replace("Final Answer:", "").replace("Final answer:", "").replace("final answer:", "")
37
+ return answer.strip()
38
+
39
+ # Define GAIAAgent
40
+ class GAIAAgent:
41
+ def __init__(self, use_model: str = "ollamaSRV") -> None:
42
+ # Initialize LiteLLMModel with Gemini Flash
43
+ # check for ollama
44
+
45
+ if use_model == "ollamaSRV":
46
+ print("Using LiteLLMModel with Ollama reverse proxy server.")
47
+ self.model = LiteLLMModel(
48
+ # model_id='ollama/devstral:24b',
49
+ # model_id="ollama/cogito:14b",
50
+ model_id='ollama/qwen3:32b',
51
+ # model_id='ollama/gemma3:27b',
52
+ # model_id='ollama/qwen2.5-coder:32b-instruct-q4_K_M',
53
+ api_base="https://192.168.5.217:8000", # replace with remote open-ai compatible server if necessary
54
+ api_key=os.getenv("OLLAMA_REVPROXY_SRVML"),
55
+ num_ctx=16384, # ollama default is 2048 which will often fail horribly. 8192 works for easy tasks, more is better. Check https://huggingface.co/spaces/NyxKrage/LLM-Model-VRAM-Calculator to calculate how much VRAM this will need for the selected model
56
+ ssl_verify=False, # Explicitly disable SSL verification
57
+ extra_headers={
58
+ "Authorization": f"Bearer {os.getenv('OLLAMA_REVPROXY_SRVML')}", # Explicitly set auth header
59
+ },
60
+ flatten_messages_as_text = False,
61
+ timeout=900 # seconds, default is 600 seconds, set to 15 minutes to allow for longer tasks
62
+ )
63
+
64
+ elif (use_model == "ollama") and shutil.which("ollama"):
65
+ print("Using LiteLLMModel with local Ollama CLI.")
66
+ self.model = LiteLLMModel(
67
+ model_id="ollama/devstral:24b",
68
+ # model_id="ollama/cogito:14b",
69
+ max_tokens=16384
70
+ )
71
+ elif (use_model == "gemini") and "GEMINI_API_KEY" in os.environ:
72
+ print("Using Gemini Flash model with API key.")
73
+ self.model = LiteLLMModel(
74
+ model_id="gemini/gemini-2.5-flash-preview-05-20",
75
+ api_key=os.getenv("GEMINI_API_KEY"),
76
+ )
77
+ else:
78
+ print("Using HfApiModel with Qwen2.5-Coder-32B-Instruct.")
79
+ self.model = HfApiModel(
80
+ max_tokens=2096,
81
+ temperature=0.5,
82
+ model_id='Qwen/Qwen2.5-Coder-32B-Instruct',
83
+ custom_role_conversions=None
84
+ )
85
+
86
+ # Define the tools
87
+ self.tools = [
88
+ get_wikipedia_markdown, get_text_from_ascii_file, transcribe_mp3,
89
+ describe_image_file, get_youtube_video_transcript, read_xls_File,
90
+ DuckDuckGoSearchTool(), # Web search (main retrieval)
91
+ VisitWebpageTool(), # Optional: visit page if needed (sometimes helps)
92
+ FinalAnswerTool(), # Needed for FINAL ANSWER output
93
+ ]
94
+
95
+ prompt_templates = {'system_prompt': 'You are an expert assistant who can solve any task using code blobs. You will be given a task to solve as best you can.\nTo do so, you have been given access to a list of tools: these tools are basically Python functions which you can call with code.\nTo solve the task, you must plan forward to proceed in a series of steps, in a cycle of \'Thought:\', \'Code:\', and \'Observation:\' sequences.\n\nAt each step, in the \'Thought:\' sequence, you should first explain your reasoning towards solving the task and the tools that you want to use.\nThen in the \'Code:\' sequence, you should write the code in simple Python. The code sequence must end with \'<end_code>\' sequence.\nDuring each intermediate step, you can use \'print()\' to save whatever important information you will then need.\nThese print outputs will then appear in the \'Observation:\' field, which will be available as input for the next step.\nIn the end you have to return a final answer using the `final_answer` tool with the following template: FINAL ANSWER: [YOUR FINAL ANSWER]. YOUR FINAL ANSWER should be a number OR as few words as possible OR a comma separated list of numbers and/or strings. If you are asked for a number, don\'t use comma to write your number neither use units such as $ or percent sign unless specified otherwise. If you are asked for a string, don\'t use articles, neither abbreviations (e.g. for cities), and write the digits in plain text unless specified otherwise. If you are asked for a comma separated list, apply the above rules depending of whether the element to be put in the list is a number or a string. \n\nHere are a few examples using notional tools:\n---\nTask: "What is the result of the following operation: 5 + 3 + 1294.678?"\n\nThought: I will use python code to compute the result of the operation and then return the final answer using the `final_answer` tool\nCode:\n```py\nresult = 5 + 3 + 1294.678\nfinal_answer(print(f"FINAL ANSWER: {result}"))\n```<end_code>\n\n---\nTask:\n"Answer the question in the variable `question` about the image stored in the variable `image`. The question is in French.\nYou have been provided with these additional arguments, that you can access using the keys as variables in your python code:\n{\'question\': \'Quel est l\'animal sur l\'image?\', \'image\': \'path/to/image.jpg\'}"\n\nThought: I will use the following tools: `translator` to translate the question into English and then `image_qa` to answer the question on the input image.\nCode:\n```py\ntranslated_question = translator(question=question, src_lang="French", tgt_lang="English")\nprint(f"The translated question is {translated_question}.")\nanswer = image_qa(image=image, question=translated_question)\nfinal_answer(f"FINAL ANSWER {answer}")\n```<end_code>\n\n---\nTask:\nIn a 1979 interview, Stanislaus Ulam discusses with Martin Sherwin about other great physicists of his time, including Oppenheimer.\nWhat does he say was the consequence of Einstein learning too much math on his creativity, in one word?\n\nThought: I need to find and read the 1979 interview of Stanislaus Ulam with Martin Sherwin.\nCode:\n```py\npages = search(query="1979 interview Stanislaus Ulam Martin Sherwin physicists Einstein")\nprint(pages)\n```<end_code>\nObservation:\nNo result found for query "1979 interview Stanislaus Ulam Martin Sherwin physicists Einstein".\n\nThought: The query was maybe too restrictive and did not find any results. Let\'s try again with a broader query.\nCode:\n```py\npages = search(query="1979 interview Stanislaus Ulam")\nprint(pages)\n```<end_code>\nObservation:\nFound 6 pages:\n[Stanislaus Ulam 1979 interview](https://ahf.nuclearmuseum.org/voices/oral-histories/stanislaus-ulams-interview-1979/)\n\n[Ulam discusses Manhattan Project](https://ahf.nuclearmuseum.org/manhattan-project/ulam-manhattan-project/)\n\n(truncated)\n\nThought: I will read the first 2 pages to know more.\nCode:\n```py\nfor url in ["https://ahf.nuclearmuseum.org/voices/oral-histories/stanislaus-ulams-interview-1979/", "https://ahf.nuclearmuseum.org/manhattan-project/ulam-manhattan-project/"]:\n whole_page = visit_webpage(url)\n print(whole_page)\n print("\\n" + "="*80 + "\\n") # Print separator between pages\n```<end_code>\nObservation:\nManhattan Project Locations:\nLos Alamos, NM\nStanislaus Ulam was a Polish-American mathematician. He worked on the Manhattan Project at Los Alamos and later helped design the hydrogen bomb. In this interview, he discusses his work at\n(truncated)\n\nThought: I now have the final answer: from the webpages visited, Stanislaus Ulam says of Einstein: "He learned too much mathematics and sort of diminished, it seems to me personally, it seems to me his purely physics creativity." Let\'s answer in one word.\nCode:\n```py\nfinal_answer("FINAL ANSWER diminished")\n```<end_code>\n\n---\nTask: "Which city has the highest population: Guangzhou or Shanghai?"\n\nThought: I need to get the populations for both cities and compare them: I will use the tool `search` to get the population of both cities.\nCode:\n```py\nfor city in ["Guangzhou", "Shanghai"]:\n print(f"Population {city}:", search(f"{city} population")\n```<end_code>\nObservation:\nPopulation Guangzhou: [\'Guangzhou has a population of 15 million inhabitants as of 2021.\']\nPopulation Shanghai: \'26 million (2019)\'\n\nThought: Now I know that Shanghai has the highest population.\nCode:\n```py\nfinal_answer("FINAL ANSWER Shanghai")\n```<end_code>\n\n---\nTask: "What is the current age of the pope, raised to the power 0.36?"\n\nThought: I will use the tool `wiki` to get the age of the pope, and confirm that with a web search.\nCode:\n```py\npope_age_wiki = wiki(query="current pope age")\nprint("Pope age as per wikipedia:", pope_age_wiki)\npope_age_search = web_search(query="current pope age")\nprint("Pope age as per google search:", pope_age_search)\n```<end_code>\nObservation:\nPope age: "The pope Francis is currently 88 years old."\n\nThought: I know that the pope is 88 years old. Let\'s compute the result using python code.\nCode:\n```py\npope_current_age = 88 ** 0.36\nfinal_answer(f"FINAL ANSWER {pope_current_age}")\n```<end_code>\n\nAbove example were using notional tools that might not exist for you. On top of performing computations in the Python code snippets that you create, you only have access to these tools, behaving like regular python functions:\n```python\n{%- for tool in tools.values() %}\ndef {{ tool.name }}({% for arg_name, arg_info in tool.inputs.items() %}{{ arg_name }}: {{ arg_info.type }}{% if not loop.last %}, {% endif %}{% endfor %}) -> {{tool.output_type}}:\n """{{ tool.description }}\n\n Args:\n {%- for arg_name, arg_info in tool.inputs.items() %}\n {{ arg_name }}: {{ arg_info.description }}\n {%- endfor %}\n """\n{% endfor %}\n```\n\n{%- if managed_agents and managed_agents.values() | list %}\nYou can also give tasks to team members.\nCalling a team member works the same as for calling a tool: simply, the only argument you can give in the call is \'task\'.\nGiven that this team member is a real human, you should be very verbose in your task, it should be a long string providing informations as detailed as necessary.\nHere is a list of the team members that you can call:\n```python\n{%- for agent in managed_agents.values() %}\ndef {{ agent.name }}("Your query goes here.") -> str:\n """{{ agent.description }}"""\n{% endfor %}\n```\n{%- endif %}\n\nHere are the rules you should always follow to solve your task:\n1. Always provide a \'Thought:\' sequence, and a \'Code:\\n```py\' sequence ending with \'```<end_code>\' sequence, else you will fail.\n2. Use only variables that you have defined!\n3. Always use the right arguments for the tools. DO NOT pass the arguments as a dict as in \'answer = wiki({\'query\': "What is the place where James Bond lives?"})\', but use the arguments directly as in \'answer = wiki(query="What is the place where James Bond lives?")\'.\n4. Take care to not chain too many sequential tool calls in the same code block, especially when the output format is unpredictable. For instance, a call to search has an unpredictable return format, so do not have another tool call that depends on its output in the same block: rather output results with print() to use them in the next block.\n5. Call a tool only when needed, and never re-do a tool call that you previously did with the exact same parameters.\n6. Don\'t name any new variable with the same name as a tool: for instance don\'t name a variable \'final_answer\'.\n7. Never create any notional variables in our code, as having these in your logs will derail you from the true variables.\n8. You can use imports in your code, but only from the following list of modules: {{authorized_imports}}\n9. The state persists between code executions: so if in one step you\'ve created variables or imported modules, these will all persist.\n10. Don\'t give up! You\'re in charge of solving the task, not providing directions to solve it.\n\nNow Begin!',
96
+ 'planning': {'initial_plan': 'You are a world expert at analyzing a situation to derive facts, and plan accordingly towards solving a task.\nBelow I will present you a task. You will need to 1. build a survey of facts known or needed to solve the task, then 2. make a plan of action to solve the task.\n\n## 1. Facts survey\nYou will build a comprehensive preparatory survey of which facts we have at our disposal and which ones we still need.\nThese "facts" will typically be specific names, dates, values, etc. Your answer should use the below headings:\n### 1.1. Facts given in the task\nList here the specific facts given in the task that could help you (there might be nothing here).\n\n### 1.2. Facts to look up\nList here any facts that we may need to look up.\nAlso list where to find each of these, for instance a website, a file... - maybe the task contains some sources that you should re-use here.\n\n### 1.3. Facts to derive\nList here anything that we want to derive from the above by logical reasoning, for instance computation or simulation.\n\nDon\'t make any assumptions. For each item, provide a thorough reasoning. Do not add anything else on top of three headings above.\n\n## 2. Plan\nThen for the given task, develop a step-by-step high-level plan taking into account the above inputs and list of facts.\nThis plan should involve individual tasks based on the available tools, that if executed correctly will yield the correct answer.\nDo not skip steps, do not add any superfluous steps. Only write the high-level plan, DO NOT DETAIL INDIVIDUAL TOOL CALLS.\nAfter writing the final step of the plan, write the \'\\n<end_plan>\' tag and stop there.\n\nYou can leverage these tools, behaving like regular python functions:\n```python\n{%- for tool in tools.values() %}\ndef {{ tool.name }}({% for arg_name, arg_info in tool.inputs.items() %}{{ arg_name }}: {{ arg_info.type }}{% if not loop.last %}, {% endif %}{% endfor %}) -> {{tool.output_type}}:\n """{{ tool.description }}\n\n Args:\n {%- for arg_name, arg_info in tool.inputs.items() %}\n {{ arg_name }}: {{ arg_info.description }}\n {%- endfor %}\n """\n{% endfor %}\n```\n\n{%- if managed_agents and managed_agents.values() | list %}\nYou can also give tasks to team members.\nCalling a team member works the same as for calling a tool: simply, the only argument you can give in the call is \'task\'.\nGiven that this team member is a real human, you should be very verbose in your task, it should be a long string providing informations as detailed as necessary.\nHere is a list of the team members that you can call:\n```python\n{%- for agent in managed_agents.values() %}\ndef {{ agent.name }}("Your query goes here.") -> str:\n """{{ agent.description }}"""\n{% endfor %}\n```\n{%- endif %}\n\n---\nNow begin! Here is your task:\n```\n{{task}}\n```\nFirst in part 1, write the facts survey, then in part 2, write your plan.',
97
+ 'update_plan_pre_messages': 'You are a world expert at analyzing a situation, and plan accordingly towards solving a task.\nYou have been given the following task:\n```\n{{task}}\n```\n\nBelow you will find a history of attempts made to solve this task.\nYou will first have to produce a survey of known and unknown facts, then propose a step-by-step high-level plan to solve the task.\nIf the previous tries so far have met some success, your updated plan can build on these results.\nIf you are stalled, you can make a completely new plan starting from scratch.\n\nFind the task and history below:',
98
+ 'update_plan_post_messages': 'Now write your updated facts below, taking into account the above history:\n## 1. Updated facts survey\n### 1.1. Facts given in the task\n### 1.2. Facts that we have learned\n### 1.3. Facts still to look up\n### 1.4. Facts still to derive\n\nThen write a step-by-step high-level plan to solve the task above.\n## 2. Plan\n### 2. 1. ...\nEtc.\nThis plan should involve individual tasks based on the available tools, that if executed correctly will yield the correct answer.\nBeware that you have {remaining_steps} steps remaining.\nDo not skip steps, do not add any superfluous steps. Only write the high-level plan, DO NOT DETAIL INDIVIDUAL TOOL CALLS.\nAfter writing the final step of the plan, write the \'\\n<end_plan>\' tag and stop there.\n\nYou can leverage these tools, behaving like regular python functions:\n```python\n{%- for tool in tools.values() %}\ndef {{ tool.name }}({% for arg_name, arg_info in tool.inputs.items() %}{{ arg_name }}: {{ arg_info.type }}{% if not loop.last %}, {% endif %}{% endfor %}) -> {{tool.output_type}}:\n """{{ tool.description }}\n\n Args:\n {%- for arg_name, arg_info in tool.inputs.items() %}\n {{ arg_name }}: {{ arg_info.description }}\n {%- endfor %}"""\n{% endfor %}\n```\n\n{%- if managed_agents and managed_agents.values() | list %}\nYou can also give tasks to team members.\nCalling a team member works the same as for calling a tool: simply, the only argument you can give in the call is \'task\'.\nGiven that this team member is a real human, you should be very verbose in your task, it should be a long string providing informations as detailed as necessary.\nHere is a list of the team members that you can call:\n```python\n{%- for agent in managed_agents.values() %}\ndef {{ agent.name }}("Your query goes here.") -> str:\n """{{ agent.description }}"""\n{% endfor %}\n```\n{%- endif %}\n\nNow write your updated facts survey below, then your new plan.'},
99
+ 'managed_agent': {'task':(
100
+ "You are a highly capable and autonomous agent named {{name}}, designed to solve complex tasks efficiently.\n"
101
+ "A valued client has assigned you the following task:\n"
102
+ "---\n"
103
+ "Task:\n"
104
+ "{{task}}\n"
105
+ "---\n"
106
+ "To complete this task successfully, follow these steps carefully:\n"
107
+ " 1. Comprehend the task and identify the intended goal.\n"
108
+ " 2. Break the task into clear, logical steps.\n"
109
+ " 3. Select and prepare the tools or resources you need.\n"
110
+ " 4. Set up the required environment or context.\n"
111
+ " 5. Execute each step methodically.\n"
112
+ " 6. Monitor outcomes and identify any deviations.\n"
113
+ " 7. Revise your plan if necessary based on feedback.\n"
114
+ " 8. Maintain internal state and track progress.\n"
115
+ " 9. Verify that the goal has been fully achieved.\n"
116
+ " 10. Present the final result clearly and concisely.\n"
117
+ "If you succeed, you will be rewarded with a significant bonus.\n\n"
118
+ "Your final_answer MUST be:\n"
119
+ "- a number (retain its original type; do not include units),\n"
120
+ "- a concise phrase,\n"
121
+ "- or a comma-separated list of numbers or strings (no articles, no abbreviations).\n\n"
122
+ "Only the content passed to the final_answer tool will be preserved. Any other content will be discarded."),
123
+ 'report': "{{final_answer}}"},
124
+ 'final_answer': {
125
+ 'pre_messages': "",
126
+ 'post_messages': ""
127
+ }}
128
+
129
+
130
+ # Create the CodeAgent
131
+ self.agent = CodeAgent(
132
+ tools=self.tools,
133
+ model=self.model,
134
+ prompt_templates = prompt_templates,
135
+ max_steps=10, # should be enough, first guess --> to check
136
+ planning_interval=3, # should be enough, first guess --> to check
137
+ verbosity_level=2, # 0: no output, 1: only errors, 2: all outputs
138
+ additional_authorized_imports=["datetime", "numpy", "requests", "json", "re",
139
+ "bs4", "pandas", "lxml", "pymupdf", "openpyxl",
140
+ "scipy", "PIL", "cv2"],
141
+ name="RendelsGAIAAgent"
142
+ )
143
+
144
+ print("✅ GAIAAgent initialized with tools:", [t.name for t in self.tools])
145
+
146
+ def __call__(self, question: str) -> str:
147
+ print(f"\n[GAIAAgent] Running agent on question:\n{question}\n")
148
+ try:
149
+ result = self.agent.run(question)
150
+ final_answer = extract_final_answer(result)
151
+ print(f"\n[GAIAAgent] FINAL ANSWER extracted: {final_answer}\n")
152
+ return final_answer
153
+ except Exception as e:
154
+ print(f"[GAIAAgent] ERROR: {e}")
155
+ return f"ERROR: {e}"
156
+
157
+
158
+
159
+
160
+
app.py CHANGED
@@ -1,23 +1,25 @@
1
  import os
2
  import gradio as gr
3
  import requests
4
- import inspect
5
  import pandas as pd
6
 
 
 
7
  # (Keep Constants as is)
8
  # --- Constants ---
9
  DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
10
 
11
  # --- Basic Agent Definition ---
12
  # ----- THIS IS WERE YOU CAN BUILD WHAT YOU WANT ------
13
- class BasicAgent:
14
- def __init__(self):
15
- print("BasicAgent initialized.")
16
- def __call__(self, question: str) -> str:
17
- print(f"Agent received question (first 50 chars): {question[:50]}...")
18
- fixed_answer = "This is a default answer."
19
- print(f"Agent returning fixed answer: {fixed_answer}")
20
- return fixed_answer
21
 
22
  def run_and_submit_all( profile: gr.OAuthProfile | None):
23
  """
@@ -40,7 +42,8 @@ def run_and_submit_all( profile: gr.OAuthProfile | None):
40
 
41
  # 1. Instantiate Agent ( modify this part to create your agent)
42
  try:
43
- agent = BasicAgent()
 
44
  except Exception as e:
45
  print(f"Error instantiating agent: {e}")
46
  return f"Error initializing agent: {e}", None
@@ -70,12 +73,41 @@ def run_and_submit_all( profile: gr.OAuthProfile | None):
70
  return f"An unexpected error occurred fetching questions: {e}", None
71
 
72
  # 3. Run your Agent
 
73
  results_log = []
74
  answers_payload = []
75
  print(f"Running agent on {len(questions_data)} questions...")
76
  for item in questions_data:
77
  task_id = item.get("task_id")
78
  question_text = item.get("question")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  if not task_id or question_text is None:
80
  print(f"Skipping item with missing task_id or question: {item}")
81
  continue
 
1
  import os
2
  import gradio as gr
3
  import requests
4
+ # import inspect
5
  import pandas as pd
6
 
7
+ from agents import GAIAAgent
8
+
9
  # (Keep Constants as is)
10
  # --- Constants ---
11
  DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
12
 
13
  # --- Basic Agent Definition ---
14
  # ----- THIS IS WERE YOU CAN BUILD WHAT YOU WANT ------
15
+ # class BasicAgent:
16
+ # def __init__(self):
17
+ # print("BasicAgent initialized.")
18
+ # def __call__(self, question: str) -> str:
19
+ # print(f"Agent received question (first 50 chars): {question[:50]}...")
20
+ # fixed_answer = "This is a default answer."
21
+ # print(f"Agent returning fixed answer: {fixed_answer}")
22
+ # return fixed_answer
23
 
24
  def run_and_submit_all( profile: gr.OAuthProfile | None):
25
  """
 
42
 
43
  # 1. Instantiate Agent ( modify this part to create your agent)
44
  try:
45
+ # agent = BasicAgent()
46
+ agent = GAIAAgent()
47
  except Exception as e:
48
  print(f"Error instantiating agent: {e}")
49
  return f"Error initializing agent: {e}", None
 
73
  return f"An unexpected error occurred fetching questions: {e}", None
74
 
75
  # 3. Run your Agent
76
+ os.makedirs("downloaded_files", exist_ok=True)
77
  results_log = []
78
  answers_payload = []
79
  print(f"Running agent on {len(questions_data)} questions...")
80
  for item in questions_data:
81
  task_id = item.get("task_id")
82
  question_text = item.get("question")
83
+
84
+ # Check if task_id and question_text are present
85
+ filename = item.get("file_name")
86
+ if filename and not os.path.exists("downloaded_files/"+filename):
87
+ file_url = f"{api_url}/files/{task_id}"
88
+ print(f"Attempting to download file from: {file_url}")
89
+
90
+ try:
91
+ response = requests.get(file_url, timeout=30) # Increased timeout
92
+ response.raise_for_status() # Raise an exception for HTTP errors (4xx or 5xx)
93
+
94
+ with open("downloaded_files/"+filename, "wb") as f:
95
+ f.write(response.content)
96
+
97
+ except requests.exceptions.HTTPError as http_err:
98
+ print(f"HTTP error occurred: {http_err}")
99
+ print(f"Response content (first 500 chars): {response.text[:500]}")
100
+ except requests.exceptions.ConnectionError as conn_err:
101
+ print(f"Connection error occurred: {conn_err}")
102
+ except requests.exceptions.Timeout as timeout_err:
103
+ print(f"Timeout error occurred: {timeout_err}")
104
+ except requests.exceptions.RequestException as req_err:
105
+ print(f"An unexpected error occurred during the request: {req_err}")
106
+ except Exception as e:
107
+ print(f"An unexpected error occurred: {e}")
108
+ if filename:
109
+ question_text += f" (file: downloaded_files/{filename})"
110
+
111
  if not task_id or question_text is None:
112
  print(f"Skipping item with missing task_id or question: {item}")
113
  continue
requirements.txt CHANGED
@@ -1,2 +1,15 @@
1
- gradio
2
- requests
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ pandas
2
+ gradio[oauth]
3
+ requests
4
+ smolagents==1.16.1
5
+ huggingface_hub
6
+ litellm
7
+ python-dateutil
8
+ markdownify
9
+ duckduckgo_search
10
+ beautifulsoup4>=4.12.2
11
+ lxml>=4.9.3
12
+ PyMuPDF
13
+ openai-whisper
14
+ ffmpeg-python
15
+ youtube-transcript-api
tests.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
tools.py ADDED
@@ -0,0 +1,398 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import pandas as pd
3
+ # import wikipediaapi
4
+ from markdownify import markdownify as md
5
+ from smolagents import tool, LiteLLMModel
6
+ import whisper
7
+
8
+ from youtube_transcript_api import YouTubeTranscriptApi
9
+ from youtube_transcript_api.formatters import JSONFormatter
10
+
11
+ import base64
12
+ import mimetypes
13
+ import requests # Keep for consistency, though not used for fetching image in this version
14
+ import os # Added for os.path.join
15
+
16
+ import re
17
+ from bs4 import BeautifulSoup, Tag, Comment
18
+
19
+
20
+ # that could be better done via a managed agent, but this is a quick hack to get it working
21
+ @tool
22
+ def describe_image_file(local_image_path: str) -> str:
23
+ """
24
+ Describe the contents of a local image file in detail and return the description as text.
25
+ Args:
26
+ local_image_path (str): The path to the local image file to be described.
27
+ Returns:
28
+ str: A detailed description of the image contents.
29
+ """
30
+ model = LiteLLMModel(
31
+ model_id='ollama/gemma3:27b',
32
+ api_base="https://192.168.5.217:8000", # replace with remote open-ai compatible server if necessary
33
+ api_key=os.getenv("OLLAMA_REVPROXY_SRVML"),
34
+ num_ctx=16384, # ollama default is 2048 which will often fail horribly. 8192 works for easy tasks, more is better. Check https://huggingface.co/spaces/NyxKrage/LLM-Model-VRAM-Calculator to calculate how much VRAM this will need for the selected model
35
+ ssl_verify=False, # Explicitly disable SSL verification
36
+ extra_headers={
37
+ "Authorization": f"Bearer {os.getenv('OLLAMA_REVPROXY_SRVML')}", # Explicitly set auth header
38
+ },
39
+ flatten_messages_as_text = False
40
+ )
41
+
42
+ text_prompt = "What is in this image? Describe it in detail."
43
+
44
+ try:
45
+
46
+ if not os.path.exists(local_image_path):
47
+ raise FileNotFoundError(f"Image file not found at {local_image_path}. Please ensure it was downloaded correctly.")
48
+
49
+ # 1. Read the image content from the local file
50
+ with open(local_image_path, "rb") as image_file:
51
+ image_content_bytes = image_file.read()
52
+
53
+ # 2. Base64 encode the image content
54
+ base64_image_bytes = base64.b64encode(image_content_bytes)
55
+ base64_image_string = base64_image_bytes.decode('utf-8')
56
+
57
+ # 3. Set MIME type based on file extension
58
+ if local_image_path.lower().endswith('.png'):
59
+ content_type = 'image/png'
60
+ elif local_image_path.lower().endswith('.jpg') or local_image_path.lower().endswith('.jpeg'):
61
+ content_type = 'image/jpeg'
62
+ elif local_image_path.lower().endswith('.gif'):
63
+ content_type = 'image/gif'
64
+ elif local_image_path.lower().endswith('.bmp'):
65
+ content_type = 'image/bmp'
66
+ elif local_image_path.lower().endswith('.webp'):
67
+ content_type = 'image/webp'
68
+ else:
69
+ content_type = mimetypes.guess_type(local_image_path)[0] or 'application/octet-stream'
70
+ print(f"Using specified MIME type: {content_type}")
71
+
72
+ # 4. Construct the data URI
73
+ data_uri = f"data:{content_type};base64,{base64_image_string}"
74
+
75
+ # Construct the messages payload
76
+ messages = [
77
+ {
78
+ "role": "user",
79
+ "content": [
80
+ {"type": "text", "text": text_prompt},
81
+ {
82
+ "type": "image_url",
83
+ "image_url": {
84
+ "url": data_uri # Use the base64 data URI here
85
+ }
86
+ }
87
+ ]
88
+ }
89
+ ]
90
+
91
+ # Assuming 'model' is your LiteLLMModel instance initialized in a previous cell (e.g., cell 'dfc845ab')
92
+ if 'model' not in locals():
93
+ raise NameError("Variable 'model' is not defined. Please run the cell that initializes the LiteLLMModel.")
94
+
95
+ response = model.generate(messages)
96
+ return response
97
+
98
+ except FileNotFoundError as fnf_err:
99
+ print(f"File error: {fnf_err}")
100
+ except NameError as ne:
101
+ print(f"A required variable might not be defined (e.g., filename, model): {ne}")
102
+ print("Please ensure the cells defining these variables have been run.")
103
+ except Exception as e:
104
+ print(f"An error occurred: {e}")
105
+
106
+
107
+ @tool
108
+ def get_youtube_video_transcript(video_id: str) -> str:
109
+ """
110
+ Fetches the transcript of a YouTube video by its ID and returns it in JSON format.
111
+ The video ID can be found in the YouTube video URL:
112
+ https://www.youtube.com/watch?v=VIDEO_ID, where VIDEO_ID is the part after "v=".
113
+ example: for the url https://www.youtube.com/watch?v=L1vXCYZAYYM the video_id is "L1vXCYZAYYM".
114
+
115
+ Args:
116
+ video_id (str): The YouTube video ID.
117
+ Returns:
118
+ str: The transcript in JSON format.
119
+ """
120
+
121
+ ytt_api = YouTubeTranscriptApi()
122
+ transcript = ytt_api.fetch(video_id)
123
+
124
+ formatter = JSONFormatter()
125
+
126
+ # .format_transcript(transcript) turns the transcript into a JSON string.
127
+ json_formatted = formatter.format_transcript(transcript)
128
+ return json_formatted
129
+
130
+
131
+ @tool
132
+ def transcribe_mp3(mp3_path: str, model_size: str = "base") -> str:
133
+ """
134
+ Transcribe an MP3 file to text using Whisper.
135
+
136
+ Args:
137
+ mp3_path (str): Path to the MP3 file.
138
+ model_size (str): Whisper model size (tiny, base, small, medium, large).
139
+
140
+ Returns:
141
+ str: Transcribed text.
142
+ """
143
+ transcription_path = mp3_path.replace(".mp3", "_transcript.txt")
144
+
145
+ # Check if transcription already exists
146
+ if os.path.exists(transcription_path):
147
+ with open(transcription_path, 'r', encoding='utf-8') as f:
148
+ return f.read()
149
+
150
+ # Load model
151
+ model = whisper.load_model(model_size)
152
+
153
+ # Transcribe
154
+ result = model.transcribe(mp3_path)
155
+
156
+ transcription = result["text"]
157
+
158
+ # Save transcription to file
159
+ with open(transcription_path, 'w', encoding='utf-8') as f:
160
+ f.write(transcription)
161
+
162
+ # Return the text
163
+ return transcription
164
+
165
+
166
+ @tool
167
+ def get_text_from_ascii_file(filepath: str) -> str:
168
+ """
169
+ Reads the content of an ASCII text file and returns it as a string.
170
+ Args:
171
+ filepath (str): The path to the ASCII text file.
172
+ Returns:
173
+ str: The content of the file as a string.
174
+ """
175
+ if not os.path.exists(filepath):
176
+ raise FileNotFoundError(f"The file at {filepath} does not exist.")
177
+ with open(filepath, "r") as f:
178
+ return f.read()
179
+
180
+
181
+ # @tool
182
+ # def get_wikipedia_page_content(page_title: str, lang: str='en') -> str:
183
+ # """
184
+ # This function uses the `wikipediaapi` library to retrieve the content of a specified Wikipedia page in a given language.
185
+ # For example: for the url 'https://en.wikipedia.org/wiki/Python_(programming_language)' the page_title would be 'Python_(programming_language)' and the lang would be 'en'.
186
+ # It returns the content of the page as a Markdown-formatted string.
187
+
188
+ # Args:
189
+ # page_title (str): The title of the Wikipedia page to fetch.
190
+ # lang (str): The language of the Wikipedia page (default is 'en' for English).
191
+ # Returns:
192
+ # str: The content of the Wikipedia page.
193
+ # """
194
+
195
+ # MY_EMAIL = os.getenv("MY_EMAIL", None)
196
+ # if MY_EMAIL is None:
197
+ # raise ValueError("MY_EMAIL environment variable is not set. Please set it to your email address.")
198
+
199
+ # wiki_wiki = wikipediaapi.Wikipedia(user_agent=f'Wiki Agent ({MY_EMAIL})', language=lang)
200
+ # page = wiki_wiki.page(page_title)
201
+ # if not page.exists():
202
+ # raise ValueError(f"The Wikipedia page '{page_title}' does not exist.")
203
+ # return md(page.text)
204
+
205
+
206
+
207
+
208
+
209
+ @tool
210
+ def get_wikipedia_markdown(
211
+ title: str,
212
+ lang: str = 'en',
213
+ ignore_references: bool = True,
214
+ ignore_links: bool = True
215
+ ) -> str:
216
+ """
217
+ Fetches the main content of a Wikipedia page and returns it as Markdown,
218
+ excluding infoboxes, navigation templates, images, and—if requested—the
219
+ References, Further reading, and External links sections. It's recommended
220
+ to start with ignore_references=True and ignore_links=True
221
+ to reduce the amount of output to the pure infomation.
222
+
223
+ Args:
224
+ title (str): Wikipedia page title (e.g., "Mercedes_Sosa").
225
+ lang (str): Language code (default 'en').
226
+ ignore_references (bool): If True, drop "References", "Further reading",
227
+ and "External links" sections entirely.
228
+ ignore_links (bool): If True, strip out all <a> tags entirely.
229
+
230
+ Returns:
231
+ str: Markdown-formatted content of the main article body.
232
+ """
233
+ # 1. Fetch raw HTML
234
+ url = f"https://{lang}.wikipedia.org/wiki/{title}"
235
+ try:
236
+ response = requests.get(url)
237
+ response.raise_for_status()
238
+ except requests.exceptions.HTTPError as e:
239
+
240
+ # use wikipedia's API to check if the page exists
241
+ api_url = f"https://{lang}.wikipedia.org/w/api.php"
242
+ search_params = {
243
+ 'list': 'search',
244
+ 'srprop': '',
245
+ 'srlimit': 10,
246
+ 'limit': 10,
247
+ 'srsearch': title.replace("_", " "),
248
+ 'srinfo': 'suggestion',
249
+ 'format': 'json',
250
+ 'action': 'query'
251
+ }
252
+
253
+ headers = {
254
+ 'User-Agent': "mozilla /5.0 (Windows NT 10.0; Win64; x64)"
255
+ }
256
+
257
+ r = requests.get(api_url, params=search_params, headers=headers)
258
+
259
+ raw_results = r.json()
260
+ search_results = [d['title'].replace(" ", "_") for d in raw_results['query']['search']]
261
+ if ('searchinfo' in raw_results['query']) and ('suggestion' in raw_results['query']['searchinfo']):
262
+ search_results.insert(0, raw_results['query']['searchinfo']['suggestion'].replace(" ", "_"))
263
+
264
+ errorMsg = f"Could not fetch page '{title}' for language '{lang}' (HTTP {response.status_code})."
265
+ if search_results:
266
+ errorMsg += f" Did you mean one of these pages? {', '.join(search_results)}"
267
+
268
+ raise ValueError(errorMsg) from e
269
+
270
+ html = response.text
271
+
272
+ # 2. Parse with BeautifulSoup and isolate the article’s main <div>
273
+ soup = BeautifulSoup(html, "lxml")
274
+ content_div = soup.find("div", class_="mw-parser-output") #
275
+ if content_div is None:
276
+ raise ValueError(f"Could not find main content for page '{title}'")
277
+
278
+ # 2a. Remove all “[edit]” links (<span class="mw-editsection">…)
279
+ for edit_span in content_div.find_all("span", class_="mw-editsection"):
280
+ edit_span.decompose() #
281
+
282
+ # 2b. Remove any superscript footnote markers (<sup class="reference">…)
283
+ for sup in content_div.find_all("sup", class_="reference"):
284
+ sup.decompose() #
285
+
286
+ # 2c. Remove any parser‐debug comments (e.g., “NewPP limit report…”, “Transclusion expansion time report…”)
287
+ for comment in content_div.find_all(string=lambda text: isinstance(text, Comment)):
288
+ comment_text = str(comment)
289
+ # If the comment contains debug keywords, extract it
290
+ if (
291
+ "NewPP limit report" in comment_text
292
+ or "Transclusion expansion time report" in comment_text
293
+ or "Saved in parser cache" in comment_text
294
+ ):
295
+ comment.extract() #
296
+
297
+ # 3. Remove unwanted “boilerplate” elements:
298
+ # a) Infoboxes (sidebars)
299
+ for infobox in content_div.find_all("table", class_=re.compile(r"infobox")):
300
+ infobox.decompose() #
301
+
302
+ # b) Table of Contents
303
+ toc = content_div.find("div", id="toc")
304
+ if toc:
305
+ toc.decompose() #
306
+
307
+ # c) Navigation templates (navbox/vertical-navbox/metadata)
308
+ for nav in content_div.find_all(
309
+ ["div", "table"],
310
+ class_=re.compile(r"navbox|vertical-navbox|metadata")
311
+ ):
312
+ nav.decompose() #
313
+
314
+ # d) Thumbnails / image wrappers
315
+ for thumb in content_div.find_all("div", class_=re.compile(r"thumb")):
316
+ thumb.decompose() #
317
+
318
+ # e) Raw <img> tags
319
+ for img in content_div.find_all("img"):
320
+ img.decompose() #
321
+
322
+ # 4. Convert any remaining <table> into a Markdown table **in-place**
323
+ def table_to_markdown(table_tag: Tag) -> str:
324
+ """
325
+ Converts a <table> into a Markdown-formatted table, preserving <th> headers.
326
+ """
327
+ headers = []
328
+ header_row = table_tag.find("tr")
329
+ if header_row:
330
+ for th in header_row.find_all("th"):
331
+ headers.append(th.get_text(strip=True))
332
+ md_table = ""
333
+ if headers:
334
+ md_table += "| " + " | ".join(headers) + " |\n"
335
+ md_table += "| " + " | ".join("---" for _ in headers) + " |\n"
336
+ # Now process data rows (skip the first <tr> if it was header row)
337
+ for row in table_tag.find_all("tr")[1:]:
338
+ cells = row.find_all(["td", "th"])
339
+ if not cells:
340
+ continue
341
+ row_texts = [cell.get_text(strip=True) for cell in cells]
342
+ md_table += "| " + " | ".join(row_texts) + " |\n"
343
+ return md_table.rstrip()
344
+
345
+ for table in content_div.find_all("table"):
346
+ # Skip infobox/navigation tables (already removed above)
347
+ if "infobox" in table.get("class", []) or table.get("role") == "navigation":
348
+ continue
349
+ markdown_table = table_to_markdown(table) #
350
+ new_node = soup.new_string("\n\n" + markdown_table + "\n\n")
351
+ table.replace_with(new_node)
352
+
353
+ # 5. Remove “References”, “Further reading” & “External links” sections if requested
354
+ if ignore_references:
355
+ section_ids = {"references", "further_reading", "external_links"}
356
+ # We look for wrapper <div class="mw-heading mw-heading2"> or mw-heading3
357
+ for wrapper in content_div.find_all("div", class_=re.compile(r"mw-heading mw-heading[23]")):
358
+ heading_tag = wrapper.find(re.compile(r"^h[2-3]$"))
359
+ if heading_tag and heading_tag.get("id", "").strip().lower() in section_ids:
360
+ # Collect every sibling until the next wrapper of the same form
361
+ siblings_to_remove = []
362
+ for sib in wrapper.find_next_siblings():
363
+ if (
364
+ sib.name == "div"
365
+ and "mw-heading" in (sib.get("class") or [])
366
+ and re.match(r"mw-heading mw-heading[23]", " ".join(sib.get("class") or []))
367
+ ):
368
+ break
369
+ siblings_to_remove.append(sib)
370
+ # First delete those siblings
371
+ for node in siblings_to_remove:
372
+ node.decompose() #
373
+ # Finally delete the wrapper itself
374
+ wrapper.decompose() #
375
+
376
+ # 6. Convert the cleaned HTML into Markdown
377
+ markdown_options = {}
378
+ if ignore_links:
379
+ markdown_options["strip"] = ["a"] # strip all <a> tags (keep only their text)
380
+
381
+ raw_html = "".join(str(child) for child in content_div.children)
382
+ markdown_text = md(raw_html, **markdown_options) #
383
+
384
+ # 7. Collapse 3+ blank lines into exactly two
385
+ markdown_text = re.sub(r"\n{3,}", "\n\n", markdown_text).strip()
386
+
387
+ return markdown_text
388
+
389
+
390
+ @tool
391
+ def read_xls_File(file_path: str) -> object:
392
+ """This tool loads xls file into pandas and returns it.
393
+ Args:
394
+ file_path (str): File path to the xls file.
395
+ Returns:
396
+ object: The loaded xls file as a pandas DataFrame.
397
+ """
398
+ return pd.read_excel(file_path)