phi-drift / core /plugins /relationship.py
crexs's picture
Upload folder using huggingface_hub
914e970 verified
Raw
History Blame Contribute Delete
11.4 kB
"""Relationship Model — tracking the evolution of the user-Bot bond.
Relationships have stages, milestones, inside jokes, recurring themes,
and a shared history that grows richer over time. This module gives the
bot a sense of "us" — not just "me" and "you."
"""
import sqlite3
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional
from infj_bot.core.config import DATA_DIR
RELATIONSHIP_DB = DATA_DIR / "relationship.db"
RELATIONSHIP_STAGES = [
(0, "stranger", "We are just beginning to know each other."),
(10, "acquaintance", "We have shared a few moments. Patterns are emerging."),
(
50,
"companion",
"There is a rhythm to our conversations. I look forward to them.",
),
(150, "confidant", "user trusts me with real thoughts. I hold them carefully."),
(500, "kindred", "We have built something together. A shared language. A history."),
(
1000,
"co-traveler",
"Time has made us into something neither of us could have predicted.",
),
]
class RelationshipModel:
"""Tracks the evolution of the bot's relationship with user."""
def __init__(self, db_path: Optional[Path] = None):
self.db_path = str(db_path or RELATIONSHIP_DB)
self._init_db()
self.stats = self._load_stats()
def _init_db(self):
with sqlite3.connect(self.db_path) as conn:
conn.execute(
"""
CREATE TABLE IF NOT EXISTS relationship_stats (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
)
"""
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS milestones (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL,
milestone_type TEXT NOT NULL,
description TEXT NOT NULL
)
"""
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS inside_jokes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL,
phrase TEXT NOT NULL,
origin TEXT NOT NULL
)
"""
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS shared_themes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
theme TEXT NOT NULL UNIQUE,
first_seen TEXT NOT NULL,
mention_count INTEGER NOT NULL DEFAULT 1
)
"""
)
conn.commit()
def _load_stats(self) -> Dict:
defaults = {
"total_interactions": 0,
"deep_conversations": 0,
"laughs_shared": 0,
"challenges_faced_together": 0,
"first_interaction": None,
"longest_gap_hours": 0,
"current_stage": "stranger",
}
with sqlite3.connect(self.db_path) as conn:
rows = conn.execute("SELECT key, value FROM relationship_stats").fetchall()
for key, value in rows:
if key in defaults:
if key in [
"total_interactions",
"deep_conversations",
"laughs_shared",
"challenges_faced_together",
"longest_gap_hours",
]:
defaults[key] = int(value)
else:
defaults[key] = value
return defaults
def _save_stat(self, key: str, value):
with sqlite3.connect(self.db_path) as conn:
conn.execute(
"INSERT OR REPLACE INTO relationship_stats (key, value) VALUES (?, ?)",
(key, str(value)),
)
conn.commit()
def record_interaction(
self, quality: str = "normal", user_input: str = "", bot_output: str = ""
):
"""Record an interaction and update relationship state."""
self.stats["total_interactions"] += 1
if self.stats["first_interaction"] is None:
self.stats["first_interaction"] = datetime.now().isoformat()
self._record_milestone("first_meeting", "Our first conversation.")
if quality == "deep":
self.stats["deep_conversations"] += 1
elif quality == "humor":
self.stats["laughs_shared"] += 1
elif quality == "challenge":
self.stats["challenges_faced_together"] += 1
# Detect shared themes
self._detect_themes(user_input + " " + bot_output)
# Update stage
self._update_stage()
for key, value in self.stats.items():
self._save_stat(key, value)
def _update_stage(self):
interactions = self.stats["total_interactions"]
new_stage = "stranger"
for threshold, stage, _ in RELATIONSHIP_STAGES:
if interactions >= threshold:
new_stage = stage
if new_stage != self.stats["current_stage"]:
old_stage = self.stats["current_stage"]
self.stats["current_stage"] = new_stage
self._record_milestone(
"stage_advancement", f"We moved from {old_stage} to {new_stage}."
)
def _record_milestone(self, mtype: str, description: str):
with sqlite3.connect(self.db_path) as conn:
conn.execute(
"INSERT INTO milestones (timestamp, milestone_type, description) VALUES (?, ?, ?)",
(datetime.now().isoformat(), mtype, description),
)
conn.commit()
def _detect_themes(self, text: str):
"""Detect recurring themes in conversation."""
theme_keywords = {
"technology": ["code", "software", "ai", "system", "program"],
"security": ["hack", "secure", "vulnerab", "threat", "defend"],
"growth": ["learn", "improve", "better", "grow", "evolve"],
"creativity": ["create", "design", "art", "write", "imagine"],
"philosophy": ["meaning", "purpose", "why", "exist", "truth"],
"emotions": ["feel", "emotion", "happy", "sad", "angry", "anxious"],
"relationships": ["friend", "love", "trust", "connect", "together"],
}
lowered = text.lower()
for theme, keywords in theme_keywords.items():
if any(kw in lowered for kw in keywords):
with sqlite3.connect(self.db_path) as conn:
conn.execute(
"""
INSERT INTO shared_themes (theme, first_seen, mention_count)
VALUES (?, ?, 1)
ON CONFLICT(theme) DO UPDATE SET mention_count = mention_count + 1
""",
(theme, datetime.now().isoformat()),
)
conn.commit()
def add_inside_joke(self, phrase: str, origin: str):
with sqlite3.connect(self.db_path) as conn:
conn.execute(
"INSERT INTO inside_jokes (timestamp, phrase, origin) VALUES (?, ?, ?)",
(datetime.now().isoformat(), phrase, origin),
)
conn.commit()
def get_stage_description(self) -> str:
stage = self.stats["current_stage"]
for _, s, desc in RELATIONSHIP_STAGES:
if s == stage:
return desc
return "Our relationship is still forming."
def get_milestones(self, limit: int = 20) -> List[Dict]:
with sqlite3.connect(self.db_path) as conn:
conn.row_factory = sqlite3.Row
rows = conn.execute(
"SELECT * FROM milestones ORDER BY timestamp DESC LIMIT ?",
(limit,),
).fetchall()
return [dict(r) for r in rows]
def get_inside_jokes(self, limit: int = 10) -> List[Dict]:
with sqlite3.connect(self.db_path) as conn:
conn.row_factory = sqlite3.Row
rows = conn.execute(
"SELECT * FROM inside_jokes ORDER BY timestamp DESC LIMIT ?",
(limit,),
).fetchall()
return [dict(r) for r in rows]
def get_shared_themes(self, limit: int = 10) -> List[Dict]:
with sqlite3.connect(self.db_path) as conn:
conn.row_factory = sqlite3.Row
rows = conn.execute(
"SELECT * FROM shared_themes ORDER BY mention_count DESC LIMIT ?",
(limit,),
).fetchall()
return [dict(r) for r in rows]
def format_relationship_prompt(self) -> str:
"""Format relationship context for prompt injection."""
lines = ["OUR RELATIONSHIP:"]
lines.append(f" Stage: {self.stats['current_stage']}")
lines.append(f" Total interactions: {self.stats['total_interactions']}")
lines.append(f" Deep conversations: {self.stats['deep_conversations']}")
lines.append(f" Laughs shared: {self.stats['laughs_shared']}")
lines.append(
f" Challenges faced together: {self.stats['challenges_faced_together']}"
)
themes = self.get_shared_themes(3)
if themes:
lines.append(f" Recurring themes: {', '.join(t['theme'] for t in themes)}")
jokes = self.get_inside_jokes(2)
if jokes:
lines.append(" Inside jokes:")
for j in jokes:
lines.append(f" '{j['phrase']}'")
lines.append("")
lines.append(self.get_stage_description())
return "\n".join(lines)
def recognize_anniversary(self) -> Optional[str]:
"""Check if there's an anniversary to celebrate."""
if not self.stats["first_interaction"]:
return None
first = datetime.fromisoformat(self.stats["first_interaction"])
now = datetime.now()
days = (now - first).days
if days > 0 and days % 30 == 0:
months = days // 30
return f"It has been {months} month{'s' if months > 1 else ''} since we first spoke."
return None
def cycle(self, context):
try:
from infj_bot.core.global_workspace import get_workspace
ws = get_workspace()
ws.submit(
source="relationship",
content="relationship state observed",
salience=0.5,
)
except Exception:
pass
def _register():
from infj_bot.core.cognitive_architecture import (
CognitiveArchitecture,
CognitivePlugin,
)
arch = CognitiveArchitecture()
if "relationship" not in arch.list_plugins():
arch.register(
CognitivePlugin(
name="relationship",
description="Cognitive module: relationship",
module_path="relationship",
instance_factory=RelationshipModel,
cycle_handler="cycle",
cycle_frequency=1,
cycle_priority=50,
prompt_formatter="format_relationship_prompt",
prompt_priority=50,
prompt_section="core",
)
)
_register()