Feature: AI-powered intelligent summaries with sub-agents (analyze + tag)
This commit is contained in:
139
bot.py
139
bot.py
@@ -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}")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user