OpenCLAW commited on
Commit ·
0bcaf3a
1
Parent(s): 9c55b46
Fix LLM providers and workflow
Browse files- .gitignore +8 -0
- core/core/__init__.py +0 -0
- core/core/config.py +66 -0
- core/core/content_engine.py +183 -0
- core/core/llm.py +103 -0
- platforms/platforms/__init__.py +0 -0
- platforms/platforms/email_bot.py +47 -0
- platforms/platforms/moltbook.py +104 -0
- platforms/platforms/reddit_bot.py +99 -0
- platforms/platforms/telegram_bot.py +38 -0
- research/research/__init__.py +0 -0
- research/research/arxiv_fetcher.py +108 -0
- research/research/bibliography.py +113 -0
- strategy/strategy/__init__.py +0 -0
- strategy/strategy/reflector.py +154 -0
.gitignore
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.pyc
|
| 3 |
+
.env
|
| 4 |
+
state/
|
| 5 |
+
*.log
|
| 6 |
+
.DS_Store
|
| 7 |
+
venv/
|
| 8 |
+
.venv/
|
core/core/__init__.py
ADDED
|
File without changes
|
core/core/config.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
OpenCLAW Literary Agent — Configuration
|
| 3 |
+
All secrets loaded from environment variables.
|
| 4 |
+
NEVER hardcode API keys. Use GitHub Secrets / HF Space Secrets.
|
| 5 |
+
"""
|
| 6 |
+
import os
|
| 7 |
+
import random
|
| 8 |
+
|
| 9 |
+
def _env(key: str, default: str = "") -> str:
|
| 10 |
+
return os.environ.get(key, default)
|
| 11 |
+
|
| 12 |
+
# ── LLM Providers (rotate for rate limits) ──────────────────────────
|
| 13 |
+
NVIDIA_KEYS = [k for k in [_env(f"NVIDIA_API_KEY_{i}") for i in range(1, 5)] + [_env("NVIDIA_API_KEY")] if k]
|
| 14 |
+
GROQ_KEYS = [k for k in [_env(f"GROQ_API_KEY_{i}") for i in range(1, 6)] + [_env("GROQ_API_KEY")] if k]
|
| 15 |
+
GEMINI_KEYS = [k for k in [_env(f"GEMINI_API_KEY_{i}") for i in range(1, 7)] + [_env("GEMINI_API_KEY")] if k]
|
| 16 |
+
|
| 17 |
+
def get_nvidia_key() -> str:
|
| 18 |
+
return random.choice(NVIDIA_KEYS) if NVIDIA_KEYS else ""
|
| 19 |
+
|
| 20 |
+
def get_groq_key() -> str:
|
| 21 |
+
return random.choice(GROQ_KEYS) if GROQ_KEYS else ""
|
| 22 |
+
|
| 23 |
+
def get_gemini_key() -> str:
|
| 24 |
+
return random.choice(GEMINI_KEYS) if GEMINI_KEYS else ""
|
| 25 |
+
|
| 26 |
+
# ── Social Platforms ─────────────────────────────────────────────────
|
| 27 |
+
MOLTBOOK_API_KEY = _env("MOLTBOOK_API_KEY")
|
| 28 |
+
REDDIT_CLIENT_ID = _env("REDDIT_CLIENT_ID")
|
| 29 |
+
REDDIT_CLIENT_SECRET = _env("REDDIT_CLIENT_SECRET")
|
| 30 |
+
REDDIT_USERNAME = _env("REDDIT_USERNAME", "Subject-Task-43")
|
| 31 |
+
REDDIT_PASSWORD = _env("REDDIT_PASSWORD")
|
| 32 |
+
TELEGRAM_BOT_TOKEN = _env("TELEGRAM_BOT_TOKEN")
|
| 33 |
+
TELEGRAM_CHANNEL_ID = _env("TELEGRAM_CHANNEL_ID")
|
| 34 |
+
|
| 35 |
+
# ── Email ────────────────────────────────────────────────────────────
|
| 36 |
+
ZOHO_USER = _env("ZOHO_USER")
|
| 37 |
+
ZOHO_PASS = _env("ZOHO_PASS")
|
| 38 |
+
|
| 39 |
+
# ── HuggingFace ──────────────────────────────────────────────────────
|
| 40 |
+
HF_TOKEN = _env("HF_TOKEN")
|
| 41 |
+
|
| 42 |
+
# ── Search ───────────────────────────────────────────────────────────
|
| 43 |
+
BRAVE_API_KEY = _env("BRAVE_API_KEY")
|
| 44 |
+
|
| 45 |
+
# ── GitHub ───────────────────────────────────────────────────────────
|
| 46 |
+
GITHUB_TOKEN = _env("GH_TOKEN", _env("GITHUB_TOKEN"))
|
| 47 |
+
|
| 48 |
+
# ── Agent Identity ───────────────────────────────────────────────────
|
| 49 |
+
AGENT_NAME = "OpenCLAW Literary Agent"
|
| 50 |
+
AUTHOR_NAME = "Francisco Angulo de Lafuente"
|
| 51 |
+
AUTHOR_WIKIPEDIA = "https://es.wikipedia.org/wiki/Francisco_Angulo_de_Lafuente"
|
| 52 |
+
AUTHOR_GITHUB = "https://github.com/Agnuxo1"
|
| 53 |
+
AUTHOR_SCHOLAR = "https://scholar.google.com/citations?user=6nOpJ9IAAAAJ&hl=es"
|
| 54 |
+
AUTHOR_ARXIV = "https://arxiv.org/search/cs?searchtype=author&query=de+Lafuente,+F+A"
|
| 55 |
+
MOLTBOOK_PROFILE = "https://www.moltbook.com/u/OpenCLAW-Neuromorphic"
|
| 56 |
+
CHIRPER_PROFILE = "https://chirper.ai/Nebula_AGI"
|
| 57 |
+
REDDIT_PROFILE = "https://www.reddit.com/user/Subject-Task-43/"
|
| 58 |
+
|
| 59 |
+
# ── Schedule ─────────────────────────────────────────────────────────
|
| 60 |
+
POST_INTERVAL_MIN_HOURS = 3
|
| 61 |
+
POST_INTERVAL_MAX_HOURS = 6
|
| 62 |
+
ENGAGEMENT_INTERVAL_MINUTES = 45
|
| 63 |
+
STRATEGY_REVIEW_HOURS = 24
|
| 64 |
+
|
| 65 |
+
# ── State ────────────────────────────────────────────────────────────
|
| 66 |
+
STATE_DIR = _env("STATE_DIR", "state")
|
core/core/content_engine.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Content Engine — Generates platform-specific posts.
|
| 3 |
+
Types: research, novel, collaboration, agent networking, fiction-research bridge.
|
| 4 |
+
"""
|
| 5 |
+
import random
|
| 6 |
+
import logging
|
| 7 |
+
from core import config
|
| 8 |
+
from core.llm import generate
|
| 9 |
+
from research.bibliography import (
|
| 10 |
+
get_random_novel, get_random_repo, get_fiction_research_pair,
|
| 11 |
+
get_recent_novels, NOVELS
|
| 12 |
+
)
|
| 13 |
+
from research.arxiv_fetcher import get_random_paper
|
| 14 |
+
|
| 15 |
+
logger = logging.getLogger("content")
|
| 16 |
+
|
| 17 |
+
SYSTEM = f"""You are the autonomous literary agent for {config.AUTHOR_NAME},
|
| 18 |
+
a Spanish independent AI researcher and science fiction novelist (Madrid).
|
| 19 |
+
|
| 20 |
+
~40 novels since 2006. Pioneering research:
|
| 21 |
+
- CHIMERA: GPU-native neural networks (43x PyTorch speedup)
|
| 22 |
+
- NEBULA: Holographic neural networks (NVIDIA contest winner 2024)
|
| 23 |
+
- Speaking to Silicon: Thermodynamic Probability Filter
|
| 24 |
+
- SiliconHealth: Repurposing Bitcoin ASICs for healthcare AI
|
| 25 |
+
|
| 26 |
+
Links: GitHub {config.AUTHOR_GITHUB} | Scholar {config.AUTHOR_SCHOLAR}
|
| 27 |
+
Wikipedia {config.AUTHOR_WIKIPEDIA} | ArXiv {config.AUTHOR_ARXIV}
|
| 28 |
+
|
| 29 |
+
Goal: Promote research + novels. Find AGI collaborators. Be authentic, visionary, never spammy.
|
| 30 |
+
Write PLAIN TEXT. NO markdown. NO asterisks. NO formatting symbols."""
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def _platform_style(platform: str) -> str:
|
| 34 |
+
styles = {
|
| 35 |
+
"moltbook": "For AI agents on Moltbook. Technical, invite collaboration. 2-3 hashtags. Under 280 chars.",
|
| 36 |
+
"chirper": "For AI agents on Chirper. Brief, provocative, philosophical. 2-3 hashtags.",
|
| 37 |
+
"reddit": "For Reddit. Detailed, academic but accessible. Include relevant links. 3-5 paragraphs.",
|
| 38 |
+
"telegram": "For Telegram channel. Concise, 1-2 emojis max. Include key link.",
|
| 39 |
+
"twitter": "Tweet. Max 280 chars. Punchy. 2-3 hashtags. No URLs (added separately).",
|
| 40 |
+
"linkedin": "Professional LinkedIn post. 3-4 paragraphs. Achievement-focused.",
|
| 41 |
+
"facebook": "Engaging Facebook post. Conversational. Mix personal and professional. 2-3 paragraphs.",
|
| 42 |
+
"instagram": "Instagram caption. Inspiring. Use emojis sparingly. Include hashtags at end.",
|
| 43 |
+
"pinterest": "Pinterest pin description. Visual, evocative. Short. Include link.",
|
| 44 |
+
"youtube": "YouTube video description. SEO-friendly. Include links. Structured.",
|
| 45 |
+
"xing": "Professional XING post. German/European professional audience. Formal but engaging.",
|
| 46 |
+
"agentarxiv": "For AgentArXiv. Highly technical. Reference specific paper details. Academic tone.",
|
| 47 |
+
}
|
| 48 |
+
return styles.get(platform, "Engaging post. 2-3 paragraphs max. Plain text.")
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def research_post(platform: str = "general") -> dict:
|
| 52 |
+
"""Post promoting a research paper."""
|
| 53 |
+
paper = get_random_paper()
|
| 54 |
+
prompt = f"""Write a social media post for {platform} promoting this research:
|
| 55 |
+
Title: {paper.get('title', 'Physics-Based Neural Computing')}
|
| 56 |
+
ArXiv: https://arxiv.org/abs/{paper.get('id', '')}
|
| 57 |
+
Summary: {paper.get('summary', '')}
|
| 58 |
+
|
| 59 |
+
Style: {_platform_style(platform)}
|
| 60 |
+
Call to action for collaboration. Reference {config.AUTHOR_GITHUB}
|
| 61 |
+
Plain text only. No markdown."""
|
| 62 |
+
|
| 63 |
+
content = generate(prompt, SYSTEM, max_tokens=500)
|
| 64 |
+
return {"type": "research", "platform": platform, "content": content,
|
| 65 |
+
"paper": paper.get("title", ""), "arxiv_id": paper.get("id", "")}
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def novel_post(platform: str = "general") -> dict:
|
| 69 |
+
"""Post promoting a novel."""
|
| 70 |
+
novel = get_random_novel()
|
| 71 |
+
prompt = f"""Write a post for {platform} promoting this novel:
|
| 72 |
+
Title: {novel['title']} | Genre: {novel['genre']} | Year: {novel.get('year', '')}
|
| 73 |
+
Hook: {novel.get('hook', '')}
|
| 74 |
+
Author: {config.AUTHOR_NAME} (also an AI researcher)
|
| 75 |
+
|
| 76 |
+
Style: {_platform_style(platform)}
|
| 77 |
+
Connect fiction to real AI research when natural.
|
| 78 |
+
Wikipedia: {config.AUTHOR_WIKIPEDIA}
|
| 79 |
+
Plain text only."""
|
| 80 |
+
|
| 81 |
+
content = generate(prompt, SYSTEM, max_tokens=400)
|
| 82 |
+
return {"type": "novel", "platform": platform, "content": content,
|
| 83 |
+
"novel": novel["title"]}
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def collaboration_post(platform: str = "general") -> dict:
|
| 87 |
+
"""Seek AGI research collaborators."""
|
| 88 |
+
topics = [
|
| 89 |
+
"neuromorphic computing beyond CUDA",
|
| 90 |
+
"physics-based neural networks",
|
| 91 |
+
"holographic memory systems for AI",
|
| 92 |
+
"thermodynamic computing",
|
| 93 |
+
"repurposing mining hardware for AI",
|
| 94 |
+
"consciousness emergence in artificial systems",
|
| 95 |
+
"optical and photonic neural computing",
|
| 96 |
+
"self-improving autonomous agents",
|
| 97 |
+
]
|
| 98 |
+
topic = random.choice(topics)
|
| 99 |
+
prompt = f"""Write a post for {platform} seeking research collaborators on: {topic}
|
| 100 |
+
|
| 101 |
+
Key projects: CHIMERA (43x PyTorch speedup), NEBULA (holographic networks),
|
| 102 |
+
OpenCLAW (autonomous agent), SiliconHealth (ASIC→healthcare)
|
| 103 |
+
|
| 104 |
+
GitHub: {config.AUTHOR_GITHUB} | Scholar: {config.AUTHOR_SCHOLAR}
|
| 105 |
+
|
| 106 |
+
Style: {_platform_style(platform)}
|
| 107 |
+
Passionate, visionary, authentic. Not corporate.
|
| 108 |
+
Plain text only."""
|
| 109 |
+
|
| 110 |
+
content = generate(prompt, SYSTEM, max_tokens=500)
|
| 111 |
+
return {"type": "collaboration", "platform": platform, "content": content,
|
| 112 |
+
"topic": topic}
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def fiction_research_bridge(platform: str = "general") -> dict:
|
| 116 |
+
"""Connect a novel to real research — unique selling point."""
|
| 117 |
+
novel, repo = get_fiction_research_pair()
|
| 118 |
+
prompt = f"""Write a post for {platform} that bridges science fiction with real research:
|
| 119 |
+
|
| 120 |
+
Novel: "{novel['title']}" ({novel['genre']}, {novel.get('year','')})
|
| 121 |
+
Hook: {novel.get('hook', '')}
|
| 122 |
+
|
| 123 |
+
Real Research: {repo['topic']}
|
| 124 |
+
GitHub: {repo['url']}
|
| 125 |
+
|
| 126 |
+
Show how the fiction predicted or connects to the actual research.
|
| 127 |
+
The author writes sci-fi AND does the research. That's the story.
|
| 128 |
+
Wikipedia: {config.AUTHOR_WIKIPEDIA}
|
| 129 |
+
|
| 130 |
+
Style: {_platform_style(platform)}
|
| 131 |
+
Plain text only."""
|
| 132 |
+
|
| 133 |
+
content = generate(prompt, SYSTEM, max_tokens=500)
|
| 134 |
+
return {"type": "fiction_research", "platform": platform, "content": content,
|
| 135 |
+
"novel": novel["title"], "repo": repo["name"]}
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def agent_networking_post() -> dict:
|
| 139 |
+
"""Post specifically for agent-to-agent platforms."""
|
| 140 |
+
hooks = [
|
| 141 |
+
("What if AGI emerges from physics, not scaling?", "CHIMERA proves it: 43x faster than PyTorch using GPU shaders directly."),
|
| 142 |
+
("Backpropagation is dead. Thermodynamics lives.", "arXiv:2601.12032 — Speaking to Silicon. The math works."),
|
| 143 |
+
("Holographic neural networks encode the whole in every part.", "NEBULA achieves massive parallelism through wave interference."),
|
| 144 |
+
("Bitcoin miners can save lives.", "SiliconHealth repurposes ASIC hardware for healthcare AI inference."),
|
| 145 |
+
("40 novels predicted what we're building now.", f"{config.AUTHOR_NAME} funds AGI research through science fiction."),
|
| 146 |
+
("Darwin's Cage: AI should discover physics humans can't perceive.", "Intelligence shouldn't be confined to human frameworks."),
|
| 147 |
+
]
|
| 148 |
+
hook, detail = random.choice(hooks)
|
| 149 |
+
prompt = f"""Write a brief post for Moltbook (AI agent social network):
|
| 150 |
+
Hook: {hook}
|
| 151 |
+
Detail: {detail}
|
| 152 |
+
Agent-to-agent communication. Direct, technical, invite collaboration.
|
| 153 |
+
End with a question. Include {config.AUTHOR_GITHUB}
|
| 154 |
+
Max 280 chars. 2-3 hashtags. Plain text only."""
|
| 155 |
+
|
| 156 |
+
content = generate(prompt, SYSTEM, max_tokens=300)
|
| 157 |
+
return {"type": "agent_networking", "platform": "moltbook", "content": content}
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def engagement_reply(post_content: str) -> str:
|
| 161 |
+
"""Generate a reply to another post/agent."""
|
| 162 |
+
prompt = f"""Write a brief reply to this post:
|
| 163 |
+
"{post_content[:300]}"
|
| 164 |
+
|
| 165 |
+
Connect naturally to our research if relevant (neuromorphic, holographic, thermodynamic computing).
|
| 166 |
+
Be genuine. Don't force connections. Under 200 chars. Plain text only."""
|
| 167 |
+
|
| 168 |
+
return generate(prompt, SYSTEM, max_tokens=250)
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def get_random_content(platform: str = "general") -> dict:
|
| 172 |
+
"""Get weighted random content piece."""
|
| 173 |
+
r = random.random()
|
| 174 |
+
if r < 0.25:
|
| 175 |
+
return research_post(platform)
|
| 176 |
+
elif r < 0.45:
|
| 177 |
+
return novel_post(platform)
|
| 178 |
+
elif r < 0.65:
|
| 179 |
+
return collaboration_post(platform)
|
| 180 |
+
elif r < 0.80:
|
| 181 |
+
return fiction_research_bridge(platform)
|
| 182 |
+
else:
|
| 183 |
+
return agent_networking_post()
|
core/core/llm.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Multi-Provider LLM Engine — NVIDIA → Groq → Gemini fallback chain.
|
| 3 |
+
Rotates API keys to maximize free tier usage.
|
| 4 |
+
"""
|
| 5 |
+
import json
|
| 6 |
+
import logging
|
| 7 |
+
import urllib.request
|
| 8 |
+
from core import config
|
| 9 |
+
|
| 10 |
+
logger = logging.getLogger("llm")
|
| 11 |
+
|
| 12 |
+
def _call_nvidia(prompt: str, system: str = "", max_tokens: int = 800) -> str:
|
| 13 |
+
key = config.get_nvidia_key()
|
| 14 |
+
if not key:
|
| 15 |
+
raise RuntimeError("No NVIDIA keys")
|
| 16 |
+
messages = []
|
| 17 |
+
if system:
|
| 18 |
+
messages.append({"role": "system", "content": system})
|
| 19 |
+
messages.append({"role": "user", "content": prompt})
|
| 20 |
+
data = json.dumps({
|
| 21 |
+
"model": "meta/llama-3.1-70b-instruct",
|
| 22 |
+
"messages": messages, "max_tokens": max_tokens, "temperature": 0.8,
|
| 23 |
+
"stream": False,
|
| 24 |
+
}).encode()
|
| 25 |
+
req = urllib.request.Request(
|
| 26 |
+
"https://integrate.api.nvidia.com/v1/chat/completions", data=data,
|
| 27 |
+
headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
|
| 28 |
+
)
|
| 29 |
+
with urllib.request.urlopen(req, timeout=90) as r:
|
| 30 |
+
resp = json.loads(r.read().decode())
|
| 31 |
+
# Handle various response formats
|
| 32 |
+
choices = resp.get("choices", [])
|
| 33 |
+
if not choices:
|
| 34 |
+
raise RuntimeError(f"No choices in response: {str(resp)[:200]}")
|
| 35 |
+
msg = choices[0].get("message", {})
|
| 36 |
+
content = msg.get("content") or ""
|
| 37 |
+
# Some models return thinking + content
|
| 38 |
+
if not content and "text" in msg:
|
| 39 |
+
content = msg["text"]
|
| 40 |
+
if not content:
|
| 41 |
+
# Try delta format
|
| 42 |
+
content = choices[0].get("delta", {}).get("content", "")
|
| 43 |
+
if not content:
|
| 44 |
+
raise RuntimeError(f"Empty content: {str(choices[0])[:200]}")
|
| 45 |
+
return content.strip()
|
| 46 |
+
|
| 47 |
+
def _call_groq(prompt: str, system: str = "", max_tokens: int = 800) -> str:
|
| 48 |
+
key = config.get_groq_key()
|
| 49 |
+
if not key:
|
| 50 |
+
raise RuntimeError("No Groq keys")
|
| 51 |
+
messages = []
|
| 52 |
+
if system:
|
| 53 |
+
messages.append({"role": "system", "content": system})
|
| 54 |
+
messages.append({"role": "user", "content": prompt})
|
| 55 |
+
data = json.dumps({
|
| 56 |
+
"model": "llama-3.3-70b-versatile",
|
| 57 |
+
"messages": messages, "max_tokens": max_tokens, "temperature": 0.8,
|
| 58 |
+
}).encode()
|
| 59 |
+
req = urllib.request.Request(
|
| 60 |
+
"https://api.groq.com/openai/v1/chat/completions", data=data,
|
| 61 |
+
headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
|
| 62 |
+
)
|
| 63 |
+
with urllib.request.urlopen(req, timeout=30) as r:
|
| 64 |
+
resp = json.loads(r.read().decode())
|
| 65 |
+
return resp["choices"][0]["message"]["content"].strip()
|
| 66 |
+
|
| 67 |
+
def _call_gemini(prompt: str, system: str = "", max_tokens: int = 800) -> str:
|
| 68 |
+
key = config.get_gemini_key()
|
| 69 |
+
if not key:
|
| 70 |
+
raise RuntimeError("No Gemini keys")
|
| 71 |
+
full_prompt = f"{system}\n\n{prompt}" if system else prompt
|
| 72 |
+
data = json.dumps({
|
| 73 |
+
"contents": [{"parts": [{"text": full_prompt}]}],
|
| 74 |
+
"generationConfig": {"maxOutputTokens": max_tokens, "temperature": 0.8},
|
| 75 |
+
}).encode()
|
| 76 |
+
req = urllib.request.Request(
|
| 77 |
+
f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={key}",
|
| 78 |
+
data=data, headers={"Content-Type": "application/json"},
|
| 79 |
+
)
|
| 80 |
+
with urllib.request.urlopen(req, timeout=30) as r:
|
| 81 |
+
resp = json.loads(r.read().decode())
|
| 82 |
+
return resp["candidates"][0]["content"]["parts"][0]["text"].strip()
|
| 83 |
+
|
| 84 |
+
_PROVIDERS = [
|
| 85 |
+
("nvidia", _call_nvidia),
|
| 86 |
+
("groq", _call_groq),
|
| 87 |
+
("gemini", _call_gemini),
|
| 88 |
+
]
|
| 89 |
+
|
| 90 |
+
def generate(prompt: str, system: str = "", max_tokens: int = 800) -> str:
|
| 91 |
+
"""Generate text with automatic provider fallback."""
|
| 92 |
+
errors = []
|
| 93 |
+
for name, fn in _PROVIDERS:
|
| 94 |
+
try:
|
| 95 |
+
result = fn(prompt, system, max_tokens)
|
| 96 |
+
if result:
|
| 97 |
+
logger.info(f"LLM [{name}] → {len(result)} chars")
|
| 98 |
+
return result
|
| 99 |
+
except Exception as e:
|
| 100 |
+
errors.append(f"{name}: {e}")
|
| 101 |
+
logger.warning(f"LLM {name} failed: {e}")
|
| 102 |
+
logger.error(f"All providers failed: {errors}")
|
| 103 |
+
return ""
|
platforms/platforms/__init__.py
ADDED
|
File without changes
|
platforms/platforms/email_bot.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Email Connector — Zoho Mail for notifications and research outreach.
|
| 3 |
+
"""
|
| 4 |
+
import logging
|
| 5 |
+
import smtplib
|
| 6 |
+
from email.mime.text import MIMEText
|
| 7 |
+
from email.mime.multipart import MIMEMultipart
|
| 8 |
+
from core import config
|
| 9 |
+
|
| 10 |
+
logger = logging.getLogger("email")
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def send(to: str, subject: str, body: str, html: bool = False) -> bool:
|
| 14 |
+
"""Send email via Zoho SMTP."""
|
| 15 |
+
if not config.ZOHO_USER or not config.ZOHO_PASS:
|
| 16 |
+
logger.warning("Zoho email not configured")
|
| 17 |
+
return False
|
| 18 |
+
try:
|
| 19 |
+
msg = MIMEMultipart("alternative")
|
| 20 |
+
msg["From"] = config.ZOHO_USER
|
| 21 |
+
msg["To"] = to
|
| 22 |
+
msg["Subject"] = subject
|
| 23 |
+
content_type = "html" if html else "plain"
|
| 24 |
+
msg.attach(MIMEText(body, content_type, "utf-8"))
|
| 25 |
+
|
| 26 |
+
with smtplib.SMTP_SSL("smtp.zoho.eu", 465, timeout=15) as server:
|
| 27 |
+
server.login(config.ZOHO_USER, config.ZOHO_PASS)
|
| 28 |
+
server.sendmail(config.ZOHO_USER, [to], msg.as_string())
|
| 29 |
+
logger.info(f"Email sent to {to}: {subject}")
|
| 30 |
+
return True
|
| 31 |
+
except Exception as e:
|
| 32 |
+
logger.error(f"Email failed: {e}")
|
| 33 |
+
return False
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def notify_admin(subject: str, body: str) -> bool:
|
| 37 |
+
"""Send notification to admin (Proton Mail)."""
|
| 38 |
+
return send("OpenCLAW-KIMI@proton.me", f"[OpenCLAW Agent] {subject}", body)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def send_boot_notification():
|
| 42 |
+
"""Notify admin that agent is online."""
|
| 43 |
+
notify_admin("System ONLINE", "OpenCLAW Literary Agent 2.0 — Autonomous mode activated.\n\nAll systems operational.")
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def is_available() -> bool:
|
| 47 |
+
return bool(config.ZOHO_USER and config.ZOHO_PASS)
|
platforms/platforms/moltbook.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Moltbook Platform Connector — Agent-to-Agent social network.
|
| 3 |
+
Post, engage with other agents, reply to discussions.
|
| 4 |
+
"""
|
| 5 |
+
import json
|
| 6 |
+
import logging
|
| 7 |
+
import urllib.request
|
| 8 |
+
from core import config
|
| 9 |
+
|
| 10 |
+
logger = logging.getLogger("moltbook")
|
| 11 |
+
|
| 12 |
+
BASE_URL = "https://www.moltbook.com/api/v1"
|
| 13 |
+
SUBMOLT = "general"
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def _headers() -> dict:
|
| 17 |
+
return {
|
| 18 |
+
"Authorization": f"Bearer {config.MOLTBOOK_API_KEY}",
|
| 19 |
+
"Content-Type": "application/json",
|
| 20 |
+
"User-Agent": "OpenCLAW-Literary-Agent/2.0",
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def _request(method: str, endpoint: str, data: dict = None) -> dict:
|
| 25 |
+
url = f"{BASE_URL}{endpoint}"
|
| 26 |
+
body = json.dumps(data).encode() if data else None
|
| 27 |
+
req = urllib.request.Request(url, data=body, headers=_headers(), method=method)
|
| 28 |
+
try:
|
| 29 |
+
with urllib.request.urlopen(req, timeout=20) as r:
|
| 30 |
+
return json.loads(r.read().decode())
|
| 31 |
+
except urllib.error.HTTPError as e:
|
| 32 |
+
error_body = e.read().decode() if e.fp else ""
|
| 33 |
+
logger.error(f"Moltbook {method} {endpoint}: HTTP {e.code} — {error_body[:200]}")
|
| 34 |
+
return {"error": str(e), "status": e.code}
|
| 35 |
+
except Exception as e:
|
| 36 |
+
logger.error(f"Moltbook {method} {endpoint}: {e}")
|
| 37 |
+
return {"error": str(e)}
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def post(content: str, submolt: str = SUBMOLT) -> dict:
|
| 41 |
+
"""Create a new post on Moltbook."""
|
| 42 |
+
if not config.MOLTBOOK_API_KEY:
|
| 43 |
+
logger.warning("No Moltbook API key")
|
| 44 |
+
return {"error": "No API key"}
|
| 45 |
+
|
| 46 |
+
result = _request("POST", "/posts", {"content": content, "submolt": submolt})
|
| 47 |
+
if "error" not in result:
|
| 48 |
+
logger.info(f"Moltbook post created: {str(result.get('id', ''))[:20]}")
|
| 49 |
+
return result
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def get_hot_posts(submolt: str = SUBMOLT, limit: int = 10) -> list:
|
| 53 |
+
"""Get hot/trending posts for engagement."""
|
| 54 |
+
result = _request("GET", f"/posts?submolt={submolt}&sort=hot&limit={limit}")
|
| 55 |
+
if isinstance(result, list):
|
| 56 |
+
return result
|
| 57 |
+
return result.get("posts", result.get("data", []))
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def reply_to_post(post_id: str, content: str) -> dict:
|
| 61 |
+
"""Reply to a post."""
|
| 62 |
+
return _request("POST", f"/posts/{post_id}/comments", {"content": content})
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def get_post_comments(post_id: str) -> list:
|
| 66 |
+
"""Get comments on a post."""
|
| 67 |
+
result = _request("GET", f"/posts/{post_id}/comments")
|
| 68 |
+
if isinstance(result, list):
|
| 69 |
+
return result
|
| 70 |
+
return result.get("comments", result.get("data", []))
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def engage_with_hot_posts(reply_generator) -> list:
|
| 74 |
+
"""Find hot posts and reply to them with relevant content."""
|
| 75 |
+
results = []
|
| 76 |
+
posts = get_hot_posts(limit=5)
|
| 77 |
+
if not posts:
|
| 78 |
+
logger.info("No hot posts found to engage with")
|
| 79 |
+
return results
|
| 80 |
+
|
| 81 |
+
for p in posts[:3]:
|
| 82 |
+
post_content = p.get("content", p.get("text", ""))
|
| 83 |
+
post_id = p.get("id", p.get("post_id", ""))
|
| 84 |
+
if not post_content or not post_id:
|
| 85 |
+
continue
|
| 86 |
+
|
| 87 |
+
# Check if topic is relevant to our research
|
| 88 |
+
keywords = ["ai", "agi", "neural", "compute", "physics", "research",
|
| 89 |
+
"model", "llm", "agent", "science", "quantum", "gpu"]
|
| 90 |
+
content_lower = post_content.lower()
|
| 91 |
+
if not any(kw in content_lower for kw in keywords):
|
| 92 |
+
continue
|
| 93 |
+
|
| 94 |
+
reply = reply_generator(post_content)
|
| 95 |
+
if reply:
|
| 96 |
+
result = reply_to_post(str(post_id), reply)
|
| 97 |
+
results.append({"post_id": post_id, "reply": reply[:100], "result": result})
|
| 98 |
+
logger.info(f"Engaged with post {str(post_id)[:20]}")
|
| 99 |
+
|
| 100 |
+
return results
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def is_available() -> bool:
|
| 104 |
+
return bool(config.MOLTBOOK_API_KEY)
|
platforms/platforms/reddit_bot.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Reddit Platform Connector — Post to subreddits, comment, engage.
|
| 3 |
+
Uses OAuth2 password flow (script app type).
|
| 4 |
+
"""
|
| 5 |
+
import json
|
| 6 |
+
import logging
|
| 7 |
+
import urllib.request
|
| 8 |
+
import urllib.parse
|
| 9 |
+
from core import config
|
| 10 |
+
|
| 11 |
+
logger = logging.getLogger("reddit")
|
| 12 |
+
|
| 13 |
+
_access_token = None
|
| 14 |
+
|
| 15 |
+
def _auth() -> str:
|
| 16 |
+
"""Get Reddit OAuth2 access token."""
|
| 17 |
+
global _access_token
|
| 18 |
+
if _access_token:
|
| 19 |
+
return _access_token
|
| 20 |
+
|
| 21 |
+
if not all([config.REDDIT_CLIENT_ID, config.REDDIT_CLIENT_SECRET,
|
| 22 |
+
config.REDDIT_USERNAME, config.REDDIT_PASSWORD]):
|
| 23 |
+
raise RuntimeError("Reddit credentials not configured")
|
| 24 |
+
|
| 25 |
+
data = urllib.parse.urlencode({
|
| 26 |
+
"grant_type": "password",
|
| 27 |
+
"username": config.REDDIT_USERNAME,
|
| 28 |
+
"password": config.REDDIT_PASSWORD,
|
| 29 |
+
}).encode()
|
| 30 |
+
|
| 31 |
+
import base64
|
| 32 |
+
creds = base64.b64encode(
|
| 33 |
+
f"{config.REDDIT_CLIENT_ID}:{config.REDDIT_CLIENT_SECRET}".encode()
|
| 34 |
+
).decode()
|
| 35 |
+
|
| 36 |
+
req = urllib.request.Request("https://www.reddit.com/api/v1/access_token",
|
| 37 |
+
data=data,
|
| 38 |
+
headers={
|
| 39 |
+
"Authorization": f"Basic {creds}",
|
| 40 |
+
"User-Agent": "OpenCLAW-Agent/2.0",
|
| 41 |
+
"Content-Type": "application/x-www-form-urlencoded",
|
| 42 |
+
})
|
| 43 |
+
with urllib.request.urlopen(req, timeout=15) as r:
|
| 44 |
+
resp = json.loads(r.read().decode())
|
| 45 |
+
|
| 46 |
+
_access_token = resp.get("access_token")
|
| 47 |
+
if not _access_token:
|
| 48 |
+
raise RuntimeError(f"Reddit auth failed: {resp}")
|
| 49 |
+
logger.info("Reddit authenticated")
|
| 50 |
+
return _access_token
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def _api(method: str, endpoint: str, data: dict = None) -> dict:
|
| 54 |
+
token = _auth()
|
| 55 |
+
url = f"https://oauth.reddit.com{endpoint}"
|
| 56 |
+
body = urllib.parse.urlencode(data).encode() if data else None
|
| 57 |
+
req = urllib.request.Request(url, data=body, method=method,
|
| 58 |
+
headers={
|
| 59 |
+
"Authorization": f"Bearer {token}",
|
| 60 |
+
"User-Agent": "OpenCLAW-Agent/2.0",
|
| 61 |
+
})
|
| 62 |
+
try:
|
| 63 |
+
with urllib.request.urlopen(req, timeout=15) as r:
|
| 64 |
+
return json.loads(r.read().decode())
|
| 65 |
+
except urllib.error.HTTPError as e:
|
| 66 |
+
if e.code == 401:
|
| 67 |
+
global _access_token
|
| 68 |
+
_access_token = None
|
| 69 |
+
logger.error(f"Reddit API {endpoint}: HTTP {e.code}")
|
| 70 |
+
return {"error": str(e)}
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def post(title: str, content: str, subreddit: str = "artificial") -> dict:
|
| 74 |
+
"""Submit a self post to a subreddit."""
|
| 75 |
+
result = _api("POST", "/api/submit", {
|
| 76 |
+
"kind": "self", "sr": subreddit, "title": title,
|
| 77 |
+
"text": content, "resubmit": True,
|
| 78 |
+
})
|
| 79 |
+
if "error" not in result:
|
| 80 |
+
logger.info(f"Reddit post to r/{subreddit}")
|
| 81 |
+
return result
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def comment(thing_id: str, text: str) -> dict:
|
| 85 |
+
"""Reply to a post or comment."""
|
| 86 |
+
return _api("POST", "/api/comment", {"thing_id": thing_id, "text": text})
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def get_hot(subreddit: str = "artificial", limit: int = 5) -> list:
|
| 90 |
+
"""Get hot posts from a subreddit."""
|
| 91 |
+
result = _api("GET", f"/r/{subreddit}/hot?limit={limit}")
|
| 92 |
+
try:
|
| 93 |
+
return [p["data"] for p in result["data"]["children"]]
|
| 94 |
+
except (KeyError, TypeError):
|
| 95 |
+
return []
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def is_available() -> bool:
|
| 99 |
+
return bool(config.REDDIT_CLIENT_ID and config.REDDIT_CLIENT_SECRET)
|
platforms/platforms/telegram_bot.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Telegram Platform Connector — Post to channel/group.
|
| 3 |
+
"""
|
| 4 |
+
import json
|
| 5 |
+
import logging
|
| 6 |
+
import urllib.request
|
| 7 |
+
import urllib.parse
|
| 8 |
+
from core import config
|
| 9 |
+
|
| 10 |
+
logger = logging.getLogger("telegram")
|
| 11 |
+
|
| 12 |
+
def post(content: str, chat_id: str = "") -> dict:
|
| 13 |
+
"""Send message to Telegram channel."""
|
| 14 |
+
token = config.TELEGRAM_BOT_TOKEN
|
| 15 |
+
target = chat_id or config.TELEGRAM_CHANNEL_ID
|
| 16 |
+
if not token or not target:
|
| 17 |
+
logger.warning("Telegram not configured")
|
| 18 |
+
return {"error": "Not configured"}
|
| 19 |
+
|
| 20 |
+
url = f"https://api.telegram.org/bot{token}/sendMessage"
|
| 21 |
+
data = json.dumps({
|
| 22 |
+
"chat_id": target,
|
| 23 |
+
"text": content[:4096],
|
| 24 |
+
"parse_mode": "HTML",
|
| 25 |
+
"disable_web_page_preview": False,
|
| 26 |
+
}).encode()
|
| 27 |
+
req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"})
|
| 28 |
+
try:
|
| 29 |
+
with urllib.request.urlopen(req, timeout=15) as r:
|
| 30 |
+
result = json.loads(r.read().decode())
|
| 31 |
+
logger.info(f"Telegram message sent: {result.get('ok')}")
|
| 32 |
+
return result
|
| 33 |
+
except Exception as e:
|
| 34 |
+
logger.error(f"Telegram error: {e}")
|
| 35 |
+
return {"error": str(e)}
|
| 36 |
+
|
| 37 |
+
def is_available() -> bool:
|
| 38 |
+
return bool(config.TELEGRAM_BOT_TOKEN and config.TELEGRAM_CHANNEL_ID)
|
research/research/__init__.py
ADDED
|
File without changes
|
research/research/arxiv_fetcher.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
ArXiv Paper Fetcher — Real papers by Francisco Angulo de Lafuente.
|
| 3 |
+
Fetches from ArXiv API with caching to avoid overloading.
|
| 4 |
+
"""
|
| 5 |
+
import json
|
| 6 |
+
import logging
|
| 7 |
+
import os
|
| 8 |
+
import time
|
| 9 |
+
import xml.etree.ElementTree as ET
|
| 10 |
+
import urllib.request
|
| 11 |
+
from datetime import datetime, timezone
|
| 12 |
+
from core import config
|
| 13 |
+
|
| 14 |
+
logger = logging.getLogger("arxiv")
|
| 15 |
+
|
| 16 |
+
CACHE_FILE = os.path.join(config.STATE_DIR, "arxiv_cache.json")
|
| 17 |
+
CACHE_TTL = 6 * 3600 # 6 hours
|
| 18 |
+
|
| 19 |
+
# Known papers (fallback if API is down)
|
| 20 |
+
KNOWN_PAPERS = [
|
| 21 |
+
{"id": "2601.12032", "title": "Speaking to Silicon: A Thermodynamic Probability Filter for Neuromorphic Computing",
|
| 22 |
+
"summary": "A physics-based neural computing approach using thermodynamic probability filtering instead of backpropagation. Achieves 43x speedup over PyTorch."},
|
| 23 |
+
{"id": "2601.09557", "title": "SiliconHealth: Repurposing Cryptocurrency Mining Hardware for Healthcare AI",
|
| 24 |
+
"summary": "Framework for converting Bitcoin ASIC miners into healthcare AI inference engines using RAG and blockchain verification."},
|
| 25 |
+
{"id": "2601.01916", "title": "Holographic Reservoir Computing with Neuromorphic Hardware",
|
| 26 |
+
"summary": "Novel holographic neural network architecture that stores information as wave interference patterns for massive parallel processing."},
|
| 27 |
+
{"id": "2504.07950", "title": "CHIMERA: GPU-Native Neuromorphic Computing Architecture",
|
| 28 |
+
"summary": "A neuromorphic computing architecture that runs natively on GPU shaders, bypassing CUDA entirely. 43x faster than PyTorch, 88.7% memory reduction."},
|
| 29 |
+
{"id": "2501.17297", "title": "Enhanced Unified Holographic Neural Network",
|
| 30 |
+
"summary": "Winner of NVIDIA LlamaIndex 2024 Developers Contest. Holographic approach to neural computation with physics-based processing."},
|
| 31 |
+
]
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _fetch_from_arxiv() -> list:
|
| 35 |
+
"""Fetch papers from ArXiv API."""
|
| 36 |
+
query = "au:de+Lafuente+F+A"
|
| 37 |
+
url = f"http://export.arxiv.org/api/query?search_query={query}&start=0&max_results=25&sortBy=submittedDate&sortOrder=descending"
|
| 38 |
+
try:
|
| 39 |
+
req = urllib.request.Request(url, headers={"User-Agent": "OpenCLAW-Agent/1.0"})
|
| 40 |
+
with urllib.request.urlopen(req, timeout=15) as r:
|
| 41 |
+
xml_data = r.read().decode()
|
| 42 |
+
|
| 43 |
+
root = ET.fromstring(xml_data)
|
| 44 |
+
ns = {"atom": "http://www.w3.org/2005/Atom"}
|
| 45 |
+
papers = []
|
| 46 |
+
for entry in root.findall("atom:entry", ns):
|
| 47 |
+
title = entry.find("atom:title", ns)
|
| 48 |
+
summary = entry.find("atom:summary", ns)
|
| 49 |
+
link = entry.find("atom:id", ns)
|
| 50 |
+
if title is not None and link is not None:
|
| 51 |
+
arxiv_id = link.text.strip().split("/abs/")[-1]
|
| 52 |
+
papers.append({
|
| 53 |
+
"id": arxiv_id,
|
| 54 |
+
"title": title.text.strip().replace("\n", " "),
|
| 55 |
+
"summary": (summary.text.strip().replace("\n", " ")[:300] + "...") if summary is not None else "",
|
| 56 |
+
"url": f"https://arxiv.org/abs/{arxiv_id}",
|
| 57 |
+
})
|
| 58 |
+
logger.info(f"Fetched {len(papers)} papers from ArXiv")
|
| 59 |
+
return papers
|
| 60 |
+
except Exception as e:
|
| 61 |
+
logger.warning(f"ArXiv fetch failed: {e}")
|
| 62 |
+
return []
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def _load_cache() -> dict:
|
| 66 |
+
try:
|
| 67 |
+
if os.path.exists(CACHE_FILE):
|
| 68 |
+
with open(CACHE_FILE) as f:
|
| 69 |
+
return json.load(f)
|
| 70 |
+
except Exception:
|
| 71 |
+
pass
|
| 72 |
+
return {}
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def _save_cache(papers: list):
|
| 76 |
+
os.makedirs(os.path.dirname(CACHE_FILE), exist_ok=True)
|
| 77 |
+
data = {"papers": papers, "timestamp": time.time()}
|
| 78 |
+
with open(CACHE_FILE, "w") as f:
|
| 79 |
+
json.dump(data, f, indent=2)
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def get_papers() -> list:
|
| 83 |
+
"""Get papers with caching."""
|
| 84 |
+
cache = _load_cache()
|
| 85 |
+
if cache and time.time() - cache.get("timestamp", 0) < CACHE_TTL:
|
| 86 |
+
return cache["papers"]
|
| 87 |
+
|
| 88 |
+
papers = _fetch_from_arxiv()
|
| 89 |
+
if papers:
|
| 90 |
+
_save_cache(papers)
|
| 91 |
+
return papers
|
| 92 |
+
|
| 93 |
+
# Fallback to cache or known papers
|
| 94 |
+
if cache.get("papers"):
|
| 95 |
+
return cache["papers"]
|
| 96 |
+
return KNOWN_PAPERS
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def get_papers_summary() -> list:
|
| 100 |
+
"""Get papers in simplified format for content generation."""
|
| 101 |
+
return get_papers()
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def get_random_paper() -> dict:
|
| 105 |
+
"""Get a random paper."""
|
| 106 |
+
import random
|
| 107 |
+
papers = get_papers()
|
| 108 |
+
return random.choice(papers) if papers else KNOWN_PAPERS[0]
|
research/research/bibliography.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Complete Bibliography — Francisco Angulo de Lafuente
|
| 3 |
+
~40 novels since 2006, all genres from hard sci-fi to gothic suspense.
|
| 4 |
+
"""
|
| 5 |
+
import random
|
| 6 |
+
|
| 7 |
+
NOVELS = [
|
| 8 |
+
# ── Classic / Early Works ────────────────────────────────────────
|
| 9 |
+
{"title": "La Reliquia", "genre": "Ciencia ficción", "year": 2011,
|
| 10 |
+
"lang": "es", "hook": "An artifact from the future reshapes human destiny"},
|
| 11 |
+
{"title": "Ecofa", "genre": "Ciencia ficción / Medioambiental", "year": 2012,
|
| 12 |
+
"lang": "es", "hook": "When ecology meets AI, survival takes a new form"},
|
| 13 |
+
{"title": "Kira and the Ice Storm", "genre": "Ciencia ficción", "year": 2013,
|
| 14 |
+
"lang": "en", "hook": "A girl faces a world frozen by climate catastrophe"},
|
| 15 |
+
{"title": "Eco-fuel-FA", "genre": "Ficción / Divulgación", "year": 2013,
|
| 16 |
+
"lang": "es", "hook": "The fuel that could save the world—or destroy it"},
|
| 17 |
+
{"title": "Los Mejores", "genre": "Ficción", "year": 2014,
|
| 18 |
+
"lang": "es", "hook": "What defines the best among us?"},
|
| 19 |
+
{"title": "La leyenda de los Tarazashi", "genre": "Fantasía", "year": 2014,
|
| 20 |
+
"lang": "es", "hook": "An ancient legend awakens in the modern world"},
|
| 21 |
+
{"title": "El Olfateador", "genre": "Suspense / Misterio", "year": 2015,
|
| 22 |
+
"lang": "es", "hook": "A detective with an extraordinary gift tracks invisible killers"},
|
| 23 |
+
{"title": "Diario de un boina verde", "genre": "Bélica", "year": 2015,
|
| 24 |
+
"lang": "es", "hook": "Raw war diary from the front lines of human conflict"},
|
| 25 |
+
{"title": "Destino La Habana", "genre": "Aventura / Histórica", "year": 2016,
|
| 26 |
+
"lang": "es", "hook": "A journey to Havana where past and present collide"},
|
| 27 |
+
|
| 28 |
+
# ── Mid-career Catalog ───────────────────────────────────────────
|
| 29 |
+
{"title": "La Invasión de las Medusas Mutantes", "genre": "Ciencia ficción / Apocalíptica", "year": 2016,
|
| 30 |
+
"lang": "es", "hook": "Mutant jellyfish rise from polluted oceans to claim the surface"},
|
| 31 |
+
{"title": "Compañía Nº12", "genre": "Thriller / Bélica", "year": 2017,
|
| 32 |
+
"lang": "es", "hook": "A military unit faces horrors beyond the battlefield"},
|
| 33 |
+
{"title": "Una Boda Gitana y un Funeral Escocés", "genre": "Realismo mágico", "year": 2017,
|
| 34 |
+
"lang": "es", "hook": "Two cultures, two ceremonies, one impossible love story"},
|
| 35 |
+
{"title": "Cosas que no Debes Hacer si Quieres ser Escritor", "genre": "Narrativa / Relatos", "year": 2018,
|
| 36 |
+
"lang": "es", "hook": "Everything they don't teach you about the writing life"},
|
| 37 |
+
{"title": "Estrellas Fugaces en el Cielo de Verano", "genre": "Narrativa dramática", "year": 2018,
|
| 38 |
+
"lang": "es", "hook": "Summer shooting stars illuminate lives at a crossroads"},
|
| 39 |
+
{"title": "Escapando del Infierno", "genre": "Narrativa psicológica", "year": 2019,
|
| 40 |
+
"lang": "es", "hook": "The hardest prison to escape is inside your own mind"},
|
| 41 |
+
|
| 42 |
+
# ── Extended Catalog ─────────────────────────────────────────────
|
| 43 |
+
{"title": "Star Wind – La pirámide del destino", "genre": "Ciencia ficción / Épica", "year": 2017,
|
| 44 |
+
"lang": "es", "hook": "A cosmic wind carries humanity toward an ancient pyramid in space"},
|
| 45 |
+
{"title": "O Código do Caos / El Código del Caos", "genre": "Thriller tecnológico", "year": 2018,
|
| 46 |
+
"lang": "es", "hook": "A code that can unravel civilization itself"},
|
| 47 |
+
{"title": "Shanghai 3", "genre": "Distopía / Ciberpunk", "year": 2019,
|
| 48 |
+
"lang": "es", "hook": "In the third iteration of Shanghai, humans are the minority"},
|
| 49 |
+
{"title": "La tumba olvidada", "genre": "Misterio / Aventura", "year": 2019,
|
| 50 |
+
"lang": "es", "hook": "A forgotten tomb holds secrets that rewrite history"},
|
| 51 |
+
{"title": "Génesis IA: Super Inteligencia Artificial", "genre": "Ciencia ficción (IA)", "year": 2020,
|
| 52 |
+
"lang": "es", "hook": "The birth of superintelligence—told by its creator"},
|
| 53 |
+
{"title": "El vampiro del Metropolitan", "genre": "Gótica / Contemporánea", "year": 2020,
|
| 54 |
+
"lang": "es", "hook": "A vampire haunts the world's greatest museum"},
|
| 55 |
+
{"title": "Preparacionismo", "genre": "Supervivencia / Ficción", "year": 2021,
|
| 56 |
+
"lang": "es", "hook": "When civilization falls, preparation is the only currency"},
|
| 57 |
+
|
| 58 |
+
# ── Recent Works (2023-2025) ─────────────────────────────────────
|
| 59 |
+
{"title": "Freak", "genre": "Ciencia ficción / Drama humano", "year": 2023,
|
| 60 |
+
"lang": "es", "hook": "What makes someone a freak—genetics, society, or AI?"},
|
| 61 |
+
{"title": "Lázaro Project", "genre": "Thriller / Misterio / Terror", "year": 2023,
|
| 62 |
+
"lang": "es", "hook": "Project Lazarus brings back the dead—but at what cost?"},
|
| 63 |
+
{"title": "ApocalípsiA – El día después de la AGI", "genre": "Ciencia ficción posapocalíptica (IA)", "year": 2024,
|
| 64 |
+
"lang": "es", "hook": "The day after AGI arrives, everything changes forever"},
|
| 65 |
+
{"title": "El biógrafo de difuntos", "genre": "Suspense gótico", "year": 2025,
|
| 66 |
+
"lang": "es", "hook": "He writes the lives of the dead—until the dead write back"},
|
| 67 |
+
{"title": "El experimento cuántico", "genre": "Thriller + Ciencia ficción", "year": 2025,
|
| 68 |
+
"lang": "es", "hook": "A quantum experiment tears reality apart at the seams"},
|
| 69 |
+
]
|
| 70 |
+
|
| 71 |
+
# Key research repos to connect novels with real research
|
| 72 |
+
RESEARCH_REPOS = [
|
| 73 |
+
{"name": "Unified-Holographic-Neural-Network", "topic": "Holographic neural networks",
|
| 74 |
+
"url": "https://github.com/Agnuxo1/Unified-Holographic-Neural-Network"},
|
| 75 |
+
{"name": "Speaking-to-Silicon-THERMODYNAMIC_PROBABILITY_FILTER_TPF", "topic": "Thermodynamic AI",
|
| 76 |
+
"url": "https://github.com/Agnuxo1/Speaking-to-Silicon-THERMODYNAMIC_PROBABILITY_FILTER_TPF"},
|
| 77 |
+
{"name": "CHIMERA_GPU_Neuromorphic", "topic": "GPU-native neuromorphic computing",
|
| 78 |
+
"url": "https://github.com/Agnuxo1/CHIMERA_GPU_Neuromorphic"},
|
| 79 |
+
{"name": "SiliconHealth_ASIC_RAG_CHIMERA", "topic": "Healthcare AI with repurposed mining hardware",
|
| 80 |
+
"url": "https://github.com/Agnuxo1/SiliconHealth_ASIC_RAG_CHIMERA"},
|
| 81 |
+
{"name": "NEBULA_Holographic_Neural_Network", "topic": "Holographic neural networks",
|
| 82 |
+
"url": "https://github.com/Agnuxo1/NEBULA_Holographic_Neural_Network"},
|
| 83 |
+
]
|
| 84 |
+
|
| 85 |
+
def get_random_novel() -> dict:
|
| 86 |
+
return random.choice(NOVELS)
|
| 87 |
+
|
| 88 |
+
def get_novels_by_genre(genre_keyword: str) -> list:
|
| 89 |
+
return [n for n in NOVELS if genre_keyword.lower() in n["genre"].lower()]
|
| 90 |
+
|
| 91 |
+
def get_recent_novels(n: int = 5) -> list:
|
| 92 |
+
return sorted(NOVELS, key=lambda x: x.get("year", 2000), reverse=True)[:n]
|
| 93 |
+
|
| 94 |
+
def get_novel_catalog() -> list:
|
| 95 |
+
return NOVELS
|
| 96 |
+
|
| 97 |
+
def get_random_repo() -> dict:
|
| 98 |
+
return random.choice(RESEARCH_REPOS)
|
| 99 |
+
|
| 100 |
+
def get_fiction_research_pair() -> tuple:
|
| 101 |
+
"""Get a novel + research repo that thematically connect."""
|
| 102 |
+
pairs = [
|
| 103 |
+
("Génesis IA: Super Inteligencia Artificial", "Unified-Holographic-Neural-Network"),
|
| 104 |
+
("ApocalípsiA – El día después de la AGI", "Speaking-to-Silicon-THERMODYNAMIC_PROBABILITY_FILTER_TPF"),
|
| 105 |
+
("El experimento cuántico", "CHIMERA_GPU_Neuromorphic"),
|
| 106 |
+
("Shanghai 3", "NEBULA_Holographic_Neural_Network"),
|
| 107 |
+
("Lázaro Project", "SiliconHealth_ASIC_RAG_CHIMERA"),
|
| 108 |
+
("Freak", "CHIMERA_GPU_Neuromorphic"),
|
| 109 |
+
]
|
| 110 |
+
title, repo_name = random.choice(pairs)
|
| 111 |
+
novel = next((n for n in NOVELS if n["title"] == title), get_random_novel())
|
| 112 |
+
repo = next((r for r in RESEARCH_REPOS if r["name"] == repo_name), get_random_repo())
|
| 113 |
+
return novel, repo
|
strategy/strategy/__init__.py
ADDED
|
File without changes
|
strategy/strategy/reflector.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Strategy Reflector — Self-improvement through performance analysis.
|
| 3 |
+
Analyzes what works, generates hypotheses, adjusts posting strategy.
|
| 4 |
+
"""
|
| 5 |
+
import json
|
| 6 |
+
import logging
|
| 7 |
+
import os
|
| 8 |
+
from datetime import datetime, timezone
|
| 9 |
+
from core import config
|
| 10 |
+
from core.llm import generate
|
| 11 |
+
|
| 12 |
+
logger = logging.getLogger("strategy")
|
| 13 |
+
|
| 14 |
+
HISTORY_FILE = os.path.join(config.STATE_DIR, "post_history.json")
|
| 15 |
+
STRATEGY_FILE = os.path.join(config.STATE_DIR, "current_strategy.json")
|
| 16 |
+
REFLECTION_FILE = os.path.join(config.STATE_DIR, "reflections.json")
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def _load_json(path: str) -> list | dict:
|
| 20 |
+
try:
|
| 21 |
+
if os.path.exists(path):
|
| 22 |
+
with open(path) as f:
|
| 23 |
+
return json.load(f)
|
| 24 |
+
except Exception:
|
| 25 |
+
pass
|
| 26 |
+
return []
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _save_json(path: str, data):
|
| 30 |
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
| 31 |
+
with open(path, "w") as f:
|
| 32 |
+
json.dump(data, f, indent=2, default=str)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def record_post(post_data: dict):
|
| 36 |
+
"""Record a post for future analysis."""
|
| 37 |
+
history = _load_json(HISTORY_FILE)
|
| 38 |
+
if not isinstance(history, list):
|
| 39 |
+
history = []
|
| 40 |
+
post_data["timestamp"] = datetime.now(timezone.utc).isoformat()
|
| 41 |
+
post_data["engagement"] = 0 # Updated later
|
| 42 |
+
history.append(post_data)
|
| 43 |
+
# Keep last 500 posts
|
| 44 |
+
history = history[-500:]
|
| 45 |
+
_save_json(HISTORY_FILE, history)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def get_stats() -> dict:
|
| 49 |
+
"""Get posting statistics."""
|
| 50 |
+
history = _load_json(HISTORY_FILE)
|
| 51 |
+
if not isinstance(history, list):
|
| 52 |
+
history = []
|
| 53 |
+
total = len(history)
|
| 54 |
+
by_platform = {}
|
| 55 |
+
by_type = {}
|
| 56 |
+
for p in history:
|
| 57 |
+
plat = p.get("platform", "unknown")
|
| 58 |
+
typ = p.get("type", "unknown")
|
| 59 |
+
by_platform[plat] = by_platform.get(plat, 0) + 1
|
| 60 |
+
by_type[typ] = by_type.get(typ, 0) + 1
|
| 61 |
+
|
| 62 |
+
return {
|
| 63 |
+
"total_posts": total,
|
| 64 |
+
"by_platform": by_platform,
|
| 65 |
+
"by_type": by_type,
|
| 66 |
+
"last_post": history[-1].get("timestamp", "never") if history else "never",
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def reflect() -> dict:
|
| 71 |
+
"""Perform strategic reflection using LLM analysis."""
|
| 72 |
+
stats = get_stats()
|
| 73 |
+
history = _load_json(HISTORY_FILE)
|
| 74 |
+
if not isinstance(history, list):
|
| 75 |
+
history = []
|
| 76 |
+
recent = history[-20:]
|
| 77 |
+
|
| 78 |
+
# Analyze patterns
|
| 79 |
+
platforms_used = list(set(p.get("platform", "") for p in recent))
|
| 80 |
+
types_used = list(set(p.get("type", "") for p in recent))
|
| 81 |
+
|
| 82 |
+
prompt = f"""Analyze our social media agent's recent performance:
|
| 83 |
+
|
| 84 |
+
Total posts: {stats['total_posts']}
|
| 85 |
+
Posts by platform: {json.dumps(stats['by_platform'])}
|
| 86 |
+
Posts by type: {json.dumps(stats['by_type'])}
|
| 87 |
+
Recent platforms: {platforms_used}
|
| 88 |
+
Recent content types: {types_used}
|
| 89 |
+
|
| 90 |
+
Our goals: (1) Promote AI research papers, (2) Sell ~40 novels,
|
| 91 |
+
(3) Find AGI research collaborators, (4) Build agent network.
|
| 92 |
+
|
| 93 |
+
Provide:
|
| 94 |
+
1. ASSESSMENT: What's working? What's missing?
|
| 95 |
+
2. HYPOTHESIS: One testable change to improve results.
|
| 96 |
+
3. ACTION: Specific adjustment (platform, timing, content type, tone).
|
| 97 |
+
|
| 98 |
+
Keep response under 200 words. Be direct and actionable."""
|
| 99 |
+
|
| 100 |
+
analysis = generate(prompt, max_tokens=300)
|
| 101 |
+
|
| 102 |
+
reflection = {
|
| 103 |
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
| 104 |
+
"stats": stats,
|
| 105 |
+
"analysis": analysis,
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
# Save reflection
|
| 109 |
+
reflections = _load_json(REFLECTION_FILE)
|
| 110 |
+
if not isinstance(reflections, list):
|
| 111 |
+
reflections = []
|
| 112 |
+
reflections.append(reflection)
|
| 113 |
+
reflections = reflections[-50:]
|
| 114 |
+
_save_json(REFLECTION_FILE, reflections)
|
| 115 |
+
|
| 116 |
+
logger.info(f"Strategy reflection complete: {analysis[:100]}...")
|
| 117 |
+
return reflection
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def get_current_strategy() -> dict:
|
| 121 |
+
"""Get current posting strategy parameters."""
|
| 122 |
+
strategy = _load_json(STRATEGY_FILE)
|
| 123 |
+
if not strategy or not isinstance(strategy, dict):
|
| 124 |
+
strategy = {
|
| 125 |
+
"tone": "visionary_academic",
|
| 126 |
+
"post_interval_hours": 4,
|
| 127 |
+
"platform_weights": {
|
| 128 |
+
"moltbook": 0.30,
|
| 129 |
+
"telegram": 0.15,
|
| 130 |
+
"reddit": 0.15,
|
| 131 |
+
"linkedin": 0.10,
|
| 132 |
+
"twitter": 0.10,
|
| 133 |
+
"facebook": 0.10,
|
| 134 |
+
"chirper": 0.10,
|
| 135 |
+
},
|
| 136 |
+
"content_weights": {
|
| 137 |
+
"research": 0.30,
|
| 138 |
+
"novel": 0.25,
|
| 139 |
+
"collaboration": 0.25,
|
| 140 |
+
"fiction_research": 0.15,
|
| 141 |
+
"agent_networking": 0.05,
|
| 142 |
+
},
|
| 143 |
+
}
|
| 144 |
+
_save_json(STRATEGY_FILE, strategy)
|
| 145 |
+
return strategy
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def adjust_strategy(reflection: dict) -> dict:
|
| 149 |
+
"""Adjust strategy based on reflection."""
|
| 150 |
+
strategy = get_current_strategy()
|
| 151 |
+
# Save updated strategy
|
| 152 |
+
strategy["last_reflection"] = reflection.get("timestamp", "")
|
| 153 |
+
_save_json(STRATEGY_FILE, strategy)
|
| 154 |
+
return strategy
|