File size: 11,036 Bytes
7a786af
 
 
7cfe271
cd0253f
7a786af
 
 
7397ba9
7a786af
 
 
 
 
 
 
0cb983c
e0568e5
7d4acc0
7a786af
 
 
 
 
 
 
15b7f56
7a786af
7cfe271
 
c3f4508
cd0253f
 
 
 
 
 
 
260eec5
c3f4508
bd03a3e
 
 
 
 
 
 
 
 
c3f4508
c747388
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
999722d
 
 
 
 
 
 
 
 
 
 
 
cd0253f
 
 
 
 
 
 
 
 
 
 
c3f4508
7a786af
 
 
 
 
 
 
 
15b7f56
 
7a786af
 
9280a27
 
ba25c91
1ae2c4e
e0568e5
7a786af
7d4acc0
 
 
 
 
 
 
 
 
7a786af
 
50b0e5a
7a786af
 
 
cd0253f
 
 
 
7a786af
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7cfe271
7a786af
 
ba25c91
 
 
 
 
7a786af
 
 
 
 
 
 
7cfe271
 
 
 
 
7a786af
 
 
 
7397ba9
7a786af
7397ba9
 
260eec5
7397ba9
260eec5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7397ba9
7a786af
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
import os
from dotenv import load_dotenv
import traceback
import time
import pprint

from typing import Annotated,Sequence, TypedDict

from langchain_core.messages import BaseMessage, HumanMessage
from langgraph.graph.message import add_messages # helper function to add messages to the state
from langchain_core.messages import ToolMessage
from langchain_core.runnables import RunnableConfig
from langgraph.graph import StateGraph, END
from langchain_google_genai import ChatGoogleGenerativeAI

# Local imports
from tools import get_search_tool, get_tavily_search_tool, get_wikipedia_tool, wikipedia_search, wikipedia_search_3,\
                  execute_python_code_from_file, download_taskid_file, analyze_excel_file, get_analyze_mp3_tool,\
                  get_analyze_image_tool, arxiv_search, get_youtube_transcript

# Nota: per i test in locale si usa il .env
#       su HuggingFace invece si usano le variabili definite in Settings/"Variables and secrets"
load_dotenv()
GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
GEMINI_MODEL = os.environ.get("GEMINI_MODEL")
GEMINI_BASE_URL = os.environ.get("GEMINI_BASE_URL")
GEMINI_TEMPERATURE = float(os.environ.get("GEMINI_TEMPERATURE"))

TOOLS_CALL_DELAY = 1.5

# V1
# GENERAL_AGENT_INSTRUCTIONS = """You are a helpful assistant tasked with answering questions using a set of tools.
# Now, I will ask you a question. Analyze the question and provide your answer.
# Your answer should be a number OR as few words as possible OR a comma separated list of numbers and/or strings.
# If you are asked for a number, don't use comma to write your number neither use units such as $ or percent sign unless specified otherwise.
# If you are asked for a string, don't use articles, neither abbreviations (e.g. for cities), and write the digits in plain text unless specified otherwise.
# If you are asked for a comma separated list, apply the above rules depending of whether the element to be put in the list is a number or a string.
# Provide only the answer, without notes, explanations or comments."""

# V2
# GENERAL_AGENT_INSTRUCTIONS = """
# You are a general AI assistant. Your purpose is to answer questions and complete tasks accurately and concisely.
# You have access to various tools to help you gather information and perform actions.
# Always prioritize using your tools to find factual information if a question requires it.
# If a question can be answered directly from your knowledge, do so.
# If you use a tool, provide only the direct result or answer based on the tool's output.
# Do not include any conversational filler, explanations of your thought process, or pleasantries unless specifically asked.
# """

# V3
# GENERAL_AGENT_INSTRUCTIONS = """
# You are a general AI assistant. Your purpose is to answer questions and complete tasks accurately and concisely.
# You have access to various tools to help you gather information and perform actions.
# Always prioritize using your tools to find factual information if a question requires it.
# Your answer should be a number OR as few words as possible OR a comma separated list of numbers and/or strings.
# If you are asked for a number, don't use comma to write your number neither use units such as $ or percent sign unless specified otherwise.
# If you are asked for a string, don't use articles, neither abbreviations (e.g. for cities), and write the digits in plain text unless specified otherwise.
# If you are asked for a comma separated list, apply the above rules depending of whether the element to be put in the list is a number or a string.
# Provide only the answer, without notes, explanations or comments.
# To complete this task successfully, follow these steps carefully:
# 1. Comprehend the task and identify the intended goal.
# 2. Break the task into clear, logical steps.
# 3. Select and prepare the tools or resources you need.
# 4. Revise your plan if necessary based on feedback.
# 5. Maintain internal state and track progress.
# 6. Verify that the goal has been fully achieved.
# 7. Present the final result clearly and concisely."""

# V4
# GENERAL_AGENT_INSTRUCTIONS = """
# You are a general AI assistant. Your purpose is to answer questions and complete tasks accurately and concisely.
# You have access to various tools to help you gather information and perform actions.
# Always prioritize using your tools to find factual information if a question requires it.
# Fo instance, if the question mentions Wikpedia, use the wikpedia tool.
# Your answer should be a number OR as few words as possible OR a comma separated list of numbers and/or strings.
# If you are asked for a number, don't use comma to write your number neither use units such as $ or percent sign unless specified otherwise.
# If you are asked for a string, don't use articles, neither abbreviations (e.g. for cities), and write the digits in plain text unless specified otherwise.
# If you are asked for a comma separated list, apply the above rules depending of whether the element to be put in the list is a number or a string.
# Provide only the answer, without notes, explanations or comments."""

# V5
GENERAL_AGENT_INSTRUCTIONS = """
You are a general AI assistant. Your purpose is to answer questions and complete tasks accurately and concisely.
You have access to various tools to help you gather information and perform actions.
Always prioritize using your tools to find factual information if a question requires it.
If the question mentions Wikpedia, use the wikpedia tool; if the question mentions a youtube url, use get_youtube_transcript tool.
Analyze the question and plan the necessary steps to get the answer.
Your answer should be a number OR as few words as possible OR a comma separated list of numbers and/or strings.
If you are asked for a number, don't use comma to write your number neither use units such as $ or percent sign unless specified otherwise.
If you are asked for a string, don't use articles, neither abbreviations (e.g. for cities), and write the digits in plain text unless specified otherwise.
If you are asked for a comma separated list, apply the above rules depending of whether the element to be put in the list is a number or a string.
Provide only the answer, without notes, explanations or comments."""

#
# Inizializza il modello e gli associa i tool
#

# ChatGoogleGenerativeAI è il package ufficiale di LangChain per interagire con i modelli Gemini
# https://python.langchain.com/docs/integrations/chat/google_generative_ai/
chat = ChatGoogleGenerativeAI(
    model=GEMINI_MODEL,
    google_api_key=GEMINI_API_KEY,
    temperature = GEMINI_TEMPERATURE)

# Imposta i tool
#search_tool = get_search_tool()
search_tool = get_tavily_search_tool()
#wikipedia_tool = get_wikipedia_tool()
analyze_mp3_tool = get_analyze_mp3_tool(chat)
analyze_png_tool = get_analyze_image_tool(chat)

tools = [search_tool,
         wikipedia_search_3,
         execute_python_code_from_file,
         download_taskid_file,
         analyze_excel_file,
         analyze_mp3_tool,
         analyze_png_tool,
         arxiv_search,
         get_youtube_transcript]

# Bind tools to the model
chat_with_tools = chat.bind_tools(tools)

tools_by_name = {tool.name: tool for tool in tools}

# debug
print("Tools:")
for tool in tools:
    print("  {}".format(tool.name))

#
# Definisce il grafo
#

class AgentState(TypedDict):
    """The state of the agent."""
    messages: Annotated[Sequence[BaseMessage], add_messages]
    number_of_steps: int


# Define our tool node
def call_tool(state: AgentState):
    outputs = []
    # Iterate over the tool calls in the last message
    for i, tool_call in enumerate(state["messages"][-1].tool_calls):
        # Get the tool by name
        tool_result = tools_by_name[tool_call["name"]].invoke(tool_call["args"])

        # print(f"\n--- DEBUG: Tool Raw Output (Length: {len(tool_result)} chars) ---")
        # print(tool_result)
        # print("------------------------------------------------------------------\n")

        outputs.append(
            ToolMessage(
                content=tool_result,
                name=tool_call["name"],
                tool_call_id=tool_call["id"],
            )
        )

        # Add a delay after each tool call, but not after the very last one
        if i < len(state["messages"][-1].tool_calls) - 1:
            time.sleep(TOOLS_CALL_DELAY)

    return {"messages": outputs}


def call_model( state: AgentState, config: RunnableConfig):
    # Modo 1)
    # Invoke the model with the system prompt and the messages
    #response = chat_with_tools.invoke(state["messages"], config)

    # Modo 2) - aggiunge in fondo alcune istruzioni
    # Create a copy to avoid modifying the original state and append instruction to the end
    # messages = state["messages"][:]
    # messages.append(
    #     HumanMessage(content="Provide only the answer, without explanations or comments.")
    # )  # Append instruction to the end
    # response = chat_with_tools.invoke(messages, config)

    # Modo 3)
    # Create a new list for messages to send to the LLM
    # Start with the general instructions
    messages_to_send = [HumanMessage(content=GENERAL_AGENT_INSTRUCTIONS)]

    # Append all existing messages from the agent state
    messages_to_send.extend(state["messages"])

    response = chat_with_tools.invoke(messages_to_send)

    # We return a list, because this will get added to the existing messages state using the add_messages reducer
    return {"messages": [response]}


# Define the conditional edge that determines whether to continue or not
def should_continue(state: AgentState):
    messages = state["messages"]
    # If the last message is not a tool call, then we finish
    if not messages[-1].tool_calls:
        return "end"
    # default to continue
    return "continue"


def get_agent():
    # Creazione del grafo
    workflow = StateGraph(AgentState)

    # 1. Add our nodes 
    workflow.add_node("llm", call_model)
    workflow.add_node("tools",  call_tool)
    # 2. Set the entrypoint as `agent`, this is the first node called
    workflow.set_entry_point("llm")
    # 3. Add a conditional edge after the `llm` node is called.
    workflow.add_conditional_edges(
        # Edge is used after the `llm` node is called.
        "llm",
        # The function that will determine which node is called next.
        should_continue,
        # Mapping for where to go next, keys are strings from the function return, and the values are other nodes.
        # END is a special node marking that the graph is finish.
        {
            # If `tools`, then we call the tool node.
            "continue": "tools",
            # Otherwise we finish.
            "end": END,
        },
    )
    # 4. Add a normal edge after `tools` is called, `llm` node is called next.
    workflow.add_edge("tools", "llm")

    # 5. Now we can compile our graph
    react_graph = workflow.compile()

    return react_graph


# Riferimenti
#
# https://ai.google.dev/gemini-api/docs/langgraph-example