csabakecskemeti commited on
Commit
acee5db
·
verified ·
1 Parent(s): 36b9bd3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +228 -236
app.py CHANGED
@@ -1,267 +1,259 @@
1
  import os
2
  import gradio as gr
3
- import requests
4
- import json
5
- import asyncio
6
- from typing import List, Dict, Any, Generator
7
  import logging
8
- from duckduckgo_search import DDGS
9
- from bs4 import BeautifulSoup
10
- import re
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
- logging.basicConfig(level=logging.INFO)
13
  logger = logging.getLogger(__name__)
14
 
 
 
 
 
 
15
 
16
- llm_ip = os.environ.get('public_ip', DEFAULT_IP)
17
- llm_port = os.environ.get('port', DEFAULT_PORT)
18
- llm_key = os.environ.get('api_key', DEFAULT_KEY)
19
- llm_model = os.environ.get('model', DEFAULT_MODEL)
20
 
21
- class WebTools:
22
- def __init__(self):
23
- self.session = requests.Session()
24
- self.session.headers.update({
25
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
26
- })
27
- self.ddgs = DDGS()
28
 
29
- def search_web(self, query: str, max_results: int = 5) -> str:
30
- """Search the web using DuckDuckGo"""
31
- try:
32
- results = self.ddgs.text(query, max_results=max_results)
33
- if not results:
34
- return f"No search results found for: {query}"
35
-
36
- formatted_results = f"Search results for '{query}':\n\n"
37
- for i, result in enumerate(results, 1):
38
- title = result.get('title', 'No title')
39
- body = result.get('body', 'No description')
40
- href = result.get('href', 'No URL')
41
- formatted_results += f"{i}. **{title}**\n{body}\nURL: {href}\n\n"
42
-
43
- return formatted_results
44
- except Exception as e:
45
- logger.error(f"Search error: {e}")
46
- return f"Search error: {str(e)}"
47
-
48
- def visit_website(self, url: str) -> str:
49
- """Visit a website and extract its text content"""
50
- try:
51
- if not url.startswith(('http://', 'https://')):
52
- url = 'https://' + url
53
-
54
- response = self.session.get(url, timeout=10)
55
- response.raise_for_status()
56
-
57
- soup = BeautifulSoup(response.content, 'html.parser')
58
-
59
- # Remove script and style elements
60
- for script in soup(["script", "style", "nav", "footer", "header"]):
61
- script.decompose()
62
-
63
- # Get text content
64
- text = soup.get_text()
65
-
66
- # Clean up text
67
- lines = (line.strip() for line in text.splitlines())
68
- chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
69
- text = ' '.join(chunk for chunk in chunks if chunk)
70
-
71
- # Limit text length
72
- if len(text) > 3000:
73
- text = text[:3000] + "... (content truncated)"
74
-
75
- return f"Content from {url}:\n\n{text}"
76
- except Exception as e:
77
- logger.error(f"Website visit error: {e}")
78
- return f"Error visiting {url}: {str(e)}"
79
-
80
- class LLMClient:
81
  def __init__(self, ip: str, port: str, api_key: str, model: str):
82
  self.ip = ip
83
  self.port = port
84
  self.api_key = api_key
85
  self.model = model
86
- self.base_url = f"http://{ip}:{port}/v1/chat/completions"
87
-
88
- def call_llm(self, messages: List[Dict], max_tokens: int = 512, stream: bool = False):
89
- """Call the LLM API"""
90
- headers = {
91
- "Content-Type": "application/json",
92
- "Authorization": f"Bearer {self.api_key}"
93
- }
94
- data = {
95
- "model": self.model,
96
- "messages": messages,
97
- "max_tokens": max_tokens,
98
- "stream": stream
99
- }
100
 
 
 
101
  try:
102
- response = requests.post(self.base_url, headers=headers, json=data,
103
- stream=stream, timeout=30)
104
- response.raise_for_status()
105
-
106
- if stream:
107
- return response
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  else:
109
- result = response.json()
110
- return result["choices"][0]["message"]["content"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  except Exception as e:
112
- logger.error(f"LLM API call failed: {e}")
113
- return f"Error: {str(e)}"
114
-
115
- class ReactAgent:
116
- def __init__(self, llm_client: LLMClient):
117
- self.llm_client = llm_client
118
- self.web_tools = WebTools()
119
- self.system_prompt = """You are a helpful AI assistant with access to web browsing capabilities. You can:
120
- 1. Search the web using DuckDuckGo
121
- 2. Visit and analyze websites
122
- 3. Answer questions based on current information
123
-
124
- When a user asks something that requires current information or web searching, use the available tools.
125
-
126
- Available tools:
127
- - search_web(query): Search DuckDuckGo for information
128
- - visit_website(url): Visit and extract content from a website
129
-
130
- Format your tool calls as: TOOL[tool_name: parameters]
131
- For example: TOOL[search_web: latest news about AI] or TOOL[visit_website: https://example.com]
132
-
133
- Always explain what you're doing and provide helpful responses based on the information you gather."""
134
-
135
- def parse_tool_calls(self, text: str) -> List[Dict]:
136
- """Parse tool calls from agent response"""
137
- tool_pattern = r'TOOL\[(\w+):\s*([^\]]+)\]'
138
- matches = re.findall(tool_pattern, text)
139
-
140
- tools = []
141
- for tool_name, params in matches:
142
- tools.append({
143
- 'name': tool_name,
144
- 'params': params.strip()
145
- })
146
- return tools
147
-
148
- def execute_tool(self, tool_name: str, params: str) -> str:
149
- """Execute a tool and return results"""
150
  try:
151
- if tool_name == 'search_web':
152
- return self.web_tools.search_web(params)
153
- elif tool_name == 'visit_website':
154
- return self.web_tools.visit_website(params)
155
- else:
156
- return f"Unknown tool: {tool_name}"
157
- except Exception as e:
158
- return f"Tool execution error: {str(e)}"
159
 
160
- def process_message(self, message: str, history: List[List[str]], max_tokens: int) -> Generator[str, None, None]:
161
- """Process user message with ReAct pattern"""
162
- try:
163
- # Format chat history
164
- messages = [{"role": "system", "content": self.system_prompt}]
165
 
 
 
166
  for user_msg, assistant_msg in history:
167
- messages.append({"role": "user", "content": user_msg})
168
- if assistant_msg:
169
- messages.append({"role": "assistant", "content": assistant_msg})
170
-
171
- messages.append({"role": "user", "content": message})
172
-
173
- # Initial LLM call
174
- response = self.llm_client.call_llm(messages, max_tokens, stream=True)
175
-
176
- current_response = ""
177
- tool_calls_made = False
178
-
179
- # Stream initial response
180
- for line in response.iter_lines():
181
- if line:
182
- line = line.decode('utf-8')
183
- if line.startswith('data: '):
184
- data_str = line[6:]
185
- if data_str.strip() == '[DONE]':
186
- break
187
- try:
188
- data = json.loads(data_str)
189
- if 'choices' in data and len(data['choices']) > 0:
190
- delta = data['choices'][0].get('delta', {})
191
- content = delta.get('content', '')
192
- if content:
193
- current_response += content
194
- yield current_response
195
- except json.JSONDecodeError:
196
- continue
197
-
198
- # Check for tool calls
199
- tool_calls = self.parse_tool_calls(current_response)
200
-
201
- if tool_calls:
202
- tool_calls_made = True
203
- for tool_call in tool_calls:
204
- yield current_response + f"\n\n🔍 Executing {tool_call['name']}..."
205
-
206
- tool_result = self.execute_tool(tool_call['name'], tool_call['params'])
207
-
208
- # Add tool result to conversation
209
- messages.append({"role": "assistant", "content": current_response})
210
- messages.append({"role": "user", "content": f"Tool result:\n{tool_result}\n\nPlease provide a helpful response based on this information."})
211
-
212
- # Get final response
213
- final_response = self.llm_client.call_llm(messages, max_tokens, stream=True)
214
-
215
- final_text = current_response + f"\n\n**Tool Results:**\n{tool_result}\n\n**Response:**\n"
216
-
217
- for line in final_response.iter_lines():
218
- if line:
219
- line = line.decode('utf-8')
220
- if line.startswith('data: '):
221
- data_str = line[6:]
222
- if data_str.strip() == '[DONE]':
223
- break
224
- try:
225
- data = json.loads(data_str)
226
- if 'choices' in data and len(data['choices']) > 0:
227
- delta = data['choices'][0].get('delta', {})
228
- content = delta.get('content', '')
229
- if content:
230
- final_text += content
231
- yield final_text
232
- except json.JSONDecodeError:
233
- continue
234
- break # Only handle first tool call for now
235
 
236
  except Exception as e:
237
  error_msg = f"Agent error: {str(e)}"
238
- logger.error(error_msg)
239
- yield error_msg
 
 
 
 
240
 
241
- # Initialize components
242
- llm_client = LLMClient(llm_ip, llm_port, llm_key, llm_model)
243
- agent = ReactAgent(llm_client)
244
 
245
  def generate_response(message: str, history: List[List[str]], system_prompt: str,
246
  max_tokens: int, ip: str, port: str, api_key: str, model: str):
247
- """Generate streaming response using the agent"""
248
- global llm_client, agent
 
 
 
 
249
 
250
- # Update LLM client if parameters changed
251
- if (ip != llm_client.ip or port != llm_client.port or
252
- api_key != llm_client.api_key or model != llm_client.model):
253
- llm_client = LLMClient(ip, port, api_key, model)
254
- agent = ReactAgent(llm_client)
255
 
256
- # Update system prompt if provided
257
- if system_prompt.strip():
258
- agent.system_prompt = system_prompt
 
 
 
259
 
260
- # Generate response
261
- for response in agent.process_message(message, history, max_tokens):
262
- yield response
 
263
 
264
- # Create Gradio interface
265
  chatbot = gr.ChatInterface(
266
  generate_response,
267
  chatbot=gr.Chatbot(
@@ -273,14 +265,14 @@ chatbot = gr.ChatInterface(
273
  ),
274
  additional_inputs=[
275
  gr.Textbox(
276
- "You are a helpful AI assistant with web browsing capabilities. You can search the web and visit websites to provide current information. Use TOOL[search_web: query] to search or TOOL[visit_website: url] to browse websites.",
277
  label="System Prompt",
278
- lines=3
279
  ),
280
  gr.Slider(50, 2048, label="Max Tokens", value=512,
281
  info="Maximum number of tokens in the response"),
282
  gr.Textbox(llm_ip, label="LLM IP Address",
283
- info="IP address of the LLM server"),
284
  gr.Textbox(llm_port, label="LLM Port",
285
  info="Port of the LLM server"),
286
  gr.Textbox(llm_key, label="API Key", type="password",
@@ -288,8 +280,8 @@ chatbot = gr.ChatInterface(
288
  gr.Textbox(llm_model, label="Model Name",
289
  info="Name of the model to use"),
290
  ],
291
- title="🤖 AI Agent with Web Browsing",
292
- description="Chat with an AI agent that can search the web and browse websites using DuckDuckGo. Use natural language to ask for current information!",
293
  theme="finlaymacklon/smooth_slate",
294
  submit_btn="Send",
295
  retry_btn="🔄 Regenerate Response",
 
1
  import os
2
  import gradio as gr
3
+ from typing import List
 
 
 
4
  import logging
5
+ import logging.handlers
6
+ import time
7
+ import random
8
+ from langchain_openai import ChatOpenAI
9
+ from langchain_core.tools import tool
10
+ from langgraph.prebuilt import create_react_agent
11
+ from langchain_core.messages import HumanMessage
12
+ from langchain_tavily import TavilySearch
13
+
14
+ # Configuration - set to False to disable detailed logging
15
+ ENABLE_DETAILED_LOGGING = True
16
+
17
+ # Setup logging with rotation (7 days max)
18
+ if ENABLE_DETAILED_LOGGING:
19
+ # Create formatter
20
+ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
21
+
22
+ # Setup console handler
23
+ console_handler = logging.StreamHandler()
24
+ console_handler.setFormatter(formatter)
25
+
26
+ # Setup rotating file handler (7 days, daily rotation)
27
+ file_handler = logging.handlers.TimedRotatingFileHandler(
28
+ 'agent.log',
29
+ when='midnight',
30
+ interval=1,
31
+ backupCount=7, # Keep 7 days of logs
32
+ encoding='utf-8'
33
+ )
34
+ file_handler.setFormatter(formatter)
35
+
36
+ # Configure root logger
37
+ logging.basicConfig(
38
+ level=logging.INFO,
39
+ handlers=[console_handler, file_handler]
40
+ )
41
+ else:
42
+ logging.basicConfig(level=logging.WARNING)
43
 
 
44
  logger = logging.getLogger(__name__)
45
 
46
+ # Configuration from environment variables
47
+ llm_ip = os.environ.get('public_ip')
48
+ llm_port = os.environ.get('port')
49
+ llm_key = os.environ.get('api_key')
50
+ llm_model = os.environ.get('model')
51
 
52
+ # Tavily API configuration
53
+ tavily_key = os.environ.get('tavily_key', '')
54
+ if tavily_key:
55
+ os.environ['TAVILY_API_KEY'] = tavily_key
56
 
57
+ # Tavily search tool integration
 
 
 
 
 
 
58
 
59
+ class ReactAgentChat:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  def __init__(self, ip: str, port: str, api_key: str, model: str):
61
  self.ip = ip
62
  self.port = port
63
  self.api_key = api_key
64
  self.model = model
65
+ self.agent = None
66
+ self._setup_agent()
 
 
 
 
 
 
 
 
 
 
 
 
67
 
68
+ def _setup_agent(self):
69
+ """Initialize the LangGraph ReAct agent"""
70
  try:
71
+ if ENABLE_DETAILED_LOGGING:
72
+ logger.info(f"=== SETTING UP AGENT ===")
73
+ logger.info(f"LLM URL: http://{self.ip}:{self.port}/v1")
74
+ logger.info(f"Model: {self.model}")
75
+
76
+ # Create OpenAI-compatible model
77
+ llm = ChatOpenAI(
78
+ base_url=f"http://{self.ip}:{self.port}/v1",
79
+ api_key=self.api_key,
80
+ model=self.model,
81
+ temperature=0.7
82
+ )
83
+ if ENABLE_DETAILED_LOGGING:
84
+ logger.info("LLM created successfully")
85
+
86
+ # Define tools - use Tavily search API with graceful error handling
87
+ if tavily_key:
88
+ if ENABLE_DETAILED_LOGGING:
89
+ logger.info("Setting up Tavily search tool")
90
+ try:
91
+ # Create custom wrapper for Tavily with error handling
92
+ @tool
93
+ def web_search(query: str) -> str:
94
+ """Search the web for current information about any topic."""
95
+ try:
96
+ tavily_tool = TavilySearch(
97
+ max_results=5,
98
+ topic="general",
99
+ include_answer=True,
100
+ search_depth="advanced"
101
+ )
102
+ result = tavily_tool.invoke({"query": query})
103
+ if ENABLE_DETAILED_LOGGING:
104
+ logger.info(f"Tavily search successful for query: {query}")
105
+ return result
106
+ except Exception as e:
107
+ error_str = str(e).lower()
108
+ if ENABLE_DETAILED_LOGGING:
109
+ logger.error(f"Tavily search failed for query '{query}': {e}")
110
+
111
+ # Check for rate limit or quota issues
112
+ if any(keyword in error_str for keyword in ['rate limit', 'quota', 'limit exceeded', 'usage limit', 'billing']):
113
+ if ENABLE_DETAILED_LOGGING:
114
+ logger.warning(f"Tavily rate limit/quota exceeded: {e}")
115
+ return "I can't search the web right now."
116
+ else:
117
+ if ENABLE_DETAILED_LOGGING:
118
+ logger.error(f"Tavily API error: {e}")
119
+ return "I can't search the web right now."
120
+
121
+ search_tool = web_search
122
+ if ENABLE_DETAILED_LOGGING:
123
+ logger.info("Tavily search tool wrapper created successfully")
124
+ except Exception as e:
125
+ if ENABLE_DETAILED_LOGGING:
126
+ logger.error(f"Failed to create Tavily tool wrapper: {e}")
127
+ # Fallback tool
128
+ @tool
129
+ def no_search(query: str) -> str:
130
+ """Search tool unavailable."""
131
+ return "I can't search the web right now."
132
+ search_tool = no_search
133
  else:
134
+ if ENABLE_DETAILED_LOGGING:
135
+ logger.warning("No Tavily API key found, creating fallback tool")
136
+ @tool
137
+ def no_search(query: str) -> str:
138
+ """Search tool unavailable."""
139
+ if ENABLE_DETAILED_LOGGING:
140
+ logger.error("Search attempted but no Tavily API key configured")
141
+ return "I can't search the web right now."
142
+ search_tool = no_search
143
+
144
+ tools = [search_tool]
145
+ if ENABLE_DETAILED_LOGGING:
146
+ logger.info(f"Tools defined: {[tool.name for tool in tools]}")
147
+
148
+ # Bind tools to the model
149
+ model_with_tools = llm.bind_tools(tools)
150
+ if ENABLE_DETAILED_LOGGING:
151
+ logger.info("Tools bound to model")
152
+
153
+ # Create the ReAct agent
154
+ self.agent = create_react_agent(model_with_tools, tools)
155
+ if ENABLE_DETAILED_LOGGING:
156
+ logger.info("ReAct agent created successfully")
157
+
158
  except Exception as e:
159
+ logger.error(f"=== AGENT SETUP ERROR ===")
160
+ logger.error(f"Failed to setup agent: {e}")
161
+ import traceback
162
+ logger.error(f"Traceback: {traceback.format_exc()}")
163
+ raise e
164
+
165
+ def update_config(self, ip: str, port: str, api_key: str, model: str):
166
+ """Update LLM configuration"""
167
+ if (ip != self.ip or port != self.port or
168
+ api_key != self.api_key or model != self.model):
169
+ self.ip = ip
170
+ self.port = port
171
+ self.api_key = api_key
172
+ self.model = model
173
+ self._setup_agent()
174
+
175
+ def chat(self, message: str, history: List[List[str]]) -> str:
176
+ """Generate chat response using ReAct agent"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
  try:
178
+ if not self.agent:
179
+ return "Error: Agent not initialized"
 
 
 
 
 
 
180
 
181
+ if ENABLE_DETAILED_LOGGING:
182
+ logger.info(f"=== USER INPUT ===")
183
+ logger.info(f"Message: {message}")
184
+ logger.info(f"History length: {len(history)}")
 
185
 
186
+ # Convert history to messages for context handling
187
+ messages = []
188
  for user_msg, assistant_msg in history:
189
+ messages.append(HumanMessage(content=user_msg))
190
+ if assistant_msg: # Only add if assistant responded
191
+ from langchain_core.messages import AIMessage
192
+ messages.append(AIMessage(content=assistant_msg))
193
+
194
+ # Add current message
195
+ messages.append(HumanMessage(content=message))
196
+
197
+ # Invoke the agent
198
+ if ENABLE_DETAILED_LOGGING:
199
+ logger.info(f"=== INVOKING AGENT ===")
200
+ logger.info(f"Total messages in history: {len(messages)}")
201
+ response = self.agent.invoke({"messages": messages})
202
+
203
+ if ENABLE_DETAILED_LOGGING:
204
+ logger.info(f"=== AGENT RESPONSE ===")
205
+ logger.info(f"Full response: {response}")
206
+ logger.info(f"Number of messages: {len(response.get('messages', []))}")
207
+
208
+ # Log each message in the response
209
+ for i, msg in enumerate(response.get("messages", [])):
210
+ logger.info(f"Message {i}: Type={type(msg).__name__}, Content={getattr(msg, 'content', 'No content')}")
211
+
212
+ # Extract the final response
213
+ final_message = response["messages"][-1].content
214
+ if ENABLE_DETAILED_LOGGING:
215
+ logger.info(f"=== FINAL MESSAGE ===")
216
+ logger.info(f"Final message: {final_message}")
217
+
218
+ return final_message
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
 
220
  except Exception as e:
221
  error_msg = f"Agent error: {str(e)}"
222
+ logger.error(f"=== AGENT ERROR ===")
223
+ logger.error(f"Error: {e}")
224
+ logger.error(f"Error type: {type(e)}")
225
+ import traceback
226
+ logger.error(f"Traceback: {traceback.format_exc()}")
227
+ return error_msg
228
 
229
+ # Global agent instance
230
+ react_agent = ReactAgentChat(llm_ip, llm_port, llm_key, llm_model)
 
231
 
232
  def generate_response(message: str, history: List[List[str]], system_prompt: str,
233
  max_tokens: int, ip: str, port: str, api_key: str, model: str):
234
+ """Generate response using ReAct agent"""
235
+ global react_agent
236
+
237
+ try:
238
+ # Update agent configuration if changed
239
+ react_agent.update_config(ip, port, api_key, model)
240
 
241
+ # Generate response
242
+ response = react_agent.chat(message, history)
 
 
 
243
 
244
+ # Stream the response word by word for better UX
245
+ words = response.split()
246
+ current_response = ""
247
+ for word in words:
248
+ current_response += word + " "
249
+ yield current_response.strip()
250
 
251
+ except Exception as e:
252
+ error_msg = f"Error: {str(e)}"
253
+ logger.error(error_msg)
254
+ yield error_msg
255
 
256
+ # Create Gradio ChatInterface
257
  chatbot = gr.ChatInterface(
258
  generate_response,
259
  chatbot=gr.Chatbot(
 
265
  ),
266
  additional_inputs=[
267
  gr.Textbox(
268
+ "You are a helpful AI assistant with web search capabilities.",
269
  label="System Prompt",
270
+ lines=2
271
  ),
272
  gr.Slider(50, 2048, label="Max Tokens", value=512,
273
  info="Maximum number of tokens in the response"),
274
  gr.Textbox(llm_ip, label="LLM IP Address",
275
+ info="IP address of the OpenAI-compatible LLM server"),
276
  gr.Textbox(llm_port, label="LLM Port",
277
  info="Port of the LLM server"),
278
  gr.Textbox(llm_key, label="API Key", type="password",
 
280
  gr.Textbox(llm_model, label="Model Name",
281
  info="Name of the model to use"),
282
  ],
283
+ title="🤖 LangGraph ReAct Agent with DuckDuckGo Search",
284
+ description="Chat with a LangGraph ReAct agent that can search the web using DuckDuckGo. Ask about current events, research topics, or any questions that require up-to-date information!",
285
  theme="finlaymacklon/smooth_slate",
286
  submit_btn="Send",
287
  retry_btn="🔄 Regenerate Response",