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,303 @@
|
|
|
1
|
+
"""Derived aggregate layer for the GraphRAG people graph (wave G1, spec §7 step 4).
|
|
2
|
+
|
|
3
|
+
Extracted from :mod:`brain.graph_rag.reconcile` (wave-G1 boundary refactor) to
|
|
4
|
+
keep that module focused on the per-document reconcile orchestration. This module
|
|
5
|
+
owns the **tenant-wide derived aggregate** concerns that both the per-document
|
|
6
|
+
reconcile and the corpus-wide refresh share:
|
|
7
|
+
|
|
8
|
+
* :func:`refresh_aggregates` — the public corpus-wide aggregate recompute backing
|
|
9
|
+
``brain graphrag refresh`` (spec §7 step 4 / §8 / §9).
|
|
10
|
+
* :func:`_recompute_aggregates` — full-tenant rebuild of ``graph_relationships``
|
|
11
|
+
from ``graph_edge_contributions`` (normalized lift + generic suppression,
|
|
12
|
+
G1-a :mod:`~brain.graph_rag.weighting`). Reused verbatim by
|
|
13
|
+
:func:`brain.graph_rag.reconcile.reconcile_document` /
|
|
14
|
+
:func:`~brain.graph_rag.reconcile.remove_document` so the weighting logic lives
|
|
15
|
+
in exactly one place.
|
|
16
|
+
* :func:`_gc_orphan_persons` / :func:`_gc_orphan_concepts` — GC of now-zero-mention
|
|
17
|
+
catalog rows, scoped to ``entity_type = 'person'`` and to the concept
|
|
18
|
+
``entity_type``s respectively. :func:`refresh_aggregates` runs BOTH (an
|
|
19
|
+
aspect-agnostic corpus-wide refresh), matching ``remove_document`` and
|
|
20
|
+
``build --force``.
|
|
21
|
+
|
|
22
|
+
Because the aggregates derive *purely* from the per-document source-of-truth, a
|
|
23
|
+
full recompute is always correct and cascade-safe; affected-only incremental
|
|
24
|
+
refresh is explicitly out of scope for v1 (spec §15).
|
|
25
|
+
|
|
26
|
+
``RefreshResult`` and ``refresh_aggregates`` are re-exported from
|
|
27
|
+
:mod:`brain.graph_rag.reconcile` (and :mod:`brain.graph_rag`) so existing
|
|
28
|
+
callers (``brain graphrag refresh`` CLI, tests) keep importing them from there
|
|
29
|
+
unchanged — this extraction is behavior-preserving, with no public-API move.
|
|
30
|
+
"""
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
from dataclasses import dataclass
|
|
34
|
+
from typing import TYPE_CHECKING, Any
|
|
35
|
+
|
|
36
|
+
import psycopg
|
|
37
|
+
|
|
38
|
+
from .backends.base import GraphBackend
|
|
39
|
+
from .weighting import generic_df_cap
|
|
40
|
+
|
|
41
|
+
if TYPE_CHECKING:
|
|
42
|
+
from .reconcile import ReconcileConfig
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"RefreshResult",
|
|
46
|
+
"refresh_aggregates",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class RefreshResult:
|
|
52
|
+
"""Outcome of a corpus-wide :func:`refresh_aggregates` call (spec §7 step 4).
|
|
53
|
+
|
|
54
|
+
Unlike ``ReconcileResult`` (which describes one document) this is the
|
|
55
|
+
tenant-wide aggregate recompute, so it carries no ``document_id``.
|
|
56
|
+
``relationship_count`` is the number of aggregate ``graph_relationships``
|
|
57
|
+
edges written for the tenant after the recompute; ``orphans_removed`` is the
|
|
58
|
+
number of now-zero-mention catalog rows GC'd in the same pass across BOTH
|
|
59
|
+
aspects (person + concept).
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
tenant_id: str
|
|
63
|
+
relationship_count: int = 0
|
|
64
|
+
orphans_removed: int = 0
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def refresh_aggregates(
|
|
68
|
+
conn: psycopg.Connection[Any],
|
|
69
|
+
*,
|
|
70
|
+
backend: GraphBackend,
|
|
71
|
+
config: ReconcileConfig | None = None,
|
|
72
|
+
) -> RefreshResult:
|
|
73
|
+
"""Corpus-wide aggregate recompute for one tenant (spec §7 step 4, §8/§9).
|
|
74
|
+
|
|
75
|
+
The corpus-wide weight/edge recompute of the tenant's derived aggregates from
|
|
76
|
+
the per-document source-of-truth, **without re-resolving any document's
|
|
77
|
+
persons** (that is
|
|
78
|
+
:func:`brain.graph_rag.reconcile.reconcile_document`'s job). It is the
|
|
79
|
+
explicit response to a corpus-wide weighting / suppression change — a new
|
|
80
|
+
``generic_df_ratio`` ⇒ a new ``suppress_ver`` — that must propagate to every
|
|
81
|
+
edge at once. Backs the ``brain graphrag refresh`` CLI. For a dropped or
|
|
82
|
+
corrupted AGE mirror (entity vertices missing) use
|
|
83
|
+
``brain graphrag build --force`` — the authoritative full rebuild — instead.
|
|
84
|
+
|
|
85
|
+
Reuses the SAME private recompute + GC helpers as ``reconcile_document`` and
|
|
86
|
+
``remove_document`` (no duplicated weighting logic): recompute every
|
|
87
|
+
``graph_relationships`` edge from ``graph_edge_contributions`` (normalized
|
|
88
|
+
lift + generic suppression, G1-a), GC any now-orphaned person AND concept
|
|
89
|
+
catalog rows (aspect-agnostic, matching ``remove_document``), then
|
|
90
|
+
rematerialize the AGE ``CO_OCCURS`` edges from the refreshed mirror and
|
|
91
|
+
DETACH DELETE the orphan vertices of both aspects.
|
|
92
|
+
|
|
93
|
+
Runs entirely inside ``with conn.transaction()`` (see the reconcile module's
|
|
94
|
+
"Connection contract"), so the relational recompute and the AGE rematerialize
|
|
95
|
+
commit or roll back together. Idempotent: a second call with the same config
|
|
96
|
+
+ source of truth converges to the identical graph (a stable no-op when
|
|
97
|
+
nothing changed).
|
|
98
|
+
|
|
99
|
+
Precondition: the tenant's ``Entity`` vertices already exist in AGE (run
|
|
100
|
+
``brain graphrag build --backfill`` first).
|
|
101
|
+
:meth:`~brain.graph_rag.backends.base.GraphBackend.refresh_cooccur_edges`
|
|
102
|
+
raises :class:`brain.errors.GraphBackendError` if a surviving relationship
|
|
103
|
+
references an entity with no AGE vertex — surfacing a refresh-before-build
|
|
104
|
+
mistake rather than silently under-materializing.
|
|
105
|
+
"""
|
|
106
|
+
# Late import keeps the module-level dependency one-way (reconcile → this
|
|
107
|
+
# module) so the extraction introduces no import cycle; reconcile is fully
|
|
108
|
+
# loaded by the time refresh_aggregates is ever called.
|
|
109
|
+
if config is None:
|
|
110
|
+
from .reconcile import ReconcileConfig
|
|
111
|
+
|
|
112
|
+
config = ReconcileConfig()
|
|
113
|
+
tenant_id = config.tenant_id
|
|
114
|
+
with conn.transaction():
|
|
115
|
+
relationship_count = _recompute_aggregates(
|
|
116
|
+
conn, tenant_id, config.generic_df_ratio
|
|
117
|
+
)
|
|
118
|
+
# GC orphans of BOTH aspects, matching remove_document (reconcile.py) and
|
|
119
|
+
# build --force (build.py): a corpus-wide refresh is aspect-agnostic, so a
|
|
120
|
+
# now-zero-mention concept catalog row + its AGE vertex must be cleaned up
|
|
121
|
+
# too, not just persons (spec §7 step 4 "DETACH DELETE zero-mention Entity
|
|
122
|
+
# vertices"). The concept GC is a no-op when no concept entities exist, so
|
|
123
|
+
# this is safe for the person-only default.
|
|
124
|
+
orphan_ids = [
|
|
125
|
+
*_gc_orphan_persons(conn, tenant_id),
|
|
126
|
+
*_gc_orphan_concepts(conn, tenant_id),
|
|
127
|
+
]
|
|
128
|
+
backend.refresh_cooccur_edges(conn, tenant_id)
|
|
129
|
+
if orphan_ids:
|
|
130
|
+
backend.detach_delete_entities(conn, tenant_id, orphan_ids)
|
|
131
|
+
return RefreshResult(
|
|
132
|
+
tenant_id=tenant_id,
|
|
133
|
+
relationship_count=relationship_count,
|
|
134
|
+
orphans_removed=len(orphan_ids),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _recompute_aggregates(
|
|
139
|
+
conn: psycopg.Connection[Any], tenant_id: str, generic_df_ratio: float
|
|
140
|
+
) -> int:
|
|
141
|
+
"""Full-tenant recompute of ``graph_relationships`` from contributions.
|
|
142
|
+
|
|
143
|
+
Refreshes every entity's derived ``doc_count``, then rebuilds the tenant's
|
|
144
|
+
aggregate edges: per pair, ``weight`` is the normalized lift over the pair's
|
|
145
|
+
co-document count and the endpoints' document frequencies, suppressed (row
|
|
146
|
+
omitted) when either endpoint exceeds the generic-frequency cap
|
|
147
|
+
(``round(generic_df_ratio × corpus_N)``). Returns the number of aggregate
|
|
148
|
+
edges written. Cascade-safe full recompute (spec §7 step 4).
|
|
149
|
+
|
|
150
|
+
**Set-based (perf-T4 G3).** The pair-by-pair Python loop + per-row
|
|
151
|
+
``INSERT`` is replaced with a single CTE-driven ``INSERT … SELECT``: two
|
|
152
|
+
aggregate CTEs compute the pair co-document counts and per-entity document
|
|
153
|
+
frequencies, then the outer SELECT joins them, applies the suppression
|
|
154
|
+
filter, computes the normalized-lift weight, and inserts every surviving
|
|
155
|
+
edge in one round-trip. The semantics are identical to the prior loop:
|
|
156
|
+
|
|
157
|
+
* The weight formula matches :func:`brain.graph_rag.weighting.normalized_lift`
|
|
158
|
+
— ``co_doc_count / min(src_df, dst_df)`` (∈ ``(0, 1]``).
|
|
159
|
+
* The generic-entity suppression matches
|
|
160
|
+
:func:`brain.graph_rag.weighting.edge_weight` — an edge is omitted iff
|
|
161
|
+
EITHER endpoint's ``df`` is *strictly greater than* ``cap`` (an entity
|
|
162
|
+
sitting exactly at the cap is kept, matching
|
|
163
|
+
:func:`is_generic_entity`).
|
|
164
|
+
* ``cap`` is computed in Python via :func:`generic_df_cap` so the banker's
|
|
165
|
+
rounding of ``corpus_N × generic_df_ratio`` is preserved exactly (SQL's
|
|
166
|
+
``round`` does not match Python's banker's rounding).
|
|
167
|
+
* Endpoint-pair ordering and per-row column values pass through unchanged:
|
|
168
|
+
the ``graph_edge_contributions`` ``(src_id, dst_id)`` pairs feed
|
|
169
|
+
``graph_relationships`` 1-to-1 with the prior loop, and the migration-012
|
|
170
|
+
``CHECK (weight > 0 AND weight <= 1)`` remains satisfied by construction.
|
|
171
|
+
|
|
172
|
+
Returns the number of materialized edges (the same number the prior loop
|
|
173
|
+
returned), read off ``cur.rowcount`` after the bulk insert.
|
|
174
|
+
"""
|
|
175
|
+
# Refresh the derived per-entity doc_count from the mentions source-of-truth.
|
|
176
|
+
conn.execute(
|
|
177
|
+
"""
|
|
178
|
+
UPDATE graph_entities ge
|
|
179
|
+
SET doc_count = COALESCE((
|
|
180
|
+
SELECT COUNT(DISTINCT m.document_id)
|
|
181
|
+
FROM graph_entity_mentions m
|
|
182
|
+
WHERE m.tenant_id = ge.tenant_id AND m.entity_id = ge.id
|
|
183
|
+
), 0)
|
|
184
|
+
WHERE ge.tenant_id = %s
|
|
185
|
+
""",
|
|
186
|
+
(tenant_id,),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Compute the generic-entity cap in Python so ``round`` uses banker's
|
|
190
|
+
# rounding identically to :func:`generic_df_cap` — keeping the SQL form's
|
|
191
|
+
# suppression boundary byte-identical to the prior loop's.
|
|
192
|
+
corpus_row = conn.execute(
|
|
193
|
+
"SELECT COUNT(DISTINCT document_id) "
|
|
194
|
+
"FROM graph_entity_mentions WHERE tenant_id = %s",
|
|
195
|
+
(tenant_id,),
|
|
196
|
+
).fetchone()
|
|
197
|
+
corpus_n = int(corpus_row[0]) if corpus_row is not None else 0
|
|
198
|
+
cap = generic_df_cap(corpus_n, generic_df_ratio)
|
|
199
|
+
|
|
200
|
+
# Clear the tenant's existing aggregate edges, then materialize every
|
|
201
|
+
# non-suppressed edge in ONE statement. The ``pair_aggregates`` CTE
|
|
202
|
+
# mirrors the prior ``SELECT src_id, dst_id, SUM(cooccur_count),
|
|
203
|
+
# COUNT(DISTINCT document_id) GROUP BY src_id, dst_id`` exactly; the
|
|
204
|
+
# ``entity_df`` CTE mirrors the prior ``df`` map (per-entity distinct
|
|
205
|
+
# document count). The outer SELECT joins them, filters by the generic
|
|
206
|
+
# cap (``df <= cap`` on BOTH endpoints — strictly-greater suppression),
|
|
207
|
+
# and computes ``LEAST(src.df, dst.df)`` as the normalized-lift divisor
|
|
208
|
+
# (the ``::float`` cast prevents integer truncation).
|
|
209
|
+
conn.execute(
|
|
210
|
+
"DELETE FROM graph_relationships WHERE tenant_id = %s", (tenant_id,)
|
|
211
|
+
)
|
|
212
|
+
with conn.cursor() as cur:
|
|
213
|
+
cur.execute(
|
|
214
|
+
"""
|
|
215
|
+
WITH pair_aggregates AS (
|
|
216
|
+
SELECT src_id, dst_id,
|
|
217
|
+
SUM(cooccur_count)::bigint AS co_count,
|
|
218
|
+
COUNT(DISTINCT document_id)::bigint AS pair_doc_count
|
|
219
|
+
FROM graph_edge_contributions
|
|
220
|
+
WHERE tenant_id = %(tenant)s
|
|
221
|
+
GROUP BY src_id, dst_id
|
|
222
|
+
),
|
|
223
|
+
entity_df AS (
|
|
224
|
+
SELECT entity_id,
|
|
225
|
+
COUNT(DISTINCT document_id)::bigint AS df
|
|
226
|
+
FROM graph_entity_mentions
|
|
227
|
+
WHERE tenant_id = %(tenant)s
|
|
228
|
+
GROUP BY entity_id
|
|
229
|
+
)
|
|
230
|
+
INSERT INTO graph_relationships
|
|
231
|
+
(tenant_id, src_id, dst_id, rel_type, weight, co_count, doc_count)
|
|
232
|
+
SELECT %(tenant)s, pa.src_id, pa.dst_id, 'co_occurs',
|
|
233
|
+
pa.pair_doc_count::float / LEAST(src.df, dst.df)::float,
|
|
234
|
+
pa.co_count,
|
|
235
|
+
pa.pair_doc_count
|
|
236
|
+
FROM pair_aggregates pa
|
|
237
|
+
JOIN entity_df src ON src.entity_id = pa.src_id
|
|
238
|
+
JOIN entity_df dst ON dst.entity_id = pa.dst_id
|
|
239
|
+
WHERE src.df <= %(cap)s AND dst.df <= %(cap)s
|
|
240
|
+
""",
|
|
241
|
+
{"tenant": tenant_id, "cap": cap},
|
|
242
|
+
)
|
|
243
|
+
written = cur.rowcount
|
|
244
|
+
return written
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _gc_orphan_persons(conn: psycopg.Connection[Any], tenant_id: str) -> list[str]:
|
|
248
|
+
"""Delete + return the ids of zero-mention person catalog rows (spec §7.4).
|
|
249
|
+
|
|
250
|
+
Scoped to ``entity_type = 'person'`` — the person aspect. Deleting the
|
|
251
|
+
``graph_entities`` row cascades to any stale relationship/contribution rows
|
|
252
|
+
(none expected post-recompute). The returned ids are handed to the backend
|
|
253
|
+
to DETACH DELETE the matching AGE vertices.
|
|
254
|
+
"""
|
|
255
|
+
return _gc_orphan_entities(conn, tenant_id, ["person"])
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _gc_orphan_concepts(conn: psycopg.Connection[Any], tenant_id: str) -> list[str]:
|
|
259
|
+
"""Delete + return the ids of zero-mention concept catalog rows (spec §7.4).
|
|
260
|
+
|
|
261
|
+
Scoped to the four concept ``entity_type``s (``topic``/``project``/``org``/
|
|
262
|
+
``tool``) — the concept aspect (wave G2-c). People are GC'd separately by
|
|
263
|
+
:func:`_gc_orphan_persons` so each aspect cleans only its own orphans, never
|
|
264
|
+
double-counting. Same cascade + DETACH-DELETE contract as the person GC.
|
|
265
|
+
"""
|
|
266
|
+
# Late import keeps the module-level dependency one-way (this module does not
|
|
267
|
+
# import the concept package at load) and avoids any import-order coupling.
|
|
268
|
+
from .concepts import CONCEPT_ENTITY_TYPES
|
|
269
|
+
|
|
270
|
+
return _gc_orphan_entities(conn, tenant_id, list(CONCEPT_ENTITY_TYPES))
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _gc_orphan_entities(
|
|
274
|
+
conn: psycopg.Connection[Any], tenant_id: str, entity_types: list[str]
|
|
275
|
+
) -> list[str]:
|
|
276
|
+
"""Delete + return zero-mention catalog rows of the given ``entity_types``.
|
|
277
|
+
|
|
278
|
+
The shared GC core behind :func:`_gc_orphan_persons` /
|
|
279
|
+
:func:`_gc_orphan_concepts`. An entity with no remaining
|
|
280
|
+
``graph_entity_mentions`` row is orphaned; deleting it cascades to any stale
|
|
281
|
+
relationship/contribution rows. ``entity_types`` is scoped via ``= ANY(%s)``
|
|
282
|
+
so each aspect GCs only its own rows.
|
|
283
|
+
"""
|
|
284
|
+
rows = conn.execute(
|
|
285
|
+
"""
|
|
286
|
+
SELECT ge.id::text FROM graph_entities ge
|
|
287
|
+
WHERE ge.tenant_id = %s
|
|
288
|
+
AND ge.entity_type = ANY(%s)
|
|
289
|
+
AND NOT EXISTS (
|
|
290
|
+
SELECT 1 FROM graph_entity_mentions m
|
|
291
|
+
WHERE m.tenant_id = ge.tenant_id AND m.entity_id = ge.id
|
|
292
|
+
)
|
|
293
|
+
""",
|
|
294
|
+
(tenant_id, entity_types),
|
|
295
|
+
).fetchall()
|
|
296
|
+
orphan_ids = [str(row[0]) for row in rows]
|
|
297
|
+
if orphan_ids:
|
|
298
|
+
conn.execute(
|
|
299
|
+
"DELETE FROM graph_entities "
|
|
300
|
+
"WHERE tenant_id = %s AND id::text = ANY(%s)",
|
|
301
|
+
(tenant_id, orphan_ids),
|
|
302
|
+
)
|
|
303
|
+
return orphan_ids
|