22Nikk0 commited on
Commit
542e1a6
·
verified ·
1 Parent(s): cc896c8

Create agentic.py

Browse files
Files changed (1) hide show
  1. agentic.py +510 -0
agentic.py ADDED
@@ -0,0 +1,510 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langgraph.graph import StateGraph, START, END
2
+ from typing_extensions import TypedDict, Annotated, Literal, Optional
3
+ from langchain_core.messages import AnyMessage, HumanMessage, SystemMessage
4
+ from langgraph.graph.message import add_messages
5
+ from langchain_mistralai import ChatMistralAI
6
+ from langchain_openai import ChatOpenAI
7
+ from langgraph.prebuilt import ToolNode, tools_condition
8
+ from langchain_core.runnables.graph import MermaidDrawMethod
9
+ from langchain_community.tools import DuckDuckGoSearchRun
10
+ from langchain_community.tools import WikipediaQueryRun
11
+ from langchain_community.utilities import WikipediaAPIWrapper
12
+ from langchain_aws import ChatBedrock
13
+ from langchain_google_genai import ChatGoogleGenerativeAI
14
+ # from langchain_google_vertexai import ChatVertexAI
15
+
16
+ from langfuse.callback import CallbackHandler
17
+
18
+ import base64
19
+
20
+ # import boto3
21
+
22
+ from yt_dlp import YoutubeDL
23
+ import os
24
+ # from urllib.parse import urlparse, parse_qs
25
+ import re
26
+ from dotenv import load_dotenv
27
+
28
+ # Load env vars from .env file
29
+ load_dotenv()
30
+
31
+ # Initialize Langfuse CallbackHandler for LangGraph/Langchain (tracing)
32
+ langfuse_handler = CallbackHandler()
33
+
34
+ class State(TypedDict):
35
+ """
36
+ A class representing the state of the agent.
37
+ """
38
+ answer: str
39
+ question: str
40
+ messages: Annotated[list[AnyMessage], add_messages]
41
+ input_file: str
42
+
43
+
44
+ def get_assistant_model():
45
+
46
+ llm_provider = os.getenv("LLM_PROVIDER", "mistral")
47
+
48
+ if llm_provider == "mistral":
49
+ assistant_model = ChatMistralAI(
50
+ model="mistral-small-latest",#"ministral-8b-latest",#
51
+ temperature=0,
52
+ max_retries=2,
53
+ api_key=os.getenv("MISTRAL_API_KEY")
54
+ )
55
+
56
+ if llm_provider == "aws":
57
+ assistant_model = ChatBedrock(
58
+ model_id="arn:aws:bedrock:us-east-1:416545197702:inference-profile/us.amazon.nova-lite-v1:0",
59
+ # provider="amazon",
60
+ temperature=0,
61
+ region_name="eu-west-3",
62
+ aws_access_key_id=os.getenv("AWS_ACCESS_KEY_ID"),
63
+ aws_secret_access_key=os.getenv("AWS_SECRET_ACCESS_KEY")
64
+ )
65
+
66
+ return assistant_model
67
+
68
+ def get_vision_model():
69
+
70
+ vlm_provider = os.getenv("VLM_PROVIDER", "mistral")
71
+
72
+ if vlm_provider == "openai":
73
+ print("Spawning Open AI VLM")
74
+ vision_model = ChatOpenAI(
75
+ model="gpt-4o",#-mini",
76
+ temperature=0,
77
+ max_tokens=None,
78
+ timeout=None,
79
+ max_retries=2,
80
+ api_key=os.getenv("OPENAI_API_KEY"),
81
+ )
82
+
83
+ if vlm_provider == "mistral":
84
+ print("Spawning Mistral VLM")
85
+ vision_model = ChatMistralAI(
86
+ model="pixtral-12b-2409",#"mistral-small-latest","pixtral-large-latest",#
87
+ temperature=0,
88
+ max_retries=2,
89
+ api_key=os.getenv("MISTRAL_API_KEY")
90
+ )
91
+
92
+ return vision_model
93
+
94
+ def get_video_handler_model():
95
+
96
+ video_handler_model = ChatGoogleGenerativeAI(
97
+ model="gemini-2.0-flash",
98
+ temperature=0,
99
+ max_tokens=None,
100
+ timeout=None,
101
+ max_retries=2,
102
+ # other params...
103
+ )
104
+
105
+ return video_handler_model
106
+
107
+ # reviewer_model = ChatMistralAI(
108
+ # model="mistral-small-latest",
109
+ # temperature=0,
110
+ # max_retries=2,
111
+ # api_key=os.getenv("MISTRAL_API_KEY"),
112
+ # )
113
+
114
+ def download_youtube_content(url: str, output_path: Optional[str] = None) -> None:
115
+ """
116
+ Download YouTube content (single video or playlist) in MP4 format only.
117
+
118
+ Args:
119
+ url (str): URL of the YouTube video or playlist
120
+ output_path (str, optional): Directory to save the downloads. Defaults to './downloads'
121
+ """
122
+ # Set default output path if none provided
123
+ if output_path is None:
124
+ output_path = os.path.join(os.getcwd(), 'downloads')
125
+
126
+ # Create output directory if it doesn't exist
127
+ os.makedirs(output_path, exist_ok=True)
128
+
129
+ # Configure yt-dlp options for MP4 only
130
+ ydl_opts = {
131
+ 'format': 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best',
132
+ 'merge_output_format': 'mp4',
133
+ 'ignoreerrors': True,
134
+ 'no_warnings': False,
135
+ 'extract_flat': False,
136
+ # Disable all additional downloads
137
+ 'writesubtitles': False,
138
+ 'writethumbnail': False,
139
+ 'writeautomaticsub': False,
140
+ 'postprocessors': [{
141
+ 'key': 'FFmpegVideoConvertor',
142
+ 'preferedformat': 'mp4',
143
+ }],
144
+ # Clean up options
145
+ 'keepvideo': False,
146
+ 'clean_infojson': True
147
+ }
148
+
149
+
150
+ ydl_opts['outtmpl'] = os.path.join(output_path, '%(title)s.%(ext)s')
151
+ print("Detected single video URL. Downloading video...")
152
+
153
+ try:
154
+ with YoutubeDL(ydl_opts) as ydl:
155
+ # Download content
156
+ ydl.download([url])
157
+ print(f"\nDownload completed successfully! Files saved to: {output_path}")
158
+
159
+ except Exception as e:
160
+ print(f"An error occurred: {str(e)}")
161
+
162
+ result = os.listdir(output_path)
163
+
164
+ video_file_names = [x for x in result if re.match(r".*\.mp4$", x)]
165
+
166
+ if len(video_file_names) == 1:
167
+ video_file_name = video_file_names.pop()
168
+ video_file_name = f"{output_path}/{video_file_name}"
169
+ else:
170
+ video_file_name = None
171
+
172
+ for other_files in result:
173
+ if f"{output_path}/{other_files}" != video_file_name:
174
+ print(f"Removing file: {other_files}")
175
+ os.remove(os.path.join(output_path, other_files))
176
+
177
+ return video_file_name
178
+
179
+ def search_webpage(state: State, url: str)-> str:
180
+ """
181
+ Search a web page based on the current state.
182
+ """
183
+ # Simulate a web page search and return a result
184
+ return "search_webpage"
185
+
186
+ web_search = DuckDuckGoSearchRun()
187
+ wikipedia_search = WikipediaQueryRun(api_wrapper=WikipediaAPIWrapper())
188
+
189
+ assistant_model = get_assistant_model()
190
+ vision_model = get_vision_model()
191
+ video_handler_model = get_video_handler_model()
192
+
193
+ def vision_model_call(state: State) -> str:
194
+ """
195
+ Vision model that can analyze images and picture and answer questions about them.
196
+
197
+ Args:
198
+ state (State) with input_file key and question key inside
199
+
200
+ Returns:
201
+ answer(string)
202
+ """
203
+ prompt = f"""
204
+ You are a powerful vision assistant, you can analyze images and answer question about the picture
205
+ The question is : {state["question"]}
206
+ """
207
+ # You need to consider system prompt of the first assistant to answer.
208
+ # Here is the system prompt: {state}
209
+ # """
210
+
211
+ image_base64 = ""
212
+ try:
213
+ with open(state["input_file"], "rb") as image_file:
214
+ image_bytes = image_file.read()
215
+
216
+ image_base64 = base64.b64encode(image_bytes).decode("utf-8")
217
+
218
+ mistral_image_handling = {
219
+ "type": "image_url",
220
+ "image_url": f"data:image/png;base64,{image_base64}",
221
+ }
222
+
223
+ openai_image_handling = {
224
+ "type": "image",
225
+ "source_type": "base64",
226
+ "mime_type": "image/png", # or image/png, etc.
227
+ "data": image_base64,
228
+ }
229
+
230
+ vision_provider = "mistral"
231
+
232
+ if vision_provider == "openai":
233
+ image_handling = openai_image_handling
234
+ else:
235
+ image_handling = mistral_image_handling
236
+
237
+ message = [
238
+ {
239
+ "role": "user",
240
+ "content": [
241
+ {
242
+ "type": "text",
243
+ "text": prompt,
244
+ },
245
+ image_handling
246
+ ]
247
+ }
248
+ ]
249
+
250
+ response = vision_model.invoke(
251
+ input=message,
252
+ config={
253
+ "callbacks": [langfuse_handler]
254
+ }
255
+ )
256
+
257
+ return response.content + "\n\n"
258
+
259
+ except Exception as e:
260
+ # A butler should handle errors gracefully
261
+ error_msg = f"Error extracting text: {str(e)}"
262
+ print(error_msg)
263
+ return ""
264
+
265
+ def video_handler_mode_call(state: State, video_url: str) -> str:
266
+ """
267
+ Video handler model that can analyze videos and answer questions about them.
268
+
269
+ Args:
270
+ state (State): with question key inside
271
+ video_url (str): URL of the YouTube video to analyze.
272
+
273
+ Returns:
274
+ answer(string)
275
+ """
276
+
277
+ prompt = f"""
278
+ 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.
279
+
280
+ Instructions:
281
+ - Carefully observe the video, paying attention to relevant details, actions, and context.
282
+ - Focus on the user's question: "{state['question']}"
283
+ - If the question requires counting, identifying, or describing, be precise and clear in your response.
284
+ - If you are unsure, state what you can infer from the video.
285
+ - Do not make up information that is not visible or inferable from the video.
286
+
287
+ """
288
+
289
+ downloaded_video = download_youtube_content(url=video_url)
290
+
291
+ print(f"Downloaded video: {downloaded_video}")
292
+
293
+ video_mime_type = "video/mp4"
294
+
295
+ with open(downloaded_video, "rb") as video_file:
296
+ encoded_video = base64.b64encode(video_file.read()).decode("utf-8")
297
+
298
+ os.remove(downloaded_video)
299
+
300
+ message = [
301
+ {
302
+ "role": "user",
303
+ "content": [
304
+ {
305
+ "type": "text",
306
+ "text": prompt,
307
+ },
308
+ {
309
+ "type": "media",
310
+ "data": encoded_video, # Use base64 string directly
311
+ "mime_type": video_mime_type,
312
+ },
313
+ ]
314
+ }
315
+ ]
316
+
317
+ response = video_handler_model.invoke(
318
+ input=message,
319
+ config={
320
+ "callbacks": [langfuse_handler]
321
+ }
322
+ )
323
+
324
+ return response.content + "\n\n"
325
+
326
+ # def router(state: State)-> Literal["OK", "retry", "failed"]:
327
+ # """Determine the next step based on FINAL ANSWER"""
328
+
329
+ # print(f"MESSAGES : {state['messages'][-1].content}")
330
+ # print(f"STATE : {state['system_prompt']}")
331
+ # if True:
332
+ # return "OK"
333
+ # else:
334
+ # if state.status == "ERROR":
335
+ # if state.error_count >= 3:
336
+ # return "failed"
337
+ # return "retry"
338
+
339
+ # def reviewer_validation(state: State) -> Literal["OK", "NOK"]:
340
+ # print(f"MESSAGES : {state['messages'][-1].content}")
341
+ # print(f"STATE : {state}")
342
+ # return "OK"
343
+ # if True:
344
+ # return "OK"
345
+ # else:
346
+ # return "NOK"
347
+
348
+ # Tools
349
+ tools = [
350
+ web_search,
351
+ # search_webpage,
352
+ wikipedia_search,
353
+ vision_model_call,
354
+ video_handler_mode_call
355
+ ]
356
+
357
+ assistant_with_tools = assistant_model.bind_tools(tools, parallel_tool_calls=False)
358
+ # reviewer_with_tools = reviewer_model.bind_tools(tools, parallel_tool_calls=False)
359
+
360
+
361
+ def assistant(state: State)-> str:
362
+ # System message
363
+ textual_description_of_tool=f"""
364
+ web_search:
365
+ {web_search.description}
366
+ Args:
367
+ {web_search.args_schema}
368
+ Returns:
369
+ {web_search.response_format}
370
+
371
+ wikipedia_search:
372
+ {wikipedia_search.description}
373
+ Args:
374
+ {wikipedia_search.args_schema}
375
+ Returns:
376
+ {wikipedia_search.response_format}
377
+
378
+ vision_model_call:
379
+ {vision_model_call.__doc__}
380
+ Args:
381
+ {vision_model_call.__annotations__}
382
+ Returns:
383
+ {vision_model_call.__annotations__['return']}
384
+
385
+ video_handler_mode_call:
386
+ {video_handler_mode_call.__doc__}
387
+ Args:
388
+ {video_handler_mode_call.__annotations__}
389
+ Returns:
390
+ {video_handler_mode_call.__annotations__['return']}
391
+ """
392
+
393
+ with open("./prompt.txt", "r") as prompt_file:
394
+ system_prompt = prompt_file.read()
395
+
396
+ file_prompt = f"If the question is about an image or a picture, and if you need to analyze a file, an image and so on... you must check the path {state['input_file']}"
397
+
398
+ video_handler_prompt = "If the question is about a video and if you need to analyze a video, you must use the video_handler_mode_call tool."
399
+
400
+ sys_msg = SystemMessage(content=system_prompt+file_prompt+video_handler_prompt+textual_description_of_tool)
401
+
402
+ response = [assistant_with_tools.invoke([sys_msg] + state["messages"])]
403
+
404
+ return {
405
+ "system_prompt": system_prompt,
406
+ "messages": response,
407
+ "question": state["question"],
408
+ "answer": state.get("answer", "")
409
+ }
410
+
411
+ # def reviewer(state: State)-> str:
412
+
413
+ # with open("./prompt.txt", "r") as prompt_file:
414
+ # system_prompt = prompt_file.read()
415
+
416
+ # sys_msg = SystemMessage(content=f"""
417
+ # You are a powerful AI assistant reviewer.
418
+ # You must review the answer of the previous assistant.
419
+ # If you need you can correct the answer.
420
+ # You need to make sure the answer is formatted correctly.
421
+ # The response should not be sent if FINAL ANSWER: is not registered and does not respect the constraints of the initial prompt.
422
+
423
+ # Here is the prompt of the previous assistant :
424
+ # {system_prompt}
425
+
426
+ # Here is the question: {state['question']}
427
+ # """)
428
+
429
+ # response = [reviewer_with_tools.invoke([sys_msg] + state["messages"])]
430
+
431
+ # return {
432
+ # "messages": response,
433
+ # }
434
+
435
+ def build_graph():
436
+ builder = StateGraph(State)
437
+
438
+ # Nodes
439
+ builder.add_node("assistant", assistant)
440
+ # builder.add_node("reviewer", reviewer)
441
+ builder.add_node("tools", ToolNode(tools))
442
+
443
+ # Edges
444
+ builder.add_edge(START, "assistant")
445
+
446
+ builder.add_conditional_edges(
447
+ "assistant",
448
+ tools_condition,
449
+ )
450
+ builder.add_edge("tools", "assistant")
451
+ # builder.add_edge("assistant", "reviewer")
452
+
453
+ # builder.add_conditional_edges(
454
+ # "assistant",
455
+ # router,
456
+ # {
457
+ # "OK": "reviewer",
458
+ # "retry": "assistant",
459
+ # "failed": END
460
+ # }
461
+ # )
462
+ # builder.add_conditional_edges(
463
+ # "reviewer",
464
+ # reviewer_validation,
465
+ # {
466
+ # "OK": END,
467
+ # "NOK": "assistant"
468
+ # }
469
+ # )
470
+
471
+ return builder.compile()
472
+
473
+ if __name__ == "__main__":
474
+
475
+ agent_graph = build_graph()
476
+
477
+ file_name = ""
478
+
479
+ # agent_graph.get_graph(xray=True).draw_mermaid_png(output_file_path="./graph.png", draw_method=MermaidDrawMethod.PYPPETEER)
480
+ question = "Where were the Vietnamese specimens described by Kuznetzov in Nedoshivina's 2010 paper eventually deposited? Just give me the city name without abbreviations."
481
+ # 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."
482
+ # question = ".rewsna eht sa \"tfel\" drow eht fo etisoppo eht etirw ,ecnetnes siht dnatsrednu uoy fI"
483
+ # 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?"
484
+ # question = "In the video https://www.youtube.com/watch?v=L1vXCYZAYYM, what is the highest number of bird species to be on camera simultaneously?"
485
+ # 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."
486
+ # question = "Can you describe the image ?"
487
+ # file_name = "images/cca530fc-4052-43b2-b130-b30968d8aa44.png"
488
+
489
+ messages = [HumanMessage(content=f"Can you answer this question please ? {question}")]
490
+
491
+ messages = agent_graph.invoke(
492
+ input={"messages": messages, "question": question, "input_file": file_name},
493
+ config={
494
+ "recursion_limit": 10,
495
+ "callbacks": [langfuse_handler]
496
+ }
497
+ )
498
+
499
+ for m in messages['messages']:
500
+ m.pretty_print()
501
+
502
+ try:
503
+ regex_result = re.search(r"FINAL ANSWER:\s*(?P<answer>.*)$", messages['messages'][-1].content)
504
+ answer = regex_result.group("answer")
505
+ except:
506
+ regex_result = re.search(r"\s*(?P<answer>.*)$", messages['messages'][-1].content)
507
+ answer = regex_result.group("answer")
508
+
509
+ print(f"ANSWER : {answer}")
510
+ # from pprint import pprint