forked from Rockachopa/Timmy-time-dashboard
142 lines
4.8 KiB
Python
142 lines
4.8 KiB
Python
"""Timmy's thinking engine — public façade.
|
|
|
|
When the server starts, Timmy begins pondering: reflecting on his existence,
|
|
recent swarm activity, scripture, creative ideas, or pure stream of
|
|
consciousness. Each thought builds on the previous one, maintaining a
|
|
continuous chain of introspection.
|
|
|
|
Usage::
|
|
|
|
from timmy.thinking import thinking_engine
|
|
|
|
# Run one thinking cycle (called by the background loop)
|
|
await thinking_engine.think_once()
|
|
|
|
# Query the thought stream
|
|
thoughts = thinking_engine.get_recent_thoughts(limit=10)
|
|
chain = thinking_engine.get_thought_chain(thought_id)
|
|
"""
|
|
|
|
import logging
|
|
import sqlite3
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
# Re-export HOT_MEMORY_PATH and SOUL_PATH so existing patch targets continue to work.
|
|
# Tests that patch "timmy.thinking.HOT_MEMORY_PATH" or "timmy.thinking.SOUL_PATH"
|
|
# should instead patch "timmy.thinking._snapshot.HOT_MEMORY_PATH" etc., but these
|
|
# re-exports are kept for any code that reads them from the top-level namespace.
|
|
from timmy.memory_system import HOT_MEMORY_PATH, SOUL_PATH # noqa: F401
|
|
from timmy.thinking._db import Thought, _get_conn
|
|
from timmy.thinking.engine import ThinkingEngine
|
|
from timmy.thinking.seeds import (
|
|
_META_OBSERVATION_PHRASES,
|
|
_SENSITIVE_PATTERNS,
|
|
_THINK_TAG_RE,
|
|
_THINKING_PROMPT,
|
|
SEED_TYPES,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Module-level singleton
|
|
thinking_engine = ThinkingEngine()
|
|
|
|
__all__ = [
|
|
"ThinkingEngine",
|
|
"Thought",
|
|
"SEED_TYPES",
|
|
"thinking_engine",
|
|
"search_thoughts",
|
|
"_THINKING_PROMPT",
|
|
"_SENSITIVE_PATTERNS",
|
|
"_META_OBSERVATION_PHRASES",
|
|
"_THINK_TAG_RE",
|
|
"HOT_MEMORY_PATH",
|
|
"SOUL_PATH",
|
|
]
|
|
|
|
|
|
# ── Search helpers ─────────────────────────────────────────────────────────
|
|
|
|
|
|
def _query_thoughts(
|
|
db_path: Path, query: str, seed_type: str | None, limit: int
|
|
) -> list[sqlite3.Row]:
|
|
"""Run the thought-search SQL and return matching rows."""
|
|
pattern = f"%{query}%"
|
|
with _get_conn(db_path) as conn:
|
|
if seed_type:
|
|
return conn.execute(
|
|
"""
|
|
SELECT id, content, seed_type, created_at
|
|
FROM thoughts
|
|
WHERE content LIKE ? AND seed_type = ?
|
|
ORDER BY created_at DESC
|
|
LIMIT ?
|
|
""",
|
|
(pattern, seed_type, limit),
|
|
).fetchall()
|
|
return conn.execute(
|
|
"""
|
|
SELECT id, content, seed_type, created_at
|
|
FROM thoughts
|
|
WHERE content LIKE ?
|
|
ORDER BY created_at DESC
|
|
LIMIT ?
|
|
""",
|
|
(pattern, limit),
|
|
).fetchall()
|
|
|
|
|
|
def _format_thought_rows(rows: list[sqlite3.Row], query: str, seed_type: str | None) -> str:
|
|
"""Format thought rows into a human-readable string."""
|
|
lines = [f'Found {len(rows)} thought(s) matching "{query}":']
|
|
if seed_type:
|
|
lines[0] += f' [seed_type="{seed_type}"]'
|
|
lines.append("")
|
|
|
|
for row in rows:
|
|
ts = datetime.fromisoformat(row["created_at"])
|
|
local_ts = ts.astimezone()
|
|
time_str = local_ts.strftime("%Y-%m-%d %I:%M %p").lstrip("0")
|
|
seed = row["seed_type"]
|
|
content = row["content"].replace("\n", " ") # Flatten newlines for display
|
|
lines.append(f"[{time_str}] ({seed}) {content[:150]}")
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
def search_thoughts(query: str, seed_type: str | None = None, limit: int = 10) -> str:
|
|
"""Search Timmy's thought history for reflections matching a query.
|
|
|
|
Use this tool when Timmy needs to recall his previous thoughts on a topic,
|
|
reflect on past insights, or build upon earlier reflections. This enables
|
|
self-awareness and continuity of thinking across time.
|
|
|
|
Args:
|
|
query: Search term to match against thought content (case-insensitive).
|
|
seed_type: Optional filter by thought category (e.g., 'existential',
|
|
'swarm', 'sovereignty', 'creative', 'memory', 'observation').
|
|
limit: Maximum number of thoughts to return (default 10, max 50).
|
|
|
|
Returns:
|
|
Formatted string with matching thoughts, newest first, including
|
|
timestamps and seed types. Returns a helpful message if no matches found.
|
|
"""
|
|
limit = max(1, min(limit, 50))
|
|
|
|
try:
|
|
rows = _query_thoughts(thinking_engine._db_path, query, seed_type, limit)
|
|
|
|
if not rows:
|
|
if seed_type:
|
|
return f'No thoughts found matching "{query}" with seed_type="{seed_type}".'
|
|
return f'No thoughts found matching "{query}".'
|
|
|
|
return _format_thought_rows(rows, query, seed_type)
|
|
|
|
except Exception as exc:
|
|
logger.warning("Thought search failed: %s", exc)
|
|
return f"Error searching thoughts: {exc}"
|