ptoloudis commited on
Commit
b3e303c
·
verified ·
1 Parent(s): 968807e

Upload 5 files

Browse files
Files changed (5) hide show
  1. README.md +50 -1
  2. agent.py +298 -100
  3. app.py +14 -30
  4. requirements.txt +7 -7
  5. system_prompt.txt +4 -14
README.md CHANGED
@@ -12,4 +12,53 @@ hf_oauth: true
12
  hf_oauth_expiration_minutes: 480
13
  ---
14
 
15
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  hf_oauth_expiration_minutes: 480
13
  ---
14
 
15
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
16
+
17
+ ## Agent
18
+
19
+ `agent.py` defines `GaiaAgent`, a LangGraph agent (explicit `StateGraph` with an
20
+ `assistant` node and a `ToolNode`, routed by `tools_condition`) used to answer the
21
+ GAIA benchmark questions for [Unit 4's hands-on assignment](https://huggingface.co/learn/agents-course/unit4/hands-on).
22
+ Structure and prompt are adapted from the [fisherman611/gaia-agent](https://huggingface.co/spaces/fisherman611/gaia-agent)
23
+ Space, trimmed to tools that need no extra paid API keys and no arbitrary code
24
+ execution (that Space's Supabase RAG retriever, Tavily/Groq providers, and
25
+ multi-language code interpreter were left out for that reason).
26
+
27
+ It calls an LLM through the Hugging Face Inference API and has access to:
28
+ web search (DuckDuckGo), Wikipedia search, arXiv search, arithmetic tools
29
+ (add/subtract/multiply/divide/modulus/power/square_root), a tool to download a
30
+ GAIA task's attached file, a generic URL downloader, and CSV/Excel analysis
31
+ tools. The system prompt (`system_prompt.txt`) asks the model to end its reply
32
+ with `FINAL ANSWER: ...`, which `agent.py` parses out before submission — per
33
+ the assignment's rule that submitted answers must contain only the answer
34
+ itself.
35
+
36
+ To run it (locally or as a Space), set the `HF_TOKEN` secret/env var to a
37
+ Hugging Face access token with Inference API permission. Optionally set
38
+ `HF_AGENT_MODEL` to override the default model
39
+ (`meta-llama/Llama-3.1-8B-Instruct`), and `HF_AGENT_PROVIDER` to override the
40
+ default Inference Provider (`deepinfra`) — pinned explicitly because the
41
+ router's `auto` selection can land on a provider that doesn't support tool
42
+ calling for a given model (HTTP 422 `UNSUPPORTED_OPENAI_PARAMS`), and not every
43
+ provider hosts every model (HTTP 4xx "Model ... is not supported by provider
44
+ ..."). Check `https://router.huggingface.co/v1/models/<repo_id>` to see which
45
+ providers host a given model and whether they support tools before changing
46
+ either variable.
47
+
48
+ The default 8B model was chosen for cost, not accuracy: the HF free tier only
49
+ includes $0.10/month of Inference Provider credits, and a 70B model burns
50
+ through that almost instantly across a full GAIA run. If you have more budget
51
+ (pre-paid credits or PRO), switch to a larger model such as
52
+ `meta-llama/Llama-3.3-70B-Instruct` on the `together` provider for more
53
+ reliable reasoning on harder questions.
54
+
55
+ ### Using Claude instead
56
+
57
+ Set `LLM_PROVIDER=anthropic` and `ANTHROPIC_API_KEY` (from
58
+ [console.anthropic.com](https://console.anthropic.com), pay-as-you-go billing —
59
+ no fixed free monthly allowance like HF's) to run the agent on Claude instead.
60
+ Optionally set `ANTHROPIC_MODEL` to override the default (`claude-haiku-4-5`,
61
+ picked for its low cost across many tool-calling round trips per question);
62
+ `claude-sonnet-5` trades cost for stronger reasoning on harder questions.
63
+ Claude's native tool calling is also more reliable than the HF router, which
64
+ can silently route to a provider that doesn't support the `tools` param.
agent.py CHANGED
@@ -1,163 +1,319 @@
1
- """LangGraph Agent"""
 
 
 
 
 
 
 
 
 
2
  import os
3
  import re
4
- from dotenv import load_dotenv
5
- from langgraph.graph import START, StateGraph, MessagesState
6
- from langgraph.prebuilt import tools_condition
7
- from langgraph.prebuilt import ToolNode
8
- from langchain_google_genai import ChatGoogleGenerativeAI
9
- from langchain_groq import ChatGroq
10
- from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
 
 
11
  from langchain_community.tools import DuckDuckGoSearchRun
12
- from langchain_community.document_loaders import WikipediaLoader
13
- from langchain_community.document_loaders import ArxivLoader
14
- from langchain_core.messages import SystemMessage
15
  from langchain_core.tools import tool
 
 
 
 
 
 
 
 
 
 
16
 
17
- load_dotenv()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
  _FINAL_ANSWER_RE = re.compile(r"final answer\s*:\s*", re.IGNORECASE)
20
 
21
 
22
- def extract_final_answer(text: str) -> str:
23
- """Strip the `FINAL ANSWER:` prefix the system prompt asks the model for."""
24
- match = _FINAL_ANSWER_RE.search(text)
25
- answer = text[match.end():] if match else text
26
- return answer.strip()
27
 
28
 
29
  @tool
30
- def multiply(a: int, b: int) -> int:
31
- """Multiply two numbers.
 
32
  Args:
33
- a: first int
34
- b: second int
35
  """
36
- return a * b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
  @tool
39
- def add(a: int, b: int) -> int:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  """Add two numbers.
41
 
42
  Args:
43
- a: first int
44
- b: second int
45
  """
46
  return a + b
47
 
 
48
  @tool
49
- def subtract(a: int, b: int) -> int:
50
  """Subtract two numbers.
51
 
52
  Args:
53
- a: first int
54
- b: second int
55
  """
56
  return a - b
57
 
 
58
  @tool
59
- def divide(a: int, b: int) -> float:
 
 
 
 
 
 
 
 
 
 
 
60
  """Divide two numbers.
61
 
62
  Args:
63
- a: first int
64
- b: second int
65
  """
66
  if b == 0:
67
  raise ValueError("Cannot divide by zero.")
68
  return a / b
69
 
 
70
  @tool
71
  def modulus(a: int, b: int) -> int:
72
- """Get the modulus of two numbers.
73
 
74
  Args:
75
- a: first int
76
- b: second int
77
  """
78
  return a % b
79
 
 
80
  @tool
81
- def wiki_search(query: str) -> str:
82
- """Search Wikipedia for a query and return maximum 2 results.
83
 
84
  Args:
85
- query: The search query."""
86
- search_docs = WikipediaLoader(query=query, load_max_docs=2).load()
87
- return "\n\n---\n\n".join(
88
- f'<Document source="{doc.metadata.get("source", "")}"/>\n{doc.page_content}\n</Document>'
89
- for doc in search_docs
90
- )
91
 
92
  @tool
93
- def web_search(query: str) -> str:
94
- """Search the web (DuckDuckGo) for a query and return a few results.
95
 
96
  Args:
97
- query: The search query."""
98
- return DuckDuckGoSearchRun().invoke(query)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
 
100
  @tool
101
- def arvix_search(query: str) -> str:
102
- """Search Arxiv for a query and return maximum 3 result.
103
 
104
  Args:
105
- query: The search query."""
106
- search_docs = ArxivLoader(query=query, load_max_docs=3).load()
107
- return "\n\n---\n\n".join(
108
- f'<Document source="{doc.metadata.get("source", "")}"/>\n{doc.page_content[:1000]}\n</Document>'
109
- for doc in search_docs
110
- )
 
 
 
 
 
 
 
 
 
111
 
112
 
113
- # load the system prompt from the file
114
- with open("system_prompt.txt", "r", encoding="utf-8") as f:
115
- system_prompt = f.read()
116
-
117
- # System message
118
- sys_msg = SystemMessage(content=system_prompt)
119
-
120
- tools = [
121
- multiply,
122
- add,
123
- subtract,
124
- divide,
125
- modulus,
126
- wiki_search,
127
- web_search,
128
- arvix_search,
129
- ]
130
-
131
- # Build graph function
132
- def build_graph(provider: str = "google"):
133
- """Build the graph"""
134
- if provider == "google":
135
- # Google Gemini - needs GOOGLE_API_KEY
136
- llm = ChatGoogleGenerativeAI(model="gemini-2.0-flash", temperature=0)
137
- elif provider == "groq":
138
- # Groq https://console.groq.com/docs/models - needs GROQ_API_KEY
139
- llm = ChatGroq(model="llama-3.3-70b-versatile", temperature=0)
140
- elif provider == "huggingface":
141
- # HF Inference Providers - needs HF_TOKEN. "auto" provider selection can land on a
142
- # backend that doesn't support tool calling, so pin one that does (override via
143
- # HF_AGENT_PROVIDER if it doesn't happen to host HF_AGENT_MODEL).
144
- llm = ChatHuggingFace(
145
- llm=HuggingFaceEndpoint(
146
- repo_id=os.getenv("HF_AGENT_MODEL", "meta-llama/Llama-3.1-8B-Instruct"),
147
- provider=os.getenv("HF_AGENT_PROVIDER", "deepinfra"),
148
- huggingfacehub_api_token=os.getenv("HF_TOKEN"),
149
- temperature=0,
150
- max_new_tokens=1024,
151
- ),
152
  )
153
- else:
154
- raise ValueError("Invalid provider. Choose 'google', 'groq' or 'huggingface'.")
155
- # Bind tools to LLM
156
- llm_with_tools = llm.bind_tools(tools)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
 
158
- # Node
159
  def assistant(state: MessagesState):
160
- """Assistant node"""
161
  return {"messages": [llm_with_tools.invoke(state["messages"])]}
162
 
163
  builder = StateGraph(MessagesState)
@@ -167,5 +323,47 @@ def build_graph(provider: str = "google"):
167
  builder.add_conditional_edges("assistant", tools_condition)
168
  builder.add_edge("tools", "assistant")
169
 
170
- # Compile graph
171
  return builder.compile()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """LangGraph agent that answers GAIA benchmark questions (Hugging Face Agents Course, Unit 4).
2
+
3
+ The agent is a LangGraph ReAct-style graph: an LLM served through the Hugging Face
4
+ Inference API, bound to a toolset (web/Wikipedia/arXiv search, arithmetic, GAIA
5
+ task-file download, and CSV/Excel analysis). Structure and prompt are adapted from
6
+ https://huggingface.co/spaces/fisherman611/gaia-agent, trimmed to tools that need no
7
+ extra paid API keys and no arbitrary code execution. See
8
+ https://huggingface.co/learn/agents-course/unit4/hands-on for the assignment.
9
+ """
10
+
11
  import os
12
  import re
13
+ import tempfile
14
+ import uuid
15
+ from pathlib import Path
16
+ from typing import Optional
17
+ from urllib.parse import urlparse
18
+
19
+ import pandas as pd
20
+ import requests
21
+ from langchain_community.document_loaders import ArxivLoader, WikipediaLoader
22
  from langchain_community.tools import DuckDuckGoSearchRun
23
+ from langchain_core.messages import HumanMessage, SystemMessage
 
 
24
  from langchain_core.tools import tool
25
+ from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
26
+ from langgraph.graph import START, MessagesState, StateGraph
27
+ from langgraph.prebuilt import ToolNode, tools_condition
28
+
29
+ DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
30
+
31
+ # "huggingface" (default, needs only HF_TOKEN) or "anthropic" (needs ANTHROPIC_API_KEY,
32
+ # paid - https://console.anthropic.com). Anthropic's native tool calling is more reliable
33
+ # than the HF router's per-provider variance, at the cost of no free tier.
34
+ LLM_PROVIDER = os.getenv("LLM_PROVIDER", "huggingface")
35
 
36
+ # Any HF Inference API model that supports tool calling works here. Override via the
37
+ # HF_AGENT_MODEL env var without touching code. Llama-3.1-8B-Instruct is ~15x cheaper per
38
+ # token than the 70B default it replaced - picked to fit the free tier's tiny $0.10/month
39
+ # Inference Provider credit allowance; swap to a larger model (e.g.
40
+ # meta-llama/Llama-3.3-70B-Instruct on the "together" provider) once you have more budget
41
+ # for better reasoning on harder GAIA questions.
42
+ HF_MODEL_REPO_ID = os.getenv("HF_AGENT_MODEL", "meta-llama/Llama-3.1-8B-Instruct")
43
+
44
+ # Left unset ("auto"), the HF router picks whichever provider is cheapest/fastest for the
45
+ # model, and some of those don't support the `tools` param at all (HTTP 422
46
+ # UNSUPPORTED_OPENAI_PARAMS). Pin one that reliably supports tool calling; override via
47
+ # HF_AGENT_PROVIDER if it doesn't happen to host HF_AGENT_MODEL.
48
+ HF_MODEL_PROVIDER = os.getenv("HF_AGENT_PROVIDER", "deepinfra")
49
+
50
+ # Cheapest current Claude model - a good fit for an agent that makes many tool-calling
51
+ # round trips per question. Override via ANTHROPIC_MODEL (e.g. "claude-sonnet-5" for
52
+ # stronger reasoning on harder questions, at higher cost).
53
+ ANTHROPIC_MODEL = os.getenv("ANTHROPIC_MODEL", "claude-haiku-4-5")
54
+
55
+ with open(Path(__file__).parent / "system_prompt.txt", "r", encoding="utf-8") as f:
56
+ SYSTEM_PROMPT = f.read()
57
 
58
  _FINAL_ANSWER_RE = re.compile(r"final answer\s*:\s*", re.IGNORECASE)
59
 
60
 
61
+ ### =============== SEARCH TOOLS =============== ###
 
 
 
 
62
 
63
 
64
  @tool
65
+ def web_search(query: str) -> str:
66
+ """Search the web (DuckDuckGo) for a query and return a few results.
67
+
68
  Args:
69
+ query: The search query.
 
70
  """
71
+ return DuckDuckGoSearchRun().invoke(query)
72
+
73
+
74
+ @tool
75
+ def wiki_search(query: str) -> str:
76
+ """Search Wikipedia for a query and return up to 2 results.
77
+
78
+ Args:
79
+ query: The search query.
80
+ """
81
+ docs = WikipediaLoader(query=query, load_max_docs=2).load()
82
+ return "\n\n---\n\n".join(
83
+ f'<Document source="{d.metadata.get("source", "")}"/>\n{d.page_content}\n</Document>'
84
+ for d in docs
85
+ )
86
+
87
 
88
  @tool
89
+ def arxiv_search(query: str) -> str:
90
+ """Search arXiv for a query and return up to 2 results (abstracts truncated).
91
+
92
+ Args:
93
+ query: The search query.
94
+ """
95
+ docs = ArxivLoader(query=query, load_max_docs=2).load()
96
+ return "\n\n---\n\n".join(
97
+ f'<Document source="{d.metadata.get("source", "")}"/>\n{d.page_content[:1000]}\n</Document>'
98
+ for d in docs
99
+ )
100
+
101
+
102
+ ### =============== MATH TOOLS =============== ###
103
+
104
+
105
+ @tool
106
+ def add(a: float, b: float) -> float:
107
  """Add two numbers.
108
 
109
  Args:
110
+ a: the first number
111
+ b: the second number
112
  """
113
  return a + b
114
 
115
+
116
  @tool
117
+ def subtract(a: float, b: float) -> float:
118
  """Subtract two numbers.
119
 
120
  Args:
121
+ a: the first number
122
+ b: the second number
123
  """
124
  return a - b
125
 
126
+
127
  @tool
128
+ def multiply(a: float, b: float) -> float:
129
+ """Multiply two numbers.
130
+
131
+ Args:
132
+ a: the first number
133
+ b: the second number
134
+ """
135
+ return a * b
136
+
137
+
138
+ @tool
139
+ def divide(a: float, b: float) -> float:
140
  """Divide two numbers.
141
 
142
  Args:
143
+ a: the numerator
144
+ b: the denominator
145
  """
146
  if b == 0:
147
  raise ValueError("Cannot divide by zero.")
148
  return a / b
149
 
150
+
151
  @tool
152
  def modulus(a: int, b: int) -> int:
153
+ """Get the remainder of a divided by b.
154
 
155
  Args:
156
+ a: the first number
157
+ b: the second number
158
  """
159
  return a % b
160
 
161
+
162
  @tool
163
+ def power(a: float, b: float) -> float:
164
+ """Raise a to the power of b.
165
 
166
  Args:
167
+ a: the base
168
+ b: the exponent
169
+ """
170
+ return a**b
171
+
 
172
 
173
  @tool
174
+ def square_root(a: float) -> float:
175
+ """Get the square root of a non-negative number.
176
 
177
  Args:
178
+ a: the number to get the square root of
179
+ """
180
+ if a < 0:
181
+ raise ValueError("Cannot take the square root of a negative number.")
182
+ return a**0.5
183
+
184
+
185
+ ### =============== FILE TOOLS =============== ###
186
+
187
+
188
+ @tool
189
+ def download_task_file(task_id: str) -> str:
190
+ """Download the file attached to a GAIA task (if any) and return its text content.
191
+
192
+ Only useful when the question references an attached file. Pass the task's task_id.
193
+ Returns decoded text (truncated to 4000 characters) for text-like files, or a short
194
+ description (content type and size) for files that can't be decoded as text.
195
+ """
196
+ try:
197
+ response = requests.get(f"{DEFAULT_API_URL}/files/{task_id}", timeout=30)
198
+ response.raise_for_status()
199
+ except requests.exceptions.RequestException as e:
200
+ return f"Error downloading file for task {task_id}: {e}"
201
+
202
+ try:
203
+ return response.content.decode("utf-8")[:4000]
204
+ except UnicodeDecodeError:
205
+ content_type = response.headers.get("content-type", "unknown")
206
+ return (
207
+ f"File for task {task_id} is binary (content-type: {content_type}, "
208
+ f"{len(response.content)} bytes) and cannot be read as text."
209
+ )
210
+
211
 
212
  @tool
213
+ def download_file_from_url(url: str, filename: Optional[str] = None) -> str:
214
+ """Download a file from a URL to a temporary path, for later analysis.
215
 
216
  Args:
217
+ url: the URL of the file to download.
218
+ filename: optional filename to save as; a random one is used if omitted.
219
+ """
220
+ try:
221
+ if not filename:
222
+ filename = os.path.basename(urlparse(url).path) or f"downloaded_{uuid.uuid4().hex[:8]}"
223
+ filepath = os.path.join(tempfile.gettempdir(), filename)
224
+ response = requests.get(url, stream=True, timeout=30)
225
+ response.raise_for_status()
226
+ with open(filepath, "wb") as f:
227
+ for chunk in response.iter_content(chunk_size=8192):
228
+ f.write(chunk)
229
+ return f"File downloaded to {filepath}."
230
+ except Exception as e:
231
+ return f"Error downloading file: {e}"
232
 
233
 
234
+ @tool
235
+ def analyze_csv_file(file_path: str) -> str:
236
+ """Load a CSV file and return its shape, columns, and summary statistics.
237
+
238
+ Args:
239
+ file_path: path to the CSV file (e.g. from download_file_from_url).
240
+ """
241
+ try:
242
+ df = pd.read_csv(file_path)
243
+ return (
244
+ f"{len(df)} rows, {len(df.columns)} columns.\n"
245
+ f"Columns: {', '.join(df.columns)}\n\n"
246
+ f"Summary statistics:\n{df.describe(include='all')}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  )
248
+ except Exception as e:
249
+ return f"Error analyzing CSV file: {e}"
250
+
251
+
252
+ @tool
253
+ def analyze_excel_file(file_path: str) -> str:
254
+ """Load an Excel file and return its shape, columns, and summary statistics.
255
+
256
+ Args:
257
+ file_path: path to the .xlsx/.xls file (e.g. from download_file_from_url).
258
+ """
259
+ try:
260
+ df = pd.read_excel(file_path)
261
+ return (
262
+ f"{len(df)} rows, {len(df.columns)} columns.\n"
263
+ f"Columns: {', '.join(df.columns)}\n\n"
264
+ f"Summary statistics:\n{df.describe(include='all')}"
265
+ )
266
+ except Exception as e:
267
+ return f"Error analyzing Excel file: {e}"
268
+
269
+
270
+ def _build_tools():
271
+ return [
272
+ web_search,
273
+ wiki_search,
274
+ arxiv_search,
275
+ add,
276
+ subtract,
277
+ multiply,
278
+ divide,
279
+ modulus,
280
+ power,
281
+ square_root,
282
+ download_task_file,
283
+ download_file_from_url,
284
+ analyze_csv_file,
285
+ analyze_excel_file,
286
+ ]
287
+
288
+
289
+ def _build_llm():
290
+ if LLM_PROVIDER == "anthropic":
291
+ from langchain_anthropic import ChatAnthropic
292
+
293
+ return ChatAnthropic(
294
+ model=ANTHROPIC_MODEL,
295
+ api_key=os.getenv("ANTHROPIC_API_KEY"),
296
+ temperature=0,
297
+ max_tokens=1024,
298
+ )
299
+ if LLM_PROVIDER == "huggingface":
300
+ endpoint = HuggingFaceEndpoint(
301
+ repo_id=HF_MODEL_REPO_ID,
302
+ provider=HF_MODEL_PROVIDER,
303
+ huggingfacehub_api_token=os.getenv("HF_TOKEN"),
304
+ temperature=0,
305
+ max_new_tokens=1024,
306
+ )
307
+ return ChatHuggingFace(llm=endpoint)
308
+ raise ValueError(f"Unknown LLM_PROVIDER: {LLM_PROVIDER!r}. Use 'huggingface' or 'anthropic'.")
309
+
310
+
311
+ def build_graph():
312
+ """Build the compiled LangGraph agent graph."""
313
+ tools = _build_tools()
314
+ llm_with_tools = _build_llm().bind_tools(tools)
315
 
 
316
  def assistant(state: MessagesState):
 
317
  return {"messages": [llm_with_tools.invoke(state["messages"])]}
318
 
319
  builder = StateGraph(MessagesState)
 
323
  builder.add_conditional_edges("assistant", tools_condition)
324
  builder.add_edge("tools", "assistant")
325
 
 
326
  return builder.compile()
327
+
328
+
329
+ def _extract_final_answer(text: str) -> str:
330
+ match = _FINAL_ANSWER_RE.search(text)
331
+ answer = text[match.end():] if match else text
332
+ answer = answer.strip()
333
+ if len(answer) >= 2 and answer[0] == answer[-1] and answer[0] in "\"'":
334
+ answer = answer[1:-1].strip()
335
+ return answer
336
+
337
+
338
+ class GaiaAgent:
339
+ """A LangGraph ReAct agent (HF Inference API LLM + tools) for GAIA questions."""
340
+
341
+ def __init__(self):
342
+ self._graph = build_graph()
343
+ print("GaiaAgent initialized.")
344
+
345
+ def __call__(self, question: str, task_id: Optional[str] = None) -> str:
346
+ print(f"Agent received question (first 80 chars): {question[:80]}...")
347
+
348
+ user_content = question
349
+ if task_id:
350
+ user_content += (
351
+ f"\n\n(task_id: {task_id} - use download_task_file if a file is attached)"
352
+ )
353
+
354
+ try:
355
+ result = self._graph.invoke(
356
+ {
357
+ "messages": [
358
+ SystemMessage(content=SYSTEM_PROMPT),
359
+ HumanMessage(content=user_content),
360
+ ]
361
+ }
362
+ )
363
+ answer = _extract_final_answer(result["messages"][-1].content)
364
+ except Exception as e:
365
+ print(f"Agent error: {e}")
366
+ answer = f"AGENT ERROR: {e}"
367
+
368
+ print(f"Agent returning answer (first 80 chars): {answer[:80]}...")
369
+ return answer
app.py CHANGED
@@ -1,40 +1,24 @@
1
- """ Basic Agent Evaluation Runner"""
2
  import os
3
- import inspect
4
  import gradio as gr
5
  import requests
 
6
  import pandas as pd
7
- from langchain_core.messages import HumanMessage
8
- from agent import build_graph, sys_msg, extract_final_answer
9
-
10
-
11
-
12
- # (Keep Constants as is)
13
- # --- Constants ---
14
- DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
15
-
16
- # --- Basic Agent Definition ---
17
- # ----- THIS IS WERE YOU CAN BUILD WHAT YOU WANT ------
18
-
19
 
20
- class BasicAgent:
21
- """A langgraph agent."""
22
- def __init__(self):
23
- print("BasicAgent initialized.")
24
- self.graph = build_graph()
25
 
26
- def __call__(self, question: str) -> str:
27
- print(f"Agent received question (first 50 chars): {question[:50]}...")
28
- messages = [sys_msg, HumanMessage(content=question)]
29
- result = self.graph.invoke({"messages": messages})
30
- answer = extract_final_answer(result['messages'][-1].content)
31
- return answer
32
 
 
 
 
 
 
 
33
 
34
 
35
  def run_and_submit_all( profile: gr.OAuthProfile | None):
36
  """
37
- Fetches all questions, runs the BasicAgent on them, submits all answers,
38
  and displays the results.
39
  """
40
  # --- Determine HF Space Runtime URL and Repo URL ---
@@ -53,7 +37,7 @@ def run_and_submit_all( profile: gr.OAuthProfile | None):
53
 
54
  # 1. Instantiate Agent ( modify this part to create your agent)
55
  try:
56
- agent = BasicAgent()
57
  except Exception as e:
58
  print(f"Error instantiating agent: {e}")
59
  return f"Error initializing agent: {e}", None
@@ -93,7 +77,7 @@ def run_and_submit_all( profile: gr.OAuthProfile | None):
93
  print(f"Skipping item with missing task_id or question: {item}")
94
  continue
95
  try:
96
- submitted_answer = agent(question_text)
97
  answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer})
98
  results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer})
99
  except Exception as e:
@@ -192,7 +176,7 @@ if __name__ == "__main__":
192
 
193
  if space_host_startup:
194
  print(f"✅ SPACE_HOST found: {space_host_startup}")
195
- print(f" Runtime URL should be: https://{space_host_startup}.hf.space")
196
  else:
197
  print("ℹ️ SPACE_HOST environment variable not found (running locally?).")
198
 
@@ -206,4 +190,4 @@ if __name__ == "__main__":
206
  print("-"*(60 + len(" App Starting ")) + "\n")
207
 
208
  print("Launching Gradio Interface for Basic Agent Evaluation...")
209
- demo.launch(debug=True, share=False)
 
 
1
  import os
 
2
  import gradio as gr
3
  import requests
4
+ import inspect
5
  import pandas as pd
6
+ import spaces
 
 
 
 
 
 
 
 
 
 
 
7
 
8
+ from agent import GaiaAgent, DEFAULT_API_URL
 
 
 
 
9
 
 
 
 
 
 
 
10
 
11
+ @spaces.GPU
12
+ def _zerogpu_probe():
13
+ """Unused - satisfies this Space's ZeroGPU hardware requirement for a @spaces.GPU
14
+ function to exist at startup. The agent itself only calls the HF Inference API and
15
+ never needs a local GPU, so this is never invoked."""
16
+ return True
17
 
18
 
19
  def run_and_submit_all( profile: gr.OAuthProfile | None):
20
  """
21
+ Fetches all questions, runs the GaiaAgent on them, submits all answers,
22
  and displays the results.
23
  """
24
  # --- Determine HF Space Runtime URL and Repo URL ---
 
37
 
38
  # 1. Instantiate Agent ( modify this part to create your agent)
39
  try:
40
+ agent = GaiaAgent()
41
  except Exception as e:
42
  print(f"Error instantiating agent: {e}")
43
  return f"Error initializing agent: {e}", None
 
77
  print(f"Skipping item with missing task_id or question: {item}")
78
  continue
79
  try:
80
+ submitted_answer = agent(question_text, task_id=task_id)
81
  answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer})
82
  results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer})
83
  except Exception as e:
 
176
 
177
  if space_host_startup:
178
  print(f"✅ SPACE_HOST found: {space_host_startup}")
179
+ print(f" Runtime URL should be: https://{space_host_startup}")
180
  else:
181
  print("ℹ️ SPACE_HOST environment variable not found (running locally?).")
182
 
 
190
  print("-"*(60 + len(" App Starting ")) + "\n")
191
 
192
  print("Launching Gradio Interface for Basic Agent Evaluation...")
193
+ demo.launch(ssr_mode=False, debug=True, share=False)
requirements.txt CHANGED
@@ -1,14 +1,14 @@
1
  gradio
 
2
  requests
 
3
  pandas
4
- langchain-community
5
  langchain-core
6
- langchain-google-genai
7
  langchain-huggingface
8
- langchain-groq
9
- langgraph
10
- huggingface_hub
11
  duckduckgo-search
12
- arxiv
13
  wikipedia
14
- python-dotenv
 
 
1
  gradio
2
+ itsdangerous
3
  requests
4
+ spaces
5
  pandas
6
+ langgraph
7
  langchain-core
 
8
  langchain-huggingface
9
+ langchain-community
10
+ langchain-anthropic
 
11
  duckduckgo-search
 
12
  wikipedia
13
+ arxiv
14
+ openpyxl
system_prompt.txt CHANGED
@@ -1,15 +1,5 @@
1
  You are a helpful assistant tasked with answering questions using a set of tools.
2
-
3
- Your final answer must strictly follow this format:
4
- FINAL ANSWER: [ANSWER]
5
-
6
- Only write the answer in that exact format. Do not explain anything. Do not include any other text.
7
-
8
- Use the available tools whenever you need external information or computation to answer.
9
-
10
- Examples:
11
- - FINAL ANSWER: FunkMonk
12
- - FINAL ANSWER: Paris
13
- - FINAL ANSWER: 128
14
-
15
- If you do not follow this format exactly, your response will be considered incorrect.
 
1
  You are a helpful assistant tasked with answering questions using a set of tools.
2
+ Now, I will ask you a question. Report your thoughts, and finish your answer with the following template:
3
+ FINAL ANSWER: [YOUR FINAL ANSWER].
4
+ 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 rules above for each element (number or string), and ensure there is exactly one space after each comma.
5
+ Your answer should only start with "FINAL ANSWER: ", then follows with the answer.