lautel commited on
Commit
af5a423
·
verified ·
1 Parent(s): 81917a3

Upload 7 files

Browse files
agentic/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ from .langgraph_agent import LangGraphAgent4GAIA
agentic/graph.png ADDED
agentic/langgraph_agent.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+ from langgraph.graph import START, StateGraph, MessagesState
4
+ from langgraph.prebuilt import tools_condition, ToolNode
5
+ from langchain_openai import ChatOpenAI
6
+ from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
7
+ from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
8
+ from omegaconf import OmegaConf
9
+ from .tools import *
10
+
11
+
12
+ def load_config(config_path: str):
13
+ config = OmegaConf.load(config_path)
14
+ return config
15
+
16
+ # --- Constants ---
17
+ CONFIG = load_config("config.yaml")
18
+ SYSTEM_PROMPT = CONFIG["system_prompt"]["custom"]
19
+
20
+ # Load environment variables from .env file
21
+ load_dotenv()
22
+
23
+
24
+ class LangGraphAgent4GAIA:
25
+ def __init__(self, model_provider: str, model_name: str):
26
+ self.sys_prompt = SystemMessage(content=SYSTEM_PROMPT)
27
+ self.graph = self.get_agent(model_provider, model_name)
28
+
29
+ def assistant(self, state: MessagesState):
30
+ """Assistant node"""
31
+ return {"messages": [self.llm_with_tools.invoke([self.sys_prompt] + state["messages"])]}
32
+
33
+ def get_agent(self, provider: str, model_name: str):
34
+ tools = [
35
+ multiply,
36
+ add,
37
+ subtract,
38
+ divide,
39
+ modulo,
40
+ web_search,
41
+ arxiv_search,
42
+ wiki_search
43
+ ]
44
+
45
+ # 1. Build graph
46
+ if provider == "openai":
47
+ llm = ChatOpenAI(
48
+ model=model_name,
49
+ temperature=0,
50
+ max_retries=2,
51
+ api_key=os.getenv("OPENAI_API_KEY")
52
+ )
53
+ elif provider == "huggingface":
54
+ llm = ChatHuggingFace(
55
+ llm=HuggingFaceEndpoint(
56
+ repo_id=model_name,
57
+ task="text-generation",
58
+ max_new_tokens=1024,
59
+ do_sample=False,
60
+ repetition_penalty=1.03,
61
+ temperature=0
62
+ ),
63
+ verbose=True
64
+ )
65
+ else:
66
+ raise ValueError("Invalid provider. Choose 'openai' or 'huggingface'.")
67
+
68
+
69
+ # 2. Bind tools to LLM
70
+ self.llm_with_tools = llm.bind_tools(tools)
71
+
72
+ builder = StateGraph(MessagesState)
73
+ builder.add_node("assistant", self.assistant)
74
+ builder.add_node("tools", ToolNode(tools))
75
+ builder.add_edge(START, "assistant")
76
+ builder.add_conditional_edges(
77
+ "assistant",
78
+ tools_condition,
79
+ )
80
+ builder.add_edge("tools", "assistant")
81
+
82
+ # Compile graph
83
+ return builder.compile()
84
+
85
+ if __name__ == "__main__":
86
+ from langchain_core.runnables.graph import MermaidDrawMethod
87
+
88
+ question = "What is the capital of Spain?"
89
+ # Build the graph
90
+ agent_manager = LangGraphAgent4GAIA(CONFIG["model"]["provider"], CONFIG["model"]["name"])
91
+ img_data = agent_manager.graph.get_graph().draw_mermaid_png(draw_method=MermaidDrawMethod.API)
92
+ with open('agentic/graph.png', "wb") as f:
93
+ f.write(img_data)
94
+
95
+ # Run the graph
96
+ messages = [HumanMessage(content=question)]
97
+ messages = agent_manager.graph.invoke({"messages": messages}, {"recursion_limit": 50})
98
+ for m in messages["messages"]:
99
+ m.pretty_print()
agentic/tools.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain_core.tools import tool
2
+ from langchain_community.tools import DuckDuckGoSearchResults
3
+ from langchain_community.tools.tavily_search import TavilySearchResults
4
+ from langchain_community.document_loaders import ArxivLoader, WikipediaLoader
5
+
6
+ @tool
7
+ def add(a: int, b: int) -> int:
8
+ """Add two numbers.
9
+
10
+ Args:
11
+ a: first int
12
+ b: second int
13
+ """
14
+ return a + b
15
+
16
+
17
+ @tool
18
+ def subtract(a: int, b: int) -> int:
19
+ """Subtract two numbers.
20
+
21
+ Args:
22
+ a: first int
23
+ b: second int
24
+ """
25
+ return a - b
26
+
27
+
28
+ @tool
29
+ def multiply(a: int, b: int) -> int:
30
+ """Multiply two numbers.
31
+
32
+ Args:
33
+ a: first int
34
+ b: second int
35
+ """
36
+ return a * b
37
+
38
+
39
+ @tool
40
+ def divide(a: int, b: int) -> int:
41
+ """Divide two numbers.
42
+
43
+ Args:
44
+ a: first int
45
+ b: second int
46
+ """
47
+ if b == 0:
48
+ raise ValueError("Cannot divide by zero.")
49
+ return a / b
50
+
51
+
52
+ @tool
53
+ def modulo(a: int, b: int) -> int:
54
+ """Get the modulus of two numbers.
55
+
56
+ Args:
57
+ a: first int
58
+ b: second int
59
+ """
60
+ return a % b
61
+
62
+
63
+ @tool
64
+ def web_search(query: str) -> str:
65
+ """Web search with Tavily for a query and return maximum 3 results.
66
+
67
+ Args:
68
+ query: The search query."""
69
+ # search_docs = DuckDuckGoSearchResults(max_results=3).invoke(query=query)
70
+ search_docs = TavilySearchResults(max_results=3).invoke(query=query)
71
+ formatted_search_docs = "\n\n---\n\n".join(
72
+ [
73
+ f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content}\n</Document>'
74
+ for doc in search_docs
75
+ ])
76
+ # return {"web_results": formatted_search_docs}
77
+ return formatted_search_docs
78
+
79
+
80
+ @tool
81
+ def arxiv_search(query: str) -> str:
82
+ """Search ArXiv for a query and return maximum 3 result.
83
+
84
+ Args:
85
+ query: The search query."""
86
+ search_docs = ArxivLoader(query=query, load_max_docs=3).load()
87
+ formatted_search_docs = "\n\n---\n\n".join(
88
+ [
89
+ f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content[:1000]}\n</Document>'
90
+ for doc in search_docs
91
+ ])
92
+ # return {"arxiv_results": formatted_search_docs}
93
+ return formatted_search_docs
94
+
95
+
96
+ @tool
97
+ def wiki_search(query: str) -> str:
98
+ """Search Wikipedia for a query and return maximum 2 results.
99
+
100
+ Args:
101
+ query: The search query."""
102
+ search_docs = WikipediaLoader(query=query, load_max_docs=2).load()
103
+ formatted_search_docs = "\n\n---\n\n".join(
104
+ [
105
+ f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content}\n</Document>'
106
+ for doc in search_docs
107
+ ])
108
+ # return {"wiki_results": formatted_search_docs}
109
+ return formatted_search_docs
app.py CHANGED
@@ -1,15 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
2
  import gradio as gr
3
  import requests
4
- import inspect
5
  import pandas as pd
 
 
 
 
 
 
 
 
6
 
7
- # (Keep Constants as is)
8
  # --- Constants ---
9
- DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
10
 
11
  # --- Basic Agent Definition ---
12
- # ----- THIS IS WERE YOU CAN BUILD WHAT YOU WANT ------
13
  class BasicAgent:
14
  def __init__(self):
15
  print("BasicAgent initialized.")
@@ -19,6 +42,23 @@ class BasicAgent:
19
  print(f"Agent returning fixed answer: {fixed_answer}")
20
  return fixed_answer
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  def run_and_submit_all( profile: gr.OAuthProfile | None):
23
  """
24
  Fetches all questions, runs the BasicAgent on them, submits all answers,
@@ -34,17 +74,17 @@ def run_and_submit_all( profile: gr.OAuthProfile | None):
34
  print("User not logged in.")
35
  return "Please Login to Hugging Face with the button.", None
36
 
37
- api_url = DEFAULT_API_URL
38
  questions_url = f"{api_url}/questions"
39
  submit_url = f"{api_url}/submit"
40
 
41
  # 1. Instantiate Agent ( modify this part to create your agent)
42
  try:
43
- agent = BasicAgent()
44
  except Exception as e:
45
  print(f"Error instantiating agent: {e}")
46
  return f"Error initializing agent: {e}", None
47
- # In the case of an app running as a hugging Face space, this link points toward your codebase ( usefull for others so please keep it public)
48
  agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
49
  print(agent_code)
50
 
 
1
+ """
2
+ This is a modified version of the original hf space code for submitting
3
+ answers from the course.
4
+
5
+ Instruction on submitting answers from
6
+ https://huggingface.co/learn/agents-course/unit4/hands-on
7
+
8
+ GET /questions: Retrieve the full list of filtered evaluation questions.
9
+ GET /random-question: Fetch a single random question from the list.
10
+ GET /files/{task_id}: Download a specific file associated with a given task ID.
11
+ POST /submit: Submit agent answers, calculate the score, and update the leaderboard.
12
+
13
+ The submit function will compare the answer to the ground truth in an EXACT MATCH manner,
14
+ hence prompt it well ! The GAIA team shared a prompting example for your agent here
15
+ (for the sake of this course, make sure you don't include the text "FINAL ANSWER" in your
16
+ submission, just make your agent reply with the answer and nothing else).
17
+ """
18
  import os
19
  import gradio as gr
20
  import requests
 
21
  import pandas as pd
22
+ from omegaconf import OmegaConf
23
+ from langchain_core.messages import HumanMessage
24
+ from agentic import LangGraphAgent4GAIA
25
+
26
+
27
+ def load_config(config_path: str):
28
+ config = OmegaConf.load(config_path)
29
+ return config
30
 
 
31
  # --- Constants ---
32
+ CONFIG = load_config("config.yaml")
33
 
34
  # --- Basic Agent Definition ---
35
+ # ----- THIS IS WHERE YOU CAN BUILD WHAT YOU WANT ------
36
  class BasicAgent:
37
  def __init__(self):
38
  print("BasicAgent initialized.")
 
42
  print(f"Agent returning fixed answer: {fixed_answer}")
43
  return fixed_answer
44
 
45
+ class Agent:
46
+ def __init__(self):
47
+ self.agent_manager = LangGraphAgent4GAIA(
48
+ model_provider=CONFIG["model"]["provider"],
49
+ model_name=CONFIG["model"]["name"]
50
+ )
51
+ print("LangGraphAgent4GAIA initialized.")
52
+
53
+ def __call__(self, question: str) -> str:
54
+ print(f"Agent received question (first 50 chars): {question[:50]}...")
55
+ messages = [HumanMessage(content=question)]
56
+ result = self.agent_manager.graph.invoke({"messages": messages}, {"recursion_limit": 50})
57
+ answer = result['messages'][-1].content
58
+ final_answer = answer.split('FINAL ANSWER: ')[-1].strip()
59
+ return final_answer
60
+
61
+
62
  def run_and_submit_all( profile: gr.OAuthProfile | None):
63
  """
64
  Fetches all questions, runs the BasicAgent on them, submits all answers,
 
74
  print("User not logged in.")
75
  return "Please Login to Hugging Face with the button.", None
76
 
77
+ api_url = CONFIG["api_url"]
78
  questions_url = f"{api_url}/questions"
79
  submit_url = f"{api_url}/submit"
80
 
81
  # 1. Instantiate Agent ( modify this part to create your agent)
82
  try:
83
+ agent = Agent()
84
  except Exception as e:
85
  print(f"Error instantiating agent: {e}")
86
  return f"Error initializing agent: {e}", None
87
+ # In the case of an app running as a hugging Face space, this link points toward your codebase (useful for others so please keep it public)
88
  agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
89
  print(agent_code)
90
 
config.yaml ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ api_url: "https://agents-course-unit4-scoring.hf.space"
2
+
3
+ system_prompt:
4
+ default: "You are a general AI assistant. I will ask you a question. Report your thoughts, and finish your answer with the following template: FINAL ANSWER: [YOUR FINAL ANSWER]. YOUR FINAL ANSWER should be a number OR as few words as possible OR a comma separated list of numbers and/or strings. If you are asked for a number, don't use comma to write your number neither use units such as $ or percent sign unless specified otherwise. If you are asked for a string, don't use articles, neither abbreviations (e.g. for cities), and write the digits in plain text unless specified otherwise. If you are asked for a comma separated list, apply the above rules depending of whether the element to be put in the list is a number or a string.\n"
5
+ custom: "You are a helpful assistant tasked with answering questions using a set of tools.\nNow, I will ask you a question. Report your thoughts, and finish your answer with the following template: \nFINAL ANSWER: [YOUR FINAL ANSWER]. \nYOUR FINAL ANSWER should be a number OR as few words as possible OR a comma separated list of numbers and/or strings. If you are asked for a number, don't use comma to write your number neither use units such as $ or percent sign unless specified otherwise. If you are asked for a string, don't use articles, neither abbreviations (e.g. for cities), and write the digits in plain text unless specified otherwise. If you are asked for a comma separated list, apply the above rules depending of whether the element to be put in the list is a number or a string.\nYour answer should only start with 'FINAL ANSWER: ', then follows with the answer. "
6
+
7
+ model:
8
+ provider: "openai"
9
+ name: "gpt-4o-mini"
10
+ # provider: "huggingface"
11
+ # name: "Qwen/Qwen2.5-Coder-32B-Instruct"
12
+ # name: "deepseek-ai/DeepSeek-R1-0528-Qwen3-8B"
13
+
14
+
15
+ code:
16
+ username: "lautel"
17
+ hf_space: "https://huggingface.co/spaces/lautel/agents-course-final-project"
18
+
19
+ paths:
20
+ output: "results"
21
+ output_filename: "results_{timestamp}.json"
22
+ logs_filename: "log_{timestamp}.txt"
requirements.txt CHANGED
@@ -1,2 +1,20 @@
1
  gradio
2
- requests
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  gradio
2
+ requests
3
+ omegaconf
4
+ pandas
5
+ tabulate
6
+ langchain
7
+ langchain-community
8
+ langchain-core
9
+ langchain-huggingface
10
+ langchain-openai
11
+ langchain-tavily
12
+ langgraph
13
+ huggingface_hub
14
+ qdrant-client
15
+ arxiv
16
+ pymupdf
17
+ wikipedia
18
+ pgvector
19
+ python-dotenv
20
+ beautifulsoup4