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
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""Pure heuristic auto-router for graph retrieval (spec §6d / §17b decisions 3-4).
|
|
2
|
+
|
|
3
|
+
:func:`route` is a **pure, deterministic, DB-free** function: given the query,
|
|
4
|
+
the requested ``mode``, an optional explicit ``person``, and a pre-fetched list
|
|
5
|
+
of :class:`KnownPerson` candidates, it decides which retrieval path
|
|
6
|
+
:func:`brain.graph_rag.retrieve.graph_rag_search` must dispatch to. Keeping it
|
|
7
|
+
pure means its full branch matrix is unit-testable without a database — the
|
|
8
|
+
caller resolves the DB-derived inputs (the known person entities) and passes
|
|
9
|
+
them in (dependency inversion).
|
|
10
|
+
|
|
11
|
+
The rule set (spec §17b decision 3; §17c Q6 — the G3-e flip; §17d Q1 — fuse):
|
|
12
|
+
|
|
13
|
+
1. **Explicit modes are honored** — including an explicit ``global`` request,
|
|
14
|
+
which now returns :data:`GLOBAL_MODE` (community summaries shipped in G3, so
|
|
15
|
+
the router dispatches to them; the G2 rejection is gone — spec §17c Q6), and
|
|
16
|
+
an explicit ``fuse`` request (:data:`FUSE_MODE`; wave G4-c, spec §17d Q1).
|
|
17
|
+
``fuse`` is honored ONLY as an explicit request — it is **never** an
|
|
18
|
+
auto-routed target (the auto branches stay local/themes/global).
|
|
19
|
+
2. **Thematic intent** is detected by a **closed regex grammar** (not an
|
|
20
|
+
open-ended keyword list): the normalized query matches
|
|
21
|
+
``\\bthemes?\\b|\\btopics?\\b|\\bpatterns?\\b|\\btrends?\\b|\\brecurring\\b``
|
|
22
|
+
**OR** ``\\bhow\\s+(has|have|did|does)\\b.{0,80}\\b(evolve|evolved|change|
|
|
23
|
+
changed|shift|shifted)\\b`` (case-insensitive).
|
|
24
|
+
3. **Person resolution precedence:** an explicit ``person`` argument first;
|
|
25
|
+
otherwise a token-boundary scan of the ``known_persons`` against the query.
|
|
26
|
+
4. **Query-match tie-break** among scanned persons: longest matched span →
|
|
27
|
+
highest ``doc_count`` → lexicographically smallest ``canonical_key``.
|
|
28
|
+
5. **Branches (auto):** ``thematic AND person → themes``;
|
|
29
|
+
``thematic AND no person → global`` (the real global community path now — G3-e
|
|
30
|
+
flipped the former G2 ``global→local`` degradation off); else → ``local``.
|
|
31
|
+
|
|
32
|
+
**G2 degradation machinery is KEEP-DORMANT (spec §17c Q6):** the
|
|
33
|
+
:data:`DEGRADED_FROM_GLOBAL` / :data:`DEGRADATION_REASON_G2` constants, the
|
|
34
|
+
:class:`RoutingDecision` ``degraded_from`` / ``degradation_reason`` fields, and
|
|
35
|
+
:class:`~brain.errors.GraphModeUnavailable` all **stay defined** for wire
|
|
36
|
+
stability, but G3 **never populates / raises** them — every path below leaves the
|
|
37
|
+
degradation fields ``None`` and no path raises ``GraphModeUnavailable``.
|
|
38
|
+
"""
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
import re
|
|
42
|
+
from collections.abc import Sequence
|
|
43
|
+
from dataclasses import dataclass
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"AUTO_MODE",
|
|
47
|
+
"DEGRADATION_REASON_G2",
|
|
48
|
+
"DEGRADED_FROM_GLOBAL",
|
|
49
|
+
"FUSE_MODE",
|
|
50
|
+
"GLOBAL_MODE",
|
|
51
|
+
"LOCAL_MODE",
|
|
52
|
+
"THEMES_MODE",
|
|
53
|
+
"KnownPerson",
|
|
54
|
+
"RoutedPerson",
|
|
55
|
+
"RoutingDecision",
|
|
56
|
+
"route",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
# The retrieval-mode vocabulary (canonical home; re-exported by retrieve.py /
|
|
60
|
+
# the package __init__ so existing ``LOCAL_MODE`` / ``THEMES_MODE`` imports keep
|
|
61
|
+
# working). ``auto`` triggers this router; ``global`` is the community path the
|
|
62
|
+
# router now dispatches to for an explicit request AND the auto
|
|
63
|
+
# thematic-no-person branch (the G3-e flip; spec §17c Q6).
|
|
64
|
+
LOCAL_MODE = "local"
|
|
65
|
+
THEMES_MODE = "themes"
|
|
66
|
+
GLOBAL_MODE = "global"
|
|
67
|
+
AUTO_MODE = "auto"
|
|
68
|
+
# Fuse (wave G4-c; spec §17d Q1): RRF of the local-graph doc leg with the
|
|
69
|
+
# vector/FTS hybrid doc leg. Honored ONLY as an explicit request — the auto
|
|
70
|
+
# router never targets it (auto stays local/themes/global).
|
|
71
|
+
FUSE_MODE = "fuse"
|
|
72
|
+
|
|
73
|
+
# Degradation signals (spec §17b decision 4) — KEEP-DORMANT after the G3-e flip
|
|
74
|
+
# (spec §17c Q6): retained for wire stability but no longer stamped onto any
|
|
75
|
+
# ``GraphContext`` / ``RoutingDecision`` (the auto thematic-no-person branch now
|
|
76
|
+
# routes to the real GLOBAL_MODE instead of degrading to local).
|
|
77
|
+
DEGRADED_FROM_GLOBAL = "global"
|
|
78
|
+
DEGRADATION_REASON_G2 = "global_unavailable_g2"
|
|
79
|
+
|
|
80
|
+
# Closed thematic-intent grammar (spec §17b decision 3 — NOT an open keyword
|
|
81
|
+
# list). Compiled once at import; case-insensitive.
|
|
82
|
+
_THEMATIC_KEYWORDS = re.compile(
|
|
83
|
+
r"\bthemes?\b|\btopics?\b|\bpatterns?\b|\btrends?\b|\brecurring\b",
|
|
84
|
+
re.IGNORECASE,
|
|
85
|
+
)
|
|
86
|
+
_THEMATIC_EVOLUTION = re.compile(
|
|
87
|
+
r"\bhow\s+(has|have|did|does)\b.{0,80}\b"
|
|
88
|
+
r"(evolve|evolved|change|changed|shift|shifted)\b",
|
|
89
|
+
re.IGNORECASE,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(frozen=True)
|
|
94
|
+
class KnownPerson:
|
|
95
|
+
"""A candidate person entity for the router's token-boundary scan.
|
|
96
|
+
|
|
97
|
+
Pre-fetched (tenant-scoped) by the caller from ``graph_entities`` so the
|
|
98
|
+
router stays DB-free. ``canonical_key`` is the lowercased People-Hub display
|
|
99
|
+
name (the dedup key + lexicographic tie-break), ``display_name`` the entity's
|
|
100
|
+
stored name, and ``doc_count`` the derived mention count (the doc-count
|
|
101
|
+
tie-break input).
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
canonical_key: str
|
|
105
|
+
display_name: str
|
|
106
|
+
doc_count: int = 0
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass(frozen=True)
|
|
110
|
+
class RoutedPerson:
|
|
111
|
+
"""The person a query routed to (spec §17b decision 3).
|
|
112
|
+
|
|
113
|
+
``source`` is ``"explicit"`` (from the ``--person`` / MCP ``person`` arg) or
|
|
114
|
+
``"scanned"`` (a token-boundary match in the query). For the explicit case
|
|
115
|
+
``canonical_key`` / ``doc_count`` are unknown (``None``) — only the caller's
|
|
116
|
+
stripped person string is carried in ``display_name``. For the scanned case
|
|
117
|
+
all three mirror the winning :class:`KnownPerson`.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
display_name: str
|
|
121
|
+
source: str
|
|
122
|
+
canonical_key: str | None = None
|
|
123
|
+
doc_count: int | None = None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass(frozen=True)
|
|
127
|
+
class RoutingDecision:
|
|
128
|
+
"""The pure router's verdict (spec §6d / §17b decisions 3-4).
|
|
129
|
+
|
|
130
|
+
``executed_mode`` is the mode the caller must dispatch to —
|
|
131
|
+
:data:`LOCAL_MODE`, :data:`THEMES_MODE`, or :data:`GLOBAL_MODE` (the G3-e flip
|
|
132
|
+
made ``global`` dispatchable for explicit ``mode='global'`` and the auto
|
|
133
|
+
thematic-no-person branch). ``requested_mode`` echoes the caller's input mode.
|
|
134
|
+
``is_thematic`` records the regex-grammar verdict. ``resolved_person`` is the
|
|
135
|
+
person the query routed to (or ``None``). ``degraded_from`` /
|
|
136
|
+
``degradation_reason`` are **KEEP-DORMANT** (spec §17c Q6): defined for wire
|
|
137
|
+
stability but never populated by G3 — always ``None``.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
executed_mode: str
|
|
141
|
+
requested_mode: str
|
|
142
|
+
is_thematic: bool
|
|
143
|
+
resolved_person: RoutedPerson | None = None
|
|
144
|
+
degraded_from: str | None = None
|
|
145
|
+
degradation_reason: str | None = None
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def route(
|
|
149
|
+
query: str,
|
|
150
|
+
*,
|
|
151
|
+
mode: str,
|
|
152
|
+
person: str | None,
|
|
153
|
+
known_persons: Sequence[KnownPerson],
|
|
154
|
+
) -> RoutingDecision:
|
|
155
|
+
"""Decide the retrieval mode for one query (pure / deterministic).
|
|
156
|
+
|
|
157
|
+
See the module docstring for the full rule set. ``known_persons`` is only
|
|
158
|
+
consulted for the ``auto`` thematic-person scan; explicit modes ignore it.
|
|
159
|
+
Never raises :class:`~brain.errors.GraphModeUnavailable` — the G3-e flip made
|
|
160
|
+
``global`` a dispatchable mode (spec §17c Q6).
|
|
161
|
+
|
|
162
|
+
Raises:
|
|
163
|
+
ValueError: an unrecognized ``mode`` (caller bug).
|
|
164
|
+
"""
|
|
165
|
+
if mode != AUTO_MODE:
|
|
166
|
+
return _route_explicit(query, mode, person)
|
|
167
|
+
return _route_auto(query, person, known_persons)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _route_explicit(query: str, mode: str, person: str | None) -> RoutingDecision:
|
|
171
|
+
"""Honor an explicit (non-``auto``) mode, ``global`` / ``fuse`` included.
|
|
172
|
+
|
|
173
|
+
The G2 ``global`` rejection is gone: an explicit ``mode='global'`` now returns
|
|
174
|
+
a :data:`GLOBAL_MODE` decision (the caller dispatches to the community path).
|
|
175
|
+
An explicit ``mode='fuse'`` returns a :data:`FUSE_MODE` decision (wave G4-c,
|
|
176
|
+
spec §17d Q1 — the caller fuses the graph + hybrid doc legs). Local / themes
|
|
177
|
+
are honored unchanged; an unknown mode is still a caller bug.
|
|
178
|
+
"""
|
|
179
|
+
if mode not in (LOCAL_MODE, THEMES_MODE, GLOBAL_MODE, FUSE_MODE):
|
|
180
|
+
raise ValueError(
|
|
181
|
+
f"unknown graph retrieval mode {mode!r} (expected one of "
|
|
182
|
+
f"{AUTO_MODE!r} / {LOCAL_MODE!r} / {THEMES_MODE!r} / "
|
|
183
|
+
f"{GLOBAL_MODE!r} / {FUSE_MODE!r})"
|
|
184
|
+
)
|
|
185
|
+
return RoutingDecision(
|
|
186
|
+
executed_mode=mode,
|
|
187
|
+
requested_mode=mode,
|
|
188
|
+
is_thematic=_is_thematic(query),
|
|
189
|
+
resolved_person=_explicit_person(person),
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _route_auto(
|
|
194
|
+
query: str, person: str | None, known_persons: Sequence[KnownPerson]
|
|
195
|
+
) -> RoutingDecision:
|
|
196
|
+
"""Run the heuristic branches for ``mode='auto'`` (spec dec. 3; §17c Q6 flip)."""
|
|
197
|
+
is_thematic = _is_thematic(query)
|
|
198
|
+
resolved = _explicit_person(person) or _scan_person(query, known_persons)
|
|
199
|
+
|
|
200
|
+
if is_thematic and resolved is not None:
|
|
201
|
+
return RoutingDecision(
|
|
202
|
+
executed_mode=THEMES_MODE,
|
|
203
|
+
requested_mode=AUTO_MODE,
|
|
204
|
+
is_thematic=True,
|
|
205
|
+
resolved_person=resolved,
|
|
206
|
+
)
|
|
207
|
+
if is_thematic:
|
|
208
|
+
# Thematic but no resolvable person → global (the real community path).
|
|
209
|
+
# G3-e flipped this off the former G2 ``global→local`` degradation: no
|
|
210
|
+
# degradation signals are stamped (spec §17c Q6 keeps them dormant).
|
|
211
|
+
return RoutingDecision(
|
|
212
|
+
executed_mode=GLOBAL_MODE,
|
|
213
|
+
requested_mode=AUTO_MODE,
|
|
214
|
+
is_thematic=True,
|
|
215
|
+
resolved_person=None,
|
|
216
|
+
)
|
|
217
|
+
return RoutingDecision(
|
|
218
|
+
executed_mode=LOCAL_MODE,
|
|
219
|
+
requested_mode=AUTO_MODE,
|
|
220
|
+
is_thematic=False,
|
|
221
|
+
resolved_person=resolved,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _is_thematic(query: str) -> bool:
|
|
226
|
+
"""``True`` iff the query matches the closed thematic-intent grammar."""
|
|
227
|
+
if not query:
|
|
228
|
+
return False
|
|
229
|
+
return bool(_THEMATIC_KEYWORDS.search(query) or _THEMATIC_EVOLUTION.search(query))
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _explicit_person(person: str | None) -> RoutedPerson | None:
|
|
233
|
+
"""Wrap a non-blank explicit ``person`` arg as the resolved person."""
|
|
234
|
+
if person is None:
|
|
235
|
+
return None
|
|
236
|
+
stripped = person.strip()
|
|
237
|
+
if not stripped:
|
|
238
|
+
return None
|
|
239
|
+
return RoutedPerson(display_name=stripped, source="explicit")
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _scan_person(
|
|
243
|
+
query: str, known_persons: Sequence[KnownPerson]
|
|
244
|
+
) -> RoutedPerson | None:
|
|
245
|
+
"""Token-boundary scan of ``known_persons`` against the query (spec dec. 3-4).
|
|
246
|
+
|
|
247
|
+
For each known person, match its ``canonical_key`` and ``display_name``
|
|
248
|
+
case-insensitively at word boundaries; the person's matched span is the
|
|
249
|
+
longest matching key. The winner is chosen by longest matched span → highest
|
|
250
|
+
``doc_count`` → lexicographically smallest ``canonical_key`` (the
|
|
251
|
+
``canonical_key`` is unique per tenant, so the ordering is total and the
|
|
252
|
+
result deterministic). Returns ``None`` when nothing matches.
|
|
253
|
+
"""
|
|
254
|
+
if not query or not known_persons:
|
|
255
|
+
return None
|
|
256
|
+
lowered = query.lower()
|
|
257
|
+
best: tuple[int, int, str] | None = None
|
|
258
|
+
winner: KnownPerson | None = None
|
|
259
|
+
for candidate in known_persons:
|
|
260
|
+
span = _best_match_span(lowered, candidate)
|
|
261
|
+
if span <= 0:
|
|
262
|
+
continue
|
|
263
|
+
# Order key: maximize span, then doc_count, then minimize canonical_key.
|
|
264
|
+
key = (-span, -candidate.doc_count, candidate.canonical_key)
|
|
265
|
+
if best is None or key < best:
|
|
266
|
+
best = key
|
|
267
|
+
winner = candidate
|
|
268
|
+
if winner is None:
|
|
269
|
+
return None
|
|
270
|
+
return RoutedPerson(
|
|
271
|
+
display_name=winner.display_name,
|
|
272
|
+
source="scanned",
|
|
273
|
+
canonical_key=winner.canonical_key,
|
|
274
|
+
doc_count=winner.doc_count,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _best_match_span(lowered_query: str, candidate: KnownPerson) -> int:
|
|
279
|
+
"""Longest word-boundary match length of a person's keys (0 = no match)."""
|
|
280
|
+
best = 0
|
|
281
|
+
for needle in (candidate.canonical_key, candidate.display_name):
|
|
282
|
+
normalized = (needle or "").strip().lower()
|
|
283
|
+
if not normalized:
|
|
284
|
+
continue
|
|
285
|
+
pattern = r"\b" + re.escape(normalized) + r"\b"
|
|
286
|
+
if re.search(pattern, lowered_query):
|
|
287
|
+
best = max(best, len(normalized))
|
|
288
|
+
return best
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"""Frozen value objects for the GraphRAG layer (no DB logic).
|
|
2
|
+
|
|
3
|
+
Two families live here:
|
|
4
|
+
|
|
5
|
+
* **Row mirrors** — :class:`GraphEntity`, :class:`EntityMention`,
|
|
6
|
+
:class:`EdgeContribution`, :class:`Edge` map 1:1 onto the migration-012 tables
|
|
7
|
+
(``graph_entities``, ``graph_entity_mentions``, ``graph_edge_contributions``,
|
|
8
|
+
``graph_relationships``). The raw ``embedding`` vector is deliberately *not*
|
|
9
|
+
carried on :class:`GraphEntity` — like :class:`brain.queries.DocumentRow` and
|
|
10
|
+
:class:`brain.search.SearchResult`, these are read-side value objects, not
|
|
11
|
+
storage handles.
|
|
12
|
+
* **Retrieval value objects** — :class:`ThemeGroup`, :class:`GraphContext`,
|
|
13
|
+
:class:`GraphExplanation` are the wire shape returned by graph retrieval
|
|
14
|
+
(spec §6/§9, §4 D8). They are populated by the G2 retrieval code; the field
|
|
15
|
+
set here is the v1 contract and later waves may extend it additively.
|
|
16
|
+
|
|
17
|
+
All tenantized rows / queries carry ``tenant_id`` (spec §4 D9). It defaults to
|
|
18
|
+
``"default"`` — the fixed tenant used by single-user local deployments — so the
|
|
19
|
+
local construction path is unchanged. ``ThemeGroup`` is the one exception: it is
|
|
20
|
+
a derived grouping over an already tenant-scoped subgraph, not a row mirror, so
|
|
21
|
+
it carries no ``tenant_id`` of its own.
|
|
22
|
+
|
|
23
|
+
All classes are ``frozen=True`` dataclasses. ``SearchResult`` is referenced only
|
|
24
|
+
under ``TYPE_CHECKING`` (with ``from __future__ import annotations``) so this
|
|
25
|
+
module never imports :mod:`brain.search` at runtime — keeping it free of any
|
|
26
|
+
import cycle with the ingest pipeline that later wires graph reconciliation in.
|
|
27
|
+
"""
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
from collections.abc import Mapping
|
|
31
|
+
from dataclasses import dataclass, field
|
|
32
|
+
from datetime import datetime
|
|
33
|
+
from typing import TYPE_CHECKING, Any
|
|
34
|
+
|
|
35
|
+
if TYPE_CHECKING:
|
|
36
|
+
from ..search import SearchResult
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class GraphEntity:
|
|
41
|
+
"""An entity node — mirrors a ``graph_entities`` row.
|
|
42
|
+
|
|
43
|
+
``entity_type`` is one of ``person``/``org``/``project``/``topic``/``tool``
|
|
44
|
+
(DB ``CHECK``-enforced). ``canonical_key`` is the dedup key (a resolved
|
|
45
|
+
person-key for people, ``lower(name)`` for concepts) and is unique per
|
|
46
|
+
``(tenant_id, entity_type, canonical_key)``. ``tenant_id`` scopes the row to
|
|
47
|
+
one tenant (spec §4 D9); single-user local deployments use the fixed default
|
|
48
|
+
tenant ``"default"``. ``doc_count`` is *derived* from mentions and refreshed
|
|
49
|
+
by the aggregate rebuild — never authoritative on write.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
id: str
|
|
53
|
+
entity_type: str
|
|
54
|
+
name: str
|
|
55
|
+
canonical_key: str
|
|
56
|
+
tenant_id: str = "default"
|
|
57
|
+
description: str | None = None
|
|
58
|
+
doc_count: int = 0
|
|
59
|
+
# Person-scoped count for the active retrieval (themes mode): distinct docs
|
|
60
|
+
# where this entity co-occurs with the queried person. None when not
|
|
61
|
+
# scope-computed (local/global/entity modes). Derived per-query, never stored.
|
|
62
|
+
scoped_doc_count: int | None = None
|
|
63
|
+
properties: dict[str, Any] = field(default_factory=dict)
|
|
64
|
+
created_at: datetime | None = None
|
|
65
|
+
updated_at: datetime | None = None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(frozen=True)
|
|
69
|
+
class EntityMention:
|
|
70
|
+
"""A per-document entity mention — mirrors a ``graph_entity_mentions`` row.
|
|
71
|
+
|
|
72
|
+
Source-of-truth row: re-ingest deletes and reinserts a document's mentions.
|
|
73
|
+
``source`` records provenance — ``"people"`` for the people pipeline or
|
|
74
|
+
``"extractor:<model>@<ver>"`` for the concept extractor. ``tenant_id`` scopes
|
|
75
|
+
the row to one tenant (part of the row's primary key; spec §4 D9).
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
entity_id: str
|
|
79
|
+
document_id: str
|
|
80
|
+
source: str
|
|
81
|
+
tenant_id: str = "default"
|
|
82
|
+
mention_count: int = 1
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True)
|
|
86
|
+
class EdgeContribution:
|
|
87
|
+
"""A per-document raw co-occurrence — mirrors ``graph_edge_contributions``.
|
|
88
|
+
|
|
89
|
+
Source-of-truth row holding the *raw* window co-occurrence count between two
|
|
90
|
+
entities within one document. Endpoints are canonicalized ``src_id < dst_id``
|
|
91
|
+
(DB ``CHECK``-enforced). No generic suppression or weighting is applied here;
|
|
92
|
+
those are derive/query-time concerns. ``tenant_id`` scopes the row to one
|
|
93
|
+
tenant (part of the row's primary key; spec §4 D9).
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
document_id: str
|
|
97
|
+
src_id: str
|
|
98
|
+
dst_id: str
|
|
99
|
+
tenant_id: str = "default"
|
|
100
|
+
cooccur_count: int = 1
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass(frozen=True)
|
|
104
|
+
class Edge:
|
|
105
|
+
"""A derived aggregate relationship — mirrors a ``graph_relationships`` row.
|
|
106
|
+
|
|
107
|
+
``weight`` is the normative association metric: normalized lift in ``(0, 1]``
|
|
108
|
+
(recomputed from contributions; doubles as the BFS path affinity). ``co_count``
|
|
109
|
+
is ``SUM`` of contribution counts and ``doc_count`` the distinct-document
|
|
110
|
+
count. Endpoints are canonicalized ``src_id < dst_id`` (DB ``CHECK``-enforced).
|
|
111
|
+
``tenant_id`` scopes the row to one tenant (part of the row's primary key;
|
|
112
|
+
spec §4 D9).
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
src_id: str
|
|
116
|
+
dst_id: str
|
|
117
|
+
weight: float
|
|
118
|
+
tenant_id: str = "default"
|
|
119
|
+
rel_type: str = "co_occurs"
|
|
120
|
+
co_count: int = 0
|
|
121
|
+
doc_count: int = 0
|
|
122
|
+
updated_at: datetime | None = None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass(frozen=True)
|
|
126
|
+
class CommunityMember:
|
|
127
|
+
"""A community ↔ entity membership — mirrors ``graph_community_members``.
|
|
128
|
+
|
|
129
|
+
Wave G3 (spec §17c Q1). One row per entity in a detected community.
|
|
130
|
+
``member_rank`` orders entities within the community (0-based, most-central
|
|
131
|
+
first) and ``member_weight`` is the entity's weighted degree inside the
|
|
132
|
+
community subgraph; both are DB ``CHECK``-enforced non-negative. At detection
|
|
133
|
+
time the owning ``community_key`` is not yet assigned (a reused key is matched
|
|
134
|
+
by Jaccard, a new one minted at persist), so it defaults to the empty string
|
|
135
|
+
until the persistence layer fills it in. ``tenant_id`` scopes the row to one
|
|
136
|
+
tenant (part of the row's primary key; spec §4 D9).
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
entity_id: str
|
|
140
|
+
member_rank: int = 0
|
|
141
|
+
member_weight: float = 0.0
|
|
142
|
+
community_key: str = ""
|
|
143
|
+
tenant_id: str = "default"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@dataclass(frozen=True)
|
|
147
|
+
class CommunityRecord:
|
|
148
|
+
"""A detected community — mirrors a ``graph_communities`` row (wave G3).
|
|
149
|
+
|
|
150
|
+
Single-level only (``level`` pinned to 0; spec §17c Q1 / §15). ``community_key``
|
|
151
|
+
is the durable, stable identity preserved across rebuilds by Jaccard matching
|
|
152
|
+
(spec §17c Q3/Q7). ``source_graph_hash`` is the tenant-graph dirty fingerprint
|
|
153
|
+
(an edge hash over ordered ``graph_relationships``); ``members_hash`` is the
|
|
154
|
+
per-community identity hash over the sorted member entity ids. The aggregate
|
|
155
|
+
stats (``member_count``/``edge_count``/``total_weight``) describe the
|
|
156
|
+
community subgraph. The ``summary*`` fields are populated lazily/eagerly at
|
|
157
|
+
build/refresh by G3-c (NULL here at detection); like :class:`GraphEntity` the
|
|
158
|
+
raw ``summary_embedding`` vector is deliberately not carried (read-side value
|
|
159
|
+
object, not a storage handle). ``tenant_id`` scopes the row to one tenant
|
|
160
|
+
(part of the row's primary key; spec §4 D9).
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
community_key: str
|
|
164
|
+
source_graph_hash: str
|
|
165
|
+
members_hash: str
|
|
166
|
+
tenant_id: str = "default"
|
|
167
|
+
level: int = 0
|
|
168
|
+
build_version: str = "networkx-louvain-v1"
|
|
169
|
+
member_count: int = 0
|
|
170
|
+
edge_count: int = 0
|
|
171
|
+
total_weight: float = 0.0
|
|
172
|
+
summary: str | None = None
|
|
173
|
+
summary_model: str | None = None
|
|
174
|
+
summary_at: datetime | None = None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@dataclass(frozen=True)
|
|
178
|
+
class ThemeGroup:
|
|
179
|
+
"""A cluster of related entities for the "themes with X" headline (spec §6b).
|
|
180
|
+
|
|
181
|
+
Produced by the scoped-subgraph grouping over X's documents. ``entities`` are
|
|
182
|
+
the group's key entities, ``doc_ids`` the representative X-documents, and
|
|
183
|
+
``score`` the group's total in-scope normalized lift (the ranking metric).
|
|
184
|
+
``summary`` is an optional on-demand ``summarize_group()`` Ollama synthesis
|
|
185
|
+
(top-K groups only, when ``--synthesize``/MCP ``synthesize=true``).
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
group_id: int
|
|
189
|
+
entities: list[GraphEntity] = field(default_factory=list)
|
|
190
|
+
doc_ids: list[str] = field(default_factory=list)
|
|
191
|
+
score: float = 0.0
|
|
192
|
+
summary: str | None = None
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@dataclass(frozen=True)
|
|
196
|
+
class CommunityGroup:
|
|
197
|
+
"""A detected community surfaced by global retrieval (spec §6c / §17c Q4-Q5).
|
|
198
|
+
|
|
199
|
+
The ranked unit of the **global** path (wave G3-d): each group is one
|
|
200
|
+
``graph_communities`` community that surfaced from the community-level RRF
|
|
201
|
+
(FTS over ``summary_tsv`` fused with vector cosine over ``summary_embedding``;
|
|
202
|
+
:func:`brain.graph_rag.global_._retrieve_global`). Distinct from
|
|
203
|
+
:class:`ThemeGroup` (a derived entity cluster over a person's scoped
|
|
204
|
+
subgraph) — a ``CommunityGroup`` mirrors a persisted, pre-summarized
|
|
205
|
+
community.
|
|
206
|
+
|
|
207
|
+
``community_key`` is the durable community identity, ``level`` the (single)
|
|
208
|
+
detection level (pinned 0; spec §17c Q1), ``member_count`` the full community
|
|
209
|
+
size, and ``score`` the fused RRF score (the ranking metric). ``summary`` is
|
|
210
|
+
the eager community summary (NULL when Ollama was unavailable at build —
|
|
211
|
+
the community then ranked on its FTS leg only). ``entities`` are the
|
|
212
|
+
representative member entities (top by ``member_rank``) and ``doc_ids`` the
|
|
213
|
+
representative documents that most mention the community's entities.
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
community_key: str
|
|
217
|
+
level: int = 0
|
|
218
|
+
member_count: int = 0
|
|
219
|
+
score: float = 0.0
|
|
220
|
+
summary: str | None = None
|
|
221
|
+
entities: list[GraphEntity] = field(default_factory=list)
|
|
222
|
+
doc_ids: list[str] = field(default_factory=list)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@dataclass(frozen=True)
|
|
226
|
+
class GraphExplanation:
|
|
227
|
+
"""Per-query graph-retrieval diagnostic (spec §6a/§9; §4 D8).
|
|
228
|
+
|
|
229
|
+
The graph analogue of :class:`brain.search.SearchExplanation`. Records the
|
|
230
|
+
seeds, resolved scope, traversal parameters, and pruning telemetry so a
|
|
231
|
+
caller can see *why* a :class:`GraphContext` looks the way it does. Populated
|
|
232
|
+
by the G2 retrieval code; provisional fields default to safe empties so the
|
|
233
|
+
object is constructible before that wave lands. ``tenant_id`` records the
|
|
234
|
+
tenant the query was scoped to (spec §4 D9 — every graph query injects it).
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
mode: str
|
|
238
|
+
tenant_id: str = "default"
|
|
239
|
+
seed_entity_ids: list[str] = field(default_factory=list)
|
|
240
|
+
person_keys: list[str] = field(default_factory=list)
|
|
241
|
+
depth: int = 0
|
|
242
|
+
frontier_cap: int = 0
|
|
243
|
+
min_edge_weight: float = 0.0
|
|
244
|
+
nodes_visited: int = 0
|
|
245
|
+
edges_considered: int = 0
|
|
246
|
+
generic_df_cap: int | None = None
|
|
247
|
+
matched_filters: dict[str, Any] = field(default_factory=dict)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@dataclass(frozen=True)
|
|
251
|
+
class GraphContext:
|
|
252
|
+
"""The distinct envelope returned by graph retrieval (spec §4 D8, §6, §9).
|
|
253
|
+
|
|
254
|
+
Themes/communities are *not* document hits, so graph retrieval returns this
|
|
255
|
+
shape rather than faking :class:`brain.search.SearchResult` parity. ``mode``
|
|
256
|
+
is the resolved retrieval mode (``local``/``themes``/``global``); ``person``
|
|
257
|
+
is set for scoped "themes with X" queries. ``docs`` reuses ``SearchResult``
|
|
258
|
+
for the document-hit portion. ``themes`` is populated for ``themes`` mode,
|
|
259
|
+
``communities`` for ``global`` mode (wave G3-d; spec §17c Q5), and
|
|
260
|
+
``entities`` for ``local`` mode. ``tenant_id`` records the tenant the query
|
|
261
|
+
was scoped to (spec §4 D9 — every graph query injects it).
|
|
262
|
+
|
|
263
|
+
Degradation signals (``requested_mode`` / ``degraded_from`` /
|
|
264
|
+
``degradation_reason``) are **KEPT DORMANT** (spec §17c Q6): they are now
|
|
265
|
+
**always** ``None``. They date to the wave-G2 era, when an ``auto`` thematic
|
|
266
|
+
query with no resolvable person degraded ``global`` → ``local`` and recorded
|
|
267
|
+
the substitution here. The G3-e router flip made ``global`` a real, executing
|
|
268
|
+
mode, so that degradation no longer happens and no code path populates these
|
|
269
|
+
fields. They are retained on the dataclass purely for wire/JSON stability
|
|
270
|
+
(the MCP + CLI ``--json`` shape keeps the keys) — not removed.
|
|
271
|
+
"""
|
|
272
|
+
|
|
273
|
+
session_id: str
|
|
274
|
+
mode: str
|
|
275
|
+
query: str
|
|
276
|
+
tenant_id: str = "default"
|
|
277
|
+
person: str | None = None
|
|
278
|
+
themes: list[ThemeGroup] = field(default_factory=list)
|
|
279
|
+
communities: list[CommunityGroup] = field(default_factory=list)
|
|
280
|
+
entities: list[GraphEntity] = field(default_factory=list)
|
|
281
|
+
docs: list[SearchResult] = field(default_factory=list)
|
|
282
|
+
explanation: GraphExplanation | None = None
|
|
283
|
+
requested_mode: str | None = None
|
|
284
|
+
degraded_from: str | None = None
|
|
285
|
+
degradation_reason: str | None = None
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@dataclass(frozen=True)
|
|
289
|
+
class EntitySummary:
|
|
290
|
+
"""Lightweight entity row for listing — projected from ``graph_entities``.
|
|
291
|
+
|
|
292
|
+
Returned by :func:`brain.graph_rag.relational.list_entities` for the
|
|
293
|
+
``brain graphrag entities`` admin surface. Does not carry the raw
|
|
294
|
+
``embedding`` vector (a storage handle, not a wire value — same convention
|
|
295
|
+
as :class:`GraphEntity` and :class:`CommunityRecord`).
|
|
296
|
+
"""
|
|
297
|
+
|
|
298
|
+
entity_type: str
|
|
299
|
+
name: str
|
|
300
|
+
canonical_key: str
|
|
301
|
+
doc_count: int
|
|
302
|
+
description: str | None = None
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
@dataclass(frozen=True)
|
|
306
|
+
class GraphStats:
|
|
307
|
+
"""At-a-glance graph overview for ``brain graphrag stats``.
|
|
308
|
+
|
|
309
|
+
Produced by :func:`brain.graph_rag.relational.graph_stats` from the
|
|
310
|
+
tenant's relational tables. ``counts_by_type`` maps each ``entity_type``
|
|
311
|
+
present in ``graph_entities`` to its row count; ``total_entities`` is their
|
|
312
|
+
sum. ``top_entities`` are the top-10 entities by ``doc_count`` (the same
|
|
313
|
+
slice ``brain graphrag entities --limit 10`` would return with sort=docs).
|
|
314
|
+
"""
|
|
315
|
+
|
|
316
|
+
counts_by_type: Mapping[str, int]
|
|
317
|
+
total_entities: int
|
|
318
|
+
total_relationships: int
|
|
319
|
+
total_communities: int
|
|
320
|
+
top_entities: tuple[EntitySummary, ...]
|