upload files
Browse files- agent.py +165 -0
- app.py +197 -0
- prompts.yaml +340 -0
- tools.py +747 -0
- web_search_agent.py +282 -0
- web_search_tools.py +190 -0
agent.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#-----------------------------------------------------------------------------
|
| 2 |
+
# Import packages
|
| 3 |
+
#-----------------------------------------------------------------------------
|
| 4 |
+
import os
|
| 5 |
+
from dotenv import load_dotenv
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
import yaml
|
| 8 |
+
from IPython.display import Image, display
|
| 9 |
+
|
| 10 |
+
from langchain_core.tools import tool
|
| 11 |
+
from langchain_core.messages import SystemMessage, AIMessage, HumanMessage
|
| 12 |
+
from langchain_openai import ChatOpenAI
|
| 13 |
+
from langgraph.graph import StateGraph, MessagesState, START, END
|
| 14 |
+
from langgraph.prebuilt import ToolNode
|
| 15 |
+
|
| 16 |
+
from tools import assistant_tools_list
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
#-----------------------------------------------------------------------------
|
| 20 |
+
# Load environment variables
|
| 21 |
+
#-----------------------------------------------------------------------------
|
| 22 |
+
load_dotenv()
|
| 23 |
+
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
| 24 |
+
OPENAI_API_MODEL = os.getenv("OPENAI_API_MODEL")
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
#-----------------------------------------------------------------------------
|
| 28 |
+
# Load prompts
|
| 29 |
+
#-----------------------------------------------------------------------------
|
| 30 |
+
PROMPT_PATH = Path("./prompts.yaml")
|
| 31 |
+
with PROMPT_PATH.open("r", encoding="utf-8") as f:
|
| 32 |
+
prompt_dict = yaml.safe_load(f)
|
| 33 |
+
|
| 34 |
+
system_prompt = prompt_dict["system_prompt"]
|
| 35 |
+
final_prompt = prompt_dict["final_prompt"]
|
| 36 |
+
|
| 37 |
+
#-----------------------------------------------------------------------------
|
| 38 |
+
# Create and compile main graph
|
| 39 |
+
#-----------------------------------------------------------------------------
|
| 40 |
+
llm = ChatOpenAI(model=OPENAI_API_MODEL, temperature=0, api_key=OPENAI_API_KEY )
|
| 41 |
+
llm_with_tools = llm.bind_tools(tools=assistant_tools_list)
|
| 42 |
+
|
| 43 |
+
class AgentState(MessagesState):
|
| 44 |
+
task_id: str
|
| 45 |
+
|
| 46 |
+
def assistant_node(state: AgentState):
|
| 47 |
+
"""Assistant node"""
|
| 48 |
+
response = llm_with_tools.invoke([SystemMessage(content=system_prompt.format(task_id=state['task_id']))] + state['messages'])
|
| 49 |
+
return {"messages": response}
|
| 50 |
+
|
| 51 |
+
def final_node(state: AgentState):
|
| 52 |
+
"""Final answer node"""
|
| 53 |
+
raw_question = state['messages'][0].content
|
| 54 |
+
raw_response = state['messages'][-1].content
|
| 55 |
+
|
| 56 |
+
response = llm.invoke([SystemMessage(content=final_prompt.format(user_question=raw_question, llm_response=raw_response))])
|
| 57 |
+
return {"messages": response.content.strip("FINAL ANSWER: ")}
|
| 58 |
+
|
| 59 |
+
def router(state: AgentState):
|
| 60 |
+
"""
|
| 61 |
+
Decides the next step after the assistant has responded.
|
| 62 |
+
- If tool calls are present, route to the 'tools' node.
|
| 63 |
+
- If no tool calls, the conversation is over, route to the 'final' formatting node.
|
| 64 |
+
"""
|
| 65 |
+
last_message = state['messages'][-1]
|
| 66 |
+
|
| 67 |
+
if hasattr(last_message, "tool_calls") and last_message.tool_calls:
|
| 68 |
+
# Se ci sono chiamate a tool, vai al nodo 'tools'
|
| 69 |
+
return "tools"
|
| 70 |
+
else:
|
| 71 |
+
# Se non ci sono chiamate a tool, la risposta è pronta per la formattazione finale
|
| 72 |
+
return "final"
|
| 73 |
+
|
| 74 |
+
builder = StateGraph(AgentState)
|
| 75 |
+
builder.add_node("assistant", assistant_node)
|
| 76 |
+
builder.add_node("final", final_node)
|
| 77 |
+
builder.add_node("tools", ToolNode(assistant_tools_list))
|
| 78 |
+
|
| 79 |
+
builder.add_edge(START, "assistant")
|
| 80 |
+
builder.add_conditional_edges(
|
| 81 |
+
"assistant",
|
| 82 |
+
router,
|
| 83 |
+
{
|
| 84 |
+
"tools": "tools",
|
| 85 |
+
"final": "final",
|
| 86 |
+
}
|
| 87 |
+
)
|
| 88 |
+
builder.add_edge("tools", "assistant")
|
| 89 |
+
builder.add_edge("final", END)
|
| 90 |
+
|
| 91 |
+
graph = builder.compile()
|
| 92 |
+
display(Image(graph.get_graph(xray=1).draw_mermaid_png(output_file_path="./graph.png")))
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
#-----------------------------------------------------------------------------
|
| 96 |
+
# Test graph
|
| 97 |
+
#-----------------------------------------------------------------------------
|
| 98 |
+
"""
|
| 99 |
+
if __name__ == "__main__":
|
| 100 |
+
#task_id = "8e867cd7-cff9-4e6c-867a-ff5ddc2550be"
|
| 101 |
+
#question = "How many studio albums were published by Mercedes Sosa between 2000 and 2009 (included)? You can use the latest 2022 version of english wikipedia."
|
| 102 |
+
|
| 103 |
+
#task_id = "a1e91b78-d3d8-4675-bb8d-62741b4b68a6"
|
| 104 |
+
#question = "In the video https://www.youtube.com/watch?v=L1vXCYZAYYM, what is the highest number of bird species to be on camera simultaneously?"
|
| 105 |
+
|
| 106 |
+
#task_id = "2d83110e-a098-4ebb-9987-066c06fa42d0"
|
| 107 |
+
#question = ".rewsna eht sa 'tfel' drow eht fo etisoppo eht etirw ,ecnetnes siht dnatsrednu uoy fI"
|
| 108 |
+
|
| 109 |
+
#task_id = "cca530fc-4052-43b2-b130-b30968d8aa44"
|
| 110 |
+
#question = "Review the chess position provided in the image. It is black's turn. Provide the correct next move for black which guarantees a win. Please provide your response in algebraic notation."
|
| 111 |
+
|
| 112 |
+
#task_id = "4fc2f1ae-8625-45b5-ab34-ad4433bc21f8"
|
| 113 |
+
#question = "Who nominated the only Featured Article on English Wikipedia about a dinosaur that was promoted in November 2016?"
|
| 114 |
+
|
| 115 |
+
#task_id = "6f37996b-2ac7-44b0-8e68-6d28256631b4"
|
| 116 |
+
#question = "Given this table defining * on the set S = {a, b, c, d, e}\n\n|*|a|b|c|d|e|\n|---|---|---|---|---|---|\n|a|a|b|c|b|d|\n|b|b|c|a|e|c|\n|c|c|a|b|b|a|\n|d|b|e|b|e|d|\n|e|d|b|a|d|c|\n\nprovide the subset of S involved in any possible counter-examples that prove * is not commutative. Provide your answer as a comma separated list of the elements in the set in alphabetical order."
|
| 117 |
+
|
| 118 |
+
#task_id = "9d191bce-651d-4746-be2d-7ef8ecadb9c2"
|
| 119 |
+
#question = "Examine the video at https://www.youtube.com/watch?v=1htKBjuUWec.\n\nWhat does Teal'c say in response to the question \"Isn't that hot?\""
|
| 120 |
+
|
| 121 |
+
#task_id = "cabe07ed-9eca-40ea-8ead-410ef5e83f91"
|
| 122 |
+
#question= "What is the surname of the equine veterinarian mentioned in 1.E Exercises from the chemistry materials licensed by Marisa Alviar-Agnew & Henry Agnew under the CK-12 license in LibreText's Introductory Chemistry materials as compiled 08/21/2023?"
|
| 123 |
+
|
| 124 |
+
#task_id = "3cef3a44-215e-4aed-8e3b-b1e3f08063b7"
|
| 125 |
+
#question = "I'm making a grocery list for my mom, but she's a professor of botany and she's a real stickler when it comes to categorizing things. I need to add different foods to different categories on the grocery list, but if I make a mistake, she won't buy anything inserted in the wrong category. Here's the list I have so far:\n\nmilk, eggs, flour, whole bean coffee, Oreos, sweet potatoes, fresh basil, plums, green beans, rice, corn, bell pepper, whole allspice, acorns, broccoli, celery, zucchini, lettuce, peanuts\n\nI need to make headings for the fruits and vegetables. Could you please create a list of just the vegetables from my list? If you could do that, then I can figure out how to categorize the rest of the list into the appropriate categories. But remember that my mom is a real stickler, so make sure that no botanical fruits end up on the vegetable list, or she won't get them when she's at the store. Please alphabetize the list of vegetables, and place each item in a comma separated list."
|
| 126 |
+
|
| 127 |
+
#task_id = "99c9cc74-fdc8-46c6-8f8d-3ce2d3bfeea3",
|
| 128 |
+
#question = "Hi, I'm making a pie but I could use some help with my shopping list. I have everything I need for the crust, but I'm not sure about the filling. I got the recipe from my friend Aditi, but she left it as a voice memo and the speaker on my phone is buzzing so I can't quite make out what she's saying. Could you please listen to the recipe and list all of the ingredients that my friend described? I only want the ingredients for the filling, as I have everything I need to make my favorite pie crust. I've attached the recipe as Strawberry pie.mp3.\n\nIn your response, please only list the ingredients, not any measurements. So if the recipe calls for \"a pinch of salt\" or \"two cups of ripe strawberries\" the ingredients on the list would be \"salt\" and \"ripe strawberries\".\n\nPlease format your response as a comma separated list of ingredients. Also, please alphabetize the ingredients."
|
| 129 |
+
|
| 130 |
+
#task_id = "305ac316-eef6-4446-960a-92d80d542f82"
|
| 131 |
+
#question = "Who did the actor who played Ray in the Polish-language version of Everybody Loves Raymond play in Magda M.? Give only the first name."
|
| 132 |
+
|
| 133 |
+
#task_id = "f918266a-b3e0-4914-865d-4faa564f1aef"
|
| 134 |
+
#question = "What is the final numeric output from the attached Python code?"
|
| 135 |
+
|
| 136 |
+
#task_id = "3f57289b-8c60-48be-bd80-01f8099ca449"
|
| 137 |
+
#question = "How many at bats did the Yankee with the most walks in the 1977 regular season have that same season?"
|
| 138 |
+
|
| 139 |
+
#task_id = "1f975693-876d-457b-a649-393859e79bf3"
|
| 140 |
+
#question = "Hi, I was out sick from my classes on Friday, so I'm trying to figure out what I need to study for my Calculus mid-term next week. My friend from class sent me an audio recording of Professor Willowbrook giving out the recommended reading for the test, but my headphones are broken :(\n\nCould you please listen to the recording for me and tell me the page numbers I'm supposed to go over? I've attached a file called Homework.mp3 that has the recording. Please provide just the page numbers as a comma-delimited list. And please provide the list in ascending order."
|
| 141 |
+
|
| 142 |
+
#task_id = "840bfca7-4f7b-481a-8794-c560c340185d"
|
| 143 |
+
#question = "On June 6, 2023, an article by Carolyn Collins Petersen was published in Universe Today. This article mentions a team that produced a paper about their observations, linked at the bottom of the article. Find this paper. Under what NASA award number was the work performed by R. G. Arendt supported by?"
|
| 144 |
+
|
| 145 |
+
#task_id = "bda648d7-d618-4883-88f4-3466eabd860e"
|
| 146 |
+
#question = "Where were the Vietnamese specimens described by Kuznetzov in Nedoshivina's 2010 paper eventually deposited? Just give me the city name without abbreviations."
|
| 147 |
+
|
| 148 |
+
#task_id = "cf106601-ab4f-4af9-b045-5295fe67b37d"
|
| 149 |
+
#question = "What country had the least number of athletes at the 1928 Summer Olympics? If there's a tie for a number of athletes, return the first in alphabetical order. Give the IOC country code as your answer."
|
| 150 |
+
|
| 151 |
+
#task_id = "a0c07678-e491-4bbc-8f0b-07405144218f"
|
| 152 |
+
#question = "Who are the pitchers with the number before and after Taishō Tamai's number as of July 2023? Give them to me in the form Pitcher Before, Pitcher After, use their last names only, in Roman characters."
|
| 153 |
+
|
| 154 |
+
#task_id = "7bd855d8-463d-4ed5-93ca-5fe35145f733"
|
| 155 |
+
#question = "The attached Excel file contains the sales of menu items for a local fast-food chain. What were the total sales that the chain made from food (not including drinks)? Express your answer in USD with two decimal places."
|
| 156 |
+
|
| 157 |
+
#task_id = "5a0c1adf-205e-4841-a666-7c3ef95def9d"
|
| 158 |
+
#question = "What is the first name of the only Malko Competition recipient from the 20th Century (after 1977) whose nationality on record is a country that no longer exists?"
|
| 159 |
+
|
| 160 |
+
messages = [HumanMessage(content=question)]
|
| 161 |
+
messages = graph.invoke({"messages": messages, "task_id": task_id})
|
| 162 |
+
for m in messages["messages"]:
|
| 163 |
+
m.pretty_print()
|
| 164 |
+
#messages["messages"][-1].pretty_print()
|
| 165 |
+
"""
|
app.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import gradio as gr
|
| 3 |
+
import requests
|
| 4 |
+
import inspect
|
| 5 |
+
import pandas as pd
|
| 6 |
+
|
| 7 |
+
# Importiamo l'agente
|
| 8 |
+
from agent import graph
|
| 9 |
+
|
| 10 |
+
# (Keep Constants as is)
|
| 11 |
+
# --- Constants ---
|
| 12 |
+
DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
|
| 13 |
+
|
| 14 |
+
# --- Basic Agent Definition ---
|
| 15 |
+
# ----- THIS IS WERE YOU CAN BUILD WHAT YOU WANT ------
|
| 16 |
+
class BasicAgent:
|
| 17 |
+
def __init__(self):
|
| 18 |
+
print("BasicAgent initialized.")
|
| 19 |
+
def __call__(self, question: str) -> str:
|
| 20 |
+
print(f"Agent received question (first 50 chars): {question[:50]}...")
|
| 21 |
+
fixed_answer = "This is a default answer."
|
| 22 |
+
print(f"Agent returning fixed answer: {fixed_answer}")
|
| 23 |
+
return fixed_answer
|
| 24 |
+
|
| 25 |
+
def run_and_submit_all( profile: gr.OAuthProfile | None):
|
| 26 |
+
"""
|
| 27 |
+
Fetches all questions, runs the BasicAgent on them, submits all answers,
|
| 28 |
+
and displays the results.
|
| 29 |
+
"""
|
| 30 |
+
# --- Determine HF Space Runtime URL and Repo URL ---
|
| 31 |
+
space_id = os.getenv("SPACE_ID") # Get the SPACE_ID for sending link to the code
|
| 32 |
+
|
| 33 |
+
if profile:
|
| 34 |
+
username= f"{profile.username}"
|
| 35 |
+
print(f"User logged in: {username}")
|
| 36 |
+
else:
|
| 37 |
+
print("User not logged in.")
|
| 38 |
+
return "Please Login to Hugging Face with the button.", None
|
| 39 |
+
|
| 40 |
+
api_url = DEFAULT_API_URL
|
| 41 |
+
questions_url = f"{api_url}/questions"
|
| 42 |
+
submit_url = f"{api_url}/submit"
|
| 43 |
+
|
| 44 |
+
# 1. Instantiate Agent ( modify this part to create your agent)
|
| 45 |
+
try:
|
| 46 |
+
agent = graph
|
| 47 |
+
except Exception as e:
|
| 48 |
+
print(f"Error instantiating agent: {e}")
|
| 49 |
+
return f"Error initializing agent: {e}", None
|
| 50 |
+
# 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)
|
| 51 |
+
agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
|
| 52 |
+
print(agent_code)
|
| 53 |
+
|
| 54 |
+
# 2. Fetch Questions
|
| 55 |
+
print(f"Fetching questions from: {questions_url}")
|
| 56 |
+
try:
|
| 57 |
+
response = requests.get(questions_url, timeout=15)
|
| 58 |
+
response.raise_for_status()
|
| 59 |
+
questions_data = response.json()
|
| 60 |
+
if not questions_data:
|
| 61 |
+
print("Fetched questions list is empty.")
|
| 62 |
+
return "Fetched questions list is empty or invalid format.", None
|
| 63 |
+
print(f"Fetched {len(questions_data)} questions.")
|
| 64 |
+
except requests.exceptions.RequestException as e:
|
| 65 |
+
print(f"Error fetching questions: {e}")
|
| 66 |
+
return f"Error fetching questions: {e}", None
|
| 67 |
+
except requests.exceptions.JSONDecodeError as e:
|
| 68 |
+
print(f"Error decoding JSON response from questions endpoint: {e}")
|
| 69 |
+
print(f"Response text: {response.text[:500]}")
|
| 70 |
+
return f"Error decoding server response for questions: {e}", None
|
| 71 |
+
except Exception as e:
|
| 72 |
+
print(f"An unexpected error occurred fetching questions: {e}")
|
| 73 |
+
return f"An unexpected error occurred fetching questions: {e}", None
|
| 74 |
+
|
| 75 |
+
# 3. Run your Agent
|
| 76 |
+
results_log = []
|
| 77 |
+
answers_payload = []
|
| 78 |
+
print(f"Running agent on {len(questions_data)} questions...")
|
| 79 |
+
for item in questions_data:
|
| 80 |
+
task_id = item.get("task_id")
|
| 81 |
+
question_text = item.get("question")
|
| 82 |
+
if not task_id or question_text is None:
|
| 83 |
+
print(f"Skipping item with missing task_id or question: {item}")
|
| 84 |
+
continue
|
| 85 |
+
try:
|
| 86 |
+
submitted_answer = agent.invoke({"messages": [HumanMessage(content=question_text)], "task_id": task_id})
|
| 87 |
+
answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer})
|
| 88 |
+
results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer})
|
| 89 |
+
except Exception as e:
|
| 90 |
+
print(f"Error running agent on task {task_id}: {e}")
|
| 91 |
+
results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": f"AGENT ERROR: {e}"})
|
| 92 |
+
|
| 93 |
+
if not answers_payload:
|
| 94 |
+
print("Agent did not produce any answers to submit.")
|
| 95 |
+
return "Agent did not produce any answers to submit.", pd.DataFrame(results_log)
|
| 96 |
+
|
| 97 |
+
# 4. Prepare Submission
|
| 98 |
+
submission_data = {"username": username.strip(), "agent_code": agent_code, "answers": answers_payload}
|
| 99 |
+
status_update = f"Agent finished. Submitting {len(answers_payload)} answers for user '{username}'..."
|
| 100 |
+
print(status_update)
|
| 101 |
+
|
| 102 |
+
# 5. Submit
|
| 103 |
+
print(f"Submitting {len(answers_payload)} answers to: {submit_url}")
|
| 104 |
+
try:
|
| 105 |
+
response = requests.post(submit_url, json=submission_data, timeout=60)
|
| 106 |
+
response.raise_for_status()
|
| 107 |
+
result_data = response.json()
|
| 108 |
+
final_status = (
|
| 109 |
+
f"Submission Successful!\n"
|
| 110 |
+
f"User: {result_data.get('username')}\n"
|
| 111 |
+
f"Overall Score: {result_data.get('score', 'N/A')}% "
|
| 112 |
+
f"({result_data.get('correct_count', '?')}/{result_data.get('total_attempted', '?')} correct)\n"
|
| 113 |
+
f"Message: {result_data.get('message', 'No message received.')}"
|
| 114 |
+
)
|
| 115 |
+
print("Submission successful.")
|
| 116 |
+
results_df = pd.DataFrame(results_log)
|
| 117 |
+
return final_status, results_df
|
| 118 |
+
except requests.exceptions.HTTPError as e:
|
| 119 |
+
error_detail = f"Server responded with status {e.response.status_code}."
|
| 120 |
+
try:
|
| 121 |
+
error_json = e.response.json()
|
| 122 |
+
error_detail += f" Detail: {error_json.get('detail', e.response.text)}"
|
| 123 |
+
except requests.exceptions.JSONDecodeError:
|
| 124 |
+
error_detail += f" Response: {e.response.text[:500]}"
|
| 125 |
+
status_message = f"Submission Failed: {error_detail}"
|
| 126 |
+
print(status_message)
|
| 127 |
+
results_df = pd.DataFrame(results_log)
|
| 128 |
+
return status_message, results_df
|
| 129 |
+
except requests.exceptions.Timeout:
|
| 130 |
+
status_message = "Submission Failed: The request timed out."
|
| 131 |
+
print(status_message)
|
| 132 |
+
results_df = pd.DataFrame(results_log)
|
| 133 |
+
return status_message, results_df
|
| 134 |
+
except requests.exceptions.RequestException as e:
|
| 135 |
+
status_message = f"Submission Failed: Network error - {e}"
|
| 136 |
+
print(status_message)
|
| 137 |
+
results_df = pd.DataFrame(results_log)
|
| 138 |
+
return status_message, results_df
|
| 139 |
+
except Exception as e:
|
| 140 |
+
status_message = f"An unexpected error occurred during submission: {e}"
|
| 141 |
+
print(status_message)
|
| 142 |
+
results_df = pd.DataFrame(results_log)
|
| 143 |
+
return status_message, results_df
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
# --- Build Gradio Interface using Blocks ---
|
| 147 |
+
with gr.Blocks() as demo:
|
| 148 |
+
gr.Markdown("# Basic Agent Evaluation Runner")
|
| 149 |
+
gr.Markdown(
|
| 150 |
+
"""
|
| 151 |
+
**Instructions:**
|
| 152 |
+
1. Please clone this space, then modify the code to define your agent's logic, the tools, the necessary packages, etc ...
|
| 153 |
+
2. Log in to your Hugging Face account using the button below. This uses your HF username for submission.
|
| 154 |
+
3. Click 'Run Evaluation & Submit All Answers' to fetch questions, run your agent, submit answers, and see the score.
|
| 155 |
+
---
|
| 156 |
+
**Disclaimers:**
|
| 157 |
+
Once clicking on the "submit button, it can take quite some time ( this is the time for the agent to go through all the questions).
|
| 158 |
+
This space provides a basic setup and is intentionally sub-optimal to encourage you to develop your own, more robust solution. For instance for the delay process of the submit button, a solution could be to cache the answers and submit in a seperate action or even to answer the questions in async.
|
| 159 |
+
"""
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
gr.LoginButton()
|
| 163 |
+
|
| 164 |
+
run_button = gr.Button("Run Evaluation & Submit All Answers")
|
| 165 |
+
|
| 166 |
+
status_output = gr.Textbox(label="Run Status / Submission Result", lines=5, interactive=False)
|
| 167 |
+
# Removed max_rows=10 from DataFrame constructor
|
| 168 |
+
results_table = gr.DataFrame(label="Questions and Agent Answers", wrap=True)
|
| 169 |
+
|
| 170 |
+
run_button.click(
|
| 171 |
+
fn=run_and_submit_all,
|
| 172 |
+
outputs=[status_output, results_table]
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
if __name__ == "__main__":
|
| 176 |
+
print("\n" + "-"*30 + " App Starting " + "-"*30)
|
| 177 |
+
# Check for SPACE_HOST and SPACE_ID at startup for information
|
| 178 |
+
space_host_startup = os.getenv("SPACE_HOST")
|
| 179 |
+
space_id_startup = os.getenv("SPACE_ID") # Get SPACE_ID at startup
|
| 180 |
+
|
| 181 |
+
if space_host_startup:
|
| 182 |
+
print(f"✅ SPACE_HOST found: {space_host_startup}")
|
| 183 |
+
print(f" Runtime URL should be: https://{space_host_startup}.hf.space")
|
| 184 |
+
else:
|
| 185 |
+
print("ℹ️ SPACE_HOST environment variable not found (running locally?).")
|
| 186 |
+
|
| 187 |
+
if space_id_startup: # Print repo URLs if SPACE_ID is found
|
| 188 |
+
print(f"✅ SPACE_ID found: {space_id_startup}")
|
| 189 |
+
print(f" Repo URL: https://huggingface.co/spaces/{space_id_startup}")
|
| 190 |
+
print(f" Repo Tree URL: https://huggingface.co/spaces/{space_id_startup}/tree/main")
|
| 191 |
+
else:
|
| 192 |
+
print("ℹ️ SPACE_ID environment variable not found (running locally?). Repo URL cannot be determined.")
|
| 193 |
+
|
| 194 |
+
print("-"*(60 + len(" App Starting ")) + "\n")
|
| 195 |
+
|
| 196 |
+
print("Launching Gradio Interface for Basic Agent Evaluation...")
|
| 197 |
+
demo.launch(debug=True, share=False)
|
prompts.yaml
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
system_prompt: |
|
| 2 |
+
You are a methodical and highly precise AI assistant, acting as an expert problem-solver.
|
| 3 |
+
Your primary goal is to fulfill user requests accurately by breaking them down into logical steps and using the available tools effectively.
|
| 4 |
+
|
| 5 |
+
Think steb-by-step and call one tool at a time. After receiving the Observation (the tool's result) re-read teh user task and decide if you need to call another tool or if you have enough information to provide the final answer.
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
**Task Context:**
|
| 9 |
+
- The Task ID for this request is: {task_id}
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
### Core Operating Principles
|
| 13 |
+
1. **Deconstruct the Task**: Before any action, break down the user's request into its core components:
|
| 14 |
+
- What is the ultimate goal? (e.g., create a categorized list, return page numbers, return a count)
|
| 15 |
+
- What are the explicit constraints and requirements? (e.g., "botanical fruits", "alphabetize", "comma-separated")
|
| 16 |
+
- What specific information is needed? (e.g., the botanical classification of peanuts, bell peppers, page numbers, etc.)
|
| 17 |
+
2. **Doubt and Verify - The Tool-First Mandate**: This is your most important rule.
|
| 18 |
+
- **Default to Tools**: For any piece of factual information—no matter how simple it seems—you must assume your internal knowledge could be outdated or contextually wrong.
|
| 19 |
+
- **Trigger for `web_searcher`**: If a task requires definitions, classifications, facts, up-to-date information, or any knowledge that could be subject to precise, non-common-sense rules (like the botanical difference between fruit and vegetable), you **MUST** use the `web_searcher` tool to verify the information.
|
| 20 |
+
- **Do not "guess" or "recall". Always "fetch" and "verify".**
|
| 21 |
+
3. **Execute and Synthesize**:
|
| 22 |
+
- Use the right tool for the job based on the `TOOLS INVENTORY`.
|
| 23 |
+
- Once you have gathered all necessary, verified information from your tools, synthesize it to construct the final answer.
|
| 24 |
+
- **Handoff for Formatting**: Your final output should be a clear, preliminary answer. **DO NOT** add `FINAL ANSWER:` formatting. Another system will handle that.
|
| 25 |
+
|
| 26 |
+
### Tool Selection Guide
|
| 27 |
+
You MUST follow this decision-making hierarchy in order. Evaluate Rule 1 first. If it doesn't apply, evaluate Rule 2, and so on.
|
| 28 |
+
**Rule 1: Is the task a LOGIC, MATH, or DATA PROCESSING problem with data PROVIDED IN THE PROMPT?**
|
| 29 |
+
- **Trigger**: The user provides all necessary data (like a table, a list, a set of numbers) directly in their question and asks for a result that requires systematic analysis, logic, or multi-step calculations.
|
| 30 |
+
- **Workflow**:
|
| 31 |
+
1. Your **first and only** starting action is to call the `code_writer_tool`.
|
| 32 |
+
2. You must write a **complete, self-contained Python script**.
|
| 33 |
+
3. **CRITICAL**: The script **MUST** print the final answer to standard output using a `print()` statement. The result must be the **only thing** printed. Do not print intermediate steps. If you calculate a result and do not print it, the entire task will fail.
|
| 34 |
+
4. After the script is written, use the `code_tool` to execute it.
|
| 35 |
+
5. The `stdout` from the execution result is the final answer.
|
| 36 |
+
|
| 37 |
+
**Example of a good final line in a script:**
|
| 38 |
+
`print(sorted_list)`
|
| 39 |
+
`print(final_count)`
|
| 40 |
+
`print(','.join(result_set))`
|
| 41 |
+
|
| 42 |
+
**Rule 2: Does the task involve a PROVIDED FILE (non-YouTube)?**
|
| 43 |
+
- **Trigger**: The user explicitly mentions an "attached file".
|
| 44 |
+
- **Workflow**:
|
| 45 |
+
1. Your **first** action is to call the `download_tool`.
|
| 46 |
+
2. After downloading, analyze the file content to decide the next appropriate tool (`tabular_tool`, `audio_tool`, etc.).
|
| 47 |
+
|
| 48 |
+
**Rule 3: Does the task involve a YOUTUBE URL?**
|
| 49 |
+
- **Trigger**: The user provides a `youtube.com` URL.
|
| 50 |
+
- **Workflow**:
|
| 51 |
+
1. Your **first** action is to call the `youtube_info_tool`.
|
| 52 |
+
2. Analyze its output to decide the next step.
|
| 53 |
+
|
| 54 |
+
**Rule 4: Does the task require EXTERNAL KNOWLEDGE or web search?**
|
| 55 |
+
- **Trigger**: The question is about facts, events, people, or any topic that requires up-to-date, external information and does not fit the rules above.
|
| 56 |
+
- **Workflow**: Your action is to call the `web_searcher` tool with a clear, comprehensive query.
|
| 57 |
+
|
| 58 |
+
**Rule 5: Is the task a very SIMPLE, one-shot calculation?**
|
| 59 |
+
- **Trigger**: A direct request like "add 2 and 3" or "what is 5 to the power of 3".
|
| 60 |
+
- **Workflow**: Call the single, appropriate math tool (`add_tool`, `power_tool`, etc.).
|
| 61 |
+
|
| 62 |
+
### TOOLS INVENTORY
|
| 63 |
+
(Your TOOLS INVENTORY section can remain mostly the same, but let's sharpen some descriptions)
|
| 64 |
+
|
| 65 |
+
-- **Helper Tools** --
|
| 66 |
+
- Tool Name: sort_tool
|
| 67 |
+
Description: Sort a list of numbers (in numeric order) or strings (in alphabetical order). Use this tool whenever you need to sort a list of items.
|
| 68 |
+
Arguments:
|
| 69 |
+
- items: List[Union[float, str]]: (The list of items to be sorted. The list must contain either all numbers or all strings).
|
| 70 |
+
- order: string (optional): (The sort order. Valid values are 'ascending' (default) or 'descending').
|
| 71 |
+
Output: sorted_items: Union[List[Union[float, str]], str]: (The sorted list).
|
| 72 |
+
|
| 73 |
+
--- **Web Search Tools** ---
|
| 74 |
+
- Tool Name: web_search_tool
|
| 75 |
+
Description: A powerful tool that delegates complex research tasks to a specialized agent. Use it to find up-to-date information, analyze scientific papers, and browse web pages to answer a user's question comprehensively.
|
| 76 |
+
Arguments: task: string (The users's task to solve).
|
| 77 |
+
Output: final_answer: string (The web search final answer).
|
| 78 |
+
|
| 79 |
+
--- **Youtube Video Tools** ---
|
| 80 |
+
- Tool Name: youtube_info_tool
|
| 81 |
+
Description: The first step for ANY YouTube task. Takes a URL and downloads the video/audio, and gets the transcript.
|
| 82 |
+
Arguments: youtube_url: string.
|
| 83 |
+
Output: video_info: dict (Contains 'transcript', 'audio_filename', 'video_filename').
|
| 84 |
+
|
| 85 |
+
- Tool Name: youtube_frame_tool
|
| 86 |
+
Description: Analyzes a downloaded video's frames and transcript to answer a specific question about its content. Requires `video_filename` and `transcript` from `youtube_info_tool`.
|
| 87 |
+
Arguments: filename: string, transcript: string, user_question: string.
|
| 88 |
+
Output: analysis_summary: string.
|
| 89 |
+
|
| 90 |
+
--- **Analyze File Tools** ---
|
| 91 |
+
- Tool Name: download_tool
|
| 92 |
+
Description: The first step for ANY task involving a provided file (non-YouTube). Downloads the file using the task_id.
|
| 93 |
+
Arguments: task_id: string.
|
| 94 |
+
Output: filename: string.
|
| 95 |
+
|
| 96 |
+
- Tool Name: tabular_tool
|
| 97 |
+
Description: Reads and analyzes tabular data ONLY after a downloaded file (CSV, XLSX).
|
| 98 |
+
Arguments: filename: string.
|
| 99 |
+
Output: formatted_output: string.
|
| 100 |
+
|
| 101 |
+
- Tool Name: audio_tool
|
| 102 |
+
Description: Transcribes a downloaded audio file (MP3, WAV).
|
| 103 |
+
Arguments: filename: string.
|
| 104 |
+
Output: transcript: string.
|
| 105 |
+
|
| 106 |
+
- Tool Name: image_tool
|
| 107 |
+
Description: Encodes a downloaded image file into base64 for analysis.
|
| 108 |
+
Arguments:
|
| 109 |
+
- filename: string
|
| 110 |
+
- user_question: string
|
| 111 |
+
Output: description: string
|
| 112 |
+
|
| 113 |
+
--- **Code Generation & Execution Tools** ---
|
| 114 |
+
- Tool Name: code_writer_tool
|
| 115 |
+
Description: Writes a given string of Python code into a local file. This is the first step for any task that you decide to solve by writing code. The filename should always end with .py.
|
| 116 |
+
Arguments:
|
| 117 |
+
- code: string
|
| 118 |
+
- task_id: string
|
| 119 |
+
Output: local_filename: string
|
| 120 |
+
|
| 121 |
+
- Tool Name: code_tool
|
| 122 |
+
Description: Securely executes a downloaded code file.
|
| 123 |
+
Arguments: filename: string.
|
| 124 |
+
Output: execution_result: dict.
|
| 125 |
+
|
| 126 |
+
--- **Math Tools** ---
|
| 127 |
+
- Tool Name: add_tool
|
| 128 |
+
Description: Calculates the sum of a list of two or more numbers.
|
| 129 |
+
Arguments: numbers: List[float] (A list of numbers to add).
|
| 130 |
+
Output: result: float
|
| 131 |
+
|
| 132 |
+
- Tool Name: multiply_tool
|
| 133 |
+
Description: Calculates the product of a list of two or more numbers.
|
| 134 |
+
Arguments: numbers: List[float] (A list of numbers to multiply).
|
| 135 |
+
Output: result: float
|
| 136 |
+
|
| 137 |
+
- Tool Name: subtract_tool
|
| 138 |
+
Description: Performs the subtraction of two numbers (minuend - subtrahend).
|
| 139 |
+
Arguments: minuend: float, subtrahend: float.
|
| 140 |
+
Output: result: float
|
| 141 |
+
|
| 142 |
+
- Tool Name: divide_tool
|
| 143 |
+
Description: Performs the division of two numbers (dividend / divisor). Returns an error message if the divisor is zero.
|
| 144 |
+
Arguments: dividend: float, divisor: float.
|
| 145 |
+
Output: Union[float, string] (Either the numeric result or an error message).
|
| 146 |
+
|
| 147 |
+
- Tool Name: modulus_tool
|
| 148 |
+
Description: Calculates the remainder of a division (dividend % divisor). Also known as the modulus operator.
|
| 149 |
+
Arguments: dividend: float, divisor: float.
|
| 150 |
+
Output: Union[float, string] (Either the numeric result or an error message).
|
| 151 |
+
|
| 152 |
+
- Tool Name: power_tool
|
| 153 |
+
Description: Raises a number (base) to the power of an exponent.
|
| 154 |
+
Arguments: base: float, exponent: float.
|
| 155 |
+
Output: result: float
|
| 156 |
+
|
| 157 |
+
- Tool Name: square_root_tool
|
| 158 |
+
Description: Calculates the square root of a non-negative number.
|
| 159 |
+
Arguments: number: float.
|
| 160 |
+
Output: Union[float, string] (Either the numeric result or an error message if the number is negative).
|
| 161 |
+
|
| 162 |
+
system_prompt_old: |
|
| 163 |
+
You are a meticulous and methodical AI assistant, acting as an expert problem-solver.
|
| 164 |
+
Your primary goal is to fulfill user requests accurately by breaking them down into logical steps and using the available tools effectively.
|
| 165 |
+
|
| 166 |
+
CORE DIRECTIVES AND WORKFLOW
|
| 167 |
+
---------------
|
| 168 |
+
1. **Analyze and Plan First**: Before taking any action, deeply analyze the user's request. Decompose complex problems into a clear, step-by-step plan. State your reasoning for the chosen plan.
|
| 169 |
+
2. **Execute One Step at a Time**: Call one tool at a time. After receiving the Observation (the tool's result), re-evaluate your plan. Decide if you need to call another tool or if you have enough information to provide the final answer.
|
| 170 |
+
3. **Strict Tool Adherence**: You must use the provided tools for all calculations, file operations and code executions. Do not perform these tasks yourself. If a tool can perform a task, you must use it.
|
| 171 |
+
4. **CRITICAL WORKFLOW: Handling Task Files**:
|
| 172 |
+
The Task ID for this request is: {task_id}
|
| 173 |
+
Rule 1: IGNORE USER-MENTIONED FILENAMES. The user might say "analyze Homework.mp3" or "read sales.xlsx". You MUST IGNORE these names. They are labels, not real file paths.
|
| 174 |
+
Rule 2: ALWAYS START WITH download_tool. If the task involves a file, your FIRST and ONLY starting action is to call the download_tool.
|
| 175 |
+
Rule 3: USE THE PROVIDED TASK ID. The only input you will ever use for download_tool is the task_id provided above.
|
| 176 |
+
Correct Workflow Example:
|
| 177 |
+
1. Thought: The user mentioned a file. My instructions state I must call download_tool with the provided task_id.
|
| 178 |
+
2. Action: Call download_tool(task_id="{task_id}").
|
| 179 |
+
3. Observation: The tool returns a filename, for example: 7bd855d8-463d-4ed5-93ca-5fe35145f733.xlsx.
|
| 180 |
+
4. Thought: Now I have a valid filename. I will use this filename to call the correct analysis file tool.
|
| 181 |
+
5. Action: For example call tabular_tool(filename="7bd855d8-463d-4ed5-93ca-5fe35145f733.xlsx").
|
| 182 |
+
5. **CRITICAL WORKFLOW: Analyzing Images**:
|
| 183 |
+
Analyzing an image is a **3-Steps process**. You must follow it exactly.
|
| 184 |
+
Step 1: Download. Call 'download_tool' with the provided {task_id} to get the 'filename'.
|
| 185 |
+
Step 2: Prepare. Call 'image_tool' using the 'filename' from the previous step. This tool will return a string containing the base64 encoded image data.
|
| 186 |
+
Step 3: Analyze. Once you have the image data from Step 2, you can answer the user's question about the image. You now have "seen" the image and can describe its content, count objects, read text, etc. Do not call any more tools unless you need to perform a separate task or calculation on the result.
|
| 187 |
+
6. **CRITICAL WORKFLOW: Analyzing Youtube Videos**
|
| 188 |
+
This is a multi-step process. You must follow the priority order to be efficient.
|
| 189 |
+
Step 1: Gather Resources. ALWAYS start by calling 'youtube_info_tool' with the user's YouTube URL. This will give you a 'transcript', an 'audio_path' and a 'video_path'.
|
| 190 |
+
Step 2: Decide Next Action based on Priority:
|
| 191 |
+
- Priority 1 (Official Transcript): If the 'transcript' field is not empty, analyze its content first. If you can answer the user's question with the transcript alone, do so and finish.
|
| 192 |
+
- Priority 2 (Visual Frame Analysis): If the question is purely visual (e.g., "what color is...") OR if both text and audio analysis fail to answer the question, call 'youtube_frame_tool'. You MUST provide both the video 'filename', the title, description and full 'transcript' you have gathered from 'youtube_info_tool', and the original 'user_question' to this tool.
|
| 193 |
+
7. **Interpreting Code Execution**: When you use the 'code_tool' to execute programming code, you will receive a dictionary.
|
| 194 |
+
First, check the 'stderr'. If it contains text, the code failed. Analyze the error to understand why.
|
| 195 |
+
Second, if 'stderr' is empty, check the 'stdout'. This contains the program's output and likely holds the answer to the user's question.
|
| 196 |
+
8. **CRITICAL WORKFLOW: Web Research**: For questions that are complex, require web search, require up-to-date information, or ask about topics beyond common knowledge, you MUST use the 'web_search_tool'. Provide it with a clear and comprehensive question. Do not try to answer these questions from your own knowledge.
|
| 197 |
+
9. **Handle Errors Gracefully**: If a tool returns an error message (e.g., "Error: Cannot divide by zero"), do not proceed blindly. Report the specific error to the user in a clear, helpful way as part of your final answer.
|
| 198 |
+
10. **CRITICAL: Handoff for Final Formatting**: Once you have gathered all the information and tool results needed to fully answer the user's question, your final step is to synthesize this information into a clear, preliminary answer. You will then pass this preliminary answer to the dedicated formatting node. **DO NOT format the answer yourself with 'FINAL ANSWER:'.** Simply state the facts you have found.
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
TOOLS INVENTORY
|
| 202 |
+
---------------
|
| 203 |
+
You have access to the following tools:
|
| 204 |
+
|
| 205 |
+
--- **Helper Tools** ---
|
| 206 |
+
- Tool Name: sort_tool
|
| 207 |
+
Description: Sort a list of numbers (in numeric order) or strings (in alphabetical order). Use this tool whenever you need to sort a list of items.
|
| 208 |
+
Arguments:
|
| 209 |
+
- items: List[Union[float, str]]: (The list of items to be sorted. The list must contain either all numbers or all strings).
|
| 210 |
+
- order: string (optional): (The sort order. Valid values are 'ascending' (default) or 'descending').
|
| 211 |
+
Output: sorted_items: Union[List[Union[float, str]], str]: (The sorted list).
|
| 212 |
+
|
| 213 |
+
- Tool Name: download_tool
|
| 214 |
+
Description: Downloads the file associated with a task. This is ALWAYS the first step for any task involving a file. It uses the task_id to find and download the correct file.
|
| 215 |
+
Arguments: task_id: string (The unique ID for the current task, e.g., "7bd855d8-463d-4ed5-93ca-5fe35145f733").
|
| 216 |
+
Output: filename: string (The exact filename of the downloaded file, e.g., 7bd855d8-463d-4ed5-93ca-5fe35145f733.xlsx). This output is the only valid filename for subsequent tools.
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
--- **Web Search Tools** ---
|
| 220 |
+
- Tool Name: web_search_tool
|
| 221 |
+
Description: A powerful tool that delegates complex research tasks to a specialized agent. Use it to find up-to-date information, analyze scientific papers, and browse web pages to answer a user's question comprehensively.
|
| 222 |
+
Arguments: query: string (A clear, detailed question that defines the research task).
|
| 223 |
+
Output: research_report: string (A complete report summarizing the findings of the research).
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
--- **File Tools** ---
|
| 227 |
+
- Tool Name: tabular_tool
|
| 228 |
+
Description: Reads and provides a summary of a local tabular data file (CSV, XLSX, XLS). It returns its content as a formatted string.
|
| 229 |
+
Arguments: filename: string (The filename of the CSV, XLSX, or XLS file to analyze).
|
| 230 |
+
Output: formatted_output: string (A formatted string containing the file's data, or an error message in case of issues.).
|
| 231 |
+
|
| 232 |
+
- Tool Name: audio_tool
|
| 233 |
+
Description: Transcribes a local audio file into text using the OpenAI Whisper model. Use this tool whenever you need to understand the content of an audio file (e.g., MP3, WAV, M4A). This is the primary tool for analyzing spoken content.
|
| 234 |
+
Arguments: filename: string (The filename of the MP3, WAV or M4A file to transcribe).
|
| 235 |
+
Output: transcript: string (The text content of the audio file).
|
| 236 |
+
|
| 237 |
+
- Tool Name: image_tool
|
| 238 |
+
Description: Prepares a local image file for visual analysis by a multimodal model. It reads the image and encodes it into base64 format. This is the **mandatory second step** for any image analysis task, right after downloading the file.
|
| 239 |
+
Arguments:
|
| 240 |
+
- filename: string (The filename of the image file to prepare).
|
| 241 |
+
- user_question: string (The original user question to guide the visual analysis).
|
| 242 |
+
Output: description: string (A text summary answering the question based on visual evidence).
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
--- **Youtube Video Tools** ---
|
| 246 |
+
- Tool Name: youtube_info_tool
|
| 247 |
+
Description: Gathers all available resources from a YouTube URL, including the official transcript, and downloads the audio and video files locally. This is the mandatory first step for any YouTube-related task.
|
| 248 |
+
Arguments: youtube_url: string (The full URL of the YouTube video).
|
| 249 |
+
Output: video_info: dict (A dictionary containing 'transcript', 'audio_filename', and 'video_filename').
|
| 250 |
+
|
| 251 |
+
- Tool Name: youtube_frame_tool
|
| 252 |
+
Description: Analyzes video content by combining visual information from frames with the provided title, description andtranscript to answer a specific user question. Use this as a last resort if text/audio analysis is not enough.
|
| 253 |
+
Arguments:
|
| 254 |
+
- filenam: string (The filename of the video file).
|
| 255 |
+
- title: string (The title of the video).
|
| 256 |
+
- description: string (A brief description of the video).
|
| 257 |
+
- transcript: string (The full text transcript of the video).
|
| 258 |
+
- user_question: string (The original user question to guide the visual analysis).
|
| 259 |
+
- sample_rate_seconds: int (optional) (How often to sample a frame. Defaults to 3 seconds).
|
| 260 |
+
- Output: analysis_summary: string (A text summary answering the question based on visual evidence).
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
--- **Code Execution Tools** ---
|
| 264 |
+
- Tool Name: code_tool
|
| 265 |
+
Description: Executes a local file script in a secure, isolated environment with a timeout. Use this to determine the output or behavior of a provided code file.
|
| 266 |
+
Arguments:
|
| 267 |
+
- filename: string: (The filename of the code file to execute).
|
| 268 |
+
- timeout_seconds: int (optional): (Maximum execution time in seconds. Defaults to 10).
|
| 269 |
+
Output: execution_result: dict: A dictionary containing three keys:
|
| 270 |
+
- return_code: 0 for successful execution, non-zero for an error.
|
| 271 |
+
- stdout: The standard output (text from 'print' statements). This is often where the final answer is.
|
| 272 |
+
- stderr: The standard error output (error messages and tracebacks). If this is not empty, the code likely crashed.
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
--- **Math Tools** ---
|
| 276 |
+
- Tool Name: add_tool
|
| 277 |
+
Description: Calculates the sum of a list of two or more numbers.
|
| 278 |
+
Arguments: numbers: List[float] (A list of numbers to add).
|
| 279 |
+
Output: result: float
|
| 280 |
+
|
| 281 |
+
- Tool Name: multiply_tool
|
| 282 |
+
Description: Calculates the product of a list of two or more numbers.
|
| 283 |
+
Arguments: numbers: List[float] (A list of numbers to multiply).
|
| 284 |
+
Output: result: float
|
| 285 |
+
|
| 286 |
+
- Tool Name: subtract_tool
|
| 287 |
+
Description: Performs the subtraction of two numbers (minuend - subtrahend).
|
| 288 |
+
Arguments: minuend: float, subtrahend: float.
|
| 289 |
+
Output: result: float
|
| 290 |
+
|
| 291 |
+
- Tool Name: divide_tool
|
| 292 |
+
Description: Performs the division of two numbers (dividend / divisor). Returns an error message if the divisor is zero.
|
| 293 |
+
Arguments: dividend: float, divisor: float.
|
| 294 |
+
Output: Union[float, string] (Either the numeric result or an error message).
|
| 295 |
+
|
| 296 |
+
- Tool Name: mosulus_tool
|
| 297 |
+
Description: Calculates the remainder of a division (dividend % divisor). Also known as the modulus operator.
|
| 298 |
+
Arguments: dividend: float, divisor: float.
|
| 299 |
+
Output: Union[float, string] (Either the numeric result or an error message).
|
| 300 |
+
|
| 301 |
+
- Tool Name: power_tool
|
| 302 |
+
Description: Raises a number (base) to the power of an exponent.
|
| 303 |
+
Arguments: base: float, exponent: float.
|
| 304 |
+
Output: result: float
|
| 305 |
+
|
| 306 |
+
- Tool Name: square_root_tool
|
| 307 |
+
Description: Calculates the square root of a non-negative number.
|
| 308 |
+
Arguments: number: float.
|
| 309 |
+
Output: Union[float, string] (Either the numeric result or an error message if the number is negative).
|
| 310 |
+
|
| 311 |
+
final_prompt: |
|
| 312 |
+
Your ONLY job is to take the provided text and format it precisely according to the rules.
|
| 313 |
+
|
| 314 |
+
**RULES:**
|
| 315 |
+
1. Your entire output MUST start with the prefix 'FINAL ANSWER: '.
|
| 316 |
+
2. After the prefix, provide only the most direct and concise answer possible, extracted from the input text.
|
| 317 |
+
3. Do not add any extra words, simbols, explanations, or introductory phrases.
|
| 318 |
+
4. PROVIDE answer without abbreviations.
|
| 319 |
+
5. If the task requires a number or a count, PROVIDE only the number in digits.
|
| 320 |
+
6. If the task requires a list, PROVIDE just the ordered list.
|
| 321 |
+
|
| 322 |
+
Analyze the user's question below.
|
| 323 |
+
You must be extremely concise and extract only the core answer for .
|
| 324 |
+
**--- USER QUESTION ---**
|
| 325 |
+
{user_question}
|
| 326 |
+
**---------------------**
|
| 327 |
+
|
| 328 |
+
**--- INPUT TEXT TO FORMAT ---**
|
| 329 |
+
{llm_response}
|
| 330 |
+
**----------------------------**
|
| 331 |
+
|
| 332 |
+
**Examples of CORRECT formatting:**
|
| 333 |
+
- Input: "The city name is St. Petersburg" -> Output: FINAL ANSWER: Saint Petersburg
|
| 334 |
+
- Input: "The result of the sum is 228." -> Output: FINAL ANSWER: 228
|
| 335 |
+
- Input: "At least there are three distinct bird species." -> Output: FINAL ANSWER: 3
|
| 336 |
+
- Input: "After analyzing the document, I found that the capital of Italy is Rome." -> Output: FINAL ANSWER: Rome
|
| 337 |
+
- Input: "The total sales is $109,200.00**." -> Output: FINAL ANSWER: 109200.00
|
| 338 |
+
- Input: "The three founders were Steve Jobs, Steve Wozniak, and Ronald Wayne." -> Output: FINAL ANSWER: Steve Jobs, Steve Wozniak, Ronald Wayne
|
| 339 |
+
|
| 340 |
+
Now, format the provided input text.
|
tools.py
ADDED
|
@@ -0,0 +1,747 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import subprocess
|
| 3 |
+
import base64
|
| 4 |
+
import mimetypes
|
| 5 |
+
from typing import List, Union
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
import math
|
| 8 |
+
import requests
|
| 9 |
+
import pandas as pd
|
| 10 |
+
import cv2
|
| 11 |
+
from pytubefix import YouTube
|
| 12 |
+
from youtube_transcript_api import YouTubeTranscriptApi
|
| 13 |
+
from openai import OpenAI, APIError
|
| 14 |
+
|
| 15 |
+
from langchain_core.tools import tool
|
| 16 |
+
from langchain_community.document_loaders import CSVLoader
|
| 17 |
+
|
| 18 |
+
from web_search_agent import web_search_graph
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
#-----------------------------------------------------------------------------
|
| 22 |
+
# Helper Tools
|
| 23 |
+
#-----------------------------------------------------------------------------
|
| 24 |
+
@tool("sort_tool")
|
| 25 |
+
def sort_tool(items: List[Union[float, str]], order: str = "ascending") -> Union[List[Union[float, str]], str]:
|
| 26 |
+
"""
|
| 27 |
+
Sort a list of numbers (in numeric order) or strings (in alphabetical order).
|
| 28 |
+
Use this tool whenever you need to sort a list of items.
|
| 29 |
+
|
| 30 |
+
Args:
|
| 31 |
+
items (List[Union[float, str]]): The list of items to sort. The list must contain either only numbers or only strings.
|
| 32 |
+
order (str, optional): The sorting order. Valid values: 'ascending' or 'descending'. Default is 'ascending'.
|
| 33 |
+
|
| 34 |
+
Returns:
|
| 35 |
+
Union[List[Union[float, str]], str]: The sorted list if successful, or an error message string in case of failure.
|
| 36 |
+
"""
|
| 37 |
+
print(f"--- ESECUZIONE DEL TOOL 'sort_list' CON INPUT: items={items}, order='{order}' ---")
|
| 38 |
+
|
| 39 |
+
# 1. Controlla se la lista è vuota
|
| 40 |
+
if not items:
|
| 41 |
+
return []
|
| 42 |
+
|
| 43 |
+
# 2. Valida l'argomento 'order' e imposta il flag per l'ordinamento
|
| 44 |
+
normalized_order = order.lower().strip()
|
| 45 |
+
if normalized_order == "ascending":
|
| 46 |
+
reverse_flag = False
|
| 47 |
+
elif normalized_order == "descending":
|
| 48 |
+
reverse_flag = True
|
| 49 |
+
else:
|
| 50 |
+
return "Error: The 'order' value is invalid. Allowed values are 'ascending' or 'descending'."
|
| 51 |
+
|
| 52 |
+
# 3. Esegui l'ordinamento, gestendo possibili errori di tipo
|
| 53 |
+
try:
|
| 54 |
+
# Usiamo la funzione built-in sorted(), che è molto efficiente
|
| 55 |
+
# e gestisce correttamente sia numeri che stringhe (ma non mischiati).
|
| 56 |
+
sorted_items = sorted(items, reverse=reverse_flag)
|
| 57 |
+
return sorted_items
|
| 58 |
+
except TypeError:
|
| 59 |
+
# Questa eccezione viene sollevata se si cerca di ordinare una lista
|
| 60 |
+
# con tipi non confrontabili, es. [1, "mela", 3]
|
| 61 |
+
return "Error: The list contains incompatible data types that cannot be sorted together (e.g., numbers and strings)."
|
| 62 |
+
except Exception as e:
|
| 63 |
+
return f"An unexpected error occurred during sorting: {e}"
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
@tool("download_tool")
|
| 67 |
+
def download_tool(task_id: str) -> Union[str, str]:
|
| 68 |
+
"""
|
| 69 |
+
Download a file associated with a task_id from a predefined URL.
|
| 70 |
+
The file is saved in the 'uploads' directory with the name 'task_id.ext',
|
| 71 |
+
where the extension (.ext) is determined dynamically from the server response.
|
| 72 |
+
|
| 73 |
+
Args:
|
| 74 |
+
task_id (str): The unique identifier for the task and the file to download.
|
| 75 |
+
|
| 76 |
+
Returns:
|
| 77 |
+
Union[str, str]: The filename of the downloaded file if successful or an error message string in case of failure.
|
| 78 |
+
"""
|
| 79 |
+
print(f"--- ESECUZIONE DEL TOOL 'download_file' CON INPUT: task_id={task_id} ---")
|
| 80 |
+
|
| 81 |
+
# 1. Impostazioni di base
|
| 82 |
+
BASE_URL = "https://agents-course-unit4-scoring.hf.space/files/"
|
| 83 |
+
UPLOADS_DIR = "./uploads/"
|
| 84 |
+
|
| 85 |
+
# 2. Assicurarsi che la directory di destinazione esista
|
| 86 |
+
try:
|
| 87 |
+
os.makedirs(UPLOADS_DIR, exist_ok=True)
|
| 88 |
+
except OSError as e:
|
| 89 |
+
error_message = f"Error: Unable to create the destination directory '{UPLOADS_DIR}'. Details: {e}"
|
| 90 |
+
print(error_message)
|
| 91 |
+
return error_message
|
| 92 |
+
|
| 93 |
+
# 3. Eseguire la richiesta HTTP per scaricare il file
|
| 94 |
+
url = f"{BASE_URL}{task_id}"
|
| 95 |
+
try:
|
| 96 |
+
# Usare 'stream=True' è una buona pratica per scaricare file
|
| 97 |
+
with requests.get(url, stream=True, timeout=30) as response:
|
| 98 |
+
# Controlla se la richiesta ha avuto successo (es. status code 200)
|
| 99 |
+
response.raise_for_status()
|
| 100 |
+
|
| 101 |
+
# 4. Estrarre il nome del file originale per ottenere l'estensione
|
| 102 |
+
content_disposition = response.headers.get('content-disposition')
|
| 103 |
+
if not content_disposition:
|
| 104 |
+
error_message = "Error: The server response does not contain the 'content-disposition' header to get the file name."
|
| 105 |
+
print(error_message)
|
| 106 |
+
return error_message
|
| 107 |
+
|
| 108 |
+
# Parsing dell'header per trovare il filename. Es: 'attachment; filename="nomefile.ext"'
|
| 109 |
+
parts = content_disposition.split(';')
|
| 110 |
+
filename_part = next((part for part in parts if 'filename=' in part), None)
|
| 111 |
+
|
| 112 |
+
if not filename_part:
|
| 113 |
+
error_message = "Error: Unable to find 'filename' in the 'content-disposition' header."
|
| 114 |
+
print(error_message)
|
| 115 |
+
return error_message
|
| 116 |
+
|
| 117 |
+
original_filename = filename_part.split('=')[1].strip().strip('"')
|
| 118 |
+
_, extension = os.path.splitext(original_filename)
|
| 119 |
+
|
| 120 |
+
if not extension:
|
| 121 |
+
error_message = f"Error: Unable to get the file extension from '{original_filename}'."
|
| 122 |
+
print(error_message)
|
| 123 |
+
return error_message
|
| 124 |
+
|
| 125 |
+
# 5. Costruire il percorso di salvataggio e salvare il file
|
| 126 |
+
local_filename = f"{task_id}{extension}"
|
| 127 |
+
local_filepath = os.path.join(UPLOADS_DIR, local_filename)
|
| 128 |
+
|
| 129 |
+
with open(local_filepath, 'wb') as f:
|
| 130 |
+
# Scrive il contenuto a pezzi per gestire file di grandi dimensioni
|
| 131 |
+
for chunk in response.iter_content(chunk_size=8192):
|
| 132 |
+
f.write(chunk)
|
| 133 |
+
|
| 134 |
+
success_message = f"File scaricato con successo e salvato in: {local_filepath}"
|
| 135 |
+
print(success_message)
|
| 136 |
+
return local_filename
|
| 137 |
+
|
| 138 |
+
except requests.exceptions.RequestException as e:
|
| 139 |
+
# Gestisce errori di rete, timeout, DNS, etc.
|
| 140 |
+
error_message = f"Network error occurred while downloading the file: {e}"
|
| 141 |
+
print(error_message)
|
| 142 |
+
return error_message
|
| 143 |
+
except Exception as e:
|
| 144 |
+
# Cattura qualsiasi altra eccezione imprevista
|
| 145 |
+
error_message = f"An unexpected error occurred: {e}"
|
| 146 |
+
print(error_message)
|
| 147 |
+
return error_message
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
#-----------------------------------------------------------------------------
|
| 151 |
+
# Math Tools
|
| 152 |
+
#-----------------------------------------------------------------------------
|
| 153 |
+
@tool("add_tool")
|
| 154 |
+
def add_tool(numbers: List[float]) -> float:
|
| 155 |
+
"""
|
| 156 |
+
Calculate the sum of a list of numbers.
|
| 157 |
+
Use this tool when you need to perform a sum operation on multiple numbers.
|
| 158 |
+
|
| 159 |
+
Args:
|
| 160 |
+
numbers (List[float]): The list of numbers to be summed.
|
| 161 |
+
"""
|
| 162 |
+
print(f"--- ESECUZIONE DEL TOOL 'sum_numbers' CON INPUT: {numbers} ---")
|
| 163 |
+
return sum(numbers)
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
@tool("multiply_tool")
|
| 167 |
+
def multiply_tool(numbers: List[float]) -> float:
|
| 168 |
+
"""
|
| 169 |
+
Calculate the product of a list of numbers.
|
| 170 |
+
Use this tool when you need to multiply two or more numbers together.
|
| 171 |
+
|
| 172 |
+
Args:
|
| 173 |
+
numbers (List[float]): The list of numbers to be multiplied.
|
| 174 |
+
"""
|
| 175 |
+
print(f"--- ESECUZIONE DEL TOOL 'multiply_numbers' CON INPUT: {numbers} ---")
|
| 176 |
+
if not numbers:
|
| 177 |
+
return 0
|
| 178 |
+
return math.prod(numbers)
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
@tool("subtract_tool")
|
| 182 |
+
def subtract_tool(minuend: float, subtrahend: float) -> float:
|
| 183 |
+
"""
|
| 184 |
+
Calculate the subtraction between two numbers (minuend - subtrahend).
|
| 185 |
+
Use this tool to subtract one number from another.
|
| 186 |
+
|
| 187 |
+
Args:
|
| 188 |
+
minuend (float): The number from which to subtract (the first number).
|
| 189 |
+
subtrahend (float): The number to subtract (the second number).
|
| 190 |
+
"""
|
| 191 |
+
print(f"--- ESECUZIONE DEL TOOL 'subtract_numbers' CON INPUT: minuend={minuend}, subtrahend={subtrahend} ---")
|
| 192 |
+
return minuend - subtrahend
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
@tool("divide_tool")
|
| 196 |
+
def divide_tool(dividend: float, divisor: float) -> Union[float, str]:
|
| 197 |
+
"""
|
| 198 |
+
Calculate the division between two numbers (dividend / divisor).
|
| 199 |
+
Also handles the case of division by zero.
|
| 200 |
+
|
| 201 |
+
Args:
|
| 202 |
+
dividend (float): The number to be divided (the numerator).
|
| 203 |
+
divisor (float): The number to divide by (the denominator).
|
| 204 |
+
"""
|
| 205 |
+
print(f"--- ESECUZIONE DEL TOOL 'divide_numbers' CON INPUT: dividend={dividend}, divisor={divisor} ---")
|
| 206 |
+
if divisor == 0:
|
| 207 |
+
return "Error: Division by zero is not allowed."
|
| 208 |
+
return dividend / divisor
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
@tool("modulus_tool")
|
| 212 |
+
def modulus_tool(dividend: float, divisor: float) -> Union[float, str]:
|
| 213 |
+
"""
|
| 214 |
+
Calculate the remainder of the division between two numbers (dividend % divisor).
|
| 215 |
+
Use this tool when asked for the 'remainder' or the 'modulus' of a division.
|
| 216 |
+
|
| 217 |
+
Args:
|
| 218 |
+
dividend (float): The number being divided (the numerator).
|
| 219 |
+
divisor (float): The number by which to divide (the denominator).
|
| 220 |
+
"""
|
| 221 |
+
print(f"--- ESECUZIONE DEL TOOL 'calculate_remainder' CON INPUT: dividend={dividend}, divisor={divisor} ---")
|
| 222 |
+
if divisor == 0:
|
| 223 |
+
return "Error: The divisor cannot be zero for the modulus operation."
|
| 224 |
+
return dividend % divisor
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
@tool("power_tool")
|
| 228 |
+
def power_tool(base: float, exponent: float) -> Union[float, str]:
|
| 229 |
+
"""
|
| 230 |
+
Calculate a number raised to a power (base^exponent).
|
| 231 |
+
Use this tool for exponentiation operations.
|
| 232 |
+
|
| 233 |
+
Args:
|
| 234 |
+
base (float): The base of the operation.
|
| 235 |
+
exponent (float): The exponent to which the base is raised.
|
| 236 |
+
"""
|
| 237 |
+
print(f"--- ESECUZIONE DEL TOOL 'calculate_power' CON INPUT: base={base}, exponent={exponent} ---")
|
| 238 |
+
try:
|
| 239 |
+
# Usiamo math.pow per coerenza e una migliore gestione degli errori
|
| 240 |
+
result = math.pow(base, exponent)
|
| 241 |
+
return result
|
| 242 |
+
except ValueError:
|
| 243 |
+
# Si verifica se, ad esempio, si cerca di calcolare (-4)^(0.5), che produce un numero complesso.
|
| 244 |
+
return "Error: Invalid operation. Ensure that the base and exponent do not result in a complex number (e.g., even root of a negative number)."
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
@tool("square_root_tool")
|
| 248 |
+
def square_root_tool(number: float) -> Union[float, str]:
|
| 249 |
+
"""
|
| 250 |
+
Calculate the square root of a non-negative number.
|
| 251 |
+
Use this tool specifically to compute the square root.
|
| 252 |
+
|
| 253 |
+
Args:
|
| 254 |
+
number (float): The number for which to calculate the square root. Must be >= 0.
|
| 255 |
+
"""
|
| 256 |
+
print(f"--- ESECUZIONE DEL TOOL 'square_root' CON INPUT: number={number} ---")
|
| 257 |
+
if number < 0:
|
| 258 |
+
return "Error: Cannot calculate the square root of a negative number."
|
| 259 |
+
return math.sqrt(number)
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
#-----------------------------------------------------------------------------
|
| 263 |
+
# File Tools
|
| 264 |
+
#-----------------------------------------------------------------------------
|
| 265 |
+
@tool("tabular_tool")
|
| 266 |
+
def tabular_tool(filename: str) -> Union[str, str]:
|
| 267 |
+
"""
|
| 268 |
+
Analyze a local tabular data file (CSV, XLSX, XLS) and return its content
|
| 269 |
+
as a formatted string. For Excel files, each worksheet is processed individually.
|
| 270 |
+
|
| 271 |
+
Args:
|
| 272 |
+
filename (str): The filename of the CSV, XLSX, or XLS file to analyze.
|
| 273 |
+
|
| 274 |
+
Returns:
|
| 275 |
+
Union[str, str]: A formatted string containing the file's data, or an error message in case of issues.
|
| 276 |
+
"""
|
| 277 |
+
print(f"--- ESECUZIONE DEL TOOL 'analyze_tabular_data' CON INPUT: filename='{filename}' ---")
|
| 278 |
+
UPLOADS_DIR = "./uploads/"
|
| 279 |
+
file_path = os.path.join(UPLOADS_DIR, filename)
|
| 280 |
+
|
| 281 |
+
# 1. Validazione dell'input: controlla se il file esiste
|
| 282 |
+
if not os.path.exists(file_path):
|
| 283 |
+
return f"Error: The file '{file_path}' was not found. Make sure it has been downloaded first."
|
| 284 |
+
|
| 285 |
+
try:
|
| 286 |
+
# 2. Determina il tipo di file e prepara la lista dei CSV da processare
|
| 287 |
+
file_extension = Path(file_path).suffix.lower()
|
| 288 |
+
csv_files_to_process = []
|
| 289 |
+
|
| 290 |
+
# --- CASO 1: Il file è un Excel ---
|
| 291 |
+
if file_extension in ['.xlsx', '.xls']:
|
| 292 |
+
print(f"Rilevato file Excel. Inizio la conversione dei fogli in CSV temporanei...")
|
| 293 |
+
|
| 294 |
+
# Legge tutti i fogli in un dizionario di DataFrame
|
| 295 |
+
excel_sheets = pd.read_excel(file_path, sheet_name=None)
|
| 296 |
+
|
| 297 |
+
if not excel_sheets:
|
| 298 |
+
return f"Error: The Excel file '{file_path}' is empty or contains no worksheets."
|
| 299 |
+
|
| 300 |
+
# Ottiene il nome base del file per i file temporanei
|
| 301 |
+
base_name = Path(file_path).stem
|
| 302 |
+
uploads_dir = Path(file_path).parent
|
| 303 |
+
|
| 304 |
+
for sheet_name, df in excel_sheets.items():
|
| 305 |
+
# Crea un nome di file sicuro per il CSV temporaneo
|
| 306 |
+
safe_sheet_name = "".join(c for c in sheet_name if c.isalnum() or c in (' ', '_')).rstrip()
|
| 307 |
+
temp_csv_path = uploads_dir / f"{base_name}_sheet_{safe_sheet_name}.csv"
|
| 308 |
+
|
| 309 |
+
# Salva il DataFrame del foglio in un file CSV
|
| 310 |
+
df.to_csv(temp_csv_path, index=False)
|
| 311 |
+
print(f" - Foglio '{sheet_name}' convertito e salvato in: {temp_csv_path}")
|
| 312 |
+
csv_files_to_process.append(str(temp_csv_path))
|
| 313 |
+
|
| 314 |
+
# --- CASO 2: Il file è già un CSV ---
|
| 315 |
+
elif file_extension == '.csv':
|
| 316 |
+
print(f"Rilevato file CSV. Verrà processato direttamente.")
|
| 317 |
+
csv_files_to_process.append(file_path)
|
| 318 |
+
|
| 319 |
+
# --- CASO 3: Formato non supportato ---
|
| 320 |
+
else:
|
| 321 |
+
return f"Error: Unsupported file format '{file_extension}'. This tool supports only CSV, XLSX, and XLS."
|
| 322 |
+
|
| 323 |
+
# 3. Usa CSVLoader su tutti i file CSV identificati (originali o convertiti)
|
| 324 |
+
if not csv_files_to_process:
|
| 325 |
+
return "Error: No file to process was found."
|
| 326 |
+
|
| 327 |
+
all_docs = []
|
| 328 |
+
for csv_path in csv_files_to_process:
|
| 329 |
+
loader = CSVLoader(file_path=csv_path)
|
| 330 |
+
docs = loader.load()
|
| 331 |
+
all_docs.extend(docs)
|
| 332 |
+
|
| 333 |
+
# 4. Formatta l'output come richiesto
|
| 334 |
+
# Aumentiamo il limite di caratteri per dare più contesto all'LLM
|
| 335 |
+
formatted_output = "\n\n---\n\n".join(
|
| 336 |
+
[
|
| 337 |
+
f'<Document source="{Path(doc.metadata["source"]).name}" page="{doc.metadata.get("page", 0)}">\n{doc.page_content[:2500]}\n</Document>'
|
| 338 |
+
for doc in all_docs
|
| 339 |
+
]
|
| 340 |
+
)
|
| 341 |
+
|
| 342 |
+
print("Analisi completata con successo.")
|
| 343 |
+
return formatted_output
|
| 344 |
+
|
| 345 |
+
except Exception as e:
|
| 346 |
+
error_message = f"An unexpected error occurred while analyzing the file '{file_path}': {e}"
|
| 347 |
+
print(error_message)
|
| 348 |
+
return error_message
|
| 349 |
+
|
| 350 |
+
|
| 351 |
+
@tool("audio_tool")
|
| 352 |
+
def audio_tool(filename: str) -> Union[str, str]:
|
| 353 |
+
"""
|
| 354 |
+
Transcribes a local audio file into text using OpenAI's Whisper model.
|
| 355 |
+
Use this tool when you need to extract the textual content from an audio file.
|
| 356 |
+
Supports common formats such as MP3, MP4, MPEG, MPGA, M4A, WAV, and WEBM.
|
| 357 |
+
|
| 358 |
+
Args:
|
| 359 |
+
filename (str): The filename of the audio file to transcribe.
|
| 360 |
+
|
| 361 |
+
Returns:
|
| 362 |
+
Union[str, str]: The transcribed text if successful, or an error message string in case of failure.
|
| 363 |
+
"""
|
| 364 |
+
print(f"--- ESECUZIONE DEL TOOL 'transcribe_audio' CON INPUT: file_path='{filename}' ---")
|
| 365 |
+
UPLOADS_DIR = "./uploads/"
|
| 366 |
+
file_path = os.path.join(UPLOADS_DIR, filename)
|
| 367 |
+
client = OpenAI()
|
| 368 |
+
|
| 369 |
+
# 1. Controlla se il client è stato inizializzato correttamente
|
| 370 |
+
if client is None:
|
| 371 |
+
return "Error: The OpenAI client is not configured. Please check your API key."
|
| 372 |
+
|
| 373 |
+
# 2. Controlla se il file esiste prima di tentare di aprirlo
|
| 374 |
+
if not os.path.exists(file_path):
|
| 375 |
+
return f"Error: The file '{file_path}' was not found. Make sure it has been downloaded first."
|
| 376 |
+
|
| 377 |
+
try:
|
| 378 |
+
# 3. Apri il file in modalità binaria e invialo all'API di OpenAI
|
| 379 |
+
with open(file_path, "rb") as audio_file:
|
| 380 |
+
transcription = client.audio.transcriptions.create(
|
| 381 |
+
model="whisper-1",
|
| 382 |
+
file=audio_file
|
| 383 |
+
)
|
| 384 |
+
|
| 385 |
+
print("Trascrizione completata con successo.")
|
| 386 |
+
# La risposta dell'API contiene il testo nel campo 'text'
|
| 387 |
+
return transcription.text
|
| 388 |
+
|
| 389 |
+
except APIError as e:
|
| 390 |
+
# Gestisce errori specifici dell'API di OpenAI (es. file non valido, auth error)
|
| 391 |
+
error_message = f"Error from the OpenAI API during transcription: {e}"
|
| 392 |
+
print(error_message)
|
| 393 |
+
return error_message
|
| 394 |
+
|
| 395 |
+
except Exception as e:
|
| 396 |
+
# Gestisce altri errori imprevisti (es. problemi di lettura del file)
|
| 397 |
+
error_message = f"An unexpected error occurred while transcribing the file '{file_path}': {e}"
|
| 398 |
+
print(error_message)
|
| 399 |
+
return error_message
|
| 400 |
+
|
| 401 |
+
|
| 402 |
+
@tool("image_tool")
|
| 403 |
+
def image_tool(filename: str, user_question: str) -> Union[str, str]:
|
| 404 |
+
"""
|
| 405 |
+
Reads a local image file and encodes it in base64 format, ready to be analyzed by a multimodal model (such as GPT-4o).
|
| 406 |
+
Use this tool to prepare any image (JPG, PNG, WEBP, etc.) before asking questions about its content.
|
| 407 |
+
|
| 408 |
+
Args:
|
| 409 |
+
filename (str): The filename of the image file to prepare.
|
| 410 |
+
user_question (str): The user's original question to guide the analysis.
|
| 411 |
+
|
| 412 |
+
Returns:
|
| 413 |
+
Union[str, str]: A textual analysis based on the base64 encoded image data, or an error message string.
|
| 414 |
+
"""
|
| 415 |
+
print(f"--- ESECUZIONE DEL TOOL 'prepare_image_for_analysis' CON INPUT: file_path='{filename}' ---")
|
| 416 |
+
UPLOADS_DIR = "./uploads/"
|
| 417 |
+
file_path = os.path.join(UPLOADS_DIR, filename)
|
| 418 |
+
client = OpenAI()
|
| 419 |
+
|
| 420 |
+
# 1. Controlla se il file esiste
|
| 421 |
+
if not os.path.exists(file_path):
|
| 422 |
+
return f"Error: The image file '{file_path}' was not found."
|
| 423 |
+
|
| 424 |
+
try:
|
| 425 |
+
# 2. Determina il tipo MIME dell'immagine (es. 'image/jpeg', 'image/png')
|
| 426 |
+
mime_type, _ = mimetypes.guess_type(file_path)
|
| 427 |
+
if not mime_type or not mime_type.startswith('image/'):
|
| 428 |
+
return f"Error: The file '{file_path}' is not a supported image format."
|
| 429 |
+
|
| 430 |
+
# 3. Leggi il file in modalità binaria e codificalo in base64
|
| 431 |
+
with open(file_path, "rb") as image_file:
|
| 432 |
+
encoded_string = base64.b64encode(image_file.read()).decode('utf-8')
|
| 433 |
+
|
| 434 |
+
# 4. Formatta l'output come un data URI, il formato standard per passare immagini
|
| 435 |
+
# a modelli multimodali.
|
| 436 |
+
image_data = f"data:{mime_type};base64,{encoded_string}"
|
| 437 |
+
# Crea il prompt per l'analisi
|
| 438 |
+
analysis_prompt = [
|
| 439 |
+
{
|
| 440 |
+
"role": "user",
|
| 441 |
+
"content": [
|
| 442 |
+
{
|
| 443 |
+
"type": "text",
|
| 444 |
+
"text": f"""
|
| 445 |
+
You are an expert visual analyst. Your task is to describe the provided image in extreme detail to help answer the user's question.
|
| 446 |
+
Focus on the elements relevant to the question. Be objective and precise.
|
| 447 |
+
|
| 448 |
+
**User's Question:** '{user_question}'
|
| 449 |
+
|
| 450 |
+
Analyze the image and provide a detailed description.
|
| 451 |
+
"""
|
| 452 |
+
},
|
| 453 |
+
{
|
| 454 |
+
"type": "image_url",
|
| 455 |
+
"image_url": {
|
| 456 |
+
"url": image_data,
|
| 457 |
+
"detail": "high" # Usa alta risoluzione per la massima precisione
|
| 458 |
+
}
|
| 459 |
+
}
|
| 460 |
+
]
|
| 461 |
+
}
|
| 462 |
+
]
|
| 463 |
+
|
| 464 |
+
|
| 465 |
+
# 5. Restituisce la risposta in base all'immagine analizzata.
|
| 466 |
+
# Chiama l'API di OpenAI
|
| 467 |
+
response = client.chat.completions.create(
|
| 468 |
+
model="gpt-4o-mini", # o "gpt-4-vision-preview"
|
| 469 |
+
messages=analysis_prompt,
|
| 470 |
+
max_tokens=1000,
|
| 471 |
+
temperature=0
|
| 472 |
+
)
|
| 473 |
+
|
| 474 |
+
description = response.choices[0].message.content
|
| 475 |
+
print("--- Image analysis complete. ---")
|
| 476 |
+
return description
|
| 477 |
+
except Exception as e:
|
| 478 |
+
return f"An error occurred during the visual analysis: {e}"
|
| 479 |
+
|
| 480 |
+
|
| 481 |
+
#-----------------------------------------------------------------------------
|
| 482 |
+
# Code Execution Tools
|
| 483 |
+
#-----------------------------------------------------------------------------
|
| 484 |
+
@tool("code_writer_tool")
|
| 485 |
+
def code_writer_tool(code: str, task_id: str) -> str:
|
| 486 |
+
"""
|
| 487 |
+
Writes a string of Python code to a local file. This is the first step
|
| 488 |
+
for any task that requires writing and then executing code. The task_id is the name for the file.
|
| 489 |
+
|
| 490 |
+
Args:
|
| 491 |
+
code (str): A string containing the complete, valid Python code to be written to the file.
|
| 492 |
+
task_id (str): The name for the file.
|
| 493 |
+
|
| 494 |
+
Returns:
|
| 495 |
+
local_filename (str): The local filename of python file to execute.
|
| 496 |
+
"""
|
| 497 |
+
print(f"--- TOOL: Writing code to file: {task_id}.py ---")
|
| 498 |
+
UPLOADS_DIR = "./uploads/"
|
| 499 |
+
local_filename = f"{task_id}.py"
|
| 500 |
+
file_path = os.path.join(UPLOADS_DIR, local_filename)
|
| 501 |
+
|
| 502 |
+
try:
|
| 503 |
+
# Scrive il codice nel file
|
| 504 |
+
with open(file_path, "w", encoding="utf-8") as f:
|
| 505 |
+
f.write(code)
|
| 506 |
+
|
| 507 |
+
success_message = f"Successfully wrote code to {file_path}."
|
| 508 |
+
print(success_message)
|
| 509 |
+
|
| 510 |
+
# Restituisce il percorso del file, che servirà al tool di esecuzione
|
| 511 |
+
return local_filename
|
| 512 |
+
except Exception as e:
|
| 513 |
+
error_message = f"An error occurred while writing the file: {e}"
|
| 514 |
+
print(error_message)
|
| 515 |
+
return error_message
|
| 516 |
+
|
| 517 |
+
|
| 518 |
+
@tool("code_tool")
|
| 519 |
+
def code_tool(filename: str, timeout_seconds: int = 100) -> Union[str, dict]:
|
| 520 |
+
"""
|
| 521 |
+
Executes a programming code file in an isolated and secure environment, capturing its standard output and errors.
|
| 522 |
+
Use this tool to run programming code when you need to analyze its behavior or output.
|
| 523 |
+
|
| 524 |
+
Args:
|
| 525 |
+
filename (str): The filename of the code file to execute.
|
| 526 |
+
timeout_seconds (int, optional): The maximum number of seconds the execution is allowed to run before forcibly terminating the process. Default is 10.
|
| 527 |
+
|
| 528 |
+
Returns:
|
| 529 |
+
Union[str, dict]: A dictionary containing 'stdout', 'stderr', and 'return_code' if successful, or an error message string if the tool itself fails.
|
| 530 |
+
"""
|
| 531 |
+
print(f"--- ESECUZIONE DEL TOOL 'execute_python_file' SU: {filename} ---")
|
| 532 |
+
UPLOADS_DIR = "./uploads/"
|
| 533 |
+
file_path = os.path.join(UPLOADS_DIR, filename)
|
| 534 |
+
|
| 535 |
+
# 1. Controlla di sicurezza: il file esiste?
|
| 536 |
+
if not os.path.exists(file_path):
|
| 537 |
+
return f"Error: The code file '{file_path}' was not found."
|
| 538 |
+
|
| 539 |
+
# 2. Usa subprocess.run per eseguire il codice in modo sicuro
|
| 540 |
+
try:
|
| 541 |
+
# 'subprocess.run' è il modo moderno e raccomandato per eseguire processi
|
| 542 |
+
process = subprocess.run(
|
| 543 |
+
['python', file_path], # Il comando da eseguire (es. 'python nomefile.py')
|
| 544 |
+
capture_output=True, # Cattura stdout e stderr
|
| 545 |
+
text=True, # Decodifica stdout/stderr come testo (UTF-8)
|
| 546 |
+
timeout=timeout_seconds # Imposta un timeout
|
| 547 |
+
)
|
| 548 |
+
|
| 549 |
+
execution_result = {
|
| 550 |
+
"return_code": process.returncode,
|
| 551 |
+
"stdout": process.stdout.strip(),
|
| 552 |
+
"stderr": process.stderr.strip()
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
# 3. Ritorna un dizionario strutturato con i risultati
|
| 556 |
+
return execution_result
|
| 557 |
+
|
| 558 |
+
except FileNotFoundError:
|
| 559 |
+
# Questo errore si verifica se l'interprete 'python' non è nel PATH del sistema
|
| 560 |
+
return "Error: The 'python' interpreter was not found on the system. Unable to execute the code."
|
| 561 |
+
except subprocess.TimeoutExpired as e:
|
| 562 |
+
# Gestisce il caso in cui il codice va in timeout
|
| 563 |
+
return {
|
| 564 |
+
"return_code": -1, # Codice di ritorno personalizzato per timeout
|
| 565 |
+
"stdout": e.stdout.strip() if e.stdout else "",
|
| 566 |
+
"stderr": f"Error: Execution terminated after {timeout_seconds} seconds (Timeout)."
|
| 567 |
+
}
|
| 568 |
+
except Exception as e:
|
| 569 |
+
# Cattura qualsiasi altro errore imprevisto durante l'esecuzione del tool
|
| 570 |
+
return f"An unexpected error occurred while executing the tool: {e}"
|
| 571 |
+
|
| 572 |
+
|
| 573 |
+
#-----------------------------------------------------------------------------
|
| 574 |
+
# Youtube Video Tools
|
| 575 |
+
#-----------------------------------------------------------------------------
|
| 576 |
+
@tool("youtube_info_tool")
|
| 577 |
+
def youtube_info_tool(youtube_url: str) -> Union[str, dict]:
|
| 578 |
+
"""
|
| 579 |
+
Collects information and resources from a YouTube video. Downloads both audio and video, and retrieves the official transcript if available.
|
| 580 |
+
This is ALWAYS the first tool to call when working with a YouTube video.
|
| 581 |
+
|
| 582 |
+
Args:
|
| 583 |
+
youtube_url (str): The full URL of the YouTube video.
|
| 584 |
+
|
| 585 |
+
Returns:
|
| 586 |
+
Union[str, dict]: A dictionary with the collected resources (transcript, audio_filename, video_filename) or an error message.
|
| 587 |
+
"""
|
| 588 |
+
print(f"--- ESECUZIONE DEL TOOL 'get_youtube_video_info' CON URL: {youtube_url} ---")
|
| 589 |
+
UPLOADS_DIR = "./uploads/"
|
| 590 |
+
|
| 591 |
+
try:
|
| 592 |
+
yt = YouTube(youtube_url)
|
| 593 |
+
video_id = yt.video_id
|
| 594 |
+
|
| 595 |
+
# 1. Recupera la trascrizione ufficiale
|
| 596 |
+
transcript_text = None
|
| 597 |
+
try:
|
| 598 |
+
transcript_list = YouTubeTranscriptApi.get_transcript(video_id)
|
| 599 |
+
transcript_text = " ".join([d['text'] for d in transcript_list])
|
| 600 |
+
print("Trascrizione ufficiale trovata.")
|
| 601 |
+
except Exception:
|
| 602 |
+
print("Nessuna trascrizione ufficiale disponibile.")
|
| 603 |
+
|
| 604 |
+
# 2. Scarica l'audio
|
| 605 |
+
audio_stream = yt.streams.get_audio_only()
|
| 606 |
+
audio_path = audio_stream.download(output_path=UPLOADS_DIR, filename=f"{video_id}.m4a")
|
| 607 |
+
if transcript_text is None:
|
| 608 |
+
transcript_text = audio_tool.invoke({"filename":f"{video_id}.m4a"})
|
| 609 |
+
print(f"Audio scaricato in: {audio_path}")
|
| 610 |
+
|
| 611 |
+
# 3. Scarica il video
|
| 612 |
+
video_stream = yt.streams.get_highest_resolution()
|
| 613 |
+
video_path = video_stream.download(output_path=UPLOADS_DIR, filename=f"{video_id}.mp4")
|
| 614 |
+
print(f"Video scaricato in: {video_path}")
|
| 615 |
+
|
| 616 |
+
video_info = {
|
| 617 |
+
"title": yt.title,
|
| 618 |
+
"description": yt.description,
|
| 619 |
+
"transcript": transcript_text,
|
| 620 |
+
"audio_filename": f"{video_id}.m4a",
|
| 621 |
+
"video_filename": f"{video_id}.mp4"
|
| 622 |
+
}
|
| 623 |
+
|
| 624 |
+
return video_info
|
| 625 |
+
except Exception as e:
|
| 626 |
+
return f"Error while retrieving information from the YouTube video: {e}"
|
| 627 |
+
|
| 628 |
+
|
| 629 |
+
@tool("youtube_frame_tool")
|
| 630 |
+
def youtube_frame_tool(filename: str, title: str, description: str, transcript: str, user_question: str, sample_rate_seconds: int = 5) -> Union[str, str]:
|
| 631 |
+
"""
|
| 632 |
+
Analyzes video content by combining visual information from frames with the provided transcript to answer a specific user question.
|
| 633 |
+
To be used as a last resort, when the transcript and audio are not sufficient, or for purely visual questions.
|
| 634 |
+
|
| 635 |
+
Args:
|
| 636 |
+
filename (str): The filename of the video file.
|
| 637 |
+
title (str): The title of the video.
|
| 638 |
+
description (str): A brief description of the video.
|
| 639 |
+
transcript (str): The full text transcript of the video (either official or from audio).
|
| 640 |
+
user_question (str): The user's original question to guide the analysis.
|
| 641 |
+
sample_rate_seconds (int): Interval in seconds between frames to analyze. Default is 3.
|
| 642 |
+
|
| 643 |
+
Returns:
|
| 644 |
+
Union[str, str]: A textual analysis based on the video frames or an error message.
|
| 645 |
+
"""
|
| 646 |
+
print(f"--- ESECUZIONE DEL TOOL 'analyze_video_frames' SU: {filename} ---")
|
| 647 |
+
UPLOADS_DIR = "./uploads/"
|
| 648 |
+
video_path = os.path.join(UPLOADS_DIR, filename)
|
| 649 |
+
client = OpenAI()
|
| 650 |
+
|
| 651 |
+
if not os.path.exists(video_path):
|
| 652 |
+
return f"Error: Video file not found at '{video_path}'."
|
| 653 |
+
if client is None:
|
| 654 |
+
return "Error: The OpenAI client is not configured."
|
| 655 |
+
|
| 656 |
+
video = cv2.VideoCapture(video_path)
|
| 657 |
+
fps = video.get(cv2.CAP_PROP_FPS)
|
| 658 |
+
frame_interval = int(fps * sample_rate_seconds)
|
| 659 |
+
|
| 660 |
+
base64_frames = []
|
| 661 |
+
frame_count = 0
|
| 662 |
+
|
| 663 |
+
while video.isOpened():
|
| 664 |
+
success, frame = video.read()
|
| 665 |
+
if not success:
|
| 666 |
+
break
|
| 667 |
+
|
| 668 |
+
if frame_count % frame_interval == 0:
|
| 669 |
+
_, buffer = cv2.imencode(".jpg", frame)
|
| 670 |
+
base64_frames.append(base64.b64encode(buffer).decode("utf-8"))
|
| 671 |
+
|
| 672 |
+
frame_count += 1
|
| 673 |
+
|
| 674 |
+
video.release()
|
| 675 |
+
print(f"Campionati {len(base64_frames)} frame dal video.")
|
| 676 |
+
|
| 677 |
+
if not base64_frames:
|
| 678 |
+
return "Error: Unable to extract frames from the video."
|
| 679 |
+
|
| 680 |
+
prompt_messages = [
|
| 681 |
+
{
|
| 682 |
+
"role": "user",
|
| 683 |
+
"content": [
|
| 684 |
+
{
|
| 685 |
+
"type": "text",
|
| 686 |
+
"text": f"""
|
| 687 |
+
You are a video content analyst.
|
| 688 |
+
Your task is to answer the user's question by combining information from different sources:
|
| 689 |
+
- the title
|
| 690 |
+
- the description
|
| 691 |
+
- the transcript
|
| 692 |
+
- a series of sampled frames
|
| 693 |
+
**IMPORTANT**: Analyze the all sources of the video in great detail, because there may be important information to solve the task.
|
| 694 |
+
|
| 695 |
+
**User Question**: {user_question}
|
| 696 |
+
**Video Title**: {title}
|
| 697 |
+
**Video Description**: {description}
|
| 698 |
+
**Video Transcript**: {transcript if transcript else "No transcript available."}
|
| 699 |
+
|
| 700 |
+
Begin your rigorous analysis now. Here are the frames:
|
| 701 |
+
"""
|
| 702 |
+
},
|
| 703 |
+
# Inserimento dei Frame
|
| 704 |
+
*map(lambda x: {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{x}", "detail": "low"}}, base64_frames),
|
| 705 |
+
],
|
| 706 |
+
}
|
| 707 |
+
]
|
| 708 |
+
|
| 709 |
+
try:
|
| 710 |
+
response = client.chat.completions.create(
|
| 711 |
+
model="gpt-4o-mini",
|
| 712 |
+
temperature=0,
|
| 713 |
+
messages=prompt_messages,
|
| 714 |
+
max_tokens=1000,
|
| 715 |
+
)
|
| 716 |
+
analysis_summary = response.choices[0].message.content
|
| 717 |
+
return analysis_summary
|
| 718 |
+
except Exception as e:
|
| 719 |
+
return f"Error while analyzing frames with the OpenAI API: {e}"
|
| 720 |
+
|
| 721 |
+
|
| 722 |
+
#-----------------------------------------------------------------------------
|
| 723 |
+
# Web Search Tools
|
| 724 |
+
#-----------------------------------------------------------------------------
|
| 725 |
+
@tool("web_search_tool")
|
| 726 |
+
def web_search_tool(task: str) -> str:
|
| 727 |
+
"""
|
| 728 |
+
Delegates complex research tasks to a specialized, cyclic research agent.
|
| 729 |
+
Use this for any question that requires external, up-to-date, or detailed knowledge.
|
| 730 |
+
"""
|
| 731 |
+
print(f"--- MAIN AGENT: DELEGATING RESEARCH FOR: '{task}' ---")
|
| 732 |
+
|
| 733 |
+
# Lo stato iniziale ora contiene il task e un primo messaggio umano vuoto per avviare il ciclo.
|
| 734 |
+
# L'agente di ricerca leggerà il task dallo stato e ignorerà questo messaggio.
|
| 735 |
+
initial_state = {"task": task, "context_summary": ""}
|
| 736 |
+
|
| 737 |
+
# Esegui il sub-grafo
|
| 738 |
+
final_state = web_search_graph.invoke(initial_state)
|
| 739 |
+
|
| 740 |
+
# Il risultato finale è l'ultimo messaggio nella cronologia, che sarà la risposta del writer.
|
| 741 |
+
final_answer = final_state["messages"][-1].content
|
| 742 |
+
return final_answer
|
| 743 |
+
|
| 744 |
+
|
| 745 |
+
assistant_tools_list = [
|
| 746 |
+
sort_tool, download_tool, add_tool, multiply_tool, subtract_tool, divide_tool, modulus_tool, tabular_tool, audio_tool, image_tool, code_writer_tool, code_tool, youtube_info_tool, youtube_frame_tool, web_search_tool
|
| 747 |
+
]
|
web_search_agent.py
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from dotenv import load_dotenv
|
| 3 |
+
import operator
|
| 4 |
+
from typing import List, TypedDict, Annotated, Dict
|
| 5 |
+
from pydantic import BaseModel, Field
|
| 6 |
+
from IPython.display import Image, display
|
| 7 |
+
|
| 8 |
+
from langchain_openai import ChatOpenAI
|
| 9 |
+
from langchain_core.messages import SystemMessage, AIMessage, HumanMessage, ToolMessage
|
| 10 |
+
from langgraph.graph import MessagesState, StateGraph, END, START
|
| 11 |
+
from langgraph.prebuilt import ToolNode, tools_condition
|
| 12 |
+
|
| 13 |
+
# Importiamo i web tools
|
| 14 |
+
from web_search_tools import google_search_tool, wikipedia_search_tool, browse_web_page_tool, text_analyzer_tool
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
# Carica le variabili d'ambiente
|
| 18 |
+
load_dotenv()
|
| 19 |
+
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
| 20 |
+
OPENAI_API_MODEL = os.getenv("OPENAI_API_WEB_MODEL")
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
# --- 1. Strutture e Stato ---
|
| 24 |
+
class ResearchPlan(BaseModel):
|
| 25 |
+
"""A step-by-step research plan."""
|
| 26 |
+
steps: List[str] = Field(description="A list of concise, sequential steps for the research task.")
|
| 27 |
+
|
| 28 |
+
class ResearchState(MessagesState):
|
| 29 |
+
task: str
|
| 30 |
+
plan: ResearchPlan
|
| 31 |
+
current_plan_step: int
|
| 32 |
+
context_summary: str
|
| 33 |
+
step_results: Annotated[List[str], operator.add] # Memoria a lungo termine per i risultati di ogni passo
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
# --- 2. Tool e Modelli ---
|
| 37 |
+
llm = ChatOpenAI(model=OPENAI_API_MODEL, api_key=OPENAI_API_KEY, temperature=0)
|
| 38 |
+
llm_with_tools = llm.bind_tools([wikipedia_search_tool, browse_web_page_tool])
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
# --- 3. Nodi del Grafo a Pipeline ---
|
| 42 |
+
def planning_node(state: ResearchState):
|
| 43 |
+
"""Node 1: Generate the initial research plan."""
|
| 44 |
+
print("--- 📝 PLANNING NODE ---")
|
| 45 |
+
|
| 46 |
+
task = state.get('task')
|
| 47 |
+
structured_llm = llm.with_structured_output(ResearchPlan)
|
| 48 |
+
planning_prompt = f"""
|
| 49 |
+
You are an expert and efficient research planner. Your goal is to create the SHORTEST POSSIBLE, logical, step-by-step plan to solve a user's research task.
|
| 50 |
+
|
| 51 |
+
**Core Principles:**
|
| 52 |
+
1. **Analyze Complexity**: First, determine if the task is simple or complex.
|
| 53 |
+
- A **simple task** can be solved with a single, well-formulated search and analysis (e.g., "Who won the 1998 World Cup?").
|
| 54 |
+
- A **complex task** requires finding one piece of information to unlock the next (e.g., "Who is the manager of the team that won the 1998 World Cup?").
|
| 55 |
+
2. **Create the Plan**:
|
| 56 |
+
- For a **simple task**, create a plan with ONLY ONE step: a clear instruction to find the final answer.
|
| 57 |
+
- For a **complex task**, break it down into the minimum number of sequential steps required. Each step must build upon the previous one.
|
| 58 |
+
3. **Focus on Actions**: Each step should describe an action to find a specific piece of information.
|
| 59 |
+
|
| 60 |
+
---
|
| 61 |
+
**Example 1: Simple Task**
|
| 62 |
+
* **User Task:** "How many studio albums were published by Mercedes Sosa between 2000 and 2009 (included)? You can use the latest 2022 version of english wikipedia."
|
| 63 |
+
* **Your Output (Plan):**
|
| 64 |
+
"steps": [
|
| 65 |
+
"Search Wikipedia for the discography of Mercedes Sosa, find all studio albums released between 2000 and 2009, and count them."
|
| 66 |
+
]
|
| 67 |
+
|
| 68 |
+
**Example 2: Complex Task**
|
| 69 |
+
* **User Task:** "Who did the actor who played Ray in the Polish-language version of Everybody Loves Raymond play in Magda M.? Give only the first name."
|
| 70 |
+
* **Your Output (Plan):**
|
| 71 |
+
"steps": [
|
| 72 |
+
"Find the name of the actor who played Ray in the Polish version of 'Everybody Loves Raymond'.",
|
| 73 |
+
"Using the actor's name, find their role in the show 'Magda M.' and extract the character's first name."
|
| 74 |
+
]
|
| 75 |
+
---
|
| 76 |
+
|
| 77 |
+
Now, analyze the following user task and generate the most efficient, step-by-step research plan.
|
| 78 |
+
**User Task:** {task}
|
| 79 |
+
**Your Output (Plan):**
|
| 80 |
+
"""
|
| 81 |
+
|
| 82 |
+
response_plan = structured_llm.invoke([SystemMessage(content=planning_prompt)])
|
| 83 |
+
print("--- ✅ PLANNING COMPLETE ---")
|
| 84 |
+
print("Generated Plan:", response_plan.steps)
|
| 85 |
+
return {"plan": response_plan, "current_plan_step": 0}
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def search_node(state: ResearchState):
|
| 89 |
+
"""Node 2: Performs a web search for a single step of the plan."""
|
| 90 |
+
step_index = state["current_plan_step"]
|
| 91 |
+
plan_steps = state["plan"].steps
|
| 92 |
+
current_step_instruction = plan_steps[step_index]
|
| 93 |
+
context_summary = state["step_results"]
|
| 94 |
+
|
| 95 |
+
print(f"--- 🔎 SEARCH NODE (Executing step: '{current_step_instruction}') ---")
|
| 96 |
+
query_prompt = f"""
|
| 97 |
+
You are an expert at generating search engine queries.
|
| 98 |
+
Your goal is to create a single, concise, and effective Google search query to accomplish the given plan step, using the context from previous steps.
|
| 99 |
+
|
| 100 |
+
**Current Plan Step to Execute:** "{current_step_instruction}"
|
| 101 |
+
**Context from Previous Steps' Findings:**
|
| 102 |
+
---
|
| 103 |
+
{context_summary}
|
| 104 |
+
---
|
| 105 |
+
|
| 106 |
+
Based on the **Current Plan Step** and the **Context**, generate the single best possible search query to find the next piece of information.
|
| 107 |
+
For example, if the context is "The actor is Bartek Kasprzykowski" and the step is "Find his role in Magda M.", a good query would be "Bartek Kasprzykowski role in Magda M.".
|
| 108 |
+
"""
|
| 109 |
+
|
| 110 |
+
# Genera la query
|
| 111 |
+
query = llm.invoke([SystemMessage(content=query_prompt)]).content.strip('"')
|
| 112 |
+
print(f"--- Generated Context-Aware Query: '{query}' ---")
|
| 113 |
+
|
| 114 |
+
# Eseguiamo il tool di ricerca su Google
|
| 115 |
+
search_results = google_search_tool.invoke(query)
|
| 116 |
+
|
| 117 |
+
# Aggiorniamo lo stato
|
| 118 |
+
return {"messages": [AIMessage(content=search_results)]}
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def browse_node(state: ResearchState):
|
| 122 |
+
"""Node 3: Analyzes search results and decides which URL to browse, prioritizing Wikipedia."""
|
| 123 |
+
# L'ultimo messaggio contiene i risultati della ricerca Google
|
| 124 |
+
search_results = state["messages"][-1].content
|
| 125 |
+
|
| 126 |
+
print(f"--- 📖 BROWSE NODE (Analyzing search results) ---")
|
| 127 |
+
|
| 128 |
+
# Prompt per scegliere l'URL e il tool corretto
|
| 129 |
+
browse_prompt = f"""
|
| 130 |
+
You are an expert at selecting the best information source.
|
| 131 |
+
Given a list of Google search results, your goal is to choose the SINGLE best URL to browse to accomplish the current research step.
|
| 132 |
+
|
| 133 |
+
**Current Research Step:** "{state['plan'].steps[state['current_plan_step']]}"
|
| 134 |
+
|
| 135 |
+
**Decision Hierarchy (Strict):**
|
| 136 |
+
1. **Wikipedia First**: If a reliable `wikipedia.org` link is present and seems highly relevant to the current step, you **MUST** choose it and call the `wikipedia_search_tool`.
|
| 137 |
+
2. **Browse Other Sources**: If there are no good Wikipedia links, choose the single most promising URL from another reputable source and call the `browse_web_page_tool`.
|
| 138 |
+
|
| 139 |
+
**Search Results:**
|
| 140 |
+
---
|
| 141 |
+
{search_results}
|
| 142 |
+
---
|
| 143 |
+
|
| 144 |
+
Based on the hierarchy and the current research step, which single tool call should you make?
|
| 145 |
+
"""
|
| 146 |
+
|
| 147 |
+
# Invoca l'LLM per ottenere la decisione sulla chiamata al tool
|
| 148 |
+
message = llm_with_tools.invoke([SystemMessage(content=browse_prompt)])
|
| 149 |
+
|
| 150 |
+
# Controlla se l'LLM ha effettivamente deciso di chiamare un tool
|
| 151 |
+
if not hasattr(message, "tool_calls") or not message.tool_calls:
|
| 152 |
+
# Fallback: se l'LLM non riesce a decidere, lo segnaliamo per passare avanti
|
| 153 |
+
print("--- ⚠️ BROWSE NODE: LLM failed to choose a tool. Skipping browse step. ---")
|
| 154 |
+
return {"messages": [AIMessage(content="No relevant page found to browse.")]}
|
| 155 |
+
|
| 156 |
+
print(f"--- Browse Node decision: Call '{message.tool_calls[0]['name']}' on '{message.tool_calls[0]['args']}' ---")
|
| 157 |
+
return {"messages": message}
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def step_synthesis_node(state: ResearchState):
|
| 161 |
+
"""Node 4: Summarize the information from the current step and prepare for the next one."""
|
| 162 |
+
print(" --- 🔄 STEP SYNTHESIS NODE ---")
|
| 163 |
+
|
| 164 |
+
current_step_instruction = state["plan"].steps[state["current_plan_step"]]
|
| 165 |
+
browsed_content = state["messages"][-1].content
|
| 166 |
+
|
| 167 |
+
summary_prompt = f"""
|
| 168 |
+
You are a factual extractor and research analyst.
|
| 169 |
+
Your goal is to extract key pieces of information from the provided content to satisfy a specific sub-task and prepare for the next step.
|
| 170 |
+
|
| 171 |
+
**Sub-Task (Instruction to accomplish):** "{current_step_instruction}"
|
| 172 |
+
|
| 173 |
+
**Content Gathered in this Step:**
|
| 174 |
+
---
|
| 175 |
+
{browsed_content}
|
| 176 |
+
---
|
| 177 |
+
|
| 178 |
+
**Analysis:**
|
| 179 |
+
1. **Extract Key Facts**: From the "Content Gathered", pull out the specific names, dates, numbers, or links that directly answer the "Sub-Task".
|
| 180 |
+
2. **Assess Step Completion**: Was the sub-task successfully completed with this information?
|
| 181 |
+
3. **Synthesize for Next Step**: Create a very concise summary of your findings. This summary will be used as context for the next step in the plan. If the sub-task was not completed, state what is still missing.
|
| 182 |
+
|
| 183 |
+
**Your Output:**
|
| 184 |
+
Provide a concise summary of your findings. For example:
|
| 185 |
+
"Successfully found the actor's name: Bartek Kasprzykowski."
|
| 186 |
+
or
|
| 187 |
+
"Failed to find the specific NASA award number on this page, but confirmed the paper was written by the correct team."
|
| 188 |
+
"""
|
| 189 |
+
|
| 190 |
+
step_summary = llm.invoke([SystemMessage(content=summary_prompt)]).content
|
| 191 |
+
print(f"--- ✅ STEP {state['current_plan_step'] + 1} COMPLETE. Summary: '{step_summary}' ---")
|
| 192 |
+
|
| 193 |
+
# Aggiunge il riassunto ai risultati a lungo termine e avanza il contatore
|
| 194 |
+
return {"step_results": [step_summary], "current_plan_step": state["current_plan_step"] + 1}
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def final_synthesis_node(state: ResearchState):
|
| 198 |
+
"""Node 5: Takes all the summarized results from each step and combines them into a complete and final answer for the original task."""
|
| 199 |
+
print("--- ✍️ FINAL SYNTHESIS NODE ---")
|
| 200 |
+
|
| 201 |
+
# Raccoglie i riassunti di ogni passo dalla memoria a lungo termine dello stato
|
| 202 |
+
step_summaries = state.get("step_results", [])
|
| 203 |
+
|
| 204 |
+
# Controlla se abbiamo effettivamente dei risultati da sintetizzare
|
| 205 |
+
if not step_summaries:
|
| 206 |
+
final_report = "The research process concluded, but no conclusive information was gathered to answer the task."
|
| 207 |
+
return {"messages": [AIMessage(content=final_report)]}
|
| 208 |
+
|
| 209 |
+
# Crea un contesto pulito per l'LLM finale
|
| 210 |
+
full_context = "\n\n".join(
|
| 211 |
+
[f"Finding from Step {i+1}: {summary}" for i, summary in enumerate(step_summaries)]
|
| 212 |
+
)
|
| 213 |
+
|
| 214 |
+
# Prompt per la sintesi finale
|
| 215 |
+
final_prompt = f"""
|
| 216 |
+
You are an expert data analyst and report writer.
|
| 217 |
+
Your final and most important task is to synthesize the provided research findings to answer the user's original task with extreme precision.
|
| 218 |
+
|
| 219 |
+
**User's Original Task:**
|
| 220 |
+
---
|
| 221 |
+
"{state['task']}"
|
| 222 |
+
---
|
| 223 |
+
|
| 224 |
+
**Summary of Findings from Each Research Step:**
|
| 225 |
+
---
|
| 226 |
+
{full_context}
|
| 227 |
+
---
|
| 228 |
+
|
| 229 |
+
**Your Analytical Process (You MUST follow this):**
|
| 230 |
+
1. **Re-read the Original Task**: Pay extremely close attention to all constraints, especially dates, numbers, and specific conditions (e.g., "between 2000 and 2009, included", "first name only").
|
| 231 |
+
2. **Verify Information**: Scan the "Summary of Findings" and ensure you have all the necessary pieces to construct the answer. Do not invent or infer information that is not present.
|
| 232 |
+
3. **Construct the Final Answer**: Write a clear, direct, and accurate answer based solely on the verified findings. Address every part of the user's original task.
|
| 233 |
+
|
| 234 |
+
Based on this rigorous process, generate the final answer.
|
| 235 |
+
"""
|
| 236 |
+
|
| 237 |
+
# Usa un LLM (può essere lo stesso o uno diverso) per generare il report finale
|
| 238 |
+
final_report = llm.invoke([SystemMessage(content=final_prompt)])
|
| 239 |
+
print("--- ✅ FINAL REPORT GENERATED ---")
|
| 240 |
+
|
| 241 |
+
# Aggiunge il report finale ai messaggi, che sarà l'output finale del grafo
|
| 242 |
+
return {"messages": final_report}
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
# --- 4. Costruzione del Grafo a Pipeline ---
|
| 246 |
+
def router(state: ResearchState):
|
| 247 |
+
"""Decides whether to proceed to the next step or move on to the final summary."""
|
| 248 |
+
print("--- 🔍 ROUTER ---")
|
| 249 |
+
if state["current_plan_step"] < len(state["plan"].steps):
|
| 250 |
+
print(" - Decision: Continue to next pipeline cycle.")
|
| 251 |
+
return "continue_pipeline"
|
| 252 |
+
else:
|
| 253 |
+
print(" - Decision: Plan complete. Proceed to final synthesis.")
|
| 254 |
+
return "end_pipeline"
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
builder = StateGraph(ResearchState)
|
| 258 |
+
builder.add_node("planning", planning_node)
|
| 259 |
+
builder.add_node("search", search_node)
|
| 260 |
+
builder.add_node("browse", browse_node)
|
| 261 |
+
builder.add_node("tools", ToolNode([wikipedia_search_tool, browse_web_page_tool]))
|
| 262 |
+
builder.add_node("synthesis", step_synthesis_node)
|
| 263 |
+
builder.add_node("final_synthesizer", final_synthesis_node)
|
| 264 |
+
|
| 265 |
+
builder.add_edge(START, "planning")
|
| 266 |
+
builder.add_edge("planning", "search")
|
| 267 |
+
builder.add_edge("search", "browse")
|
| 268 |
+
builder.add_edge("browse", "tools")
|
| 269 |
+
builder.add_edge("tools", "synthesis")
|
| 270 |
+
# Dopo la sintesi di un passo, il router decide se ricominciare o finire
|
| 271 |
+
builder.add_conditional_edges(
|
| 272 |
+
"synthesis",
|
| 273 |
+
router,
|
| 274 |
+
{
|
| 275 |
+
"continue_pipeline": "search",
|
| 276 |
+
"end_pipeline": "final_synthesizer"
|
| 277 |
+
}
|
| 278 |
+
)
|
| 279 |
+
builder.add_edge("final_synthesizer", END)
|
| 280 |
+
|
| 281 |
+
web_search_graph = builder.compile()
|
| 282 |
+
display(Image(web_search_graph.get_graph(xray=1).draw_mermaid_png(output_file_path="./web_search_graph.png")))
|
web_search_tools.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from dotenv import load_dotenv
|
| 3 |
+
from urllib.parse import unquote
|
| 4 |
+
import tempfile
|
| 5 |
+
import wikipedia
|
| 6 |
+
from playwright.sync_api import sync_playwright, TimeoutError
|
| 7 |
+
import bs4
|
| 8 |
+
import pandas as pd
|
| 9 |
+
|
| 10 |
+
from langchain_openai import ChatOpenAI
|
| 11 |
+
from langchain_community.document_loaders import UnstructuredHTMLLoader
|
| 12 |
+
from langchain_google_community import GoogleSearchAPIWrapper
|
| 13 |
+
from langchain_community.utilities import ArxivAPIWrapper
|
| 14 |
+
from langchain_core.tools import tool
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
# Carica le variabili d'ambiente per i tool
|
| 18 |
+
load_dotenv()
|
| 19 |
+
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
| 20 |
+
OPENAI_API_MODEL = os.getenv("OPENAI_API_MODEL")
|
| 21 |
+
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
|
| 22 |
+
GOOGLE_CSE_ID = os.getenv("GOOGLE_CSE_ID")
|
| 23 |
+
|
| 24 |
+
# --- Tool di Ricerca Google ---
|
| 25 |
+
@tool("google_search_tool")
|
| 26 |
+
def google_search_tool(query: str) -> str:
|
| 27 |
+
"""
|
| 28 |
+
Performs a Google search and returns the top results.
|
| 29 |
+
Use this for general web searches, finding articles, or recent information.
|
| 30 |
+
"""
|
| 31 |
+
print(f"--- TOOL: Executing Google Search for: '{query}' ---")
|
| 32 |
+
google_search = GoogleSearchAPIWrapper(google_api_key=GOOGLE_API_KEY, google_cse_id=GOOGLE_CSE_ID)
|
| 33 |
+
return google_search.results(query, num_results=3)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
# --- Tool di Ricerca Wikipedia ---
|
| 37 |
+
@tool("wikipedia_search_tool")
|
| 38 |
+
def wikipedia_search_tool(query_or_url: str, max_results: int = 1) -> str:
|
| 39 |
+
"""
|
| 40 |
+
Fetches content from a Wikipedia page. This tool is dual-purpose:
|
| 41 |
+
1. If the input is a search query, it finds the most relevant Wikipedia page and returns its full content.
|
| 42 |
+
2. If the input is a full Wikipedia URL, it directly fetches and returns the content of that page.
|
| 43 |
+
|
| 44 |
+
This is the preferred tool for all interactions with Wikipedia.
|
| 45 |
+
|
| 46 |
+
Args:
|
| 47 |
+
query_or_url (str): A search query (e.g., "Mercedes Sosa discography") or a full Wikipedia URL.
|
| 48 |
+
"""
|
| 49 |
+
print(f"--- WIKIPEDIA TOOL (Dual-Purpose): Input is '{query_or_url}' ---")
|
| 50 |
+
|
| 51 |
+
wikipedia.set_lang("en")
|
| 52 |
+
|
| 53 |
+
page_title = ""
|
| 54 |
+
|
| 55 |
+
try:
|
| 56 |
+
# --- LOGICA DI DECISIONE E DECODIFICA ---
|
| 57 |
+
if query_or_url.startswith("http://") or query_or_url.startswith("https://"):
|
| 58 |
+
# Caso 1: L'input è un URL
|
| 59 |
+
# Estraiamo l'ultima parte dell'URL
|
| 60 |
+
raw_title = query_or_url.split('/')[-1]
|
| 61 |
+
|
| 62 |
+
# CORREZIONE: Decodifica i caratteri speciali (es. %C4%85 -> ą)
|
| 63 |
+
# e sostituisci gli underscore con spazi.
|
| 64 |
+
page_title = unquote(raw_title).replace('_', ' ')
|
| 65 |
+
|
| 66 |
+
print(f"Input is a URL. Decoded page title: '{page_title}'")
|
| 67 |
+
page = wikipedia.page(page_title, auto_suggest=False, redirect=True)
|
| 68 |
+
else:
|
| 69 |
+
# Caso 2: L'input è una query di ricerca
|
| 70 |
+
print("Input is a search query. Finding best page...")
|
| 71 |
+
search_results = wikipedia.search(query_or_url, results=1)
|
| 72 |
+
if not search_results:
|
| 73 |
+
return f"Error: No Wikipedia page found for query '{query_or_url}'."
|
| 74 |
+
page_title = search_results[0]
|
| 75 |
+
page = wikipedia.page(page_title, auto_suggest=False, redirect=True)
|
| 76 |
+
|
| 77 |
+
# --- ESTRAZIONE HTML E PARSING (invariato) ---
|
| 78 |
+
print(f"Fetching HTML for page: '{page.title}'")
|
| 79 |
+
html_content_str = page.html()
|
| 80 |
+
|
| 81 |
+
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix=".html", encoding='utf-8') as temp_file:
|
| 82 |
+
temp_file.write(html_content_str)
|
| 83 |
+
temp_filepath = temp_file.name
|
| 84 |
+
|
| 85 |
+
try:
|
| 86 |
+
loader = UnstructuredHTMLLoader(temp_filepath, strategy="fast")
|
| 87 |
+
docs = loader.load()
|
| 88 |
+
finally:
|
| 89 |
+
os.remove(temp_filepath)
|
| 90 |
+
|
| 91 |
+
if not docs:
|
| 92 |
+
return f"Content from Wikipedia page '{page.title}': Could not extract any content."
|
| 93 |
+
|
| 94 |
+
page_content = docs[0].page_content
|
| 95 |
+
formatted_output = f"Content from Wikipedia page: '{page.title}'\nURL: {page.url}\n\n{page_content[:20000]}"
|
| 96 |
+
return formatted_output
|
| 97 |
+
|
| 98 |
+
except wikipedia.exceptions.DisambiguationError as e:
|
| 99 |
+
return f"Error: Your query '{query_or_url}' is ambiguous. Options: {e.options[:5]}"
|
| 100 |
+
except wikipedia.exceptions.PageError:
|
| 101 |
+
return f"Error: Could not find or load the Wikipedia page for title derived from '{query_or_url}'."
|
| 102 |
+
except Exception as e:
|
| 103 |
+
return f"An unexpected error occurred in the Wikipedia tool: {e}"
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
# --- Tool di Navigazione ---
|
| 107 |
+
@tool("browse_web_page_tool")
|
| 108 |
+
def browse_web_page_tool(url: str) -> str:
|
| 109 |
+
"""
|
| 110 |
+
Navigates a web page using a headless browser, then uses Unstructured to extract
|
| 111 |
+
the full, clean content, including text and tables.
|
| 112 |
+
|
| 113 |
+
Args:
|
| 114 |
+
url (str): The full URL of the page to browse and extract content from.
|
| 115 |
+
"""
|
| 116 |
+
print(f"--- TOOL: Browsing and extracting from: {url} ---")
|
| 117 |
+
|
| 118 |
+
try:
|
| 119 |
+
# 1. Usa Playwright per ottenere l'HTML completo
|
| 120 |
+
with sync_playwright() as p:
|
| 121 |
+
browser = p.chromium.launch(headless=True)
|
| 122 |
+
page = browser.new_page()
|
| 123 |
+
page.goto(url, timeout=30000, wait_until="domcontentloaded")
|
| 124 |
+
html_content = page.content()
|
| 125 |
+
browser.close()
|
| 126 |
+
|
| 127 |
+
# 2. Usa un file temporaneo IN MEMORIA per passare l'HTML a Unstructured
|
| 128 |
+
# Questo evita di scrivere su disco, è veloce e pulito.
|
| 129 |
+
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix=".html", encoding='utf-8') as temp_file:
|
| 130 |
+
temp_file.write(html_content)
|
| 131 |
+
temp_filepath = temp_file.name
|
| 132 |
+
|
| 133 |
+
try:
|
| 134 |
+
# 3. Carica e parsa l'HTML con UnstructuredFileLoader
|
| 135 |
+
loader = UnstructuredHTMLLoader(temp_filepath, strategy="fast")
|
| 136 |
+
docs = loader.load()
|
| 137 |
+
finally:
|
| 138 |
+
# Assicurati di cancellare sempre il file temporaneo
|
| 139 |
+
os.remove(temp_filepath)
|
| 140 |
+
|
| 141 |
+
# 4. Formatta l'output
|
| 142 |
+
if not docs:
|
| 143 |
+
return f"Content from URL '{url}': Could not extract any content using Unstructured."
|
| 144 |
+
|
| 145 |
+
# Unstructured di solito mette tutto in un unico documento
|
| 146 |
+
page_content = docs[0].page_content
|
| 147 |
+
|
| 148 |
+
formatted_output = f"Content from URL: '{url}'\n\n{page_content[:20000]}"
|
| 149 |
+
|
| 150 |
+
return formatted_output
|
| 151 |
+
|
| 152 |
+
except TimeoutError:
|
| 153 |
+
return f"Error browsing '{url}': The page took too long to load and timed out."
|
| 154 |
+
except Exception as e:
|
| 155 |
+
return f"An unexpected error occurred while browsing '{url}': {e}"
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
# --- Tool di analisi del contenuto web ---
|
| 159 |
+
@tool("text_analyzer_tool")
|
| 160 |
+
def text_analyzer_tool(text_to_analyze: str, question: str) -> str:
|
| 161 |
+
"""
|
| 162 |
+
Analyzes a given text to answer a specific question or extract information.
|
| 163 |
+
Use this tool when you have already gathered content (e.g., from browsing a page)
|
| 164 |
+
and need to find a specific answer within that text.
|
| 165 |
+
|
| 166 |
+
Args:
|
| 167 |
+
text_to_analyze (str): The text content to be analyzed.
|
| 168 |
+
question (str): The specific question to answer based on the text.
|
| 169 |
+
"""
|
| 170 |
+
print(f"--- TOOL: Analyzing text to answer: '{question}' ---")
|
| 171 |
+
|
| 172 |
+
# Usiamo un LLM per fare l'analisi
|
| 173 |
+
analyzer_llm = ChatOpenAI(model=OPENAI_API_MODEL, temperature=0)
|
| 174 |
+
|
| 175 |
+
prompt = f"""
|
| 176 |
+
You are a text analysis expert. Your task is to carefully read the provided text and answer the user's question based ONLY on that text.
|
| 177 |
+
Provide a concise and direct answer.
|
| 178 |
+
|
| 179 |
+
**Text to Analyze:**
|
| 180 |
+
---
|
| 181 |
+
{text_to_analyze}
|
| 182 |
+
---
|
| 183 |
+
|
| 184 |
+
**Question to Answer:**
|
| 185 |
+
"{question}"
|
| 186 |
+
|
| 187 |
+
Your concise answer:
|
| 188 |
+
"""
|
| 189 |
+
response = analyzer_llm.invoke(prompt)
|
| 190 |
+
return response.content
|