rubra-v3 / evolution_engine.py
getalvi's picture
Update evolution_engine.py
391e388 verified
Raw
History Blame Contribute Delete
39.8 kB
"""
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
# Health tracking — adaptive timeouts + circuit breaker for Parliament models
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)
# ─── API credentials ──────────────────────────────────────────────────────────
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 OF MODELS
# 10 specialized members — each inspects one domain
# ═══════════════════════════════════════════════════════
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", # qwen-qwq-32b fully decommissioned by Groq (June 2026) — same swap as brain.py's "reason" cascade
"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", # llama-3.3-70b-versatile deprecated by Groq (June 2026)
"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", # qwen3-235b-a22b:free discontinued from OR free tier (404 confirmed in prod logs)
"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", # llama-4-scout fully removed from Cerebras now (confirmed via their live models.list() — only zai-glm-4.7 + gpt-oss-120b served)
"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", # Cerebras deprecated llama-3.3-70b (Feb 2026) — 404 confirmed in prod logs
"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", # mistral-small-3.2-24b:free pulled from OR free tier (404 confirmed in prod logs)
"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", # self_coding = complex multi-file work, fits M.1's role
"provider": OR_URL,
"key": OR_KEY,
"vote_weight": 1.5,
},
]
# ═══════════════════════════════════════════════════════
# INFINITE MEMORY SYSTEM
# Remembers people, places, moments, facts — forever
# ═══════════════════════════════════════════════════════
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": {}, # session_id → [events]
"semantic": {}, # topic → facts
"personal": {}, # user_id → profile
"world": {}, # entity → info
"learned": [], # [{skill, description, timestamp}]
"evolution": [], # history of self-improvements
"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))
# ── Episodic memory — remember specific moments ───────────────────────────
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 {},
})
# Keep last 1000 moments per session
if len(self.db["episodic"][session_id]) > 1000:
self.db["episodic"][session_id] = self.db["episodic"][session_id][-1000:]
self._save()
# ── Personal memory — remember people ────────────────────────────────────
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)
# ── Semantic memory — remember facts ─────────────────────────────────────
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()}
# Don't duplicate
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:]
# ── World memory — places, entities, events ───────────────────────────────
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()
# ── Learned skills ────────────────────────────────────────────────────────
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()
# ── Semantic search across all memory ─────────────────────────────────────
def search(self, query: str, limit: int = 5) -> List[dict]:
query_words = set(query.lower().split())
results = []
# Search episodic
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})
# Search semantic
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})
# Search world
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()
# Singleton
memory = InfiniteMemory()
# ═══════════════════════════════════════════════════════
# SELF-LEARNING ENGINE — Internet → RUBRA's brain
# ═══════════════════════════════════════════════════════
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
# RSS feeds
for feed_url, category in RSS_FEEDS[:4]: # limit to avoid rate limits
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
# ArXiv latest papers
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
# ═══════════════════════════════════════════════════════
# SELF-CODE-WRITING ENGINE
# RUBRA writes its own improvements
# ═══════════════════════════════════════════════════════
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}
# Circuit breaker: skip models whose circuit is OPEN
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 ""
# Shared, reliable last-resort chain — used when a member's assigned model is
# dead/discontinued/rate-limited so one bad model assignment doesn't
# permanently zero out that member's vote every single Parliament cycle.
#
# IMPORTANT: this MUST span multiple independent providers, not just
# multiple models on one provider. Production logs showed a real case where
# the entire fallback was a single Groq model — and that Groq organization
# was itself restricted, so EVERY member whose primary failed also failed
# on "the fallback", because the fallback was just as dead as everything
# else. A provider-level outage/suspension/restriction is common enough
# (seen here with both Groq and a suspended Gemini key at the same time)
# that the fallback chain needs to survive losing an entire provider, not
# just one model on it.
_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 # no key configured, or this IS the member's already-failed primary
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:
# Save to code directory
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
# ═══════════════════════════════════════════════════════
# PARLIAMENT SESSION
# 10 models inspect RUBRA and vote on improvements
# ═══════════════════════════════════════════════════════
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:
# Extract JSON from response
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
# Fallback: parse as text
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...")
# Get RUBRA's current state
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),
}
# All members inspect in parallel
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": []}
# Prioritize by HIGH priority + vote weight
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)
# Take top 3 HIGH priority improvements
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:
# Ask Architect to implement
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"])
# Save session log
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:] # Keep last 100 sessions
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
# ═══════════════════════════════════════════════════════
# HUMANOID BEHAVIOR ENGINE
# Makes RUBRA remember and respond like a human
# ═══════════════════════════════════════════════════════
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 detection
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 detection
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 detection
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 = []
# Personal profile
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))
# Relevant memories
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."""
# Remember this moment
self.memory.remember_moment(session_id, "user", message)
self.memory.remember_moment(session_id, "assistant", response)
# Extract personal info
self.extract_personal_info(message, session_id)
# Extract facts from conversation
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())
# Update stats
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()
# Singleton
humanoid = HumanoidBehavior()
# ═══════════════════════════════════════════════════════
# 24/7 BACKGROUND SCHEDULER
# ═══════════════════════════════════════════════════════
_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()
# Internet learning — every 2 hours
if now - last_learning > 7200:
try:
await learn_from_internet()
last_learning = now
except Exception as e:
log.error(f"[SCHEDULER] Learning error: {e}")
# Parliament session — every 6 hours
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}")
# Sleep 30 minutes between checks
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}")
# ═══════════════════════════════════════════════════════
# PUBLIC API
# ═══════════════════════════════════════════════════════
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()