feat: enhance logging and error handling across various tools; update DuckDuckGo dependency (#2282)

Co-authored-by: Alex <a@tushynski.me>
This commit is contained in:
Siddhant RaiandAlex authored and GitHub committed 2026-03-12 16:50:29 +00:00
1 parent 918bbf0369
commit 13ad3b5dce
10 files changed
+223 -73

No files matched your search

+7 -2
View File
@@ -1,6 +1,11 @@
import logging
import requests
from application.agents.tools.base import Tool
logger = logging.getLogger(__name__)
class BraveSearchTool(Tool):
"""
@@ -41,7 +46,7 @@ class BraveSearchTool(Tool):
"""
Performs a web search using the Brave Search API.
"""
print(f"Performing Brave web search for: {query}")
logger.info(f"Brave web search: {query}")
url = f"{self.base_url}/web/search"
@@ -94,7 +99,7 @@ class BraveSearchTool(Tool):
"""
Performs an image search using the Brave Search API.
"""
print(f"Performing Brave image search for: {query}")
logger.info(f"Brave image search: {query}")
url = f"{self.base_url}/images/search"
+140 -45
View File
@@ -1,5 +1,14 @@
import logging
import time
from typing import Any, Dict, Optional
from application.agents.tools.base import Tool
from duckduckgo_search import DDGS
logger = logging.getLogger(__name__)
MAX_RETRIES = 3
RETRY_DELAY = 2.0
DEFAULT_TIMEOUT = 15
class DuckDuckGoSearchTool(Tool):
@@ -10,71 +19,123 @@ class DuckDuckGoSearchTool(Tool):
def __init__(self, config):
self.config = config
self.timeout = config.get("timeout", DEFAULT_TIMEOUT)
def _get_ddgs_client(self):
from ddgs import DDGS
return DDGS(timeout=self.timeout)
def _execute_with_retry(self, operation, operation_name: str) -> Dict[str, Any]:
last_error = None
for attempt in range(1, MAX_RETRIES + 1):
try:
results = operation()
return {
"status_code": 200,
"results": list(results) if results else [],
"message": f"{operation_name} completed successfully.",
}
except Exception as e:
last_error = e
error_str = str(e).lower()
if "ratelimit" in error_str or "429" in error_str:
if attempt < MAX_RETRIES:
delay = RETRY_DELAY * attempt
logger.warning(
f"{operation_name} rate limited, retrying in {delay}s (attempt {attempt}/{MAX_RETRIES})"
)
time.sleep(delay)
continue
logger.error(f"{operation_name} failed: {e}")
break
return {
"status_code": 500,
"results": [],
"message": f"{operation_name} failed: {str(last_error)}",
}
def execute_action(self, action_name, **kwargs):
actions = {
"ddg_web_search": self._web_search,
"ddg_image_search": self._image_search,
"ddg_news_search": self._news_search,
}
if action_name in actions:
return actions[action_name](**kwargs)
else:
if action_name not in actions:
raise ValueError(f"Unknown action: {action_name}")
return actions[action_name](**kwargs)
def _web_search(
self,
query,
max_results=5,
):
print(f"Performing DuckDuckGo web search for: {query}")
query: str,
max_results: int = 5,
region: str = "wt-wt",
safesearch: str = "moderate",
timelimit: Optional[str] = None,
) -> Dict[str, Any]:
logger.info(f"DuckDuckGo web search: {query}")
try:
results = DDGS().text(
def operation():
client = self._get_ddgs_client()
return client.text(
query,
max_results=max_results,
region=region,
safesearch=safesearch,
timelimit=timelimit,
max_results=min(max_results, 20),
)
return {
"status_code": 200,
"results": results,
"message": "Web search completed successfully.",
}
except Exception as e:
return {
"status_code": 500,
"message": f"Web search failed: {str(e)}",
}
return self._execute_with_retry(operation, "Web search")
def _image_search(
self,
query,
max_results=5,
):
print(f"Performing DuckDuckGo image search for: {query}")
query: str,
max_results: int = 5,
region: str = "wt-wt",
safesearch: str = "moderate",
timelimit: Optional[str] = None,
) -> Dict[str, Any]:
logger.info(f"DuckDuckGo image search: {query}")
try:
results = DDGS().images(
keywords=query,
max_results=max_results,
def operation():
client = self._get_ddgs_client()
return client.images(
query,
region=region,
safesearch=safesearch,
timelimit=timelimit,
max_results=min(max_results, 50),
)
return {
"status_code": 200,
"results": results,
"message": "Image search completed successfully.",
}
except Exception as e:
return {
"status_code": 500,
"message": f"Image search failed: {str(e)}",
}
return self._execute_with_retry(operation, "Image search")
def _news_search(
self,
query: str,
max_results: int = 5,
region: str = "wt-wt",
safesearch: str = "moderate",
timelimit: Optional[str] = None,
) -> Dict[str, Any]:
logger.info(f"DuckDuckGo news search: {query}")
def operation():
client = self._get_ddgs_client()
return client.news(
query,
region=region,
safesearch=safesearch,
timelimit=timelimit,
max_results=min(max_results, 20),
)
return self._execute_with_retry(operation, "News search")
def get_actions_metadata(self):
return [
{
"name": "ddg_web_search",
"description": "Perform a web search using DuckDuckGo.",
"description": "Search the web using DuckDuckGo. Returns titles, URLs, and snippets.",
"parameters": {
"type": "object",
"properties": {
@@ -84,7 +145,15 @@ class DuckDuckGoSearchTool(Tool):
},
"max_results": {
"type": "integer",
"description": "Number of results to return (default: 5)",
"description": "Number of results (default: 5, max: 20)",
},
"region": {
"type": "string",
"description": "Region code (default: wt-wt for worldwide, us-en for US)",
},
"timelimit": {
"type": "string",
"description": "Time filter: d (day), w (week), m (month), y (year)",
},
},
"required": ["query"],
@@ -92,17 +161,43 @@ class DuckDuckGoSearchTool(Tool):
},
{
"name": "ddg_image_search",
"description": "Perform an image search using DuckDuckGo.",
"description": "Search for images using DuckDuckGo. Returns image URLs and metadata.",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query",
"description": "Image search query",
},
"max_results": {
"type": "integer",
"description": "Number of results to return (default: 5, max: 50)",
"description": "Number of results (default: 5, max: 50)",
},
"region": {
"type": "string",
"description": "Region code (default: wt-wt for worldwide)",
},
},
"required": ["query"],
},
},
{
"name": "ddg_news_search",
"description": "Search for news articles using DuckDuckGo. Returns recent news.",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "News search query",
},
"max_results": {
"type": "integer",
"description": "Number of results (default: 5, max: 20)",
},
"timelimit": {
"type": "string",
"description": "Time filter: d (day), w (week), m (month)",
},
},
"required": ["query"],
+3 -1
View File
@@ -28,6 +28,8 @@ from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAu
from pydantic import AnyHttpUrl, ValidationError
from redis import Redis
logger = logging.getLogger(__name__)
mongo = MongoDB.get_client()
db = mongo[settings.MONGO_DB_NAME]
@@ -263,7 +265,7 @@ class MCPTool(Tool):
finally:
loop.close()
except Exception as e:
print(f"Error occurred while running async operation: {e}")
logger.error(f"Error occurred while running async operation: {e}")
raise
def discover_tools(self) -> List[Dict]:
+13 -12
View File
@@ -1,6 +1,12 @@
import logging
import psycopg2
from application.agents.tools.base import Tool
logger = logging.getLogger(__name__)
class PostgresTool(Tool):
"""
PostgreSQL Database Tool
@@ -17,17 +23,12 @@ class PostgresTool(Tool):
"postgres_execute_sql": self._execute_sql,
"postgres_get_schema": self._get_schema,
}
if action_name in actions:
return actions[action_name](**kwargs)
else:
if action_name not in actions:
raise ValueError(f"Unknown action: {action_name}")
return actions[action_name](**kwargs)
def _execute_sql(self, sql_query):
"""
Executes an SQL query against the PostgreSQL database using a connection string.
"""
conn = None # Initialize conn to None for error handling
conn = None
try:
conn = psycopg2.connect(self.connection_string)
cur = conn.cursor()
@@ -54,14 +55,14 @@ class PostgresTool(Tool):
except psycopg2.Error as e:
error_message = f"Database error: {e}"
print(f"Database error: {e}")
logger.error(error_message)
return {
"status_code": 500,
"message": "Failed to execute SQL query.",
"error": error_message,
}
finally:
if conn: # Ensure connection is closed even if errors occur
if conn:
conn.close()
def _get_schema(self, db_name):
@@ -110,14 +111,14 @@ class PostgresTool(Tool):
except psycopg2.Error as e:
error_message = f"Database error: {e}"
print(f"Database error: {e}")
logger.error(error_message)
return {
"status_code": 500,
"message": "Failed to retrieve database schema.",
"error": error_message,
}
finally:
if conn: # Ensure connection is closed even if errors occur
if conn:
conn.close()
def get_actions_metadata(self):
+9 -6
View File
@@ -1,6 +1,11 @@
import logging
import requests
from application.agents.tools.base import Tool
logger = logging.getLogger(__name__)
class TelegramTool(Tool):
"""
@@ -18,21 +23,19 @@ class TelegramTool(Tool):
"telegram_send_message": self._send_message,
"telegram_send_image": self._send_image,
}
if action_name in actions:
return actions[action_name](**kwargs)
else:
if action_name not in actions:
raise ValueError(f"Unknown action: {action_name}")
return actions[action_name](**kwargs)
def _send_message(self, text, chat_id):
print(f"Sending message: {text}")
logger.info(f"Telegram: sending message to {chat_id}")
url = f"https://api.telegram.org/bot{self.token}/sendMessage"
payload = {"chat_id": chat_id, "text": text}
response = requests.post(url, data=payload)
return {"status_code": response.status_code, "message": "Message sent"}
def _send_image(self, image_url, chat_id):
print(f"Sending image: {image_url}")
logger.info(f"Telegram: sending image to {chat_id}")
url = f"https://api.telegram.org/bot{self.token}/sendPhoto"
payload = {"chat_id": chat_id, "photo": image_url}
response = requests.post(url, data=payload)
@@ -18,6 +18,7 @@ from application.core.json_schema_utils import (
JsonSchemaValidationError,
normalize_json_schema_payload,
)
from application.error import sanitize_api_error
from application.templates.namespaces import NamespaceManager
from application.templates.template_engine import TemplateEngine, TemplateRenderError
@@ -99,6 +100,7 @@ class WorkflowEngine:
log_entry["state_snapshot"] = dict(self.state)
self.execution_log.append(log_entry)
user_friendly_error = sanitize_api_error(e)
yield {
"type": "workflow_step",
"node_id": node.id,
@@ -106,9 +108,9 @@ class WorkflowEngine:
"node_title": node.title,
"status": "failed",
"state_snapshot": dict(self.state),
"error": str(e),
"error": user_friendly_error,
}
yield {"type": "error", "error": str(e)}
yield {"type": "error", "error": user_friendly_error}
break
log_entry["state_snapshot"] = dict(self.state)
self.execution_log.append(log_entry)
+9 -1
View File
@@ -15,6 +15,7 @@ from application.core.model_utils import (
from application.core.mongo_db import MongoDB
from application.core.settings import settings
from application.error import sanitize_api_error
from application.llm.llm_creator import LLMCreator
from application.utils import check_required_fields
@@ -240,7 +241,14 @@ class BaseAnswerResource:
data = json.dumps({"type": "thought", "thought": line["thought"]})
yield f"data: {data}\n\n"
elif "type" in line:
data = json.dumps(line)
if line.get("type") == "error":
sanitized_error = {
"type": "error",
"error": sanitize_api_error(line.get("error", "An error occurred"))
}
data = json.dumps(sanitized_error)
else:
data = json.dumps(line)
yield f"data: {data}\n\n"
if is_structured and structured_chunks:
structured_data = {
+22
View File
@@ -13,3 +13,25 @@ def response_error(code_status, message=None):
def bad_request(status_code=400, message=''):
return response_error(code_status=status_code, message=message)
def sanitize_api_error(error) -> str:
"""
Convert technical API errors to user-friendly messages.
Works with both Exception objects and error message strings.
"""
error_str = str(error).lower()
if "503" in error_str or "unavailable" in error_str or "high demand" in error_str:
return "The AI service is temporarily unavailable due to high demand. Please try again in a moment."
if "429" in error_str or "rate limit" in error_str or "quota" in error_str:
return "Rate limit exceeded. Please wait a moment before trying again."
if "401" in error_str or "unauthorized" in error_str or "invalid api key" in error_str:
return "Authentication error. Please check your API configuration."
if "timeout" in error_str or "timed out" in error_str:
return "The request timed out. Please try again."
if "connection" in error_str or "network" in error_str:
return "Network error. Please check your connection and try again."
original = str(error)
if len(original) > 200 or "{" in original or "traceback" in error_str:
return "An error occurred while processing your request. Please try again later."
return original
+15 -3
View File
@@ -158,7 +158,11 @@ class GoogleLLM(BaseLLM):
if isinstance(content, list):
parts = []
for item in content:
if isinstance(item, dict) and "text" in item and item["text"] is not None:
if (
isinstance(item, dict)
and "text" in item
and item["text"] is not None
):
parts.append(item["text"])
return "\n".join(parts)
return ""
@@ -236,7 +240,9 @@ class GoogleLLM(BaseLLM):
raise ValueError(f"Unexpected content type: {type(content)}")
if parts:
cleaned_messages.append(types.Content(role=role, parts=parts))
system_instruction = "\n\n".join(system_instructions) if system_instructions else None
system_instruction = (
"\n\n".join(system_instructions) if system_instructions else None
)
return cleaned_messages, system_instruction
def _clean_schema(self, schema_obj):
@@ -336,7 +342,10 @@ class GoogleLLM(BaseLLM):
return f"function_call:{name}"
function_response = getattr(part, "function_response", None)
if function_response:
name = getattr(function_response, "name", "") or "function_response"
name = (
getattr(function_response, "name", "")
or "function_response"
)
return f"function_response:{name}"
if isinstance(message, dict):
content = message.get("content")
@@ -507,6 +516,9 @@ class GoogleLLM(BaseLLM):
yield {"type": "thought", "thought": chunk_text}
else:
yield chunk_text
except Exception as e:
logging.error(f"GoogleLLM: Stream error: {e}", exc_info=True)
raise
finally:
if hasattr(response, "close"):
response.close()
+1 -1
View File
@@ -10,7 +10,7 @@ docling>=2.16.0
rapidocr>=1.4.0
onnxruntime>=1.19.0
docx2txt==0.9
duckduckgo-search==8.1.1
ddgs>=8.0.0
ebooklib==0.20
escodegen==1.0.11
esprima==4.0.1