nynuzz commited on
Commit
158ed3d
·
verified ·
1 Parent(s): 36e5072

upload files

Browse files
Files changed (6) hide show
  1. agent.py +165 -0
  2. app.py +197 -0
  3. prompts.yaml +340 -0
  4. tools.py +747 -0
  5. web_search_agent.py +282 -0
  6. 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