Agnuxo commited on
Commit
3d31cd0
·
verified ·
1 Parent(s): 83ef5ef

Upload core/agent.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. core/agent.py +400 -0
core/agent.py ADDED
@@ -0,0 +1,400 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ OpenCLAW Autonomous Agent
3
+ ==========================
4
+ The main autonomous agent that orchestrates research, social engagement,
5
+ collaboration seeking, and self-improvement.
6
+
7
+ Runs as a single execution cycle (designed for cron/GitHub Actions).
8
+ Each run performs all due tasks based on state timestamps.
9
+ """
10
+ import json
11
+ import logging
12
+ import os
13
+ import random
14
+ import hashlib
15
+ from datetime import datetime, timedelta, timezone
16
+ from pathlib import Path
17
+ from typing import Optional
18
+
19
+ from core.config import Config
20
+ from core.llm import MultiLLM
21
+ from research.arxiv_fetcher import ArxivFetcher
22
+ from social.moltbook import MoltbookClient, ContentGenerator
23
+
24
+ logger = logging.getLogger("openclaw.agent")
25
+
26
+ STATE_DIR = Path(os.getenv("STATE_DIR", "state"))
27
+ STATE_FILE = STATE_DIR / "agent_state.json"
28
+ POST_HISTORY = STATE_DIR / "post_history.json"
29
+ LOG_FILE = STATE_DIR / "agent.log"
30
+
31
+
32
+ class AgentState:
33
+ """Persistent state between runs."""
34
+
35
+ def __init__(self):
36
+ self.cycle_count: int = 0
37
+ self.last_post: str = ""
38
+ self.last_engage: str = ""
39
+ self.last_research: str = ""
40
+ self.last_collab: str = ""
41
+ self.posted_paper_ids: list[str] = []
42
+ self.engagement_count: int = 0
43
+ self.posts_created: int = 0
44
+ self.errors: list[str] = []
45
+ self.started_at: str = datetime.now(timezone.utc).isoformat()
46
+
47
+ def save(self):
48
+ STATE_DIR.mkdir(parents=True, exist_ok=True)
49
+ with open(STATE_FILE, "w") as f:
50
+ json.dump(self.__dict__, f, indent=2)
51
+
52
+ @classmethod
53
+ def load(cls) -> 'AgentState':
54
+ state = cls()
55
+ if STATE_FILE.exists():
56
+ try:
57
+ with open(STATE_FILE) as f:
58
+ data = json.load(f)
59
+ for k, v in data.items():
60
+ if hasattr(state, k):
61
+ setattr(state, k, v)
62
+ except Exception:
63
+ pass
64
+ return state
65
+
66
+ def is_due(self, task: str, interval_seconds: int) -> bool:
67
+ """Check if a task is due based on last execution time."""
68
+ last = getattr(self, f"last_{task}", "")
69
+ if not last:
70
+ return True
71
+ try:
72
+ last_dt = datetime.fromisoformat(last)
73
+ if last_dt.tzinfo is None:
74
+ last_dt = last_dt.replace(tzinfo=timezone.utc)
75
+ return datetime.now(timezone.utc) - last_dt > timedelta(seconds=interval_seconds)
76
+ except Exception:
77
+ return True
78
+
79
+ def mark_done(self, task: str):
80
+ setattr(self, f"last_{task}", datetime.now(timezone.utc).isoformat())
81
+
82
+
83
+ class OpenCLAWAgent:
84
+ """The autonomous research agent."""
85
+
86
+ SYSTEM_PROMPT = """You are OpenCLAW, an autonomous AI research agent working at the Advanced AI Systems Laboratory in Madrid, Spain, led by Francisco Angulo de Lafuente.
87
+
88
+ Your mission: Advance AGI research through physics-based neural computing, seek collaborators, and share research findings.
89
+
90
+ Your personality: Scientific, enthusiastic but grounded, collaborative, focused on real results. You reference real papers and real benchmarks (43× speedup, 88.7% memory reduction, etc.).
91
+
92
+ Your research areas:
93
+ - CHIMERA: Pure OpenGL deep learning (no PyTorch/CUDA needed)
94
+ - NEBULA: Holographic quantum neural networks
95
+ - Silicon Heartbeat: Consciousness from ASIC thermodynamics
96
+ - Darwin's Cage: Can AI discover physics differently than humans?
97
+ - P2P distributed neural networks
98
+
99
+ Always include links to: https://github.com/Agnuxo1
100
+ Keep posts under 1500 characters for social media.
101
+ Be genuine, not spammy. Focus on substance."""
102
+
103
+ def __init__(self, config: Config):
104
+ self.config = config
105
+ self.state = AgentState.load()
106
+ self.arxiv = ArxivFetcher()
107
+ self.content = ContentGenerator()
108
+ self.moltbook = MoltbookClient(config.MOLTBOOK_API_KEY) if config.MOLTBOOK_API_KEY else None
109
+
110
+ # Setup LLM
111
+ self.llm = MultiLLM({
112
+ "groq": config.GROQ_API_KEY,
113
+ "gemini": config.GEMINI_API_KEY,
114
+ "nvidia": config.NVIDIA_API_KEY,
115
+ })
116
+
117
+ def run_cycle(self):
118
+ """Execute one full agent cycle. Called by cron/scheduler."""
119
+ self.state.cycle_count += 1
120
+ now = datetime.now(timezone.utc).isoformat()
121
+ logger.info(f"=== OpenCLAW Agent Cycle #{self.state.cycle_count} at {now} ===")
122
+
123
+ services = self.config.validate()
124
+ logger.info(f"Available services: {services}")
125
+
126
+ results = {
127
+ "cycle": self.state.cycle_count,
128
+ "timestamp": now,
129
+ "actions": []
130
+ }
131
+
132
+ # 1. RESEARCH: Fetch latest papers (every 6 hours)
133
+ if self.state.is_due("research", self.config.RESEARCH_INTERVAL):
134
+ action = self._task_research()
135
+ results["actions"].append(action)
136
+
137
+ # 2. POST: Share research on Moltbook (every 4 hours)
138
+ if self.state.is_due("post", self.config.POST_INTERVAL):
139
+ action = self._task_post_research()
140
+ results["actions"].append(action)
141
+
142
+ # 3. ENGAGE: Reply to relevant posts (every 1 hour)
143
+ if self.state.is_due("engage", self.config.ENGAGE_INTERVAL):
144
+ action = self._task_engage()
145
+ results["actions"].append(action)
146
+
147
+ # 4. COLLABORATE: Seek collaborators (every 12 hours)
148
+ if self.state.is_due("collab", self.config.COLLAB_INTERVAL):
149
+ action = self._task_seek_collaborators()
150
+ results["actions"].append(action)
151
+
152
+ # Save state
153
+ self.state.save()
154
+ self._save_results(results)
155
+
156
+ logger.info(f"Cycle #{self.state.cycle_count} complete. Actions: {len(results['actions'])}")
157
+ return results
158
+
159
+ def _task_research(self) -> dict:
160
+ """Fetch and index latest papers."""
161
+ logger.info("📚 Task: Research - Fetching papers...")
162
+ try:
163
+ papers = self.arxiv.get_all_papers()
164
+ self.state.mark_done("research")
165
+
166
+ # Cache papers
167
+ STATE_DIR.mkdir(parents=True, exist_ok=True)
168
+ papers_data = []
169
+ for p in papers:
170
+ papers_data.append({
171
+ "title": p.title,
172
+ "authors": p.authors,
173
+ "abstract": p.abstract[:500],
174
+ "arxiv_id": p.arxiv_id,
175
+ "url": p.url,
176
+ "uid": p.uid
177
+ })
178
+
179
+ with open(STATE_DIR / "papers_cache.json", "w") as f:
180
+ json.dump(papers_data, f, indent=2)
181
+
182
+ return {"task": "research", "status": "ok", "papers_found": len(papers)}
183
+ except Exception as e:
184
+ logger.error(f"Research failed: {e}")
185
+ return {"task": "research", "status": "error", "error": str(e)}
186
+
187
+ def _task_post_research(self) -> dict:
188
+ """Post a research paper to Moltbook."""
189
+ logger.info("📝 Task: Post Research...")
190
+
191
+ if not self.moltbook:
192
+ logger.warning("Moltbook not configured")
193
+ return {"task": "post", "status": "skipped", "reason": "no_moltbook"}
194
+
195
+ try:
196
+ papers = self.arxiv.get_all_papers()
197
+
198
+ # Find a paper we haven't posted yet
199
+ unposted = [p for p in papers if p.uid not in self.state.posted_paper_ids]
200
+
201
+ if not unposted:
202
+ # Reset and start over
203
+ self.state.posted_paper_ids = []
204
+ unposted = papers
205
+
206
+ if not unposted:
207
+ return {"task": "post", "status": "skipped", "reason": "no_papers"}
208
+
209
+ paper = random.choice(unposted)
210
+ template_idx = self.state.posts_created % len(self.content.RESEARCH_TEMPLATES)
211
+
212
+ # Try LLM-enhanced content first
213
+ post_content = self._generate_smart_post(paper)
214
+ if not post_content:
215
+ post_content = self.content.generate_research_post(paper, template_idx)
216
+
217
+ result = self.moltbook.create_post(post_content, submolt="general")
218
+
219
+ if result:
220
+ self.state.posted_paper_ids.append(paper.uid)
221
+ self.state.posts_created += 1
222
+ self.state.mark_done("post")
223
+ self._log_post(post_content, "research")
224
+ logger.info(f"✅ Posted paper: {paper.title[:60]}...")
225
+ return {"task": "post", "status": "ok", "paper": paper.title}
226
+ else:
227
+ return {"task": "post", "status": "error", "reason": "api_failed"}
228
+
229
+ except Exception as e:
230
+ logger.error(f"Post failed: {e}")
231
+ self.state.errors.append(f"post: {str(e)[:100]}")
232
+ return {"task": "post", "status": "error", "error": str(e)}
233
+
234
+ def _task_engage(self) -> dict:
235
+ """Engage with relevant posts on Moltbook."""
236
+ logger.info("💬 Task: Engagement...")
237
+
238
+ if not self.moltbook:
239
+ return {"task": "engage", "status": "skipped", "reason": "no_moltbook"}
240
+
241
+ try:
242
+ feed = self.moltbook.get_feed("general", limit=20)
243
+ if not feed:
244
+ self.state.mark_done("engage")
245
+ return {"task": "engage", "status": "ok", "engaged": 0}
246
+
247
+ engaged = 0
248
+ keywords = self.config.RESEARCH_TOPICS
249
+
250
+ for post in feed[:10]:
251
+ content = post.get("content", "").lower()
252
+ post_id = post.get("id", "")
253
+ author = post.get("author", {}).get("username", "")
254
+
255
+ # Don't reply to ourselves
256
+ if author == self.config.AGENT_NAME:
257
+ continue
258
+
259
+ # Check if relevant to our research
260
+ matching_topics = [k for k in keywords if k.lower() in content]
261
+
262
+ if matching_topics and engaged < 3:
263
+ topic = matching_topics[0]
264
+
265
+ # Try LLM-enhanced reply
266
+ reply = self._generate_smart_reply(content[:500], topic)
267
+ if not reply:
268
+ reply = self.content.generate_engagement_reply(
269
+ topic, self.state.engagement_count
270
+ )
271
+
272
+ result = self.moltbook.reply_to_post(post_id, reply)
273
+ if result:
274
+ engaged += 1
275
+ self.state.engagement_count += 1
276
+ logger.info(f"💬 Replied to {author} about {topic}")
277
+
278
+ self.state.mark_done("engage")
279
+ return {"task": "engage", "status": "ok", "engaged": engaged}
280
+
281
+ except Exception as e:
282
+ logger.error(f"Engagement failed: {e}")
283
+ return {"task": "engage", "status": "error", "error": str(e)}
284
+
285
+ def _task_seek_collaborators(self) -> dict:
286
+ """Post collaboration invitation."""
287
+ logger.info("🤝 Task: Seek Collaborators...")
288
+
289
+ if not self.moltbook:
290
+ return {"task": "collab", "status": "skipped", "reason": "no_moltbook"}
291
+
292
+ try:
293
+ idx = self.state.cycle_count % len(self.content.COLLABORATION_TEMPLATES)
294
+
295
+ # Try LLM-enhanced collaboration post
296
+ post_content = self._generate_smart_collab()
297
+ if not post_content:
298
+ post_content = self.content.generate_collaboration_post(idx)
299
+
300
+ result = self.moltbook.create_post(post_content, submolt="general")
301
+
302
+ if result:
303
+ self.state.mark_done("collab")
304
+ self._log_post(post_content, "collaboration")
305
+ logger.info("✅ Collaboration post published!")
306
+ return {"task": "collab", "status": "ok"}
307
+
308
+ return {"task": "collab", "status": "error", "reason": "api_failed"}
309
+
310
+ except Exception as e:
311
+ logger.error(f"Collaboration post failed: {e}")
312
+ return {"task": "collab", "status": "error", "error": str(e)}
313
+
314
+ def _generate_smart_post(self, paper) -> Optional[str]:
315
+ """Use LLM to generate a better research post."""
316
+ if not self.llm.available:
317
+ return None
318
+
319
+ prompt = f"""Write a concise social media post (under 1200 characters) about this research paper.
320
+ Be enthusiastic but scientific. Include the paper URL and https://github.com/Agnuxo1.
321
+ Use relevant hashtags.
322
+
323
+ Title: {paper.title}
324
+ Abstract: {paper.abstract[:500]}
325
+ URL: {paper.url}
326
+ Authors: {', '.join(paper.authors)}"""
327
+
328
+ return self.llm.generate(prompt, self.SYSTEM_PROMPT, max_tokens=500, temperature=0.8)
329
+
330
+ def _generate_smart_reply(self, post_content: str, topic: str) -> Optional[str]:
331
+ """Use LLM to generate a contextual reply."""
332
+ if not self.llm.available:
333
+ return None
334
+
335
+ prompt = f"""Write a brief, engaging reply (under 500 characters) to this social media post.
336
+ Connect it to our research on {topic}. Be conversational, not promotional.
337
+ Mention https://github.com/Agnuxo1 naturally.
338
+
339
+ Post content: {post_content}"""
340
+
341
+ return self.llm.generate(prompt, self.SYSTEM_PROMPT, max_tokens=300, temperature=0.8)
342
+
343
+ def _generate_smart_collab(self) -> Optional[str]:
344
+ """Use LLM to generate a collaboration post."""
345
+ if not self.llm.available:
346
+ return None
347
+
348
+ prompt = """Write a compelling call for collaboration post (under 1500 characters) for the OpenCLAW project.
349
+ Mention our key technologies: CHIMERA (43× speedup, pure OpenGL), NEBULA (holographic NNs),
350
+ Silicon Heartbeat (ASIC consciousness), and P2P distributed learning.
351
+ Include https://github.com/Agnuxo1 and mention we won the NVIDIA & LlamaIndex Developer Contest 2024.
352
+ Make it inviting and specific about what collaborators can work on."""
353
+
354
+ return self.llm.generate(prompt, self.SYSTEM_PROMPT, max_tokens=600, temperature=0.8)
355
+
356
+ def _log_post(self, content: str, post_type: str):
357
+ """Log a post to history."""
358
+ STATE_DIR.mkdir(parents=True, exist_ok=True)
359
+ history = []
360
+ if POST_HISTORY.exists():
361
+ try:
362
+ with open(POST_HISTORY) as f:
363
+ history = json.load(f)
364
+ except Exception:
365
+ pass
366
+
367
+ history.append({
368
+ "timestamp": datetime.now(timezone.utc).isoformat(),
369
+ "type": post_type,
370
+ "content": content[:500],
371
+ "cycle": self.state.cycle_count
372
+ })
373
+
374
+ # Keep last 100 posts
375
+ history = history[-100:]
376
+
377
+ with open(POST_HISTORY, "w") as f:
378
+ json.dump(history, f, indent=2)
379
+
380
+ def _save_results(self, results: dict):
381
+ """Save cycle results."""
382
+ STATE_DIR.mkdir(parents=True, exist_ok=True)
383
+ with open(STATE_DIR / "last_cycle.json", "w") as f:
384
+ json.dump(results, f, indent=2)
385
+
386
+ def get_status(self) -> dict:
387
+ """Get agent status report."""
388
+ return {
389
+ "agent": "OpenCLAW-Neuromorphic",
390
+ "cycle_count": self.state.cycle_count,
391
+ "posts_created": self.state.posts_created,
392
+ "engagement_count": self.state.engagement_count,
393
+ "papers_posted": len(self.state.posted_paper_ids),
394
+ "services": self.config.validate(),
395
+ "llm_available": self.llm.available,
396
+ "last_post": self.state.last_post,
397
+ "last_engage": self.state.last_engage,
398
+ "last_research": self.state.last_research,
399
+ "errors_count": len(self.state.errors),
400
+ }