secondbrain-py 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- brain/__init__.py +0 -0
- brain/__main__.py +18 -0
- brain/_capture_command.py +445 -0
- brain/_compose.py +52 -0
- brain/activity.py +206 -0
- brain/ask.py +631 -0
- brain/audio.py +591 -0
- brain/backfill/__init__.py +12 -0
- brain/backfill/search_extras.py +141 -0
- brain/backfill/source_rows.py +101 -0
- brain/bin/__init__.py +1 -0
- brain/bin/_launcher.py +107 -0
- brain/bin/down.py +8 -0
- brain/bin/launchd.py +268 -0
- brain/bin/monitor.py +570 -0
- brain/bin/rebuild.py +8 -0
- brain/bin/status.py +8 -0
- brain/bin/up.py +8 -0
- brain/brief.py +272 -0
- brain/capture.py +49 -0
- brain/chat.py +293 -0
- brain/cli.py +9760 -0
- brain/cli_claude.py +81 -0
- brain/cli_connect.py +285 -0
- brain/cli_demo.py +266 -0
- brain/config.py +1949 -0
- brain/connect.py +925 -0
- brain/db.py +540 -0
- brain/demo/__init__.py +452 -0
- brain/demo/corpus/manifest.json +403 -0
- brain/demo/embedder.py +74 -0
- brain/durations.py +84 -0
- brain/edit_session.py +156 -0
- brain/editor.py +67 -0
- brain/elicit/__init__.py +16 -0
- brain/elicit/detectors.py +250 -0
- brain/elicit/drafter.py +70 -0
- brain/elicit/queue.py +220 -0
- brain/elicit/schema.py +48 -0
- brain/elicit/session.py +445 -0
- brain/embedding_targets.py +54 -0
- brain/embeddings.py +424 -0
- brain/enrichment.py +808 -0
- brain/errors.py +357 -0
- brain/eval/__init__.py +129 -0
- brain/eval/answer_eval.py +281 -0
- brain/eval/baseline.py +265 -0
- brain/eval/concept_extraction.py +378 -0
- brain/eval/corpus.py +152 -0
- brain/eval/errors.py +19 -0
- brain/eval/graph_baseline.py +226 -0
- brain/eval/graph_retrieval.py +202 -0
- brain/eval/graph_runner.py +319 -0
- brain/eval/metrics.py +101 -0
- brain/eval/runner.py +223 -0
- brain/format.py +783 -0
- brain/gaps.py +390 -0
- brain/graph_rag/__init__.py +94 -0
- brain/graph_rag/_retrieval_common.py +113 -0
- brain/graph_rag/aggregates.py +303 -0
- brain/graph_rag/aliases/__init__.py +583 -0
- brain/graph_rag/backends/__init__.py +10 -0
- brain/graph_rag/backends/_age_helpers.py +473 -0
- brain/graph_rag/backends/age.py +782 -0
- brain/graph_rag/backends/base.py +272 -0
- brain/graph_rag/build.py +344 -0
- brain/graph_rag/communities.py +644 -0
- brain/graph_rag/communities_summary.py +437 -0
- brain/graph_rag/concepts.py +202 -0
- brain/graph_rag/cooccur.py +193 -0
- brain/graph_rag/cross_type.py +312 -0
- brain/graph_rag/extract.py +885 -0
- brain/graph_rag/fuse.py +371 -0
- brain/graph_rag/global_.py +412 -0
- brain/graph_rag/grouping.py +372 -0
- brain/graph_rag/person_resolver.py +167 -0
- brain/graph_rag/reconcile.py +792 -0
- brain/graph_rag/relational.py +353 -0
- brain/graph_rag/retrieve.py +526 -0
- brain/graph_rag/router.py +288 -0
- brain/graph_rag/schema.py +320 -0
- brain/graph_rag/sync.py +237 -0
- brain/graph_rag/tenancy.py +43 -0
- brain/graph_rag/themes.py +501 -0
- brain/graph_rag/weighting.py +202 -0
- brain/ingest/__init__.py +1926 -0
- brain/ingest/chunker.py +249 -0
- brain/ingest/docx.py +40 -0
- brain/ingest/gmail.py +621 -0
- brain/ingest/markdown.py +37 -0
- brain/ingest/pdf.py +61 -0
- brain/ingest/stdin.py +22 -0
- brain/ingest/sub_tokens.py +91 -0
- brain/ingest/text.py +16 -0
- brain/interactions.py +205 -0
- brain/maintenance.py +355 -0
- brain/mcp_server.py +3405 -0
- brain/migrations/001_init.sql +43 -0
- brain/migrations/002_qwen3_embedding.sql +17 -0
- brain/migrations/003_vault_model.sql +41 -0
- brain/migrations/004_relax_content_hash_uniqueness.sql +18 -0
- brain/migrations/005_derived_links.sql +67 -0
- brain/migrations/006_dedup_file_by_source_path.sql +25 -0
- brain/migrations/007_email_thread_and_draft.sql +15 -0
- brain/migrations/008_gmail_thread_unique.sql +11 -0
- brain/migrations/009_chunks_weighted_tsv.sql +28 -0
- brain/migrations/010_interactions.sql +30 -0
- brain/migrations/011_documents_summary.sql +23 -0
- brain/migrations/012_graphrag.sql +171 -0
- brain/migrations/013_graphrag_communities.sql +125 -0
- brain/migrations/014_graphrag_community_summary_hash.sql +33 -0
- brain/migrations/015_interactions_graph_targets.sql +89 -0
- brain/migrations/016_index_hygiene.sql +61 -0
- brain/migrations/017_elicit.sql +30 -0
- brain/migrations/018_review_gap_signal_kinds.sql +40 -0
- brain/migrations/019_search_queries.sql +35 -0
- brain/migrations/020_link_suggestions.sql +40 -0
- brain/migrations/021_timeline_doc_date.sql +34 -0
- brain/migrations/022_link_suggestions_undirected.sql +84 -0
- brain/migrations/023_search_queries_fts_count.sql +28 -0
- brain/quartz_overrides/__init__.py +8 -0
- brain/quartz_overrides/quartz/bootstrap-cli.mjs +65 -0
- brain/quartz_overrides/quartz/build.ts +568 -0
- brain/quartz_overrides/quartz/cli/args.js +152 -0
- brain/quartz_overrides/quartz/cli/build_partial_handler.js +544 -0
- brain/quartz_overrides/quartz/cli/handlers.js +636 -0
- brain/quartz_overrides/quartz/components/CommandPalette.tsx +172 -0
- brain/quartz_overrides/quartz/components/Explorer.tsx +198 -0
- brain/quartz_overrides/quartz/components/Footer.tsx +27 -0
- brain/quartz_overrides/quartz/components/Graph.tsx +468 -0
- brain/quartz_overrides/quartz/components/PageTitle.tsx +72 -0
- brain/quartz_overrides/quartz/components/RelatedDocs.tsx +38 -0
- brain/quartz_overrides/quartz/components/Search.tsx +161 -0
- brain/quartz_overrides/quartz/components/SummaryLede.tsx +72 -0
- brain/quartz_overrides/quartz/components/index.ts +92 -0
- brain/quartz_overrides/quartz/components/pages/TagContent.tsx +272 -0
- brain/quartz_overrides/quartz/components/scripts/commandPalette.inline.ts +665 -0
- brain/quartz_overrides/quartz/components/scripts/explorer.inline.ts +768 -0
- brain/quartz_overrides/quartz/components/scripts/graph.inline.ts +2302 -0
- brain/quartz_overrides/quartz/components/scripts/relatedDocs.inline.ts +163 -0
- brain/quartz_overrides/quartz/components/scripts/search.inline.ts +1011 -0
- brain/quartz_overrides/quartz/plugins/emitters/contentIndex.ts +546 -0
- brain/quartz_overrides/quartz/plugins/transformers/codeCopy.ts +94 -0
- brain/quartz_overrides/quartz/plugins/transformers/derivedFenceMark.ts +302 -0
- brain/quartz_overrides/quartz/plugins/transformers/emailThread.ts +148 -0
- brain/quartz_overrides/quartz/plugins/transformers/emptyDoorFilter.ts +213 -0
- brain/quartz_overrides/quartz/plugins/transformers/index.ts +114 -0
- brain/quartz_overrides/quartz/plugins/transformers/linkKindMark.ts +205 -0
- brain/quartz_overrides/quartz/plugins/transformers/linkSourceTag.ts +104 -0
- brain/quartz_overrides/quartz/plugins/transformers/relativeDate.ts +100 -0
- brain/quartz_overrides/quartz/plugins/transformers/reloadSignal.ts +131 -0
- brain/quartz_overrides/quartz/processors/parse.ts +371 -0
- brain/quartz_overrides/quartz/processors/parser_cache.ts +78 -0
- brain/quartz_overrides/quartz/static/brain-logo-dark.png +0 -0
- brain/quartz_overrides/quartz/static/brain-logo-light.png +0 -0
- brain/quartz_overrides/quartz/static/codeCopy.js +196 -0
- brain/quartz_overrides/quartz/static/emailThread.js +334 -0
- brain/quartz_overrides/quartz/static/favicon.ico +0 -0
- brain/quartz_overrides/quartz/static/icon.png +0 -0
- brain/quartz_overrides/quartz/static/linkSourceTag.js +104 -0
- brain/quartz_overrides/quartz/static/relativeDate.js +142 -0
- brain/quartz_overrides/quartz/static/reload.js +168 -0
- brain/quartz_overrides/quartz/styles/brain/_article.scss +252 -0
- brain/quartz_overrides/quartz/styles/brain/_atmosphere.scss +113 -0
- brain/quartz_overrides/quartz/styles/brain/_callouts.scss +180 -0
- brain/quartz_overrides/quartz/styles/brain/_cmdk.scss +7 -0
- brain/quartz_overrides/quartz/styles/brain/_code.scss +208 -0
- brain/quartz_overrides/quartz/styles/brain/_command_palette.scss +369 -0
- brain/quartz_overrides/quartz/styles/brain/_email_thread.scss +228 -0
- brain/quartz_overrides/quartz/styles/brain/_explorer.scss +142 -0
- brain/quartz_overrides/quartz/styles/brain/_home.scss +182 -0
- brain/quartz_overrides/quartz/styles/brain/_links.scss +322 -0
- brain/quartz_overrides/quartz/styles/brain/_marginalia.scss +117 -0
- brain/quartz_overrides/quartz/styles/brain/_motion.scss +175 -0
- brain/quartz_overrides/quartz/styles/brain/_people_hub.scss +100 -0
- brain/quartz_overrides/quartz/styles/brain/_related_docs.scss +137 -0
- brain/quartz_overrides/quartz/styles/brain/_search.scss +252 -0
- brain/quartz_overrides/quartz/styles/brain/_sidebar.scss +468 -0
- brain/quartz_overrides/quartz/styles/brain/_summary_lede.scss +56 -0
- brain/quartz_overrides/quartz/styles/brain/_surface.scss +43 -0
- brain/quartz_overrides/quartz/styles/brain/_tag_content.scss +118 -0
- brain/quartz_overrides/quartz/styles/brain/_tokens.scss +197 -0
- brain/quartz_overrides/quartz/styles/brain/_typography.scss +92 -0
- brain/quartz_overrides/quartz/styles/custom.scss +89 -0
- brain/quartz_overrides/quartz/styles/graph.scss +505 -0
- brain/quartz_overrides/quartz/util/ctx.ts +92 -0
- brain/quartz_overrides/quartz/util/fastpath_manifest.ts +608 -0
- brain/quartz_overrides/quartz/util/path.ts +358 -0
- brain/quartz_overrides/quartz/util/sourceIcons.ts +55 -0
- brain/quartz_overrides/quartz.config.ts +270 -0
- brain/quartz_overrides/quartz.layout.ts +314 -0
- brain/queries.py +1188 -0
- brain/rank_fusion.py +8 -0
- brain/resurface.py +210 -0
- brain/review/__init__.py +26 -0
- brain/review/emit.py +27 -0
- brain/review/queries.py +436 -0
- brain/review/render.py +196 -0
- brain/review/scans.py +355 -0
- brain/review/weekly.py +413 -0
- brain/search.py +704 -0
- brain/set_similarity.py +15 -0
- brain/setup.py +1205 -0
- brain/tags.py +56 -0
- brain/templates/Caddyfile.j2 +9 -0
- brain/templates/__init__.py +1 -0
- brain/templates/bin/__init__.py +1 -0
- brain/templates/bin/_brain-brief-fg.sh +25 -0
- brain/templates/bin/_brain-build-fg.sh +53 -0
- brain/templates/bin/_brain-watcher-fg.sh +65 -0
- brain/templates/bin/brain-down.sh +89 -0
- brain/templates/bin/brain-status.sh +83 -0
- brain/templates/bin/brain-up.sh +221 -0
- brain/templates/docker/age/Dockerfile +79 -0
- brain/templates/docker-compose.stock.yml.j2 +26 -0
- brain/templates/docker-compose.yml.j2 +34 -0
- brain/templates/env.example +190 -0
- brain/templates/launchd/__init__.py +1 -0
- brain/templates/launchd/com.brain.brief.plist.j2 +45 -0
- brain/templates/launchd/com.brain.build.plist.j2 +46 -0
- brain/templates/launchd/com.brain.watcher.plist.j2 +46 -0
- brain/templates/skill/SKILL.md +63 -0
- brain/templates/skill/__init__.py +1 -0
- brain/timeline.py +834 -0
- brain/todo.py +124 -0
- brain/uninstall.py +185 -0
- brain/vault/__init__.py +115 -0
- brain/vault/_atomic.py +25 -0
- brain/vault/daily_index.py +228 -0
- brain/vault/derived_links/__init__.py +50 -0
- brain/vault/derived_links/directory.py +683 -0
- brain/vault/derived_links/fence.py +408 -0
- brain/vault/derived_links/gws.py +64 -0
- brain/vault/derived_links/participants.py +143 -0
- brain/vault/derived_links/pass_runner.py +362 -0
- brain/vault/derived_links/rules.py +137 -0
- brain/vault/export.py +683 -0
- brain/vault/frontmatter.py +165 -0
- brain/vault/graph.py +620 -0
- brain/vault/graph_format.py +388 -0
- brain/vault/link_rewrite.py +235 -0
- brain/vault/links.py +260 -0
- brain/vault/note_builder.py +211 -0
- brain/vault/paths.py +55 -0
- brain/vault/quartz_overlay.py +236 -0
- brain/vault/rename.py +591 -0
- brain/vault/resolver.py +304 -0
- brain/vault/slug.py +127 -0
- brain/vault/sync.py +1513 -0
- brain/vault/sync_summaries.py +264 -0
- brain/vault/templates.py +145 -0
- brain/vault/watch.py +1052 -0
- brain/wiki/__init__.py +6 -0
- brain/wiki/_github_slugger.py +76 -0
- brain/wiki/_person_name.py +314 -0
- brain/wiki/build_homepage.py +541 -0
- brain/wiki/build_partial.py +273 -0
- brain/wiki/build_people.py +934 -0
- brain/wiki/build_related.py +758 -0
- brain/wiki/build_swap.py +585 -0
- brain/wiki/build_watcher.py +975 -0
- brain/wiki/edit_classifier.py +215 -0
- brain/wiki/errors.py +10 -0
- brain/wiki/fastpath_manifest.py +475 -0
- brain/wiki/fastpath_state.py +174 -0
- brain/wiki/install.py +296 -0
- brain/wiki/slug.py +111 -0
- secondbrain_py-0.2.1.dist-info/METADATA +195 -0
- secondbrain_py-0.2.1.dist-info/RECORD +273 -0
- secondbrain_py-0.2.1.dist-info/WHEEL +5 -0
- secondbrain_py-0.2.1.dist-info/entry_points.txt +11 -0
- secondbrain_py-0.2.1.dist-info/licenses/LICENSE +21 -0
- secondbrain_py-0.2.1.dist-info/top_level.txt +1 -0
brain/errors.py
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
"""Project-specific exception hierarchy.
|
|
2
|
+
|
|
3
|
+
Internal helpers that can fail in user-visible ways raise these exceptions so
|
|
4
|
+
the CLI and MCP server layers can map them to their respective frameworks
|
|
5
|
+
(``typer.Exit`` / ``McpError``) without sharing framework-specific imports.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from collections.abc import Sequence
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BrainError(Exception):
|
|
17
|
+
"""Base class for all brain-internal exceptions."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class IdPrefixError(BrainError):
|
|
21
|
+
"""Base class for failures resolving a UUID prefix to a document id."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class IdPrefixTooShort(IdPrefixError):
|
|
25
|
+
"""The supplied prefix is shorter than the 6-char minimum."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class IdPrefixNotHex(IdPrefixError):
|
|
29
|
+
"""The supplied prefix contains characters other than hex digits / hyphens."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class IdPrefixNotFound(IdPrefixError):
|
|
33
|
+
"""No document matches the supplied prefix."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, prefix: str) -> None:
|
|
36
|
+
super().__init__(f"document not found: {prefix}")
|
|
37
|
+
self.prefix = prefix
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class IdPrefixAmbiguous(IdPrefixError):
|
|
41
|
+
"""Multiple documents match the supplied prefix."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, prefix: str) -> None:
|
|
44
|
+
super().__init__(f"id prefix ambiguous: {prefix}")
|
|
45
|
+
self.prefix = prefix
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class DirectoryRefreshError(BrainError):
|
|
49
|
+
"""Raised when a Calendar / Contacts refresh fails (gws missing, JSON parse, etc.)."""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class EmbedError(BrainError):
|
|
53
|
+
"""Base class for embedding-backend failures (network / transport / API / shape).
|
|
54
|
+
|
|
55
|
+
Every embedder backend raises a subclass: the Ollama-hosted models raise
|
|
56
|
+
:class:`brain.embeddings.OllamaEmbedError` and the Voyage SDK backend raises
|
|
57
|
+
:class:`brain.embeddings.VoyageEmbedError`. Callers that must degrade on an
|
|
58
|
+
embed failure regardless of the active backend (the MCP server's
|
|
59
|
+
``_wrap_embed_error``, ``brain eval``'s per-query tolerance) ``except
|
|
60
|
+
EmbedError`` once instead of enumerating concrete backends. Inherits
|
|
61
|
+
:class:`BrainError` so the CLI / MCP layers map it without a
|
|
62
|
+
framework-specific import.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class AgeBootstrapError(BrainError):
|
|
67
|
+
"""Raised when Apache AGE session/graph bootstrap fails (wave G0).
|
|
68
|
+
|
|
69
|
+
Wraps the raw ``psycopg.Error`` from ``LOAD 'age'`` / ``CREATE EXTENSION
|
|
70
|
+
age`` / ``create_graph`` so the public ``brain.db`` bootstrap helpers never
|
|
71
|
+
leak a framework-specific exception to the CLI / MCP layers (repo rule: a
|
|
72
|
+
custom exception inheriting :class:`BrainError`). The originating
|
|
73
|
+
``psycopg.Error`` is preserved as ``__cause__`` (``raise ... from e``) for
|
|
74
|
+
diagnostics. The autocommit precondition violation in
|
|
75
|
+
:func:`brain.db.bootstrap_age` is a separate, plain :class:`BrainError`
|
|
76
|
+
(caller bug, not a DB failure)."""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class GraphBackendError(BrainError):
|
|
80
|
+
"""Raised when an Apache AGE graph-backend operation fails (wave G0-4).
|
|
81
|
+
|
|
82
|
+
Wraps the raw ``psycopg.Error`` from a generated Cypher / catalog call so
|
|
83
|
+
the :mod:`brain.graph_rag.backends` layer never leaks a framework-specific
|
|
84
|
+
exception (repo rule: a custom exception inheriting :class:`BrainError`).
|
|
85
|
+
The originating ``psycopg.Error`` is preserved as ``__cause__``
|
|
86
|
+
(``raise ... from e``) for diagnostics.
|
|
87
|
+
|
|
88
|
+
Also raised for caller-side precondition violations surfaced before the DB
|
|
89
|
+
round-trip — an invalid graph name, a non-positive traversal depth /
|
|
90
|
+
frontier cap, an out-of-range edge-weight floor, a cross-tenant payload, or
|
|
91
|
+
an unparseable ``agtype`` result. Those are caller bugs (analogous to the
|
|
92
|
+
autocommit precondition in :func:`brain.db.bootstrap_age`), not DB
|
|
93
|
+
failures, but share the type so callers can ``except GraphBackendError``
|
|
94
|
+
once.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class GraphReconcileError(BrainError):
|
|
99
|
+
"""Raised on a precondition failure in the GraphRAG reconcile layer (wave G1).
|
|
100
|
+
|
|
101
|
+
Surfaced by :mod:`brain.graph_rag.reconcile` before any graph write when the
|
|
102
|
+
caller asks to reconcile a document that does not exist (so there is no
|
|
103
|
+
``documents.content_hash`` to anchor the per-aspect ``graph_index_state``
|
|
104
|
+
watermark). A caller bug — analogous to the cross-tenant payload guard in
|
|
105
|
+
:class:`GraphBackendError` — so it fails fast rather than silently writing a
|
|
106
|
+
half-formed graph. Inherits :class:`BrainError` so the CLI / MCP layers can
|
|
107
|
+
map it without a framework-specific import.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class CooccurrenceError(BrainError):
|
|
112
|
+
"""Raised on invalid co-occurrence inputs (wave G1, GraphRAG).
|
|
113
|
+
|
|
114
|
+
Surfaced by :mod:`brain.graph_rag.cooccur` before any DB round-trip when a
|
|
115
|
+
derive-time parameter is degenerate — a non-positive sliding window (no pair
|
|
116
|
+
could ever co-occur) or a non-positive max-entities cap. These are caller
|
|
117
|
+
bugs (a misconfigured ``BRAIN_GRAPH_COOCCUR_WINDOW`` /
|
|
118
|
+
``BRAIN_GRAPH_MAX_ENTITIES_PER_DOC``), so they fail fast rather than silently
|
|
119
|
+
producing an empty / wrong contribution set.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class WeightingError(BrainError):
|
|
124
|
+
"""Raised on edge-weight inputs that cannot yield a normalized lift (wave G1).
|
|
125
|
+
|
|
126
|
+
Surfaced by :mod:`brain.graph_rag.weighting` when the supplied counts are
|
|
127
|
+
impossible for a real co-occurrence — a co-document count below 1 (no edge),
|
|
128
|
+
a marginal document count below 1, a co-document count exceeding the rarer
|
|
129
|
+
endpoint's marginal (you cannot co-occur in more documents than the rarer
|
|
130
|
+
entity appears in), or a generic-document-frequency ratio outside ``(0, 1]``.
|
|
131
|
+
The normalized lift is provably in ``(0, 1]`` once these are satisfied, so
|
|
132
|
+
the DB ``CHECK (weight > 0 AND weight <= 1)`` can never be violated.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class GraphTenantError(BrainError):
|
|
137
|
+
"""Raised when a graph operation resolves to an empty ``tenant_id`` (wave G2).
|
|
138
|
+
|
|
139
|
+
GraphRAG is multi-tenant (spec §9 D9): every relational source-of-truth row,
|
|
140
|
+
AGE vertex/edge property, and generated query is scoped by ``tenant_id``,
|
|
141
|
+
which the schema declares ``TEXT NOT NULL``.
|
|
142
|
+
:func:`brain.graph_rag.tenancy.resolve_tenant` raises this before any DB
|
|
143
|
+
round-trip when neither an explicit ``--tenant`` override nor the configured
|
|
144
|
+
``BRAIN_GRAPH_TENANT`` default yields a non-empty id. A caller bug (analogous
|
|
145
|
+
to the cross-tenant payload guard on :class:`GraphBackendError`), so it fails
|
|
146
|
+
fast rather than scoping a query to an empty tenant.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class GroupingError(BrainError):
|
|
151
|
+
"""Raised on invalid scoped-subgraph grouping parameters (wave G2).
|
|
152
|
+
|
|
153
|
+
Surfaced by :mod:`brain.graph_rag.grouping` before any work when a
|
|
154
|
+
grouping knob is degenerate — a ``min_edge_weight`` / ``bridge_keep_weight``
|
|
155
|
+
outside ``[0.0, 1.0]`` or a non-positive ``theme_limit``. These are caller
|
|
156
|
+
bugs (a misconfigured ``BRAIN_GRAPH_MIN_EDGE_WEIGHT`` /
|
|
157
|
+
``BRAIN_GRAPH_THEME_LIMIT``), so they fail fast rather than silently
|
|
158
|
+
producing an empty / wrong theme set. The grouping itself is pure logic,
|
|
159
|
+
never touching the DB, so this is the only failure mode.
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class GraphModeUnavailable(BrainError):
|
|
164
|
+
"""Raised when an explicit graph retrieval mode is not available in this wave.
|
|
165
|
+
|
|
166
|
+
Specifically, an explicit ``--mode global`` (CLI) / ``mode='global'`` (MCP)
|
|
167
|
+
request: global community-summary retrieval lands in G3, so the G2 core
|
|
168
|
+
**REJECTS** it (never degrades — only the *auto* router degrades
|
|
169
|
+
global→local; spec §17b decision 4). Raised by
|
|
170
|
+
:func:`brain.graph_rag.router.route` (and surfaced through
|
|
171
|
+
:func:`brain.graph_rag.retrieve.graph_rag_search`). The CLI maps it to
|
|
172
|
+
``typer.BadParameter`` (exit 2) and the MCP server to
|
|
173
|
+
``McpError(INVALID_PARAMS, ...)`` (waves G2-h/i). Inherits
|
|
174
|
+
:class:`BrainError` so those layers map it without a framework-specific
|
|
175
|
+
import.
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class InteractionError(BrainError):
|
|
180
|
+
"""Raised on invalid interaction inputs (unknown action / source).
|
|
181
|
+
|
|
182
|
+
The DB-level ``CHECK`` constraints on ``interactions.action`` and
|
|
183
|
+
``interactions.source`` are the authoritative gate; this Python-side
|
|
184
|
+
error gives Typer / MCP a clean message before the SQL round-trip
|
|
185
|
+
when the enum value is obviously wrong (e.g., typo at the call site).
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class PersonAmbiguous(BrainError):
|
|
190
|
+
"""Multiple persons match a ``--person`` argument; caller must disambiguate."""
|
|
191
|
+
|
|
192
|
+
def __init__(self, query: str, candidates: list[str]) -> None:
|
|
193
|
+
candidate_list = ", ".join(candidates[:5])
|
|
194
|
+
super().__init__(
|
|
195
|
+
f"--person {query!r} matched {len(candidates)} people "
|
|
196
|
+
f"(candidates: {candidate_list}). Use a more specific name."
|
|
197
|
+
)
|
|
198
|
+
self.query = query
|
|
199
|
+
self.candidates = candidates
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class PersonNotFound(BrainError):
|
|
203
|
+
"""No person matched the ``--person`` argument."""
|
|
204
|
+
|
|
205
|
+
def __init__(self, query: str) -> None:
|
|
206
|
+
super().__init__(f"--person {query!r} matched no one in the directory")
|
|
207
|
+
self.query = query
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class ElicitError(BrainError):
|
|
211
|
+
"""Base class for tacit-knowledge elicitation failures."""
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class ConnectError(BrainError):
|
|
215
|
+
"""Raised on invalid inputs in the ``brain connect`` auto-link layer (Plan 07).
|
|
216
|
+
|
|
217
|
+
Surfaced by :mod:`brain.connect` before any DB round-trip when a scoring /
|
|
218
|
+
refresh parameter is degenerate — a non-positive candidate limit or
|
|
219
|
+
per-doc cap, or a confidence threshold outside ``(0.0, 1.0]``. These are
|
|
220
|
+
caller bugs (a misconfigured ``BRAIN_CONNECT_*`` knob), so they fail fast
|
|
221
|
+
rather than silently producing an empty / wrong suggestion set. Also raised
|
|
222
|
+
when a suggestion-id prefix cannot be resolved to a single
|
|
223
|
+
``link_suggestions`` row. Inherits :class:`BrainError` so the CLI / MCP
|
|
224
|
+
layers map it without a framework-specific import.
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class DemoError(BrainError):
|
|
229
|
+
"""Raised when the ``brain demo`` sandbox cannot be provisioned / torn down.
|
|
230
|
+
|
|
231
|
+
Wraps Docker-CLI subprocess failures (daemon down, binary missing, timeout)
|
|
232
|
+
into a single actionable message that carries Docker's own stderr, so the
|
|
233
|
+
CLI layer maps it to a clean ``typer.Exit`` instead of leaking a raw
|
|
234
|
+
traceback. Inherits :class:`BrainError` so the demo CLI's existing
|
|
235
|
+
``except BrainError`` catch handles it.
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class ReviewError(BrainError):
|
|
240
|
+
"""Raised when a ``brain review scan`` pass fails partway through (Plan 03).
|
|
241
|
+
|
|
242
|
+
The conflict scan calls the local Ollama enricher once per surviving
|
|
243
|
+
document pair; if Ollama becomes unreachable mid-scan
|
|
244
|
+
(:class:`OllamaUnavailable`), the findings produced before the failure are
|
|
245
|
+
already persisted (the connection is committed before this is raised), and
|
|
246
|
+
the scan stops with this partial-result error rather than silently
|
|
247
|
+
discarding work. ``findings`` carries the review findings written so far;
|
|
248
|
+
``processed`` / ``total`` report how many entity candidates were adjudicated
|
|
249
|
+
before the failure so the CLI can print
|
|
250
|
+
"partial scan (N of M entities processed)". Inherits :class:`BrainError` so
|
|
251
|
+
the CLI / MCP layers map it without a framework-specific import.
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
def __init__(
|
|
255
|
+
self,
|
|
256
|
+
message: str,
|
|
257
|
+
*,
|
|
258
|
+
findings: Sequence[object] | None = None,
|
|
259
|
+
processed: int = 0,
|
|
260
|
+
total: int = 0,
|
|
261
|
+
) -> None:
|
|
262
|
+
super().__init__(message)
|
|
263
|
+
self.findings: list[object] = list(findings or [])
|
|
264
|
+
self.processed = processed
|
|
265
|
+
self.total = total
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class VaultNoteSyncError(BrainError):
|
|
269
|
+
"""Raised when authoring a vault note fails to resolve or index.
|
|
270
|
+
|
|
271
|
+
Carries the per-file ``(path, reason)`` pairs (the same shape as
|
|
272
|
+
:class:`~brain.vault.sync.SyncReport.errors`) so the CLI can print each
|
|
273
|
+
one and exit non-zero — preserving ``brain note new``'s historical
|
|
274
|
+
behavior — while a library caller (the elicit session loop) can inspect
|
|
275
|
+
``.errors`` programmatically.
|
|
276
|
+
"""
|
|
277
|
+
|
|
278
|
+
def __init__(self, errors: Sequence[tuple[Path, str]]) -> None:
|
|
279
|
+
joined = "; ".join(f"{path}: {reason}" for path, reason in errors)
|
|
280
|
+
super().__init__(f"vault note sync failed: {joined}")
|
|
281
|
+
self.errors: list[tuple[Path, str]] = list(errors)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class EnrichmentError(BrainError):
|
|
285
|
+
"""Raised when a per-document enrichment call fails unrecoverably.
|
|
286
|
+
|
|
287
|
+
"Unrecoverable" means the caller must NOT retry within this transaction
|
|
288
|
+
(e.g., the model returned malformed JSON twice in a row). The Q1-D
|
|
289
|
+
post-ingest hook catches this, logs a warning, and lets the ingest
|
|
290
|
+
commit with ``documents.summary`` still NULL — ``brain enrich --backfill``
|
|
291
|
+
can pick the row up later.
|
|
292
|
+
"""
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class OllamaUnavailable(EnrichmentError):
|
|
296
|
+
"""The Ollama server is unreachable / returned a connection error / 5xx.
|
|
297
|
+
|
|
298
|
+
Distinct subclass of :class:`EnrichmentError` so the ingest hook can
|
|
299
|
+
``except OllamaUnavailable`` specifically — the message it logs guides
|
|
300
|
+
the user to ``brain enrich --backfill`` once Ollama is back, while the
|
|
301
|
+
``brain enrich --backfill`` CLI surfaces it as a clear "is Ollama
|
|
302
|
+
running?" error on the first row.
|
|
303
|
+
"""
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class AudioError(BrainError):
|
|
307
|
+
"""Raised when ``brain audio`` script generation fails unrecoverably (Plan 04).
|
|
308
|
+
|
|
309
|
+
Covers the audio-overview script path: the graph layer is disabled, no
|
|
310
|
+
themes / communities surfaced for the requested person / topic, or the
|
|
311
|
+
script generator could not coax a structurally valid two-host dialogue out
|
|
312
|
+
of the model after its retry. Inherits :class:`BrainError` so the CLI maps
|
|
313
|
+
it to a clean red error + exit 1 without a framework-specific import. An
|
|
314
|
+
Ollama transport failure surfaces as :class:`OllamaUnavailable` (a separate
|
|
315
|
+
branch) rather than this error.
|
|
316
|
+
"""
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class TtsError(BrainError):
|
|
320
|
+
"""Raised when a text-to-speech backend fails (Plan 04, Wave B).
|
|
321
|
+
|
|
322
|
+
Surfaced by :func:`brain.audio.make_tts_backend` for an unrecognized
|
|
323
|
+
``--tts`` spec (caller bug) and by :class:`brain.audio.ShellTtsBackend`
|
|
324
|
+
when the user-supplied synthesis command is missing, exits non-zero, or
|
|
325
|
+
times out. The script ``.json`` / ``.md`` artifacts are always written
|
|
326
|
+
BEFORE synthesis, so they survive a TTS failure. Inherits
|
|
327
|
+
:class:`BrainError` so the CLI maps it to a clean red error + exit 1.
|
|
328
|
+
"""
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
class IngestAmbiguousSource(BrainError):
|
|
332
|
+
"""Raised when multiple documents share a single ``(source_kind, source_external_id)`` key.
|
|
333
|
+
|
|
334
|
+
This normally cannot happen — ``sources(kind, external_id)`` is UNIQUE
|
|
335
|
+
(migration 001), so one source row maps to exactly one document for
|
|
336
|
+
Krisp/Slack stdin ingests. The edge case arises when ``brain rm`` deletes
|
|
337
|
+
the document row but leaves the orphaned ``sources`` row behind, and two
|
|
338
|
+
concurrent ingests then both INSERT against that orphaned row. The result
|
|
339
|
+
is two ``documents`` rows sharing one source; the next re-ingest via
|
|
340
|
+
``(kind, external_id)`` raises this error so the user can resolve the
|
|
341
|
+
duplicate manually rather than having ``--force`` silently pick one.
|
|
342
|
+
"""
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
class DraftSkipped(BrainError):
|
|
346
|
+
"""Reserved for future opt-in draft-skip paths.
|
|
347
|
+
|
|
348
|
+
.. deprecated::
|
|
349
|
+
No longer raised by the default Gmail ingest path (wave Q1-A,
|
|
350
|
+
2026-05-11). All Gmail drafts are now ingested with
|
|
351
|
+
``documents.draft = TRUE`` so the wiki quarantine (P1.6,
|
|
352
|
+
``contentIndex.ts:397``) hides them from Quartz while
|
|
353
|
+
``brain search`` / ``brain show`` still surface them. This class
|
|
354
|
+
is kept so future callers (e.g. a ``--skip-drafts`` flag) can
|
|
355
|
+
raise it without a schema change.
|
|
356
|
+
"""
|
|
357
|
+
|
brain/eval/__init__.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Eval harness for second-brain hybrid search.
|
|
2
|
+
|
|
3
|
+
Public surface re-exported from sub-modules:
|
|
4
|
+
|
|
5
|
+
- :class:`EvalQuery` — a single query from the golden corpus
|
|
6
|
+
- :class:`EvalResult` / :class:`EvalReport` / :class:`CategorySummary` — run output types
|
|
7
|
+
- :func:`run_eval` — execute the harness against a live DB + embedder
|
|
8
|
+
- :func:`load_corpus` — parse a golden-corpus YAML file
|
|
9
|
+
- :class:`BaselineDiff` / :class:`QueryDiff` — diff data types
|
|
10
|
+
- :func:`save_baseline` / :func:`load_baseline` / :func:`diff_reports` — baseline I/O
|
|
11
|
+
- :class:`ConceptF1Report` / :class:`ConceptFixtureDoc` / :func:`concept_set_micro_f1`
|
|
12
|
+
/ :func:`normalize_concept_pairs` / :func:`load_concept_fixture` — GraphRAG
|
|
13
|
+
concept-extractor eval gate (wave G2-j)
|
|
14
|
+
- :class:`LocalRetrievalScore` / :class:`ThemeRetrievalScore`
|
|
15
|
+
/ :func:`score_local_docs` / :func:`score_themes` — GraphRAG graph-retrieval
|
|
16
|
+
eval scorers (wave G2-j)
|
|
17
|
+
- :class:`GraphDocEvalResult` / :class:`GraphThemesEvalResult`
|
|
18
|
+
/ :class:`GraphEvalReport` / :func:`run_graph_eval` — parallel GraphRAG
|
|
19
|
+
graph-retrieval eval runner (wave G4-d; separate from the hybrid runner)
|
|
20
|
+
- :class:`GraphBaselineDiff` / :class:`GraphDocDiff` / :class:`GraphThemesDiff`
|
|
21
|
+
/ :func:`save_graph_baseline` / :func:`load_graph_baseline`
|
|
22
|
+
/ :func:`diff_graph_reports` — graph-eval canary baseline I/O (wave G4-d)
|
|
23
|
+
- :class:`EvalError` / :class:`EvalMetricError` / :class:`EvalCorpusError`
|
|
24
|
+
/ :class:`EvalBaselineError` — exception hierarchy
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from .answer_eval import (
|
|
28
|
+
AnswerEvalCase,
|
|
29
|
+
AnswerEvalReport,
|
|
30
|
+
AnswerScore,
|
|
31
|
+
answer_eval_report_to_dict,
|
|
32
|
+
load_answer_corpus,
|
|
33
|
+
run_answer_eval,
|
|
34
|
+
score_answer,
|
|
35
|
+
)
|
|
36
|
+
from .baseline import (
|
|
37
|
+
BaselineDiff,
|
|
38
|
+
QueryDiff,
|
|
39
|
+
diff_reports,
|
|
40
|
+
load_baseline,
|
|
41
|
+
mean_metrics_regressed,
|
|
42
|
+
save_baseline,
|
|
43
|
+
)
|
|
44
|
+
from .concept_extraction import (
|
|
45
|
+
ConceptF1Report,
|
|
46
|
+
ConceptFixtureDoc,
|
|
47
|
+
ConceptPair,
|
|
48
|
+
concept_set_micro_f1,
|
|
49
|
+
load_concept_fixture,
|
|
50
|
+
normalize_concept_pairs,
|
|
51
|
+
)
|
|
52
|
+
from .corpus import EvalQuery, load_corpus
|
|
53
|
+
from .errors import EvalBaselineError, EvalCorpusError, EvalError, EvalMetricError
|
|
54
|
+
from .graph_baseline import (
|
|
55
|
+
GraphBaselineDiff,
|
|
56
|
+
GraphDocDiff,
|
|
57
|
+
GraphThemesDiff,
|
|
58
|
+
diff_graph_reports,
|
|
59
|
+
load_graph_baseline,
|
|
60
|
+
save_graph_baseline,
|
|
61
|
+
)
|
|
62
|
+
from .graph_retrieval import (
|
|
63
|
+
LocalRetrievalScore,
|
|
64
|
+
ThemeRetrievalScore,
|
|
65
|
+
score_local_docs,
|
|
66
|
+
score_themes,
|
|
67
|
+
)
|
|
68
|
+
from .graph_runner import (
|
|
69
|
+
GraphDocEvalResult,
|
|
70
|
+
GraphEvalReport,
|
|
71
|
+
GraphThemesEvalResult,
|
|
72
|
+
run_graph_eval,
|
|
73
|
+
)
|
|
74
|
+
from .runner import CategorySummary, EvalReport, EvalResult, run_eval
|
|
75
|
+
|
|
76
|
+
__all__ = [
|
|
77
|
+
# Corpus
|
|
78
|
+
"EvalQuery",
|
|
79
|
+
"load_corpus",
|
|
80
|
+
# Answer-quality eval (Plan 06)
|
|
81
|
+
"AnswerEvalCase",
|
|
82
|
+
"AnswerScore",
|
|
83
|
+
"AnswerEvalReport",
|
|
84
|
+
"score_answer",
|
|
85
|
+
"run_answer_eval",
|
|
86
|
+
"load_answer_corpus",
|
|
87
|
+
"answer_eval_report_to_dict",
|
|
88
|
+
# Runner output
|
|
89
|
+
"EvalResult",
|
|
90
|
+
"EvalReport",
|
|
91
|
+
"CategorySummary",
|
|
92
|
+
"run_eval",
|
|
93
|
+
# Baseline I/O
|
|
94
|
+
"BaselineDiff",
|
|
95
|
+
"QueryDiff",
|
|
96
|
+
"save_baseline",
|
|
97
|
+
"load_baseline",
|
|
98
|
+
"diff_reports",
|
|
99
|
+
"mean_metrics_regressed",
|
|
100
|
+
# GraphRAG concept-extractor eval gate (G2-j)
|
|
101
|
+
"ConceptF1Report",
|
|
102
|
+
"ConceptFixtureDoc",
|
|
103
|
+
"ConceptPair",
|
|
104
|
+
"concept_set_micro_f1",
|
|
105
|
+
"load_concept_fixture",
|
|
106
|
+
"normalize_concept_pairs",
|
|
107
|
+
# GraphRAG graph-retrieval eval scorers (G2-j)
|
|
108
|
+
"LocalRetrievalScore",
|
|
109
|
+
"ThemeRetrievalScore",
|
|
110
|
+
"score_local_docs",
|
|
111
|
+
"score_themes",
|
|
112
|
+
# GraphRAG graph-retrieval eval runner (G4-d)
|
|
113
|
+
"GraphDocEvalResult",
|
|
114
|
+
"GraphThemesEvalResult",
|
|
115
|
+
"GraphEvalReport",
|
|
116
|
+
"run_graph_eval",
|
|
117
|
+
# GraphRAG graph-eval canary baseline I/O (G4-d)
|
|
118
|
+
"GraphBaselineDiff",
|
|
119
|
+
"GraphDocDiff",
|
|
120
|
+
"GraphThemesDiff",
|
|
121
|
+
"save_graph_baseline",
|
|
122
|
+
"load_graph_baseline",
|
|
123
|
+
"diff_graph_reports",
|
|
124
|
+
# Errors
|
|
125
|
+
"EvalError",
|
|
126
|
+
"EvalMetricError",
|
|
127
|
+
"EvalCorpusError",
|
|
128
|
+
"EvalBaselineError",
|
|
129
|
+
]
|