diff --git a/src/dashboard/app.py b/src/dashboard/app.py index 042b9965..5ded70c6 100644 --- a/src/dashboard/app.py +++ b/src/dashboard/app.py @@ -233,6 +233,22 @@ _SYNTHESIZED_STATE: dict = { } + +async function _reflection_scheduler() -> None: + """Background task: execute Timmy's self-reflection cycle every 4 hours.""" + from timmy.reflection import reflection_engine + + await asyncio.sleep(15) # Stagger after other schedulers + + while True: + try: + await reflection_engine.reflect_once() + except Exception as exc: + logger.error("Reflection scheduler error: %s", exc) + + await asyncio.sleep(4 * 3600) + + async def _presence_watcher() -> None: """Background task: watch ~/.timmy/presence.json and broadcast changes via WS. @@ -380,6 +396,7 @@ def _startup_background_tasks() -> list[asyncio.Task]: asyncio.create_task(_thinking_scheduler()), asyncio.create_task(_loop_qa_scheduler()), asyncio.create_task(_presence_watcher()), + asyncio.create_task(_reflection_scheduler()), asyncio.create_task(_start_chat_integrations_background()), ] diff --git a/src/dashboard/models/__init__.py b/src/dashboard/models/__init__.py new file mode 100644 index 00000000..4f0b2d64 --- /dev/null +++ b/src/dashboard/models/__init__.py @@ -0,0 +1 @@ +from .calm import Task, JournalEntry\nfrom .reflection import Reflection\n \ No newline at end of file diff --git a/src/dashboard/models/reflection.py b/src/dashboard/models/reflection.py new file mode 100644 index 00000000..b5069f51 --- /dev/null +++ b/src/dashboard/models/reflection.py @@ -0,0 +1,14 @@ + +from datetime import UTC, datetime +from sqlalchemy import Column, DateTime, Integer, String, Text +from .database import Base + +class Reflection(Base): + __tablename__ = "reflections" + + id = Column(Integer, primary_key=True, index=True) + content = Column(Text, nullable=False) + sentiment = Column(String(50), nullable=True) + focus_area = Column(String(100), nullable=True) + + created_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False) diff --git a/src/dashboard/routes/thinking.py b/src/dashboard/routes/thinking.py index a15b39ca..45b40808 100644 --- a/src/dashboard/routes/thinking.py +++ b/src/dashboard/routes/thinking.py @@ -61,3 +61,11 @@ async def thought_chain_api(thought_id: str): } for t in chain ] + +@router.post("/reflect", response_class=JSONResponse) +async def trigger_reflection(): + """Trigger a self-reflection cycle.""" + reflection = await thinking_engine.reflect() + if not reflection: + return JSONResponse({"error": "Failed to generate reflection"}, status_code=500) + return {"status": "ok", "reflection": reflection} diff --git a/src/timmy/reflection.py b/src/timmy/reflection.py new file mode 100644 index 00000000..81362f10 --- /dev/null +++ b/src/timmy/reflection.py @@ -0,0 +1,82 @@ + +import logging +from datetime import UTC, datetime, timedelta +from typing import List, Optional + +from sqlalchemy.orm import Session +from dashboard.models.database import SessionLocal +from dashboard.models.calm import Task, JournalEntry +from dashboard.models.reflection import Reflection +from integrations.llm.ollama import query_ollama + +logger = logging.getLogger(__name__) + +class ReflectionEngine: + """Engine for Timmy's self-reflection loop.""" + + async def reflect_once(self) -> Optional[Reflection]: + """Review recent activity and generate a reflection.""" + logger.info("Starting self-reflection cycle...") + + db = SessionLocal() + try: + # 1. Gather context + now = datetime.now(UTC) + since = now - timedelta(hours=24) + + recent_tasks = db.query(Task).filter(Task.updated_at >= since).all() + recent_journal = db.query(JournalEntry).filter(JournalEntry.created_at >= since).first() + + # 2. Build prompt + context = f"Recent Tasks: {[t.title for t in recent_tasks]}\n" + if recent_journal: + context += f"Journal: {recent_journal.evening_reflection}\n" + + prompt = f""" + You are Timmy, an AI agent. Review your recent activity and provide a short, insightful self-reflection. + Focus on what you've achieved, what you've missed, and how you're feeling about your current progress. + + Context: + {context} + + Output format: + Reflection: [Your reflection text] + Sentiment: [positive/neutral/negative] + Focus Area: [e.g., Productivity, Health, Learning] + """ + + # 3. Query LLM + response = await query_ollama(prompt) + + # 4. Parse and save + reflection_text = "" + sentiment = "neutral" + focus_area = "General" + + for line in response.split("\n"): + if line.startswith("Reflection:"): + reflection_text = line.replace("Reflection:", "").strip() + elif line.startswith("Sentiment:"): + sentiment = line.replace("Sentiment:", "").strip().lower() + elif line.startswith("Focus Area:"): + focus_area = line.replace("Focus Area:", "").strip() + + if reflection_text: + reflection = Reflection( + content=reflection_text, + sentiment=sentiment, + focus_area=focus_area + ) + db.add(reflection) + db.commit() + db.refresh(reflection) + logger.info("Self-reflection saved: %s", reflection_text[:50]) + return reflection + + except Exception as exc: + logger.error("Reflection error: %s", exc) + finally: + db.close() + return None + +reflection_engine = ReflectionEngine() diff --git a/src/timmy/thinking.py b/src/timmy/thinking.py index 6bab20c9..450793d9 100644 --- a/src/timmy/thinking.py +++ b/src/timmy/thinking.py @@ -316,6 +316,48 @@ class ThinkingEngine: self._write_journal(thought) await self._broadcast(thought) + async def reflect(self) -> str | None: + """Periodic self-reflection: summarize recent state and goals.""" + from timmy.memory_system import get_memory_context, store_last_reflection + + # 1. Gather context + recent_thoughts = self.get_recent_thoughts(limit=10) + thought_text = " +".join([f"- {t.content}" for t in reversed(recent_thoughts)]) + + memory_context = get_memory_context("recent activity", max_tokens=1000) + system_snapshot = self._gather_system_snapshot() + + prompt = f""" +You are Timmy, reflecting on your current state. +Review your recent thoughts and activity to create a concise "Self-Reflection". + +Recent Thoughts: +{thought_text} + +Recent Memory Context: +{memory_context} + +System Snapshot: +{system_snapshot} + +Write a 3-4 sentence reflection that summarizes: +1. What you've been focused on recently. +2. Your current "vibe" or state of mind. +3. Your immediate goals or what you're pondering next. + +Be introspective and sovereign. +""" + try: + reflection = await self._call_agent(prompt) + if reflection: + store_last_reflection(reflection.strip()) + logger.info("ThinkingEngine: Generated self-reflection") + return reflection.strip() + except Exception as exc: + logger.error("Self-reflection failed: %s", exc) + return None + async def think_once(self, prompt: str | None = None) -> Thought | None: """Execute one thinking cycle.