Aya1610 commited on
Commit
1db3abb
·
verified ·
1 Parent(s): 9ee0e03

Update agent.py

Browse files
Files changed (1) hide show
  1. agent.py +254 -284
agent.py CHANGED
@@ -1,6 +1,7 @@
1
  # GAIA Agent Solution with LangGraph and OpenAI - Standalone Version
2
  import os
3
  import operator
 
4
  import json
5
  import re
6
  import requests
@@ -16,330 +17,299 @@ from langchain_openai import ChatOpenAI
16
  from langchain_core.tools import tool
17
  from langchain_core.utils.function_calling import convert_to_openai_tool
18
  from openai import OpenAI
 
 
 
 
 
19
 
 
 
 
20
 
 
 
 
 
 
 
 
 
21
 
22
- # Set your OpenAI API key
23
- openai_api_key = os.getenv("OPENAI_API_KEY") # Replace with your actual key
24
 
25
 
26
- # ---------------------
27
- # Tool Definitions
28
- # ---------------------
29
 
30
- # Web Search Tool
31
- search_tool = DuckDuckGoSearchResults(max_results=3)
32
- wikipedia = WikipediaQueryRun(api_wrapper=WikipediaAPIWrapper(top_k_results=3))
33
 
34
- #video analysis
35
  @tool
36
- def analyze_youtube_video(youtube_link: str, question: str) -> str:
37
- """
38
- Analyze a YouTube video to answer a specific question about its content.
39
- Returns the answer to the question based on the video's transcript.
40
-
41
- Args:
42
- youtube_link: URL of the YouTube video
43
- question: Specific question about the video content
44
-
45
- Returns:
46
- Answer to the question or error message
47
- """
48
- # Extract video ID from various YouTube URL formats
49
- def extract_video_id(url):
50
- regex = r"(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^\"&?\/\s]{11})"
51
- match = re.search(regex, url)
52
- return match.group(1) if match else None
53
-
54
- try:
55
- # Get video ID
56
- video_id = extract_video_id(youtube_link)
57
- if not video_id:
58
- return "Error: Invalid YouTube URL format"
59
-
60
- # Get transcript
61
- transcript = YouTubeTranscriptApi.get_transcript(video_id)
62
- transcript_text = " ".join([entry['text'] for entry in transcript])
63
-
64
- # Use OpenAI to answer the question based on transcript
65
- client = OpenAI()
66
- response = client.chat.completions.create(
67
- model="gpt-4-turbo",
68
- messages=[
69
- {"role": "system", "content": "Answer the user's question based EXCLUSIVELY on the video transcript below. Be precise and quote directly when possible."},
70
- {"role": "user", "content": f"Question: {question}\n\nTranscript:\n{transcript_text}"}
71
- ],
72
- max_tokens=300
73
- )
74
-
75
- return response.choices[0].message.content
76
-
77
- except Exception as e:
78
- return f"Error: {str(e)}"
79
 
80
- # Image Description Tool (using GPT-4 Vision)
81
  @tool
82
- def describe_image(image_url: str) -> str:
83
- """Generate detailed description of an image from its URL"""
84
- vision_client = OpenAI()
85
-
86
- # Handle GAIA-style image references
87
- if not image_url.startswith("http"):
88
- return "Error: Invalid image URL format"
89
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  try:
91
- response = vision_client.chat.completions.create(
92
- model="gpt-4-vision-preview",
93
- messages=[
94
- {
95
- "role": "user",
96
- "content": [
97
- {"type": "text", "text": "Describe this image in extreme detail. Include all text, objects, colors, context, and any identifiable information."},
98
- {"type": "image_url", "image_url": {"url": image_url}}
99
- ]
100
- }
101
- ],
102
- max_tokens=1000
103
  )
104
- return response.choices[0].message.content
105
  except Exception as e:
106
- return f"Error describing image: {str(e)}"
107
 
108
- # Math Tool with safe evaluation
109
  @tool
110
- def calculate(expression: str) -> Union[float, str]:
111
- """Evaluate mathematical expressions using safe methods"""
112
  try:
113
- # Safe evaluation for basic math
114
- if re.match(r"^[\d\+\-\*\/\s\.\(\)]+$", expression):
115
- return eval(expression)
116
- else:
117
- return "Error: Only basic math operations allowed"
 
 
 
 
118
  except Exception as e:
119
- return f"Calculation error: {str(e)}"
120
 
121
- # Date Conversion Tool
122
  @tool
123
- def convert_date(date_str: str, format: str) -> str:
124
- """Convert dates between formats (e.g. 'January 5, 2023' to '2023-01-05')"""
125
- from datetime import datetime
126
  try:
127
- # Try common date formats
128
- for fmt in ("%B %d, %Y", "%d %B %Y", "%m/%d/%Y", "%Y-%m-%d"):
129
- try:
130
- dt = datetime.strptime(date_str, fmt)
131
- return dt.strftime(format)
132
- except:
133
- continue
134
- return "Error: Unsupported date format"
135
  except Exception as e:
136
- return f"Date conversion error: {str(e)}"
137
 
138
- # Currency Conversion Tool
139
- @tool
140
- def convert_currency(amount: float, from_currency: str, to_currency: str) -> float:
141
- """Convert currency using current exchange rates"""
142
- # Simplified version for demonstration
143
- rates = {
144
- "USD": {"EUR": 0.93, "GBP": 0.80, "JPY": 154.62},
145
- "EUR": {"USD": 1.07, "GBP": 0.86, "JPY": 166.26},
146
- "GBP": {"USD": 1.25, "EUR": 1.16, "JPY": 193.27},
147
- "JPY": {"USD": 0.0065, "EUR": 0.0060, "GBP": 0.0052}
148
- }
149
-
150
- try:
151
- return round(amount * rates[from_currency.upper()][to_currency.upper()], 2)
152
- except:
153
- return "Error: Currency not supported"
154
- @tool
155
- def process_audio_note(audio_url: str, instructions: str) -> str:
156
- """
157
- Extract specific information from an audio note based on user instructions.
158
- Handles various requests like recipes, meeting notes, reminders, etc.
159
-
160
- Args:
161
- audio_url: URL of the audio file
162
- instructions: Specific instructions for what to extract and how to format
163
-
164
- Returns:
165
- Requested information formatted as specified
166
- """
167
  try:
168
- # Download audio file
169
- response = requests.get(audio_url)
170
- response.raise_for_status()
171
-
172
- # Create temporary audio file
173
- with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as temp_audio:
174
- temp_audio.write(response.content)
175
- temp_audio_path = temp_audio.name
176
-
177
- # Transcribe audio using Whisper
178
- client = OpenAI()
179
- with open(temp_audio_path, "rb") as audio_file:
180
- transcript = client.audio.transcriptions.create(
181
- model="whisper-large-v3",
182
- file=audio_file,
183
- response_format="text"
184
- )
185
-
186
- # Create system prompt based on instructions
187
- system_prompt = (
188
- "You're an audio processing assistant. Carefully follow these instructions:\n"
189
- f"{instructions}\n\n"
190
- "Transcript of the audio note:\n"
191
  )
192
-
193
- # Process transcript to extract requested information
194
- response = client.chat.completions.create(
195
- model="gpt-4-turbo",
196
- messages=[
197
- {"role": "system", "content": system_prompt},
198
- {"role": "user", "content": transcript}
199
- ],
200
- max_tokens=1000
201
  )
202
-
203
- return response.choices[0].message.content
204
-
205
  except Exception as e:
206
- return f"Error processing audio: {str(e)}"
207
- # ---------------------
208
- # Agent Setup
209
- # ---------------------
210
 
211
- # Available tools
212
- tools = [search_tool, wikipedia, analyze_youtube_video, describe_image, calculate, convert_date, convert_currency, process_audio_note]
213
- tools_as_openai = [convert_to_openai_tool(t) for t in tools]
 
 
 
 
 
 
 
 
 
214
 
215
- # Agent State Definition
216
- class AgentState(TypedDict):
217
- messages: Annotated[Sequence[BaseMessage], operator.add]
218
 
219
- # Initialize LLM
220
- model = ChatOpenAI(model="gpt-4-turbo", temperature=0)
221
 
222
- # ---------------------
223
- # Helper Functions
224
- # ---------------------
225
 
226
- def extract_image_url(messages: Sequence[BaseMessage], reference: str) -> str:
227
- """Extract actual image URL from message context using regex"""
228
- # Search all messages for image URLs
229
- url_pattern = r"https?://[^\s]+?\.(?:jpg|jpeg|png|gif)"
230
-
231
- for msg in messages:
232
- matches = re.findall(url_pattern, msg.content)
233
- if matches:
234
- # Return the first match if we have a reference hint
235
- if reference.lower() in msg.content.lower():
236
- return matches[0]
237
- # Otherwise just return any found image URL
238
- return matches[0]
239
 
240
- # Fallback to GAIA image reference format
241
- return f"https://gaia-benchmark.com/images/{reference}.jpg"
242
-
243
- # ---------------------
244
- # Graph Nodes
245
- # ---------------------
246
-
247
- def run_agent(state: AgentState):
248
- """Node: Run the agent's reasoning"""
249
- messages = state["messages"]
250
- response = model.invoke(messages, tools=tools_as_openai)
251
- return {"messages": [response]}
252
-
253
- def run_tools(state: AgentState):
254
- """Node: Execute tools based on agent's request"""
255
- messages = state["messages"]
256
- last_message = messages[-1]
257
 
258
- tool_messages = []
 
259
 
260
- # Safely handle tool calls
261
- if hasattr(last_message, 'tool_calls') and last_message.tool_calls:
262
- for tool_call in last_message.tool_calls:
263
- try:
264
- # Safely get name and arguments with defaults
265
- function_name = tool_call.get("name", "unknown_tool")
266
- function_args = tool_call.get("args", {})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
 
268
- # Find matching tool
269
- tool = next((t for t in tools if t.name == function_name), None)
 
 
270
 
271
- if tool:
272
- try:
273
- # Special handling for image URLs
274
- if function_name == "describe_image":
275
- image_url = function_args.get("image_url", "")
276
- if not image_url.startswith("http"):
277
- # Extract image URL from context
278
- function_args["image_url"] = extract_image_url(messages, image_url)
 
 
 
 
 
 
 
 
 
 
 
 
279
 
280
- # Execute tool
281
- output = tool.invoke(function_args)
282
- content = f"Tool Result: {str(output)}"
283
- except Exception as e:
284
- content = f"Tool Error: {str(e)}"
285
- else:
286
- content = f"Tool {function_name} not available"
 
 
 
287
 
288
- tool_messages.append(
289
- ToolMessage(
290
- content=content,
291
- name=function_name,
292
- tool_call_id=tool_call.get("id", "unknown")
293
- )
294
- )
295
- except Exception as e:
296
- # Handle any errors in processing a tool call
297
- content = f"Error processing tool call: {str(e)}"
298
- tool_messages.append(
299
- ToolMessage(
300
- content=content,
301
- name="error_handler",
302
- tool_call_id="unknown"
 
 
 
 
 
 
 
 
 
 
 
303
  )
304
- )
305
- else:
306
- # Handle cases where no tool calls are present
307
- tool_messages.append(
308
- ToolMessage(
309
- content="No tools were called in the last message",
310
- name="no_tool_called",
311
- tool_call_id="none"
312
- )
313
- )
314
 
315
- return {"messages": tool_messages}
 
316
 
317
- # ---------------------
318
- # Graph Construction
319
- # ---------------------
320
- def should_continue(state: AgentState):
321
- """Decision function for graph flow"""
322
- last_message = state["messages"][-1]
323
- if hasattr(last_message, 'tool_calls') and last_message.tool_calls:
324
- return "run_tools"
325
- return "end"
326
-
327
- def build_graph():
328
- """Build and compile the LangGraph agent"""
329
- # Build the graph
330
- graph = StateGraph(AgentState)
331
- graph.add_node("run_agent", run_agent)
332
- graph.add_node("run_tools", run_tools)
333
- graph.set_entry_point("run_agent")
334
 
335
- graph.add_conditional_edges(
336
- "run_agent",
337
- should_continue,
338
- {
339
- "run_tools": "run_tools",
340
- "end": END
341
- }
342
- )
343
 
344
- graph.add_edge("run_tools", "run_agent")
345
- return graph.compile()
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  # GAIA Agent Solution with LangGraph and OpenAI - Standalone Version
2
  import os
3
  import operator
4
+ from dotenv import load_dotenv
5
  import json
6
  import re
7
  import requests
 
17
  from langchain_core.tools import tool
18
  from langchain_core.utils.function_calling import convert_to_openai_tool
19
  from openai import OpenAI
20
+ from langgraph.graph import START, StateGraph, MessagesState
21
+ from langgraph.prebuilt import tools_condition, ToolNode
22
+ from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
23
+ from langchain_core.tools import tool
24
+ load_dotenv()
25
 
26
+ # --- Supabase Setup (only if credentials are provided) ---
27
+ supabase_url = os.getenv("SUPABASE_URL")
28
+ supabase_key = os.getenv("SUPABASE_SERVICE_KEY") or os.getenv("SUPABASE_KEY")
29
 
30
+ if supabase_url and supabase_key:
31
+ from supabase.client import Client, create_client
32
+ from langchain_community.vectorstores import SupabaseVectorStore
33
+ from langchain.tools.retriever import create_retriever_tool
34
+ from langchain_openai import OpenAIEmbeddings
35
+ supabase: Client = create_client(supabase_url, supabase_key)
36
+ else:
37
+ supabase = None
38
 
39
+ # --- Standard Imports ---
 
40
 
41
 
42
+ # OpenAI LLM
43
+ from langchain_openai import ChatOpenAI
 
44
 
45
+ # Optional document loaders
46
+ from langchain_community.tools.tavily_search import TavilySearchResults
47
+ from langchain_community.document_loaders import WikipediaLoader, ArxivLoader
48
 
49
+ # --- Simple Math Tools ---
50
  @tool
51
+ def multiply(a: int, b: int) -> int:
52
+ """Multiply two integers and return the result"""
53
+ return a * b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
 
55
  @tool
56
+ def add(a: int, b: int) -> int:
57
+ """Add two integers and return the sum"""
58
+ return a + b
59
+
60
+ @tool
61
+ def subtract(a: int, b: int) -> int:
62
+ """Subtract the second integer from the first and return the difference"""
63
+ return a - b
64
+
65
+ @tool
66
+ def divide(a: int, b: int) -> float:
67
+ """Divide the first integer by the second and return the quotient"""
68
+ if b == 0:
69
+ raise ValueError("Cannot divide by zero.")
70
+ return a / b
71
+
72
+ @tool
73
+ def modulus(a: int, b: int) -> int:
74
+ """Return the modulus of dividing the first integer by the second"""
75
+ return a % b
76
+
77
+ # --- Search Tools ---
78
+ @tool
79
+ def wiki_search(query: str) -> str:
80
+ """Search Wikipedia for the query and return up to 2 documents"""
81
  try:
82
+ docs = WikipediaLoader(query=query, load_max_docs=2).load()
83
+ return "\n\n---\n\n".join(
84
+ f'<Document source="{doc.metadata["source"]}"/>\n{doc.page_content}' for doc in docs
 
 
 
 
 
 
 
 
 
85
  )
 
86
  except Exception as e:
87
+ return f"Wikipedia search failed: {str(e)}"
88
 
 
89
  @tool
90
+ def web_search(query: str) -> str:
91
+ """Search the web using Tavily and return up to 3 results"""
92
  try:
93
+ tavily_api_key = os.getenv("search")
94
+ if not tavily_api_key:
95
+ return "Web search unavailable: TAVILY_API_KEY not configured"
96
+
97
+ search_tool = TavilySearchResults(max_results=3, api_key=tavily_api_key)
98
+ docs = search_tool.invoke({"query": query})
99
+ return "\n\n---\n\n".join(
100
+ f'<Document source="{doc.get("url", "Unknown")}"/>\n{doc.get("content", "")}' for doc in docs
101
+ )
102
  except Exception as e:
103
+ return f"Web search failed: {str(e)}"
104
 
 
105
  @tool
106
+ def arxiv_search(query: str) -> str:
107
+ """Search Arxiv for the query and return up to 3 documents"""
 
108
  try:
109
+ docs = ArxivLoader(query=query, load_max_docs=3).load()
110
+ return "\n\n---\n\n".join(
111
+ f'<Document source="{doc.metadata["source"]}"/>\n{doc.page_content[:1000]}' for doc in docs
112
+ )
 
 
 
 
113
  except Exception as e:
114
+ return f"Arxiv search failed: {str(e)}"
115
 
116
+ # --- Assemble Tools List ---
117
+ tools = [multiply, add, subtract, divide, modulus, wiki_search, web_search, arxiv_search]
118
+
119
+ # If supabase is configured, add retriever tool
120
+ if supabase:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  try:
122
+ embeddings = OpenAIEmbeddings()
123
+ vector_store = SupabaseVectorStore(
124
+ client=supabase,
125
+ embedding=embeddings,
126
+ table_name="documents",
127
+ query_name="match_documents_langchain",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  )
129
+ retriever_tool = create_retriever_tool(
130
+ retriever=vector_store.as_retriever(),
131
+ name="Question Search",
132
+ description="Retrieve similar questions from the vector store",
 
 
 
 
 
133
  )
134
+ tools.append(retriever_tool)
 
 
135
  except Exception as e:
136
+ print(f"Could not initialize Supabase retriever: {e}")
 
 
 
137
 
138
+ # --- Load System Prompt ---
139
+ def load_system_prompt():
140
+ """Load system prompt with fallback"""
141
+ try:
142
+ with open("system_prompt.txt", "r", encoding="utf-8") as f:
143
+ return SystemMessage(content=f.read())
144
+ except FileNotFoundError:
145
+ # Fallback system prompt
146
+ default_prompt = """You are a helpful AI assistant with access to various tools including:
147
+ - Math operations (add, subtract, multiply, divide, modulus)
148
+ - Search capabilities (Wikipedia, Arxiv, web search via Tavily)
149
+ - Information retrieval
150
 
151
+ Use these tools when appropriate to answer questions accurately and helpfully. When performing calculations, always use the provided math tools. When users ask for information that might require current data or research, use the appropriate search tools.
 
 
152
 
153
+ Be concise but thorough in your responses. If you use a tool, explain what you found or calculated."""
154
+ return SystemMessage(content=default_prompt)
155
 
156
+ sys_msg = load_system_prompt()
 
 
157
 
158
+ # --- Graph Builder (OpenAI) ---
159
+ def build_graph():
160
+ """
161
+ Build and return a StateGraph using OpenAI ChatGPT with tools.
162
+ """
163
+ print("=== BUILDING OPENAI GRAPH ===")
 
 
 
 
 
 
 
164
 
165
+ # Check for OpenAI API key
166
+ openai_api_key = os.getenv("OPENAI_API_KEY")
167
+ print(f"OpenAI API Key: {'Found' if openai_api_key else 'Not found'}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
 
169
+ if openai_api_key:
170
+ print(f"API Key starts with: {openai_api_key[:10]}...")
171
 
172
+ try:
173
+ if openai_api_key and len(openai_api_key.strip()) > 0:
174
+ print("Attempting to initialize OpenAI ChatGPT...")
175
+
176
+ # Initialize OpenAI LLM
177
+ llm = ChatOpenAI(
178
+ model="gpt-3.5-turbo", # You can change to "gpt-4" if you have access
179
+ temperature=0.1,
180
+ api_key=openai_api_key.strip(),
181
+ max_tokens=512
182
+ )
183
+
184
+ # Test the connection
185
+ test_response = llm.invoke([HumanMessage(content="Hello")])
186
+ print("✓ Successfully connected to OpenAI")
187
+ print(f"Test response: {test_response.content[:50]}...")
188
+
189
+ else:
190
+ raise Exception("No valid OPENAI_API_KEY found")
191
+
192
+ except Exception as e:
193
+ print(f"Error initializing OpenAI LLM: {e}")
194
+ print("Creating functional mock LLM...")
195
+
196
+ class FunctionalMockLLM:
197
+ def bind_tools(self, tools):
198
+ self.tools = tools
199
+ return self
200
 
201
+ def invoke(self, messages):
202
+ from langchain_core.messages import AIMessage
203
+ import json
204
+ import re
205
 
206
+ last_msg = messages[-1] if messages else None
207
+ if not last_msg:
208
+ return AIMessage(content="Please ask me a question!")
209
+
210
+ content = getattr(last_msg, 'content', str(last_msg))
211
+ content_lower = content.lower()
212
+
213
+ # Handle math operations with tool calls
214
+ math_patterns = [
215
+ (r'(\d+)\s*\+\s*(\d+)', 'add'),
216
+ (r'(\d+)\s*-\s*(\d+)', 'subtract'),
217
+ (r'(\d+)\s*\*\s*(\d+)', 'multiply'),
218
+ (r'(\d+)\s*/\s*(\d+)', 'divide'),
219
+ (r'(\d+)\s*%\s*(\d+)', 'modulus'),
220
+ ]
221
+
222
+ for pattern, operation in math_patterns:
223
+ match = re.search(pattern, content)
224
+ if match:
225
+ a, b = int(match.group(1)), int(match.group(2))
226
 
227
+ tool_call = {
228
+ "name": operation,
229
+ "args": {"a": a, "b": b},
230
+ "id": f"call_{operation}_{a}_{b}"
231
+ }
232
+
233
+ return AIMessage(
234
+ content=f"I'll {operation} {a} and {b} for you.",
235
+ tool_calls=[tool_call]
236
+ )
237
 
238
+ # Handle search requests
239
+ if any(word in content_lower for word in ['search', 'find', 'look up', 'what is', 'who is', 'tell me about']):
240
+ # Extract search query
241
+ search_query = content
242
+ for phrase in ['search for', 'find', 'look up', 'what is', 'who is', 'tell me about']:
243
+ search_query = search_query.lower().replace(phrase, '').strip()
244
+
245
+ if len(search_query) > 100:
246
+ search_query = search_query[:100]
247
+
248
+ if 'wikipedia' in content_lower:
249
+ tool_name = "wiki_search"
250
+ elif 'arxiv' in content_lower or 'research' in content_lower or 'paper' in content_lower:
251
+ tool_name = "arxiv_search"
252
+ else:
253
+ tool_name = "web_search"
254
+
255
+ tool_call = {
256
+ "name": tool_name,
257
+ "args": {"query": search_query},
258
+ "id": f"call_{tool_name}_{hash(search_query) % 1000}"
259
+ }
260
+
261
+ return AIMessage(
262
+ content=f"I'll search for information about: {search_query}",
263
+ tool_calls=[tool_call]
264
  )
265
+
266
+ # Default response for other questions
267
+ return AIMessage(content=f"I understand you're asking: {content[:200]}... I can help with math calculations and information searches. Please configure OPENAI_API_KEY for full functionality, or try asking me to calculate something or search for information.")
268
+
269
+ llm = FunctionalMockLLM()
270
+ print(" Using functional mock LLM")
 
 
 
 
271
 
272
+ # Bind tools to LLM
273
+ llm_with_tools = llm.bind_tools(tools)
274
 
275
+ def retriever(state: MessagesState):
276
+ """Add system message and handle retrieval if Supabase is available"""
277
+ messages = [sys_msg] + state["messages"]
278
+
279
+ if supabase and len(tools) > 8: # Check if retriever tool was added
280
+ try:
281
+ query = state["messages"][-1].content
282
+ docs = vector_store.similarity_search(query, k=1)
283
+ if docs:
284
+ doc = docs[0]
285
+ content = doc.page_content
286
+ answer = content.split("Final answer :")[-1].strip() if "Final answer :" in content else content.strip()
287
+ return {"messages": messages + [AIMessage(content=f"Retrieved context: {answer}")]}
288
+ except Exception as e:
289
+ print(f"Retrieval error: {e}")
290
+
291
+ return {"messages": messages}
292
 
293
+ def assistant(state: MessagesState):
294
+ """Main assistant function"""
295
+ try:
296
+ response = llm_with_tools.invoke(state["messages"])
297
+ return {"messages": [response]}
298
+ except Exception as e:
299
+ print(f"Assistant error: {e}")
300
+ return {"messages": [AIMessage(content=f"I encountered an error: {str(e)}. Please make sure your OPENAI_API_KEY is configured correctly.")]}
301
 
302
+ # Build the graph
303
+ g = StateGraph(MessagesState)
304
+ g.add_node("retriever", retriever)
305
+ g.add_node("assistant", assistant)
306
+ g.add_node("tools", ToolNode(tools))
307
+
308
+ # Define edges
309
+ g.add_edge(START, "retriever")
310
+ g.add_edge("retriever", "assistant")
311
+ g.add_conditional_edges("assistant", tools_condition)
312
+ g.add_edge("tools", "assistant")
313
+
314
+ print("✓ Graph compiled successfully")
315
+ return g.compile()