Optimize NotificationBus subscribe cursor and update CLAUDE.md
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 <noreply@anthropic.com>cora-start
parent
20708e6033
commit
712829a610
|
|
@ -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 <package>
|
||||
|
||||
# Add a dev/test dependency
|
||||
uv add --group test <package>
|
||||
```
|
||||
|
||||
## 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
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Reference in New Issue