fix: resolve 6 dashboard bugs and rebuild Task Queue + Work Orders (#144) (#144)

Round 2+3 bug fix batch:

1. Ollama timeout: Add request_timeout=300 to prevent socket read errors
   on complex 30-60s prompts (production crash fix)

2. Memory API: Create missing HTMX partial templates (memory_facts.html,
   memory_results.html) so Save/Search buttons work

3. CALM page: Add create_tables() call so SQLAlchemy tables exist on
   first request (was returning HTTP 500)

4. Task Queue: Full SQLite-backed rebuild with CRUD endpoints, HTMX
   partials, and action buttons (approve/veto/pause/cancel/retry)

5. Work Orders: Full SQLite-backed rebuild with submit/approve/reject/
   execute pipeline and HTMX polling partials

6. Memory READ tool: Add memory_read function so Timmy stops calling
   read_file when trying to recall stored facts

Also: Close GitHub issues #115, #114, #112, #110 as won't-fix.
Comment on #107 confirming prune_memories() already wired to startup.

Tests: 33 new tests across 4 test files, all passing.
Full suite: 1155 passed, 2 pre-existing failures (hands_shell).

Co-authored-by: Trip T <trip@local>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Alexander Whitestone
2026-03-07 23:21:30 -05:00
committed by GitHub
parent b8164e46b0
commit e36a1dc939
18 changed files with 1036 additions and 14 deletions

View File

@@ -38,6 +38,8 @@ from dashboard.routes.chat_api import router as chat_api_router
from dashboard.routes.thinking import router as thinking_router
from dashboard.routes.calm import router as calm_router
from dashboard.routes.swarm import router as swarm_router
from dashboard.routes.tasks import router as tasks_router
from dashboard.routes.work_orders import router as work_orders_router
from dashboard.routes.system import router as system_router
from dashboard.routes.paperclip import router as paperclip_router
from infrastructure.router.api import router as cascade_router
@@ -333,6 +335,8 @@ app.include_router(chat_api_router)
app.include_router(thinking_router)
app.include_router(calm_router)
app.include_router(swarm_router)
app.include_router(tasks_router)
app.include_router(work_orders_router)
app.include_router(system_router)
app.include_router(paperclip_router)
app.include_router(cascade_router)

View File

@@ -12,6 +12,11 @@ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
def create_tables():
"""Create all tables defined by models that have imported Base."""
Base.metadata.create_all(bind=engine)
def get_db():
db = SessionLocal()
try:

View File

@@ -8,9 +8,12 @@ from fastapi.responses import HTMLResponse
from sqlalchemy.orm import Session
from dashboard.models.calm import JournalEntry, Task, TaskCertainty, TaskState
from dashboard.models.database import SessionLocal, engine, get_db
from dashboard.models.database import SessionLocal, engine, get_db, create_tables
from dashboard.templating import templates
# Ensure CALM tables exist (safe to call multiple times)
create_tables()
logger = logging.getLogger(__name__)
router = APIRouter(tags=["calm"])

View File

@@ -77,11 +77,6 @@ async def self_modify_queue(request: Request):
)
@router.get("/tasks", response_class=HTMLResponse)
async def tasks_page(request: Request):
return templates.TemplateResponse(request, "tasks.html", {"tasks": []})
@router.get("/swarm/mission-control", response_class=HTMLResponse)
async def mission_control(request: Request):
return templates.TemplateResponse(request, "mission_control.html", {})
@@ -104,11 +99,6 @@ async def hands_page(request: Request):
return templates.TemplateResponse(request, "hands.html", {"executions": []})
@router.get("/work-orders/queue", response_class=HTMLResponse)
async def work_orders(request: Request):
return templates.TemplateResponse(request, "work_orders.html", {"orders": []})
@router.get("/creative/ui", response_class=HTMLResponse)
async def creative_ui(request: Request):
return templates.TemplateResponse(request, "creative.html", {})

View File

@@ -0,0 +1,375 @@
"""Task Queue routes — SQLite-backed CRUD for the task management dashboard."""
import logging
import sqlite3
import uuid
from datetime import datetime
from pathlib import Path
from typing import Optional
from fastapi import APIRouter, HTTPException, Request, Form
from fastapi.responses import HTMLResponse, JSONResponse
from dashboard.templating import templates
logger = logging.getLogger(__name__)
router = APIRouter(tags=["tasks"])
# ---------------------------------------------------------------------------
# Database helpers
# ---------------------------------------------------------------------------
DB_PATH = Path("data/tasks.db")
VALID_STATUSES = {
"pending_approval", "approved", "running", "paused",
"completed", "vetoed", "failed", "backlogged",
}
VALID_PRIORITIES = {"low", "normal", "high", "urgent"}
def _get_db() -> sqlite3.Connection:
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(str(DB_PATH))
conn.row_factory = sqlite3.Row
conn.execute("""
CREATE TABLE IF NOT EXISTS tasks (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
description TEXT DEFAULT '',
status TEXT DEFAULT 'pending_approval',
priority TEXT DEFAULT 'normal',
assigned_to TEXT DEFAULT '',
created_by TEXT DEFAULT 'operator',
result TEXT DEFAULT '',
created_at TEXT DEFAULT (datetime('now')),
completed_at TEXT
)
""")
conn.commit()
return conn
def _row_to_dict(row: sqlite3.Row) -> dict:
return dict(row)
class _EnumLike:
"""Thin wrapper so Jinja templates can use task.status.value."""
def __init__(self, v: str):
self.value = v
def __str__(self):
return self.value
def __eq__(self, other):
if isinstance(other, str):
return self.value == other
return NotImplemented
class _TaskView:
"""Lightweight view object for Jinja template rendering."""
def __init__(self, row: dict):
self.id = row["id"]
self.title = row.get("title", "")
self.description = row.get("description", "")
self.status = _EnumLike(row.get("status", "pending_approval"))
self.priority = _EnumLike(row.get("priority", "normal"))
self.assigned_to = row.get("assigned_to", "")
self.created_by = row.get("created_by", "operator")
self.result = row.get("result", "")
self.created_at = row.get("created_at", "")
self.completed_at = row.get("completed_at")
self.steps = [] # reserved for future use
# ---------------------------------------------------------------------------
# Page routes
# ---------------------------------------------------------------------------
@router.get("/tasks", response_class=HTMLResponse)
async def tasks_page(request: Request):
"""Render the main task queue page with 3-column layout."""
db = _get_db()
try:
pending = [_TaskView(_row_to_dict(r)) for r in db.execute(
"SELECT * FROM tasks WHERE status IN ('pending_approval') ORDER BY created_at DESC"
).fetchall()]
active = [_TaskView(_row_to_dict(r)) for r in db.execute(
"SELECT * FROM tasks WHERE status IN ('approved','running','paused') ORDER BY created_at DESC"
).fetchall()]
completed = [_TaskView(_row_to_dict(r)) for r in db.execute(
"SELECT * FROM tasks WHERE status IN ('completed','vetoed','failed') ORDER BY completed_at DESC LIMIT 50"
).fetchall()]
finally:
db.close()
return templates.TemplateResponse(request, "tasks.html", {
"pending_count": len(pending),
"pending": pending,
"active": active,
"completed": completed,
"agents": [], # no agent roster wired yet
"pre_assign": "",
})
# ---------------------------------------------------------------------------
# HTMX partials (polled by the template)
# ---------------------------------------------------------------------------
@router.get("/tasks/pending", response_class=HTMLResponse)
async def tasks_pending(request: Request):
db = _get_db()
try:
rows = db.execute(
"SELECT * FROM tasks WHERE status='pending_approval' ORDER BY created_at DESC"
).fetchall()
finally:
db.close()
tasks = [_TaskView(_row_to_dict(r)) for r in rows]
parts = []
for task in tasks:
parts.append(templates.TemplateResponse(
request, "partials/task_card.html", {"task": task}
).body.decode())
if not parts:
return HTMLResponse('<div class="empty-column">No pending tasks</div>')
return HTMLResponse("".join(parts))
@router.get("/tasks/active", response_class=HTMLResponse)
async def tasks_active(request: Request):
db = _get_db()
try:
rows = db.execute(
"SELECT * FROM tasks WHERE status IN ('approved','running','paused') ORDER BY created_at DESC"
).fetchall()
finally:
db.close()
tasks = [_TaskView(_row_to_dict(r)) for r in rows]
parts = []
for task in tasks:
parts.append(templates.TemplateResponse(
request, "partials/task_card.html", {"task": task}
).body.decode())
if not parts:
return HTMLResponse('<div class="empty-column">No active tasks</div>')
return HTMLResponse("".join(parts))
@router.get("/tasks/completed", response_class=HTMLResponse)
async def tasks_completed(request: Request):
db = _get_db()
try:
rows = db.execute(
"SELECT * FROM tasks WHERE status IN ('completed','vetoed','failed') ORDER BY completed_at DESC LIMIT 50"
).fetchall()
finally:
db.close()
tasks = [_TaskView(_row_to_dict(r)) for r in rows]
parts = []
for task in tasks:
parts.append(templates.TemplateResponse(
request, "partials/task_card.html", {"task": task}
).body.decode())
if not parts:
return HTMLResponse('<div class="empty-column">No completed tasks yet</div>')
return HTMLResponse("".join(parts))
# ---------------------------------------------------------------------------
# Form-based create (used by the modal in tasks.html)
# ---------------------------------------------------------------------------
@router.post("/tasks/create", response_class=HTMLResponse)
async def create_task_form(
request: Request,
title: str = Form(...),
description: str = Form(""),
priority: str = Form("normal"),
assigned_to: str = Form(""),
):
"""Create a task from the modal form and return a task card partial."""
task_id = str(uuid.uuid4())
now = datetime.utcnow().isoformat()
priority = priority if priority in VALID_PRIORITIES else "normal"
db = _get_db()
try:
db.execute(
"INSERT INTO tasks (id, title, description, priority, assigned_to, created_at) VALUES (?, ?, ?, ?, ?, ?)",
(task_id, title, description, priority, assigned_to, now),
)
db.commit()
row = db.execute("SELECT * FROM tasks WHERE id=?", (task_id,)).fetchone()
finally:
db.close()
task = _TaskView(_row_to_dict(row))
return templates.TemplateResponse(request, "partials/task_card.html", {"task": task})
# ---------------------------------------------------------------------------
# Task action endpoints (approve, veto, modify, pause, cancel, retry)
# ---------------------------------------------------------------------------
@router.post("/tasks/{task_id}/approve", response_class=HTMLResponse)
async def approve_task(request: Request, task_id: str):
return await _set_status(request, task_id, "approved")
@router.post("/tasks/{task_id}/veto", response_class=HTMLResponse)
async def veto_task(request: Request, task_id: str):
return await _set_status(request, task_id, "vetoed")
@router.post("/tasks/{task_id}/pause", response_class=HTMLResponse)
async def pause_task(request: Request, task_id: str):
return await _set_status(request, task_id, "paused")
@router.post("/tasks/{task_id}/cancel", response_class=HTMLResponse)
async def cancel_task(request: Request, task_id: str):
return await _set_status(request, task_id, "vetoed")
@router.post("/tasks/{task_id}/retry", response_class=HTMLResponse)
async def retry_task(request: Request, task_id: str):
return await _set_status(request, task_id, "approved")
@router.post("/tasks/{task_id}/modify", response_class=HTMLResponse)
async def modify_task(
request: Request,
task_id: str,
title: str = Form(...),
description: str = Form(""),
):
db = _get_db()
try:
db.execute(
"UPDATE tasks SET title=?, description=? WHERE id=?",
(title, description, task_id),
)
db.commit()
row = db.execute("SELECT * FROM tasks WHERE id=?", (task_id,)).fetchone()
finally:
db.close()
if not row:
raise HTTPException(404, "Task not found")
task = _TaskView(_row_to_dict(row))
return templates.TemplateResponse(request, "partials/task_card.html", {"task": task})
async def _set_status(request: Request, task_id: str, new_status: str):
"""Helper to update status and return refreshed task card."""
completed_at = datetime.utcnow().isoformat() if new_status in ("completed", "vetoed", "failed") else None
db = _get_db()
try:
db.execute(
"UPDATE tasks SET status=?, completed_at=COALESCE(?, completed_at) WHERE id=?",
(new_status, completed_at, task_id),
)
db.commit()
row = db.execute("SELECT * FROM tasks WHERE id=?", (task_id,)).fetchone()
finally:
db.close()
if not row:
raise HTTPException(404, "Task not found")
task = _TaskView(_row_to_dict(row))
return templates.TemplateResponse(request, "partials/task_card.html", {"task": task})
# ---------------------------------------------------------------------------
# JSON API (for programmatic access / Timmy's tool calls)
# ---------------------------------------------------------------------------
@router.post("/api/tasks", response_class=JSONResponse, status_code=201)
async def api_create_task(request: Request):
"""Create a task via JSON API."""
body = await request.json()
title = body.get("title")
if not title:
raise HTTPException(422, "title is required")
task_id = str(uuid.uuid4())
now = datetime.utcnow().isoformat()
priority = body.get("priority", "normal")
if priority not in VALID_PRIORITIES:
priority = "normal"
db = _get_db()
try:
db.execute(
"INSERT INTO tasks (id, title, description, priority, assigned_to, created_by, created_at) "
"VALUES (?, ?, ?, ?, ?, ?, ?)",
(
task_id,
title,
body.get("description", ""),
priority,
body.get("assigned_to", ""),
body.get("created_by", "operator"),
now,
),
)
db.commit()
row = db.execute("SELECT * FROM tasks WHERE id=?", (task_id,)).fetchone()
finally:
db.close()
return JSONResponse(_row_to_dict(row), status_code=201)
@router.get("/api/tasks", response_class=JSONResponse)
async def api_list_tasks():
"""List all tasks as JSON."""
db = _get_db()
try:
rows = db.execute("SELECT * FROM tasks ORDER BY created_at DESC").fetchall()
finally:
db.close()
return JSONResponse([_row_to_dict(r) for r in rows])
@router.patch("/api/tasks/{task_id}/status", response_class=JSONResponse)
async def api_update_status(task_id: str, request: Request):
"""Update task status via JSON API."""
body = await request.json()
new_status = body.get("status")
if not new_status or new_status not in VALID_STATUSES:
raise HTTPException(422, f"Invalid status. Must be one of: {VALID_STATUSES}")
completed_at = datetime.utcnow().isoformat() if new_status in ("completed", "vetoed", "failed") else None
db = _get_db()
try:
db.execute(
"UPDATE tasks SET status=?, completed_at=COALESCE(?, completed_at) WHERE id=?",
(new_status, completed_at, task_id),
)
db.commit()
row = db.execute("SELECT * FROM tasks WHERE id=?", (task_id,)).fetchone()
finally:
db.close()
if not row:
raise HTTPException(404, "Task not found")
return JSONResponse(_row_to_dict(row))
@router.delete("/api/tasks/{task_id}", response_class=JSONResponse)
async def api_delete_task(task_id: str):
"""Delete a task."""
db = _get_db()
try:
cursor = db.execute("DELETE FROM tasks WHERE id=?", (task_id,))
db.commit()
finally:
db.close()
if cursor.rowcount == 0:
raise HTTPException(404, "Task not found")
return JSONResponse({"success": True, "id": task_id})

View File

@@ -0,0 +1,239 @@
"""Work Orders routes — SQLite-backed submit/review/execute pipeline."""
import logging
import sqlite3
import uuid
from datetime import datetime
from pathlib import Path
from fastapi import APIRouter, HTTPException, Request, Form
from fastapi.responses import HTMLResponse, JSONResponse
from dashboard.templating import templates
logger = logging.getLogger(__name__)
router = APIRouter(tags=["work-orders"])
DB_PATH = Path("data/work_orders.db")
PRIORITIES = ["low", "medium", "high", "critical"]
CATEGORIES = ["bug", "feature", "suggestion", "maintenance", "security"]
VALID_STATUSES = {"submitted", "triaged", "approved", "in_progress", "completed", "rejected"}
def _get_db() -> sqlite3.Connection:
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(str(DB_PATH))
conn.row_factory = sqlite3.Row
conn.execute("""
CREATE TABLE IF NOT EXISTS work_orders (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
description TEXT DEFAULT '',
priority TEXT DEFAULT 'medium',
category TEXT DEFAULT 'suggestion',
submitter TEXT DEFAULT 'dashboard',
related_files TEXT DEFAULT '',
status TEXT DEFAULT 'submitted',
result TEXT DEFAULT '',
rejection_reason TEXT DEFAULT '',
created_at TEXT DEFAULT (datetime('now')),
completed_at TEXT
)
""")
conn.commit()
return conn
class _EnumLike:
def __init__(self, v: str):
self.value = v
def __str__(self):
return self.value
def __eq__(self, other):
if isinstance(other, str):
return self.value == other
return NotImplemented
class _WOView:
"""View object for Jinja template rendering."""
def __init__(self, row: dict):
self.id = row["id"]
self.title = row.get("title", "")
self.description = row.get("description", "")
self.priority = _EnumLike(row.get("priority", "medium"))
self.category = _EnumLike(row.get("category", "suggestion"))
self.submitter = row.get("submitter", "dashboard")
self.status = _EnumLike(row.get("status", "submitted"))
raw_files = row.get("related_files", "")
self.related_files = [f.strip() for f in raw_files.split(",") if f.strip()] if raw_files else []
self.result = row.get("result", "")
self.rejection_reason = row.get("rejection_reason", "")
self.created_at = row.get("created_at", "")
self.completed_at = row.get("completed_at")
self.execution_mode = None
def _row_to_dict(row: sqlite3.Row) -> dict:
return dict(row)
def _query_wos(db, statuses):
placeholders = ",".join("?" for _ in statuses)
return [
_WOView(_row_to_dict(r))
for r in db.execute(
f"SELECT * FROM work_orders WHERE status IN ({placeholders}) ORDER BY created_at DESC",
statuses,
).fetchall()
]
# ---------------------------------------------------------------------------
# Page route
# ---------------------------------------------------------------------------
@router.get("/work-orders/queue", response_class=HTMLResponse)
async def work_orders_page(request: Request):
db = _get_db()
try:
pending = _query_wos(db, ["submitted", "triaged"])
active = _query_wos(db, ["approved", "in_progress"])
completed = _query_wos(db, ["completed"])
rejected = _query_wos(db, ["rejected"])
finally:
db.close()
return templates.TemplateResponse(request, "work_orders.html", {
"pending_count": len(pending),
"pending": pending,
"active": active,
"completed": completed,
"rejected": rejected,
"priorities": PRIORITIES,
"categories": CATEGORIES,
})
# ---------------------------------------------------------------------------
# Form submit
# ---------------------------------------------------------------------------
@router.post("/work-orders/submit", response_class=HTMLResponse)
async def submit_work_order(
request: Request,
title: str = Form(...),
description: str = Form(""),
priority: str = Form("medium"),
category: str = Form("suggestion"),
submitter: str = Form("dashboard"),
related_files: str = Form(""),
):
wo_id = str(uuid.uuid4())
now = datetime.utcnow().isoformat()
priority = priority if priority in PRIORITIES else "medium"
category = category if category in CATEGORIES else "suggestion"
db = _get_db()
try:
db.execute(
"INSERT INTO work_orders (id, title, description, priority, category, submitter, related_files, created_at) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
(wo_id, title, description, priority, category, submitter, related_files, now),
)
db.commit()
row = db.execute("SELECT * FROM work_orders WHERE id=?", (wo_id,)).fetchone()
finally:
db.close()
wo = _WOView(_row_to_dict(row))
return templates.TemplateResponse(request, "partials/work_order_card.html", {"wo": wo})
# ---------------------------------------------------------------------------
# HTMX partials
# ---------------------------------------------------------------------------
@router.get("/work-orders/queue/pending", response_class=HTMLResponse)
async def pending_partial(request: Request):
db = _get_db()
try:
wos = _query_wos(db, ["submitted", "triaged"])
finally:
db.close()
if not wos:
return HTMLResponse(
'<div style="color: var(--text-muted); font-size: 0.8rem; padding: 12px 0;">'
"No pending work orders.</div>"
)
parts = []
for wo in wos:
parts.append(
templates.TemplateResponse(request, "partials/work_order_card.html", {"wo": wo}).body.decode()
)
return HTMLResponse("".join(parts))
@router.get("/work-orders/queue/active", response_class=HTMLResponse)
async def active_partial(request: Request):
db = _get_db()
try:
wos = _query_wos(db, ["approved", "in_progress"])
finally:
db.close()
if not wos:
return HTMLResponse(
'<div style="color: var(--text-muted); font-size: 0.8rem; padding: 12px 0;">'
"No work orders currently in progress.</div>"
)
parts = []
for wo in wos:
parts.append(
templates.TemplateResponse(request, "partials/work_order_card.html", {"wo": wo}).body.decode()
)
return HTMLResponse("".join(parts))
# ---------------------------------------------------------------------------
# Action endpoints
# ---------------------------------------------------------------------------
async def _update_status(request: Request, wo_id: str, new_status: str, **extra):
completed_at = datetime.utcnow().isoformat() if new_status in ("completed", "rejected") else None
db = _get_db()
try:
sets = ["status=?", "completed_at=COALESCE(?, completed_at)"]
vals = [new_status, completed_at]
for col, val in extra.items():
sets.append(f"{col}=?")
vals.append(val)
vals.append(wo_id)
db.execute(f"UPDATE work_orders SET {', '.join(sets)} WHERE id=?", vals)
db.commit()
row = db.execute("SELECT * FROM work_orders WHERE id=?", (wo_id,)).fetchone()
finally:
db.close()
if not row:
raise HTTPException(404, "Work order not found")
wo = _WOView(_row_to_dict(row))
return templates.TemplateResponse(request, "partials/work_order_card.html", {"wo": wo})
@router.post("/work-orders/{wo_id}/approve", response_class=HTMLResponse)
async def approve_wo(request: Request, wo_id: str):
return await _update_status(request, wo_id, "approved")
@router.post("/work-orders/{wo_id}/reject", response_class=HTMLResponse)
async def reject_wo(request: Request, wo_id: str):
return await _update_status(request, wo_id, "rejected")
@router.post("/work-orders/{wo_id}/execute", response_class=HTMLResponse)
async def execute_wo(request: Request, wo_id: str):
return await _update_status(request, wo_id, "in_progress")

View File

@@ -0,0 +1,13 @@
{% if facts %}
<ul class="mc-fact-list" style="list-style: none; padding: 0;">
{% for fact in facts %}
<li class="memory-fact" id="fact-{{ fact.id }}" style="display:flex; align-items:center; gap:8px; padding:6px 0; border-bottom:1px solid rgba(255,255,255,0.08);">
<span class="fact-content" style="flex:1;">{{ fact.content }}</span>
<button class="mc-btn mc-btn-small" onclick="editFact('{{ fact.id }}', this)" style="font-size:0.7rem; padding:2px 8px;">EDIT</button>
<button class="mc-btn mc-btn-small" onclick="deleteFact('{{ fact.id }}')" style="font-size:0.7rem; padding:2px 8px; color:#ef4444;">DEL</button>
</li>
{% endfor %}
</ul>
{% else %}
<p class="mc-text-secondary">No personal facts stored yet.</p>
{% endif %}

View File

@@ -0,0 +1,34 @@
<div class="mc-results-section">
<h3>Search Results</h3>
{% if results %}
<div class="memory-results">
{% for mem in results %}
<div class="memory-entry" data-relevance="{{ mem.relevance_score }}">
<div class="memory-header">
<span class="memory-source">{{ mem.source }}</span>
<span class="memory-type mc-badge">{{ mem.context_type }}</span>
{% if mem.relevance_score %}
<span class="memory-score">{{ "%.2f"|format(mem.relevance_score) }}</span>
{% endif %}
</div>
<div class="memory-content">{{ mem.content }}</div>
<div class="memory-meta">
<span class="memory-time">{{ mem.timestamp[11:16] }}</span>
{% if mem.agent_id %}
<span class="memory-agent">Agent: {{ mem.agent_id[:8] }}...</span>
{% endif %}
{% if mem.task_id %}
<span class="memory-task">Task: {{ mem.task_id[:8] }}...</span>
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="mc-empty-state">
<p>No results found for "{{ query }}"</p>
<p class="mc-text-secondary">Try different keywords or check spelling.</p>
</div>
{% endif %}
</div>

View File

@@ -275,7 +275,7 @@ def create_timmy(
return Agent(
name="Agent",
model=Ollama(id=model_name, host=settings.ollama_url),
model=Ollama(id=model_name, host=settings.ollama_url, request_timeout=300),
db=SqliteDb(db_file=db_file),
description=full_prompt,
add_history_to_context=True,

View File

@@ -63,7 +63,7 @@ class BaseAgent(ABC):
return Agent(
name=self.name,
model=Ollama(id=settings.ollama_model, host=settings.ollama_url),
model=Ollama(id=settings.ollama_model, host=settings.ollama_url, request_timeout=300),
description=system_prompt,
tools=tool_instances if tool_instances else None,
add_history_to_context=True,

View File

@@ -365,6 +365,49 @@ def memory_search(query: str, top_k: int = 5) -> str:
return "\n\n".join(parts)
def memory_read(query: str = "", top_k: int = 5) -> str:
"""Read from persistent memory — search facts, notes, and past conversations.
This is the primary tool for recalling stored information. If no query
is given, returns the most recent personal facts. With a query, it
searches semantically across all stored memories.
Args:
query: Optional search term. Leave empty to list recent facts.
top_k: Maximum results to return (default 5).
Returns:
Formatted string of memory contents.
"""
if top_k is None:
top_k = 5
parts: list[str] = []
# Always include personal facts first
try:
from timmy.memory.vector_store import search_memories
facts = search_memories(query or "", limit=top_k, min_relevance=0.0)
fact_entries = [e for e in facts if (e.context_type or "") == "fact"]
if fact_entries:
parts.append("## Personal Facts")
for entry in fact_entries[:top_k]:
parts.append(f"- {entry.content[:300]}")
except Exception as exc:
logger.debug("Vector store unavailable for memory_read: %s", exc)
# If a query was provided, also do semantic search
if query:
search_result = memory_search(query, top_k)
if search_result and search_result != "No relevant memories found.":
parts.append("\n## Search Results")
parts.append(search_result)
if not parts:
return "No memories stored yet. Use memory_write to store information."
return "\n".join(parts)
def memory_write(content: str, context_type: str = "fact") -> str:
"""Store a piece of information in persistent memory.

View File

@@ -447,10 +447,11 @@ def create_full_toolkit(base_dir: str | Path | None = None):
# Memory search and write — persistent recall across all channels
try:
from timmy.semantic_memory import memory_search, memory_write
from timmy.semantic_memory import memory_search, memory_write, memory_read
toolkit.register(memory_search, name="memory_search")
toolkit.register(memory_write, name="memory_write")
toolkit.register(memory_read, name="memory_read")
except Exception:
logger.debug("Memory tools not available")