wt002 commited on
Commit
415844f
·
verified ·
1 Parent(s): eaba916

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +230 -456
app.py CHANGED
@@ -22,485 +22,259 @@ DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
22
  #Load environment variables
23
  load_dotenv()
24
 
25
- import io
26
- import contextlib
27
- import traceback
28
- from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline
29
- from smolagents import Tool, CodeAgent, DuckDuckGoSearchTool, FinalAnswerTool, HfApiModel
30
-
31
-
32
- class CodeLlamaTool(Tool):
33
- name = "code_llama_tool"
34
- description = "Solves reasoning/code questions using Meta Code Llama 7B Instruct"
35
-
36
- inputs = {
37
- "question": {
38
- "type": "string",
39
- "description": "The question requiring code-based or reasoning-based solution"
40
- }
41
- }
42
- output_type = "string"
43
-
44
- def __init__(self):
45
- self.model_id = "codellama/CodeLlama-7b-Instruct-hf"
46
- token = os.getenv("HF_TOKEN")
47
-
48
- self.tokenizer = AutoTokenizer.from_pretrained(self.model_id, token=token)
49
- self.model = AutoModelForCausalLM.from_pretrained(
50
- self.model_id,
51
- device_map="auto",
52
- torch_dtype="auto",
53
- token=token
54
- )
55
- self.pipeline = pipeline(
56
- "text-generation",
57
- model=self.model,
58
- tokenizer=self.tokenizer,
59
- max_new_tokens=512,
60
- temperature=0.2,
61
- truncation=True
62
  )
63
 
64
- def forward(self, question: str) -> str:
65
- prompt = f"""You are an AI that uses Python code to answer questions.
66
- Question: {question}
67
- Instructions:
68
- - If solving requires code, use a block like <tool>code</tool>.
69
- - Always end with <final>FINAL ANSWER</final> containing the final number or string.
70
- Example:
71
- Question: What is 5 * sqrt(36)?
72
- Answer:
73
- <tool>
74
- import math
75
- print(5 * math.sqrt(36))
76
- </tool>
77
- <final>30.0</final>
78
- Answer:"""
79
-
80
- response = self.pipeline(prompt)[0]["generated_text"]
81
- return self.parse_and_execute(response)
82
-
83
- def parse_and_execute(self, response: str) -> str:
84
- try:
85
- # Extract and run code if exists
86
- if "<tool>" in response and "</tool>" in response:
87
- code = response.split("<tool>")[1].split("</tool>")[0].strip()
88
- result = self._run_code(code)
89
- return f"FINAL ANSWER (code output): {result}"
90
-
91
- # Extract final result directly
92
- elif "<final>" in response and "</final>" in response:
93
- final = response.split("<final>")[1].split("</final>")[0].strip()
94
- return f"FINAL ANSWER: {final}"
95
-
96
- return f"Could not extract final answer.\n\n{response}"
97
-
98
- except Exception as e:
99
- return f"Error in parse_and_execute: {str(e)}\n\nFull response:\n{response}"
100
-
101
- def _run_code(self, code: str) -> str:
102
- buffer = io.StringIO()
103
- try:
104
- with contextlib.redirect_stdout(buffer):
105
- exec(code, {})
106
- return buffer.getvalue().strip()
107
- except Exception:
108
- return f"Error executing code:\n{traceback.format_exc()}"
109
-
110
-
111
-
112
- from duckduckgo_search import DDGS
113
- import wikipedia
114
- import arxiv
115
- from transformers import pipeline
116
- import os
117
- import re
118
- import ast
119
- import subprocess
120
- import sys
121
-
122
- # ===== Search Tools =====
123
- class DuckDuckGoSearchTool:
124
- def __init__(self, max_results=3):
125
- self.description = "Search web using DuckDuckGo. Input: search query"
126
- self.max_results = max_results
127
-
128
- def run(self, query: str) -> str:
129
- try:
130
- with DDGS() as ddgs:
131
- results = [r for r in ddgs.text(query, max_results=self.max_results)]
132
- return "\n\n".join(
133
- f"Title: {res['title']}\nURL: {res['href']}\nSnippet: {res['body']}"
134
- for res in results
135
- )
136
- except Exception as e:
137
- return f"Search error: {str(e)}"
138
-
139
- class WikiSearchTool:
140
- def __init__(self, sentences=3):
141
- self.description = "Get Wikipedia summaries. Input: search phrase"
142
- self.sentences = sentences
143
-
144
- def run(self, query: str) -> str:
145
- try:
146
- return wikipedia.summary(query, sentences=self.sentences)
147
- except wikipedia.DisambiguationError as e:
148
- return f"Disambiguation error. Options: {', '.join(e.options[:5])}"
149
- except wikipedia.PageError:
150
- return "Page not found"
151
- except Exception as e:
152
- return f"Wikipedia error: {str(e)}"
153
-
154
- class ArxivSearchTool:
155
- def __init__(self, max_results=3):
156
- self.description = "Search academic papers on arXiv. Input: search query"
157
- self.max_results = max_results
158
-
159
- def run(self, query: str) -> str:
160
- try:
161
- results = arxiv.Search(
162
- query=query,
163
- max_results=self.max_results,
164
- sort_by=arxiv.SortCriterion.Relevance
165
- ).results()
166
-
167
- output = []
168
- for r in results:
169
- output.append(
170
- f"Title: {r.title}\n"
171
- f"Authors: {', '.join(a.name for a in r.authors)}\n"
172
- f"Published: {r.published.strftime('%Y-%m-%d')}\n"
173
- f"Summary: {r.summary[:250]}...\n"
174
- f"URL: {r.entry_id}"
175
- )
176
- return "\n\n".join(output)
177
- except Exception as e:
178
- return f"arXiv error: {str(e)}"
179
 
180
- # ===== QA Tools =====
181
- class HuggingFaceDocumentQATool:
182
- def __init__(self):
183
- self.description = "Answer questions from documents. Input: 'document_text||question'"
184
- self.model = pipeline(
185
- 'question-answering',
186
- model='deepset/roberta-base-squad2',
187
- tokenizer='deepset/roberta-base-squad2'
188
- )
189
 
190
- def run(self, input_str: str) -> str:
191
- try:
192
- if '||' not in input_str:
193
- return "Invalid format. Use: 'document_text||question'"
194
-
195
- context, question = input_str.split('||', 1)
196
- result = self.model(question=question, context=context)
197
- return result['answer']
198
- except Exception as e:
199
- return f"QA error: {str(e)}"
200
-
201
-
202
-
203
- from transformers import BlipProcessor, BlipForQuestionAnswering
204
-
205
- class HuggingFaceImageQATool(Tool):
206
- name = "image_qa"
207
- description = "Answer questions about an image."
208
- inputs = {
209
- "image_path": {"type": "string", "description": "Path to image"},
210
- "question": {"type": "string", "description": "Question about the image"}
211
- }
212
- output_type = "string"
213
-
214
- def __init__(self):
215
- self.processor = BlipProcessor.from_pretrained("Salesforce/blip-vqa-base")
216
- self.model = BlipForQuestionAnswering.from_pretrained("Salesforce/blip-vqa-base")
217
-
218
- def forward(self, image_path: str, question: str) -> str:
219
- image = Image.open(image_path)
220
- inputs = self.processor(image, question, return_tensors="pt")
221
- out = self.model.generate(**inputs)
222
- return self.processor.decode(out[0], skip_special_tokens=True)
223
-
224
-
225
- from transformers import pipeline
226
-
227
- class HuggingFaceTranslationTool(Tool):
228
- name = "translate"
229
- description = "Translate text from English to another language."
230
- inputs = {
231
- "text": {"type": "string", "description": "Text to translate"}
232
- }
233
- output_type = "string"
234
-
235
- def __init__(self):
236
- self.translator = pipeline("translation", model="Helsinki-NLP/opus-mt-en-fr")
237
-
238
- def forward(self, text: str) -> str:
239
- return self.translator(text)[0]["translation_text"]
240
 
 
 
 
 
 
 
 
 
 
 
241
 
242
- # ===== Code Execution =====
243
- class PythonCodeExecutionTool:
244
- def __init__(self):
245
- self.description = "Execute Python code. Input: valid Python code"
246
-
247
- def run(self, code: str) -> str:
248
- try:
249
- # Isolate code in a clean environment
250
- env = {}
251
- exec(f"def __temp_func__():\n {indent_code(code)}", env)
252
- output = env['__temp_func__']()
253
- return str(output)
254
- except Exception as e:
255
- return f"Execution error: {str(e)}"
256
 
257
  def indent_code(code: str) -> str:
258
- """Add proper indentation for multiline code"""
259
  return '\n '.join(code.splitlines())
260
 
261
- # ===== Answer Formatting =====
262
- class FinalAnswerTool:
263
- def __init__(self):
264
- self.description = "Format final answer. Input: answer content"
265
-
266
- def run(self, answer: str) -> str:
267
- return f"FINAL ANSWER: {answer}"
268
-
269
- #from smolagents import Tool
270
- #from langchain_community.document_loaders import WikipediaLoader
271
-
272
- class WikiSearchTool(Tool):
273
- name = "wiki_search"
274
- description = "Search Wikipedia for a query and return up to 2 results."
275
- inputs = {
276
- "query": {"type": "string", "description": "The search term for Wikipedia."}
277
  }
278
- output_type = "string"
279
-
280
- def forward(self, query: str) -> str:
281
- search_docs = WikipediaLoader(query=query, load_max_docs=2).load()
282
-
283
- formatted_search_docs = "\n\n---\n\n".join(
284
- [
285
- f'<Document source="{doc.metadata.get("source", "Wikipedia")}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content}\n</Document>'
286
- for doc in search_docs
287
- ]
288
- )
289
- return formatted_search_docs
290
 
 
 
 
291
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
 
293
-
294
- class StringReverseTool(Tool):
295
- name = "reverse_message"
296
- description = "When you received a strange text, try to reverse it and perform action described in reversed message."
297
- inputs = {
298
- "message": {
299
- "type": "string",
300
- "description": "A message, which looks like strange and can be reversed to get actions to execute."
301
- }
302
- }
303
- output_type = "string"
304
-
305
- def __init__(self):
306
- return
307
-
308
- def forward(self, message: str):
309
- return message[::-1]
310
-
311
- class KeywordsExtractorTool(Tool):
312
- """Extracts top 5 keywords from a given text based on frequency."""
313
-
314
- name = "keywords_extractor"
315
- description = "This tool returns the 5 most frequent keywords occur in provided block of text."
316
-
317
- inputs = {
318
- "text": {
319
- "type": "string",
320
- "description": "Text to analyze for keywords.",
321
- }
322
  }
323
- output_type = "string"
 
 
 
 
 
 
 
 
 
 
 
 
324
 
325
- def forward(self, text: str) -> str:
326
- try:
327
- all_words = re.findall(r'\b\w+\b', text.lower())
328
- conjunctions = {'a', 'and', 'of', 'is', 'in', 'to', 'the'}
329
- filtered_words = []
330
- for w in all_words:
331
- if w not in conjunctions:
332
- filtered_words.push(w)
333
- word_counts = Counter(filtered_words)
334
- k = 5
335
- return heapq.nlargest(k, word_counts.items(), key=lambda x: x[1])
336
- except Exception as e:
337
- return f"Error during extracting most common words: {e}"
 
338
 
339
- @tool
340
- def parse_excel_to_json(task_id: str) -> dict:
341
- """
342
- For a given task_id fetch and parse an Excel file and save parsed data in structured JSON file.
343
- Args:
344
- task_id: An task ID to fetch.
345
-
346
- Returns:
 
 
 
 
 
 
 
347
  {
348
- "task_id": str,
349
- "sheets": {
350
- "SheetName1": [ {col1: val1, col2: val2, ...}, ... ],
351
- ...
352
- },
353
- "status": "Success" | "Error"
354
- }
355
- """
356
- url = f"https://agents-course-unit4-scoring.hf.space/files/{task_id}"
357
-
358
- try:
359
- response = requests.get(url, timeout=100)
360
- if response.status_code != 200:
361
- return {"task_id": task_id, "sheets": {}, "status": f"{response.status_code} - Failed"}
362
-
363
- xls_content = pd.ExcelFile(BytesIO(response.content))
364
- json_sheets = {}
365
-
366
- for sheet in xls_content.sheet_names:
367
- df = xls_content.parse(sheet)
368
- df = df.dropna(how="all")
369
- rows = df.head(20).to_dict(orient="records")
370
- json_sheets[sheet] = rows
371
-
372
- return {
373
- "task_id": task_id,
374
- "sheets": json_sheets,
375
- "status": "Success"
376
  }
 
 
 
 
377
 
378
- except Exception as e:
379
- return {
380
- "task_id": task_id,
381
- "sheets": {},
382
- "status": f"Error in parsing Excel file: {str(e)}"
383
- }
384
-
385
-
386
-
387
- class VideoTranscriptionTool(Tool):
388
- """Fetch transcripts from YouTube videos"""
389
- name = "transcript_video"
390
- description = "Fetch text transcript from YouTube movies with optional timestamps"
391
- inputs = {
392
- "url": {"type": "string", "description": "YouTube video URL or ID"},
393
- "include_timestamps": {"type": "boolean", "description": "If timestamps should be included in output", "nullable": True}
394
- }
395
- output_type = "string"
396
-
397
- def forward(self, url: str, include_timestamps: bool = False) -> str:
398
-
399
- if "youtube.com/watch" in url:
400
- video_id = url.split("v=")[1].split("&")[0]
401
- elif "youtu.be/" in url:
402
- video_id = url.split("youtu.be/")[1].split("?")[0]
403
- elif len(url.strip()) == 11: # Direct ID
404
- video_id = url.strip()
405
- else:
406
- return f"YouTube URL or ID: {url} is invalid!"
407
-
408
- try:
409
- transcription = YouTubeTranscriptApi.get_transcript(video_id)
410
-
411
- if include_timestamps:
412
- formatted_transcription = []
413
- for part in transcription:
414
- timestamp = f"{int(part['start']//60)}:{int(part['start']%60):02d}"
415
- formatted_transcription.append(f"[{timestamp}] {part['text']}")
416
- return "\n".join(formatted_transcription)
417
- else:
418
- return " ".join([part['text'] for part in transcription])
419
-
420
- except Exception as e:
421
- return f"Error in extracting YouTube transcript: {str(e)}"
422
-
423
  class BasicAgent:
424
  def __init__(self):
425
- token = os.environ.get("HF_API_TOKEN")
426
- model = HfApiModel(
427
- temperature=0.1,
428
- token=token
429
- )
430
-
431
- # Existing tools
432
- search_tool = DuckDuckGoSearchTool()
433
- wiki_search_tool = WikiSearchTool()
434
- str_reverse_tool = StringReverseTool()
435
- keywords_extract_tool = KeywordsExtractorTool()
436
- speech_to_text_tool = SpeechToTextTool()
437
- visit_webpage_tool = VisitWebpageTool()
438
- final_answer_tool = FinalAnswerTool()
439
- video_transcription_tool = VideoTranscriptionTool()
440
-
441
- # ✅ New Llama Tool
442
- code_llama_tool = CodeLlamaTool()
443
- arxiv_search_tool = ArxivSearchTool()
444
- doc_qa_tool = HuggingFaceDocumentQATool()
445
- image_qa_tool = HuggingFaceImageQATool()
446
- translation_tool = HuggingFaceTranslationTool()
447
- python_tool = PythonCodeExecutionTool()
448
-
449
- system_prompt = f"""
450
- You are my general AI assistant. Your primary goal is to answer the user's question accurately and concisely.
451
-
452
- Here's a detailed plan for answering:
453
- 1. **Understand the Question:** Carefully parse the question to identify key entities, relationships, and the type of information requested.
454
- 2. **Reasoning Steps (Chain-of-Thought):** Before attempting to answer, outline a step-by-step reasoning process. This helps in breaking down complex questions.
455
- 3. **Tool Selection and Usage:** Based on your reasoning, select the most appropriate tool(s) to gather information or perform operations.
456
- - Use `search_tool` (DuckDuckGoSearchTool) for general web searches.
457
- - Use `wiki_search_tool` for encyclopedic knowledge.
458
- - Use `arxiv_search_tool` for scientific papers.
459
- - Use `visit_webpage_tool` to read content from URLs found via search.
460
- - Use `doc_qa_tool` for answering questions about specific documents (if provided).
461
- - Use `image_qa_tool` for questions about images.
462
- - Use `translation_tool` for language translation.
463
- - Use `python_tool` or `code_llama_tool` for code generation, execution, or complex calculations/data manipulation.
464
- - Use `keywords_extract_tool` to identify important terms from text.
465
- - Use `str_reverse_tool` for string manipulation if needed (less common for Q&A).
466
- - Use `speech_to_text_tool` or `video_transcription_tool` if audio/video input is part of the question.
467
- - Use `parse_excel_to_json` if the question involves data from Excel.
468
- 4. **Information Synthesis:** Combine and process the information obtained from tools. Cross-reference if necessary to ensure accuracy.
469
- 5. **Formulate Final Answer:** Construct the final answer according to the specified format.
470
-
471
- **Final Answer Format:**
472
- Return your final answer in a single line, formatted as follows: "FINAL ANSWER: [YOUR FINAL ANSWER]".
473
- [YOUR FINAL ANSWER] should be a number, a string, or a comma-separated list of numbers and/or strings, depending on the question.
474
- - If the answer is a number, do not use commas or units (e.g., $, %) unless explicitly specified in the question.
475
- - If the answer is a string, do not use articles (a, an, the) or common abbreviations (e.g., "NY" for "New York") unless specified. Write digits in plain text unless specified.
476
- - If the answer is a comma-separated list, apply the above rules for each element based on whether it is a number or a string.
477
- - If you cannot find a definitive answer, state "FINAL ANSWER: I don't know."
478
-
479
- Let's think step by step.
480
- """
481
- self.agent.prompt_templates["system_prompt"] = self.agent.prompt_templates["system_prompt"] + system_prompt
482
-
483
- self.agent = CodeAgent(
484
- model=model,
485
- tools=[
486
- search_tool, wiki_search_tool, str_reverse_tool,
487
- keywords_extract_tool, speech_to_text_tool,
488
- visit_webpage_tool, final_answer_tool,
489
- parse_excel_to_json, video_transcription_tool,
490
- arxiv_search_tool,
491
- doc_qa_tool, image_qa_tool,
492
- translation_tool, python_tool,
493
- code_llama_tool # 🔧 Add here
494
- ],
495
- add_base_tools=True
496
- )
497
- self.agent.prompt_templates["system_prompt"] = self.agent.prompt_templates["system_prompt"] + system_prompt
498
-
499
  def __call__(self, question: str) -> str:
500
- print(f"Agent received question (first 50 chars): {question[:50]}...")
501
- answer = self.agent.run(question)
502
- print(f"Agent returning answer: {answer}")
503
- return answer
 
 
 
 
 
 
504
 
505
 
506
  def run_and_submit_all( profile: gr.OAuthProfile | None):
 
22
  #Load environment variables
23
  load_dotenv()
24
 
25
+ from langgraph.graph import END, StateGraph
26
+ from langchain_core.prompts import ChatPromptTemplate
27
+ from langchain_core.messages import HumanMessage, AIMessage, ToolMessage
28
+ from langchain_core.tools import tool
29
+ from typing import Dict, List, TypedDict, Annotated
30
+ import operator
31
+
32
+ # ====== Tool Definitions ======
33
+ @tool
34
+ def duckduckgo_search(query: str) -> str:
35
+ """Search web using DuckDuckGo. Returns top 3 results."""
36
+ from duckduckgo_search import DDGS
37
+ with DDGS() as ddgs:
38
+ return "\n\n".join(
39
+ f"Title: {res['title']}\nURL: {res['href']}\nSnippet: {res['body']}"
40
+ for res in ddgs.text(query, max_results=3)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  )
42
 
43
+ @tool
44
+ def wikipedia_search(query: str) -> str:
45
+ """Get Wikipedia summaries. Returns first 3 sentences."""
46
+ import wikipedia
47
+ try:
48
+ return wikipedia.summary(query, sentences=3)
49
+ except wikipedia.DisambiguationError as e:
50
+ return f"Disambiguation options: {', '.join(e.options[:3])}"
51
+ except wikipedia.PageError:
52
+ return "Page not found"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
 
54
+ @tool
55
+ def arxiv_search(query: str) -> str:
56
+ """Search academic papers on arXiv. Returns top 3 results."""
57
+ import arxiv
58
+ results = arxiv.Search(
59
+ query=query,
60
+ max_results=3,
61
+ sort_by=arxiv.SortCriterion.Relevance
62
+ ).results()
63
 
64
+ return "\n\n".join(
65
+ f"Title: {r.title}\nAuthors: {', '.join(a.name for a in r.authors)}\n"
66
+ f"Published: {r.published.strftime('%Y-%m-%d')}\nSummary: {r.summary[:250]}..."
67
+ for r in results
68
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
 
70
+ @tool
71
+ def document_qa(input_str: str) -> str:
72
+ """Answer questions from documents. Input format: 'document_text||question'"""
73
+ from transformers import pipeline
74
+ if '||' not in input_str:
75
+ return "Invalid format. Use: 'document_text||question'"
76
+
77
+ context, question = input_str.split('||', 1)
78
+ qa_model = pipeline('question-answering', model='deepset/roberta-base-squad2')
79
+ return qa_model(question=question, context=context)['answer']
80
 
81
+ @tool
82
+ def python_execution(code: str) -> str:
83
+ """Execute Python code and return output."""
84
+ try:
85
+ # Create isolated environment
86
+ env = {}
87
+ exec(f"def __exec_fn__():\n {indent_code(code)}\nresult = __exec_fn__()", env)
88
+ return str(env.get('result', 'No output'))
89
+ except Exception as e:
90
+ return f"Error: {str(e)}"
 
 
 
 
91
 
92
  def indent_code(code: str) -> str:
 
93
  return '\n '.join(code.splitlines())
94
 
95
+ # ====== Agent State ======
96
+ class AgentState(TypedDict):
97
+ question: str
98
+ history: Annotated[List[Dict], operator.add]
99
+ context: str
100
+ reasoning: str
101
+ iterations: int
102
+
103
+ # ====== Graph Components ======
104
+ def init_state(question: str) -> AgentState:
105
+ return {
106
+ "question": question,
107
+ "history": [],
108
+ "context": f"User question: {question}",
109
+ "reasoning": "",
110
+ "iterations": 0
111
  }
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
+ def should_continue(state: AgentState) -> str:
114
+ """Determine if agent should continue or finish"""
115
+ last_msg = state['history'][-1]
116
 
117
+ # Stop conditions
118
+ if state['iterations'] >= 5:
119
+ return "end"
120
+ if "FINAL ANSWER:" in last_msg.get('content', ''):
121
+ return "end"
122
+ if last_msg['role'] == 'tool':
123
+ return "reason"
124
+ return "continue"
125
+
126
+ def reasoning_node(state: AgentState) -> AgentState:
127
+ """Agent reasoning and tool selection"""
128
+ from langchain_community.chat_models import ChatHuggingFace
129
+ from langchain.schema import SystemMessage
130
+
131
+ # Build prompt
132
+ prompt = ChatPromptTemplate.from_messages([
133
+ SystemMessage(content=(
134
+ "You are an intelligent AI assistant. Follow this process:\n"
135
+ "1. Analyze the question: {question}\n"
136
+ "2. Review context: {context}\n"
137
+ "3. Reasoning Steps:\n{reasoning}\n"
138
+ "4. Select ONE tool to use next OR provide FINAL ANSWER\n\n"
139
+ "Available Tools:\n"
140
+ "- duckduckgo_search: For current information\n"
141
+ "- wikipedia_search: For factual knowledge\n"
142
+ "- arxiv_search: For academic topics\n"
143
+ "- document_qa: For questions about documents\n"
144
+ "- python_execution: For calculations/code\n\n"
145
+ "Response Format:\n"
146
+ "Reasoning: [Your step-by-step analysis]\n"
147
+ "Action: [Tool name OR 'Final Answer']\n"
148
+ "Action Input: [Tool parameters OR final response]"
149
+ )),
150
+ *state['history']
151
+ ])
152
+
153
+ # Initialize model
154
+ llm = ChatHuggingFace(
155
+ model_name="HuggingFaceH4/zephyr-7b-beta",
156
+ temperature=0.3
157
+ )
158
+
159
+ # Generate response
160
+ response = llm.invoke(prompt.format_messages(
161
+ question=state['question'],
162
+ context=state['context'],
163
+ reasoning=state['reasoning']
164
+ ))
165
+
166
+ # Parse response
167
+ content = response.content
168
+ reasoning, action, action_input = parse_agent_response(content)
169
+
170
+ # Update state
171
+ state['history'].append(AIMessage(content=content))
172
+ state['reasoning'] += f"\nStep {state['iterations']+1}: {reasoning}"
173
+
174
+ if "final answer" in action.lower():
175
+ state['history'].append(AIMessage(
176
+ content=f"FINAL ANSWER: {action_input}"
177
+ ))
178
+ else:
179
+ state['history'].append({
180
+ "role": "action_request",
181
+ "tool": action,
182
+ "input": action_input
183
+ })
184
+
185
+ return state
186
 
187
+ def tool_node(state: AgentState) -> AgentState:
188
+ """Execute selected tool and update state"""
189
+ last_action = state['history'][-1]
190
+ tool_name = last_action['tool']
191
+ tool_input = last_action['input']
192
+
193
+ # Tool mapping
194
+ tools = {
195
+ "duckduckgo_search": duckduckgo_search,
196
+ "wikipedia_search": wikipedia_search,
197
+ "arxiv_search": arxiv_search,
198
+ "document_qa": document_qa,
199
+ "python_execution": python_execution
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
  }
201
+
202
+ # Execute tool
203
+ tool_result = tools[tool_name].invoke(tool_input)
204
+
205
+ # Update state
206
+ state['history'].append(ToolMessage(
207
+ content=tool_result,
208
+ tool_call_id=tool_name
209
+ ))
210
+ state['context'] = f"Tool Result ({tool_name}): {tool_result}"
211
+ state['iterations'] += 1
212
+
213
+ return state
214
 
215
+ def parse_agent_response(response: str) -> tuple:
216
+ """Extract reasoning, action, and input from response"""
217
+ reasoning = response.split("Reasoning:")[1].split("Action:")[0].strip()
218
+ action_part = response.split("Action:")[1].strip()
219
+
220
+ if "Action Input:" in action_part:
221
+ action, action_input = action_part.split("Action Input:", 1)
222
+ action = action.strip()
223
+ action_input = action_input.strip()
224
+ else:
225
+ action = action_part
226
+ action_input = ""
227
+
228
+ return reasoning, action, action_input
229
 
230
+ # ====== Agent Graph ======
231
+ def create_agent_workflow():
232
+ workflow = StateGraph(AgentState)
233
+
234
+ # Define nodes
235
+ workflow.add_node("reason", reasoning_node)
236
+ workflow.add_node("action", tool_node)
237
+
238
+ # Set entry point
239
+ workflow.set_entry_point("reason")
240
+
241
+ # Define edges
242
+ workflow.add_conditional_edges(
243
+ "reason",
244
+ should_continue,
245
  {
246
+ "continue": "action",
247
+ "reason": "reason",
248
+ "end": END
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
  }
250
+ )
251
+
252
+ workflow.add_edge("action", "reason")
253
+ return workflow.compile()
254
 
255
+ # ====== Agent Interface ======
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
  class BasicAgent:
257
  def __init__(self):
258
+ self.workflow = create_agent_workflow()
259
+ self.tools = [
260
+ duckduckgo_search,
261
+ wikipedia_search,
262
+ arxiv_search,
263
+ document_qa,
264
+ python_execution
265
+ ]
266
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  def __call__(self, question: str) -> str:
268
+ state = init_state(question)
269
+ final_state = self.workflow.invoke(state)
270
+
271
+ # Extract final answer
272
+ for msg in reversed(final_state['history']):
273
+ if msg.get('content', '').startswith("FINAL ANSWER:"):
274
+ return msg['content'].split("FINAL ANSWER:")[1].strip()
275
+
276
+ return "No final answer found"
277
+
278
 
279
 
280
  def run_and_submit_all( profile: gr.OAuthProfile | None):