File size: 14,991 Bytes
71f9f04
80710dc
 
6eb8def
80710dc
 
 
378c064
6eb8def
80710dc
 
 
ff95a9a
6eb8def
 
 
 
 
80710dc
 
 
 
6eb8def
 
 
 
80710dc
6ed5e0b
 
 
 
378c064
71f9f04
6ed5e0b
80710dc
6eb8def
 
 
 
 
 
 
80710dc
6eb8def
80710dc
 
6eb8def
80710dc
 
6eb8def
80710dc
 
 
 
7f0c067
80710dc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6eb8def
80710dc
 
 
 
 
 
 
 
 
 
 
 
6eb8def
80710dc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6eb8def
 
 
 
80710dc
6eb8def
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80710dc
6eb8def
80710dc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6eb8def
80710dc
6eb8def
 
 
80710dc
 
 
 
 
 
 
6eb8def
 
 
80710dc
 
 
 
 
 
6eb8def
 
80710dc
 
 
7f0c067
5c20eca
80710dc
 
 
 
 
f732e70
80710dc
 
6eb8def
 
80710dc
 
 
 
 
6eb8def
f732e70
80710dc
 
 
 
 
 
 
 
6eb8def
80710dc
 
 
 
 
6eb8def
 
 
 
 
80710dc
 
 
 
 
 
 
 
 
71f9f04
6eb8def
80710dc
 
 
 
6eb8def
80710dc
1e866d2
6eb8def
 
80710dc
 
 
 
 
1e866d2
6eb8def
 
80710dc
 
 
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
242
243
244
245
246
247
248
249
250
251
import os
import openai
import wikipedia
from tavily import TavilyClient # Importa TavilyClient
import arxiv
import json
import re

# Carica le chiavi API dalle variabili d'ambiente
# Per i test locali, puoi usare python-dotenv e un file .env
# from dotenv import load_dotenv
# load_dotenv()

# La libreria OpenAI >1.0.0 gestisce automaticamente OPENAI_API_KEY
# La chiave Tavily deve essere caricata esplicitamente o passata al client

TAVILY_API_KEY = os.getenv("TAVILY_API_KEY")

if not os.getenv("OPENAI_API_KEY"):
    print("ATTENZIONE: La variabile d'ambiente OPENAI_API_KEY non è impostata.")
    print("L'agente potrebbe non funzionare correttamente senza una chiave API OpenAI valida.")

if not TAVILY_API_KEY:
    print("ATTENZIONE: La variabile d'ambiente TAVILY_API_KEY non è impostata.")
    print("La ricerca Tavily non funzionerà senza una chiave API valida.")


MAX_WIKIPEDIA_RESULTS = 3
MAX_TAVILY_RESULTS = 3 # Tavily può restituire risultati più ricchi
MAX_ARXIV_RESULTS = 2
MAX_TOOL_ITERATIONS = 6

class GaiaAgent:
    def __init__(self, model_name="o4-mini"):
        self.model_name = model_name
        self.openai_client = openai.OpenAI() # Inizializza il client OpenAI
        if TAVILY_API_KEY:
            self.tavily_client = TavilyClient(api_key=TAVILY_API_KEY)
        else:
            self.tavily_client = None
            print("Cliente Tavily non inizializzato a causa della mancanza di TAVILY_API_KEY.")
        
        print(f"GaiaAgent initialized with model: {self.model_name}")
        if not self.openai_client.api_key:
             print("WARNING: OpenAI API key not found by the client. Ensure OPENAI_API_KEY is set.")


    def _call_openai_api(self, messages, tools=None, tool_choice=None):
        try:
            response = self.openai_client.chat.completions.create(
                model=self.model_name,
                messages=messages,
                tools=tools,
                tool_choice=tool_choice,
                #temperature=0.1,
            )
            return response
        except openai.APIError as e:
            print(f"Errore API OpenAI: {e}")
            return f"Errore durante la chiamata all'API OpenAI: {str(e)}"
        except Exception as e:
            print(f"Errore imprevisto durante la chiamata OpenAI API: {e}")
            return f"Errore imprevisto durante la chiamata OpenAI API: {str(e)}"


    def _execute_python_code(self, code_string: str) -> str:
        print(f"Esecuzione codice Python (primi 200 caratteri): {code_string[:200]}...")
        try:
            code_string = re.sub(r"^```python\n", "", code_string)
            code_string = re.sub(r"\n```$", "", code_string)
            code_string = code_string.strip()
            import io
            from contextlib import redirect_stdout
            f = io.StringIO()
            with redirect_stdout(f):
                exec(code_string, {})
            s = f.getvalue()
            if not s:
                s = "Codice eseguito con successo, nessun output esplicito (print)."
            print(f"Esecuzione codice riuscita. Output (primi 200 caratteri): {s[:200]}")
            return s
        except Exception as e:
            print(f"Errore durante l'esecuzione del codice Python: {e}")
            return f"Errore nell'esecuzione del codice: {str(e)}"

    def _search_wikipedia(self, query: str) -> str:
        print(f"Ricerca Wikipedia per: {query}")
        try:
            wikipedia.set_lang("en")
            results = wikipedia.search(query, results=MAX_WIKIPEDIA_RESULTS)
            if not results:
                return "Nessun risultato trovato su Wikipedia."
            summaries = []
            for res_title in results:
                try:
                    page = wikipedia.page(res_title, auto_suggest=False, preload=True)
                    summary_text = page.summary.replace("\n", " ")[:700]
                    summaries.append(f"Title: {page.title}\nSummary: {summary_text}...")
                except wikipedia.exceptions.DisambiguationError as e:
                    options = ", ".join(e.options[:3])
                    summaries.append(f"Pagina di disambiguazione per '{res_title}': Le opzioni includono {options}. Prova una query più specifica.")
                except wikipedia.exceptions.PageError:
                    summaries.append(f"Pagina '{res_title}' non trovata su Wikipedia.")
                except Exception as e_page:
                    summaries.append(f"Errore nel recuperare la pagina '{res_title}': {str(e_page)}")
            return "\n\n".join(summaries) if summaries else "Nessun sommario trovato per i risultati di Wikipedia."
        except Exception as e:
            return f"Errore durante la ricerca su Wikipedia: {str(e)}"

    def _search_tavily(self, query: str) -> str:
        print(f"Ricerca Tavily per: {query}")
        if not self.tavily_client:
            return "Errore: Tavily API key non configurata. Impossibile eseguire la ricerca."
        try:
            # Puoi personalizzare i parametri di ricerca di Tavily qui
            # ad esempio: search_depth="advanced", include_answer=True, include_raw_content=False
            response = self.tavily_client.search(
                query=query,
                search_depth="basic", # "basic" per velocità, "advanced" per risposte più approfondite
                max_results=MAX_TAVILY_RESULTS,
                include_answer=True # Chiede a Tavily di provare a fornire una risposta diretta
            )
            
            results_str = ""
            if response.get("answer"):
                results_str += f"Risposta diretta da Tavily: {response['answer']}\n\n"

            if response.get("results"):
                results_str += "Risultati della ricerca:\n"
                for r in response["results"]:
                    results_str += f"Title: {r.get('title', 'N/A')}\nURL: {r.get('url', 'N/A')}\nContent Snippet: {r.get('content', 'N/A')[:500]}...\n\n"
            
            return results_str if results_str else "Nessun risultato significativo trovato da Tavily."

        except Exception as e:
            return f"Errore durante la ricerca su Tavily: {str(e)}"

    def _search_arxiv(self, query: str) -> str:
        print(f"Ricerca Arxiv per: {query}")
        try:
            search = arxiv.Search(
                query=query,
                max_results=MAX_ARXIV_RESULTS,
                sort_by=arxiv.SortCriterion.Relevance
            )
            client = arxiv.Client()
            results_data = []
            for r in client.results(search):
                authors = ", ".join([str(a) for a in r.authors])
                results_data.append(f"Title: {r.title}\nAuthors: {authors}\nPublished: {r.published.date()}\nSummary: {r.summary.replace_summary_newline_chars_with_spaces()[:700]}...\nLink: {r.pdf_url}")
            return "\n\n".join(results_data) if results_data else "Nessun risultato trovato su Arxiv."
        except Exception as e:
            return f"Errore durante la ricerca su Arxiv: {str(e)}"

    def __call__(self, question: str) -> str:
        print(f"GaiaAgent ha ricevuto la domanda (primi 100 caratteri): {question[:100]}...")
        if not self.openai_client.api_key:
            return "Errore: OPENAI_API_KEY non configurata o non valida per l'agente."
        if not self.tavily_client:
             print("Avviso: Tavily client non inizializzato. La ricerca web non sarà disponibile.")


        tools = [
            {"type": "function", "function": {
                "name": "search_wikipedia",
                "description": "Cerca informazioni su Wikipedia. Utile per conoscenza generale, fatti, definizioni, storia.",
                "parameters": {"type": "object", "properties": {"query": {"type": "string", "description": "La query di ricerca per Wikipedia."}}, "required": ["query"]}}},
            {"type": "function", "function": {
                "name": "search_tavily", # Nome dello strumento aggiornato
                "description": "Cerca sul web usando l'API di Tavily. Utile per eventi attuali, informazioni specifiche, siti web, o quando Wikipedia non è sufficiente. Può fornire risposte dirette e snippet di contenuto.",
                "parameters": {"type": "object", "properties": {"query": {"type": "string", "description": "La query di ricerca per Tavily."}}, "required": ["query"]}}},
            {"type": "function", "function": {
                "name": "search_arxiv",
                "description": "Cerca su Arxiv articoli scientifici e preprint (fisica, matematica, informatica, ecc.).",
                "parameters": {"type": "object", "properties": {"query": {"type": "string", "description": "La query di ricerca per Arxiv (es. autore, titolo, parole chiave)."}}, "required": ["query"]}}},
            {"type": "function", "function": {
                "name": "execute_python_code",
                "description": "Esegue una stringa di codice Python e restituisce il suo output. Usare per calcoli, manipolazione dati o qualsiasi task che richieda esecuzione di codice. Assicurarsi che il codice stampi il risultato su stdout. Il codice viene eseguito in un ambiente stateless.",
                "parameters": {"type": "object", "properties": {"code_string": {"type": "string", "description": "Il codice Python da eseguire. Esempio: 'print(1+1)'"}}, "required": ["code_string"]}}}
        ]

        system_prompt = (
            "You are a general AI assistant that uses the tools available. I will ask you a question. You must think and output only the exact answer to the question with no comments, so your final answer must be a number OR as few words as possible OR a comma separated list of numbers and/or strings. If you are asked for a number, don't use comma to write your number neither use units such as $ or percent sign unless specified otherwise. If you are asked for a string, don't use articles, neither abbreviations (e.g. for cities), and write the digits in plain text unless specified otherwise. If you are asked for a comma separated list, apply the above rules depending of whether the element to be put in the list is a number or a string. If you are asked for a code number, give the code number nothing else. Do not add a dot at the end of the answer. Pay attention at the question and at the expected output."
            )

        messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": question}]

        for iteration in range(MAX_TOOL_ITERATIONS):
            print(f"Agente: Chiamata OpenAI. Iterazione {iteration + 1}. Ultimo messaggio: {messages[-1]['role']}")
            
            api_response_or_error = self._call_openai_api(messages, tools=tools, tool_choice="auto")

            if isinstance(api_response_or_error, str):
                return api_response_or_error

            response_message = api_response_or_error.choices[0].message

            if response_message.tool_calls:
                print(f"OpenAI suggerisce chiamate a strumenti: {[tc.function.name for tc in response_message.tool_calls]}")
                messages.append(response_message)
                
                for tool_call in response_message.tool_calls:
                    function_name = tool_call.function.name
                    try:
                        function_args = json.loads(tool_call.function.arguments)
                    except json.JSONDecodeError as e:
                        print(f"Errore nel decodificare gli argomenti JSON per {function_name}: {e}")
                        tool_output = f"Errore: argomenti JSON invalidi per {function_name}."
                        messages.append({"tool_call_id": tool_call.id, "role": "tool", "name": function_name, "content": tool_output})
                        continue

                    print(f"Esecuzione strumento: {function_name} con argomenti: {function_args}")
                    tool_output = ""
                    if function_name == "search_wikipedia":
                        tool_output = self._search_wikipedia(query=function_args.get("query",""))
                    elif function_name == "search_tavily": # Aggiornato per chiamare Tavily
                        if not self.tavily_client:
                            tool_output = "Errore: Tavily client non inizializzato. Impossibile eseguire la ricerca Tavily."
                        else:
                            tool_output = self._search_tavily(query=function_args.get("query",""))
                    elif function_name == "search_arxiv":
                        tool_output = self._search_arxiv(query=function_args.get("query",""))
                    elif function_name == "execute_python_code":
                        tool_output = self._execute_python_code(code_string=function_args.get("code_string",""))
                    else:
                        tool_output = f"Strumento sconosciuto: {function_name}"
                    
                    print(f"Output strumento {function_name} (primi 100 caratteri): {str(tool_output)[:100]}")
                    messages.append({"tool_call_id": tool_call.id, "role": "tool", "name": function_name, "content": str(tool_output)})
            
            else:
                final_answer = response_message.content
                if final_answer:
                    print(f"Agente restituisce risposta finale (primi 200 caratteri): {final_answer[:200]}")
                    return final_answer
                else:
                    print("L'LLM non ha chiamato strumenti e non ha fornito contenuto. Tento di forzare una risposta.")
                    messages.append({"role": "user", "content": "Please provide the best possible answer based on the information you have gathered so far, without using any other tools."})
                    final_attempt_response = self._call_openai_api(messages)
                    if isinstance(final_attempt_response, str): return final_attempt_response
                    if final_attempt_response and final_attempt_response.choices[0].message.content:
                        return final_attempt_response.choices[0].message.content
                    return "L'agente ha ricevuto una risposta finale vuota e non è riuscito a generarne una alternativa."

        print("L'agente ha raggiunto il numero massimo di iterazioni degli strumenti.")
        messages.append({"role": "user", "content": "You have reached your tool usage limit. Please provide the best possible answer based on the information you have gathered so far."})
        final_summary_response = self._call_openai_api(messages)
        if isinstance(final_summary_response, str): return final_summary_response
        if final_summary_response and final_summary_response.choices[0].message.content:
            return final_summary_response.choices[0].message.content
        return "L'agente ha raggiunto il numero massimo di iterazioni e non è riuscito a formulare una risposta finale."