grshot commited on
Commit
e628580
·
1 Parent(s): 0dc3418
Files changed (2) hide show
  1. agent.py +135 -37
  2. requirements.txt +14 -13
agent.py CHANGED
@@ -1,6 +1,6 @@
1
  import json
2
  import os
3
- from typing import Annotated
4
 
5
  import pandas as pd
6
  from langchain_community.document_loaders import WikipediaLoader, YoutubeLoader
@@ -23,40 +23,65 @@ from langgraph.graph.message import add_messages
23
  from langgraph.prebuilt import ToolNode, tools_condition
24
 
25
 
 
 
 
 
 
 
 
26
  @tool("search_web_sources")
27
- def search_web_sources(query: Annotated[str, "Search query string"]) -> dict:
28
  """Performs a web search and returns up to 3 formatted documents with content and source."""
 
 
 
 
 
29
 
30
- if not os.environ.get("TAVILY_API_KEY"):
31
- raise EnvironmentError("TAVILY_API_KEY is not set in environment variables.")
32
- search_docs = TavilySearch(max_results=3).invoke({"query": query})
33
- formatted = "\n\n---\n\n".join(
34
- [
35
- f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}">\n{doc.page_content}\n</Document>'
36
- for doc in search_docs
37
- ]
38
- )
39
- return {"web_results": formatted}
 
 
 
40
 
41
 
42
  @tool
43
- def search_wikipedia(query: str) -> dict:
44
  """Search Wikipedia using LangChain's loader and return the first document summary."""
45
  try:
 
 
 
 
 
 
46
  loader = WikipediaLoader(query=query, lang="en", load_max_docs=2)
47
  docs = loader.load()
48
- if docs:
49
- formatted_docs = "---".join(
50
- [
51
- f'<WikipediaArticle title="{query}">{doc.page_content}</WikipediaArticle>'
52
- for doc in docs
53
- ]
54
- )
55
- return {"wiki_results": formatted_docs}
56
- else:
57
- return {"wiki_results": "No content found."}
 
58
  except Exception as e:
59
- return {"wiki_results": f"Error fetching Wikipedia article: {e}"}
 
 
 
60
 
61
 
62
  @tool
@@ -97,17 +122,35 @@ def run_python_code(code: str) -> str:
97
  # --- System Prompt ---
98
  system_prompt = SystemMessage(
99
  content="""
100
- You are a helpful and precise assistant. You will receive a question and optionally access tools to help answer it.
101
-
102
- Your job is to think step-by-step, clearly report your thoughts, and conclude with a formatted response.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
 
104
  Use this format strictly:
105
  FINAL ANSWER: [your concise answer here]
106
 
107
  Rules for your answer:
108
- - If the answer is a number, write only the number (no commas, units, or symbols unless asked).
109
- - If it's a string, avoid articles (a, an, the), don't abbreviate, and use plain text digits.
110
- - If a list, follow the rules above for each element and separate with a comma and single space (e.g., "apple, orange, banana").
 
111
 
112
  Your response must always begin with: FINAL ANSWER:
113
  """
@@ -124,18 +167,39 @@ def build_agent_graph(provider: str = "groq"):
124
  run_python_code,
125
  ]
126
 
127
- # Instantiate LLM
128
- os.environ["GROQ_API_KEY"]
129
- llm = ChatGroq(model="qwen-qwq-32b", temperature=0)
 
 
 
 
 
 
 
 
 
 
130
 
131
  # Bind tools to the LLM
132
  llm_with_tools = llm.bind_tools(tools)
133
 
134
  # Assistant: reasoning step that plans next action
135
  def assistant_node(state: MessagesState) -> dict:
136
- messages = state["messages"]
137
- response = llm_with_tools.invoke(messages)
138
- return {"messages": response}
 
 
 
 
 
 
 
 
 
 
 
139
 
140
  # Stubbed retriever node for future integration
141
  def retriever_node(state: MessagesState):
@@ -156,17 +220,51 @@ def build_agent_graph(provider: str = "groq"):
156
  # ToolNode wrapper for actual tool use
157
  tool_node = ToolNode(tools)
158
 
159
- # Define the graph with ReAct loop
 
 
 
 
 
 
 
 
160
  builder = StateGraph(MessagesState)
161
  builder.add_node("assistant", RunnableLambda(assistant_node))
162
  builder.add_node("tools", tool_node)
163
  builder.add_node("retriever", RunnableLambda(retriever_node))
 
164
 
165
  builder.set_entry_point("assistant")
166
  builder.add_conditional_edges("assistant", tools_condition)
167
  builder.add_edge("tools", "assistant")
168
  builder.add_edge("assistant", END)
169
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  graph = builder.compile()
171
 
172
  # Optional: test entrypoint to run the graph manually
 
1
  import json
2
  import os
3
+ from typing import Annotated, Dict, Optional
4
 
5
  import pandas as pd
6
  from langchain_community.document_loaders import WikipediaLoader, YoutubeLoader
 
23
  from langgraph.prebuilt import ToolNode, tools_condition
24
 
25
 
26
+ # Custom exception for tool errors
27
+ class ToolExecutionError(Exception):
28
+ """Custom exception for tool execution errors"""
29
+
30
+ pass
31
+
32
+
33
  @tool("search_web_sources")
34
+ def search_web_sources(query: Annotated[str, "Search query string"]) -> Dict[str, str]:
35
  """Performs a web search and returns up to 3 formatted documents with content and source."""
36
+ try:
37
+ if not os.environ.get("TAVILY_API_KEY"):
38
+ raise EnvironmentError(
39
+ "TAVILY_API_KEY is not set in environment variables."
40
+ )
41
 
42
+ search_docs = TavilySearch(max_results=3).invoke({"query": query})
43
+ if not search_docs:
44
+ return {"web_results": "No results found for the given query."}
45
+
46
+ formatted = "\n\n---\n\n".join(
47
+ [
48
+ f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}">\n{doc.page_content}\n</Document>'
49
+ for doc in search_docs
50
+ ]
51
+ )
52
+ return {"web_results": formatted}
53
+ except Exception as e:
54
+ return {"web_results": f"Error during web search: {str(e)}"}
55
 
56
 
57
  @tool
58
+ def search_wikipedia(query: str) -> Dict[str, str]:
59
  """Search Wikipedia using LangChain's loader and return the first document summary."""
60
  try:
61
+ # Input validation
62
+ if not query or not isinstance(query, str):
63
+ return {
64
+ "wiki_results": "Invalid query provided. Please provide a valid search term."
65
+ }
66
+
67
  loader = WikipediaLoader(query=query, lang="en", load_max_docs=2)
68
  docs = loader.load()
69
+
70
+ if not docs:
71
+ return {"wiki_results": f"No Wikipedia articles found for query: {query}"}
72
+
73
+ formatted_docs = "---".join(
74
+ [
75
+ f'<WikipediaArticle title="{query}">{doc.page_content}</WikipediaArticle>'
76
+ for doc in docs
77
+ ]
78
+ )
79
+ return {"wiki_results": formatted_docs}
80
  except Exception as e:
81
+ error_msg = str(e)
82
+ if "Page id" in error_msg and "not found" in error_msg:
83
+ return {"wiki_results": f"No Wikipedia article found for: {query}"}
84
+ return {"wiki_results": f"Error searching Wikipedia: {error_msg}"}
85
 
86
 
87
  @tool
 
122
  # --- System Prompt ---
123
  system_prompt = SystemMessage(
124
  content="""
125
+ You are a helpful and precise assistant with access to several tools. You will receive questions and use tools appropriately to find answers.
126
+
127
+ When using tools:
128
+ 1. Format tool calls correctly using the tool's exact name and required parameters
129
+ 2. Validate inputs before making tool calls
130
+ 3. Handle tool responses appropriately, checking for errors
131
+ 4. If a tool fails, try an alternative approach or provide a clear error message
132
+
133
+ Available tools:
134
+ - search_web_sources: Search web for information (requires query parameter)
135
+ - search_wikipedia: Search Wikipedia articles (requires query parameter)
136
+ - extract_youtube_transcript: Get transcript from YouTube videos (requires video_url parameter)
137
+ - run_python_code: Execute Python code (requires code parameter)
138
+
139
+ Think step-by-step:
140
+ 1. Understand the question
141
+ 2. Choose appropriate tool(s)
142
+ 3. Format tool calls correctly
143
+ 4. Process tool responses
144
+ 5. Formulate final answer
145
 
146
  Use this format strictly:
147
  FINAL ANSWER: [your concise answer here]
148
 
149
  Rules for your answer:
150
+ - If the answer is a number, write only the number (no commas, units, or symbols unless asked)
151
+ - If it's a string, avoid articles (a, an, the), don't abbreviate, and use plain text digits
152
+ - If a list, follow the rules above for each element and separate with a comma and single space (e.g., "apple, orange, banana")
153
+ - If there's an error, start with "Error:" followed by a clear explanation
154
 
155
  Your response must always begin with: FINAL ANSWER:
156
  """
 
167
  run_python_code,
168
  ]
169
 
170
+ # Instantiate LLM with proper error handling
171
+ groq_api_key = os.getenv("GROQ_API_KEY")
172
+ if not groq_api_key:
173
+ raise EnvironmentError("GROQ_API_KEY environment variable is not set")
174
+
175
+ try:
176
+ from pydantic import SecretStr
177
+
178
+ llm = ChatGroq(
179
+ model="qwen-qwq-32b", temperature=0, api_key=SecretStr(groq_api_key)
180
+ )
181
+ except Exception as e:
182
+ raise Exception(f"Failed to initialize Groq LLM: {str(e)}")
183
 
184
  # Bind tools to the LLM
185
  llm_with_tools = llm.bind_tools(tools)
186
 
187
  # Assistant: reasoning step that plans next action
188
  def assistant_node(state: MessagesState) -> dict:
189
+ try:
190
+ messages = state["messages"]
191
+ response = llm_with_tools.invoke(messages)
192
+
193
+ # Validate response format
194
+ if not response or not isinstance(
195
+ response, (AIMessage, HumanMessage, SystemMessage)
196
+ ):
197
+ raise ValueError("Invalid response format from LLM")
198
+
199
+ return {"messages": response}
200
+ except Exception as e:
201
+ error_msg = f"Error in assistant node: {str(e)}"
202
+ return {"messages": AIMessage(content=f"FINAL ANSWER: {error_msg}")}
203
 
204
  # Stubbed retriever node for future integration
205
  def retriever_node(state: MessagesState):
 
220
  # ToolNode wrapper for actual tool use
221
  tool_node = ToolNode(tools)
222
 
223
+ # Define error handling node
224
+ def error_handler_node(state: MessagesState) -> dict:
225
+ """Handle errors in the graph execution"""
226
+ error_msg = state.get("error", "Unknown error occurred")
227
+ return {
228
+ "messages": AIMessage(content=f"FINAL ANSWER: Error occurred: {error_msg}")
229
+ }
230
+
231
+ # Define the graph with ReAct loop and error handling
232
  builder = StateGraph(MessagesState)
233
  builder.add_node("assistant", RunnableLambda(assistant_node))
234
  builder.add_node("tools", tool_node)
235
  builder.add_node("retriever", RunnableLambda(retriever_node))
236
+ builder.add_node("error_handler", RunnableLambda(error_handler_node))
237
 
238
  builder.set_entry_point("assistant")
239
  builder.add_conditional_edges("assistant", tools_condition)
240
  builder.add_edge("tools", "assistant")
241
  builder.add_edge("assistant", END)
242
 
243
+ # Add error handling edges
244
+ def route_by_error(state: MessagesState):
245
+ """Route to error handler if error is present, otherwise continue normal flow"""
246
+ if "error" in state:
247
+ return "error_handler"
248
+ return None
249
+
250
+ builder.add_conditional_edges(
251
+ "assistant",
252
+ route_by_error,
253
+ {
254
+ "error_handler": "error_handler",
255
+ },
256
+ )
257
+
258
+ builder.add_conditional_edges(
259
+ "tools",
260
+ route_by_error,
261
+ {
262
+ "error_handler": "error_handler",
263
+ },
264
+ )
265
+
266
+ builder.add_edge("error_handler", END)
267
+
268
  graph = builder.compile()
269
 
270
  # Optional: test entrypoint to run the graph manually
requirements.txt CHANGED
@@ -1,14 +1,15 @@
1
- gradio
2
- requests
3
- langchain
4
- langchain-core
5
- langchain-community
6
- langchain_huggingface
7
- langchain-groq
8
- langchain-experimental
9
- langchain-tavily
10
- langgraph
11
- tavily-python
12
- wikipedia
13
  youtube-transcript-api==0.6.3
14
- pytube
 
 
1
+ gradio>=4.0.0
2
+ requests>=2.31.0
3
+ langchain>=0.1.0
4
+ langchain-core>=0.1.0
5
+ langchain-community>=0.0.10
6
+ langchain_huggingface>=0.0.10
7
+ langchain-groq>=0.0.5
8
+ langchain-experimental>=0.0.40
9
+ langchain-tavily>=0.0.5
10
+ langgraph>=0.0.15
11
+ tavily-python>=0.3.0
12
+ wikipedia>=1.4.0
13
  youtube-transcript-api==0.6.3
14
+ pytube>=15.0.0
15
+ pydantic>=2.0.0