leofltt commited on
Commit
e7d661c
·
1 Parent(s): 9e08ba5

langgraph way

Browse files
Files changed (1) hide show
  1. app.py +149 -84
app.py CHANGED
@@ -3,10 +3,13 @@ import gradio as gr
3
  import requests
4
  import inspect
5
  import pandas as pd
 
6
  from langchain_huggingface import HuggingFacePipeline
7
  from langchain_core.prompts import PromptTemplate
8
- from langchain.agents import AgentExecutor, create_react_agent
9
- from langchain import hub
 
 
10
  from langchain.tools import Tool
11
  from langchain_community.tools import DuckDuckGoSearchResults, WikipediaQueryRun
12
  from langchain_community.utilities import WikipediaAPIWrapper
@@ -19,6 +22,12 @@ import torch
19
  from transformers import pipeline
20
  import av
21
  import torchaudio
 
 
 
 
 
 
22
 
23
  # (Keep Constants as is)
24
  # --- Constants ---
@@ -53,8 +62,11 @@ def file_reader_tool(url: str) -> str:
53
  The URL must point directly to the file.
54
  """
55
  try:
 
 
 
56
  # Download the file content
57
- response = requests.get(url)
58
  response.raise_for_status() # Raise an exception for bad status codes
59
 
60
  # Get the content type to guess the file type
@@ -114,10 +126,15 @@ def web_reader_tool(url: str) -> str:
114
  It fetches the HTML and parses it to extract clean text.
115
  """
116
  try:
117
- response = requests.get(url)
 
 
 
118
  response.raise_for_status()
119
  soup = BeautifulSoup(response.content, "html.parser")
120
  return soup.get_text()
 
 
121
  except requests.exceptions.RequestException as e:
122
  return f"Error: Could not download the file from the URL: {e}"
123
  except Exception as e:
@@ -190,6 +207,11 @@ def video_analyzer_tool(url: str) -> str:
190
  except Exception as e:
191
  return f"Error analyzing video: {e}"
192
 
 
 
 
 
 
193
 
194
  # --- Basic Agent Definition ---
195
  # ----- THIS IS WERE YOU CAN BUILD WHAT YOU WANT ------
@@ -203,13 +225,11 @@ class BasicAgent:
203
  "Warning: HUGGING_FACE_HUB_TOKEN secret not found. This will fail for gated models."
204
  )
205
 
206
- # It's better to initialize the model here to avoid reloading it for every call.
207
- # Using a smaller model for faster response times.
208
- self.llm = HuggingFacePipeline.from_model_id(
209
- model_id="google/gemma-2-2b-it",
210
- task="text-generation",
211
- model_kwargs={"dtype": "float32"},
212
- pipeline_kwargs={"max_new_tokens": 256},
213
  )
214
 
215
  tools = [
@@ -250,90 +270,135 @@ class BasicAgent:
250
  description="Analyzes the audio track of a video from a URL by transcribing it into text. The URL must point directly to a video file (e.g., .mp4).",
251
  ),
252
  ]
253
-
254
- # A more direct, instruction-based prompt with high-quality examples.
255
- gemma_react_prompt = '''<start_of_turn>user
256
- You are a helpful agent. Your goal is to answer the user's question.
257
- You have access to the following tools:
258
 
259
  {tools}
260
 
261
- To answer the question, you MUST follow these steps:
262
- 1. **Thought:** First, create a step-by-step plan to answer the question. The plan should be a numbered list. Then, start executing the first step of the plan.
263
- 2. **Action:** Choose one of the available tools for your 'Action'. Your 'Action' must be one of [{tool_names}].
264
- 3. **Action Input:** Provide the input for that tool. The input should be a simple string, not a Python expression.
265
- 4. **Observation:** Wait for the result of the tool.
266
- 5. **Thought:** Analyze the result and the current plan. If a tool fails, you MUST try to recover by using a different tool or a different input. If you get stuck, you MUST try to search for the information using DuckDuckGo Search Results. If you find a relevant URL, you can use the Web Page Reader tool to read its content. If the plan needs to be revised, state the new plan. Then, proceed with the next step.
267
- 6. Repeat steps 2-5 until you have enough information to answer the question.
268
- 7. **Thought:** Once you have the final answer, you MUST use the following template: `FINAL ANSWER: [YOUR FINAL ANSWER]`.
269
-
270
- **IMPORTANT RULES for the FINAL ANSWER:**
271
- * YOUR FINAL ANSWER should be a number OR as few words as possible OR a comma separated list of numbers and/or strings.
272
- * 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.
273
- * 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.
274
- * If you are asked for a comma separated list, Apply the rules above for each element (number or string), ensure there is exactly one space after each comma.
275
- * Your answer should only start with "FINAL ANSWER: ", then follows with the answer.
276
-
277
- Do not make up observations.
278
-
279
- Here are some examples:
280
-
281
- Question: In the YouTube video at `https://www.youtube.com/watch?v=example`, the speaker mentions a specific battle. Use the transcript to find the name of the battle, then find out which year it took place.
282
-
283
- Thought:
284
- My plan is:
285
- 1. Get the transcript of the YouTube video.
286
- 2. Find the name of the battle mentioned in the transcript.
287
- 3. Use Wikipedia to find the year the battle took place.
288
- I will now execute step 1.
289
- Action: YouTube Transcript Reader
290
- Action Input: https://www.youtube.com/watch?v=example
291
- Observation: ...in the video today we are talking about the famous Battle of Hastings...
292
- Thought: I have completed step 1 and 2 of my plan. The transcript mentions the "Battle of Hastings". Now I need to execute step 3 and find the year it took place. Wikipedia is best for historical events.
293
- Action: Wikipedia
294
- Action Input: Battle of Hastings
295
- Observation: Page: Battle of Hastings. Summary: The Battle of Hastings was fought on 14 October 1066...
296
- Thought: I have found the year of the battle, 1066. I have completed all steps of my plan.
297
- FINAL ANSWER: Battle of Hastings, 1066
298
-
299
- Question: How many studio albums did Mercedes Sosa release between 2000 and 2009?
300
-
301
- Thought:
302
- My plan is:
303
- 1. Search for Mercedes Sosa's discography.
304
- 2. From the search results, find a reliable source like Wikipedia or a music database.
305
- 3. Read the content of the source.
306
- 4. Count the number of studio albums released between 2000 and 2009.
307
- I will now execute step 1.
308
- Action: DuckDuckGo Search Results
309
- Action Input: Mercedes Sosa discography
310
- Observation: [snippet: Mercedes Sosa discography. Studio albums. Year, Album, Label. 1959, La voz de la zafra, RCA Victor. ... 2000, Acústico, ... 2005, Corazón libre, ... 2009, Cantora, ... link: https://en.wikipedia.org/wiki/Mercedes_Sosa_discography], ...
311
- Thought: The search results show a Wikipedia page that seems to contain the discography. I will read the content of that page.
312
- Action: Web Page Reader
313
- Action Input: https://en.wikipedia.org/wiki/Mercedes_Sosa_discography
314
- Observation: ...Studio albums... 2000 Acústico ... 2005 Corazón libre ... 2009 Cantora ...
315
- Thought: I have the list of albums. Now I need to count the ones between 2000 and 2009. I see "Acústico" (2000), "Corazón libre" (2005), and "Cantora" (2009). That's 3 albums.
316
- FINAL ANSWER: 3
317
-
318
- Begin!
319
-
320
- Question: {input}
321
- Thought:{agent_scratchpad}<end_of_turn>
322
  <start_of_turn>model
323
- '''
324
- prompt = PromptTemplate.from_template(gemma_react_prompt)
 
 
 
325
 
326
- agent = create_react_agent(self.llm, tools, prompt)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
327
 
328
- self.agent_executor = AgentExecutor(
329
- agent=agent, tools=tools, verbose=True, handle_parsing_errors=True
 
 
 
 
 
330
  )
331
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
332
  def __call__(self, question: str) -> str:
333
  print(f"Agent received question (first 50 chars): {question[:50]}...")
334
  try:
335
- result = self.agent_executor.invoke({"input": question})
336
- analysis = result["output"]
337
  print(f"Agent returning analysis: {analysis}")
338
  return analysis
339
  except Exception as e:
 
3
  import requests
4
  import inspect
5
  import pandas as pd
6
+ import re
7
  from langchain_huggingface import HuggingFacePipeline
8
  from langchain_core.prompts import PromptTemplate
9
+ from langchain.agents.format_scratchpad import format_log_to_str
10
+ from langchain_core.runnables import RunnablePassthrough
11
+ from langchain.tools.render import render_text_description
12
+ from langchain_core.agents import AgentAction, AgentFinish
13
  from langchain.tools import Tool
14
  from langchain_community.tools import DuckDuckGoSearchResults, WikipediaQueryRun
15
  from langchain_community.utilities import WikipediaAPIWrapper
 
22
  from transformers import pipeline
23
  import av
24
  import torchaudio
25
+ from langgraph.graph import StateGraph, END, START
26
+ from langgraph.prebuilt import ToolNode
27
+ from typing import TypedDict, Annotated, List, Union
28
+ from langchain_core.messages import AnyMessage, SystemMessage, HumanMessage, ToolMessage, BaseMessage
29
+ import operator
30
+ import json
31
 
32
  # (Keep Constants as is)
33
  # --- Constants ---
 
62
  The URL must point directly to the file.
63
  """
64
  try:
65
+ headers = {
66
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'
67
+ }
68
  # Download the file content
69
+ response = requests.get(url, headers=headers)
70
  response.raise_for_status() # Raise an exception for bad status codes
71
 
72
  # Get the content type to guess the file type
 
126
  It fetches the HTML and parses it to extract clean text.
127
  """
128
  try:
129
+ headers = {
130
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'
131
+ }
132
+ response = requests.get(url, headers=headers)
133
  response.raise_for_status()
134
  soup = BeautifulSoup(response.content, "html.parser")
135
  return soup.get_text()
136
+ except requests.exceptions.HTTPError as e:
137
+ return f"Error: Could not download the file from the URL. The server returned a {e.response.status_code} error. Please try a different URL or a different tool."
138
  except requests.exceptions.RequestException as e:
139
  return f"Error: Could not download the file from the URL: {e}"
140
  except Exception as e:
 
207
  except Exception as e:
208
  return f"Error analyzing video: {e}"
209
 
210
+ class AgentState(TypedDict):
211
+ input: str
212
+ chat_history: list[BaseMessage]
213
+ agent_outcome: Union[AgentAction, AgentFinish, None]
214
+ intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add]
215
 
216
  # --- Basic Agent Definition ---
217
  # ----- THIS IS WERE YOU CAN BUILD WHAT YOU WANT ------
 
225
  "Warning: HUGGING_FACE_HUB_TOKEN secret not found. This will fail for gated models."
226
  )
227
 
228
+ self.pipeline = pipeline(
229
+ "text-generation",
230
+ model="google/gemma-2-2b-it",
231
+ torch_dtype=torch.float32,
232
+ device="mps",
 
 
233
  )
234
 
235
  tools = [
 
270
  description="Analyzes the audio track of a video from a URL by transcribing it into text. The URL must point directly to a video file (e.g., .mp4).",
271
  ),
272
  ]
273
+ prompt_template_str = """<start_of_turn>user
274
+ You are a helpful assistant tasked with answering questions. You have access to the following tools:
 
 
 
275
 
276
  {tools}
277
 
278
+ To use a tool, you must respond with a JSON blob containing "action" and "action_input" keys.
279
+ The "action" must be one of [{tool_names}].
280
+
281
+ If you have the final answer, you must respond with:
282
+ FINAL ANSWER: [your answer]
283
+
284
+ The user has provided the following constraints for the final answer:
285
+ YOUR FINAL ANSWER should be a number OR as few words as possible OR a comma separated list of numbers and/or strings. If you are asked for a number, don't use comma to write your number neither use units such as $ or percent sign unless specified otherwise. If you are asked for a string, don't use articles, neither abbreviations (e.g. for cities), and write the digits in plain text unless specified otherwise. If you are asked for a comma separated list, Apply the rules above for each element (number or string), ensure there is exactly one space after each comma.
286
+ Your answer should only start with "FINAL ANSWER: ", then follows with the answer.
287
+
288
+ Here is the conversation history:
289
+ {agent_scratchpad}
290
+
291
+ Now, answer this question:
292
+ Question: {input}<end_of_turn>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
  <start_of_turn>model
294
+ """
295
+ prompt = PromptTemplate.from_template(prompt_template_str).partial(
296
+ tools=render_text_description(tools),
297
+ tool_names=", ".join([t.name for t in tools]),
298
+ )
299
 
300
+ def format_scratchpad(intermediate_steps):
301
+ log = []
302
+ for action, observation in intermediate_steps:
303
+ log.append(action.log)
304
+ log.append(f"Observation: {observation}")
305
+ return "\n".join(log)
306
+
307
+ def llm_wrapper(prompt_value):
308
+ prompt_str = prompt_value.to_string()
309
+ outputs = self.pipeline(
310
+ prompt_str,
311
+ max_new_tokens=256,
312
+ return_full_text=False,
313
+ )
314
+ return outputs[0]["generated_text"]
315
 
316
+ agent_runnable = (
317
+ RunnablePassthrough.assign(
318
+ agent_scratchpad=lambda x: format_scratchpad(x["intermediate_steps"])
319
+ )
320
+ | prompt
321
+ | llm_wrapper
322
+ | self._parse_agent_output
323
  )
324
 
325
+ def run_agent(state):
326
+ agent_outcome = agent_runnable.invoke(state)
327
+ return {"agent_outcome": agent_outcome}
328
+
329
+ def execute_tools(state):
330
+ agent_action = state["agent_outcome"]
331
+ tool_name = agent_action.tool
332
+
333
+ tool_to_use = None
334
+ for tool in tools:
335
+ if tool.name == tool_name:
336
+ tool_to_use = tool
337
+ break
338
+
339
+ if not tool_to_use:
340
+ output = f"Error: Tool '{tool_name}' not found."
341
+ else:
342
+ try:
343
+ output = tool_to_use.run(agent_action.tool_input)
344
+ except Exception as e:
345
+ output = f"Error executing tool '{tool_name}': {e}"
346
+
347
+ return {"intermediate_steps": [(agent_action, str(output))]}
348
+
349
+ def should_continue(state):
350
+ if isinstance(state["agent_outcome"], AgentFinish):
351
+ return "end"
352
+ else:
353
+ return "continue"
354
+
355
+ workflow = StateGraph(AgentState)
356
+ workflow.add_node("agent", run_agent)
357
+ workflow.add_node("action", execute_tools)
358
+ workflow.set_entry_point("agent")
359
+ workflow.add_conditional_edges(
360
+ "agent",
361
+ should_continue,
362
+ {
363
+ "continue": "action",
364
+ "end": END,
365
+ },
366
+ )
367
+ workflow.add_edge("action", "agent")
368
+ self.agent_executor = workflow.compile()
369
+
370
+ def _parse_agent_output(self, llm_output: str) -> Union[AgentAction, AgentFinish]:
371
+ if "FINAL ANSWER:" in llm_output:
372
+ return AgentFinish(
373
+ return_values={"output": llm_output.split("FINAL ANSWER:")[-1].strip()},
374
+ log=llm_output,
375
+ )
376
+
377
+ try:
378
+ llm_output_stripped = llm_output.strip()
379
+ if llm_output_stripped.startswith("```json"):
380
+ llm_output_stripped = llm_output_stripped[7:-3].strip()
381
+
382
+ response = json.loads(llm_output_stripped)
383
+
384
+ if "action" in response and "action_input" in response:
385
+ return AgentAction(
386
+ tool=response["action"],
387
+ tool_input=response["action_input"],
388
+ log=llm_output,
389
+ )
390
+ else:
391
+ # Malformed JSON with missing keys
392
+ return AgentFinish(return_values={"output": f"Malformed JSON. Missing keys.\nLLM Output:\n{llm_output}"}, log=llm_output)
393
+
394
+ except json.JSONDecodeError as e:
395
+ return AgentFinish(return_values={"output": f"JSON Parse Error: {e}\nLLM Output:\n{llm_output}"}, log=llm_output)
396
+
397
  def __call__(self, question: str) -> str:
398
  print(f"Agent received question (first 50 chars): {question[:50]}...")
399
  try:
400
+ result = self.agent_executor.invoke({"input": question, "chat_history": []})
401
+ analysis = result['agent_outcome'].return_values["output"]
402
  print(f"Agent returning analysis: {analysis}")
403
  return analysis
404
  except Exception as e: