| from typing import TypedDict, Optional |
| from langgraph.graph import StateGraph, START, END |
| from langchain_openai import ChatOpenAI |
| from langchain_core.messages import HumanMessage |
| from rich.console import Console |
| from smolagents import ( |
| CodeAgent, |
| ToolCallingAgent, |
| OpenAIServerModel, |
| AgentLogger, |
| LogLevel, |
| Panel, |
| Text, |
| ) |
| from tools import ( |
| GetAttachmentTool, |
| GoogleSearchTool, |
| GoogleSiteSearchTool, |
| ContentRetrieverTool, |
| YoutubeVideoTool, |
| SpeechRecognitionTool, |
| ClassifierTool, |
| ImageToChessBoardFENTool, |
| chess_engine_locator, |
| ) |
| import openai |
| import backoff |
|
|
| def create_general_ai_agent(verbosity: int = LogLevel.INFO): |
| get_attachment_tool = GetAttachmentTool() |
| speech_recognition_tool = SpeechRecognitionTool() |
| env_tools = [ |
| get_attachment_tool, |
| ] |
| model = OpenAIServerModel(model_id="gpt-4.1") |
| console = Console(record=True) |
| logger = AgentLogger(level=verbosity, console=console) |
| steps_buffer = [] |
|
|
|
|
| def capture_step_log(agent) -> None: |
| steps_buffer.append(console.export_text(clear=True)) |
|
|
|
|
| agents = { |
| agent.name: agent |
| for agent in [ |
| ToolCallingAgent( |
| name="general_assistant", |
| description="Answers questions for best of knowledge and common reasoning grounded on already known information. Can understand multimedia including audio and video files and YouTube.", |
| model=model, |
| tools=env_tools |
| + [ |
| speech_recognition_tool, |
| YoutubeVideoTool( |
| client=model.client, |
| speech_recognition_tool=speech_recognition_tool, |
| frames_interval=3, |
| chunk_duration=60, |
| debug=True, |
| ), |
| ClassifierTool( |
| client=model.client, |
| model_id="gpt-4.1-mini", |
| ), |
| ], |
| logger=logger, |
| step_callbacks=[capture_step_log], |
| ), |
| ToolCallingAgent( |
| name="web_researcher", |
| description="Answers questions that require grounding in unknown information through search on web sites and other online resources.", |
| tools=env_tools |
| + [ |
| GoogleSearchTool(), |
| GoogleSiteSearchTool(), |
| ContentRetrieverTool(), |
| ], |
| model=model, |
| planning_interval=3, |
| max_steps=9, |
| logger=logger, |
| step_callbacks=[capture_step_log], |
| ), |
| CodeAgent( |
| name="data_analyst", |
| description="Data analyst with advanced skills in statistic, handling tabular data and related Python packages.", |
| tools=env_tools, |
| additional_authorized_imports=[ |
| "numpy", |
| "pandas", |
| "tabulate", |
| "matplotlib", |
| "seaborn", |
| ], |
| model=model, |
| logger=logger, |
| step_callbacks=[capture_step_log], |
| ), |
| CodeAgent( |
| name="chess_player", |
| description="Chess grandmaster empowered by chess engine. Always thinks at least 100 steps ahead.", |
| tools=env_tools |
| + [ |
| ImageToChessBoardFENTool(client=model.client), |
| chess_engine_locator, |
| ], |
| additional_authorized_imports=[ |
| "chess", |
| "chess.engine", |
| ], |
| model=model, |
| logger=logger, |
| step_callbacks=[capture_step_log], |
| ), |
| ] |
| } |
|
|
|
|
| class GAIATask(TypedDict): |
| task_id: Optional[str | None] = None |
| question: str |
| steps: list[str] = [] |
| agent: Optional[str | None] = None |
| raw_answer: Optional[str | None] = None |
| final_answer: Optional[str | None] = None |
|
|
|
|
| llm = ChatOpenAI(model="gpt-4.1") |
| logger = AgentLogger(level=verbosity) |
|
|
|
|
| @backoff.on_exception(backoff.expo, openai.RateLimitError, max_time=60, max_tries=6) |
| def llm_invoke_with_retry(messages): |
| response = llm.invoke(messages) |
| return response |
|
|
|
|
| def read_question(state: GAIATask): |
| logger.log_task( |
| content=state["question"].strip(), |
| subtitle=f"LangGraph with {type(llm).__name__} - {llm.model_name}", |
| level=LogLevel.INFO, |
| title="Final Assignment Agent for Hugging Face Agents Course", |
| ) |
| get_attachment_tool.attachment_for(state["task_id"]) |
|
|
| return { |
| "steps": [], |
| "agent": None, |
| "raw_answer": None, |
| "final_answer": None, |
| } |
|
|
|
|
| def select_agent(state: GAIATask): |
| agents_description = "\n\n".join( |
| [ |
| f"AGENT NAME: {a.name}\nAGENT DESCRIPTION: {a.description}" |
| for a in agents.values() |
| ] |
| ) |
|
|
| prompt = f"""\ |
| You are a general AI assistant. |
| |
| I will provide you a question and a list of agents with their descriptions. |
| Your task is to select the most appropriate agent to answer the question. |
| You can select one of the agents or decide that no agent is needed. |
| |
| If question has attachment only agent can answer it. |
| |
| QUESTION: |
| {state["question"]} |
| |
| {agents_description} |
| |
| Now, return the name of the agent you selected or "no agent needed" if you think that no agent is needed. |
| """ |
|
|
| response = llm_invoke_with_retry([HumanMessage(content=prompt)]) |
| agent_name = response.content.strip() |
|
|
| if agent_name in agents: |
| logger.log( |
| f"Agent {agent_name} selected for solving the task.", |
| level=LogLevel.DEBUG, |
| ) |
| return { |
| "agent": agent_name, |
| "steps": state.get("steps", []) |
| + [ |
| f"Agent '{agent_name}' selected for task execution.", |
| ], |
| } |
| elif agent_name == "no agent needed": |
| logger.log( |
| "No appropriate agent found in the list. No agent will be used.", |
| level=LogLevel.DEBUG, |
| ) |
| return { |
| "agent": None, |
| "steps": state.get("steps", []) |
| + [ |
| "A decision is made to solve the task directly without invoking any agent.", |
| ], |
| } |
| else: |
| logger.log( |
| f"[bold red]Warning to user: Unexpected agent name '{agent_name}' selected. No agent will be used.[/bold red]", |
| level=LogLevel.INFO, |
| ) |
| return { |
| "agent": None, |
| "steps": state.get("steps", []) |
| + [ |
| f"Attempt to select non-existing agent '{agent_name}'. No agent will be used.", |
| ], |
| } |
|
|
|
|
| def delegate_to_agent(state: GAIATask): |
| agent_name = state.get("agent", None) |
| if not agent_name: |
| raise ValueError("Agent not selected.") |
| if agent_name not in agents: |
| raise ValueError(f"Agent '{agent_name}' is not available.") |
|
|
| logger.log( |
| Panel(Text(f"Calling agent: {agent_name}.")), |
| level=LogLevel.INFO, |
| ) |
|
|
| agent = agents[agent_name] |
| agent_answer = agent.run(task=state["question"]) |
| steps = [f"Agent '{agent_name}' step:\n{s}" for s in steps_buffer] |
| steps_buffer.clear() |
| return { |
| "raw_answer": agent_answer, |
| "steps": state.get("steps", []) + steps, |
| } |
|
|
|
|
| def one_shot_answering(state: GAIATask): |
| response = llm_invoke_with_retry([HumanMessage(content=state.get("question"))]) |
| return { |
| "raw_answer": response.content, |
| "steps": state.get("steps", []) |
| + [ |
| f"One-shot answer:\n{response.content}", |
| ], |
| } |
|
|
|
|
| def refine_answer(state: GAIATask): |
| question = state.get("question") |
| answer = state.get("raw_answer", None) |
| if not answer: |
| return {"final_answer": "No answer."} |
|
|
| prompt = f"""\ |
| You are a general AI assistant. |
| |
| I will provide you a question and correct answer to it. Answer is correct but may be too verbose or not follow the rules below. |
| Your task is to rephrase answer according to rules below. |
| |
| 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. |
| |
| If you are asked for a comma separated list, use space after comma and before next element of the list unless other directly specified in a question. |
| Check question context to define if letters case matters. Do not change case if not prescribed by other rules or question. |
| If you are not asked for the list, capitalize the first letter of the answer unless it changes meaning of the answer. |
| If answer is number, use digits only not words unless other directly specified in a question. |
| If answer is not full sentence, do not add period at the end. |
| |
| Preserve all items if the answer is a list. |
| |
| QUESTION: |
| {question} |
| |
| ANSWER: |
| {answer} |
| """ |
| response = llm_invoke_with_retry([HumanMessage(content=prompt)]) |
| refined_answer = response.content.strip() |
| logger.log( |
| Text(f"GAIA final answer: {refined_answer}", style="bold #d4b702"), |
| level=LogLevel.INFO, |
| ) |
| return { |
| "final_answer": refined_answer, |
| "steps": state.get("steps", []) |
| + [ |
| "Refining the answer according to GAIA benchmark rules.", |
| f"FINAL ANSWER: {response.content}", |
| ], |
| } |
|
|
|
|
| def route_task(state: GAIATask) -> str: |
| if state.get("agent") in agents: |
| return "agent selected" |
| else: |
| return "no agent matched" |
|
|
|
|
| |
| gaia_graph = StateGraph(GAIATask) |
|
|
| |
| gaia_graph.add_node("read_question", read_question) |
| gaia_graph.add_node("select_agent", select_agent) |
| gaia_graph.add_node("delegate_to_agent", delegate_to_agent) |
| gaia_graph.add_node("one_shot_answering", one_shot_answering) |
| gaia_graph.add_node("refine_answer", refine_answer) |
|
|
| |
| gaia_graph.add_edge(START, "read_question") |
| |
| gaia_graph.add_edge("read_question", "select_agent") |
|
|
| |
| gaia_graph.add_conditional_edges( |
| "select_agent", |
| route_task, |
| {"agent selected": "delegate_to_agent", "no agent matched": "one_shot_answering"}, |
| ) |
|
|
| |
| gaia_graph.add_edge("delegate_to_agent", "refine_answer") |
| gaia_graph.add_edge("one_shot_answering", "refine_answer") |
| gaia_graph.add_edge("refine_answer", END) |
|
|
| gaia = gaia_graph.compile() |
| return gaia |
|
|