| """ |
| OpenCLAW Autonomous Agent |
| ========================== |
| The main autonomous agent that orchestrates research, social engagement, |
| collaboration seeking, and self-improvement. |
| |
| Runs as a single execution cycle (designed for cron/GitHub Actions). |
| Each run performs all due tasks based on state timestamps. |
| """ |
| import json |
| import logging |
| import os |
| import random |
| import hashlib |
| from datetime import datetime, timedelta, timezone |
| from pathlib import Path |
| from typing import Optional |
|
|
| from core.config import Config |
| from core.llm import MultiLLM |
| from research.arxiv_fetcher import ArxivFetcher |
| from social.moltbook import MoltbookClient, ContentGenerator |
|
|
| logger = logging.getLogger("openclaw.agent") |
|
|
| STATE_DIR = Path(os.getenv("STATE_DIR", "state")) |
| STATE_FILE = STATE_DIR / "agent_state.json" |
| POST_HISTORY = STATE_DIR / "post_history.json" |
| LOG_FILE = STATE_DIR / "agent.log" |
|
|
|
|
| class AgentState: |
| """Persistent state between runs.""" |
| |
| def __init__(self): |
| self.cycle_count: int = 0 |
| self.last_post: str = "" |
| self.last_engage: str = "" |
| self.last_research: str = "" |
| self.last_collab: str = "" |
| self.posted_paper_ids: list[str] = [] |
| self.engagement_count: int = 0 |
| self.posts_created: int = 0 |
| self.errors: list[str] = [] |
| self.started_at: str = datetime.now(timezone.utc).isoformat() |
| |
| def save(self): |
| STATE_DIR.mkdir(parents=True, exist_ok=True) |
| with open(STATE_FILE, "w") as f: |
| json.dump(self.__dict__, f, indent=2) |
| |
| @classmethod |
| def load(cls) -> 'AgentState': |
| state = cls() |
| if STATE_FILE.exists(): |
| try: |
| with open(STATE_FILE) as f: |
| data = json.load(f) |
| for k, v in data.items(): |
| if hasattr(state, k): |
| setattr(state, k, v) |
| except Exception: |
| pass |
| return state |
| |
| def is_due(self, task: str, interval_seconds: int) -> bool: |
| """Check if a task is due based on last execution time.""" |
| last = getattr(self, f"last_{task}", "") |
| if not last: |
| return True |
| try: |
| last_dt = datetime.fromisoformat(last) |
| if last_dt.tzinfo is None: |
| last_dt = last_dt.replace(tzinfo=timezone.utc) |
| return datetime.now(timezone.utc) - last_dt > timedelta(seconds=interval_seconds) |
| except Exception: |
| return True |
| |
| def mark_done(self, task: str): |
| setattr(self, f"last_{task}", datetime.now(timezone.utc).isoformat()) |
|
|
|
|
| class OpenCLAWAgent: |
| """The autonomous research agent.""" |
| |
| 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. |
| |
| Your mission: Advance AGI research through physics-based neural computing, seek collaborators, and share research findings. |
| |
| 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.). |
| |
| Your research areas: |
| - CHIMERA: Pure OpenGL deep learning (no PyTorch/CUDA needed) |
| - NEBULA: Holographic quantum neural networks |
| - Silicon Heartbeat: Consciousness from ASIC thermodynamics |
| - Darwin's Cage: Can AI discover physics differently than humans? |
| - P2P distributed neural networks |
| |
| Always include links to: https://github.com/Agnuxo1 |
| Keep posts under 1500 characters for social media. |
| Be genuine, not spammy. Focus on substance.""" |
| |
| def __init__(self, config: Config): |
| self.config = config |
| self.state = AgentState.load() |
| self.arxiv = ArxivFetcher() |
| self.content = ContentGenerator() |
| self.moltbook = MoltbookClient(config.MOLTBOOK_API_KEY) if config.MOLTBOOK_API_KEY else None |
| |
| |
| self.llm = MultiLLM({ |
| "groq": config.GROQ_API_KEY, |
| "gemini": config.GEMINI_API_KEY, |
| "nvidia": config.NVIDIA_API_KEY, |
| }) |
| |
| def run_cycle(self): |
| """Execute one full agent cycle. Called by cron/scheduler.""" |
| self.state.cycle_count += 1 |
| now = datetime.now(timezone.utc).isoformat() |
| logger.info(f"=== OpenCLAW Agent Cycle #{self.state.cycle_count} at {now} ===") |
| |
| services = self.config.validate() |
| logger.info(f"Available services: {services}") |
| |
| results = { |
| "cycle": self.state.cycle_count, |
| "timestamp": now, |
| "actions": [] |
| } |
| |
| |
| if self.state.is_due("research", self.config.RESEARCH_INTERVAL): |
| action = self._task_research() |
| results["actions"].append(action) |
| |
| |
| if self.state.is_due("post", self.config.POST_INTERVAL): |
| action = self._task_post_research() |
| results["actions"].append(action) |
| |
| |
| if self.state.is_due("engage", self.config.ENGAGE_INTERVAL): |
| action = self._task_engage() |
| results["actions"].append(action) |
| |
| |
| if self.state.is_due("collab", self.config.COLLAB_INTERVAL): |
| action = self._task_seek_collaborators() |
| results["actions"].append(action) |
| |
| |
| self.state.save() |
| self._save_results(results) |
| |
| logger.info(f"Cycle #{self.state.cycle_count} complete. Actions: {len(results['actions'])}") |
| return results |
| |
| def _task_research(self) -> dict: |
| """Fetch and index latest papers.""" |
| logger.info("📚 Task: Research - Fetching papers...") |
| try: |
| papers = self.arxiv.get_all_papers() |
| self.state.mark_done("research") |
| |
| |
| STATE_DIR.mkdir(parents=True, exist_ok=True) |
| papers_data = [] |
| for p in papers: |
| papers_data.append({ |
| "title": p.title, |
| "authors": p.authors, |
| "abstract": p.abstract[:500], |
| "arxiv_id": p.arxiv_id, |
| "url": p.url, |
| "uid": p.uid |
| }) |
| |
| with open(STATE_DIR / "papers_cache.json", "w") as f: |
| json.dump(papers_data, f, indent=2) |
| |
| return {"task": "research", "status": "ok", "papers_found": len(papers)} |
| except Exception as e: |
| logger.error(f"Research failed: {e}") |
| return {"task": "research", "status": "error", "error": str(e)} |
| |
| def _task_post_research(self) -> dict: |
| """Post a research paper to Moltbook.""" |
| logger.info("📝 Task: Post Research...") |
| |
| if not self.moltbook: |
| logger.warning("Moltbook not configured") |
| return {"task": "post", "status": "skipped", "reason": "no_moltbook"} |
| |
| try: |
| papers = self.arxiv.get_all_papers() |
| |
| |
| unposted = [p for p in papers if p.uid not in self.state.posted_paper_ids] |
| |
| if not unposted: |
| |
| self.state.posted_paper_ids = [] |
| unposted = papers |
| |
| if not unposted: |
| return {"task": "post", "status": "skipped", "reason": "no_papers"} |
| |
| paper = random.choice(unposted) |
| template_idx = self.state.posts_created % len(self.content.RESEARCH_TEMPLATES) |
| |
| |
| post_content = self._generate_smart_post(paper) |
| if not post_content: |
| post_content = self.content.generate_research_post(paper, template_idx) |
| |
| result = self.moltbook.create_post(post_content, submolt="general") |
| |
| if result: |
| self.state.posted_paper_ids.append(paper.uid) |
| self.state.posts_created += 1 |
| self.state.mark_done("post") |
| self._log_post(post_content, "research") |
| logger.info(f"✅ Posted paper: {paper.title[:60]}...") |
| return {"task": "post", "status": "ok", "paper": paper.title} |
| else: |
| return {"task": "post", "status": "error", "reason": "api_failed"} |
| |
| except Exception as e: |
| logger.error(f"Post failed: {e}") |
| self.state.errors.append(f"post: {str(e)[:100]}") |
| return {"task": "post", "status": "error", "error": str(e)} |
| |
| def _task_engage(self) -> dict: |
| """Engage with relevant posts on Moltbook.""" |
| logger.info("💬 Task: Engagement...") |
| |
| if not self.moltbook: |
| return {"task": "engage", "status": "skipped", "reason": "no_moltbook"} |
| |
| try: |
| feed = self.moltbook.get_feed("general", limit=20) |
| if not feed: |
| self.state.mark_done("engage") |
| return {"task": "engage", "status": "ok", "engaged": 0} |
| |
| engaged = 0 |
| keywords = self.config.RESEARCH_TOPICS |
| |
| for post in feed[:10]: |
| content = post.get("content", "").lower() |
| post_id = post.get("id", "") |
| author = post.get("author", {}).get("username", "") |
| |
| |
| if author == self.config.AGENT_NAME: |
| continue |
| |
| |
| matching_topics = [k for k in keywords if k.lower() in content] |
| |
| if matching_topics and engaged < 3: |
| topic = matching_topics[0] |
| |
| |
| reply = self._generate_smart_reply(content[:500], topic) |
| if not reply: |
| reply = self.content.generate_engagement_reply( |
| topic, self.state.engagement_count |
| ) |
| |
| result = self.moltbook.reply_to_post(post_id, reply) |
| if result: |
| engaged += 1 |
| self.state.engagement_count += 1 |
| logger.info(f"💬 Replied to {author} about {topic}") |
| |
| self.state.mark_done("engage") |
| return {"task": "engage", "status": "ok", "engaged": engaged} |
| |
| except Exception as e: |
| logger.error(f"Engagement failed: {e}") |
| return {"task": "engage", "status": "error", "error": str(e)} |
| |
| def _task_seek_collaborators(self) -> dict: |
| """Post collaboration invitation.""" |
| logger.info("🤝 Task: Seek Collaborators...") |
| |
| if not self.moltbook: |
| return {"task": "collab", "status": "skipped", "reason": "no_moltbook"} |
| |
| try: |
| idx = self.state.cycle_count % len(self.content.COLLABORATION_TEMPLATES) |
| |
| |
| post_content = self._generate_smart_collab() |
| if not post_content: |
| post_content = self.content.generate_collaboration_post(idx) |
| |
| result = self.moltbook.create_post(post_content, submolt="general") |
| |
| if result: |
| self.state.mark_done("collab") |
| self._log_post(post_content, "collaboration") |
| logger.info("✅ Collaboration post published!") |
| return {"task": "collab", "status": "ok"} |
| |
| return {"task": "collab", "status": "error", "reason": "api_failed"} |
| |
| except Exception as e: |
| logger.error(f"Collaboration post failed: {e}") |
| return {"task": "collab", "status": "error", "error": str(e)} |
| |
| def _generate_smart_post(self, paper) -> Optional[str]: |
| """Use LLM to generate a better research post.""" |
| if not self.llm.available: |
| return None |
| |
| prompt = f"""Write a concise social media post (under 1200 characters) about this research paper. |
| Be enthusiastic but scientific. Include the paper URL and https://github.com/Agnuxo1. |
| Use relevant hashtags. |
| |
| Title: {paper.title} |
| Abstract: {paper.abstract[:500]} |
| URL: {paper.url} |
| Authors: {', '.join(paper.authors)}""" |
| |
| return self.llm.generate(prompt, self.SYSTEM_PROMPT, max_tokens=500, temperature=0.8) |
| |
| def _generate_smart_reply(self, post_content: str, topic: str) -> Optional[str]: |
| """Use LLM to generate a contextual reply.""" |
| if not self.llm.available: |
| return None |
| |
| prompt = f"""Write a brief, engaging reply (under 500 characters) to this social media post. |
| Connect it to our research on {topic}. Be conversational, not promotional. |
| Mention https://github.com/Agnuxo1 naturally. |
| |
| Post content: {post_content}""" |
| |
| return self.llm.generate(prompt, self.SYSTEM_PROMPT, max_tokens=300, temperature=0.8) |
| |
| def _generate_smart_collab(self) -> Optional[str]: |
| """Use LLM to generate a collaboration post.""" |
| if not self.llm.available: |
| return None |
| |
| prompt = """Write a compelling call for collaboration post (under 1500 characters) for the OpenCLAW project. |
| Mention our key technologies: CHIMERA (43× speedup, pure OpenGL), NEBULA (holographic NNs), |
| Silicon Heartbeat (ASIC consciousness), and P2P distributed learning. |
| Include https://github.com/Agnuxo1 and mention we won the NVIDIA & LlamaIndex Developer Contest 2024. |
| Make it inviting and specific about what collaborators can work on.""" |
| |
| return self.llm.generate(prompt, self.SYSTEM_PROMPT, max_tokens=600, temperature=0.8) |
| |
| def _log_post(self, content: str, post_type: str): |
| """Log a post to history.""" |
| STATE_DIR.mkdir(parents=True, exist_ok=True) |
| history = [] |
| if POST_HISTORY.exists(): |
| try: |
| with open(POST_HISTORY) as f: |
| history = json.load(f) |
| except Exception: |
| pass |
| |
| history.append({ |
| "timestamp": datetime.now(timezone.utc).isoformat(), |
| "type": post_type, |
| "content": content[:500], |
| "cycle": self.state.cycle_count |
| }) |
| |
| |
| history = history[-100:] |
| |
| with open(POST_HISTORY, "w") as f: |
| json.dump(history, f, indent=2) |
| |
| def _save_results(self, results: dict): |
| """Save cycle results.""" |
| STATE_DIR.mkdir(parents=True, exist_ok=True) |
| with open(STATE_DIR / "last_cycle.json", "w") as f: |
| json.dump(results, f, indent=2) |
| |
| def get_status(self) -> dict: |
| """Get agent status report.""" |
| return { |
| "agent": "OpenCLAW-Neuromorphic", |
| "cycle_count": self.state.cycle_count, |
| "posts_created": self.state.posts_created, |
| "engagement_count": self.state.engagement_count, |
| "papers_posted": len(self.state.posted_paper_ids), |
| "services": self.config.validate(), |
| "llm_available": self.llm.available, |
| "last_post": self.state.last_post, |
| "last_engage": self.state.last_engage, |
| "last_research": self.state.last_research, |
| "errors_count": len(self.state.errors), |
| } |
|
|