know-do-graph 0.1.0__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.
- agents/__init__.py +0 -0
- agents/extraction_agent/__init__.py +0 -0
- agents/extraction_agent/agent.py +170 -0
- agents/graph_agent/__init__.py +5 -0
- agents/graph_agent/agent.py +373 -0
- agents/graph_agent/tools.py +2106 -0
- agents/maintenance_agent/__init__.py +0 -0
- agents/maintenance_agent/agent.py +283 -0
- agents/orchestrator/__init__.py +0 -0
- agents/orchestrator/agent.py +217 -0
- agents/review_agent/__init__.py +0 -0
- agents/review_agent/agent.py +188 -0
- agents/review_agent/tools.py +472 -0
- api/__init__.py +0 -0
- api/main.py +136 -0
- api/routes/__init__.py +0 -0
- api/routes/agent.py +81 -0
- api/routes/entries.py +411 -0
- api/routes/graph.py +132 -0
- api/routes/mem.py +179 -0
- api/routes/remote.py +815 -0
- api/routes/remote_sync.py +230 -0
- api/routes/retrieve.py +88 -0
- core/__init__.py +0 -0
- core/app_state.py +9 -0
- core/events.py +84 -0
- core/extraction/__init__.py +0 -0
- core/extraction/wikilink_parser.py +48 -0
- core/graph/__init__.py +0 -0
- core/graph/graph.py +204 -0
- core/memory/__init__.py +0 -0
- core/memory/memgraph.py +458 -0
- core/resources/starter.db +0 -0
- core/retrieval/__init__.py +0 -0
- core/retrieval/embedder.py +122 -0
- core/retrieval/fusion.py +52 -0
- core/retrieval/progressive.py +399 -0
- core/retrieval/retrieval.py +346 -0
- core/retrieval/vector_store.py +91 -0
- core/schemas/__init__.py +0 -0
- core/schemas/edge.py +46 -0
- core/schemas/entry.py +388 -0
- core/storage/__init__.py +0 -0
- core/storage/database.py +104 -0
- core/storage/models.py +66 -0
- core/storage/repository.py +243 -0
- core/sync/__init__.py +20 -0
- core/sync/autolink.py +301 -0
- core/sync/db_merge.py +297 -0
- core/sync/db_watcher.py +84 -0
- core/sync/remote_sync.py +345 -0
- examples/__init__.py +0 -0
- examples/example_entries.py +206 -0
- examples/pymatgen_interface_examples.py +811 -0
- frontend/dist/assets/index-BLfo7ZZu.css +1 -0
- frontend/dist/assets/index-G-mYbZ9R.js +83 -0
- frontend/dist/assets/index-G-mYbZ9R.js.map +1 -0
- frontend/dist/index.html +92 -0
- know_do_graph-0.1.0.dist-info/METADATA +765 -0
- know_do_graph-0.1.0.dist-info/RECORD +63 -0
- know_do_graph-0.1.0.dist-info/WHEEL +4 -0
- know_do_graph-0.1.0.dist-info/entry_points.txt +2 -0
- main.py +944 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
"""Progressive (staged) retrieval over the hierarchical skill memory.
|
|
2
|
+
|
|
3
|
+
Layers (see :class:`core.schemas.entry.SkillLevel`):
|
|
4
|
+
L1 — Capability (entry_type ∈ {capability, workflow})
|
|
5
|
+
L2 — Procedure (entry_type = procedure)
|
|
6
|
+
L3 — Heuristic (entry_type = heuristic)
|
|
7
|
+
L4 — Constraint (entry_type = constraint)
|
|
8
|
+
|
|
9
|
+
The motivation is to avoid dumping all paper knowledge as a single flat blob
|
|
10
|
+
into the agent's context. Typical flow::
|
|
11
|
+
|
|
12
|
+
goal
|
|
13
|
+
→ ProgressiveRetriever.plan(goal) # L1 / L2 only
|
|
14
|
+
→ execution
|
|
15
|
+
→ verifier feedback or uncertainty
|
|
16
|
+
→ ProgressiveRetriever.heuristics_for(skill) # L3 on demand
|
|
17
|
+
→ ProgressiveRetriever.constraints_for(skill) # L4 on demand
|
|
18
|
+
|
|
19
|
+
This module is a thin layer on top of :class:`RetrievalEngine` — it reuses the
|
|
20
|
+
hybrid keyword+vector ranking and adds level/edge filters.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from typing import Optional
|
|
26
|
+
|
|
27
|
+
from sqlalchemy.orm import Session
|
|
28
|
+
|
|
29
|
+
from core.graph.graph import KnowDoGraph
|
|
30
|
+
from core.retrieval.retrieval import RetrievalEngine
|
|
31
|
+
from core.schemas.edge import EdgeRelation
|
|
32
|
+
from core.schemas.entry import (
|
|
33
|
+
DEFAULT_LEVEL_FOR_TYPE,
|
|
34
|
+
Entry,
|
|
35
|
+
EntryType,
|
|
36
|
+
SkillLevel,
|
|
37
|
+
implied_level,
|
|
38
|
+
)
|
|
39
|
+
from core.storage.models import EdgeModel, EntryModel
|
|
40
|
+
|
|
41
|
+
# Levels considered "planner context" (cheap to load up front).
|
|
42
|
+
_PLAN_LEVELS = {SkillLevel.L1, SkillLevel.L2}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ProgressiveRetriever:
|
|
46
|
+
"""Staged retrieval interface for the hierarchical operational memory."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, db: Session, graph: KnowDoGraph) -> None:
|
|
49
|
+
self._db = db
|
|
50
|
+
self._graph = graph
|
|
51
|
+
self._engine = RetrievalEngine(db, graph)
|
|
52
|
+
|
|
53
|
+
# ------------------------------------------------------------------
|
|
54
|
+
# Stage 1 — planning context (L1 + L2)
|
|
55
|
+
# ------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
def plan(
|
|
58
|
+
self,
|
|
59
|
+
goal: str,
|
|
60
|
+
k: int = 5,
|
|
61
|
+
mode: str = "hybrid",
|
|
62
|
+
include_l2: bool = True,
|
|
63
|
+
) -> list[Entry]:
|
|
64
|
+
"""Return planner-level candidates (L1 capabilities, optionally L2 procedures).
|
|
65
|
+
|
|
66
|
+
Heuristics (L3) and constraints (L4) are deliberately excluded — call
|
|
67
|
+
:meth:`heuristics_for` / :meth:`constraints_for` once a candidate is
|
|
68
|
+
selected.
|
|
69
|
+
"""
|
|
70
|
+
allowed = {SkillLevel.L1, SkillLevel.L2} if include_l2 else {SkillLevel.L1}
|
|
71
|
+
# Pull a generous superset, then level-filter; we don't push the filter
|
|
72
|
+
# into SQL because skill_level lives in the metadata JSON blob.
|
|
73
|
+
candidates = self._engine.search_entries(query=goal, limit=max(k * 4, 20), mode=mode)
|
|
74
|
+
out: list[Entry] = []
|
|
75
|
+
for e in candidates:
|
|
76
|
+
if implied_level(e.entry_type, e.metadata.skill_level) in allowed:
|
|
77
|
+
out.append(e)
|
|
78
|
+
if len(out) >= k:
|
|
79
|
+
break
|
|
80
|
+
return out
|
|
81
|
+
|
|
82
|
+
# ------------------------------------------------------------------
|
|
83
|
+
# Stage 2 — heuristics (L3)
|
|
84
|
+
# ------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
def heuristics_for(
|
|
87
|
+
self,
|
|
88
|
+
skill: str,
|
|
89
|
+
k: int = 5,
|
|
90
|
+
include_semantic_fallback: bool = True,
|
|
91
|
+
) -> list[Entry]:
|
|
92
|
+
"""Return L3 heuristics attached to *skill* (id, slug, or alias).
|
|
93
|
+
|
|
94
|
+
Resolution order:
|
|
95
|
+
1. Nodes connected to *skill* by an inbound ``heuristic_for`` edge.
|
|
96
|
+
2. (Fallback, optional) Semantic search restricted to L3 entries.
|
|
97
|
+
"""
|
|
98
|
+
return self._sidecar_for(
|
|
99
|
+
skill=skill,
|
|
100
|
+
edge_relation=EdgeRelation.heuristic_for,
|
|
101
|
+
target_level=SkillLevel.L3,
|
|
102
|
+
target_entry_type=EntryType.heuristic,
|
|
103
|
+
k=k,
|
|
104
|
+
include_semantic_fallback=include_semantic_fallback,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# ------------------------------------------------------------------
|
|
108
|
+
# Stage 3 — constraints / failure modes (L4)
|
|
109
|
+
# ------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
def constraints_for(
|
|
112
|
+
self,
|
|
113
|
+
skill: str,
|
|
114
|
+
k: int = 5,
|
|
115
|
+
include_semantic_fallback: bool = True,
|
|
116
|
+
) -> list[Entry]:
|
|
117
|
+
"""Return L4 constraints / failure modes attached to *skill*.
|
|
118
|
+
|
|
119
|
+
Resolution order:
|
|
120
|
+
1. Nodes connected to *skill* by an inbound ``constraint_on`` or
|
|
121
|
+
``warning_about`` edge.
|
|
122
|
+
2. (Fallback, optional) Semantic search restricted to L4 entries.
|
|
123
|
+
"""
|
|
124
|
+
return self._sidecar_for(
|
|
125
|
+
skill=skill,
|
|
126
|
+
edge_relation=EdgeRelation.constraint_on,
|
|
127
|
+
extra_edge_relations=(EdgeRelation.warning_about,),
|
|
128
|
+
target_level=SkillLevel.L4,
|
|
129
|
+
target_entry_type=EntryType.constraint,
|
|
130
|
+
k=k,
|
|
131
|
+
include_semantic_fallback=include_semantic_fallback,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# ------------------------------------------------------------------
|
|
135
|
+
# Stage 4 — bundle for verifier / debugging loop
|
|
136
|
+
# ------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
def expand(
|
|
139
|
+
self,
|
|
140
|
+
skill: str,
|
|
141
|
+
stages: Optional[list[str]] = None,
|
|
142
|
+
k: int = 5,
|
|
143
|
+
) -> dict:
|
|
144
|
+
"""Return a bundle of additional context for an already-selected skill.
|
|
145
|
+
|
|
146
|
+
``stages`` is a subset of {"heuristics", "constraints", "decomposition"}.
|
|
147
|
+
Defaults to ``["heuristics", "constraints"]``.
|
|
148
|
+
"""
|
|
149
|
+
stages = stages or ["heuristics", "constraints"]
|
|
150
|
+
anchor = self._engine.resolve_identifier(skill)
|
|
151
|
+
if anchor is None:
|
|
152
|
+
return {"error": f"Skill '{skill}' not found.", "skill": skill}
|
|
153
|
+
|
|
154
|
+
bundle: dict = {
|
|
155
|
+
"skill": {
|
|
156
|
+
"id": anchor.id,
|
|
157
|
+
"slug": anchor.slug,
|
|
158
|
+
"title": anchor.title,
|
|
159
|
+
"level": (implied_level(anchor.entry_type, anchor.metadata.skill_level) or SkillLevel.L1).value,
|
|
160
|
+
},
|
|
161
|
+
}
|
|
162
|
+
if "heuristics" in stages:
|
|
163
|
+
bundle["heuristics"] = [self._summarize(e) for e in self.heuristics_for(anchor.id, k=k)]
|
|
164
|
+
if "constraints" in stages:
|
|
165
|
+
bundle["constraints"] = [self._summarize(e) for e in self.constraints_for(anchor.id, k=k)]
|
|
166
|
+
if "decomposition" in stages:
|
|
167
|
+
bundle["decomposition"] = [
|
|
168
|
+
self._summarize(e) for e in self._decomposition_for(anchor.id, k=k * 2)
|
|
169
|
+
]
|
|
170
|
+
return bundle
|
|
171
|
+
|
|
172
|
+
# ==================================================================
|
|
173
|
+
# Internal helpers
|
|
174
|
+
# ==================================================================
|
|
175
|
+
|
|
176
|
+
def _sidecar_for(
|
|
177
|
+
self,
|
|
178
|
+
skill: str,
|
|
179
|
+
edge_relation: EdgeRelation,
|
|
180
|
+
target_level: SkillLevel,
|
|
181
|
+
target_entry_type: EntryType,
|
|
182
|
+
k: int,
|
|
183
|
+
include_semantic_fallback: bool,
|
|
184
|
+
extra_edge_relations: tuple[EdgeRelation, ...] = (),
|
|
185
|
+
) -> list[Entry]:
|
|
186
|
+
anchor = self._engine.resolve_identifier(skill)
|
|
187
|
+
if anchor is None:
|
|
188
|
+
return []
|
|
189
|
+
|
|
190
|
+
# 1) Graph-attached sidecar nodes (authoritative).
|
|
191
|
+
seen: set[str] = set()
|
|
192
|
+
out: list[Entry] = []
|
|
193
|
+
relations = (edge_relation, *extra_edge_relations)
|
|
194
|
+
for source_id in self._inbound_sources(anchor.id, relations):
|
|
195
|
+
if source_id in seen:
|
|
196
|
+
continue
|
|
197
|
+
seen.add(source_id)
|
|
198
|
+
entry = self._engine.get_entry_by_id(source_id)
|
|
199
|
+
if entry is None:
|
|
200
|
+
continue
|
|
201
|
+
out.append(entry)
|
|
202
|
+
|
|
203
|
+
# 2) Semantic fallback restricted to the right level / type.
|
|
204
|
+
if include_semantic_fallback and len(out) < k:
|
|
205
|
+
need = k - len(out)
|
|
206
|
+
query = f"{anchor.title} {' '.join(anchor.tags)}".strip()
|
|
207
|
+
candidates = self._engine.search_entries(
|
|
208
|
+
query=query,
|
|
209
|
+
entry_type=target_entry_type,
|
|
210
|
+
limit=max(need * 4, 10),
|
|
211
|
+
mode="semantic",
|
|
212
|
+
)
|
|
213
|
+
for e in candidates:
|
|
214
|
+
if e.id in seen or e.id == anchor.id:
|
|
215
|
+
continue
|
|
216
|
+
if implied_level(e.entry_type, e.metadata.skill_level) != target_level:
|
|
217
|
+
continue
|
|
218
|
+
out.append(e)
|
|
219
|
+
seen.add(e.id)
|
|
220
|
+
if len(out) >= k:
|
|
221
|
+
break
|
|
222
|
+
|
|
223
|
+
return out[:k]
|
|
224
|
+
|
|
225
|
+
def _decomposition_for(self, skill_id: str, k: int) -> list[Entry]:
|
|
226
|
+
"""Return L2 nodes connected via ``decomposes_to`` from *skill_id*.
|
|
227
|
+
|
|
228
|
+
Note: ``decomposes_to`` is recorded as source=L1, target=L2 (the
|
|
229
|
+
capability decomposes into procedure).
|
|
230
|
+
"""
|
|
231
|
+
out: list[Entry] = []
|
|
232
|
+
seen: set[str] = set()
|
|
233
|
+
for edge in (
|
|
234
|
+
self._db.query(EdgeModel)
|
|
235
|
+
.filter(EdgeModel.source_id == skill_id, EdgeModel.relation == EdgeRelation.decomposes_to.value)
|
|
236
|
+
.limit(k * 4)
|
|
237
|
+
.all()
|
|
238
|
+
):
|
|
239
|
+
if edge.target_id in seen:
|
|
240
|
+
continue
|
|
241
|
+
seen.add(edge.target_id)
|
|
242
|
+
entry = self._engine.get_entry_by_id(edge.target_id)
|
|
243
|
+
if entry is not None:
|
|
244
|
+
out.append(entry)
|
|
245
|
+
if len(out) >= k:
|
|
246
|
+
break
|
|
247
|
+
return out
|
|
248
|
+
|
|
249
|
+
def _inbound_sources(
|
|
250
|
+
self,
|
|
251
|
+
target_id: str,
|
|
252
|
+
relations: tuple[EdgeRelation, ...],
|
|
253
|
+
) -> list[str]:
|
|
254
|
+
rel_values = [r.value for r in relations]
|
|
255
|
+
rows = (
|
|
256
|
+
self._db.query(EdgeModel)
|
|
257
|
+
.filter(EdgeModel.target_id == target_id, EdgeModel.relation.in_(rel_values))
|
|
258
|
+
.all()
|
|
259
|
+
)
|
|
260
|
+
return [r.source_id for r in rows]
|
|
261
|
+
|
|
262
|
+
# ------------------------------------------------------------------
|
|
263
|
+
# Cheap counts (used by /remote/entry to surface a progressive hint)
|
|
264
|
+
# ------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
def count_attached(self, skill: str) -> dict:
|
|
267
|
+
"""Return counts of L3 heuristics and L4 constraints **edge-attached** to
|
|
268
|
+
*skill* (id, slug, or alias).
|
|
269
|
+
|
|
270
|
+
This is a cheap probe — no entry bodies are loaded and no semantic
|
|
271
|
+
fallback is performed. It deliberately only counts nodes that are
|
|
272
|
+
explicitly connected to the current node via ``heuristic_for`` /
|
|
273
|
+
``constraint_on`` / ``warning_about`` edges, so callers can prompt
|
|
274
|
+
the user / agent to drill down only when there is something to find.
|
|
275
|
+
"""
|
|
276
|
+
anchor = self._engine.resolve_identifier(skill)
|
|
277
|
+
if anchor is None:
|
|
278
|
+
return {"resolved": False, "heuristics": 0, "constraints": 0}
|
|
279
|
+
|
|
280
|
+
heur = len(self._inbound_sources(anchor.id, (EdgeRelation.heuristic_for,)))
|
|
281
|
+
cons = len(
|
|
282
|
+
self._inbound_sources(
|
|
283
|
+
anchor.id,
|
|
284
|
+
(EdgeRelation.constraint_on, EdgeRelation.warning_about),
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
return {
|
|
288
|
+
"resolved": True,
|
|
289
|
+
"anchor_id": anchor.id,
|
|
290
|
+
"heuristics": heur,
|
|
291
|
+
"constraints": cons,
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
# ------------------------------------------------------------------
|
|
295
|
+
# Scoped search — search inside the L3/L4 sidecars of a single skill
|
|
296
|
+
# ------------------------------------------------------------------
|
|
297
|
+
|
|
298
|
+
# ``kind`` → (edge relations to follow inbound, expected target level)
|
|
299
|
+
_SIDECAR_KINDS: dict[str, tuple[tuple[EdgeRelation, ...], SkillLevel]] = {
|
|
300
|
+
"heuristics": ((EdgeRelation.heuristic_for,), SkillLevel.L3),
|
|
301
|
+
"constraints": (
|
|
302
|
+
(EdgeRelation.constraint_on, EdgeRelation.warning_about),
|
|
303
|
+
SkillLevel.L4,
|
|
304
|
+
),
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
def search_attached(
|
|
308
|
+
self,
|
|
309
|
+
skill: str,
|
|
310
|
+
kind: str,
|
|
311
|
+
query: Optional[str] = None,
|
|
312
|
+
tags: Optional[list[str]] = None,
|
|
313
|
+
limit: int = 10,
|
|
314
|
+
mode: str = "hybrid",
|
|
315
|
+
) -> tuple[list[Entry], int]:
|
|
316
|
+
"""Search the L3/L4 sidecar nodes attached to *skill*.
|
|
317
|
+
|
|
318
|
+
Returns ``(entries, total_attached)`` where ``total_attached`` is the
|
|
319
|
+
size of the scope (useful for paginating / warning the caller when
|
|
320
|
+
the scope is large).
|
|
321
|
+
|
|
322
|
+
- ``kind`` is ``"heuristics"`` (L3) or ``"constraints"`` (L4).
|
|
323
|
+
- When ``query`` is given, runs the same hybrid keyword+vector search
|
|
324
|
+
as :meth:`RetrievalEngine.search_entries` but **restricts the
|
|
325
|
+
candidate pool to the attached sidecar nodes**.
|
|
326
|
+
- When ``query`` is None, returns up to ``limit`` of the attached
|
|
327
|
+
nodes ordered by ``usage_count`` desc so the most-used experience
|
|
328
|
+
/ most-cited limitation surfaces first.
|
|
329
|
+
|
|
330
|
+
An L3/L4 node attached to multiple parents is unaffected — we scope
|
|
331
|
+
by inbound edges to *this* anchor, so the same node will correctly
|
|
332
|
+
appear under each parent it is attached to.
|
|
333
|
+
"""
|
|
334
|
+
spec = self._SIDECAR_KINDS.get(kind)
|
|
335
|
+
if spec is None:
|
|
336
|
+
raise ValueError(f"Unknown sidecar kind: {kind!r}")
|
|
337
|
+
relations, _target_level = spec
|
|
338
|
+
|
|
339
|
+
anchor = self._engine.resolve_identifier(skill)
|
|
340
|
+
if anchor is None:
|
|
341
|
+
return [], 0
|
|
342
|
+
|
|
343
|
+
scope_ids = set(self._inbound_sources(anchor.id, relations))
|
|
344
|
+
total = len(scope_ids)
|
|
345
|
+
if not scope_ids:
|
|
346
|
+
return [], 0
|
|
347
|
+
|
|
348
|
+
# No query → return a usage-ranked slice of the scope. Cheap path
|
|
349
|
+
# that never loads the whole scope when it's large.
|
|
350
|
+
if not query:
|
|
351
|
+
rows = (
|
|
352
|
+
self._db.query(EntryModel)
|
|
353
|
+
.filter(EntryModel.id.in_(scope_ids))
|
|
354
|
+
.all()
|
|
355
|
+
)
|
|
356
|
+
entries = [Entry(**r.to_dict()) for r in rows]
|
|
357
|
+
entries = self._filter_by_tags(entries, tags)
|
|
358
|
+
entries.sort(
|
|
359
|
+
key=lambda e: (e.metadata.usage_count or 0),
|
|
360
|
+
reverse=True,
|
|
361
|
+
)
|
|
362
|
+
return entries[:limit], total
|
|
363
|
+
|
|
364
|
+
# With query → hybrid search, then intersect with scope. We over-fetch
|
|
365
|
+
# so the post-filter still has room to return ``limit`` items.
|
|
366
|
+
oversample = max(limit * 10, 50)
|
|
367
|
+
ranked = self._engine.search_entries(
|
|
368
|
+
query=query,
|
|
369
|
+
tags=tags,
|
|
370
|
+
limit=oversample,
|
|
371
|
+
mode=mode,
|
|
372
|
+
)
|
|
373
|
+
scoped = [e for e in ranked if e.id in scope_ids]
|
|
374
|
+
return scoped[:limit], total
|
|
375
|
+
|
|
376
|
+
@staticmethod
|
|
377
|
+
def _filter_by_tags(entries: list[Entry], tags: Optional[list[str]]) -> list[Entry]:
|
|
378
|
+
if not tags:
|
|
379
|
+
return entries
|
|
380
|
+
wanted = {t.lower() for t in tags}
|
|
381
|
+
return [e for e in entries if any(t.lower() in wanted for t in (e.tags or []))]
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
@staticmethod
|
|
385
|
+
def _summarize(entry: Entry) -> dict:
|
|
386
|
+
level = implied_level(entry.entry_type, entry.metadata.skill_level)
|
|
387
|
+
return {
|
|
388
|
+
"id": entry.id,
|
|
389
|
+
"slug": entry.slug,
|
|
390
|
+
"title": entry.title,
|
|
391
|
+
"entry_type": entry.entry_type.value,
|
|
392
|
+
"level": level.value if level else None,
|
|
393
|
+
"tags": entry.tags,
|
|
394
|
+
"content": entry.content,
|
|
395
|
+
"applicability": entry.metadata.applicability,
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
__all__ = ["ProgressiveRetriever"]
|