6.0 KiB
6.0 KiB
second-brain
A personal knowledge system: video/article extraction pipeline + LLM-maintained wiki.
Architecture
Three-layer system:
- Wiki — long-term structured memory. Markdown files in an Obsidian vault, maintained by the wiki compiler.
- Context assembler — builds the prompt bundle for each extraction. Phase 1 is thin: domain template + focus field + transcript.
- Extractor — stateless single-shot LLM call per source. Pure function: bundle in, structured extraction out.
Pipeline stages
pending → pulled → transcribed → analyzed → accepted → published
Project layout
src/second_brain/
├── adapters/ # Pull adapters (youtube.py, article.py)
├── context/ # Context assembler
├── extractor/ # LLM extraction engine + Pydantic schema
├── llm/ # claude_cli.py — thin subprocess wrapper around `claude -p`
├── compiler/ # Wiki compiler (reads vault, writes pages, git commits)
├── scheduler/ # Rate-limit smoother (call-count for CLI, token-budget for API)
└── web/ # FastAPI + Jinja2 + HTMX UI
Setup
cp config/settings.toml.example config/settings.toml
# Edit settings.toml with your paths
uv run second-brain serve
CLI commands
second-brain add <url>— queue a source URL (video or article)second-brain process— run the pipeline on pending itemssecond-brain serve— launch the web UI (default port 8000)second-brain compile— run the wiki compilersecond-brain schedule— start the overnight scheduler
Extractor backends
[extractor].backend in config/settings.toml picks the LLM transport.
cli(default) —src/second_brain/llm/claude_cli.pyshells out toclaude -p --output-format json --model <model> --json-schema <schema>. Runs under the Max OAuth subscription (no per-token billing). Hermetic isolation: spawned insidetempfile.TemporaryDirectory(prefix="sb-claude-cli-"), withANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKENstripped from env so the CLI uses OAuth instead of an API key, and with--no-session-persistence --disable-slash-commands --tools "" --setting-sources ""so host CLAUDE.md / hooks / agents don't leak in.api— uses theanthropicSDK. Lazy-imported only when this backend is selected. The SDK is an optional dependency (uv sync --extra api).
Schema-validated output gotcha: when --json-schema is used, the parsed object lands at payload["structured_output"], not payload["result"]. The wrapper checks both. The engine prefers the structured payload via LLMExtraction.model_validate(structured) and falls back to parsing the text field.
Environment variables
ANTHROPIC_API_KEY— only required whenextractor.backend = "api". The defaultclibackend uses your Max OAuth and does not need it.SECOND_BRAIN_CONFIG— optional override for the settings file path.
Domains
development— software, programming, systemscontent— content creation strategy, video productionbusiness— entrepreneurship, marketing, operationshomelab— self-hosted infrastructure, networking, DevOps
Key files
config/settings.toml— vault path, DB path, scheduler window,[extractor]backend configprompts/<domain>.md— per-domain extraction prompt templatessrc/second_brain/extractor/schema.py—LLMExtraction(LLM-only fields, drives the--json-schemacontract) +ExtractionResult(LLMExtraction + SourceMeta filled from the DB)src/second_brain/llm/claude_cli.py— CLI subprocess wrappersrc/second_brain/models.py— SQLAlchemy models +utcnow()helper (replaces deprecateddatetime.utcnow)postgres-migration-planning.md— open questions blocking the SQLite → Postgres+pgvector migration
Conventions / gotchas
- Naive UTC timestamps everywhere. Use
from second_brain.models import utcnowrather thandatetime.utcnow()(deprecated in 3.12) ordatetime.now()(local time). - Re-query inside the session. SQLAlchemy ORM attribute access after the session closes raises
DetachedInstanceError. Theprocesscommand and the web accept/reject endpoints both collect IDs first, then re-query inside the per-iteration / per-request session block, and pre-compute any dict views needed for templates inside thewith db.session()block. - Starlette
TemplateResponsesignature. Starlette ≥ 0.36 requirestemplates.TemplateResponse(request, name, context). The old(name, context)form raisesTypeError: unhashable type: 'dict'. Do not put"request": requestinto the context dict — it's auto-injected now. - Vault git repo auto-init. The wiki compiler calls
_ensure_git_repo()on every run, whichgit inits the vault and creates an empty seed commit if no.gitis present._git_commit()also checksgit diff --cached --quietbefore committing to avoid empty commits. - Scheduler throttling shape. The CLI backend is throttled by
max_calls_per_hour(Max subscription limits are call-shaped, not token-shaped). The API backend usesmax_tokens_per_hour. Both sharemin_gap_seconds. The scheduler targets sources inTRANSCRIBEDstate — earlier code mistakenly filtered onANALYZED, making it a no-op.
In-flight work
- Postgres + pgvector migration. SQLite is the current backing store. Travis wants to move to his existing Postgres + pgvector instance and use the shared
/projects/shared/python/embedding_chunkingmodule for vector indexing. Planning questions are enumerated inpostgres-migration-planning.mdat the project root; no migration work has started yet. - Vault location. Recommended
/opt/projects/second-brain-vault/as the production location (peer to the project dir). Not yet confirmed.
Validation
Per Travis's global CLAUDE.md, run zero-check after any code change, and a-review after non-trivial diffs (> 50 lines). a-review script: python3 ~/.claude/skills/a-review/run_review.py, output goes to reviews/a-review-<date>.md.