This repository has been archived on 2026-03-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Timmy-time-dashboard/src/timmy/session.py

162 lines
5.2 KiB
Python
Raw Normal View History

"""Persistent chat session for Timmy.
Holds a singleton Agno Agent and a stable session_id so conversation
history persists across HTTP requests via Agno's SQLite storage.
This is the primary entry point for dashboard chat instead of
creating a new agent per request, we reuse a single instance and
let Agno's session_id mechanism handle conversation continuity.
"""
import logging
import re
from typing import Optional
logger = logging.getLogger(__name__)
# Default session ID for the dashboard (stable across requests)
_DEFAULT_SESSION_ID = "dashboard"
# Module-level singleton agent (lazy-initialized, reused for all requests)
_agent = None
# ---------------------------------------------------------------------------
# Response sanitization patterns
# ---------------------------------------------------------------------------
# Matches raw JSON tool calls: {"name": "python", "parameters": {...}}
_TOOL_CALL_JSON = re.compile(
r'\{\s*"name"\s*:\s*"[^"]+?"\s*,\s*"parameters"\s*:\s*\{.*?\}\s*\}',
re.DOTALL,
)
# Matches function-call-style text: memory_search(query="...") etc.
_FUNC_CALL_TEXT = re.compile(
r"\b(?:memory_search|web_search|shell|python|read_file|write_file|list_files|calculator)"
r"\s*\([^)]*\)",
)
# Matches chain-of-thought narration lines the model should keep internal
_COT_PATTERNS = [
re.compile(
r"^(?:Since |Using |Let me |I'll use |I will use |Here's a possible ).*$", re.MULTILINE
),
re.compile(r"^(?:I found a relevant |This context suggests ).*$", re.MULTILINE),
]
def _get_agent():
"""Lazy-initialize the singleton agent."""
global _agent
if _agent is None:
from timmy.agent import create_timmy
try:
_agent = create_timmy()
logger.info("Session: Timmy agent initialized (singleton)")
except Exception as exc:
logger.error("Session: Failed to create Timmy agent: %s", exc)
raise
return _agent
def chat(message: str, session_id: Optional[str] = None) -> str:
"""Send a message to Timmy and get a response.
Uses a persistent agent and session_id so Agno's SQLite history
provides multi-turn conversation context.
Args:
message: The user's message.
session_id: Optional session identifier (defaults to "dashboard").
Returns:
The agent's response text.
"""
sid = session_id or _DEFAULT_SESSION_ID
agent = _get_agent()
# Pre-processing: extract user facts
_extract_facts(message)
# Run with session_id so Agno retrieves history from SQLite
feat: agentic loop for multi-step tasks + regression fixes (#148) * fix: name extraction blocklist, memory preview escaping, and gitignore cleanup - Add _NAME_BLOCKLIST to extract_user_name() to reject gerunds and UI-state words like "Sending" that were incorrectly captured as user names - Collapse whitespace in get_memory_status() preview so newlines survive JSON serialization without showing raw \n escape sequences - Broaden .gitignore from specific memory/self/user_profile.md to memory/self/ and untrack memory/self/methodology.md (runtime-edited file) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: catch Ollama connection errors in session.py + add 71 smoke tests - Wrap agent.run() in session.py with try/except so Ollama connection failures return a graceful fallback message instead of dumping raw tracebacks to Docker logs - Add tests/test_smoke.py with 71 tests covering every GET route: core pages, feature pages, JSON APIs, and a parametrized no-500 sweep — catches import errors, template failures, and schema mismatches that unit tests miss Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: agentic loop for multi-step tasks + Round 10 regression fixes Agentic loop (Parts 1-4): - Add multi-step chaining instructions to system prompt - New agentic_loop.py with plan→execute→adapt→summarize flow - Register plan_and_execute tool for background task execution - Add max_agent_steps config setting (default: 10) - Discord fix: 300s timeout, typing indicator, send error handling - 16 new unit + e2e tests for agentic loop Round 10 regressions (R1-R5, P1): - R1: Fix literal \n escape sequences in tool responses - R2: Chat timeout/error feedback in agent panel - R3: /hands infinite spinner → static empty states - R4: /self-coding infinite spinner → static stats + journal - R5: /grok/status raw JSON → HTML dashboard template - P1: VETO confirmation dialog on task cards Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: briefing route 500 in CI when agno is MagicMock stub _call_agent() returned a MagicMock instead of a string when agno is stubbed in tests, causing SQLite "Error binding parameter 4" on save. Ensure the return value is always an actual string. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: briefing route 500 in CI — graceful degradation at route level When agno is stubbed with MagicMock in CI, agent.run() returns a MagicMock instead of raising — so the exception handler never fires and a MagicMock propagates as the summary to SQLite, which can't bind it. Fix: catch at the route level and return a fallback Briefing object. This follows the project's graceful degradation pattern — the briefing page always renders, even when the backend is completely unavailable. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Trip T <trip@local> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 01:46:29 -05:00
try:
run = agent.run(message, stream=False, session_id=sid)
response_text = run.content if hasattr(run, "content") else str(run)
except Exception as exc:
logger.error("Session: agent.run() failed: %s", exc)
return "I'm having trouble reaching my language model right now. Please try again shortly."
# Post-processing: clean up any leaked tool calls or chain-of-thought
response_text = _clean_response(response_text)
return response_text
def reset_session(session_id: Optional[str] = None) -> None:
"""Reset a session (clear conversation context).
This clears the ConversationManager state. Agno's SQLite history
is not cleared that provides long-term continuity.
"""
sid = session_id or _DEFAULT_SESSION_ID
try:
from timmy.conversation import conversation_manager
conversation_manager.clear_context(sid)
except Exception as exc:
logger.debug("Session: context clear failed for %s: %s", sid, exc)
def _extract_facts(message: str) -> None:
"""Extract user facts from message and persist to memory system.
Ported from TimmyWithMemory._extract_and_store_facts().
Runs as a best-effort post-processor failures are logged, not raised.
"""
try:
from timmy.conversation import conversation_manager
name = conversation_manager.extract_user_name(message)
if name:
try:
from timmy.memory_system import memory_system
memory_system.update_user_fact("Name", name)
logger.info("Session: Learned user name: %s", name)
except Exception as exc:
logger.debug("Session: fact persist failed: %s", exc)
except Exception as exc:
logger.debug("Session: Fact extraction skipped: %s", exc)
def _clean_response(text: str) -> str:
"""Remove hallucinated tool calls and chain-of-thought narration.
Small models sometimes output raw JSON tool calls or narrate their
internal reasoning instead of just answering. This strips those
artifacts from the response.
"""
if not text:
return text
feat: agentic loop for multi-step tasks + regression fixes (#148) * fix: name extraction blocklist, memory preview escaping, and gitignore cleanup - Add _NAME_BLOCKLIST to extract_user_name() to reject gerunds and UI-state words like "Sending" that were incorrectly captured as user names - Collapse whitespace in get_memory_status() preview so newlines survive JSON serialization without showing raw \n escape sequences - Broaden .gitignore from specific memory/self/user_profile.md to memory/self/ and untrack memory/self/methodology.md (runtime-edited file) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: catch Ollama connection errors in session.py + add 71 smoke tests - Wrap agent.run() in session.py with try/except so Ollama connection failures return a graceful fallback message instead of dumping raw tracebacks to Docker logs - Add tests/test_smoke.py with 71 tests covering every GET route: core pages, feature pages, JSON APIs, and a parametrized no-500 sweep — catches import errors, template failures, and schema mismatches that unit tests miss Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: agentic loop for multi-step tasks + Round 10 regression fixes Agentic loop (Parts 1-4): - Add multi-step chaining instructions to system prompt - New agentic_loop.py with plan→execute→adapt→summarize flow - Register plan_and_execute tool for background task execution - Add max_agent_steps config setting (default: 10) - Discord fix: 300s timeout, typing indicator, send error handling - 16 new unit + e2e tests for agentic loop Round 10 regressions (R1-R5, P1): - R1: Fix literal \n escape sequences in tool responses - R2: Chat timeout/error feedback in agent panel - R3: /hands infinite spinner → static empty states - R4: /self-coding infinite spinner → static stats + journal - R5: /grok/status raw JSON → HTML dashboard template - P1: VETO confirmation dialog on task cards Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: briefing route 500 in CI when agno is MagicMock stub _call_agent() returned a MagicMock instead of a string when agno is stubbed in tests, causing SQLite "Error binding parameter 4" on save. Ensure the return value is always an actual string. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: briefing route 500 in CI — graceful degradation at route level When agno is stubbed with MagicMock in CI, agent.run() returns a MagicMock instead of raising — so the exception handler never fires and a MagicMock propagates as the summary to SQLite, which can't bind it. Fix: catch at the route level and return a fallback Briefing object. This follows the project's graceful degradation pattern — the briefing page always renders, even when the backend is completely unavailable. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Trip T <trip@local> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 01:46:29 -05:00
# Convert literal \n escape sequences to actual newlines
# (models sometimes output these in tool-result text)
text = text.replace("\\n", "\n")
# Strip JSON tool call blocks
text = _TOOL_CALL_JSON.sub("", text)
# Strip function-call-style text
text = _FUNC_CALL_TEXT.sub("", text)
# Strip chain-of-thought narration lines
for pattern in _COT_PATTERNS:
text = pattern.sub("", text)
# Clean up leftover blank lines and whitespace
lines = [line for line in text.split("\n") if line.strip()]
text = "\n".join(lines)
return text.strip()