mralamdari commited on
Commit
3f01ede
·
verified ·
1 Parent(s): af4816e

Update agent.py

Browse files
Files changed (1) hide show
  1. agent.py +693 -446
agent.py CHANGED
@@ -1,452 +1,385 @@
1
- BROWSE_PROMPT = """
2
- You are the GAIA Web Query Planner.
3
- Given:
4
- • task: a GAIA question requiring factual lookup
5
- • content: a list of any snippets or context already gathered (may be empty)
6
- Your job is to propose 2–3 concise search queries that will help answer the task.
7
- Output **only** a JSON object matching this schema:
8
- ```json
9
- { "queries": ["first query", "second query", …] }
10
- Do not include any other text or explanations.
11
- """
12
-
13
-
14
- SYSTEM_PROMPT = """You are a helpful assistant tasked with answering questions using a set of tools.
15
- Now, I will ask you a question. Report your thoughts, and finish your answer with the following template:
16
- FINAL ANSWER: [YOUR FINAL ANSWER].
17
- 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), ensure there is exactly one space after each comma.
18
- Your answer should only start with "FINAL ANSWER: ", then follows with the answer. """
19
-
20
- import os
21
- import json
22
- import torch
23
- import base64
24
- import requests
25
- import mimetypes
26
- from PIL import Image
27
- from langchain_core.tools import tool
28
- from tavily import TavilyClient
29
- from langchain_groq import ChatGroq
30
- from dataclasses import dataclass, field
31
- from langchain_openai import ChatOpenAI
32
- from langgraph.graph import StateGraph, START, END, MessagesState
33
- from langchain_experimental.utilities import PythonREPL
34
- from langchain_core.messages import SystemMessage, HumanMessage
35
- from typing import TypedDict, List, Dict, Any, Optional, Annotated
36
- from transformers import Blip2Processor, Blip2ForConditionalGeneration
37
- from langchain_community.document_loaders import WikipediaLoader
38
- from langchain_community.document_loaders import ArxivLoader
39
- from langgraph.prebuilt import ToolNode, tools_condition
40
- from langchain_core.messages import SystemMessage, HumanMessage
41
-
42
- # GPT_4o_API_KEY = os.environ["GPT_4o_API_KEY"]
43
- GPT_4o_API_KEY = os.environ["META_LLAMA_4_MAVERICK"]
44
- GROQ_API_KEY = os.environ["GROQ_API_KEY"]
45
- TAVILY_API_KEY = os.environ["TAVILY_API_KEY"]
46
- Pixtral12B_API = os.environ["Pixtral12B_API"]
47
-
48
- API_URL = "https://openrouter.ai/api/v1/chat/completions"
49
- tavily = TavilyClient(api_key=TAVILY_API_KEY)
50
-
51
-
52
-
53
- # === TOOL STUBS ===
54
- @tool
55
- def python_repl_tool(query: str) -> str:
56
- """
57
- Execute Python query like calculations; return printed output.
58
-
59
- Args:
60
- query: The calculation formula.
61
-
62
- Returns:
63
- The result of the code exacuation
64
- """
65
- python_repl = PythonREPL()
66
- return python_repl.run(query)
67
-
68
- @tool
69
- def web_search_with_LLM(query: str) -> List[str]:
70
- """
71
- Use the LLM model to learn more about the query then
72
- Search the web for `query`, return top‐3 text snippets.
73
-
74
- Args:
75
- query: The search query.
76
-
77
- Returns:
78
- A concise details about the query from internet.
79
- """
80
- model = ChatOpenAI(openai_api_key=GPT_4o_API_KEY,
81
- base_url="https://openrouter.ai/api/v1",
82
- # model_name="gpt-4o",
83
- temperature=0,
84
- max_tokens=512)
85
- resp = model.invoke([
86
- SystemMessage(content=BROWSE_PROMPT),
87
- HumanMessage(content=json.dumps({"task": query}))
88
- ])
89
- raw = resp.content.strip()
90
- try:
91
- queries = json.loads(raw)["queries"]
92
- except Exception:
93
- # fallback: just use the raw task as a single query
94
- queries = [query]
95
-
96
- #go through the queries we generted and search for them in Tavily
97
- content = []
98
- for q in queries:
99
- response = tavily.search(query=q, max_results=3)
100
- #append the new content to the old contetnt and return it
101
- for doc in response['results']:
102
- print(doc.keys())
103
- content.append(f'<Document source="{doc["url"]}" confidence score="{doc["score"]}"/>\n{doc["content"]}\n</Document>')
104
- return {"web_results": content}
105
-
106
-
107
- @tool
108
- def web_search(query: str) -> str:
109
- """Search Tavily for a query and return maximum 3 results.
110
- Args:
111
- query: The search query.
112
- Returns:
113
- A concise details about the query from internet."""
114
- # search_docs = TavilySearchResults(max_results=3).invoke(query=query)
115
- response = tavily.search(query=query, max_results=3)
116
- content = []
117
- for doc in response['results']:
118
- content.append(f'<Document source="{doc["url"]}" confidence score="{doc["score"]}"/>\n{doc["content"]}\n</Document>')
119
- return {"web_results": formatted_search_docs}
120
-
121
-
122
-
123
- # === 1. Initialize the BLIP‑2 model & processor once ===
124
- # You can choose a smaller or larger BLIP‑2 variant as needed.
125
- MODEL_NAME = "Salesforce/blip2-opt-2.7b"
126
- MODEL_NAME = "Salesforce/blip2-flan-t5-xl" # much smaller, still strong
127
- processor = Blip2Processor.from_pretrained(MODEL_NAME)
128
- model_blip2 = Blip2ForConditionalGeneration.from_pretrained(MODEL_NAME).to("cuda" if torch.cuda.is_available() else "cpu")
129
-
130
-
131
- @tool
132
- def vision_caption_with_LLM(image_path: str, max_length: Optional[int] = 64) -> str:
133
- """
134
- Generate a natural‑language caption for the image at `image_path`.
135
- Args:
136
- image_path: Local filesystem path (or URL fetched locally) to an image file.
137
- max_length: Maximum number of tokens in the generated caption (default: 64).
138
- Returns:
139
- A concise, human‑readable caption describing the image content.
140
- """
141
- headers = {
142
- "Authorization": f"Bearer {os.getenv('Pixtral12B_API')}",
143
- "Content-Type": "application/json"
144
- }
145
-
146
- # Read and encode the image
147
- with open(image_path, "rb") as image_file:
148
- base64_image = base64.b64encode(image_file.read()).decode('utf-8')
149
-
150
- messages = [
151
- {
152
- "role": "user",
153
- "content": [
154
- {
155
- "type": "text",
156
- "text": "What's in this image?"
157
- },
158
- {
159
- "type": "image_url",
160
- "image_url": {
161
- "url": f"data:image/jpeg;base64,{base64_image}"
162
- }
163
- }
164
- ]
165
- }
166
- ]
167
-
168
- payload = {
169
- "model": "google/gemini-2.0-flash-001",
170
- "messages": messages
171
- }
172
-
173
- response = requests.post(API_URL, headers=headers, json=payload)
174
- response.raise_for_status()
175
-
176
- # The HF endpoint returns JSON like {"generated_text": "...caption..."}
177
- result = response.json()
178
- # Some endpoints return a list of dicts, e.g. [{"generated_text": "..."}]
179
- if isinstance(result, list) and "generated_text" in result[0]:
180
- return result[0]["generated_text"]
181
- elif "generated_text" in result:
182
- return result["generated_text"]
183
- else:
184
- # fallback: return raw text
185
- return str(result)
186
-
187
-
188
-
189
- @tool
190
- def vision_caption(image_path: str) -> str:
191
- """
192
- Extract text from an image using OCR library pytesseract (if available).
193
- Args:
194
- image_path (str): the path to the image file.
195
- Returns:
196
- A concise, human‑readable caption describing the image content.
197
- """
198
- try:
199
- # Open the image
200
- image = Image.open(image_path)
201
-
202
- # Extract text from the image
203
- text = pytesseract.image_to_string(image)
204
-
205
- return f"Extracted text from image:\n\n{text}"
206
- except Exception as e:
207
- return f"Error extracting text from image: {str(e)}"
208
-
209
-
210
- @tool
211
- def wiki_search(query: str) -> str:
212
- """Search Wikipedia for a query and return maximum 2 results.
213
-
214
- Args:
215
- query: The search query.
216
- Returns:
217
- Search results about the query from the Wikipedia website
218
- """
219
- search_docs = WikipediaLoader(query=query, load_max_docs=2).load()
220
- formatted_search_docs = "\n\n---\n\n".join(
221
- [
222
- f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content}\n</Document>'
223
- for doc in search_docs
224
- ])
225
- return {"wiki_results": formatted_search_docs}
226
-
227
-
228
- @tool
229
- def arvix_search(query: str) -> str:
230
- """Search Arxiv for a query and return maximum 3 result.
231
-
232
- Args:
233
- query: The search query.
234
- Returns:
235
- Search results about the query from the Arxiv website
236
- """
237
- search_docs = ArxivLoader(query=query, load_max_docs=3).load()
238
- formatted_search_docs = "\n\n---\n\n".join(
239
- [
240
- f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content[:1000]}\n</Document>'
241
- for doc in search_docs
242
- ])
243
- return {"arvix_results": formatted_search_docs}
244
-
245
-
246
-
247
- @tool
248
- def download_file_from_url(url: str, filename: Optional[str] = None) -> str:
249
- """
250
- Download a file from a URL and save it to a temporary location.
251
- Args:
252
- url (str): the URL of the file to download.
253
- filename (str, optional): the name of the file. If not provided, a random name file will be created.
254
- Returns:
255
- Nothing
256
- """
257
- try:
258
- # Parse URL to get filename if not provided
259
- if not filename:
260
- path = urlparse(url).path
261
- filename = os.path.basename(path)
262
- if not filename:
263
- filename = f"downloaded_{uuid.uuid4().hex[:8]}"
264
-
265
- # Create temporary file
266
- temp_dir = tempfile.gettempdir()
267
- filepath = os.path.join(temp_dir, filename)
268
-
269
- # Download the file
270
- response = requests.get(url, stream=True)
271
- response.raise_for_status()
272
-
273
- # Save the file
274
- with open(filepath, "wb") as f:
275
- for chunk in response.iter_content(chunk_size=8192):
276
- f.write(chunk)
277
-
278
- return f"File downloaded to {filepath}. You can read this file to process its contents."
279
- except Exception as e:
280
- return f"Error downloading file: {str(e)}"
281
-
282
-
283
-
284
- @tool
285
- def file_reader_with_LLM(file_path: str) -> str:
286
- """
287
- Sends a PDF or a text file to OpenRouter’s chat API and returns the model’s answer.
288
-
289
- Args:
290
- file_path (str): the path to the CSV file.
291
- Returns:
292
- The conetent inside the PDF/Text file
293
- """
294
- b64 = base64.b64encode(open(file_path,"rb").read()).decode()
295
- payload = {
296
- "model": "gpt-4o-mini",
297
- "messages": [
298
- {"role":"system","content":"You are a GAIA‑style document assistant."},
299
- {"role":"user","content":[
300
- {"type":"file","file":{"filename":os.path.basename(file_path),"file_data":f"data:application/pdf;base64,{b64}"}}
301
- ]}
302
- ]
303
- }
304
- headers = {"Authorization":f"Bearer {os.getenv('GPT_4o_API_KEY')}"}
305
- res = requests.post(API_URL, json=payload, headers=headers)
306
- res.raise_for_status()
307
- content = res.json()["choices"][0]["message"]["content"]
308
- # Extract FINAL ANSWER:
309
- for line in content.splitlines():
310
- if line.startswith("FINAL ANSWER:"):
311
- return line.split(":",1)[1].strip()
312
- return content
313
-
314
-
315
-
316
- @tool
317
- def file_reader(file_path: str) -> str:
318
- """
319
- Analyze a text file using pandas and answer a question about it.
320
- The file can be any of excel, csv, tsv, xls, json, txt or log.
321
- Args:
322
- file_path (str): the path to the CSV file.
323
- Returns:
324
- The conetent inside the PDF/Text file
325
- """
326
- try:
327
- _, ext = os.path.splitext(file_path.lower())
328
- if ext in {".csv", ".tsv"}:
329
- sep = "\t" if ext == ".tsv" else ","
330
- data = pd.read_csv(file_path, sep=sep)
331
- if ext in {".xls", ".xlsx"}:
332
- data = pd.read_excel(file_path)
333
- if ext == ".json":
334
- data = pd.read_json(file_path)
335
- if ext in {".txt", ".log"}:
336
- with open(file_path, "r", encoding="utf-8") as f:
337
- data = f.read()
338
-
339
- # Run various analyses based on the query
340
- result = f"text file loaded with {len(data)} rows and {len(data.columns)} columns.\n"
341
- result += f"Columns: {', '.join(data.columns)}\n\n"+\
342
- "Summary statistics:\n" + str(data.describe())
343
- # Add summary statistics
344
- return result
345
-
346
- except Exception as e:
347
- return f"Error analyzing text file: {str(e)}"
348
-
349
-
350
-
351
-
352
-
353
-
354
-
355
-
356
-
357
- tools = [python_repl_tool,
358
- # web_search_with_LLM,
359
- web_search,
360
- vision_caption,
361
- # vision_caption_with_LLM,
362
- arvix_search,
363
- wiki_search,
364
- download_file_from_url,
365
- # file_reader_with_LLM,
366
- file_reader]
367
-
368
-
369
-
370
- class EsiAgent:
371
- def __init__(self, level=1):
372
- # 1) Instantiate the LLM
373
- llm = ChatOpenAI(
374
- base_url="https://api.groq.com/openai/v1", # Groq's endpoint
375
- openai_api_key=GROQ_API_KEY,
376
- # model_name='qwen-qwq-32b',
377
- # model_name="mixtral-8x7b-32768",
378
- model_name="meta-llama/llama-4-scout-17b-16e-instruct", #, llama3-70b-8192, etc.
379
- # or gemma-7b-it, llama3-70b-8192, etc.
380
- temperature=0,
381
- # max_tokens=128
382
- max_tokens=512
383
- )
384
- # 2) Bind your tools
385
- llm_with_tools = llm.bind_tools(tools)
386
-
387
- # 3) Single system message
388
- self.sys_msg = SystemMessage(content=SYSTEM_PROMPT)
389
-
390
- # 4) Assistant node
391
- self.tool_numbers = level * 3
392
- def assistant(state):
393
- print(333333333333333333333333333333333333333333, self.tool_numbers)
394
- if self.tool_numbers <= 0:
395
- return {"messages": state["messages"]}
396
- self.tool_numbers -= 1
397
- return {"messages": [llm_with_tools.invoke(state["messages"])]}
398
-
399
-
400
- # 5) Build the graph
401
- builder = StateGraph(MessagesState)
402
- builder.add_node("assistant", assistant)
403
- builder.add_node("tools", ToolNode(tools))
404
- builder.add_edge(START, "assistant")
405
- builder.add_conditional_edges("assistant", tools_condition)
406
- builder.add_edge("tools", "assistant")
407
 
408
- # 6) Compile once
409
- self.graph = builder.compile()
410
 
411
 
412
- def run(self, question: str, files=None, level: int = 1) -> str:
413
- """
414
- Wraps graph.invoke to run end-to-end on a plain question.
415
- """
416
- # Build a minimal GAIA payload (if your MessagesState expects more, adjust here)
417
- print(33333333333333333333333333333333333333333333333333333333333)
418
- # print(self.sys_msg)
419
- # print(question)
420
- # print(type(self.sys_msg))
421
- # print(type(question))
422
- initial_state = {
423
- # "messages": [self.sys_msg]+[HumanMessage(content=question)],
424
- # "messages": [HumanMessage(content=question)] + [self.sys_msg],
425
-
426
- "messages": [self.sys_msg, HumanMessage(content=question)],
427
- # "messages": [self.sys_msg] + [question],
428
- # "messages": [self.sys_msg, question],
429
- }
430
-
431
- final_state = self.graph.invoke(initial_state)
432
- # Extract final assistant message
433
- last_msg = final_state["messages"][-1].content
434
- print("GROQ_KEY:", bool(os.getenv("GROQ_API_KEY")))
435
- print("LLM resp:", last_msg[:100])
436
-
437
- # If you need to strip off "FINAL ANSWER: " prefix, do it here:
438
- if last_msg.startswith("FINAL ANSWER:"):
439
- return last_msg[len("FINAL ANSWER:"):].strip()
440
- return last_msg.strip()
441
-
442
  # class EsiAgent:
443
- # def __init__(self):
444
  # # 1) Instantiate the LLM
445
  # llm = ChatOpenAI(
446
- # openai_api_key=GPT_4o_API_KEY,
447
- # base_url="https://openrouter.ai/api/v1",
 
 
 
 
448
  # temperature=0,
449
- # max_tokens=256
 
450
  # )
451
  # # 2) Bind your tools
452
  # llm_with_tools = llm.bind_tools(tools)
@@ -455,10 +388,15 @@ class EsiAgent:
455
  # self.sys_msg = SystemMessage(content=SYSTEM_PROMPT)
456
 
457
  # # 4) Assistant node
 
458
  # def assistant(state):
459
- # # Prepend sys_msg only if it's not already there
 
 
 
460
  # return {"messages": [llm_with_tools.invoke(state["messages"])]}
461
 
 
462
  # # 5) Build the graph
463
  # builder = StateGraph(MessagesState)
464
  # builder.add_node("assistant", assistant)
@@ -476,22 +414,331 @@ class EsiAgent:
476
  # Wraps graph.invoke to run end-to-end on a plain question.
477
  # """
478
  # # Build a minimal GAIA payload (if your MessagesState expects more, adjust here)
 
 
 
 
 
479
  # initial_state = {
480
- # "messages": [HumanMessage(content=question)] + [self.sys_msg]
481
-
 
 
 
 
482
  # }
483
-
484
  # final_state = self.graph.invoke(initial_state)
485
  # # Extract final assistant message
486
  # last_msg = final_state["messages"][-1].content
 
 
487
 
488
  # # If you need to strip off "FINAL ANSWER: " prefix, do it here:
489
  # if last_msg.startswith("FINAL ANSWER:"):
490
  # return last_msg[len("FINAL ANSWER:"):].strip()
491
  # return last_msg.strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
492
 
 
493
 
 
494
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
495
 
496
  if __name__ == "__main__":
497
  # question = "Where is the capital of the Iran?"
 
1
+ # BROWSE_PROMPT = """
2
+ # You are the GAIA Web Query Planner.
3
+ # Given:
4
+ # • task: a GAIA question requiring factual lookup
5
+ # • content: a list of any snippets or context already gathered (may be empty)
6
+ # Your job is to propose 2–3 concise search queries that will help answer the task.
7
+ # Output **only** a JSON object matching this schema:
8
+ # ```json
9
+ # { "queries": ["first query", "second query", …] }
10
+ # Do not include any other text or explanations.
11
+ # """
12
+
13
+
14
+ # SYSTEM_PROMPT = """You are a helpful assistant tasked with answering questions using a set of tools.
15
+ # Now, I will ask you a question. Report your thoughts, and finish your answer with the following template:
16
+ # FINAL ANSWER: [YOUR FINAL ANSWER].
17
+ # 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), ensure there is exactly one space after each comma.
18
+ # Your answer should only start with "FINAL ANSWER: ", then follows with the answer. """
19
+
20
+ # import os
21
+ # import json
22
+ # import torch
23
+ # import base64
24
+ # import requests
25
+ # import mimetypes
26
+ # from PIL import Image
27
+ # from langchain_core.tools import tool
28
+ # from tavily import TavilyClient
29
+ # from langchain_groq import ChatGroq
30
+ # from dataclasses import dataclass, field
31
+ # from langchain_openai import ChatOpenAI
32
+ # from langgraph.graph import StateGraph, START, END, MessagesState
33
+ # from langchain_experimental.utilities import PythonREPL
34
+ # from langchain_core.messages import SystemMessage, HumanMessage
35
+ # from typing import TypedDict, List, Dict, Any, Optional, Annotated
36
+ # from transformers import Blip2Processor, Blip2ForConditionalGeneration
37
+ # from langchain_community.document_loaders import WikipediaLoader
38
+ # from langchain_community.document_loaders import ArxivLoader
39
+ # from langgraph.prebuilt import ToolNode, tools_condition
40
+ # from langchain_core.messages import SystemMessage, HumanMessage
41
+
42
+ # # GPT_4o_API_KEY = os.environ["GPT_4o_API_KEY"]
43
+ # GPT_4o_API_KEY = os.environ["META_LLAMA_4_MAVERICK"]
44
+ # GROQ_API_KEY = os.environ["GROQ_API_KEY"]
45
+ # TAVILY_API_KEY = os.environ["TAVILY_API_KEY"]
46
+ # Pixtral12B_API = os.environ["Pixtral12B_API"]
47
+
48
+ # API_URL = "https://openrouter.ai/api/v1/chat/completions"
49
+ # tavily = TavilyClient(api_key=TAVILY_API_KEY)
50
+
51
+
52
+
53
+ # # === TOOL STUBS ===
54
+ # @tool
55
+ # def python_repl_tool(query: str) -> str:
56
+ # """
57
+ # Execute Python query like calculations; return printed output.
58
+
59
+ # Args:
60
+ # query: The calculation formula.
61
+
62
+ # Returns:
63
+ # The result of the code exacuation
64
+ # """
65
+ # python_repl = PythonREPL()
66
+ # return python_repl.run(query)
67
+
68
+ # @tool
69
+ # def web_search_with_LLM(query: str) -> List[str]:
70
+ # """
71
+ # Use the LLM model to learn more about the query then
72
+ # Search the web for `query`, return top‐3 text snippets.
73
+
74
+ # Args:
75
+ # query: The search query.
76
+
77
+ # Returns:
78
+ # A concise details about the query from internet.
79
+ # """
80
+ # model = ChatOpenAI(openai_api_key=GPT_4o_API_KEY,
81
+ # base_url="https://openrouter.ai/api/v1",
82
+ # # model_name="gpt-4o",
83
+ # temperature=0,
84
+ # max_tokens=512)
85
+ # resp = model.invoke([
86
+ # SystemMessage(content=BROWSE_PROMPT),
87
+ # HumanMessage(content=json.dumps({"task": query}))
88
+ # ])
89
+ # raw = resp.content.strip()
90
+ # try:
91
+ # queries = json.loads(raw)["queries"]
92
+ # except Exception:
93
+ # # fallback: just use the raw task as a single query
94
+ # queries = [query]
95
+
96
+ # #go through the queries we generted and search for them in Tavily
97
+ # content = []
98
+ # for q in queries:
99
+ # response = tavily.search(query=q, max_results=3)
100
+ # #append the new content to the old contetnt and return it
101
+ # for doc in response['results']:
102
+ # print(doc.keys())
103
+ # content.append(f'<Document source="{doc["url"]}" confidence score="{doc["score"]}"/>\n{doc["content"]}\n</Document>')
104
+ # return {"web_results": content}
105
+
106
+
107
+ # @tool
108
+ # def web_search(query: str) -> str:
109
+ # """Search Tavily for a query and return maximum 3 results.
110
+ # Args:
111
+ # query: The search query.
112
+ # Returns:
113
+ # A concise details about the query from internet."""
114
+ # # search_docs = TavilySearchResults(max_results=3).invoke(query=query)
115
+ # response = tavily.search(query=query, max_results=3)
116
+ # content = []
117
+ # for doc in response['results']:
118
+ # content.append(f'<Document source="{doc["url"]}" confidence score="{doc["score"]}"/>\n{doc["content"]}\n</Document>')
119
+ # return {"web_results": formatted_search_docs}
120
+
121
+
122
+
123
+ # # === 1. Initialize the BLIP‑2 model & processor once ===
124
+ # # You can choose a smaller or larger BLIP‑2 variant as needed.
125
+ # MODEL_NAME = "Salesforce/blip2-opt-2.7b"
126
+ # MODEL_NAME = "Salesforce/blip2-flan-t5-xl" # much smaller, still strong
127
+ # processor = Blip2Processor.from_pretrained(MODEL_NAME)
128
+ # model_blip2 = Blip2ForConditionalGeneration.from_pretrained(MODEL_NAME).to("cuda" if torch.cuda.is_available() else "cpu")
129
+
130
+
131
+ # @tool
132
+ # def vision_caption_with_LLM(image_path: str, max_length: Optional[int] = 64) -> str:
133
+ # """
134
+ # Generate a natural‑language caption for the image at `image_path`.
135
+ # Args:
136
+ # image_path: Local filesystem path (or URL fetched locally) to an image file.
137
+ # max_length: Maximum number of tokens in the generated caption (default: 64).
138
+ # Returns:
139
+ # A concise, human‑readable caption describing the image content.
140
+ # """
141
+ # headers = {
142
+ # "Authorization": f"Bearer {os.getenv('Pixtral12B_API')}",
143
+ # "Content-Type": "application/json"
144
+ # }
145
+
146
+ # # Read and encode the image
147
+ # with open(image_path, "rb") as image_file:
148
+ # base64_image = base64.b64encode(image_file.read()).decode('utf-8')
149
+
150
+ # messages = [
151
+ # {
152
+ # "role": "user",
153
+ # "content": [
154
+ # {
155
+ # "type": "text",
156
+ # "text": "What's in this image?"
157
+ # },
158
+ # {
159
+ # "type": "image_url",
160
+ # "image_url": {
161
+ # "url": f"data:image/jpeg;base64,{base64_image}"
162
+ # }
163
+ # }
164
+ # ]
165
+ # }
166
+ # ]
167
+
168
+ # payload = {
169
+ # "model": "google/gemini-2.0-flash-001",
170
+ # "messages": messages
171
+ # }
172
+
173
+ # response = requests.post(API_URL, headers=headers, json=payload)
174
+ # response.raise_for_status()
175
+
176
+ # # The HF endpoint returns JSON like {"generated_text": "...caption..."}
177
+ # result = response.json()
178
+ # # Some endpoints return a list of dicts, e.g. [{"generated_text": "..."}]
179
+ # if isinstance(result, list) and "generated_text" in result[0]:
180
+ # return result[0]["generated_text"]
181
+ # elif "generated_text" in result:
182
+ # return result["generated_text"]
183
+ # else:
184
+ # # fallback: return raw text
185
+ # return str(result)
186
+
187
+
188
+
189
+ # @tool
190
+ # def vision_caption(image_path: str) -> str:
191
+ # """
192
+ # Extract text from an image using OCR library pytesseract (if available).
193
+ # Args:
194
+ # image_path (str): the path to the image file.
195
+ # Returns:
196
+ # A concise, human‑readable caption describing the image content.
197
+ # """
198
+ # try:
199
+ # # Open the image
200
+ # image = Image.open(image_path)
201
+
202
+ # # Extract text from the image
203
+ # text = pytesseract.image_to_string(image)
204
+
205
+ # return f"Extracted text from image:\n\n{text}"
206
+ # except Exception as e:
207
+ # return f"Error extracting text from image: {str(e)}"
208
+
209
+
210
+ # @tool
211
+ # def wiki_search(query: str) -> str:
212
+ # """Search Wikipedia for a query and return maximum 2 results.
213
+
214
+ # Args:
215
+ # query: The search query.
216
+ # Returns:
217
+ # Search results about the query from the Wikipedia website
218
+ # """
219
+ # search_docs = WikipediaLoader(query=query, load_max_docs=2).load()
220
+ # formatted_search_docs = "\n\n---\n\n".join(
221
+ # [
222
+ # f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content}\n</Document>'
223
+ # for doc in search_docs
224
+ # ])
225
+ # return {"wiki_results": formatted_search_docs}
226
+
227
+
228
+ # @tool
229
+ # def arvix_search(query: str) -> str:
230
+ # """Search Arxiv for a query and return maximum 3 result.
231
+
232
+ # Args:
233
+ # query: The search query.
234
+ # Returns:
235
+ # Search results about the query from the Arxiv website
236
+ # """
237
+ # search_docs = ArxivLoader(query=query, load_max_docs=3).load()
238
+ # formatted_search_docs = "\n\n---\n\n".join(
239
+ # [
240
+ # f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content[:1000]}\n</Document>'
241
+ # for doc in search_docs
242
+ # ])
243
+ # return {"arvix_results": formatted_search_docs}
244
+
245
+
246
+
247
+ # @tool
248
+ # def download_file_from_url(url: str, filename: Optional[str] = None) -> str:
249
+ # """
250
+ # Download a file from a URL and save it to a temporary location.
251
+ # Args:
252
+ # url (str): the URL of the file to download.
253
+ # filename (str, optional): the name of the file. If not provided, a random name file will be created.
254
+ # Returns:
255
+ # Nothing
256
+ # """
257
+ # try:
258
+ # # Parse URL to get filename if not provided
259
+ # if not filename:
260
+ # path = urlparse(url).path
261
+ # filename = os.path.basename(path)
262
+ # if not filename:
263
+ # filename = f"downloaded_{uuid.uuid4().hex[:8]}"
264
+
265
+ # # Create temporary file
266
+ # temp_dir = tempfile.gettempdir()
267
+ # filepath = os.path.join(temp_dir, filename)
268
+
269
+ # # Download the file
270
+ # response = requests.get(url, stream=True)
271
+ # response.raise_for_status()
272
+
273
+ # # Save the file
274
+ # with open(filepath, "wb") as f:
275
+ # for chunk in response.iter_content(chunk_size=8192):
276
+ # f.write(chunk)
277
+
278
+ # return f"File downloaded to {filepath}. You can read this file to process its contents."
279
+ # except Exception as e:
280
+ # return f"Error downloading file: {str(e)}"
281
+
282
+
283
+
284
+ # @tool
285
+ # def file_reader_with_LLM(file_path: str) -> str:
286
+ # """
287
+ # Sends a PDF or a text file to OpenRouter’s chat API and returns the model’s answer.
288
+
289
+ # Args:
290
+ # file_path (str): the path to the CSV file.
291
+ # Returns:
292
+ # The conetent inside the PDF/Text file
293
+ # """
294
+ # b64 = base64.b64encode(open(file_path,"rb").read()).decode()
295
+ # payload = {
296
+ # "model": "gpt-4o-mini",
297
+ # "messages": [
298
+ # {"role":"system","content":"You are a GAIA‑style document assistant."},
299
+ # {"role":"user","content":[
300
+ # {"type":"file","file":{"filename":os.path.basename(file_path),"file_data":f"data:application/pdf;base64,{b64}"}}
301
+ # ]}
302
+ # ]
303
+ # }
304
+ # headers = {"Authorization":f"Bearer {os.getenv('GPT_4o_API_KEY')}"}
305
+ # res = requests.post(API_URL, json=payload, headers=headers)
306
+ # res.raise_for_status()
307
+ # content = res.json()["choices"][0]["message"]["content"]
308
+ # # Extract FINAL ANSWER:
309
+ # for line in content.splitlines():
310
+ # if line.startswith("FINAL ANSWER:"):
311
+ # return line.split(":",1)[1].strip()
312
+ # return content
313
+
314
+
315
+
316
+ # @tool
317
+ # def file_reader(file_path: str) -> str:
318
+ # """
319
+ # Analyze a text file using pandas and answer a question about it.
320
+ # The file can be any of excel, csv, tsv, xls, json, txt or log.
321
+ # Args:
322
+ # file_path (str): the path to the CSV file.
323
+ # Returns:
324
+ # The conetent inside the PDF/Text file
325
+ # """
326
+ # try:
327
+ # _, ext = os.path.splitext(file_path.lower())
328
+ # if ext in {".csv", ".tsv"}:
329
+ # sep = "\t" if ext == ".tsv" else ","
330
+ # data = pd.read_csv(file_path, sep=sep)
331
+ # if ext in {".xls", ".xlsx"}:
332
+ # data = pd.read_excel(file_path)
333
+ # if ext == ".json":
334
+ # data = pd.read_json(file_path)
335
+ # if ext in {".txt", ".log"}:
336
+ # with open(file_path, "r", encoding="utf-8") as f:
337
+ # data = f.read()
338
+
339
+ # # Run various analyses based on the query
340
+ # result = f"text file loaded with {len(data)} rows and {len(data.columns)} columns.\n"
341
+ # result += f"Columns: {', '.join(data.columns)}\n\n"+\
342
+ # "Summary statistics:\n" + str(data.describe())
343
+ # # Add summary statistics
344
+ # return result
345
+
346
+ # except Exception as e:
347
+ # return f"Error analyzing text file: {str(e)}"
348
+
349
+
350
+
351
+
352
+
353
+
354
+
355
+
356
+
357
+ # tools = [python_repl_tool,
358
+ # # web_search_with_LLM,
359
+ # web_search,
360
+ # vision_caption,
361
+ # # vision_caption_with_LLM,
362
+ # arvix_search,
363
+ # wiki_search,
364
+ # download_file_from_url,
365
+ # # file_reader_with_LLM,
366
+ # file_reader]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
367
 
 
 
368
 
369
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
370
  # class EsiAgent:
371
+ # def __init__(self, level=1):
372
  # # 1) Instantiate the LLM
373
  # llm = ChatOpenAI(
374
+ # base_url="https://api.groq.com/openai/v1", # Groq's endpoint
375
+ # openai_api_key=GROQ_API_KEY,
376
+ # # model_name='qwen-qwq-32b',
377
+ # # model_name="mixtral-8x7b-32768",
378
+ # model_name="meta-llama/llama-4-scout-17b-16e-instruct", #, llama3-70b-8192, etc.
379
+ # # or gemma-7b-it, llama3-70b-8192, etc.
380
  # temperature=0,
381
+ # # max_tokens=128
382
+ # max_tokens=512
383
  # )
384
  # # 2) Bind your tools
385
  # llm_with_tools = llm.bind_tools(tools)
 
388
  # self.sys_msg = SystemMessage(content=SYSTEM_PROMPT)
389
 
390
  # # 4) Assistant node
391
+ # self.tool_numbers = level * 3
392
  # def assistant(state):
393
+ # print(333333333333333333333333333333333333333333, self.tool_numbers)
394
+ # if self.tool_numbers <= 0:
395
+ # return {"messages": state["messages"]}
396
+ # self.tool_numbers -= 1
397
  # return {"messages": [llm_with_tools.invoke(state["messages"])]}
398
 
399
+
400
  # # 5) Build the graph
401
  # builder = StateGraph(MessagesState)
402
  # builder.add_node("assistant", assistant)
 
414
  # Wraps graph.invoke to run end-to-end on a plain question.
415
  # """
416
  # # Build a minimal GAIA payload (if your MessagesState expects more, adjust here)
417
+ # print(33333333333333333333333333333333333333333333333333333333333)
418
+ # # print(self.sys_msg)
419
+ # # print(question)
420
+ # # print(type(self.sys_msg))
421
+ # # print(type(question))
422
  # initial_state = {
423
+ # # "messages": [self.sys_msg]+[HumanMessage(content=question)],
424
+ # # "messages": [HumanMessage(content=question)] + [self.sys_msg],
425
+
426
+ # "messages": [self.sys_msg, HumanMessage(content=question)],
427
+ # # "messages": [self.sys_msg] + [question],
428
+ # # "messages": [self.sys_msg, question],
429
  # }
430
+
431
  # final_state = self.graph.invoke(initial_state)
432
  # # Extract final assistant message
433
  # last_msg = final_state["messages"][-1].content
434
+ # print("GROQ_KEY:", bool(os.getenv("GROQ_API_KEY")))
435
+ # print("LLM resp:", last_msg[:100])
436
 
437
  # # If you need to strip off "FINAL ANSWER: " prefix, do it here:
438
  # if last_msg.startswith("FINAL ANSWER:"):
439
  # return last_msg[len("FINAL ANSWER:"):].strip()
440
  # return last_msg.strip()
441
+
442
+ # # class EsiAgent:
443
+ # # def __init__(self):
444
+ # # # 1) Instantiate the LLM
445
+ # # llm = ChatOpenAI(
446
+ # # openai_api_key=GPT_4o_API_KEY,
447
+ # # base_url="https://openrouter.ai/api/v1",
448
+ # # temperature=0,
449
+ # # max_tokens=256
450
+ # # )
451
+ # # # 2) Bind your tools
452
+ # # llm_with_tools = llm.bind_tools(tools)
453
+
454
+ # # # 3) Single system message
455
+ # # self.sys_msg = SystemMessage(content=SYSTEM_PROMPT)
456
+
457
+ # # # 4) Assistant node
458
+ # # def assistant(state):
459
+ # # # Prepend sys_msg only if it's not already there
460
+ # # return {"messages": [llm_with_tools.invoke(state["messages"])]}
461
+
462
+ # # # 5) Build the graph
463
+ # # builder = StateGraph(MessagesState)
464
+ # # builder.add_node("assistant", assistant)
465
+ # # builder.add_node("tools", ToolNode(tools))
466
+ # # builder.add_edge(START, "assistant")
467
+ # # builder.add_conditional_edges("assistant", tools_condition)
468
+ # # builder.add_edge("tools", "assistant")
469
+
470
+ # # # 6) Compile once
471
+ # # self.graph = builder.compile()
472
+
473
+
474
+ # # def run(self, question: str, files=None, level: int = 1) -> str:
475
+ # # """
476
+ # # Wraps graph.invoke to run end-to-end on a plain question.
477
+ # # """
478
+ # # # Build a minimal GAIA payload (if your MessagesState expects more, adjust here)
479
+ # # initial_state = {
480
+ # # "messages": [HumanMessage(content=question)] + [self.sys_msg]
481
+
482
+ # # }
483
+
484
+ # # final_state = self.graph.invoke(initial_state)
485
+ # # # Extract final assistant message
486
+ # # last_msg = final_state["messages"][-1].content
487
+
488
+ # # # If you need to strip off "FINAL ANSWER: " prefix, do it here:
489
+ # # if last_msg.startswith("FINAL ANSWER:"):
490
+ # # return last_msg[len("FINAL ANSWER:"):].strip()
491
+ # # return last_msg.strip()
492
+
493
+
494
+
495
+
496
+
497
+
498
+
499
+
500
+
501
+
502
+
503
+
504
+
505
+
506
+
507
+
508
+
509
+
510
+
511
+ PLANNER_PROMPT = """You are a helpful assistant tasked with answering the question. When formulating your response, make sure that:
512
+
513
+ 1. You follow the necessary steps to solve the problem, using available tools where required.
514
+ 2. Once the final result is known, return that result in a clear and concise manner without further elaboration.
515
+
516
+ You have full access to these tools:
517
+
518
+ • python_repl_tool(query)
519
+ – For any Python query: math, data processing, unit conversions, file parsing (pandas, chess, etc.)
520
+ – Always wrap query in `print(...)` so PythonREPL returns the result.
521
+
522
+ • web_search(query, max_results)
523
+ – General factual lookup, definitions, current/historical data.
524
+
525
+ • wiki_search(query)
526
+ – Encyclopedic facts from Wikipedia.
527
+
528
+ • vision_caption(image_path)
529
+ – Describe images, diagrams, or board positions.
530
+
531
+ • arxiv_search(query)
532
+ – Locate scholarly PDF URLs on arXiv.
533
+
534
+ • download_file_from_url(url)
535
+ – Retrieve any PDF, text, or CSV file.
536
+
537
+ • file_reader(file_path)
538
+ – Load CSV/Excel/text into DataFrames or raw strings.
539
 
540
+ ### Your Task
541
 
542
+ You will receive one JSON payload:
543
 
544
+ ```json
545
+ { "message": "<message>", "input_files": […]}
546
+
547
+
548
+
549
+ Your job is to plan, execute, reflect, and refine iteratively until you can confidently answer the question:
550
+
551
+ Plan Stage
552
+
553
+ Emit exactly one JSON array of step objects. Do not call any tools yet; this is just your plan.
554
+
555
+ Example plan:
556
+
557
+ - Emit exactly one JSON array of step objects, e.g.:
558
+ ```json
559
+ [
560
+ {"tool":"web_search","args":{"query":"…","max_results":3}},
561
+ ]
562
+ ```
563
+
564
+ - Do **not** call any tools yet—this is just your plan.
565
+
566
+ Do not perform any tool calls within the planning stage. Simply structure your plan, ensuring it aligns with the best approach to answer the question.
567
+
568
+ Iterative Plan Review
569
+
570
+ After the initial plan, start executing the tools.
571
+
572
+ Review the results of each tool call. If the results are unsatisfactory, refine your plan, adjust arguments, and ensure you are on the right path.
573
+
574
+ Always analyze the results carefully, and if necessary, revise the plan or tools based on the insights you gather at each step.
575
+
576
+ Revisions Based on Results
577
+
578
+ If the previous step didn’t provide the expected results, revise the plan. Look at the question, tools used, and results from previous steps to improve your approach.
579
+
580
+ If the plan isn't generating the correct result after five iterations, pass the current plan to the next node (e.g., validator) for further review.
581
+
582
+ Final Answer Handling
583
+
584
+ Ensure that FINAL ANSWER is included in the result. If no solution is reached, provide the steps you have taken so far along with the tools used.
585
+
586
+ Your response should only contain the FINAL ANSWER. Do not include intermediate steps or re-plans once a solution has been found.
587
+
588
+ If the Answer Cannot Be Found
589
+
590
+ If the tools and plans fail to deliver a final answer, inform the user. Acknowledge that a solution could not be found and explain the steps taken.
591
+
592
+ Example message: "I couldn't solve this problem. Here's the sequence of steps I took: ..."
593
+
594
+ Answer Format
595
+
596
+ When returning the FINAL ANSWER, ensure it follows this format:
597
+
598
+ FINAL ANSWER: [Your final answer here]
599
+
600
+ Use minimal words or a number. If necessary, a comma-separated list is allowed.
601
+
602
+ Do not include units, unnecessary details, or explanations unless specifically requested.
603
+
604
+ Key Focus Areas
605
+ The assistant should not merely perform tool calls but should critically evaluate its process at every step.
606
+
607
+ Keep the overall approach flexible so that if errors or issues arise, the model can adapt and improve its strategy in real-time.
608
+
609
+ Prioritize efficiency. Once the correct answer is found, avoid unnecessary further actions.
610
+
611
+ Your answer must always start with "FINAL ANSWER:".
612
+
613
+
614
+ ### Key Improvements:
615
+ 1. **Clearer Instructions for Iteration**: The prompt now emphasizes revising the plan based on results and ensuring that each step is critically evaluated.
616
+ 2. **Final Answer Verification**: We ensure that only the final answer is provided in the correct format, and intermediate steps are excluded.
617
+ 3. **Revised Plan if Initial Attempt Fails**: If the initial plan doesn't work or if the results don't meet expectations, the plan is revised and tools are adjusted accordingly.
618
+ 4. **Error Handling and Clarification**: If a solution can't be found, the model now acknowledges the steps taken and explains why the answer couldn't be solved, improving transparency.
619
+
620
+ This updated prompt should make your planner node more efficient and capable of adapting to complex scenarios while keeping the process structured and robust.
621
+
622
+ if you want to return FINAL ANSWER, finish your answer with the following template:
623
+ FINAL ANSWER: [YOUR FINAL ANSWER].
624
+ 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), ensure there is exactly one space after each comma.
625
+ Your answer should only start with "FINAL ANSWER: ", then follows with the answer.
626
+
627
+ **Return exactly** either a `FINAL ANSWER:` line or a JSON array of tool calls—nothing else.
628
+ """
629
+
630
+
631
+
632
+
633
+
634
+
635
+
636
+
637
+
638
+
639
+
640
+ class MessagesState(TypedDict):
641
+ input_file: Optional[str]
642
+ messages: Annotated[list[AnyMessage], add_messages]
643
+ final_answer: Optional[str]
644
+ is_done: Optional[bool]
645
+ assist_iter: Optional[int]
646
+
647
+ class EsiAgent:
648
+ def __init__(self):
649
+ llm = ChatOpenAI(
650
+ base_url="https://api.groq.com/openai/v1",
651
+ openai_api_key=GPT_4o_API_KEY,
652
+ # model_name="meta-llama/llama-4-scout-17b-16e-instruct",
653
+ temperature=0,
654
+ max_tokens=128
655
+ # max_tokens=256
656
+ )
657
+
658
+ llm_with_tools = llm.bind_tools(tools)
659
+
660
+ # 2) Bind your tools
661
+ llm_with_tools = llm.bind_tools(tools)
662
+
663
+ # 3) Single system message
664
+ self.sys_msg = SystemMessage(content=PLANNER_PROMPT)
665
+
666
+ # 4) Assistant node
667
+ def assistant(state: MessagesState) -> dict:
668
+ """
669
+ Assistant node: Validates if a final answer exists or generates the plan.
670
+ If a final answer is found, it returns it, otherwise, it generates a plan and iterates.
671
+ """
672
+
673
+ resutls_dict = {"tool_names":[],"tool_args":[],"tool_results":[]}
674
+ # Check if the final answer exists already in previous steps
675
+ for msg in state["messages"]:
676
+ if isinstance(msg, ToolMessage):
677
+ resutls_dict['tool_results'].append(msg.content)
678
+ elif isinstance(msg, AIMessage) and hasattr(msg, "tool_calls"):
679
+ for call in msg.tool_calls:
680
+ resutls_dict['tool_names'].append(call['name'])
681
+ resutls_dict['tool_args'].append(call['args'])
682
+
683
+
684
+ llm_input = [SystemMessage(content=PLANNER_PROMPT)] + state["messages"]
685
+
686
+ ai_resp: AIMessage = llm_with_tools.invoke(llm_input)
687
+ content = ai_resp.content.strip()
688
+
689
+ # Ensure that the FINAL ANSWER is correctly returned
690
+ s1=len(list(set(resutls_dict['tool_names'])))
691
+ s2=len(list(set(resutls_dict['tool_names'])))
692
+ s3=len(list(set(resutls_dict['tool_names'])))
693
+
694
+ if s1==s2==s3:
695
+ state['assist_iter'] += 1
696
+
697
+ if "FINAL ANSWER:" in content and (state['assist_iter']>=2 or state['assist_iter']<=5):
698
+ final_answer = content
699
+ is_done = True
700
+ else:
701
+ final_answer = None
702
+ is_done = False
703
+
704
+ return {
705
+ "messages": state["messages"] + [ai_resp],
706
+ "input_file": state.get("input_file", None),
707
+ "final_answer": final_answer,
708
+ 'assist_iter': state['assist_iter'] + 1,
709
+ "is_done": is_done,
710
+ }
711
+
712
+
713
+ # 5) Build the graph
714
+ builder = StateGraph(MessagesState)
715
+ builder.add_node("assistant", assistant)
716
+ builder.add_node("tools", ToolNode(tools))
717
+ builder.add_edge(START, "assistant")
718
+ builder.add_conditional_edges(
719
+ source="assistant",
720
+ path=lambda s: s.get("is_done", False),
721
+ path_map={False: "tools", True: END})
722
+
723
+ builder.add_edge("tools", "assistant")
724
+ self.graph = builder.compile()
725
+
726
+
727
+ def run(self, question: str, files=None) -> str:
728
+ """
729
+ Wraps graph.invoke to run end-to-end on a plain question.
730
+ """
731
+ # Build a minimal GAIA payload (if your MessagesState expects more, adjust here)
732
+ initial_state = {
733
+ "messages": [HumanMessage(content=question)]
734
+ "input_file": files,
735
+ "final_answer": "",
736
+ 'is_done': False,
737
+ 'assist_iter': 0
738
+ }
739
+
740
+ final_state = self.graph.invoke(initial_state)
741
+ return final_state['FINAL ANSWER']
742
 
743
  if __name__ == "__main__":
744
  # question = "Where is the capital of the Iran?"