kamorou commited on
Commit
6d3c99f
·
verified ·
1 Parent(s): 20bc99a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +116 -68
app.py CHANGED
@@ -250,6 +250,7 @@
250
 
251
  import os
252
  import io
 
253
  import requests
254
  import pandas as pd
255
  import gradio as gr
@@ -261,11 +262,7 @@ import operator
261
  from langchain_core.messages import BaseMessage, HumanMessage, ToolMessage, AIMessage, SystemMessage
262
  from langchain_core.tools import tool
263
  from langchain_huggingface import HuggingFaceEndpoint
264
- # <<<--- CHANGE 1: Import new components for building the agent --->>>
265
- from langchain.agents import AgentExecutor, create_tool_calling_agent
266
- from langchain_core.prompts import ChatPromptTemplate
267
  from langgraph.graph import StateGraph, END
268
- from langgraph.prebuilt import ToolNode
269
  from tavily import TavilyClient
270
  import pypdf
271
 
@@ -274,21 +271,32 @@ DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
274
  FILES_DIR = "./files"
275
  os.makedirs(FILES_DIR, exist_ok=True)
276
 
277
- # --- System Prompt (Unchanged) ---
278
  AGENT_SYSTEM_PROMPT = """You are a world-class AI agent, specialized in solving complex problems from the GAIA benchmark.
279
  Your task is to analyze the user's question, think step-by-step, and use the provided tools to find the correct answer.
280
- CRITICAL INSTRUCTIONS:
281
- 1. **Analyze the Goal:** First, understand what the user is asking for.
282
- 2. **Plan & Execute:** Formulate a plan and use the available tools (`tavily_search`, `read_file`, `python_interpreter`) to gather information.
283
- 3. **Final Answer Format:** Once you are absolutely certain of the answer, you MUST provide it directly and concisely.
284
- - DO NOT include your reasoning, thoughts, or any conversational text like 'The answer is...', 'Here is the result:', or 'Based on my search...'.
285
- - Your final response must ONLY be the answer itself.
 
 
 
 
 
 
 
 
 
 
 
 
286
  EXAMPLES OF CORRECT FINAL ANSWERS:
287
- - If the question asks for a year: `2023`
288
- - If it asks for a name: `John Doe`
289
- - If it asks for a number: `42`
290
- - If it asks for a comma-separated list: `item1, item2, item3`
291
- Think, use your tools, and then provide ONLY the final, precise answer.
292
  """
293
 
294
  #
@@ -317,7 +325,7 @@ def read_file(url: str) -> str:
317
  response = requests.get(url)
318
  response.raise_for_status()
319
  with open(filename, 'wb') as f: f.write(response.content)
320
-
321
  if url.lower().endswith('.pdf'):
322
  try:
323
  pdf_reader = pypdf.PdfReader(filename)
@@ -341,44 +349,85 @@ def python_interpreter(code: str) -> str:
341
 
342
  #
343
  # ================================================================================================
344
- # ✅ 2. CONFIGURE AND BUILD THE AGENT GRAPH (CORRECTED IMPLEMENTATION)
345
  # ================================================================================================
346
  #
347
  class AgentState(TypedDict):
348
- # <<<--- CHANGE 2: The state is now simpler. It tracks the input and the agent's output. --->>>
349
- input: str
350
- agent_outcome: dict | None
351
-
352
  def build_agent_graph():
353
  """Builds the LangGraph agent."""
354
  tools = [tavily_search, read_file, python_interpreter]
355
-
 
356
  repo_id = "CohereForAI/c4ai-command-r-plus"
357
  llm = HuggingFaceEndpoint(
358
- repo_id=repo_id, max_new_tokens=1024, temperature=0.1,
359
  huggingfacehub_api_token=os.getenv("HUGGINGFACEHUB_API_TOKEN")
360
  )
361
-
362
- # <<<--- CHANGE 3: Create a proper agent prompt template --->>>
363
- # This prompt is specifically designed to instruct the model on how to use tools.
364
- prompt = ChatPromptTemplate.from_messages([
365
- ("system", AGENT_SYSTEM_PROMPT),
366
- ("human", "{input}"),
367
- # The 'agent_scratchpad' placeholder is crucial for the agent to remember previous tool calls.
368
- ("placeholder", "{agent_scratchpad}"),
369
- ])
370
-
371
- # <<<--- CHANGE 4: Create the tool-calling agent runnable --->>>
372
- agent = create_tool_calling_agent(llm, tools, prompt)
373
-
374
- # <<<--- CHANGE 5: Create the AgentExecutor which will run the agent loop --->>>
375
- # This replaces the manual LangGraph loop for calling the model and tools.
376
- # It's a robust, pre-built component for this exact purpose.
377
- agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
378
-
379
- # We are not using LangGraph for the agent loop anymore, as AgentExecutor handles it.
380
- # We just need a simple callable class that invokes it.
381
- return agent_executor
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
382
 
383
  #
384
  # ================================================================================================
@@ -387,24 +436,28 @@ def build_agent_graph():
387
  #
388
  class GaiaAgent:
389
  def __init__(self):
390
- print("GaiaAgent initialized. Building Command R+ agent with AgentExecutor...")
391
- # The agent_app is now the fully-formed AgentExecutor
392
  self.agent_app = build_agent_graph()
393
 
394
  def __call__(self, question: str) -> str:
395
  print(f"\n{'='*60}\nAgent received question: {question[:100]}...\n{'='*60}")
396
-
397
- # <<<--- CHANGE 6: Invoke the AgentExecutor and extract the final answer --->>>
398
- try:
399
- # The AgentExecutor takes a dictionary and returns the final output in the 'output' key.
400
- response = self.agent_app.invoke({"input": question})
401
- final_answer = str(response.get("output", "")).strip()
402
- print(f"\n--- Agent finished. Final Answer: {final_answer} ---\n")
403
- return final_answer
404
- except Exception as e:
405
- print(f"An error occurred during agent execution: {e}")
406
- return f"AGENT_EXECUTION_ERROR: {e}"
407
-
 
 
 
 
 
408
 
409
  # --- The rest of the file (run_and_submit_all, Gradio UI) remains the same ---
410
  def run_and_submit_all( profile: gr.OAuthProfile | None):
@@ -431,15 +484,13 @@ def run_and_submit_all( profile: gr.OAuthProfile | None):
431
  answers_payload = []
432
  print(f"Running agent on {len(questions_data)} questions...")
433
 
434
- # Instantiate the agent once to save time
435
- agent_instance = GaiaAgent()
436
 
437
  for item in questions_data:
438
  task_id = item.get("task_id")
439
  question_text = item.get("question")
440
  if not task_id or question_text is None: continue
441
  try:
442
- # Reuse the same agent instance
443
  submitted_answer = agent_instance(question_text)
444
  answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer})
445
  results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer})
@@ -471,14 +522,11 @@ def run_and_submit_all( profile: gr.OAuthProfile | None):
471
  return status_message, results_df
472
 
473
  with gr.Blocks() as demo:
474
- gr.Markdown("# GAIA Agent Final Assessment (Open Source: Command R+ - Corrected)")
475
  gr.Markdown(
476
  """
477
- **Instructor's Note:** This version corrects the agent construction logic to be compatible with the `HuggingFaceEndpoint`.
478
- It now uses the standard `create_tool_calling_agent` and `AgentExecutor` from LangChain for robust tool use.
479
- 1. Ensure you have a **`HUGGINGFACEHUB_API_TOKEN`** and a **`TAVILY_API_KEY`** set in your Space secrets.
480
- 2. Your `requirements.txt` should include `langchain`, `langchain-huggingface`, and `langchain-core`.
481
- 3. Let's run the evaluation again!
482
  """
483
  )
484
  gr.LoginButton()
 
250
 
251
  import os
252
  import io
253
+ import json
254
  import requests
255
  import pandas as pd
256
  import gradio as gr
 
262
  from langchain_core.messages import BaseMessage, HumanMessage, ToolMessage, AIMessage, SystemMessage
263
  from langchain_core.tools import tool
264
  from langchain_huggingface import HuggingFaceEndpoint
 
 
 
265
  from langgraph.graph import StateGraph, END
 
266
  from tavily import TavilyClient
267
  import pypdf
268
 
 
271
  FILES_DIR = "./files"
272
  os.makedirs(FILES_DIR, exist_ok=True)
273
 
274
+ # --- System Prompt (Updated for Manual Tool Calling) ---
275
  AGENT_SYSTEM_PROMPT = """You are a world-class AI agent, specialized in solving complex problems from the GAIA benchmark.
276
  Your task is to analyze the user's question, think step-by-step, and use the provided tools to find the correct answer.
277
+
278
+ **TOOL USAGE INSTRUCTIONS:**
279
+ When you need to use a tool, you MUST respond with a JSON object containing the tool name and its arguments. The JSON object should have two keys: "tool_name" and "parameters".
280
+
281
+ Here is an example of how to call the `tavily_search` tool:
282
+ ```json
283
+ {
284
+ "tool_name": "tavily_search",
285
+ "parameters": {
286
+ "query": "What was the score of the 2023 FIFA Women's World Cup final?"
287
+ }
288
+ }```
289
+
290
+ **CRITICAL FINAL ANSWER INSTRUCTIONS:**
291
+ Once you have gathered all the necessary information and are absolutely certain of the answer, you MUST provide it directly and concisely.
292
+ - Your final response must ONLY be the answer itself.
293
+ - DO NOT wrap the final answer in a JSON object or include any conversational text like 'The answer is...'.
294
+
295
  EXAMPLES OF CORRECT FINAL ANSWERS:
296
+ - `2023`
297
+ - `John Doe`
298
+ - `42`
299
+ - `broccoli, celery, lettuce, sweet potatoes`
 
300
  """
301
 
302
  #
 
325
  response = requests.get(url)
326
  response.raise_for_status()
327
  with open(filename, 'wb') as f: f.write(response.content)
328
+
329
  if url.lower().endswith('.pdf'):
330
  try:
331
  pdf_reader = pypdf.PdfReader(filename)
 
349
 
350
  #
351
  # ================================================================================================
352
+ # ✅ 2. CONFIGURE AND BUILD THE AGENT GRAPH (MANUAL LANGGRAPH IMPLEMENTATION)
353
  # ================================================================================================
354
  #
355
  class AgentState(TypedDict):
356
+ messages: Annotated[List[BaseMessage], operator.add]
357
+
 
 
358
  def build_agent_graph():
359
  """Builds the LangGraph agent."""
360
  tools = [tavily_search, read_file, python_interpreter]
361
+ tool_map = {tool.name: tool for tool in tools}
362
+
363
  repo_id = "CohereForAI/c4ai-command-r-plus"
364
  llm = HuggingFaceEndpoint(
365
+ repo_id=repo_id, max_new_tokens=1024, temperature=0,
366
  huggingfacehub_api_token=os.getenv("HUGGINGFACEHUB_API_TOKEN")
367
  )
368
+
369
+ def call_model(state: AgentState):
370
+ """Invokes the LLM and wraps the response in an AIMessage."""
371
+ messages = state['messages']
372
+ # We combine the system prompt with the rest of the conversation.
373
+ prompt_str = ""
374
+ for msg in messages:
375
+ if isinstance(msg, SystemMessage):
376
+ prompt_str += f"<|SYSTEM|>\n{msg.content}\n"
377
+ elif isinstance(msg, HumanMessage):
378
+ prompt_str += f"<|USER|>\n{msg.content}\n"
379
+ elif isinstance(msg, AIMessage):
380
+ prompt_str += f"<|ASSISTANT|>\n{msg.content}\n"
381
+ elif isinstance(msg, ToolMessage):
382
+ prompt_str += f"<|TOOL_RESULT|>\n{msg.content}\n"
383
+
384
+ prompt_str += "<|ASSISTANT|>"
385
+
386
+ response_text = llm.invoke(prompt_str)
387
+ return {"messages": [AIMessage(content=response_text)]}
388
+
389
+ def should_continue(state: AgentState) -> str:
390
+ """Determines whether to call a tool or end the loop."""
391
+ last_message_content = state['messages'][-1].content.strip()
392
+
393
+ # A simple check: if the response looks like a JSON object, it's a tool call.
394
+ if last_message_content.startswith('{') and last_message_content.endswith('}'):
395
+ # More robust check for JSON tool call
396
+ try:
397
+ json.loads(last_message_content)
398
+ return "action"
399
+ except json.JSONDecodeError:
400
+ return "end" # It's not valid JSON, so it must be the final answer
401
+ else:
402
+ return "end"
403
+
404
+ def call_tool_node(state: AgentState):
405
+ """Parses the tool call from the LLM output and executes it."""
406
+ last_message_content = state['messages'][-1].content.strip()
407
+ try:
408
+ tool_call_data = json.loads(last_message_content)
409
+ tool_name = tool_call_data.get("tool_name")
410
+ parameters = tool_call_data.get("parameters", {})
411
+
412
+ if tool_name not in tool_map:
413
+ error_message = f"Error: Tool '{tool_name}' not found."
414
+ return {"messages": [ToolMessage(content=error_message, tool_call_id="error")]}
415
+
416
+ selected_tool = tool_map[tool_name]
417
+ tool_output = selected_tool.invoke(parameters)
418
+ return {"messages": [ToolMessage(content=str(tool_output), tool_call_id=tool_name)]}
419
+
420
+ except Exception as e:
421
+ error_message = f"Error processing tool call: {e}. Content: '{last_message_content}'"
422
+ return {"messages": [ToolMessage(content=error_message, tool_call_id="error")]}
423
+
424
+ workflow = StateGraph(AgentState)
425
+ workflow.add_node("agent", call_model)
426
+ workflow.add_node("action", call_tool_node)
427
+ workflow.set_entry_point("agent")
428
+ workflow.add_conditional_edges("agent", should_continue, {"action": "action", "end": END})
429
+ workflow.add_edge('action', 'agent')
430
+ return workflow.compile()
431
 
432
  #
433
  # ================================================================================================
 
436
  #
437
  class GaiaAgent:
438
  def __init__(self):
439
+ print("GaiaAgent initialized. Building Command R+ agent with manual LangGraph loop...")
 
440
  self.agent_app = build_agent_graph()
441
 
442
  def __call__(self, question: str) -> str:
443
  print(f"\n{'='*60}\nAgent received question: {question[:100]}...\n{'='*60}")
444
+ initial_input = {
445
+ "messages": [
446
+ SystemMessage(content=AGENT_SYSTEM_PROMPT),
447
+ HumanMessage(content=question)
448
+ ]
449
+ }
450
+ final_state = None
451
+ for i, step in enumerate(self.agent_app.stream(initial_input, {"recursion_limit": 15})):
452
+ if i == 0: print("--- Starting Agentic Loop ---")
453
+ print(f"--- Step {i+1} ---")
454
+ print(step)
455
+ final_state = list(step.values())[0] # Get the state from the graph step
456
+
457
+ final_answer_message = final_state['messages'][-1]
458
+ final_answer = str(final_answer_message.content).strip()
459
+ print(f"\n--- Agent finished. Final Answer: {final_answer} ---\n")
460
+ return final_answer
461
 
462
  # --- The rest of the file (run_and_submit_all, Gradio UI) remains the same ---
463
  def run_and_submit_all( profile: gr.OAuthProfile | None):
 
484
  answers_payload = []
485
  print(f"Running agent on {len(questions_data)} questions...")
486
 
487
+ agent_instance = GaiaAgent() # Instantiate the agent once
 
488
 
489
  for item in questions_data:
490
  task_id = item.get("task_id")
491
  question_text = item.get("question")
492
  if not task_id or question_text is None: continue
493
  try:
 
494
  submitted_answer = agent_instance(question_text)
495
  answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer})
496
  results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer})
 
522
  return status_message, results_df
523
 
524
  with gr.Blocks() as demo:
525
+ gr.Markdown("# GAIA Agent Final Assessment (Open Source: Command R+ - Final)")
526
  gr.Markdown(
527
  """
528
+ **Instructor's Note:** This version uses a robust, manual LangGraph loop to handle tool calls for the `HuggingFaceEndpoint`.
529
+ It explicitly tells the model to generate JSON for tool calls and parses this JSON from the text output. This is the correct, fundamental way to build agents with models that don't support modern tool-binding abstractions.
 
 
 
530
  """
531
  )
532
  gr.LoginButton()