sebastianfrench commited on
Commit
c285622
·
1 Parent(s): 5d8f022

add youtube transcription

Browse files
agents/search_agent.py CHANGED
@@ -16,13 +16,16 @@ class SearchAgent:
16
 
17
  state = workflow.invoke({
18
  "messages":messages,
 
19
  }, config={"callbacks": [langfuse_handler]})
20
 
21
- print(state["external_information"])
22
  return state["answer"]
23
 
24
  if __name__ == "__main__":
25
- question = "How many studio albums were published by Mercedes Sosa between 2000 and 2009 (included)? You can use the latest 2022 version of english wikipedia."
 
 
 
26
  agent = SearchAgent()
27
  submit_answer = agent(question)
28
 
 
16
 
17
  state = workflow.invoke({
18
  "messages":messages,
19
+ "question": question,
20
  }, config={"callbacks": [langfuse_handler]})
21
 
 
22
  return state["answer"]
23
 
24
  if __name__ == "__main__":
25
+ #question = "In the video https://www.youtube.com/watch?v=L1vXCYZAYYM, what is the highest number of bird species to be on camera simultaneously?"
26
+ question = """Examine the video at https://www.youtube.com/watch?v=1htKBjuUWec.
27
+
28
+ What does Teal'c say in response to the question "Isn't that hot?"""
29
  agent = SearchAgent()
30
  submit_answer = agent(question)
31
 
graphs/evaluation.py CHANGED
@@ -1,14 +1,17 @@
1
  from models.models import groq_model, anthropic_model
2
- from tools.search import taivily_search, serper_search
3
  from langgraph.graph import StateGraph, START, END, MessagesState
 
4
  from langchain_core.messages import HumanMessage, SystemMessage
5
  from typing import List, TypedDict
6
  from langgraph.prebuilt import ToolNode
7
-
8
 
9
  tools = [
10
- #taivily_search,
11
  serper_search,
 
 
12
  ]
13
 
14
  class EvaluationState(TypedDict):
@@ -19,6 +22,8 @@ class EvaluationState(TypedDict):
19
  answer: str
20
  external_information: str
21
  has_enough_information: bool
 
 
22
 
23
  bound_model_llama = groq_model.bind_tools(tools)
24
  bound_model_antrhropic = anthropic_model.bind_tools(tools)
@@ -27,8 +32,7 @@ def call_node(state: EvaluationState):
27
  """
28
  This node call the model with the question and the tools
29
  """
30
- question = state["messages"][-1].content
31
- state["question"] = question
32
  response = bound_model_llama.invoke(state["messages"])
33
 
34
  state["messages"].append(response)
@@ -37,24 +41,23 @@ def call_node(state: EvaluationState):
37
  tool_node = ToolNode(tools)
38
 
39
 
40
- def parse_response(state: EvaluationState):
41
  """
42
- Parse the response from the model and return the final answer
43
  """
44
- prompt = f"""I will ask you a question. Report your thoughts, and finish with only YOUR FINAL ANSWER.
45
- YOUR FINAL ANSWER should be a number OR as few words as possible OR a comma separated list of numbers and/or strings.
46
- 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.
47
- 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.
48
- 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.
49
- ---question---
50
- {state["question"]}
51
- ---relevant information---
52
- {state["external_information"]}
53
- ---answer---
54
- """
55
-
56
-
57
- response = groq_model.invoke(prompt)
58
  state["messages"].append(response)
59
  state["answer"] = response.content
60
  return state
@@ -69,30 +72,114 @@ def map_answer(state: EvaluationState):
69
  "answer": answer.content
70
  }
71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  def build_workflow():
73
  """
74
- Build search workflow
75
  """
76
  workflow = StateGraph(EvaluationState)
77
  workflow.add_node("agent", call_node)
78
  workflow.add_node("action", tool_node)
79
- workflow.add_node("parse_response", parse_response)
80
  workflow.add_node("map_answer", map_answer)
81
-
82
- workflow.add_edge(START,"agent")
 
 
 
83
  workflow.add_edge("agent", "action")
84
- workflow.add_edge("action", "parse_response")
85
- workflow.add_edge("parse_response", "map_answer")
86
- workflow.add_edge("map_answer", END)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
 
88
  return workflow.compile()
89
 
 
90
  """ if __name__ == "__main__":
91
- question = "How many studio albums were published by Mercedes Sosa between 2000 and 2009 (included)? You can use the latest 2022 version of english wikipedia."
92
  # Build the graph
93
  graph = build_workflow()
94
- # Run the graph
95
- messages = [HumanMessage(content=question)]
96
- messages = graph.invoke({"messages": messages})
97
- for m in messages["messages"]:
98
- m.pretty_print() """
 
1
  from models.models import groq_model, anthropic_model
2
+ from tools import taivily_search, serper_search, execute_code, get_youtube_transcript
3
  from langgraph.graph import StateGraph, START, END, MessagesState
4
+ from langgraph.types import Command
5
  from langchain_core.messages import HumanMessage, SystemMessage
6
  from typing import List, TypedDict
7
  from langgraph.prebuilt import ToolNode
8
+ from IPython.display import Image
9
 
10
  tools = [
11
+ taivily_search,
12
  serper_search,
13
+ get_youtube_transcript,
14
+ execute_code
15
  ]
16
 
17
  class EvaluationState(TypedDict):
 
22
  answer: str
23
  external_information: str
24
  has_enough_information: bool
25
+ is_valid_answer: bool
26
+ step_counter: dict[str, int]
27
 
28
  bound_model_llama = groq_model.bind_tools(tools)
29
  bound_model_antrhropic = anthropic_model.bind_tools(tools)
 
32
  """
33
  This node call the model with the question and the tools
34
  """
35
+ print(state["question"])
 
36
  response = bound_model_llama.invoke(state["messages"])
37
 
38
  state["messages"].append(response)
 
41
  tool_node = ToolNode(tools)
42
 
43
 
44
+ def answer_question(state: EvaluationState):
45
  """
46
+ This node get the context information and call the model to get the answer.
47
  """
48
+ prompt = f"""## Instruction \n I will ask you a question. Report your thoughts, and finish with only YOUR FINAL ANSWER.
49
+ YOUR FINAL ANSWER should be a number OR as few words as possible OR a comma separated list of numbers and/or strings.
50
+ 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.
51
+ 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.
52
+ 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.
53
+ ## Question
54
+ {state["question"]}
55
+ ## Relevant information
56
+ {state["external_information"]}
57
+ ## answer"""
58
+
59
+
60
+ response = anthropic_model.invoke(prompt)
 
61
  state["messages"].append(response)
62
  state["answer"] = response.content
63
  return state
 
72
  "answer": answer.content
73
  }
74
 
75
+ def validator(state: EvaluationState):
76
+ """
77
+ Validate if the answer fills the requirements
78
+ """
79
+ # Initialize or update validator step counter
80
+ if "step_counter" not in state:
81
+ state["step_counter"] = { "validator": 0}
82
+
83
+ # Increment the validator step counter
84
+ state["step_counter"]["validator"] = state["step_counter"].get("validator", 0) + 1
85
+
86
+ # Check if we've hit the validator recursion limit
87
+ if state["step_counter"]["validator"] >= 3: # Smaller limit for validator recursion
88
+ print("Validator recursion limit reached. Accepting current answer format.")
89
+ state["is_valid_answer"] = True
90
+ return state
91
+
92
+ answer = state["answer"]
93
+ result = anthropic_model.invoke(f"Validate if the answer fits the next requirements: \n\n{answer}\n\nThe answer should be a number, string or a list of numbers and/or strings. If the answer fits the requirements, return 'yes', otherwise return 'no'.")
94
+ is_valid_answer = result.content.startswith("yes")
95
+ state["is_valid_answer"] = is_valid_answer
96
+
97
+ return state
98
+
99
+ def evaluator(state: EvaluationState):
100
+ """
101
+ Evaluate if it is needed more infomation to resolve the question.
102
+ """
103
+ if "step_counter" not in state:
104
+ state["step_counter"] = { "evaluator": 0}
105
+
106
+ state["step_counter"]["evaluator"] = state["step_counter"].get("evaluator", 0) + 1
107
+
108
+ total_iterations = state["step_counter"].get("evaluator", 0)
109
+ if total_iterations >= 5: # Using higher threshold for combined count
110
+ state["has_enough_information"] = True
111
+ return state
112
+
113
+ prompt = f"""Does the context information are enough to resolve the answer? \n # Context information \n {state["external_information"]} \n # Question \n {state["question"]} \n If the context information is enough to resolve the question, return 'yes', otherwise return what is missing."""
114
+ result = anthropic_model.invoke(prompt)
115
+ has_enough_information = result.content.startswith("yes")
116
+ state["has_enough_information"] = has_enough_information
117
+
118
+ if not has_enough_information:
119
+ # Only update messages and external information if we need more info
120
+ state["messages"] = [SystemMessage(content=result.content)]
121
+ state["external_information"] = f"{state['external_information']}\n\n---\n\n{result.content}"
122
+
123
+ return state
124
+
125
  def build_workflow():
126
  """
127
+ Build search workflow with conditional edge for evaluation
128
  """
129
  workflow = StateGraph(EvaluationState)
130
  workflow.add_node("agent", call_node)
131
  workflow.add_node("action", tool_node)
132
+ workflow.add_node("answer_question", answer_question)
133
  workflow.add_node("map_answer", map_answer)
134
+ workflow.add_node("evaluator", evaluator)
135
+ workflow.add_node("validator", validator)
136
+
137
+ # Define edges
138
+ workflow.add_edge(START, "agent")
139
  workflow.add_edge("agent", "action")
140
+ workflow.add_edge("action", "evaluator")
141
+
142
+ # Explicit conditional edges from evaluator
143
+ def route_evaluator(state):
144
+ if state["has_enough_information"]:
145
+ return "answer_question"
146
+ else:
147
+ return "agent"
148
+
149
+ workflow.add_conditional_edges("evaluator", route_evaluator,{"answer_question":"answer_question","agent":"agent"})
150
+
151
+ # Connect answer_question to map_answer
152
+ workflow.add_edge("answer_question", "map_answer")
153
+ workflow.add_edge("map_answer", "validator")
154
+
155
+ # Explicit conditional edges from validator
156
+ def route_validator(state):
157
+ if state["is_valid_answer"]:
158
+ return END
159
+ else:
160
+ return "map_answer"
161
+
162
+ workflow.add_conditional_edges("validator", route_validator, {"map_answer":"map_answer", END:END})
163
+
164
+ # Check if we need to manually add the edges for visualization
165
+ try:
166
+ # These are just for visualization and may not affect actual execution
167
+ workflow._graph.add_edge("evaluator", "agent", condition="needs more info")
168
+ workflow._graph.add_edge("evaluator", "answer_question", condition="has enough info")
169
+ workflow._graph.add_edge("validator", "map_answer", condition="invalid answer")
170
+ workflow._graph.add_edge("validator", END, condition="valid answer")
171
+ except:
172
+ # Skip if this approach doesn't work with current LangGraph version
173
+ pass
174
 
175
  return workflow.compile()
176
 
177
+
178
  """ if __name__ == "__main__":
 
179
  # Build the graph
180
  graph = build_workflow()
181
+
182
+ # Get the Mermaid diagram as text
183
+ mermaid_text = graph.get_graph().draw_mermaid()
184
+
185
+ print(mermaid_text) """
graphs/output/workflow_graph.mmd ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ config:
3
+ flowchart:
4
+ curve: linear
5
+ ---
6
+ graph TD;
7
+ __start__(<p>__start__</p>)
8
+ agent(agent)
9
+ action(action)
10
+ answer_question(answer_question)
11
+ map_answer(map_answer)
12
+ evaluator(evaluator)
13
+ validator(validator)
14
+ __end__(<p>__end__</p>)
15
+ __start__ --> agent;
16
+ action --> evaluator;
17
+ agent --> action;
18
+ evaluator --> __end__;
19
+ classDef default fill:#f2f0ff,line-height:1.2
20
+ classDef first fill-opacity:0
21
+ classDef last fill:#bfb6fc
models/models.py CHANGED
@@ -5,8 +5,8 @@ from dotenv import load_dotenv
5
  load_dotenv()
6
 
7
  anthropic_model = ChatAnthropic(
8
- model="claude-3-5-haiku-latest",
9
- temperature=0
10
  )
11
 
12
  groq_model = ChatGroq(
 
5
  load_dotenv()
6
 
7
  anthropic_model = ChatAnthropic(
8
+ model="claude-3-7-sonnet-20250219",
9
+ temperature=0.4
10
  )
11
 
12
  groq_model = ChatGroq(
requirements.txt CHANGED
@@ -9,11 +9,17 @@ arxiv==2.2.0
9
  attrs==25.3.0
10
  backoff==2.2.1
11
  beautifulsoup4==4.13.4
 
12
  certifi==2025.4.26
13
  charset-normalizer==3.4.2
14
  click==8.1.8
15
  dataclasses-json==0.6.7
 
 
16
  distro==1.9.0
 
 
 
17
  fastapi==0.115.12
18
  feedparser==6.0.11
19
  ffmpy==0.5.0
@@ -31,6 +37,7 @@ httpx==0.28.1
31
  httpx-sse==0.4.0
32
  huggingface-hub==0.30.2
33
  idna==3.10
 
34
  Jinja2==3.1.6
35
  jiter==0.9.0
36
  jsonpatch==1.33
@@ -54,12 +61,14 @@ mdurl==0.1.2
54
  multidict==6.4.3
55
  mypy_extensions==1.1.0
56
  numpy==2.2.5
 
57
  orjson==3.10.18
58
  ormsgpack==1.9.1
59
  packaging==24.2
60
  pandas==2.2.3
61
  pillow==11.2.1
62
  propcache==0.3.1
 
63
  pydantic==2.11.4
64
  pydantic-settings==2.9.1
65
  pydantic_core==2.33.2
@@ -97,6 +106,9 @@ uvicorn==0.34.2
97
  websockets==15.0.1
98
  wikipedia==1.4.0
99
  wrapt==1.17.2
 
100
  xxhash==3.5.0
101
  yarl==1.20.0
 
 
102
  zstandard==0.23.0
 
9
  attrs==25.3.0
10
  backoff==2.2.1
11
  beautifulsoup4==4.13.4
12
+ bytecode==0.16.2
13
  certifi==2025.4.26
14
  charset-normalizer==3.4.2
15
  click==8.1.8
16
  dataclasses-json==0.6.7
17
+ ddtrace==3.6.0
18
+ Deprecated==1.2.18
19
  distro==1.9.0
20
+ e2b==1.4.0
21
+ e2b-code-interpreter==1.5.0
22
+ envier==0.6.1
23
  fastapi==0.115.12
24
  feedparser==6.0.11
25
  ffmpy==0.5.0
 
37
  httpx-sse==0.4.0
38
  huggingface-hub==0.30.2
39
  idna==3.10
40
+ importlib_metadata==8.6.1
41
  Jinja2==3.1.6
42
  jiter==0.9.0
43
  jsonpatch==1.33
 
61
  multidict==6.4.3
62
  mypy_extensions==1.1.0
63
  numpy==2.2.5
64
+ opentelemetry-api==1.32.1
65
  orjson==3.10.18
66
  ormsgpack==1.9.1
67
  packaging==24.2
68
  pandas==2.2.3
69
  pillow==11.2.1
70
  propcache==0.3.1
71
+ protobuf==5.29.4
72
  pydantic==2.11.4
73
  pydantic-settings==2.9.1
74
  pydantic_core==2.33.2
 
106
  websockets==15.0.1
107
  wikipedia==1.4.0
108
  wrapt==1.17.2
109
+ xmltodict==0.14.2
110
  xxhash==3.5.0
111
  yarl==1.20.0
112
+ yt-dlp==2024.4.9
113
+ zipp==3.21.0
114
  zstandard==0.23.0
tools/__init__.py CHANGED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ from tools.search import taivily_search, serper_search
2
+ from tools.sandbox import execute_code, get_youtube_transcript
3
+
4
+ __all__ = ["taivily_search", "serper_search", "execute_code", "get_youtube_transcript"]
tools/sandbox.py ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain_core.tools import tool
2
+ from typing import Annotated
3
+ from typing_extensions import Annotated
4
+ from langchain_core.tools.base import InjectedToolCallId
5
+ from langchain_core.runnables import RunnableConfig
6
+ from langgraph.types import Command
7
+ from langchain_core.messages import ToolMessage
8
+ import os
9
+ from dotenv import load_dotenv
10
+ import json
11
+ import asyncio
12
+ import tempfile
13
+ from pathlib import Path
14
+ import yt_dlp
15
+ from e2b_code_interpreter import Sandbox
16
+
17
+ load_dotenv()
18
+
19
+ @tool
20
+ def execute_code(code: str, tool_call_id: Annotated[str, InjectedToolCallId], config: RunnableConfig) -> Command:
21
+ """
22
+ Execute code in a secure E2B sandbox environment.
23
+
24
+ Args:
25
+ code: The code to execute. Should be Python code without the triple backticks.
26
+ """
27
+ try:
28
+ loop = asyncio.get_event_loop()
29
+ except RuntimeError:
30
+ loop = asyncio.new_event_loop()
31
+ asyncio.set_event_loop(loop)
32
+
33
+ result = loop.run_until_complete(_execute_code_in_sandbox(code, os.getenv("E2B_API_KEY")))
34
+
35
+ formatted_result = f"""# Code Execution Results
36
+ ## Code
37
+ ```python
38
+ {code}
39
+ ```
40
+ ## Output
41
+ ```
42
+ {result['stdout']}
43
+ ```
44
+ ## Errors
45
+ ```
46
+ {result['stderr']}
47
+ ```
48
+ """
49
+
50
+ external_information = f"{config.get('external_information', '')}\n---\n# Code Execution Results \n{formatted_result}"
51
+ return Command(
52
+ update={
53
+ "external_information": external_information,
54
+ "messages": [ToolMessage(content=formatted_result, tool_call_id=tool_call_id)]
55
+ }
56
+ )
57
+
58
+ async def _execute_code_in_sandbox(code: str, api_key: str):
59
+ """Execute code in E2B sandbox and return the results."""
60
+ sbx = Sandbox()
61
+ execution = sbx.run_code(code)
62
+
63
+ files = sbx.files.list("/")
64
+
65
+ return {
66
+ "stdout": execution.stdout,
67
+ "stderr": execution.stderr,
68
+ "files": files
69
+ }
70
+
71
+ @tool
72
+ def get_youtube_transcript(url: str, tool_call_id: Annotated[str, InjectedToolCallId] = None, config: RunnableConfig = None) -> Command | str:
73
+ """
74
+ This tool extracts the transcript text from YouTube videos, returns the transcript as a string.
75
+ Args:
76
+ url: The YouTube video URL.
77
+ Returns:
78
+ The transcript as a string, or an error message if the transcript couldn't be obtained
79
+ """
80
+ temp_dir = tempfile.mkdtemp()
81
+ current_dir = os.getcwd()
82
+
83
+ try:
84
+ os.chdir(temp_dir)
85
+
86
+ ydl_opts = {
87
+ 'writesubtitles': True,
88
+ 'writeautomaticsub': True,
89
+ 'subtitleslangs': ['en'],
90
+ 'skip_download': True,
91
+ 'outtmpl': 'subtitle',
92
+ }
93
+
94
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
95
+ ydl.extract_info(url, download=True)
96
+
97
+ subtitle_content = ""
98
+ subtitle_files = list(Path(temp_dir).glob("*.vtt")) + list(Path(temp_dir).glob("*.srt"))
99
+
100
+ if subtitle_files:
101
+ with open(subtitle_files[0], 'r', encoding='utf-8') as f:
102
+ subtitle_content = f.read()
103
+
104
+ lines = subtitle_content.split('\n')
105
+ cleaned_lines = []
106
+ for line in lines:
107
+ if line.strip() and not line.strip().isdigit() and not '-->' in line and not line.startswith('WEBVTT'):
108
+ cleaned_lines.append(line)
109
+ subtitle_content = '\n '.join(cleaned_lines)
110
+ else:
111
+ subtitle_content = "Error: No subtitles found for this video."
112
+
113
+ except Exception as e:
114
+ subtitle_content = f"Error retrieving YouTube transcript: {str(e)}"
115
+ finally:
116
+ os.chdir(current_dir)
117
+
118
+ try:
119
+ for file in os.listdir(temp_dir):
120
+ os.remove(os.path.join(temp_dir, file))
121
+ os.rmdir(temp_dir)
122
+ except:
123
+ pass
124
+
125
+ external_information= f"{config.get('external_information', '')}\n---\n# Youtube transcript \n{subtitle_content}"
126
+ return Command(
127
+ update={
128
+ "external_information": external_information,
129
+ "messages": [ToolMessage(content=subtitle_content, tool_call_id=tool_call_id)]
130
+ }
131
+ )
132
+
133
+ """ if __name__ == "__main__":
134
+ # Simple test: print "Hello World"
135
+ test_code = "print(\"Hello World\")"
136
+
137
+ # Build a minimal RunnableConfig with no external information
138
+ config = RunnableConfig(**{"external_information": ""})
139
+
140
+ # Execute the test code
141
+ # Call the underlying function to bypass the BaseTool wrapper
142
+ cmd: Command = execute_code.func(
143
+ test_code,
144
+ "test-call",
145
+ config,
146
+ )
147
+
148
+ # Print the output from the sandbox execution
149
+ updates = getattr(cmd, 'update', {}) or {}
150
+ for msg in updates.get('messages', []):
151
+ print(msg.content) """