Feature: AI-powered intelligent summaries with sub-agents (analyze + tag)

This commit is contained in:
Remora
2026-02-09 18:56:26 +01:00
parent bec7f13d82
commit e5d562ef70

139
bot.py
View File

@@ -14,6 +14,8 @@ from pathlib import Path
from dotenv import load_dotenv from dotenv import load_dotenv
import logging import logging
from urllib.parse import urlparse from urllib.parse import urlparse
import subprocess
import sys
# Load .env file # Load .env file
load_dotenv() load_dotenv()
@@ -157,54 +159,83 @@ def fetch_url_content(url):
logger.error(f" ❌ Error: {e}") logger.error(f" ❌ Error: {e}")
return {"title": "Fetch failed", "status": "error", "error": str(e), "content": ""} return {"title": "Fetch failed", "status": "error", "error": str(e), "content": ""}
# Analyze with OpenClaw gateway (Haiku) # Analyze content with AI (Haiku via gateway)
def analyze_with_gateway(url, title, content): def analyze_content(url, title, content, link_type):
"""Send to OpenClaw gateway for AI analysis""" """Analyze content and create intelligent summary"""
logger.debug(f" 🤖 Analyzing with gateway: {url}") logger.debug(f" 🤖 Analyzing content: {url}")
# Build analysis prompt # Build analysis prompt
prompt = f"""Analyze this webpage briefly (2-3 sentences max): analysis_prompt = f"""Analyze this link and create a brief summary useful for Laurent.
URL: {url} **Link**: {link_type} - {title}
Title: {title} **URL**: {url}
Content excerpt: **Content (first 1500 chars)**:
{content[:1000]} {content[:1500]}
Provide: ---
1. What is this about? (1 sentence)
2. Who should read this? (optional)
3. Suggested Tududi tag (e.g., "to-read", "learning", "inspiration", "tool")
Keep it concise!""" Respond in JSON format ONLY (no markdown, no explanation):
{{
"summary": "1-2 sentences max: What is it? Why would Laurent find it useful?",
"tag": "one of: to-read, tool, inspiration, learning, reference, interesting, project, tutorial, article, code, security",
"relevance": "very-relevant OR relevant OR nice-to-have"
}}
Be concise and practical."""
try: try:
response = requests.post( # Use OpenClaw CLI to invoke sessions_spawn
f"{GATEWAY_URL}/v1/messages", # This spawns a sub-agent that analyzes the content
headers={"Authorization": f"Bearer {GATEWAY_TOKEN}"} if GATEWAY_TOKEN else {}, result = subprocess.run(
json={ [
"messages": [{"role": "user", "content": prompt}], sys.executable, "-m", "openclaw",
"model": "openrouter/anthropic/claude-haiku-4.5", "sessions", "spawn",
"max_tokens": 150 "--task", analysis_prompt,
}, "--model", "openrouter/anthropic/claude-haiku-4.5",
timeout=10 "--thinking", "off",
"--timeout", "15"
],
capture_output=True,
text=True,
timeout=20
) )
if response.status_code == 200: if result.returncode == 0:
result = response.json() output = result.stdout
analysis = result.get("content", [{}])[0].get("text", "") logger.debug(f" Sub-agent response: {output[:200]}")
logger.debug(f" ✓ Analysis complete")
return analysis # Try to parse JSON
try:
json_match = re.search(r'\{[^{}]*"summary"[^{}]*\}', output, re.DOTALL)
if json_match:
analysis_data = json.loads(json_match.group())
logger.debug(f" ✓ Analysis parsed successfully")
return analysis_data
except json.JSONDecodeError:
pass
# Fallback: extract summary from text
summary_line = output.split('\n')[0][:200]
return {
"summary": summary_line,
"tag": "interesting",
"relevance": "relevant"
}
else: else:
logger.warning(f" Gateway error: {response.status_code}") logger.warning(f" Sub-agent error: {result.stderr[:200]}")
return None
except subprocess.TimeoutExpired:
logger.warning(f" Analysis timeout")
return None return None
except Exception as e: except Exception as e:
logger.warning(f" Gateway timeout/error: {e}") logger.warning(f" Analysis error: {e}")
return None return None
# Send to Tududi inbox # Send to Tududi inbox
def add_to_tududi(title, url, link_type, analysis=""): def add_to_tududi(title, url, link_type, summary="", tag=""):
"""Add to Tududi inbox with summary""" """Add to Tududi inbox with intelligent summary"""
logger.debug(f" 📌 Adding to Tududi: {title}") logger.debug(f" 📌 Adding to Tududi: {title}")
try: try:
@@ -212,9 +243,14 @@ def add_to_tududi(title, url, link_type, analysis=""):
logger.warning(" TUDUDI_API_KEY not set") logger.warning(" TUDUDI_API_KEY not set")
return False return False
# Format the inbox content
content = f"📌 **{link_type}**: {title}\n🔗 {url}" content = f"📌 **{link_type}**: {title}\n🔗 {url}"
if analysis:
content += f"\n\n💡 Summary:\n{analysis}" if summary:
content += f"\n\n💡 **Summary**:\n{summary}"
if tag:
content += f"\n\n🏷️ **Tag**: {tag}"
response = requests.post( response = requests.post(
f"{TUDUDI_API_URL}/inbox", f"{TUDUDI_API_URL}/inbox",
@@ -226,8 +262,8 @@ def add_to_tududi(title, url, link_type, analysis=""):
timeout=5 timeout=5
) )
if response.status_code == 200: if response.status_code in [200, 201]: # 200 or 201 are both OK
logger.info(f" ✓ Added to Tududi inbox") logger.info(f" ✓ Added to Tududi inbox with tag: {tag}")
return True return True
else: else:
logger.warning(f" Tududi error: {response.status_code}") logger.warning(f" Tududi error: {response.status_code}")
@@ -278,24 +314,27 @@ class LinkAnalyzerBot(discord.Client):
fetch_result = fetch_url_content(url) fetch_result = fetch_url_content(url)
title = fetch_result["title"] title = fetch_result["title"]
# Analyze with gateway (disabled for now - no valid endpoint) # Analyze content if fetch was successful
analysis = None analysis_data = None
# if fetch_result["status"] == "ok": if fetch_result["status"] == "ok":
# analysis = analyze_with_gateway(url, title, fetch_result.get("content", "")) analysis_data = analyze_content(url, title, fetch_result.get("content", ""), link_type)
# Add to Tududi # Prepare summary for Tududi
tududi_ok = add_to_tududi(title, url, link_type, analysis or "") summary_text = ""
tag = "interesting"
if analysis_data:
summary_text = analysis_data.get("summary", "")
tag = analysis_data.get("tag", "interesting")
# Add to Tududi with summary
tududi_ok = add_to_tududi(title, url, link_type, summary_text, tag)
# Format response for Discord # Format response for Discord
response_text = f"📌 **{link_type}**: {title}" response_text = f"📌 **{link_type}**: {title}"
if fetch_result.get("description"): if summary_text:
# Add description if available response_text += f"\n\n💡 {summary_text}"
desc = fetch_result["description"][:150] if tag:
response_text += f"\n📝 {desc}" response_text += f"\n\n🏷️ Tag: `{tag}`"
if analysis:
# Truncate to 200 chars for Discord
summary = analysis[:200].split('\n')[0]
response_text += f"\n💡 {summary}"
logger.debug(f"Posting response: {response_text}") logger.debug(f"Posting response: {response_text}")