2026-02-25 19:18:08 -05:00
|
|
|
"""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
|
|
|
|
|
|
2026-03-14 18:40:15 -04:00
|
|
|
import httpx
|
|
|
|
|
|
2026-03-18 21:37:17 -04:00
|
|
|
from timmy.cognitive_state import cognitive_tracker
|
2026-03-15 13:58:35 -04:00
|
|
|
from timmy.confidence import estimate_confidence
|
2026-03-15 12:34:48 -04:00
|
|
|
from timmy.session_logger import get_session_logger
|
|
|
|
|
|
2026-02-25 19:18:08 -05:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
2026-03-18 22:01:51 -04:00
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Confidence annotation (SOUL.md: visible uncertainty)
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
_CONFIDENCE_THRESHOLD = 0.7
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _annotate_confidence(text: str, confidence: float | None) -> str:
|
|
|
|
|
"""Append a confidence tag when below threshold.
|
|
|
|
|
|
|
|
|
|
SOUL.md: "When I am uncertain, I must say so in proportion to my uncertainty."
|
|
|
|
|
"""
|
|
|
|
|
if confidence is not None and confidence < _CONFIDENCE_THRESHOLD:
|
|
|
|
|
return text + f"\n\n[confidence: {confidence:.0%}]"
|
|
|
|
|
return text
|
|
|
|
|
|
|
|
|
|
|
2026-02-25 19:18:08 -05:00
|
|
|
# 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(
|
2026-03-09 21:54:04 -04:00
|
|
|
r'\{\s*"name"\s*:\s*"[^"]+?"\s*,\s*"(?:parameters|arguments)"\s*:\s*\{.*?\}\s*\}',
|
2026-02-25 19:18:08 -05:00
|
|
|
re.DOTALL,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Matches function-call-style text: memory_search(query="...") etc.
|
|
|
|
|
_FUNC_CALL_TEXT = re.compile(
|
2026-03-14 18:13:51 -04:00
|
|
|
r"\b(?:memory_search|shell|python|read_file|write_file|list_files|calculator)"
|
2026-03-08 12:50:44 -04:00
|
|
|
r"\s*\([^)]*\)",
|
2026-02-25 19:18:08 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Matches chain-of-thought narration lines the model should keep internal
|
|
|
|
|
_COT_PATTERNS = [
|
2026-03-08 12:50:44 -04:00
|
|
|
re.compile(
|
|
|
|
|
r"^(?:Since |Using |Let me |I'll use |I will use |Here's a possible ).*$", re.MULTILINE
|
|
|
|
|
),
|
2026-02-25 19:18:08 -05:00
|
|
|
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
|
2026-03-08 12:50:44 -04:00
|
|
|
|
2026-02-25 19:18:08 -05:00
|
|
|
try:
|
2026-03-14 19:43:11 -04:00
|
|
|
_agent = create_timmy(session_id=_DEFAULT_SESSION_ID)
|
2026-02-25 19:18:08 -05:00
|
|
|
logger.info("Session: Timmy agent initialized (singleton)")
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
logger.error("Session: Failed to create Timmy agent: %s", exc)
|
|
|
|
|
raise
|
|
|
|
|
return _agent
|
|
|
|
|
|
|
|
|
|
|
2026-03-12 21:40:32 -04:00
|
|
|
async def chat(message: str, session_id: str | None = None) -> str:
|
2026-02-25 19:18:08 -05:00
|
|
|
"""Send a message to Timmy and get a response.
|
|
|
|
|
|
|
|
|
|
Uses a persistent agent and session_id so Agno's SQLite history
|
2026-03-12 21:40:32 -04:00
|
|
|
provides multi-turn conversation context. Uses ``arun()`` so MCP
|
|
|
|
|
tool servers are auto-connected.
|
2026-02-25 19:18:08 -05:00
|
|
|
|
|
|
|
|
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()
|
2026-03-15 12:34:48 -04:00
|
|
|
session_logger = get_session_logger()
|
|
|
|
|
|
|
|
|
|
# Record user message before sending to agent
|
|
|
|
|
session_logger.record_message("user", message)
|
2026-02-25 19:18:08 -05:00
|
|
|
|
|
|
|
|
# Pre-processing: extract user facts
|
|
|
|
|
_extract_facts(message)
|
|
|
|
|
|
2026-03-19 02:54:19 -04:00
|
|
|
# Inject deep-focus context when active
|
|
|
|
|
message = _prepend_focus_context(message)
|
|
|
|
|
|
2026-02-25 19:18:08 -05:00
|
|
|
# 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:
|
2026-03-12 21:40:32 -04:00
|
|
|
run = await agent.arun(message, stream=False, session_id=sid)
|
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
|
|
|
response_text = run.content if hasattr(run, "content") else str(run)
|
2026-03-14 18:40:15 -04:00
|
|
|
except (httpx.ConnectError, httpx.ReadError, ConnectionError) as exc:
|
|
|
|
|
logger.error("Ollama disconnected: %s", exc)
|
2026-03-15 12:34:48 -04:00
|
|
|
session_logger.record_error(str(exc), context="chat")
|
|
|
|
|
session_logger.flush()
|
2026-03-14 18:40:15 -04:00
|
|
|
return "Ollama appears to be disconnected. Check that ollama serve is running."
|
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
|
|
|
except Exception as exc:
|
2026-03-12 21:40:32 -04:00
|
|
|
logger.error("Session: agent.arun() failed: %s", exc)
|
2026-03-15 12:34:48 -04:00
|
|
|
session_logger.record_error(str(exc), context="chat")
|
|
|
|
|
session_logger.flush()
|
2026-03-18 20:27:06 -04:00
|
|
|
return (
|
|
|
|
|
"I'm having trouble reaching my inference backend right now. Please try again shortly."
|
|
|
|
|
)
|
2026-02-25 19:18:08 -05:00
|
|
|
|
|
|
|
|
# Post-processing: clean up any leaked tool calls or chain-of-thought
|
|
|
|
|
response_text = _clean_response(response_text)
|
|
|
|
|
|
2026-03-15 13:58:35 -04:00
|
|
|
# Estimate confidence of the response
|
|
|
|
|
confidence = estimate_confidence(response_text)
|
|
|
|
|
logger.debug("Response confidence: %.2f", confidence)
|
|
|
|
|
|
2026-03-18 22:01:51 -04:00
|
|
|
response_text = _annotate_confidence(response_text, confidence)
|
2026-03-15 19:36:52 -04:00
|
|
|
|
2026-03-15 12:34:48 -04:00
|
|
|
# Record Timmy response after getting it
|
2026-03-15 13:58:35 -04:00
|
|
|
session_logger.record_message("timmy", response_text, confidence=confidence)
|
2026-03-15 12:34:48 -04:00
|
|
|
|
2026-03-18 21:37:17 -04:00
|
|
|
# Update cognitive state (observable signal for Matrix avatar)
|
|
|
|
|
cognitive_tracker.update(message, response_text)
|
|
|
|
|
|
2026-03-15 12:34:48 -04:00
|
|
|
# Flush session logs to disk
|
|
|
|
|
session_logger.flush()
|
|
|
|
|
|
2026-02-25 19:18:08 -05:00
|
|
|
return response_text
|
|
|
|
|
|
|
|
|
|
|
2026-03-12 21:40:32 -04:00
|
|
|
async def chat_with_tools(message: str, session_id: str | None = None):
|
2026-03-09 21:54:04 -04:00
|
|
|
"""Send a message and return the full Agno RunOutput.
|
|
|
|
|
|
|
|
|
|
Callers should check ``run_output.status``:
|
|
|
|
|
- ``RunStatus.paused`` — tools need confirmation (see ``run_output.requirements``)
|
|
|
|
|
- ``RunStatus.completed`` — response ready in ``run_output.content``
|
|
|
|
|
|
2026-03-12 21:40:32 -04:00
|
|
|
Uses ``arun()`` so MCP tool servers are auto-connected.
|
|
|
|
|
|
2026-03-09 21:54:04 -04:00
|
|
|
Returns:
|
|
|
|
|
An Agno ``RunOutput`` object (or a lightweight surrogate on error).
|
|
|
|
|
"""
|
|
|
|
|
sid = session_id or _DEFAULT_SESSION_ID
|
|
|
|
|
agent = _get_agent()
|
2026-03-15 12:34:48 -04:00
|
|
|
session_logger = get_session_logger()
|
|
|
|
|
|
|
|
|
|
# Record user message before sending to agent
|
|
|
|
|
session_logger.record_message("user", message)
|
|
|
|
|
|
2026-03-09 21:54:04 -04:00
|
|
|
_extract_facts(message)
|
|
|
|
|
|
2026-03-19 02:54:19 -04:00
|
|
|
# Inject deep-focus context when active
|
|
|
|
|
message = _prepend_focus_context(message)
|
|
|
|
|
|
2026-03-09 21:54:04 -04:00
|
|
|
try:
|
2026-03-15 12:34:48 -04:00
|
|
|
run_output = await agent.arun(message, stream=False, session_id=sid)
|
|
|
|
|
# Record Timmy response after getting it
|
2026-03-15 13:40:40 -04:00
|
|
|
response_text = (
|
|
|
|
|
run_output.content if hasattr(run_output, "content") and run_output.content else ""
|
|
|
|
|
)
|
2026-03-15 13:58:35 -04:00
|
|
|
confidence = estimate_confidence(response_text) if response_text else None
|
|
|
|
|
logger.debug("Response confidence: %.2f", confidence)
|
2026-03-15 19:36:52 -04:00
|
|
|
|
2026-03-18 22:01:51 -04:00
|
|
|
response_text = _annotate_confidence(response_text, confidence)
|
|
|
|
|
run_output.content = response_text
|
2026-03-15 19:36:52 -04:00
|
|
|
|
2026-03-15 13:58:35 -04:00
|
|
|
session_logger.record_message("timmy", response_text, confidence=confidence)
|
2026-03-15 12:34:48 -04:00
|
|
|
session_logger.flush()
|
|
|
|
|
return run_output
|
2026-03-14 18:40:15 -04:00
|
|
|
except (httpx.ConnectError, httpx.ReadError, ConnectionError) as exc:
|
|
|
|
|
logger.error("Ollama disconnected: %s", exc)
|
2026-03-15 12:34:48 -04:00
|
|
|
session_logger.record_error(str(exc), context="chat_with_tools")
|
|
|
|
|
session_logger.flush()
|
2026-03-14 18:40:15 -04:00
|
|
|
return _ErrorRunOutput(
|
|
|
|
|
"Ollama appears to be disconnected. Check that ollama serve is running."
|
|
|
|
|
)
|
2026-03-09 21:54:04 -04:00
|
|
|
except Exception as exc:
|
2026-03-12 21:40:32 -04:00
|
|
|
logger.error("Session: agent.arun() failed: %s", exc)
|
2026-03-15 12:34:48 -04:00
|
|
|
session_logger.record_error(str(exc), context="chat_with_tools")
|
|
|
|
|
session_logger.flush()
|
2026-03-09 21:54:04 -04:00
|
|
|
# Return a duck-typed object that callers can handle uniformly
|
|
|
|
|
return _ErrorRunOutput(
|
2026-03-18 20:27:06 -04:00
|
|
|
"I'm having trouble reaching my inference backend right now. Please try again shortly."
|
2026-03-09 21:54:04 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-03-12 21:40:32 -04:00
|
|
|
async def continue_chat(run_output, session_id: str | None = None):
|
2026-03-09 21:54:04 -04:00
|
|
|
"""Resume a paused run after tool confirmation / rejection.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
run_output: The paused ``RunOutput`` returned by ``chat_with_tools()``.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
A new ``RunOutput`` with the resumed execution results.
|
|
|
|
|
"""
|
|
|
|
|
sid = session_id or _DEFAULT_SESSION_ID
|
|
|
|
|
agent = _get_agent()
|
2026-03-15 12:34:48 -04:00
|
|
|
session_logger = get_session_logger()
|
2026-03-09 21:54:04 -04:00
|
|
|
|
|
|
|
|
try:
|
2026-03-15 12:34:48 -04:00
|
|
|
result = await agent.acontinue_run(run_response=run_output, stream=False, session_id=sid)
|
|
|
|
|
# Record Timmy response after getting it
|
|
|
|
|
response_text = result.content if hasattr(result, "content") and result.content else ""
|
2026-03-15 13:58:35 -04:00
|
|
|
confidence = estimate_confidence(response_text) if response_text else None
|
|
|
|
|
logger.debug("Response confidence: %.2f", confidence)
|
2026-03-15 19:36:52 -04:00
|
|
|
|
2026-03-18 22:01:51 -04:00
|
|
|
response_text = _annotate_confidence(response_text, confidence)
|
|
|
|
|
result.content = response_text
|
2026-03-15 19:36:52 -04:00
|
|
|
|
2026-03-15 13:58:35 -04:00
|
|
|
session_logger.record_message("timmy", response_text, confidence=confidence)
|
2026-03-15 12:34:48 -04:00
|
|
|
session_logger.flush()
|
|
|
|
|
return result
|
2026-03-14 18:40:15 -04:00
|
|
|
except (httpx.ConnectError, httpx.ReadError, ConnectionError) as exc:
|
|
|
|
|
logger.error("Ollama disconnected: %s", exc)
|
2026-03-15 12:34:48 -04:00
|
|
|
session_logger.record_error(str(exc), context="continue_chat")
|
|
|
|
|
session_logger.flush()
|
2026-03-14 18:40:15 -04:00
|
|
|
return _ErrorRunOutput(
|
|
|
|
|
"Ollama appears to be disconnected. Check that ollama serve is running."
|
|
|
|
|
)
|
2026-03-09 21:54:04 -04:00
|
|
|
except Exception as exc:
|
2026-03-12 21:40:32 -04:00
|
|
|
logger.error("Session: agent.acontinue_run() failed: %s", exc)
|
2026-03-15 12:34:48 -04:00
|
|
|
session_logger.record_error(str(exc), context="continue_chat")
|
|
|
|
|
session_logger.flush()
|
2026-03-09 21:54:04 -04:00
|
|
|
return _ErrorRunOutput(f"Error continuing run: {exc}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _ErrorRunOutput:
|
|
|
|
|
"""Lightweight stand-in for RunOutput when the model is unreachable."""
|
|
|
|
|
|
|
|
|
|
def __init__(self, message: str):
|
|
|
|
|
self.content = message
|
|
|
|
|
self.status = "ERROR"
|
|
|
|
|
self.requirements = []
|
|
|
|
|
self.tools = []
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def active_requirements(self):
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
2026-03-12 21:40:32 -04:00
|
|
|
async def chat_raw(message: str, session_id: str | None = None) -> tuple[str, str]:
|
2026-03-09 21:54:04 -04:00
|
|
|
"""Send a message and return both cleaned and raw responses.
|
|
|
|
|
|
|
|
|
|
Backward-compatible wrapper around :func:`chat_with_tools`.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
(cleaned_response, raw_response) — cleaned has tool-call JSON and
|
|
|
|
|
chain-of-thought stripped; raw is the model's original output.
|
|
|
|
|
"""
|
2026-03-12 21:40:32 -04:00
|
|
|
run = await chat_with_tools(message, session_id)
|
2026-03-09 21:54:04 -04:00
|
|
|
raw_response = run.content if hasattr(run, "content") and run.content else ""
|
|
|
|
|
cleaned = _clean_response(raw_response)
|
|
|
|
|
return cleaned, raw_response
|
|
|
|
|
|
|
|
|
|
|
ruff (#169)
* polish: streamline nav, extract inline styles, improve tablet UX
- Restructure desktop nav from 8+ flat links + overflow dropdown into
5 grouped dropdowns (Core, Agents, Intel, System, More) matching
the mobile menu structure to reduce decision fatigue
- Extract all inline styles from mission_control.html and base.html
notification elements into mission-control.css with semantic classes
- Replace JS-built innerHTML with secure DOM construction in
notification loader and chat history
- Add CONNECTING state to connection indicator (amber) instead of
showing OFFLINE before WebSocket connects
- Add tablet breakpoint (1024px) with larger touch targets for
Apple Pencil / stylus use and safe-area padding for iPad toolbar
- Add active-link highlighting in desktop dropdown menus
- Rename "Mission Control" page title to "System Overview" to
disambiguate from the chat home page
- Add "Home — Timmy Time" page title to index.html
https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h
* fix(security): move auth-gate credentials to environment variables
Hardcoded username, password, and HMAC secret in auth-gate.py replaced
with os.environ lookups. Startup now refuses to run if any variable is
unset. Added AUTH_GATE_SECRET/USER/PASS to .env.example.
https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h
* refactor(tooling): migrate from black+isort+bandit to ruff
Replace three separate linting/formatting tools with a single ruff
invocation. Updates tox.ini (lint, format, pre-push, pre-commit envs),
.pre-commit-config.yaml, and CI workflow. Fixes all ruff errors
including unused imports, missing raise-from, and undefined names.
Ruff config maps existing bandit skips to equivalent S-rules.
https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-03-11 12:23:35 -04:00
|
|
|
def reset_session(session_id: str | None = None) -> None:
|
2026-02-25 19:18:08 -05:00
|
|
|
"""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
|
2026-03-08 12:50:44 -04:00
|
|
|
|
2026-02-25 19:18:08 -05:00
|
|
|
conversation_manager.clear_context(sid)
|
2026-03-07 18:49:37 -05:00
|
|
|
except Exception as exc:
|
|
|
|
|
logger.debug("Session: context clear failed for %s: %s", sid, exc)
|
2026-02-25 19:18:08 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
2026-03-08 12:50:44 -04:00
|
|
|
|
2026-02-25 19:18:08 -05:00
|
|
|
name = conversation_manager.extract_user_name(message)
|
|
|
|
|
if name:
|
|
|
|
|
try:
|
|
|
|
|
from timmy.memory_system import memory_system
|
2026-03-08 12:50:44 -04:00
|
|
|
|
2026-02-25 19:18:08 -05:00
|
|
|
memory_system.update_user_fact("Name", name)
|
|
|
|
|
logger.info("Session: Learned user name: %s", name)
|
2026-03-07 18:49:37 -05:00
|
|
|
except Exception as exc:
|
|
|
|
|
logger.debug("Session: fact persist failed: %s", exc)
|
2026-02-25 19:18:08 -05:00
|
|
|
except Exception as exc:
|
|
|
|
|
logger.debug("Session: Fact extraction skipped: %s", exc)
|
|
|
|
|
|
|
|
|
|
|
2026-03-19 02:54:19 -04:00
|
|
|
def _prepend_focus_context(message: str) -> str:
|
|
|
|
|
"""Prepend deep-focus context to a message when focus mode is active."""
|
|
|
|
|
try:
|
|
|
|
|
from timmy.focus import focus_manager
|
|
|
|
|
|
|
|
|
|
ctx = focus_manager.get_focus_context()
|
|
|
|
|
if ctx:
|
|
|
|
|
return f"{ctx}\n\n{message}"
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
logger.debug("Focus context injection skipped: %s", exc)
|
|
|
|
|
return message
|
|
|
|
|
|
|
|
|
|
|
2026-02-25 19:18:08 -05:00
|
|
|
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")
|
|
|
|
|
|
2026-02-25 19:18:08 -05:00
|
|
|
# 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()
|