From 750cde489d7aee03bd32320940bd60af3ebb1be5 Mon Sep 17 00:00:00 2001 From: Scott Idem Date: Fri, 8 May 2026 21:41:26 -0400 Subject: [PATCH] feat: session_search tool + tool expansion docs update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit session_search (tools/files.py): - Full-text search across past session logs, exposed to the orchestrator - Params: query (required), limit (default 5, max 20) - Returns dated excerpts, newest first; own sessions only via ContextVars - User-level — no TOOL_ROLES gating needed - Registered in __init__.py callables + TOOL_CATEGORIES["Files"] ARCH__FUTURE.md §2: updated tool count to 44, marked prior tools complete, added Round 2 planned tools table (session_search now done, reminders due dates, http_post, nc_talk_history, task_list priority filter, http_fetch max_chars), noted datetime_now is not needed (already in system prompt via context_loader) TODO__Agents.md: session_search checked off, Round 2 task list added Co-Authored-By: Claude Sonnet 4.6 --- cortex/tools/__init__.py | 5 ++- cortex/tools/files.py | 75 +++++++++++++++++++++++++++++++++-- documentation/ARCH__FUTURE.md | 39 ++++++++++-------- documentation/TODO__Agents.md | 30 +++++++++++++- 4 files changed, 125 insertions(+), 24 deletions(-) diff --git a/cortex/tools/__init__.py b/cortex/tools/__init__.py index 0a7b2dc..8022a3d 100644 --- a/cortex/tools/__init__.py +++ b/cortex/tools/__init__.py @@ -30,7 +30,7 @@ from tools.ae_knowledge import ( journal_entry_prepend as _ae_journal_entry_prepend, ) from tools.ae_tasks import task_list as _ae_task_list -from tools.files import file_read as _file_read, file_list as _file_list, file_write as _file_write +from tools.files import file_read as _file_read, file_list as _file_list, file_write as _file_write, session_search as _session_search from tools.system import ( shell_exec as _shell_exec, claude_allow_dir as _claude_allow_dir, @@ -89,7 +89,7 @@ import tools.agent_notes as _mod_agent_notes TOOL_CATEGORIES: dict[str, list[str]] = { "Web": ["web_search", "http_fetch"], - "Files": ["file_read", "file_list", "file_write"], + "Files": ["file_read", "file_list", "file_write", "session_search"], "Shell": ["shell_exec", "claude_allow_dir"], "System": ["cortex_restart", "cortex_logs", "cortex_status", "cortex_update"], "Tasks": ["task_list", "task_create", "task_update", "task_complete"], @@ -126,6 +126,7 @@ _CALLABLES: dict[str, callable] = { "file_read": _file_read, "file_list": _file_list, "file_write": _file_write, + "session_search": _session_search, "shell_exec": _shell_exec, "claude_allow_dir": _claude_allow_dir, "cortex_restart": _cortex_restart, diff --git a/cortex/tools/files.py b/cortex/tools/files.py index 332ed31..198c613 100644 --- a/cortex/tools/files.py +++ b/cortex/tools/files.py @@ -1,13 +1,15 @@ """ -File read tool — restricted to known-safe directory roots. +File read/write/search tools — restricted to known-safe directory roots. Lets the orchestrator read local files (documentation, notes, config references) -without exposing arbitrary filesystem access. All paths are resolved and checked -against an allowlist of roots before any read is performed. +and search past session logs without exposing arbitrary filesystem access. +All paths are resolved and checked against an allowlist of roots before any +read or write is performed. """ import asyncio import logging +import re from pathlib import Path from google.genai import types @@ -225,6 +227,55 @@ def _sync_file_write(path: str, content: str, mode: str) -> str: return f"Write error: {e}" +_SEARCH_EXCERPT_CHARS = 150 + + +async def session_search(query: str, limit: int = 5) -> str: + """Search past session logs for a keyword or phrase. + + Returns up to `limit` matching excerpts with session dates, newest first. + Only searches the current user's own sessions (per-persona isolation via ContextVars). + """ + return await asyncio.to_thread(_sync_session_search, query, limit) + + +def _sync_session_search(query: str, limit: int) -> str: + from persona import persona_path + sessions_dir = persona_path() / "sessions" + if not sessions_dir.exists(): + return "No session logs found." + + limit = max(1, min(limit, 20)) + pattern = re.compile(re.escape(query), re.IGNORECASE) + session_files = sorted(sessions_dir.glob("*.md"), reverse=True) + + matches = [] + for sf in session_files: + if len(matches) >= limit: + break + try: + text = sf.read_text() + except OSError: + continue + for m in pattern.finditer(text): + if len(matches) >= limit: + break + start = max(0, m.start() - _SEARCH_EXCERPT_CHARS) + end = min(len(text), m.end() + _SEARCH_EXCERPT_CHARS) + excerpt = text[start:end].strip() + if start > 0: + excerpt = "…" + excerpt + if end < len(text): + excerpt = excerpt + "…" + matches.append(f"[{sf.stem}] {excerpt}") + + if not matches: + return f"No matches for '{query}' across {len(session_files)} session logs." + + header = f"Session search: '{query}' — {len(matches)} match(es) across {len(session_files)} logs\n" + return header + "\n\n".join(matches) + + DECLARATIONS = [ types.FunctionDeclaration( name="file_read", @@ -278,4 +329,22 @@ DECLARATIONS = [ required=["path", "content"], ), ), + types.FunctionDeclaration( + name="session_search", + description=( + "Search past conversation session logs for a keyword or phrase. " + "Use this to recall what was discussed in previous sessions — " + "e.g. 'what did we decide about X?', 'when did we set up Y?'. " + "Returns matching excerpts with session dates, newest first. " + "Only searches this user's own sessions." + ), + parameters=types.Schema( + type=types.Type.OBJECT, + properties={ + "query": types.Schema(type=types.Type.STRING, description="Keyword or phrase to search for"), + "limit": types.Schema(type=types.Type.INTEGER, description="Max results to return (default 5, max 20)"), + }, + required=["query"], + ), + ), ] diff --git a/documentation/ARCH__FUTURE.md b/documentation/ARCH__FUTURE.md index c1fac6a..919a488 100644 --- a/documentation/ARCH__FUTURE.md +++ b/documentation/ARCH__FUTURE.md @@ -46,27 +46,32 @@ Full API reference: [`docs/OPEN_WEBUI_API.md`](../docs/OPEN_WEBUI_API.md) ## 2. Orchestrator Tool Expansions -**Status:** Planned. Current tool count: 27. These fill obvious gaps. +**Status:** Ongoing. Current tool count: 44. Previously planned tools are all complete. -New tools for `cortex/tools/` — each follows the existing async pattern (implement function, -add `FunctionDeclaration`, register in `__init__.py`). +### Completed +All originally planned tools are live: `cortex_restart`, `cortex_logs`, `http_fetch`, +`file_list`, `file_write`, `nc_talk_send`, `email_send`, `web_push`, `agent_notes_*`. -| Tool | Module | Description | -|---|---|---| -| `cortex_restart` | `system.py` | `systemctl --user restart cortex` — Inara can apply her own config changes; returns last 10 log lines after restart | -| `cortex_logs` | `system.py` | `journalctl --user -u cortex -n N` — tail service logs for debugging | -| `http_fetch` | `web.py` | Fetch a specific URL and return content; for health checks, API probing, webhook testing — not a search, a direct GET/POST | -| `file_list` | `scratch.py` or new `files.py` | List files and directories at a path; currently only `file_read` exists | -| `file_write` | `files.py` | Write content to a file with a path allow-list (persona dir + scratch by default) | -| `nc_talk_send` | new `notify.py` | Proactively send a message to the user via Nextcloud Talk outbound API | -| `email_send` | `notify.py` | Send email via existing `email_utils.py` SMTP helper | -| `web_push` | `notify.py` | Browser push notification via Web Push API (requires push subscription stored per-user in `home/{user}/push_sub.json`; pairs with the PWA service worker) | +### Next additions -**Safety note for `cortex_restart`:** The service will drop in-flight SSE connections on restart. -Only call if no streaming response is active. Add a check or a short delay before restarting. +**Datetime note:** The current date and time is already injected into every system prompt +via `context_loader.py` (`--- System --- Current date and time: ...`). A dedicated +`datetime_now` tool is not needed — the timestamp is always in context. -**Safety note for `file_write`:** Enforce an allow-list at the tool level, not just in the prompt. -Default allow: `home/{user}/persona/{name}/` and `/tmp/cortex-scratch/`. Reject any path outside. +| Tool | Module | Priority | Description | +|---|---|---|---| +| `session_search` | new `search.py` or `files.py` | High | Full-text search across past session logs. The UI search already exists (`GET /sessions/search`) — this exposes it to the orchestrator so the agent can answer "what did we discuss about X last month?" | +| `reminders due dates` | `reminders.py` | Medium | Add optional `due` field to `reminders_add`. Surface only due/overdue reminders in context rather than the full flat list. Makes reminders time-aware rather than always-on noise. | +| `http_post` | `web.py` | Medium | POST to an external URL — for webhooks, REST APIs, form submissions. Requires a per-user host allowlist (same pattern as `email_send`) to prevent misuse. | +| `nc_talk_history` | `notify.py` | Medium | Read recent messages from a Nextcloud Talk conversation. The bot can send but cannot read — adding read capability gives it full context before replying. | +| `task_list` priority filter | `tasks.py` | Low | `task_list` accepts `status` but not `priority`. Add `priority` param so the agent can ask "what are my high-priority tasks?" without returning everything. | +| `http_fetch` max_chars | `web.py` | Low | Currently hardcapped at 8,192 chars. Accept optional `max_chars` param so callers can request more or less content. | + +### Not needed / deferred +- **`datetime_now`** — already in system prompt (see note above) +- **`memory_read`** — memory files are already loaded into system prompt at Tier 2+; a tool adds no value except at Tier 1, which is a rare edge case +- **Calculator** — modern models handle arithmetic well; `shell_exec` covers edge cases for admins +- **Google Calendar** — useful but requires Google API OAuth scope expansion; defer until auth layer supports it --- diff --git a/documentation/TODO__Agents.md b/documentation/TODO__Agents.md index 31103aa..98738f4 100644 --- a/documentation/TODO__Agents.md +++ b/documentation/TODO__Agents.md @@ -55,8 +55,7 @@ automatically. Remaining work is quality/reliability parity, not ground-up desig - `/manifest.json` and `/sw.js` served at root; added to `_PUBLIC` in auth_middleware - Tested: install prompt confirmed working in Chromium -### [Tools] Orchestrator tool expansions -New tools for `cortex/tools/` — higher-value additions that fill obvious gaps. +### [Tools] Orchestrator tool expansions — Round 1 ✅ - [x] **`cortex_restart`** — detached subprocess, 5s delay, admin-only, confirm-required — 2026-04-29 - [x] **`cortex_logs`** — `journalctl --user -u cortex -n N`, admin-only — 2026-04-29 - [x] **`http_fetch`** — direct URL fetch via httpx, 8192 char cap — 2026-04-29 @@ -66,6 +65,33 @@ New tools for `cortex/tools/` — higher-value additions that fill obvious gaps. - [x] **`email_send`** — SMTP via email_utils, per-user regex allowlist in `home/{user}/email_allowlist.json`, managed via Settings UI textarea + Files panel raw editor — 2026-04-29 - [x] **`web_push`** — VAPID push via pywebpush; subscriptions in `home/{user}/push_subscriptions.json`; "Enable notifications" toggle in ☰ menu; sw.js push+notificationclick handlers — 2026-05-05 +### [Tools] Orchestrator tool expansions — Round 2 +Next additions identified 2026-05-08. See `ARCH__FUTURE.md` §2 for design notes. + +**Note:** `datetime_now` is NOT needed — current date/time is already injected into every +system prompt by `context_loader.py` at all tiers. + +- [x] **`session_search`** — expose existing session search to the orchestrator — 2026-05-08 + - Wraps session log grep as a tool callable in `tools/files.py` + - Params: `query: str`, `limit: int = 5` (max 20) + - Returns: excerpts with session date, newest first; own sessions only via ContextVars + - User-level (no TOOL_ROLES entry needed) +- [ ] **`reminders` due-date support** — make reminders time-aware + - Add optional `due: str` (ISO date or natural language) to `reminders_add` + - Filter context surfacing: only show reminders where `due` is today or past (or no due date) + - `reminders_list` should show due date and overdue status +- [ ] **`http_post`** — POST to external URLs + - Params: `url: str`, `body: dict | str`, `headers: dict | None` + - Per-user host allowlist in `home/{user}/http_allowlist.json` (same pattern as email) + - Default: blocked unless URL host matches an allowlist entry + - Confirm-required for safety +- [ ] **`nc_talk_history`** — read recent Talk messages before replying + - Params: `conversation_token: str`, `limit: int = 20` + - Returns last N messages with sender + timestamp + - Admin-only (requires NC Talk API credentials from channels.json) +- [ ] **`task_list` priority filter** — add `priority` param alongside existing `status` +- [ ] **`http_fetch` max_chars** — optional param, default 8192, cap at 32768 + ### [Channel] Proactive notifications Inara reaches out on her own initiative via NC Talk or Google Chat when a reminder fires, a cron job completes, or something else warrants attention. The cron/reminder