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/activity.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Time-windowed activity-reader queries (shared: brain brief + brain review weekly).
|
|
2
|
+
|
|
3
|
+
Single home for the read-only, time-bounded queries that both proactivity
|
|
4
|
+
features need: which documents were interacted with in a window
|
|
5
|
+
(:func:`iter_activity_docs`), which were ingested in a window
|
|
6
|
+
(:func:`iter_ingested_docs`), and which were captured in the last N hours
|
|
7
|
+
(:func:`recent_captures`). Plus :func:`week_bounds`, the ISO-week parser.
|
|
8
|
+
|
|
9
|
+
Import direction is one-way: ``brain.brief`` → ``brain.activity`` and
|
|
10
|
+
``brain.review.weekly`` → ``brain.activity``, never the reverse. This module
|
|
11
|
+
has no dependency on the brief or weekly-report shapes — it only knows query
|
|
12
|
+
shapes — so it stays the neutral shared layer.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from datetime import UTC, datetime
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
import psycopg
|
|
22
|
+
|
|
23
|
+
from .queries import DocumentRow
|
|
24
|
+
|
|
25
|
+
# Strict ``YYYY-Www`` shape (e.g. ``2026-W23``). Anchored so a stray suffix or a
|
|
26
|
+
# bare year is rejected before reaching ``datetime.fromisocalendar`` (which
|
|
27
|
+
# would otherwise raise a less-actionable error).
|
|
28
|
+
_ISO_WEEK_RE = re.compile(r"^(\d{4})-W(\d{2})$")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class ActivityDoc:
|
|
33
|
+
"""One document interacted with inside an activity window.
|
|
34
|
+
|
|
35
|
+
``interaction_count`` is the number of ``interactions`` rows for this doc in
|
|
36
|
+
the window; ``last_at`` is the most recent of them. ``tags`` is the doc's
|
|
37
|
+
tag array (may be empty).
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
document_id: str
|
|
41
|
+
title: str
|
|
42
|
+
interaction_count: int
|
|
43
|
+
last_at: datetime
|
|
44
|
+
tags: list[str]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class IngestedDoc:
|
|
49
|
+
"""One document ingested inside an activity window.
|
|
50
|
+
|
|
51
|
+
``source_kind`` is the joined ``sources.kind`` (``None`` for sourceless
|
|
52
|
+
manual docs). ``tags`` is the doc's tag array (may be empty).
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
document_id: str
|
|
56
|
+
title: str
|
|
57
|
+
ingested_at: datetime
|
|
58
|
+
source_kind: str | None
|
|
59
|
+
tags: list[str]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def week_bounds(iso_week: str) -> tuple[datetime, datetime]:
|
|
63
|
+
"""Parse ``"YYYY-Www"`` → ``(monday 00:00 UTC, sunday 23:59:59 UTC)``.
|
|
64
|
+
|
|
65
|
+
Uses :meth:`datetime.fromisocalendar`, so ISO year-boundaries are correct:
|
|
66
|
+
``week_bounds("2026-W01")`` → ``2025-12-29 00:00`` .. ``2026-01-04 23:59:59``.
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
ValueError: ``iso_week`` is not the strict ``YYYY-Www`` shape, or the
|
|
70
|
+
week number is out of range for the ISO year (e.g. ``2026-W53``).
|
|
71
|
+
"""
|
|
72
|
+
match = _ISO_WEEK_RE.match(iso_week)
|
|
73
|
+
if match is None:
|
|
74
|
+
raise ValueError(
|
|
75
|
+
f"week must be in 'YYYY-Www' form (e.g. '2026-W23'); got {iso_week!r}"
|
|
76
|
+
)
|
|
77
|
+
year, week = int(match.group(1)), int(match.group(2))
|
|
78
|
+
# fromisocalendar raises ValueError on week 0 / out-of-range weeks.
|
|
79
|
+
start = datetime.fromisocalendar(year, week, 1).replace(tzinfo=UTC)
|
|
80
|
+
end = datetime.fromisocalendar(year, week, 7).replace(
|
|
81
|
+
hour=23, minute=59, second=59, tzinfo=UTC
|
|
82
|
+
)
|
|
83
|
+
return start, end
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def current_iso_week() -> str:
|
|
87
|
+
"""Return the current ISO week as ``"YYYY-Www"`` (UTC), e.g. ``"2026-W23"``.
|
|
88
|
+
|
|
89
|
+
The default ``--week`` for ``brain review weekly``. Uses ``%G-W%V`` so the
|
|
90
|
+
ISO year + zero-padded ISO week round-trip through :func:`week_bounds`.
|
|
91
|
+
"""
|
|
92
|
+
return datetime.now(UTC).strftime("%G-W%V")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def iter_activity_docs(
|
|
96
|
+
conn: psycopg.Connection[Any],
|
|
97
|
+
*,
|
|
98
|
+
after: datetime,
|
|
99
|
+
before: datetime,
|
|
100
|
+
limit: int = 20,
|
|
101
|
+
) -> list[ActivityDoc]:
|
|
102
|
+
"""Return documents interacted with in ``[after, before]``, busiest first.
|
|
103
|
+
|
|
104
|
+
Joins ``interactions`` to ``documents`` over the time window, counts
|
|
105
|
+
interactions per doc, and orders by interaction count (then recency, then
|
|
106
|
+
id) for a deterministic result. Graph-target interaction rows
|
|
107
|
+
(``document_id IS NULL``) are excluded. Returns ``[]`` for an empty window.
|
|
108
|
+
"""
|
|
109
|
+
rows = conn.execute(
|
|
110
|
+
"""
|
|
111
|
+
SELECT d.id::text, d.title, d.tags,
|
|
112
|
+
COUNT(*) AS interaction_count,
|
|
113
|
+
MAX(i.at) AS last_at
|
|
114
|
+
FROM interactions i
|
|
115
|
+
JOIN documents d ON d.id = i.document_id
|
|
116
|
+
WHERE i.at BETWEEN %s AND %s
|
|
117
|
+
AND i.document_id IS NOT NULL
|
|
118
|
+
GROUP BY d.id, d.title, d.tags
|
|
119
|
+
ORDER BY interaction_count DESC, last_at DESC, d.id
|
|
120
|
+
LIMIT %s
|
|
121
|
+
""",
|
|
122
|
+
(after, before, limit),
|
|
123
|
+
).fetchall()
|
|
124
|
+
return [
|
|
125
|
+
ActivityDoc(
|
|
126
|
+
document_id=r[0],
|
|
127
|
+
title=r[1],
|
|
128
|
+
tags=list(r[2] or []),
|
|
129
|
+
interaction_count=int(r[3]),
|
|
130
|
+
last_at=r[4],
|
|
131
|
+
)
|
|
132
|
+
for r in rows
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def iter_ingested_docs(
|
|
137
|
+
conn: psycopg.Connection[Any],
|
|
138
|
+
*,
|
|
139
|
+
after: datetime,
|
|
140
|
+
before: datetime,
|
|
141
|
+
limit: int = 10,
|
|
142
|
+
) -> list[IngestedDoc]:
|
|
143
|
+
"""Return documents ingested in ``[after, before]``, newest first.
|
|
144
|
+
|
|
145
|
+
Left-joins ``sources`` so ``source_kind`` is populated (``None`` for manual
|
|
146
|
+
docs without a source row). Ordered by ingest time then id for determinism.
|
|
147
|
+
Returns ``[]`` for an empty window.
|
|
148
|
+
"""
|
|
149
|
+
rows = conn.execute(
|
|
150
|
+
"""
|
|
151
|
+
SELECT d.id::text, d.title, d.ingested_at, d.tags, s.kind
|
|
152
|
+
FROM documents d
|
|
153
|
+
LEFT JOIN sources s ON s.id = d.source_id
|
|
154
|
+
WHERE d.ingested_at BETWEEN %s AND %s
|
|
155
|
+
ORDER BY d.ingested_at DESC, d.id
|
|
156
|
+
LIMIT %s
|
|
157
|
+
""",
|
|
158
|
+
(after, before, limit),
|
|
159
|
+
).fetchall()
|
|
160
|
+
return [
|
|
161
|
+
IngestedDoc(
|
|
162
|
+
document_id=r[0],
|
|
163
|
+
title=r[1],
|
|
164
|
+
ingested_at=r[2],
|
|
165
|
+
tags=list(r[3] or []),
|
|
166
|
+
source_kind=r[4],
|
|
167
|
+
)
|
|
168
|
+
for r in rows
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def recent_captures(
|
|
173
|
+
conn: psycopg.Connection[Any],
|
|
174
|
+
*,
|
|
175
|
+
since_hours: int,
|
|
176
|
+
limit: int,
|
|
177
|
+
) -> list[DocumentRow]:
|
|
178
|
+
"""Return docs ingested in the last ``since_hours`` hours, newest first.
|
|
179
|
+
|
|
180
|
+
The ``brain brief`` "recent captures" section. Returns the light
|
|
181
|
+
:class:`brain.queries.DocumentRow` projection (no body / summary), mirroring
|
|
182
|
+
:func:`brain.queries.list_documents`. The window is computed in SQL relative
|
|
183
|
+
to ``NOW()`` so the caller need not pass a timestamp.
|
|
184
|
+
"""
|
|
185
|
+
rows = conn.execute(
|
|
186
|
+
"""
|
|
187
|
+
SELECT d.id::text, d.title, d.content_type, d.tags, s.kind, d.ingested_at
|
|
188
|
+
FROM documents d
|
|
189
|
+
LEFT JOIN sources s ON s.id = d.source_id
|
|
190
|
+
WHERE d.ingested_at >= NOW() - (%s * INTERVAL '1 hour')
|
|
191
|
+
ORDER BY d.ingested_at DESC, d.id
|
|
192
|
+
LIMIT %s
|
|
193
|
+
""",
|
|
194
|
+
(since_hours, limit),
|
|
195
|
+
).fetchall()
|
|
196
|
+
return [
|
|
197
|
+
DocumentRow(
|
|
198
|
+
id=r[0],
|
|
199
|
+
title=r[1],
|
|
200
|
+
content_type=r[2],
|
|
201
|
+
tags=list(r[3] or []),
|
|
202
|
+
source_kind=r[4],
|
|
203
|
+
ingested_at=r[5],
|
|
204
|
+
)
|
|
205
|
+
for r in rows
|
|
206
|
+
]
|