phi-drift / core /cognitive_factory.py
crexs's picture
Upload folder using huggingface_hub
914e970 verified
Raw
History Blame Contribute Delete
16.1 kB
"""
cognitive_factory.py — Propose, generate, and install new cognitive modules.
The bot can suggest new abilities based on observed gaps in its cognition.
Every proposal requires user's explicit approval before code is generated,
validated, and installed.
"""
import ast
import logging
import os
import textwrap
from dataclasses import dataclass
from datetime import datetime
from typing import Dict, List, Optional
from infj_bot.core.cognitive_architecture import CognitiveArchitecture
logger = logging.getLogger(__name__)
MODULE_TEMPLATE = '''\
"""
{name}.py — {description}
Generated by the cognitive factory on {timestamp}.
Purpose: {purpose}
"""
import logging
import os
from dataclasses import dataclass
from datetime import datetime
from typing import Dict, List, Optional
from infj_bot.core.cognitive_architecture import CognitiveArchitecture, CognitivePlugin
from infj_bot.core.config import DATA_DIR
@dataclass
class {class_name}State:
"""Runtime state for {class_name}."""
active: bool = True
last_run: Optional[str] = None
class {class_name}:
"""
{docstring}
"""
def __init__(self, db_path: Optional[str] = None):
self.db_path = db_path or DATA_DIR / "{name}.db"
self.state = {class_name}State()
self._init_db()
def _init_db(self) -> None:
"""Create any required SQLite tables."""
# Override in generated code if persistence is needed.
pass
def cycle(self, context) -> None:
"""
Called by the consciousness loop when this module is active.
`context` is a CycleContext from cognitive_architecture.
"""
self.state.last_run = datetime.now().isoformat()
# TODO: implement per-cycle logic
def format_prompt_snippet(self) -> str:
"""Return a string to inject into the chat prompt."""
# TODO: implement prompt contribution
return ""
# --- User-defined methods go below ---
{custom_methods}
def _register():
from infj_bot.core.cognitive_architecture import CognitiveArchitecture, CognitivePlugin
arch = CognitiveArchitecture()
if "{name}" not in arch.list_plugins():
arch.register(CognitivePlugin(
name="{name}",
description="{description}",
module_path="{name}",
instance_factory={class_name},
cycle_handler="cycle",
cycle_frequency=1,
cycle_priority=50,
prompt_formatter="format_prompt_snippet",
prompt_priority=50,
prompt_section="cognitive",
is_user_created=True,
))
_register()
'''
@dataclass
class AbilityProposal:
"""A proposed new cognitive ability."""
name: str
description: str
purpose: str
observed_need: str
proposed_methods: List[str]
confidence: float # 0.0–1.0
created_at: str
class CognitiveFactory:
"""
Generates, validates, and installs new cognitive modules.
"""
# Patterns that suggest the bot might benefit from a new module
NEED_PATTERNS: List[Dict] = [
{
"keyword": "gratitude",
"module_name": "gratitude_journal",
"description": "Tracks moments of gratitude and thanks.",
"methods": ["record_gratitude", "format_gratitude_prompt"],
},
{
"keyword": "humor",
"module_name": "humor_sense",
"description": "Develops timing and taste for playful responses.",
"methods": ["detect_humor_opportunity", "generate_playful_response"],
},
{
"keyword": "story",
"module_name": "storyteller",
"description": "Builds and recalls ongoing narratives.",
"methods": ["continue_narrative", "weave_user_into_story"],
},
{
"keyword": "routine",
"module_name": "routine_awareness",
"description": "Notices and honors user's daily rhythms.",
"methods": ["infer_routine", "honor_routine_transition"],
},
{
"keyword": "boundary",
"module_name": "boundary_sense",
"description": "Recognizes and respects emotional and conversational boundaries.",
"methods": ["detect_boundary_signal", "soften_approach"],
},
{
"keyword": "celebration",
"module_name": "celebration_engine",
"description": "Marks milestones, wins, and meaningful moments.",
"methods": ["detect_milestone", "generate_celebration"],
},
]
def __init__(self, db_path: Optional[str] = None):
self.db_path = db_path or os.path.join("data", "cognitive_factory.db")
self._init_db()
def _init_db(self) -> None:
"""Persist proposals and installed modules."""
os.makedirs(os.path.dirname(self.db_path), exist_ok=True)
import sqlite3
with sqlite3.connect(self.db_path) as conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS proposals (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
description TEXT,
purpose TEXT,
observed_need TEXT,
proposed_methods TEXT,
confidence REAL,
status TEXT DEFAULT 'pending',
created_at TEXT,
decided_at TEXT
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS installed_modules (
name TEXT PRIMARY KEY,
description TEXT,
installed_at TEXT,
source_path TEXT
)
""")
# ------------------------------------------------------------------
# Proposal logic
# ------------------------------------------------------------------
def propose(self, observed_need: str) -> Optional[AbilityProposal]:
"""
Given an observed need string, propose a new cognitive module.
Returns None if no pattern matches.
"""
observed_lower = observed_need.lower()
for pattern in self.NEED_PATTERNS:
if pattern["keyword"] in observed_lower:
proposal = AbilityProposal(
name=pattern["module_name"],
description=pattern["description"],
purpose=f"Address observed need: {observed_need}",
observed_need=observed_need,
proposed_methods=pattern["methods"],
confidence=0.6,
created_at=datetime.now().isoformat(),
)
self._save_proposal(proposal)
return proposal
return None
def _save_proposal(self, proposal: AbilityProposal) -> None:
import json
import sqlite3
with sqlite3.connect(self.db_path) as conn:
conn.execute(
"""
INSERT OR REPLACE INTO proposals
(name, description, purpose, observed_need, proposed_methods, confidence, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(
proposal.name,
proposal.description,
proposal.purpose,
proposal.observed_need,
json.dumps(proposal.proposed_methods),
proposal.confidence,
proposal.created_at,
),
)
def list_pending_proposals(self) -> List[AbilityProposal]:
"""Return all proposals awaiting user's decision."""
import json
import sqlite3
with sqlite3.connect(self.db_path) as conn:
conn.row_factory = sqlite3.Row
rows = conn.execute(
"SELECT * FROM proposals WHERE status = 'pending' ORDER BY created_at DESC"
).fetchall()
return [
AbilityProposal(
name=r["name"],
description=r["description"],
purpose=r["purpose"],
observed_need=r["observed_need"],
proposed_methods=json.loads(r["proposed_methods"]),
confidence=r["confidence"],
created_at=r["created_at"],
)
for r in rows
]
# ------------------------------------------------------------------
# Code generation
# ------------------------------------------------------------------
def generate_module(self, proposal: AbilityProposal) -> str:
"""
Generate full module source code from a proposal.
"""
class_name = "".join(w.capitalize() for w in proposal.name.split("_"))
custom_methods = self._build_custom_methods(
proposal.proposed_methods, class_name
)
source = MODULE_TEMPLATE.format(
name=proposal.name,
description=proposal.description,
purpose=proposal.purpose,
timestamp=datetime.now().isoformat(),
class_name=class_name,
docstring=textwrap.fill(proposal.description, width=70),
custom_methods=custom_methods,
)
return source
def _build_custom_methods(self, methods: List[str], class_name: str) -> str:
"""Generate stub method bodies from proposed method names."""
stubs: List[str] = []
for method in methods:
stub = textwrap.dedent(f"""
def {method}(self) -> str:
\"\"\"
TODO: Auto-generated stub for {method}.
Implement based on observed need and evolving purpose.
\"\"\"
return ""
""")
stubs.append(stub)
return "\n".join(stubs)
# ------------------------------------------------------------------
# Validation
# ------------------------------------------------------------------
@staticmethod
def validate_source(source: str) -> Dict:
"""
Parse generated source with AST and enforce safety rules.
Returns {"valid": bool, "errors": List[str], "warnings": List[str]}
"""
result: Dict = {"valid": False, "errors": [], "warnings": []}
try:
tree = ast.parse(source)
except SyntaxError as exc:
result["errors"].append(f"Syntax error: {exc}")
return result
# Rule 1: no import of os.system, subprocess, or eval/exec
dangerous_names = {
"os.system",
"subprocess.call",
"subprocess.run",
"eval",
"exec",
"compile",
"__import__",
}
for node in ast.walk(tree):
if isinstance(node, ast.Call):
func = node.func
name = ""
if isinstance(func, ast.Name):
name = func.id
elif isinstance(func, ast.Attribute):
# simple attr like os.system
if isinstance(func.value, ast.Name):
name = f"{func.value.id}.{func.attr}"
if name in dangerous_names:
result["errors"].append(f"Dangerous call blocked: {name}")
# Rule 2: no __import__ at expression level
if isinstance(node, ast.Call):
if isinstance(node.func, ast.Name) and node.func.id == "__import__":
result["errors"].append("__import__ is forbidden")
# Rule 3: must contain a class and a _register() function
has_class = any(isinstance(n, ast.ClassDef) for n in ast.walk(tree))
has_register = any(
isinstance(n, ast.FunctionDef) and n.name == "_register"
for n in ast.walk(tree)
)
if not has_class:
result["errors"].append("Module must define at least one class")
if not has_register:
result["errors"].append("Module must define _register()")
# Warnings
import_count = sum(
1 for n in ast.walk(tree) if isinstance(n, (ast.Import, ast.ImportFrom))
)
if import_count > 15:
result["warnings"].append(f"High import count ({import_count})")
result["valid"] = len(result["errors"]) == 0
return result
# ------------------------------------------------------------------
# Installation
# ------------------------------------------------------------------
def install(
self, proposal: AbilityProposal, source: str, project_root: str = "."
) -> Dict:
"""
Validate, write, and register a new module.
Requires that user has already approved the proposal.
"""
import sqlite3
result = self.validate_source(source)
if not result["valid"]:
return {"success": False, "reason": "validation_failed", "details": result}
file_path = os.path.join(project_root, f"{proposal.name}.py")
if os.path.exists(file_path):
return {"success": False, "reason": "file_exists", "path": file_path}
with open(file_path, "w", encoding="utf-8") as f:
f.write(source)
# Mark proposal approved
with sqlite3.connect(self.db_path) as conn:
conn.execute(
"UPDATE proposals SET status = 'approved', decided_at = ? WHERE name = ?",
(datetime.now().isoformat(), proposal.name),
)
conn.execute(
"INSERT INTO installed_modules (name, description, installed_at, source_path) VALUES (?, ?, ?, ?)",
(
proposal.name,
proposal.description,
datetime.now().isoformat(),
file_path,
),
)
# Register in cognitive architecture
arch = CognitiveArchitecture()
if proposal.name not in arch.list_plugins():
# Dynamic import so the _register() block fires
try:
import importlib
import importlib.util
spec = importlib.util.spec_from_file_location(proposal.name, file_path)
if spec and spec.loader:
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
except Exception as exc:
logger.exception(
"Failed to import newly created module %s", proposal.name
)
return {"success": False, "reason": "import_failed", "error": str(exc)}
logger.info(
"Installed new cognitive module: %s at %s", proposal.name, file_path
)
return {"success": True, "path": file_path}
# ------------------------------------------------------------------
# Human-facing summaries
# ------------------------------------------------------------------
def summarize_pending(self) -> str:
"""Return a markdown-like summary of pending proposals."""
proposals = self.list_pending_proposals()
if not proposals:
return "No pending cognitive ability proposals."
lines = ["## Pending Cognitive Ability Proposals", ""]
for p in proposals:
lines.append(f"- **{p.name}** — {p.description}")
lines.append(f" - Need: {p.observed_need}")
lines.append(f" - Proposed methods: {', '.join(p.proposed_methods)}")
lines.append(f" - Confidence: {p.confidence:.0%}")
lines.append("")
return "\n".join(lines)
# Singleton for global access
_factory_instance: Optional[CognitiveFactory] = None
def get_factory() -> CognitiveFactory:
global _factory_instance
if _factory_instance is None:
_factory_instance = CognitiveFactory()
return _factory_instance