OpenCLAW commited on
Commit
0bcaf3a
·
1 Parent(s): 9c55b46

Fix LLM providers and workflow

Browse files
.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