From 712829a6101b3d92763afd1a9c5bd0bdb903a4bb Mon Sep 17 00:00:00 2001 From: PeninsulaInd Date: Mon, 16 Feb 2026 18:01:18 -0600 Subject: [PATCH] Optimize NotificationBus subscribe cursor and update CLAUDE.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace inefficient two-query approach (fetch up to 10k rows to find max ID) with a single SELECT MAX(id) query. Update CLAUDE.md test count (51 → 118) and add 3 missing test file descriptions. Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 134 ++++++++++++++++++++++++++++++++++++ cheddahbot/db.py | 5 ++ cheddahbot/notifications.py | 8 +-- 3 files changed, 140 insertions(+), 7 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..1bc9082 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,134 @@ +# CLAUDE.md + +## Project Overview + +CheddahBot is a personal AI assistant for Bryan's SEO/AI agency and general life. It has a two-brain architecture: a **chat brain** (OpenRouter/Ollama/LM Studio) for conversational UI, and an **execution brain** (Claude Code CLI) for autonomous tasks like press releases, file operations, and shell commands among others. + +The bot polls ClickUp for tasks, maps them to skills, and auto-executes or asks permission — then reports results back to ClickUp and the chat UI. + +## Coding Workflow Rules - while we are working on building/changing the code +- Before making any changes, explain WHAT you plan to change and WHY +- Wait for my approval before editing files UNLESS I TELL YOU I AM GOING AWAY FOR A BIT - then you can use your judgement but commit the code at every big change. +- After making changes, provide a brief summary of every file modified and what changed +- If you encounter a problem during implementation, STOP and explain it instead of trying to fix it silently +- Never refactor, rename, or reorganize code beyond what was explicitly asked for + +## Architecture + +``` +Gradio UI (ui.py) + ↓ +Agent (agent.py) ← Memory (memory.py, 4-layer: identity/long-term/daily/semantic) + ↓ +LLM Adapter (llm.py) + ├── Chat brain: OpenRouter / Ollama / LM Studio + └── Execution brain: Claude Code CLI (subprocess) + ↓ +Tool Registry (tools/__init__.py) ← auto-discovers tools in tools/ + ↓ +Scheduler (scheduler.py) + ├── Poll loop: cron-based scheduled tasks + ├── Heartbeat: periodic checklist from HEARTBEAT.md + └── ClickUp loop: polls ClickUp → maps to skills → executes + ↓ +NotificationBus (notifications.py) → Gradio / future Discord / Slack +``` + +## Commands + +```bash +# Run the app +uv run python -m cheddahbot + +# Run tests (118 tests, ~3s) +uv run pytest + +# Run tests verbose +uv run pytest -v --no-cov + +# Run only integration tests (requires live ClickUp API token) +uv run pytest -m integration + +# Lint +uv run ruff check . + +# Format +uv run ruff format . + +# Add a dependency +uv add + +# Add a dev/test dependency +uv add --group test +``` + +## Key Files + +| File | Purpose | +|------|---------| +| `cheddahbot/__main__.py` | Entry point, wires all components | +| `cheddahbot/agent.py` | Core agentic loop (chat + tool execution) | +| `cheddahbot/llm.py` | Two-brain LLM adapter | +| `cheddahbot/config.py` | Dataclass config (env → YAML → defaults) | +| `cheddahbot/db.py` | SQLite persistence (WAL, thread-safe) | +| `cheddahbot/scheduler.py` | Three daemon threads: poll, heartbeat, ClickUp | +| `cheddahbot/clickup.py` | ClickUp REST API v2 client (httpx) | +| `cheddahbot/notifications.py` | UI-agnostic pub/sub notification bus | +| `cheddahbot/memory.py` | 4-layer memory with semantic search | +| `cheddahbot/router.py` | System prompt builder | +| `cheddahbot/ui.py` | Gradio web interface | +| `cheddahbot/tools/` | Tool modules (auto-discovered) | +| `config.yaml` | Runtime configuration | +| `identity/SOUL.md` | Agent personality | +| `identity/USER.md` | User profile | +| `skills/` | Prompt templates for tools (press releases, etc.) | + +## Conventions + +- **Config precedence**: env vars > config.yaml > dataclass defaults +- **ClickUp env vars**: `CLICKUP_API_TOKEN`, `CLICKUP_WORKSPACE_ID`, `CLICKUP_SPACE_ID` +- **Tool registration**: Use the `@tool("name", "description", category="cat")` decorator in any file under `cheddahbot/tools/` — auto-discovered on startup +- **Tool context**: Tools can accept `ctx: dict = None` to get `config`, `db`, `agent`, `memory` injected +- **Database**: SQLite with WAL mode, thread-local connections via `threading.local()` +- **KV store**: Task state stored as JSON at `clickup:task:{id}:state` keys +- **ClickUp field mapping**: `Work Category` field (not `Task Type`) identifies task types like "Press Release", "Link Building". The `Client` field (not `Company`) holds the client name. +- **Notifications**: All scheduler events go through `NotificationBus.push()`, never directly to a UI +- **Tests**: Use `respx` to mock httpx calls, `tmp_db` fixture for isolated SQLite instances + +## ClickUp Skill Mapping + +The scheduler maps ClickUp `Work Category` → tool name via `config.yaml`: + +```yaml +skill_map: + "Press Release": + tool: "write_press_releases" + auto_execute: true + field_mapping: + topic: "task_name" # uses ClickUp task name + company_name: "Client" # looks up "Client" custom field +``` + +Task lifecycle: `to do` → discovered → approved/awaiting_approval → executing → completed/failed + +## Testing + +Tests live in `tests/` and use pytest. All tests run offline with mocked APIs. + +- `test_clickup.py` — API response parsing + HTTP client (respx mocks) +- `test_db.py` — `kv_scan` and notifications table methods +- `test_notifications.py` — NotificationBus pub/sub behavior +- `test_clickup_tools.py` — Chat tool state machine (approve/decline) +- `test_email.py` — EmailClient SMTP send + attachments (mocked) +- `test_docx_export.py` — Plain text → .docx formatting and file creation +- `test_press_advantage.py` — Press Advantage API client, company parsing, link building, submit tool + +Fixtures in `conftest.py`: `tmp_db` (fresh SQLite), `sample_clickup_task_data` (realistic API response). + +## Don't + +- Don't edit `.env` — it contains secrets +- Don't manually activate venvs — use `uv run` +- Don't add to `requirements.txt` — use `uv add` (pyproject.toml) +- Don't call tools directly from UI code — go through `NotificationBus` for scheduler events +- Don't store ClickUp state outside of `kv_store` — it's the single source of truth diff --git a/cheddahbot/db.py b/cheddahbot/db.py index efbf6ed..e56908e 100644 --- a/cheddahbot/db.py +++ b/cheddahbot/db.py @@ -216,6 +216,11 @@ class Database: self._conn.commit() return cur.lastrowid + def get_max_notification_id(self) -> int: + """Return the highest notification id, or 0 if the table is empty.""" + row = self._conn.execute("SELECT MAX(id) FROM notifications").fetchone() + return row[0] or 0 + def get_notifications_after(self, after_id: int = 0, limit: int = 50) -> list[dict]: """Get notifications with id > after_id.""" rows = self._conn.execute( diff --git a/cheddahbot/notifications.py b/cheddahbot/notifications.py index 4e2df5b..5e68e55 100644 --- a/cheddahbot/notifications.py +++ b/cheddahbot/notifications.py @@ -46,13 +46,7 @@ class NotificationBus: with self._lock: self._listeners[listener_id] = callback # Start cursor at latest notification so listener only gets new ones - recent = self._db.get_notifications_after(0, limit=1) - if recent: - # Get the max id - all_notifs = self._db.get_notifications_after(0, limit=10000) - self._cursors[listener_id] = all_notifs[-1]["id"] if all_notifs else 0 - else: - self._cursors[listener_id] = 0 + self._cursors[listener_id] = self._db.get_max_notification_id() log.info("Listener '%s' subscribed to notification bus", listener_id) def unsubscribe(self, listener_id: str):