22Nikk0 commited on
Commit
94a2b99
·
verified ·
1 Parent(s): 2f61648

Update agentic.py

Browse files
Files changed (1) hide show
  1. agentic.py +563 -122
agentic.py CHANGED
@@ -18,7 +18,8 @@ from langfuse.callback import CallbackHandler
18
  import base64
19
  import json
20
  import time
21
- from pprint import pprint
 
22
 
23
  # import boto3
24
 
@@ -34,30 +35,41 @@ load_dotenv()
34
  # Initialize Langfuse CallbackHandler for LangGraph/Langchain (tracing)
35
  langfuse_handler = CallbackHandler()
36
 
 
37
  class State(TypedDict):
38
  """
39
  A class representing the state of the agent.
40
  """
41
- answer: str
42
  question: str
43
  messages: Annotated[list[AnyMessage], add_messages]
44
  input_file: str
 
 
 
 
 
 
 
 
 
45
 
 
46
 
47
- def get_assistant_model():
 
48
 
49
  llm_provider = os.getenv("LLM_PROVIDER", "mistral")
50
 
51
  if llm_provider == "mistral":
52
- assistant_model = ChatMistralAI(
53
- model="mistral-small-latest",#"ministral-8b-latest",#"mistral-large-2411",#
54
  temperature=0,
55
  max_retries=2,
56
  api_key=os.getenv("MISTRAL_API_KEY")
57
  )
58
 
59
  if llm_provider == "aws":
60
- assistant_model = ChatBedrock(
61
  model_id="arn:aws:bedrock:us-east-1:416545197702:inference-profile/us.amazon.nova-lite-v1:0",
62
  # provider="amazon",
63
  temperature=0,
@@ -66,7 +78,7 @@ def get_assistant_model():
66
  aws_secret_access_key=os.getenv("AWS_SECRET_ACCESS_KEY")
67
  )
68
 
69
- return assistant_model
70
 
71
  def get_vision_model():
72
 
@@ -107,6 +119,21 @@ def get_video_handler_model():
107
 
108
  return video_handler_model
109
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
  def download_youtube_content(url: str, output_path: Optional[str] = None) -> None:
112
  """
@@ -173,41 +200,263 @@ def download_youtube_content(url: str, output_path: Optional[str] = None) -> Non
173
 
174
  return video_file_name
175
 
176
- def search_webpage(state: State, url: str)-> str:
177
- """
178
- Search a web page based on the current state.
179
- """
180
- # Simulate a web page search and return a result
181
- return "search_webpage"
182
 
183
  web_search = DuckDuckGoSearchRun()
184
  wikipedia_search = WikipediaQueryRun(api_wrapper=WikipediaAPIWrapper())
185
 
186
- assistant_model = get_assistant_model()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  vision_model = get_vision_model()
188
  video_handler_model = get_video_handler_model()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
 
190
- def vision_model_call(state: State) -> str:
191
  """
192
- Vision model that can analyze images and picture and answer questions about them.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
 
194
- Args:
195
- state (State) with input_file key and question key inside
 
 
 
 
 
 
 
 
196
 
197
- Returns:
198
- answer(string)
199
  """
 
 
 
 
200
  prompt = f"""
201
- You are a powerful vision assistant, you can analyze images and answer question about the picture
202
- The question is : {state["question"]}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  """
204
- # You need to consider system prompt of the first assistant to answer.
205
- # Here is the system prompt: {state}
206
- # """
207
 
208
  image_base64 = ""
209
  try:
210
- with open(state["input_file"], "rb") as image_file:
211
  image_bytes = image_file.read()
212
 
213
  image_base64 = base64.b64encode(image_bytes).decode("utf-8")
@@ -244,46 +493,68 @@ def vision_model_call(state: State) -> str:
244
  }
245
  ]
246
 
247
- response = vision_model.invoke(
248
  input=message,
249
- config={
250
- "callbacks": [langfuse_handler]
251
- }
252
- )
253
 
254
- return response.content + "\n\n"
 
 
 
 
255
 
256
  except Exception as e:
257
  # A butler should handle errors gracefully
258
  error_msg = f"Error extracting text: {str(e)}"
259
  print(error_msg)
260
- return ""
261
 
262
- def video_handler_model_call(state: State, video_url: str) -> str:
263
  """
264
- Video handler model that can analyze videos and answer questions about them.
 
 
 
265
 
266
- Args:
267
- state (State): with question key inside
268
- video_url (str): URL of the YouTube video to analyze.
269
 
270
- Returns:
271
- answer(string)
272
  """
273
 
274
  prompt = f"""
275
- You are a highly capable video analysis assistant. Your task is to watch and analyze the provided video content and answer the user's question as accurately and concisely as possible.
276
-
277
- Instructions:
278
- - Carefully observe the video, paying attention to relevant details, actions, and context.
279
- - Focus on the user's question: "{state['question']}"
280
- - If the question requires counting, identifying, or describing, be precise and clear in your response.
281
- - If you are unsure, state what you can infer from the video.
282
- - Do not make up information that is not visible or inferable from the video.
283
-
 
 
 
 
 
284
  """
285
 
286
- downloaded_video = download_youtube_content(url=video_url)
 
 
 
 
 
 
 
 
 
 
 
287
 
288
  print(f"Downloaded video: {downloaded_video}")
289
 
@@ -311,113 +582,283 @@ def video_handler_model_call(state: State, video_url: str) -> str:
311
  }
312
  ]
313
 
314
- response = video_handler_model.invoke(
315
  input=message,
316
- config={
317
- "callbacks": [langfuse_handler]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
318
  }
319
- )
320
-
321
- return response.content + "\n\n"
322
-
323
- # Tools
324
- tools = [
325
- web_search,
326
- # search_webpage,
327
- wikipedia_search,
328
- vision_model_call,
329
- video_handler_model_call
330
- ]
331
 
332
- assistant_with_tools = assistant_model.bind_tools(tools, parallel_tool_calls=False)
 
 
 
 
 
333
 
 
334
 
335
- def assistant(state: State)-> str:
336
- # System message
337
- textual_description_of_tool=f"""
338
- web_search:
339
- {web_search.description}
340
- Args:
341
- {web_search.args_schema}
342
- Returns:
343
- {web_search.response_format}
344
-
345
- wikipedia_search:
346
- {wikipedia_search.description}
347
- Args:
348
- {wikipedia_search.args_schema}
349
- Returns:
350
- {wikipedia_search.response_format}
351
-
352
- vision_model_call:
353
- {vision_model_call.__doc__}
354
- Args:
355
- {vision_model_call.__annotations__}
356
- Returns:
357
- {vision_model_call.__annotations__['return']}
358
-
359
- video_handler_model_call:
360
- {video_handler_model_call.__doc__}
361
- Args:
362
- {video_handler_model_call.__annotations__}
363
- Returns:
364
- {video_handler_model_call.__annotations__['return']}
365
- """
366
 
367
- with open("./prompt.txt", "r") as prompt_file:
368
- system_prompt = prompt_file.read()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
 
370
- sys_msg = SystemMessage(content=system_prompt+textual_description_of_tool)
371
 
372
- response = [assistant_with_tools.invoke([sys_msg] + state["messages"])]
 
 
 
 
 
 
 
 
 
 
 
 
373
 
374
  return {
375
- "system_prompt": system_prompt,
376
  "messages": response,
377
- "question": state["question"],
378
- "answer": state.get("answer", "")
379
  }
380
 
 
381
 
382
- def build_graph():
383
- builder = StateGraph(State)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
 
385
- # Nodes
386
- builder.add_node("assistant", assistant)
387
- # builder.add_node("reviewer", reviewer)
388
- builder.add_node("tools", ToolNode(tools))
389
 
390
- # Edges
391
- builder.add_edge(START, "assistant")
392
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
393
  builder.add_conditional_edges(
394
- "assistant",
395
- tools_condition,
 
 
 
 
 
 
 
 
396
  )
397
- builder.add_edge("tools", "assistant")
 
 
 
 
 
 
 
 
398
 
399
  return builder.compile()
400
 
 
 
401
  if __name__ == "__main__":
402
 
403
  agent_graph = build_graph()
404
 
405
- file_name = ""
 
 
 
406
 
407
- # question = "In the video https://www.youtube.com/watch?v=L1vXCYZAYYM, what is the highest number of bird species to be on camera simultaneously?"
408
- 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."
409
- # file_name = question_json.get("file_name", "")
 
 
 
 
 
 
410
 
411
  print(f"QUESTION : {question}")
412
  print(f"FILE: {file_name}")
413
 
414
- messages = [HumanMessage(content=f"Can you answer this question please ? {question}")]
 
 
415
 
416
  messages = agent_graph.invoke(
417
- input={"messages": messages, "question": question, "input_file": file_name},
418
  config={
419
  "recursion_limit": 10,
420
- "callbacks": [langfuse_handler]
421
  }
422
  )
423
 
 
18
  import base64
19
  import json
20
  import time
21
+ import requests
22
+
23
 
24
  # import boto3
25
 
 
35
  # Initialize Langfuse CallbackHandler for LangGraph/Langchain (tracing)
36
  langfuse_handler = CallbackHandler()
37
 
38
+ ######## STATE ########
39
  class State(TypedDict):
40
  """
41
  A class representing the state of the agent.
42
  """
 
43
  question: str
44
  messages: Annotated[list[AnyMessage], add_messages]
45
  input_file: str
46
+ downloaded_file: Optional[str]
47
+ task_id: str
48
+ web_wiki_search_node_result: AnyMessage
49
+ thinking_node_result: AnyMessage
50
+ vision_node_result: AnyMessage
51
+ video_node_result: AnyMessage
52
+ audio_node_result: AnyMessage
53
+ code_node_result: AnyMessage
54
+ next: str
55
 
56
+ ########################
57
 
58
+ ######## MODELS ########
59
+ def get_general_model():
60
 
61
  llm_provider = os.getenv("LLM_PROVIDER", "mistral")
62
 
63
  if llm_provider == "mistral":
64
+ general_model = ChatMistralAI(
65
+ model="mistral-large-2411",#"ministral-8b-latest",#"mistral-small-latest",#"mistral-small-latest",#
66
  temperature=0,
67
  max_retries=2,
68
  api_key=os.getenv("MISTRAL_API_KEY")
69
  )
70
 
71
  if llm_provider == "aws":
72
+ general_model = ChatBedrock(
73
  model_id="arn:aws:bedrock:us-east-1:416545197702:inference-profile/us.amazon.nova-lite-v1:0",
74
  # provider="amazon",
75
  temperature=0,
 
78
  aws_secret_access_key=os.getenv("AWS_SECRET_ACCESS_KEY")
79
  )
80
 
81
+ return general_model
82
 
83
  def get_vision_model():
84
 
 
119
 
120
  return video_handler_model
121
 
122
+ def get_audio_handler_model():
123
+ audio_handler_model = ChatOpenAI(
124
+ model="gpt-4o-audio-preview-2024-12-17",#,gpt-4o-mini-audio-preview-2024-12-17",#
125
+ temperature=0,
126
+ max_tokens=None,
127
+ timeout=None,
128
+ max_retries=2,
129
+ api_key=os.getenv("OPENAI_API_KEY"),
130
+ )
131
+
132
+ return audio_handler_model
133
+
134
+ ########################
135
+
136
+ ######## Functions ########
137
 
138
  def download_youtube_content(url: str, output_path: Optional[str] = None) -> None:
139
  """
 
200
 
201
  return video_file_name
202
 
 
 
 
 
 
 
203
 
204
  web_search = DuckDuckGoSearchRun()
205
  wikipedia_search = WikipediaQueryRun(api_wrapper=WikipediaAPIWrapper())
206
 
207
+ def download_input_file(task_id: str) -> str:
208
+ """
209
+ Download the file specified in state input_file key.
210
+ You only need the task_id to download the file.
211
+
212
+ Args:
213
+ task_id (str): The task_id of the file to download.
214
+
215
+ Returns:
216
+ str: The path to the downloaded file.
217
+ """
218
+
219
+ output_path = os.path.join(os.getcwd(), 'downloads')
220
+
221
+ api_url = os.getenv("DEFAULT_API_URL")
222
+
223
+ # Create output directory if it doesn't exist
224
+ os.makedirs(output_path, exist_ok=True)
225
+
226
+ # Construct the full URL
227
+ url = f"{api_url}/files/{task_id}"
228
+
229
+ try:
230
+ # Send a GET request to download the file
231
+ response = requests.get(url, stream=True)
232
+ response.raise_for_status() # Raise an error for bad status codes
233
+
234
+ headers = dict(response.headers)
235
+ attachement = headers["content-disposition"]
236
+
237
+ regex_result = re.search(r'filename="(.*)"', attachement)
238
+ filename = regex_result.group(1)
239
+
240
+ # Define the output file path
241
+ output_file_path = os.path.join(output_path, filename)
242
+
243
+ # Write the file to the output path
244
+ with open(output_file_path, 'wb') as file:
245
+ for chunk in response.iter_content(chunk_size=8192):
246
+ file.write(chunk)
247
+
248
+ print(f"File downloaded successfully and saved to: {output_file_path}")
249
+
250
+ return output_file_path
251
+
252
+ except requests.exceptions.RequestException as e:
253
+ print(f"An error occurred while downloading the file: {str(e)}")
254
+ return ""
255
+
256
+ ########################
257
+
258
+ ######## LLM associations ########
259
+
260
+ general_model = get_general_model()
261
  vision_model = get_vision_model()
262
  video_handler_model = get_video_handler_model()
263
+ audio_handler_model = get_audio_handler_model()
264
+
265
+ ########################
266
+
267
+ ######## Nodes Definition ########
268
+
269
+ general_model = get_general_model()
270
+
271
+ search_tools = [
272
+ web_search,
273
+ wikipedia_search,
274
+ ]
275
+
276
+ download_file_tool = [ download_input_file ]
277
+
278
+ web_search_node_agent = general_model.bind_tools(search_tools, parallel_tool_calls=False)
279
 
280
+ def thinking_node(state: State) -> dict:
281
  """
282
+ A powerful node to answer general questions, reflection, maths, deduction, prediction.
283
+ This node does not handle files
284
+ This node does not handle images or pictures
285
+ This node does not handle videos
286
+ This node does not handle audio
287
+ This node does not handle code
288
+
289
+ Args:
290
+ state (State): A dictionary containing the current state of the agent, including the 'question' key which holds the question to be answered.
291
+
292
+ Returns:
293
+ dict: A dictionary containing the response from the web search node, with the key 'thinking_node_result' holding the list of messages generated by the general model.
294
+ """
295
+
296
+ prompt = f"""
297
+ You are a powerful assistant that answers general questions, reflection, maths, deduction, prediction.
298
+ You do not handle files
299
+ You do not handle images or pictures
300
+ You do not handle videos
301
+ You do not handle audio
302
+ You do not handle code
303
+
304
+ 1. You need to fully understand the question
305
+ 2. You must think hard about what is relevant in the question to make the best answer
306
+ 3. Report your thought process in detail, explaining your reasoning step-by-step.
307
+
308
+ Here is the question {state['question']}
309
+ Now provide your response immediately without any preamble.
310
+ """
311
+
312
+ state["thinking_node_result"] = state.get("thinking_node_result", "")
313
+
314
+ sys_msg = SystemMessage(content=prompt)
315
+
316
+ thinking_node_response = [general_model.invoke([sys_msg] + [state["thinking_node_result"]])]
317
+
318
+ thinking_node_response[-1].pretty_print()
319
+
320
+ return {
321
+ "thinking_node_result": thinking_node_response,
322
+ }
323
 
324
+ def code_node(state: State) -> dict:
325
+ """
326
+ A powerful node to handle and understand code.
327
+ This node does not handle images or pictures
328
+ This node does not handle videos
329
+ This node does not handle audio
330
+ This node does not access the web
331
+
332
+ Args:
333
+ state (State): A dictionary containing the current state of the agent, including the 'question' key which holds the question to be answered.
334
 
335
+ Returns:
336
+ dict: A dictionary containing the response from the web search node, with the key 'code_node_result' holding the list of messages generated by the general model.
337
  """
338
+
339
+ with open(state["downloaded_file"], "r") as code_file:
340
+ code = code_file.read()
341
+
342
  prompt = f"""
343
+ You are a powerful assistant that handle and understand code.
344
+ You do not handle images or pictures
345
+ You do not handle videos
346
+ You do not handle audio
347
+
348
+ 1. You need to fully understand the question.
349
+ 2. You must think hard about the code and predict the result to answer the question.
350
+ 3. Report your thought process in detail, explaining your reasoning step-by-step.
351
+
352
+ Here is the question : {state['question']}
353
+ Here is the code : {code}
354
+
355
+ Now provide your response immediately without any preamble.
356
+ """
357
+
358
+ sys_msg = SystemMessage(content=prompt)
359
+
360
+ code_node_response = [general_model.invoke([sys_msg])]
361
+
362
+ code_node_response[-1].pretty_print()
363
+
364
+ return {
365
+ "code_node_result": code_node_response,
366
+ }
367
+
368
+ def web_wiki_search_node(state: State) -> dict:
369
+ """
370
+ A powerful node to answer questions and make research on the web based on the question provided in the state.
371
+ This node does not handle files
372
+ This node does not handle images or pictures
373
+ This node does not handle videos
374
+ This node does not handle audio
375
+ This node does not handle code
376
+
377
+ Args:
378
+ state (State): A dictionary containing the current state of the agent, including the 'question' key which holds the question to be answered.
379
+
380
+ Returns:
381
+ dict: A dictionary containing the response from the web search node, with the key 'web_wiki_search_node_result' holding the list of messages generated by the general model.
382
+ """
383
+
384
+ prompt = f"""
385
+ You are a powerful assistant that makes research on the web in order to give the best answer to the question.
386
+ You do not handle files
387
+ You do not handle images or pictures
388
+ You do not handle videos
389
+ You do not handle audio
390
+ You do not handle code
391
+
392
+ 1. You need to fully understand the question
393
+ 2. You must think hard about what is relevant in the question to make the best search with write words
394
+ 3. You must use the best of the tools you have to answer the question precisly
395
+ 4. Report your thought process in detail, explaining your reasoning step-by-step.
396
+
397
+ Here are the tools available:
398
+ web_search:
399
+ {web_search.description}
400
+ Args:
401
+ {web_search.args_schema}
402
+ Returns:
403
+ {web_search.response_format}
404
+
405
+ wikipedia_search:
406
+ {wikipedia_search.description}
407
+ Args:
408
+ {wikipedia_search.args_schema}
409
+ Returns:
410
+ {wikipedia_search.response_format}
411
+
412
+ Here is the question {state['question']}
413
+ Now provide your response immediately without any preamble.
414
+ """
415
+
416
+ state["web_wiki_search_node_result"] = state.get("web_wiki_search_node_result", "")
417
+
418
+ sys_msg = SystemMessage(content=prompt)
419
+
420
+ web_wiki_search_node_response = [web_search_node_agent.invoke([sys_msg] + [state["web_wiki_search_node_result"]])]
421
+
422
+ web_wiki_search_node_response[-1].pretty_print()
423
+
424
+ return {
425
+ "web_wiki_search_node_result": web_wiki_search_node_response,
426
+ }
427
+
428
+
429
+ def vision_node(state: State) -> dict:
430
+ """
431
+ Vision model that can analyze images and pictures and answer questions about them.
432
+ This node does not handle videos.
433
+ This node does not handle audio.
434
+ This node does not handle code.
435
+
436
+ Args:
437
+ state (State): A dictionary containing the current state of the agent, including the 'question' key which holds the question to be answered and the 'input_file' key which holds the path to the image file.
438
+ Returns:
439
+ dict: A dictionary containing the response from the vision node, with the key 'vision_node_result' holding the list of messages generated by the vision model.
440
+ """
441
+
442
+ prompt = f"""
443
+ You are a powerful vision assistant, you can analyze images and answer question about the picture
444
+ You do not download files
445
+ You do not handle videos
446
+ You do not handle audio
447
+ You do not handle code
448
+
449
+ 1. You need to fully understand the question
450
+ 2. You must think hard about what is relevant in the image to make the best answer to the question
451
+ 4. Report your thought process in detail, explaining your reasoning step-by-step.
452
+
453
+ Here is the question {state['question']}
454
+ Now provide your response immediately without any preamble.
455
  """
 
 
 
456
 
457
  image_base64 = ""
458
  try:
459
+ with open(state["downloaded_file"], "rb") as image_file:
460
  image_bytes = image_file.read()
461
 
462
  image_base64 = base64.b64encode(image_bytes).decode("utf-8")
 
493
  }
494
  ]
495
 
496
+ vision_node_response = [vision_model.invoke(
497
  input=message,
498
+ # config={
499
+ # "callbacks": [langfuse_handler]
500
+ # }
501
+ )]
502
 
503
+ vision_node_response[-1].pretty_print()
504
+
505
+ return {
506
+ "vision_node_result": vision_node_response
507
+ }
508
 
509
  except Exception as e:
510
  # A butler should handle errors gracefully
511
  error_msg = f"Error extracting text: {str(e)}"
512
  print(error_msg)
513
+ return {}
514
 
515
+ def video_node(state: State) -> str:
516
  """
517
+ Video handler model that can analyze videos and answer questions about them.
518
+ This node does not handle images or pictures.
519
+ This node does not handle audio.
520
+ This node does not handle code.
521
 
522
+ Args:
523
+ state (State): A dictionary containing the current state of the agent, including the 'question' key which holds the question to be answered.
 
524
 
525
+ Returns:
526
+ dict: A dictionary containing the response from the video handler node, with the key 'video_node_result' holding the list of messages generated by the video handler model.
527
  """
528
 
529
  prompt = f"""
530
+ You are a highly capable video analysis assistant. Your task is to watch and analyze the provided video content and answer the user's question as accurately and concisely as possible.
531
+ You do not handle images or pictures.
532
+ You do not handle audio.
533
+ You do not handle code.
534
+
535
+ 1. You need to fully understand the question
536
+ 2. Carefully observe the video, paying attention to relevant details, actions, and context.
537
+ 3. Focus on the user's question.
538
+ 4. If the question requires counting, identifying, or describing, be precise and clear in your response.
539
+ 5. If you are unsure, state what you can infer from the video.
540
+ 6. Do not make up information that is not visible or inferable from the video.
541
+
542
+ Here is the question {state['question']}
543
+ Now provide your response immediately without any preamble.
544
  """
545
 
546
+ if re.search(r'youtube\.com', state["question"]):
547
+ # More flexible regex pattern to match YouTube URLs
548
+ regex_result = re.search(r"(?P<youtube_url>https://(?:www\.)?youtube\.com/watch\?v=[a-zA-Z0-9_-]+)", state["question"])
549
+ if regex_result:
550
+ video_url = regex_result.group("youtube_url")
551
+ downloaded_video = download_youtube_content(url=video_url)
552
+ else:
553
+ # Fallback if regex doesn't match
554
+ print("Could not extract YouTube URL from question. Using question as fallback.")
555
+ downloaded_video = state["downloaded_file"]
556
+ else:
557
+ downloaded_video = state["downloaded_file"]
558
 
559
  print(f"Downloaded video: {downloaded_video}")
560
 
 
582
  }
583
  ]
584
 
585
+ video_node_response = [video_handler_model.invoke(
586
  input=message,
587
+ # config={
588
+ # "callbacks": [langfuse_handler]
589
+ # }
590
+ )]
591
+
592
+ video_node_response[-1].pretty_print()
593
+
594
+ return {
595
+ "video_node_result": video_node_response
596
+ }
597
+
598
+ def audio_node(state: State) -> str:
599
+ """
600
+ Audio handler model that can analyze audio and answer questions about it.
601
+ This node does not handle images or pictures.
602
+ This node does not handle video.
603
+ This node does not handle code.
604
+
605
+ Args:
606
+ state (State): with question key inside
607
+
608
+ Returns:
609
+ dict: A dictionary containing the response from the video handler node, with the key 'audioo_node_result' holding the list of messages generated by the audio handler model.
610
+ """
611
+
612
+ prompt = f"""
613
+ You are a highly capable audio analysis assistant. Your task is to listen to and analyze the provided audio content and answer the user's question as accurately and concisely as possible.
614
+ You do not handle images or pictures.
615
+ You do not handle video.
616
+ You do not handle code.
617
+
618
+ 1. You need to fully understand the question:
619
+ 2. Carefully listen to the audio, paying attention to relevant details, actions, and context.
620
+ 3. Focus on the user's question.
621
+ 4. If the question requires counting, identifying, or describing, be precise and clear in your response.
622
+ 5. If you are unsure, state what you can infer from the audio.
623
+ 6. Do not make up information that is not audible or inferable from the audio.
624
+
625
+ Here is the question {state['question']}
626
+ Now provide your response immediately without any preamble.
627
+ """
628
+
629
+ downloaded_audio = state["downloaded_file"]
630
+
631
+ print(f"Downloaded audio: {downloaded_audio}")
632
+
633
+ audio_format = re.search(r'\.(\w+)$', downloaded_audio).group(1)
634
+
635
+ with open(downloaded_audio, "rb") as audio_file:
636
+ encoded_audio = base64.b64encode(audio_file.read()).decode()
637
+
638
+ os.remove(downloaded_audio)
639
+
640
+ message = [
641
+ {
642
+ "role": "user",
643
+ "content": [
644
+ {
645
+ "type": "text",
646
+ "text": prompt,
647
+ },
648
+ {
649
+ "type": "input_audio",
650
+ "input_audio": {
651
+ "data": encoded_audio,
652
+ "format": audio_format,
653
+ }
654
+ },
655
+ ]
656
  }
657
+ ]
 
 
 
 
 
 
 
 
 
 
 
658
 
659
+ audio_node_response = [audio_handler_model.invoke(
660
+ input=message,
661
+ # config={
662
+ # "callbacks": [langfuse_handler]
663
+ # }
664
+ )]
665
 
666
+ audio_node_response[-1].pretty_print()
667
 
668
+ return {
669
+ "audio_node_result": audio_node_response
670
+ }
671
+
672
+ def format_answer_node(state: State):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
673
 
674
+ prompt = """
675
+ You are the best assistant for final answer formating.
676
+
677
+ 1. You must not change the content of the response of the last node.
678
+ 2. You must fully understand the question
679
+ 3. You must return the answer by following hard the format and the constraints
680
+ 4. Report your thought process in detail, explaining your reasoning step-by-step.
681
+
682
+ 5. Conclude your answer with the following template:
683
+ FINAL ANSWER: [YOUR FINAL ANSWER]
684
+
685
+ ## Response Format
686
+ - If asked for a number:
687
+ - Provide the number without commas, dollar signs, percent signs, or any units (unless specified).
688
+ - If asked for a string:
689
+ - Write the string without articles (a, an, the).
690
+ - Don't answer a full sentence when a short version is enough.
691
+ - Do not use abbreviations (e.g., for cities).
692
+ - Write digits in plain text (e.g., "one" instead of "1") unless specified otherwise.
693
+ - If asked for a comma-separated list:
694
+ - Apply the above rules for numbers and strings to each element in the list.
695
+
696
+ ## Constraints
697
+ - You must not answer if the constraints above are not respected.
698
+ - Your final answer should be provided in the format: FINAL ANSWER: [YOUR FINAL ANSWER]
699
+ - Your final answer should be a number, a string, or a comma-separated list of numbers and/or strings, following the specified formatting rules.
700
+
701
+ Now provide your response immediately without any preamble.
702
+ """
703
+
704
+ nodes_response = [HumanMessage(content="Here are the results of the previous nodes")]
705
 
706
+ question = [HumanMessage(content=state["question"])]
707
 
708
+ for node_result in ["web_wiki_search_node_result", "vision_node_result", "video_node_result", "audio_node_result", "thinking_node_result", "code_node_result"]:
709
+ result = state.get(node_result, "")
710
+ if result:
711
+ # Ensure result is a string. If it's a message object, extract its content.
712
+ if hasattr(result, "content"):
713
+ content = result.content
714
+ else:
715
+ content = str(result)
716
+ nodes_response.append(HumanMessage(content=content))
717
+
718
+ sys_msg = SystemMessage(content=prompt)
719
+
720
+ response = [general_model.invoke([sys_msg] + state["messages"]+ question + nodes_response)]
721
 
722
  return {
 
723
  "messages": response,
 
 
724
  }
725
 
726
+ ########################
727
 
728
+ ######## Entry Node ########
729
+ def entry_node(state: State)-> str:
730
+ # System message
731
+
732
+ system_prompt = f"""
733
+ You are a powerful assistant that handle the user message and manage other nodes in order to provide the best answer to the question.
734
+ You do not handle images or pictures
735
+ You do not handle videos
736
+ You do not handle audio
737
+ You do not handle code
738
+
739
+ 1. You need to fully understand the subject of the question
740
+ 2. You need to understand the subject of the question with the question itself and the file extension
741
+ For example of extensions:
742
+ - .py is for code
743
+ - .wav or .mp3 is for audio
744
+ - a youtube url is for video
745
+ - a .jpg, .png, .jpeg is for image
746
+ 3. You must think hard about what is relevant in the question to make the best choice for the next node
747
+ 4. You must not answer the question by yourself
748
+ 5. Report your thought process in detail, explaining your reasoning step-by-step.
749
+
750
+ Here are the nodes you can choose:
751
+ - thinking_node: {thinking_node.__doc__}
752
+ - web_wiki_search_node: {web_wiki_search_node.__doc__}
753
+ - vision_node: {vision_node.__doc__}
754
+ - video_node: {video_node.__doc__}
755
+ - audio_node: {audio_node.__doc__}
756
+ - code_node: {code_node.__doc__}
757
+
758
+ Here is the question : {state['question']}
759
+ Here is the file : {state.get("input_file", "no file to handle")}
760
+
761
+ Now provide your response immediately respecting this format: 'next node: \'the node name you choose\''.
762
+ """
763
+
764
+ downloaded = ""
765
+ # If there's an input file, download it directly
766
+ if state.get("input_file", None):
767
+ downloaded = download_input_file(state.get("task_id"))
768
+
769
+ sys_msg = SystemMessage(content=system_prompt)
770
+
771
+ entry_node_response = [general_model.invoke([sys_msg] + state["messages"])]
772
+
773
+ entry_node_response[-1].pretty_print()
774
+
775
+ regex_result = re.search(r'.*next\snode:\s+(?P<next_node>.*)$', entry_node_response[-1].content)
776
+
777
+ next_node = "END"
778
+ if regex_result:
779
+ # Extract the node name and remove any quotes around it
780
+ next_node = regex_result.group("next_node").strip().strip('\'"')
781
+
782
+ return {
783
+ "next": next_node,
784
+ "downloaded_file": downloaded
785
+ }
786
 
787
+ ########################
 
 
 
788
 
789
+ ######## Build Graph ########
 
790
 
791
+ def build_graph():
792
+ builder = StateGraph(State)
793
+ builder.add_node("entry_node", entry_node)
794
+ builder.add_node("web_wiki_search_node", web_wiki_search_node)
795
+ builder.add_node("vision_node", vision_node)
796
+ builder.add_node("video_node", video_node)
797
+ builder.add_node("audio_node", audio_node)
798
+ builder.add_node("code_node", code_node)
799
+ builder.add_node("thinking_node", thinking_node)
800
+ builder.add_node("format_answer_node", format_answer_node)
801
+
802
+ builder.add_edge(START, "entry_node")
803
+
804
+ # Conditional routing from entry_node to specialized nodes
805
  builder.add_conditional_edges(
806
+ "entry_node",
807
+ lambda state: state["next"],
808
+ {
809
+ "web_wiki_search_node": "web_wiki_search_node",
810
+ "vision_node": "vision_node",
811
+ "video_node": "video_node",
812
+ "audio_node": "audio_node",
813
+ "code_node": "code_node",
814
+ "thinking_node": "thinking_node"
815
+ }
816
  )
817
+ # After specialized node, go to END
818
+ builder.add_edge("web_wiki_search_node", "format_answer_node")
819
+ builder.add_edge("vision_node", "format_answer_node")
820
+ builder.add_edge("video_node", "format_answer_node")
821
+ builder.add_edge("audio_node", "format_answer_node")
822
+ builder.add_edge("code_node", "format_answer_node")
823
+ builder.add_edge("thinking_node", "format_answer_node")
824
+ builder.add_edge("format_answer_node", END)
825
+
826
 
827
  return builder.compile()
828
 
829
+ ########################
830
+
831
  if __name__ == "__main__":
832
 
833
  agent_graph = build_graph()
834
 
835
+ with open("image.png", "wb") as png:
836
+ png.write(agent_graph.get_graph(xray=True).draw_mermaid_png())
837
+
838
+ # print(vision_node.__doc__)
839
 
840
+ input = {
841
+ "task_id": "9d191bce-651d-4746-be2d-7ef8ecadb9c2",
842
+ "question": "Examine the video at https://www.youtube.com/watch?v=1htKBjuUWec. What does Teal'c say in response to the question \"Isn't that hot?\"",
843
+ "file_name": ""
844
+ }
845
+
846
+ question = input.get("question", "No question found")
847
+ file_name = input.get("file_name", "")
848
+ task_id = input.get("task_id", "")
849
 
850
  print(f"QUESTION : {question}")
851
  print(f"FILE: {file_name}")
852
 
853
+ user_prompt = [HumanMessage(content=f"Can you answer the question please ?")]
854
+
855
+ user_input = {"messages": user_prompt, "question": question, "input_file": file_name, "task_id": task_id}
856
 
857
  messages = agent_graph.invoke(
858
+ input=user_input,
859
  config={
860
  "recursion_limit": 10,
861
+ # "callbacks": [langfuse_handler]
862
  }
863
  )
864