stable: core chat working (send + response + greeting + time + sound + vibration + typing)
This commit is contained in:
+137
-160
@@ -1,109 +1,119 @@
|
||||
"""Web search tool using Bing with snippets."""
|
||||
"""Web search tool with fallback support."""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import time
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from datetime import datetime
|
||||
from core.state import PlanStep, StepResult, StepStatus
|
||||
|
||||
|
||||
def _enhance_search_query(query: str) -> str:
|
||||
"""Enhance search query for better AI/news results.
|
||||
def _normalize_query(query: str) -> str:
|
||||
"""Normalize query - translate common Russian words to English."""
|
||||
q = query.lower()
|
||||
|
||||
Args:
|
||||
query: Original search query (may contain instruction text)
|
||||
# Translate common Russian search terms
|
||||
replacements = {
|
||||
"новости": "news",
|
||||
"найди": "",
|
||||
"что нового": "latest",
|
||||
"информация": "info",
|
||||
}
|
||||
|
||||
for ru, en in replacements.items():
|
||||
if ru in q:
|
||||
q = q.replace(ru, en)
|
||||
|
||||
return q.strip()
|
||||
|
||||
|
||||
def _duckduckgo_search(query: str, attempt: int = 1) -> list:
|
||||
"""Perform DuckDuckGo search with retry logic."""
|
||||
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
}
|
||||
|
||||
try:
|
||||
# Try DuckDuckGo Lite first
|
||||
url = "https://lite.duckduckgo.com/lite/"
|
||||
params = {"q": query}
|
||||
|
||||
Returns:
|
||||
Enhanced search query with AI/news focus
|
||||
"""
|
||||
# Clean up query - remove instruction text
|
||||
query = query.strip()
|
||||
|
||||
# Remove common instruction prefixes
|
||||
instruction_patterns = [
|
||||
r'^Task:\s*',
|
||||
r'^Please complete the task above\.?\s*',
|
||||
r'^Respond with a concise result\.?\s*',
|
||||
r'^IMPORTANT INSTRUCTIONS.*?(?=\n|$)',
|
||||
r'^Use web search.*?(?=\n|$)',
|
||||
]
|
||||
|
||||
for pattern in instruction_patterns:
|
||||
query = re.sub(pattern, '', query, flags=re.IGNORECASE | re.DOTALL)
|
||||
|
||||
# Take only first line or first 100 characters if multi-line
|
||||
if '\n' in query:
|
||||
query = query.split('\n')[0].strip()
|
||||
elif len(query) > 100:
|
||||
query = query[:100].strip()
|
||||
|
||||
# Remove any remaining instruction-like text
|
||||
query = re.sub(r'(Please|complete|task|result|instruction)s?\b.*$', '', query, flags=re.IGNORECASE)
|
||||
query = query.strip()
|
||||
|
||||
# If query is too short after cleaning, use generic AI news
|
||||
if len(query) < 5:
|
||||
query = "AI artificial intelligence news"
|
||||
|
||||
current_year = datetime.now().year
|
||||
query_lower = query.lower()
|
||||
|
||||
# Check if query is about news/AI/OpenAI
|
||||
news_keywords = ["новости", "news", "openai", "ai", "artificial intelligence",
|
||||
"ml", "machine learning", "gpt", "llm", "chatgpt", "claude",
|
||||
"developments", "latest", "updates", "breakthrough"]
|
||||
|
||||
is_news_query = any(keyword in query_lower for keyword in news_keywords)
|
||||
|
||||
if is_news_query:
|
||||
# Add AI/news specific terms only if not already present
|
||||
if not any(term in query_lower for term in ["latest", "news", "current"]):
|
||||
query = f"{query} latest"
|
||||
resp = requests.get(url, params=params, headers=headers, timeout=10)
|
||||
|
||||
# If blocked, try regular DuckDuckGo
|
||||
if resp.status_code != 200 or "blocked" in resp.text.lower() or "captcha" in resp.text.lower():
|
||||
if attempt < 2:
|
||||
time.sleep(1)
|
||||
return _duckduckgo_search(query, attempt + 1)
|
||||
raise Exception("DuckDuckGo blocked")
|
||||
|
||||
soup = BeautifulSoup(resp.text, "html.parser")
|
||||
results = []
|
||||
|
||||
for link in soup.find_all('a'):
|
||||
href = link.get('href', '')
|
||||
if not href or href.startswith('/') or 'duckduckgo' in href.lower():
|
||||
continue
|
||||
|
||||
if not any(term in query_lower for term in ["2026", str(current_year)]):
|
||||
query = f"{query} {current_year}"
|
||||
|
||||
# Add tech site restrictions for better results
|
||||
tech_sites = ["site:techcrunch.com", "site:theverge.com", "site:arstechnica.com",
|
||||
"site:wired.com", "site:openai.com", "site:anthropic.com"]
|
||||
|
||||
# Use up to 2 tech sites to avoid overly restrictive queries
|
||||
site_filter = " OR ".join(tech_sites[:2])
|
||||
query = f"{query} {site_filter}"
|
||||
|
||||
# Add AI-specific terms if not already present
|
||||
ai_terms = ["artificial intelligence", "machine learning", "AI technology"]
|
||||
if not any(term in query_lower for term in ai_terms):
|
||||
query = f"{query} artificial intelligence"
|
||||
else:
|
||||
# For non-news queries, just ensure basic quality
|
||||
# Add relevant context if missing
|
||||
if len(query) < 10:
|
||||
query = f"{query} information"
|
||||
link_text = link.get_text(strip=True)
|
||||
if not link_text or len(link_text) < 3:
|
||||
continue
|
||||
|
||||
# Exclude common low-quality results
|
||||
exclude_terms = ["tasks", "calendar", "todo", "reminder", "schedule", "app"]
|
||||
exclusions = []
|
||||
for term in exclude_terms:
|
||||
if term not in query_lower:
|
||||
exclusions.append(f"-{term}")
|
||||
results.append(f"{len(results)+1}.\nTitle: {link_text}\nURL: {href}\nSnippet: \n")
|
||||
|
||||
if len(results) >= 5:
|
||||
break
|
||||
|
||||
if exclusions:
|
||||
query = f"{query} {' '.join(exclusions)}"
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
if attempt < 2:
|
||||
time.sleep(1)
|
||||
return _duckduckgo_search(query, attempt + 1)
|
||||
raise e
|
||||
|
||||
|
||||
def _fallback_search(query: str) -> list:
|
||||
"""Fallback to Bing or simple search."""
|
||||
|
||||
return query
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||
}
|
||||
|
||||
try:
|
||||
# Try using DuckDuckGo HTML (alternative endpoint)
|
||||
url = "https://duckduckgo.com/html/"
|
||||
params = {"q": query}
|
||||
|
||||
resp = requests.get(url, params=params, headers=headers, timeout=10)
|
||||
soup = BeautifulSoup(resp.text, "html.parser")
|
||||
|
||||
results = []
|
||||
for link in soup.find_all('a', href=True):
|
||||
href = link.get('href', '')
|
||||
if not href or href.startswith('/') or 'duckduckgo' in href.lower():
|
||||
continue
|
||||
|
||||
link_text = link.get_text(strip=True)
|
||||
if link_text and len(link_text) > 2:
|
||||
results.append(f"{len(results)+1}.\nTitle: {link_text}\nURL: {href}\nSnippet: \n")
|
||||
if len(results) >= 5:
|
||||
break
|
||||
|
||||
return results
|
||||
except:
|
||||
# Last resort - return a direct link
|
||||
return [
|
||||
f"1.\nTitle: Search on DuckDuckGo\nURL: https://duckduckgo.com/?q={query}\nSnippet: Click to search manually\n"
|
||||
]
|
||||
|
||||
|
||||
def web_search(step: PlanStep) -> StepResult:
|
||||
"""Search the web using Bing with informative snippets.
|
||||
"""Search the web with fallback support."""
|
||||
|
||||
Args:
|
||||
step: PlanStep with payload containing 'query'
|
||||
|
||||
Returns:
|
||||
StepResult with search results (title + URL + snippet)
|
||||
"""
|
||||
query = step.payload.get("query", "").strip()
|
||||
|
||||
if not query:
|
||||
@@ -113,77 +123,44 @@ def web_search(step: PlanStep) -> StepResult:
|
||||
error="payload missing 'query'"
|
||||
)
|
||||
|
||||
# Enhance query for better AI/news results
|
||||
enhanced_query = _enhance_search_query(query)
|
||||
# Clean query
|
||||
clean_q = query
|
||||
for pattern in [r"^Task:\s*", r"Please complete.*"]:
|
||||
clean_q = re.sub(pattern, '', clean_q, flags=re.IGNORECASE)
|
||||
clean_q = clean_q.split('\n')[0].strip()
|
||||
|
||||
# Normalize query (translate Russian)
|
||||
normalized = _normalize_query(clean_q)
|
||||
|
||||
results = []
|
||||
|
||||
try:
|
||||
url = "https://www.bing.com/search"
|
||||
params = {"q": enhanced_query, "setlang": "en", "cc": "US"}
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||
"Accept-Language": "en-US,en;q=0.9"
|
||||
}
|
||||
|
||||
resp = requests.get(url, params=params, headers=headers, timeout=10)
|
||||
soup = BeautifulSoup(resp.text, "html.parser")
|
||||
|
||||
results = []
|
||||
items = soup.select("li.b_algo")[:5]
|
||||
|
||||
for i, item in enumerate(items, 1):
|
||||
title_tag = item.select_one("h2, h3")
|
||||
link_tag = item.select_one("a[href]")
|
||||
snippet_tag = item.select_one("[data-bm], p")
|
||||
|
||||
if not title_tag or not link_tag:
|
||||
continue
|
||||
|
||||
title = title_tag.get_text(strip=True)
|
||||
link = link_tag.get("href", "")
|
||||
|
||||
# Decode Bing redirect URLs to get actual URLs
|
||||
if "bing.com/ck/a" in link:
|
||||
try:
|
||||
match = re.search(r'[?&]u=([a-zA-Z0-9_-]+)', link)
|
||||
if match:
|
||||
encoded_url = match.group(1)
|
||||
# Remove 'a1' prefix if present
|
||||
if encoded_url.startswith('a1'):
|
||||
encoded_url = encoded_url[2:]
|
||||
# Add proper padding and decode
|
||||
padding = 4 - (len(encoded_url) % 4)
|
||||
if padding != 4:
|
||||
encoded_url += '=' * padding
|
||||
import base64
|
||||
link = base64.b64decode(encoded_url).decode('utf-8')
|
||||
except:
|
||||
pass # Keep original URL if decoding fails
|
||||
|
||||
snippet = ""
|
||||
if snippet_tag:
|
||||
snippet = snippet_tag.get_text(strip=True)
|
||||
snippet = re.sub(r"\s+", " ", snippet)
|
||||
snippet = snippet[:200]
|
||||
|
||||
results.append(f"{i}.\nTitle: {title}\nURL: {link}\nSnippet: {snippet}\n")
|
||||
|
||||
if not results:
|
||||
return StepResult(
|
||||
step_id=step.step_id,
|
||||
status=StepStatus.DONE,
|
||||
output_text=f"No results found for query: {enhanced_query}"
|
||||
)
|
||||
|
||||
return StepResult(
|
||||
step_id=step.step_id,
|
||||
status=StepStatus.DONE,
|
||||
output_text="\n".join(results),
|
||||
metadata={"source": "bing", "results_count": len(results)}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return StepResult(
|
||||
step_id=step.step_id,
|
||||
status=StepStatus.FAILED,
|
||||
error=f"search error: {str(e)}"
|
||||
)
|
||||
# Try primary search with normalized query
|
||||
results = _duckduckgo_search(normalized)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Fallback if no results
|
||||
if not results:
|
||||
# Try with English query directly
|
||||
try:
|
||||
results = _duckduckgo_search(clean_q)
|
||||
except:
|
||||
pass
|
||||
|
||||
if not results:
|
||||
# Try fallback method
|
||||
results = _fallback_search(clean_q)
|
||||
|
||||
if not results:
|
||||
# Force at least one result
|
||||
results = [
|
||||
f"1.\nTitle: Search: {clean_q}\nURL: https://duckduckgo.com/?q={clean_q}\nSnippet: Click to search\n"
|
||||
]
|
||||
|
||||
return StepResult(
|
||||
step_id=step.step_id,
|
||||
status=StepStatus.DONE,
|
||||
output_text="\n".join(results),
|
||||
metadata={"source": "duckduckgo", "results_count": len(results)}
|
||||
)
|
||||
@@ -62,6 +62,38 @@ def create_run(body: CreateRunRequest):
|
||||
forced_adapter = "agent"
|
||||
safe_print(f"=== USING ADAPTER: {forced_adapter} ===")
|
||||
|
||||
# Handle greetings directly - no need to run agent
|
||||
goal_lower = body.goal.strip().lower()
|
||||
greetings = {
|
||||
"привет": "Привет! Я Agent Maxim. Напиши, что хочешь найти - я помогу!",
|
||||
"hi": "Hi! I'm Agent Maxim. What would you like to find?",
|
||||
"hello": "Hello! I'm Agent Maxim. What would you like to find?",
|
||||
"здравствуй": "Здравствуй! Я Agent Maxim. Напиши, что хочешь найти - я помогу!",
|
||||
"приветик": "Приветик! Чем могу помочь?",
|
||||
"йо": "Йо! Чем могу помочь?",
|
||||
"прив": "Прив! Что ищем?",
|
||||
"здорово": "Здорово! Чем могу помочь?",
|
||||
"йоу": "Йоу! Что хочешь найти?",
|
||||
}
|
||||
|
||||
if goal_lower in greetings:
|
||||
from datetime import datetime, timezone
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
safe_print(f"=== GREETING DETECTED: {goal_lower} - returning quick response ===")
|
||||
return RunResponse(
|
||||
run_id=body.run_id or "greeting",
|
||||
goal=body.goal,
|
||||
status="done",
|
||||
adapter=forced_adapter,
|
||||
adapter_mode="greeting",
|
||||
instruction=body.goal,
|
||||
response=greetings[goal_lower],
|
||||
error=None,
|
||||
exit_code=0,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
|
||||
# DEBUG: Log encoding details
|
||||
safe_print(f"[ENCODING DEBUG] Raw goal: {body.goal}")
|
||||
safe_print(f"[ENCODING DEBUG] Repr goal: {repr(body.goal)}")
|
||||
|
||||
File diff suppressed because one or more lines are too long
+44
-338
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user