feature/web-search-and-cron-improvements #2

Merged
tanyar09 merged 12 commits from feature/web-search-and-cron-improvements into feature/cleanup-providers-llama-only 2026-03-06 13:20:19 -05:00
3 changed files with 83 additions and 47 deletions
Showing only changes of commit edb409bb0c - Show all commits

View File

@ -131,6 +131,34 @@ class CronTool(Tool):
# Build schedule - prioritize 'in_seconds' for relative time, then 'at' for absolute time # Build schedule - prioritize 'in_seconds' for relative time, then 'at' for absolute time
delete_after = False delete_after = False
# Special case: recurring job with duration limit (every_seconds + in_seconds)
if every_seconds is not None and in_seconds is not None:
# Create multiple one-time jobs for "every X seconds for Y seconds"
from datetime import datetime, timedelta
num_jobs = max(1, in_seconds // every_seconds)
results = []
for i in range(num_jobs):
job_time = datetime.now() + timedelta(seconds=i * every_seconds)
job_at = job_time.isoformat()
try:
dt = datetime.fromisoformat(job_at)
at_ms = int(dt.timestamp() * 1000)
schedule = CronSchedule(kind="at", at_ms=at_ms)
job = self._cron.add_job(
name=f"{message[:25]} ({i+1}/{num_jobs})" if num_jobs > 1 else message[:30],
schedule=schedule,
message=message,
deliver=True,
channel=channel,
to=chat_id,
delete_after_run=True,
reminder=reminder,
)
results.append(f"Created job '{job.name}' (id: {job.id})")
except Exception as e:
results.append(f"Error creating job {i+1}: {str(e)}")
return f"Created {len([r for r in results if 'Created' in r])} reminder(s):\n" + "\n".join(results)
# Handle relative time (in_seconds) - compute datetime automatically # Handle relative time (in_seconds) - compute datetime automatically
if in_seconds is not None: if in_seconds is not None:
from datetime import datetime, timedelta from datetime import datetime, timedelta

View File

@ -101,9 +101,9 @@ class WebSearchTool(Tool):
try: try:
n = min(max(count or self.max_results, 1), 10) n = min(max(count or self.max_results, 1), 10)
# Try using duckduckgo_search library if available # Try using ddgs library if available (renamed from duckduckgo_search)
try: try:
from duckduckgo_search import DDGS from ddgs import DDGS
with DDGS() as ddgs: with DDGS() as ddgs:
results = [] results = []
for r in ddgs.text(query, max_results=n): for r in ddgs.text(query, max_results=n):
@ -123,51 +123,58 @@ class WebSearchTool(Tool):
lines.append(f" {item['description']}") lines.append(f" {item['description']}")
return "\n".join(lines) return "\n".join(lines)
except ImportError: except ImportError:
# Fallback: use DuckDuckGo instant answer API (simpler, but limited) # ddgs package not installed, fall through to fallback
async with httpx.AsyncClient( pass
follow_redirects=True, except Exception as e:
timeout=15.0 # Log ddgs errors but fall through to fallback API
) as client: import logging
# Use DuckDuckGo instant answer API (no key needed) logging.debug(f"ddgs search error: {e}")
url = "https://api.duckduckgo.com/"
r = await client.get(
url,
params={"q": query, "format": "json", "no_html": "1", "skip_disambig": "1"},
headers={"User-Agent": USER_AGENT},
)
r.raise_for_status()
data = r.json()
results = [] # Fallback: use DuckDuckGo instant answer API (simpler, but limited)
# Get RelatedTopics (search results) async with httpx.AsyncClient(
if "RelatedTopics" in data: follow_redirects=True,
for topic in data["RelatedTopics"][:n]: timeout=15.0
if "Text" in topic and "FirstURL" in topic: ) as client:
results.append({ # Use DuckDuckGo instant answer API (no key needed)
"title": topic.get("Text", "").split(" - ")[0] if " - " in topic.get("Text", "") else topic.get("Text", "")[:50], url = "https://api.duckduckgo.com/"
"url": topic.get("FirstURL", ""), r = await client.get(
"description": topic.get("Text", "") url,
}) params={"q": query, "format": "json", "no_html": "1", "skip_disambig": "1"},
headers={"User-Agent": USER_AGENT},
)
r.raise_for_status()
data = r.json()
# Also check AbstractText for direct answer results = []
if "AbstractText" in data and data["AbstractText"]: # Get RelatedTopics (search results)
results.insert(0, { if "RelatedTopics" in data:
"title": data.get("Heading", query), for topic in data["RelatedTopics"][:n]:
"url": data.get("AbstractURL", ""), if "Text" in topic and "FirstURL" in topic:
"description": data.get("AbstractText", "") results.append({
}) "title": topic.get("Text", "").split(" - ")[0] if " - " in topic.get("Text", "") else topic.get("Text", "")[:50],
"url": topic.get("FirstURL", ""),
"description": topic.get("Text", "")
})
if not results: # Also check AbstractText for direct answer
return f"No results found for: {query}. Try installing 'duckduckgo-search' package for better results: pip install duckduckgo-search" if "AbstractText" in data and data["AbstractText"]:
results.insert(0, {
"title": data.get("Heading", query),
"url": data.get("AbstractURL", ""),
"description": data.get("AbstractText", "")
})
lines = [f"Results for: {query}\n"] if not results:
for i, item in enumerate(results[:n], 1): return f"No results found for: {query}. Try installing 'ddgs' package for better results: pip install ddgs"
lines.append(f"{i}. {item['title']}\n {item['url']}")
if item['description']: lines = [f"Results for: {query}\n"]
lines.append(f" {item['description']}") for i, item in enumerate(results[:n], 1):
return "\n".join(lines) lines.append(f"{i}. {item['title']}\n {item['url']}")
if item['description']:
lines.append(f" {item['description']}")
return "\n".join(lines)
except Exception as e: except Exception as e:
return f"Error searching: {e}. Try installing 'duckduckgo-search' package: pip install duckduckgo-search" return f"Error searching: {e}. Try installing 'ddgs' package: pip install ddgs"
class WebFetchTool(Tool): class WebFetchTool(Tool):

View File

@ -24,6 +24,7 @@ dependencies = [
"websockets>=12.0", "websockets>=12.0",
"websocket-client>=1.6.0", "websocket-client>=1.6.0",
"httpx>=0.25.0", "httpx>=0.25.0",
"ddgs>=9.0.0",
"oauth-cli-kit>=0.1.1", "oauth-cli-kit>=0.1.1",
"loguru>=0.7.0", "loguru>=0.7.0",
"readability-lxml>=0.8.0", "readability-lxml>=0.8.0",