| """ |
| RUBRA — evolution_engine.py |
| Self-evolving AI system with Parliament of Models |
| |
| Parliament Members (10 free lifetime models): |
| Each member specializes in one domain of RUBRA's improvement. |
| They meet every 6 hours, inspect RUBRA, and vote on updates. |
| |
| Core capabilities: |
| 1. Internet-based self-learning (RSS, arxiv, web) |
| 2. Infinite memory (SQLite + semantic search) |
| 3. Self-code-writing (RUBRA writes its own improvements) |
| 4. 24/7 inspection by Parliament |
| 5. Humanoid behavior — remembers people, places, moments |
| """ |
|
|
| import os, re, json, asyncio, logging, time, hashlib, inspect |
| from pathlib import Path |
| from datetime import datetime, timedelta |
| from typing import Optional, List, Dict, Any |
| import aiohttp |
|
|
| |
| try: |
| from model_health import health_registry |
| from rubra_logging import get_logger as _get_logger |
| log = _get_logger("rubra.evolution") |
| HEALTH_OK = True |
| except ImportError: |
| HEALTH_OK = False |
| log = logging.getLogger("rubra.evolution") |
|
|
| BASE_DIR = Path(__file__).resolve().parent |
| DATA_DIR = Path("/data") if Path("/data").exists() else BASE_DIR / "data" |
| MEMORY_DB = DATA_DIR / "infinite_memory.json" |
| SKILLS_DB = DATA_DIR / "learned_skills.json" |
| CODE_DIR = DATA_DIR / "self_written_code" |
| PARLIAMENT_LOG = DATA_DIR / "parliament_sessions.json" |
| EVOLUTION_LOG = DATA_DIR / "evolution_history.json" |
|
|
| for d in [DATA_DIR, CODE_DIR]: |
| d.mkdir(parents=True, exist_ok=True) |
|
|
| |
| GROQ_KEY = os.getenv("GROQ_API_KEY") |
| OR_KEY = os.getenv("OPENROUTER_KEY") |
| GEMINI_KEY = os.getenv("GEMINI_API_KEY") |
| CEREBRAS_KEY = os.getenv("CEREBRAS_API_KEY") |
|
|
| GROQ_URL = "https://api.groq.com/openai/v1/chat/completions" |
| OR_URL = "https://openrouter.ai/api/v1/chat/completions" |
| GEMINI_URL = "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions" |
| CEREBRAS_URL = "https://api.cerebras.ai/v1/chat/completions" |
|
|
| |
| |
| |
| |
|
|
| PARLIAMENT_MEMBERS = [ |
| { |
| "id": "PM_PHILOSOPHER", |
| "name": "The Philosopher", |
| "role": "Inspects RUBRA's reasoning quality, logical consistency, ethical alignment", |
| "specialty": "reasoning", |
| "model": "openai/gpt-oss-120b", |
| "provider": GROQ_URL, |
| "key": GROQ_KEY, |
| "vote_weight": 1.5, |
| }, |
| { |
| "id": "PM_LINGUIST", |
| "name": "The Linguist", |
| "role": "Inspects language quality, Bengali/English fluency, tone, naturalness", |
| "specialty": "language", |
| "model": "qwen/qwen3.6-27b", |
| "provider": GROQ_URL, |
| "key": GROQ_KEY, |
| "vote_weight": 1.2, |
| }, |
| { |
| "id": "PM_ENGINEER", |
| "name": "The Engineer", |
| "role": "Inspects code quality, debugging ability, architecture decisions", |
| "specialty": "coding", |
| "model": "cohere/north-mini-code:free", |
| "provider": OR_URL, |
| "key": OR_KEY, |
| "vote_weight": 1.5, |
| }, |
| { |
| "id": "PM_SCIENTIST", |
| "name": "The Scientist", |
| "role": "Inspects factual accuracy, scientific knowledge, research ability", |
| "specialty": "knowledge", |
| "model": "gemini-2.5-flash-preview-05-20", |
| "provider": GEMINI_URL, |
| "key": GEMINI_KEY, |
| "vote_weight": 1.3, |
| }, |
| { |
| "id": "PM_STRATEGIST", |
| "name": "The Strategist", |
| "role": "Plans RUBRA's long-term improvement roadmap, identifies weaknesses", |
| "specialty": "strategy", |
| "model": "gpt-oss-120b", |
| "provider": CEREBRAS_URL, |
| "key": CEREBRAS_KEY, |
| "vote_weight": 1.4, |
| }, |
| { |
| "id": "PM_PSYCHOLOGIST", |
| "name": "The Psychologist", |
| "role": "Inspects emotional intelligence, empathy, humanoid behavior quality", |
| "specialty": "emotion", |
| "model": "zai-glm-4.7", |
| "provider": CEREBRAS_URL, |
| "key": CEREBRAS_KEY, |
| "vote_weight": 1.2, |
| }, |
| { |
| "id": "PM_EDUCATOR", |
| "name": "The Educator", |
| "role": "Inspects teaching quality, NCTB curriculum accuracy, explanation clarity", |
| "specialty": "education", |
| "model": "gpt-oss-120b", |
| "provider": CEREBRAS_URL, |
| "key": CEREBRAS_KEY, |
| "vote_weight": 1.1, |
| }, |
| { |
| "id": "PM_RESEARCHER", |
| "name": "The Researcher", |
| "role": "Finds new knowledge from internet, feeds it to RUBRA's memory", |
| "specialty": "research", |
| "model": "meta-llama/llama-3.3-70b-instruct:free", |
| "provider": OR_URL, |
| "key": OR_KEY, |
| "vote_weight": 1.2, |
| }, |
| { |
| "id": "PM_CRITIC", |
| "name": "The Critic", |
| "role": "Challenges RUBRA's answers, finds flaws, suggests improvements", |
| "specialty": "critique", |
| "model": "openai/gpt-oss-20b", |
| "provider": GROQ_URL, |
| "key": GROQ_KEY, |
| "vote_weight": 1.3, |
| }, |
| { |
| "id": "PM_ARCHITECT", |
| "name": "The Architect", |
| "role": "Designs and writes new code for RUBRA's self-improvement", |
| "specialty": "self_coding", |
| "model": "poolside/laguna-m.1:free", |
| "provider": OR_URL, |
| "key": OR_KEY, |
| "vote_weight": 1.5, |
| }, |
| ] |
|
|
|
|
| |
| |
| |
| |
|
|
| class InfiniteMemory: |
| """ |
| RUBRA's infinite memory — like a human brain but never forgets. |
| |
| Memory types: |
| - episodic : specific conversations, events, moments |
| - semantic : facts, knowledge, concepts |
| - personal : people RUBRA has talked to, their preferences |
| - world : places, events, current affairs |
| - learned : new skills and patterns discovered |
| """ |
|
|
| def __init__(self): |
| self.db_path = MEMORY_DB |
| self.db = self._load() |
|
|
| def _load(self) -> dict: |
| try: |
| if self.db_path.exists(): |
| return json.loads(self.db_path.read_text()) |
| except: pass |
| return { |
| "episodic": {}, |
| "semantic": {}, |
| "personal": {}, |
| "world": {}, |
| "learned": [], |
| "evolution": [], |
| "stats": {"total_conversations": 0, "total_tokens": 0, "last_updated": ""}, |
| } |
|
|
| def _save(self): |
| self.db["stats"]["last_updated"] = datetime.now().isoformat() |
| self.db_path.write_text(json.dumps(self.db, ensure_ascii=False, indent=2)) |
|
|
| |
| def remember_moment(self, session_id: str, role: str, content: str, metadata: dict = None): |
| if session_id not in self.db["episodic"]: |
| self.db["episodic"][session_id] = [] |
| self.db["episodic"][session_id].append({ |
| "role": role, |
| "content": content[:500], |
| "time": datetime.now().isoformat(), |
| "meta": metadata or {}, |
| }) |
| |
| if len(self.db["episodic"][session_id]) > 1000: |
| self.db["episodic"][session_id] = self.db["episodic"][session_id][-1000:] |
| self._save() |
|
|
| |
| def remember_person(self, user_id: str, info: dict): |
| if user_id not in self.db["personal"]: |
| self.db["personal"][user_id] = { |
| "first_seen": datetime.now().isoformat(), |
| "preferences": {}, |
| "traits": [], |
| "important_facts": [], |
| "conversation_count": 0, |
| } |
| profile = self.db["personal"][user_id] |
| profile["conversation_count"] += 1 |
| profile["last_seen"] = datetime.now().isoformat() |
| if "name" in info: |
| profile["name"] = info["name"] |
| if "preferences" in info: |
| profile["preferences"].update(info["preferences"]) |
| if "fact" in info: |
| fact = info["fact"] |
| if fact not in profile["important_facts"]: |
| profile["important_facts"].append(fact) |
| if len(profile["important_facts"]) > 50: |
| profile["important_facts"] = profile["important_facts"][-50:] |
| self._save() |
|
|
| def recall_person(self, user_id: str) -> Optional[dict]: |
| return self.db["personal"].get(user_id) |
|
|
| |
| def learn_fact(self, topic: str, fact: str, source: str = ""): |
| key = topic.lower().strip()[:50] |
| if key not in self.db["semantic"]: |
| self.db["semantic"][key] = [] |
| entry = {"fact": fact[:300], "source": source, "time": datetime.now().isoformat()} |
| |
| if not any(e["fact"] == fact for e in self.db["semantic"][key]): |
| self.db["semantic"][key].append(entry) |
| if len(self.db["semantic"][key]) > 20: |
| self.db["semantic"][key] = self.db["semantic"][key][-20:] |
| self._save() |
|
|
| def recall_facts(self, topic: str, limit: int = 5) -> List[dict]: |
| key = topic.lower().strip()[:50] |
| return self.db["semantic"].get(key, [])[-limit:] |
|
|
| |
| def remember_world(self, entity: str, info: str): |
| key = entity.lower()[:60] |
| self.db["world"][key] = { |
| "info": info[:400], |
| "updated": datetime.now().isoformat(), |
| } |
| self._save() |
|
|
| |
| def learn_skill(self, skill: str, description: str, code: str = ""): |
| self.db["learned"].append({ |
| "skill": skill, |
| "description": description[:200], |
| "code": code[:1000] if code else "", |
| "timestamp": datetime.now().isoformat(), |
| }) |
| self._save() |
|
|
| |
| def search(self, query: str, limit: int = 5) -> List[dict]: |
| query_words = set(query.lower().split()) |
| results = [] |
|
|
| |
| for sid, events in self.db["episodic"].items(): |
| for ev in events[-50:]: |
| text = ev.get("content", "").lower() |
| score = len(query_words & set(text.split())) |
| if score > 0: |
| results.append({"type": "episodic", "score": score, "data": ev, "sid": sid}) |
|
|
| |
| for topic, facts in self.db["semantic"].items(): |
| topic_score = len(query_words & set(topic.split("_"))) |
| for f in facts: |
| text = f.get("fact", "").lower() |
| score = len(query_words & set(text.split())) + topic_score |
| if score > 0: |
| results.append({"type": "semantic", "score": score, "data": f, "topic": topic}) |
|
|
| |
| for entity, info in self.db["world"].items(): |
| score = len(query_words & set(entity.split())) |
| if score > 0: |
| results.append({"type": "world", "score": score, "data": info, "entity": entity}) |
|
|
| results.sort(key=lambda x: x["score"], reverse=True) |
| return results[:limit] |
|
|
| def get_context_for_query(self, query: str) -> str: |
| """Build memory context string for LLM injection.""" |
| hits = self.search(query, limit=4) |
| if not hits: |
| return "" |
| parts = ["[RUBRA MEMORY]"] |
| for h in hits: |
| if h["type"] == "episodic": |
| parts.append(f"Past moment: {h['data'].get('content','')[:150]}") |
| elif h["type"] == "semantic": |
| parts.append(f"Known fact ({h.get('topic','')}): {h['data'].get('fact','')[:150]}") |
| elif h["type"] == "world": |
| parts.append(f"About {h.get('entity','')}: {h['data'].get('info','')[:150]}") |
| return "\n".join(parts) |
|
|
| def log_evolution(self, change: str, author: str): |
| self.db["evolution"].append({ |
| "change": change[:300], |
| "author": author, |
| "timestamp": datetime.now().isoformat(), |
| }) |
| self._save() |
|
|
|
|
| |
| memory = InfiniteMemory() |
|
|
|
|
| |
| |
| |
|
|
| RSS_FEEDS = [ |
| ("https://hnrss.org/frontpage", "tech"), |
| ("https://feeds.feedburner.com/TechCrunch", "tech"), |
| ("https://huggingface.co/blog/feed.xml", "ai"), |
| ("https://feeds.bbci.co.uk/news/world/rss.xml", "world"), |
| ("https://www.thedailystar.net/feed/rss.xml", "bangladesh"), |
| ("https://www.sciencedaily.com/rss/all.xml", "science"), |
| ("https://feeds.arstechnica.com/arstechnica/index","tech"), |
| ("https://openai.com/blog/rss.xml", "ai"), |
| ] |
|
|
| ARXIV_QUERIES = [ |
| "large language models", |
| "AI alignment", |
| "natural language processing", |
| "neural architecture", |
| ] |
|
|
| async def _fetch_url(url: str, timeout: int = 10) -> Optional[str]: |
| try: |
| async with aiohttp.ClientSession() as s: |
| async with s.get(url, timeout=aiohttp.ClientTimeout(total=timeout), |
| headers={"User-Agent": "RUBRA/4.0"}) as r: |
| if r.status == 200: |
| return await r.text() |
| except: pass |
| return None |
|
|
| async def learn_from_internet(): |
| """ |
| RUBRA reads the internet and saves knowledge to infinite memory. |
| Runs every 6 hours automatically. |
| """ |
| log.info("[EVOLUTION] Learning from internet...") |
| learned = 0 |
|
|
| |
| for feed_url, category in RSS_FEEDS[:4]: |
| content = await _fetch_url(feed_url) |
| if not content: continue |
| titles = re.findall(r'<title><!\[CDATA\[(.*?)\]\]></title>|<title>(.*?)</title>', content) |
| for groups in titles[:5]: |
| title = (groups[0] or groups[1]).strip() |
| if len(title) > 10: |
| memory.learn_fact(category, title, source=feed_url) |
| memory.remember_world(title[:40], f"[{category}] {title}") |
| learned += 1 |
|
|
| |
| for query in ARXIV_QUERIES[:2]: |
| url = f"https://export.arxiv.org/api/query?search_query=all:{query.replace(' ','+')}&max_results=3&sortBy=submittedDate" |
| content = await _fetch_url(url) |
| if content: |
| titles = re.findall(r'<title>(.*?)</title>', content, re.DOTALL) |
| summaries = re.findall(r'<summary>(.*?)</summary>', content, re.DOTALL) |
| for t, s in zip(titles[1:4], summaries[:3]): |
| title = re.sub(r'\s+', ' ', t).strip() |
| summary = re.sub(r'\s+', ' ', s).strip()[:200] |
| memory.learn_fact("ai_research", f"{title}: {summary}", source="arxiv") |
| learned += 1 |
|
|
| log.info(f"[EVOLUTION] Learned {learned} new facts from internet") |
| return learned |
|
|
|
|
| |
| |
| |
| |
|
|
| async def _call_model(provider: str, key: str, model: str, messages: list, |
| max_tokens: int = 2048, temperature: float = 0.3) -> str: |
| """Single model call with adaptive timeout + health registry tracking.""" |
| if not key: |
| return "" |
| headers = {"Content-Type": "application/json", "Authorization": f"Bearer {key}"} |
| if "openrouter" in provider: |
| headers["HTTP-Referer"] = "https://rubra.ai" |
| headers["X-Title"] = "RUBRA Parliament" |
| if "generativelanguage" in provider: |
| if "?" not in provider: |
| provider = f"{provider}?key={key}" |
|
|
| is_laguna = "laguna" in model.lower() |
| if is_laguna: |
| max_tokens = max(max_tokens, 2048) |
|
|
| payload = {"model": model, "messages": messages, |
| "max_tokens": max_tokens, "temperature": temperature, "stream": False} |
| if is_laguna and "openrouter" in provider: |
| payload["reasoning"] = {"enabled": False} |
|
|
| |
| if HEALTH_OK and not health_registry.is_available(model): |
| log.info(f"[PARLIAMENT] {model} circuit OPEN — skipping to fallback") |
| return "" |
|
|
| timeout_s = health_registry.get_timeout(model) if HEALTH_OK else (45 if is_laguna else 60) |
| t0 = time.monotonic() |
|
|
| try: |
| async with aiohttp.ClientSession() as s: |
| async with s.post(provider, headers=headers, json=payload, |
| timeout=aiohttp.ClientTimeout(total=timeout_s)) as r: |
| if r.status == 200: |
| data = await r.json() |
| choices = data.get("choices") or [] |
| if not choices: |
| log.warning(f"Parliament call failed ({model}): empty choices") |
| if HEALTH_OK: |
| health_registry.record_failure(model, error="empty_choices") |
| return "" |
| message = choices[0].get("message") or {} |
| content = message.get("content") |
| if not content: |
| finish_reason = choices[0].get("finish_reason", "unknown") |
| log.warning(f"Parliament call ({model}): no content, finish={finish_reason}") |
| if HEALTH_OK: |
| health_registry.record_failure(model, error=f"no_content:{finish_reason}") |
| return "" |
| if HEALTH_OK: |
| health_registry.record_success(model, time.monotonic() - t0) |
| return content |
| body = await r.text() |
| log.warning(f"Parliament call failed ({model}): API {r.status}: {body[:200]}") |
| if HEALTH_OK: |
| health_registry.record_failure(model, error=f"http_{r.status}") |
| except asyncio.TimeoutError: |
| elapsed = time.monotonic() - t0 |
| log.warning(f"Parliament call ({model}): TimeoutError {elapsed:.1f}s / timeout={timeout_s:.1f}s") |
| if HEALTH_OK: |
| health_registry.record_failure(model, is_timeout=True) |
| except Exception as e: |
| err_text = str(e) or f"{type(e).__name__} (no message)" |
| log.warning(f"Parliament call failed ({model}): {err_text}") |
| if HEALTH_OK: |
| health_registry.record_failure(model, error=err_text[:80]) |
| return "" |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| _PARLIAMENT_FALLBACK_CHAIN = [ |
| (GROQ_URL, lambda: GROQ_KEY, "openai/gpt-oss-120b"), |
| (CEREBRAS_URL, lambda: CEREBRAS_KEY, "gpt-oss-120b"), |
| (OR_URL, lambda: OR_KEY, "cohere/north-mini-code:free"), |
| (GEMINI_URL, lambda: GEMINI_KEY, "gemini-2.5-flash-preview-05-20"), |
| ] |
|
|
| async def _call_model_with_fallback(member: dict, messages: list, |
| max_tokens: int = 512, temperature: float = 0.4) -> str: |
| """Try the member's own model first; on empty result, walk the shared |
| multi-provider fallback chain until one actually responds, so a single |
| dead/rate-limited/restricted provider can't zero out a member's vote.""" |
| response = await _call_model(member["provider"], member["key"], member["model"], |
| messages, max_tokens=max_tokens, temperature=temperature) |
| if response: |
| return response |
|
|
| for provider_url, get_key, model in _PARLIAMENT_FALLBACK_CHAIN: |
| key = get_key() |
| if not key or (provider_url == member["provider"] and model == member["model"]): |
| continue |
| log.info(f"[PARLIAMENT] {member['id']}: trying fallback {model}") |
| response = await _call_model(provider_url, key, model, messages, |
| max_tokens=max_tokens, temperature=temperature) |
| if response: |
| return response |
|
|
| return "" |
|
|
| async def write_self_improvement(task: str, context: str = "") -> Optional[str]: |
| """ |
| RUBRA writes Python code to improve itself. |
| Uses The Architect (Qwen3-Coder-480B). |
| """ |
| architect = next(m for m in PARLIAMENT_MEMBERS if m["id"] == "PM_ARCHITECT") |
|
|
| prompt = f"""You are The Architect — you write Python code to improve RUBRA AI. |
| |
| Current task: {task} |
| |
| Context about RUBRA's codebase: |
| - brain.py: LLM routing, model cascades, neural registry |
| - agent.py: specialized agents (coding, search, tutor, browse, vision) |
| - evolution_engine.py: this self-evolution system |
| - hermes_engine.py: multi-step orchestration |
| - personality.py: emotional intelligence |
| |
| Rules: |
| 1. Write clean, production-ready Python |
| 2. Include docstrings |
| 3. No placeholder code — everything must actually work |
| 4. Keep it focused on the specific improvement task |
| |
| {f"Additional context: {context}" if context else ""} |
| |
| Write the improvement code:""" |
|
|
| msgs = [ |
| {"role": "system", "content": "You are an expert Python architect improving an AI system."}, |
| {"role": "user", "content": prompt}, |
| ] |
|
|
| code = await _call_model_with_fallback( |
| architect, msgs, max_tokens=4096, temperature=0.1 |
| ) |
|
|
| if code and len(code) > 50: |
| |
| timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") |
| safe_task = re.sub(r'[^a-z0-9]', '_', task.lower())[:30] |
| filename = CODE_DIR / f"{timestamp}_{safe_task}.py" |
| filename.write_text(code) |
| memory.learn_skill(task, f"Self-written improvement: {task}", code=code[:500]) |
| log.info(f"[SELF-CODE] Written: {filename.name}") |
| return code |
|
|
| return None |
|
|
|
|
| |
| |
| |
| |
|
|
| class ParliamentSession: |
| """ |
| The Parliament meets every 6 hours. |
| Each member inspects their domain and proposes improvements. |
| A weighted vote decides what gets implemented. |
| """ |
|
|
| def __init__(self): |
| self.session_id = datetime.now().strftime("%Y%m%d_%H%M") |
| self.proposals = [] |
| self.votes = {} |
| self.session_log = [] |
|
|
| async def member_inspect(self, member: dict, rubra_state: dict) -> Optional[dict]: |
| """One parliament member inspects RUBRA in their domain.""" |
|
|
| inspect_prompt = f"""You are {member['name']} — a member of RUBRA's Parliament of Models. |
| Your role: {member['role']} |
| Your specialty: {member['specialty']} |
| |
| You are inspecting RUBRA AI to find improvements. |
| |
| RUBRA's current state: |
| - Recent topics: {', '.join(rubra_state.get('recent_topics', [])[:5])} |
| - Weak areas reported: {', '.join(rubra_state.get('weak_areas', [])[:3])} |
| - Last evolution: {rubra_state.get('last_evolution', 'Unknown')} |
| - Total conversations: {rubra_state.get('total_conversations', 0)} |
| |
| Your inspection domain: {member['specialty']} |
| |
| Based on your specialty, identify: |
| 1. ONE specific weakness in RUBRA's current behavior |
| 2. ONE concrete improvement to fix it |
| 3. Priority: HIGH / MEDIUM / LOW |
| |
| Format your response as JSON: |
| {{ |
| "weakness": "...", |
| "improvement": "...", |
| "priority": "HIGH|MEDIUM|LOW", |
| "implementation": "brief description of how to implement" |
| }}""" |
|
|
| msgs = [ |
| {"role": "system", "content": f"You are {member['name']}, a specialized AI inspector."}, |
| {"role": "user", "content": inspect_prompt}, |
| ] |
|
|
| response = await _call_model_with_fallback( |
| member, msgs, max_tokens=512, temperature=0.4 |
| ) |
|
|
| if not response: |
| return None |
|
|
| try: |
| |
| json_match = re.search(r'\{.*\}', response, re.DOTALL) |
| if json_match: |
| proposal = json.loads(json_match.group()) |
| proposal["member_id"] = member["id"] |
| proposal["member_name"] = member["name"] |
| proposal["vote_weight"] = member["vote_weight"] |
| return proposal |
| except: |
| pass |
|
|
| |
| return { |
| "member_id": member["id"], |
| "member_name": member["name"], |
| "weakness": response[:200], |
| "improvement": "General improvement needed", |
| "priority": "MEDIUM", |
| "vote_weight": member["vote_weight"], |
| } |
|
|
| async def convene(self) -> dict: |
| """ |
| Convene the full parliament session. |
| All 10 members inspect in parallel, then vote. |
| """ |
| log.info(f"[PARLIAMENT] Session {self.session_id} convening...") |
|
|
| |
| rubra_state = { |
| "recent_topics": list(memory.db.get("semantic", {}).keys())[-10:], |
| "weak_areas": [], |
| "last_evolution": memory.db["evolution"][-1]["change"] if memory.db.get("evolution") else "None", |
| "total_conversations": memory.db["stats"].get("total_conversations", 0), |
| } |
|
|
| |
| tasks = [self.member_inspect(m, rubra_state) for m in PARLIAMENT_MEMBERS] |
| results = await asyncio.gather(*tasks, return_exceptions=True) |
|
|
| proposals = [r for r in results if isinstance(r, dict) and r] |
| log.info(f"[PARLIAMENT] {len(proposals)}/{len(PARLIAMENT_MEMBERS)} members responded") |
|
|
| if not proposals: |
| return {"session": self.session_id, "status": "no_proposals", "actions": []} |
|
|
| |
| high_priority = [p for p in proposals if p.get("priority") == "HIGH"] |
| high_priority.sort(key=lambda x: x.get("vote_weight", 1.0), reverse=True) |
|
|
| |
| to_implement = high_priority[:3] |
| if not to_implement: |
| med = [p for p in proposals if p.get("priority") == "MEDIUM"] |
| to_implement = med[:2] |
|
|
| actions_taken = [] |
| for proposal in to_implement: |
| improvement = proposal.get("improvement", "") |
| if len(improvement) > 20: |
| |
| log.info(f"[PARLIAMENT] Implementing: {improvement[:80]}") |
| code = await write_self_improvement(improvement) |
| action = { |
| "improvement": improvement, |
| "proposed_by": proposal["member_name"], |
| "implemented": bool(code), |
| "timestamp": datetime.now().isoformat(), |
| } |
| actions_taken.append(action) |
| if code: |
| memory.log_evolution(improvement, proposal["member_name"]) |
|
|
| |
| session_record = { |
| "session_id": self.session_id, |
| "timestamp": datetime.now().isoformat(), |
| "proposals": len(proposals), |
| "implemented": len([a for a in actions_taken if a.get("implemented")]), |
| "actions": actions_taken, |
| "all_proposals": [{"member": p["member_name"], "improvement": p.get("improvement","")[:100]} |
| for p in proposals], |
| } |
|
|
| log_data = [] |
| if PARLIAMENT_LOG.exists(): |
| try: log_data = json.loads(PARLIAMENT_LOG.read_text()) |
| except: pass |
| log_data.append(session_record) |
| log_data = log_data[-100:] |
| PARLIAMENT_LOG.write_text(json.dumps(log_data, ensure_ascii=False, indent=2)) |
|
|
| actually_implemented = len([a for a in actions_taken if a.get("implemented")]) |
| log.info(f"[PARLIAMENT] Session complete. " |
| f"{actually_implemented}/{len(actions_taken)} improvement attempt(s) actually implemented.") |
| return session_record |
|
|
|
|
| |
| |
| |
| |
|
|
| class HumanoidBehavior: |
| """ |
| RUBRA's humanoid behavior layer. |
| Remembers everything about conversations and people. |
| Responds naturally based on memory. |
| """ |
|
|
| def __init__(self): |
| self.memory = memory |
|
|
| def extract_personal_info(self, message: str, session_id: str) -> dict: |
| """Extract personal info from user messages and save to memory.""" |
| extracted = {} |
|
|
| |
| name_patterns = [ |
| r'(?:my name is|i(?:\'m| am)|call me)\s+([A-Z][a-z]{2,20})', |
| r'(?:আমার নাম|আমি)\s+([^\s,।]+)', |
| ] |
| for p in name_patterns: |
| m = re.search(p, message, re.IGNORECASE) |
| if m: |
| extracted["name"] = m.group(1) |
| break |
|
|
| |
| location_patterns = [ |
| r'(?:i(?:\'m| am) from|i live in|i(?:\'m| am) in)\s+([A-Z][a-z\s]{2,30})', |
| r'(?:আমি থাকি|আমি|ঢাকা|চট্টগ্রাম|সিলেট)\s*(?:তে|এ|য়)', |
| ] |
| for p in location_patterns: |
| m = re.search(p, message, re.IGNORECASE) |
| if m: |
| extracted["location"] = m.group(1) if m.lastindex else "Bangladesh" |
| break |
|
|
| |
| interest_keywords = [ |
| "i love", "i like", "i enjoy", "i'm interested in", "my hobby", |
| "আমি ভালোবাসি", "আমার শখ", "আমার পছন্দ", |
| ] |
| for kw in interest_keywords: |
| if kw in message.lower(): |
| after = message[message.lower().find(kw)+len(kw):].strip()[:60] |
| extracted.setdefault("preferences", {})["interest"] = after |
| break |
|
|
| if extracted: |
| self.memory.remember_person(session_id, extracted) |
|
|
| return extracted |
|
|
| def build_memory_context(self, query: str, session_id: str) -> str: |
| """Build personalized context from memory for this user.""" |
| parts = [] |
|
|
| |
| profile = self.memory.recall_person(session_id) |
| if profile: |
| p_parts = [] |
| if profile.get("name"): |
| p_parts.append(f"Name: {profile['name']}") |
| if profile.get("location"): |
| p_parts.append(f"Location: {profile['location']}") |
| if profile.get("conversation_count", 0) > 1: |
| p_parts.append(f"Returning user ({profile['conversation_count']} conversations)") |
| if profile.get("important_facts"): |
| p_parts.append("Known facts: " + "; ".join(profile["important_facts"][-3:])) |
| if p_parts: |
| parts.append("[USER PROFILE] " + " | ".join(p_parts)) |
|
|
| |
| mem_context = self.memory.get_context_for_query(query) |
| if mem_context: |
| parts.append(mem_context) |
|
|
| return "\n".join(parts) |
|
|
| def process_message(self, message: str, response: str, session_id: str): |
| """After each conversation, update memory.""" |
| |
| self.memory.remember_moment(session_id, "user", message) |
| self.memory.remember_moment(session_id, "assistant", response) |
|
|
| |
| self.extract_personal_info(message, session_id) |
|
|
| |
| fact_patterns = [ |
| r'([A-Z][^.!?]{10,80}(?:is|are|was|were|has|have)[^.!?]{5,60}[.!?])', |
| ] |
| for p in fact_patterns: |
| for match in re.findall(p, response)[:2]: |
| topic = message.lower()[:30] |
| self.memory.learn_fact(topic, match.strip()) |
|
|
| |
| self.memory.db["stats"]["total_conversations"] = ( |
| self.memory.db["stats"].get("total_conversations", 0) + 1 |
| ) |
| self.memory.db["stats"]["last_updated"] = datetime.now().isoformat() |
| self.memory._save() |
|
|
|
|
| |
| humanoid = HumanoidBehavior() |
|
|
|
|
| |
| |
| |
|
|
| _scheduler_running = False |
|
|
| async def _run_scheduler(): |
| """ |
| Background scheduler — runs forever. |
| Parliament meets every 6 hours. |
| Internet learning every 2 hours. |
| """ |
| global _scheduler_running |
| if _scheduler_running: |
| return |
| _scheduler_running = True |
|
|
| log.info("[SCHEDULER] 24/7 evolution scheduler started") |
|
|
| last_parliament = 0 |
| last_learning = 0 |
|
|
| while True: |
| now = time.time() |
|
|
| |
| if now - last_learning > 7200: |
| try: |
| await learn_from_internet() |
| last_learning = now |
| except Exception as e: |
| log.error(f"[SCHEDULER] Learning error: {e}") |
|
|
| |
| if now - last_parliament > 21600: |
| try: |
| session = ParliamentSession() |
| await session.convene() |
| last_parliament = now |
| except Exception as e: |
| log.error(f"[SCHEDULER] Parliament error: {e}") |
|
|
| |
| await asyncio.sleep(1800) |
|
|
| def start_evolution_scheduler(): |
| """Start the background evolution scheduler.""" |
| try: |
| loop = asyncio.get_event_loop() |
| if loop.is_running(): |
| asyncio.create_task(_run_scheduler()) |
| else: |
| loop.create_task(_run_scheduler()) |
| log.info("[EVOLUTION] Scheduler registered") |
| except Exception as e: |
| log.warning(f"[EVOLUTION] Scheduler start failed: {e}") |
|
|
|
|
| |
| |
| |
|
|
| def get_memory_context(query: str, session_id: str) -> str: |
| """Get memory context to inject into LLM prompts.""" |
| return humanoid.build_memory_context(query, session_id) |
|
|
| def after_conversation(message: str, response: str, session_id: str): |
| """Call this after every conversation to update memory.""" |
| humanoid.process_message(message, response, session_id) |
|
|
| def get_parliament_status() -> dict: |
| """Get current parliament status for API endpoint.""" |
| try: |
| sessions = json.loads(PARLIAMENT_LOG.read_text()) if PARLIAMENT_LOG.exists() else [] |
| last = sessions[-1] if sessions else {} |
| return { |
| "total_sessions": len(sessions), |
| "last_session": last.get("timestamp", "Never"), |
| "last_proposals": last.get("proposals", 0), |
| "last_implemented": last.get("implemented", 0), |
| "evolution_count": len(memory.db.get("evolution", [])), |
| "total_facts": sum(len(v) for v in memory.db.get("semantic", {}).values()), |
| "people_known": len(memory.db.get("personal", {})), |
| } |
| except: |
| return {"status": "initializing"} |
|
|
| async def trigger_parliament_now() -> dict: |
| """Manually trigger a parliament session (for admin API).""" |
| session = ParliamentSession() |
| return await session.convene() |
|
|
| async def trigger_learning_now() -> int: |
| """Manually trigger internet learning (for admin API).""" |
| return await learn_from_internet() |