langchain-okf 0.2.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.
- langchain_okf/__init__.py +35 -0
- langchain_okf/_parser.py +289 -0
- langchain_okf/document_loader.py +189 -0
- langchain_okf/generator.py +1810 -0
- langchain_okf/graph.py +145 -0
- langchain_okf/py.typed +0 -0
- langchain_okf/retriever.py +489 -0
- langchain_okf/toolkit.py +639 -0
- langchain_okf/validate.py +200 -0
- langchain_okf-0.2.0.dist-info/METADATA +367 -0
- langchain_okf-0.2.0.dist-info/RECORD +13 -0
- langchain_okf-0.2.0.dist-info/WHEEL +4 -0
- langchain_okf-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""LangChain integration for the Open Knowledge Format (OKF).
|
|
2
|
+
|
|
3
|
+
Provides document loaders, retrievers, graph utilities, and agent tools
|
|
4
|
+
for working with OKF knowledge bundles in LangChain pipelines.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from langchain_okf.document_loader import OKFBundleLoader
|
|
8
|
+
from langchain_okf.generator import OKFBundleGenerator
|
|
9
|
+
from langchain_okf.graph import OKFKnowledgeGraph
|
|
10
|
+
from langchain_okf.retriever import OKFRetriever
|
|
11
|
+
from langchain_okf.toolkit import (
|
|
12
|
+
OKFListConceptsTool,
|
|
13
|
+
OKFReadConceptTool,
|
|
14
|
+
OKFRelatedConceptsTool,
|
|
15
|
+
OKFSearchTool,
|
|
16
|
+
OKFToolkit,
|
|
17
|
+
OKFVerifyConceptTool,
|
|
18
|
+
OKFWriteConceptTool,
|
|
19
|
+
)
|
|
20
|
+
from langchain_okf.validate import validate_bundle
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"OKFBundleGenerator",
|
|
24
|
+
"OKFBundleLoader",
|
|
25
|
+
"OKFKnowledgeGraph",
|
|
26
|
+
"OKFRetriever",
|
|
27
|
+
"OKFListConceptsTool",
|
|
28
|
+
"OKFReadConceptTool",
|
|
29
|
+
"OKFRelatedConceptsTool",
|
|
30
|
+
"OKFSearchTool",
|
|
31
|
+
"OKFVerifyConceptTool",
|
|
32
|
+
"OKFWriteConceptTool",
|
|
33
|
+
"OKFToolkit",
|
|
34
|
+
"validate_bundle",
|
|
35
|
+
]
|
langchain_okf/_parser.py
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""OKF concept document parser — OKF v0.2.
|
|
2
|
+
|
|
3
|
+
Aligns with OKF v0.2 specification:
|
|
4
|
+
https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md
|
|
5
|
+
|
|
6
|
+
Key v0.2 additions over v0.1:
|
|
7
|
+
- ``generated`` / ``verified`` trust families (§5.2)
|
|
8
|
+
- ``sources`` provenance list (§5.1)
|
|
9
|
+
- ``status`` and ``stale_after`` lifecycle fields (§5.4, §5.5)
|
|
10
|
+
- Actor convention for ``generated.by`` / ``verified[].by`` (§7)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from datetime import date, datetime, timezone
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
import yaml
|
|
22
|
+
|
|
23
|
+
_FRONTMATTER_DELIM = "---"
|
|
24
|
+
_RESERVED_FILENAMES = {"index.md", "log.md"}
|
|
25
|
+
_CROSS_LINK_RE = re.compile(
|
|
26
|
+
r"\[([^\]]*)\]\((/[^)]+\.md|\.\./[^)]+\.md|(?!\w+://)[^)]+\.md)\)"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _to_iso8601_str(val: Any) -> str:
|
|
31
|
+
"""Normalize datetime/date or string values to ISO 8601 UTC string."""
|
|
32
|
+
if val is None or val == "":
|
|
33
|
+
return ""
|
|
34
|
+
if isinstance(val, (datetime, date)):
|
|
35
|
+
if isinstance(val, datetime) and val.tzinfo is not None:
|
|
36
|
+
return val.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
37
|
+
return val.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
38
|
+
return str(val)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class OKFParseError(ValueError):
|
|
42
|
+
"""Raised when an OKF document cannot be parsed."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class OKFConcept:
|
|
47
|
+
"""A parsed OKF v0.2 concept document.
|
|
48
|
+
|
|
49
|
+
Attributes:
|
|
50
|
+
concept_id: Slash-separated path identity (e.g. ``tables/orders``).
|
|
51
|
+
frontmatter: Parsed YAML frontmatter dict.
|
|
52
|
+
body: Markdown body after frontmatter.
|
|
53
|
+
source_path: Absolute path to the source ``.md`` file.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
concept_id: str
|
|
57
|
+
frontmatter: dict[str, Any] = field(default_factory=dict)
|
|
58
|
+
body: str = ""
|
|
59
|
+
source_path: str = ""
|
|
60
|
+
_cached_links: list[str] | None = field(
|
|
61
|
+
default=None, init=False, repr=False, compare=False
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# ── Core fields (§4.1) ────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def type(self) -> str:
|
|
68
|
+
return str(self.frontmatter.get("type", ""))
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def title(self) -> str:
|
|
72
|
+
return str(self.frontmatter.get("title", ""))
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def description(self) -> str:
|
|
76
|
+
return str(self.frontmatter.get("description", ""))
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def resource(self) -> str:
|
|
80
|
+
return str(self.frontmatter.get("resource", ""))
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def tags(self) -> list[str]:
|
|
84
|
+
t = self.frontmatter.get("tags", [])
|
|
85
|
+
return t if isinstance(t, list) else [t]
|
|
86
|
+
|
|
87
|
+
# ── v0.1 compat ──────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def timestamp(self) -> str:
|
|
91
|
+
"""Legacy v0.1 field. Prefer ``generated_at``."""
|
|
92
|
+
return str(self.frontmatter.get("timestamp", ""))
|
|
93
|
+
|
|
94
|
+
# ── Trust: generated (§5.2) ───────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def generated(self) -> dict[str, str]:
|
|
98
|
+
"""``generated: { by, at }`` block, or empty dict."""
|
|
99
|
+
g = self.frontmatter.get("generated", {})
|
|
100
|
+
return g if isinstance(g, dict) else {}
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def generated_by(self) -> str:
|
|
104
|
+
return self.generated.get("by", "")
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def generated_at(self) -> str:
|
|
108
|
+
"""ISO 8601 datetime string of last meaningful content change."""
|
|
109
|
+
return _to_iso8601_str(self.generated.get("at", self.timestamp))
|
|
110
|
+
|
|
111
|
+
# ── Trust: verified (§5.2) ────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def verified(self) -> list[dict[str, str]]:
|
|
115
|
+
"""Verification events as a list of ``{ by, at }`` dicts.
|
|
116
|
+
|
|
117
|
+
Per spec §5.2, a bare mapping is treated as a one-element list.
|
|
118
|
+
"""
|
|
119
|
+
v = self.frontmatter.get("verified")
|
|
120
|
+
if v is None:
|
|
121
|
+
return []
|
|
122
|
+
raw = [v] if isinstance(v, dict) else (v if isinstance(v, list) else [])
|
|
123
|
+
out = []
|
|
124
|
+
for e in raw:
|
|
125
|
+
if isinstance(e, dict):
|
|
126
|
+
item = dict(e)
|
|
127
|
+
if "at" in item:
|
|
128
|
+
item["at"] = _to_iso8601_str(item["at"])
|
|
129
|
+
out.append(item)
|
|
130
|
+
return out
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def trust_tier(self) -> str:
|
|
134
|
+
"""Derived trust tier per §5.3.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
``"human-reviewed"`` if any verifier uses ``human:`` prefix.
|
|
138
|
+
``"machine-confirmed"`` if verified by non-human actors only.
|
|
139
|
+
``"unverified"`` if ``verified`` is absent or empty.
|
|
140
|
+
"""
|
|
141
|
+
events = self.verified
|
|
142
|
+
if not events:
|
|
143
|
+
return "unverified"
|
|
144
|
+
if any(e.get("by", "").startswith("human:") for e in events):
|
|
145
|
+
return "human-reviewed"
|
|
146
|
+
return "machine-confirmed"
|
|
147
|
+
|
|
148
|
+
# ── Lifecycle: status (§5.4) ──────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def status(self) -> str:
|
|
152
|
+
"""``draft`` | ``stable`` | ``deprecated``. Defaults to ``stable``."""
|
|
153
|
+
return str(self.frontmatter.get("status", "stable"))
|
|
154
|
+
|
|
155
|
+
# ── Lifecycle: stale_after (§5.5) ─────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def stale_after(self) -> str:
|
|
159
|
+
return _to_iso8601_str(self.frontmatter.get("stale_after", ""))
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def is_stale(self) -> bool:
|
|
163
|
+
"""True when ``now >= stale_after`` (§5.5)."""
|
|
164
|
+
sa = self.stale_after
|
|
165
|
+
if not sa:
|
|
166
|
+
return False
|
|
167
|
+
try:
|
|
168
|
+
dt = datetime.fromisoformat(sa.replace("Z", "+00:00"))
|
|
169
|
+
return datetime.now(timezone.utc) >= dt
|
|
170
|
+
except (ValueError, TypeError):
|
|
171
|
+
return False
|
|
172
|
+
|
|
173
|
+
# ── Provenance: sources (§5.1) ────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def sources(self) -> list[dict[str, Any]]:
|
|
177
|
+
"""List of source provenance entries per §5.1."""
|
|
178
|
+
s = self.frontmatter.get("sources", [])
|
|
179
|
+
return s if isinstance(s, list) else []
|
|
180
|
+
|
|
181
|
+
# ── Attested Computation fields (§10) ─────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def runtime(self) -> str:
|
|
185
|
+
"""Computation runtime (e.g. ``bigquery``, ``python``). §10."""
|
|
186
|
+
return str(self.frontmatter.get("runtime", ""))
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def parameters(self) -> list[dict[str, Any]]:
|
|
190
|
+
"""Declared parameters for an Attested Computation. §10."""
|
|
191
|
+
p = self.frontmatter.get("parameters", [])
|
|
192
|
+
return p if isinstance(p, list) else []
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def executor(self) -> dict[str, Any]:
|
|
196
|
+
"""Executor block ``{ resource, receipt }``. §10."""
|
|
197
|
+
e = self.frontmatter.get("executor", {})
|
|
198
|
+
return e if isinstance(e, dict) else {}
|
|
199
|
+
|
|
200
|
+
@property
|
|
201
|
+
def attester(self) -> dict[str, Any]:
|
|
202
|
+
"""Attester block ``{ resource }``. §10."""
|
|
203
|
+
a = self.frontmatter.get("attester", {})
|
|
204
|
+
return a if isinstance(a, dict) else {}
|
|
205
|
+
|
|
206
|
+
# ── Graph links ───────────────────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
def cross_links(self) -> list[str]:
|
|
209
|
+
"""Extract concept IDs referenced via markdown links in the body."""
|
|
210
|
+
if self._cached_links is not None:
|
|
211
|
+
return self._cached_links
|
|
212
|
+
links: list[str] = []
|
|
213
|
+
for _, href in _CROSS_LINK_RE.findall(self.body):
|
|
214
|
+
if href.endswith(".md"):
|
|
215
|
+
href = href[:-3]
|
|
216
|
+
href = href.lstrip("./")
|
|
217
|
+
if href.startswith("/"):
|
|
218
|
+
href = href[1:]
|
|
219
|
+
if href:
|
|
220
|
+
links.append(href)
|
|
221
|
+
self._cached_links = links
|
|
222
|
+
return links
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# ── Parsing helpers ───────────────────────────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def parse_frontmatter(text: str) -> tuple[dict[str, Any], str]:
|
|
229
|
+
"""Split an OKF markdown file into frontmatter dict and body string."""
|
|
230
|
+
lines = text.splitlines()
|
|
231
|
+
if not lines or lines[0].strip() != _FRONTMATTER_DELIM:
|
|
232
|
+
return {}, text
|
|
233
|
+
|
|
234
|
+
end_idx = None
|
|
235
|
+
for i in range(1, len(lines)):
|
|
236
|
+
if lines[i].strip() == _FRONTMATTER_DELIM:
|
|
237
|
+
end_idx = i
|
|
238
|
+
break
|
|
239
|
+
if end_idx is None:
|
|
240
|
+
raise OKFParseError("Unterminated YAML frontmatter block")
|
|
241
|
+
|
|
242
|
+
fm_text = "\n".join(lines[1:end_idx])
|
|
243
|
+
try:
|
|
244
|
+
fm = yaml.safe_load(fm_text) or {}
|
|
245
|
+
except yaml.YAMLError as e:
|
|
246
|
+
raise OKFParseError(f"Invalid YAML in frontmatter: {e}") from e
|
|
247
|
+
if not isinstance(fm, dict):
|
|
248
|
+
raise OKFParseError("Frontmatter must be a YAML mapping")
|
|
249
|
+
|
|
250
|
+
body = "\n".join(lines[end_idx + 1 :])
|
|
251
|
+
if body.startswith("\n"):
|
|
252
|
+
body = body[1:]
|
|
253
|
+
return fm, body
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def is_reserved(path: Path) -> bool:
|
|
257
|
+
"""Check if a filename is an OKF reserved name (index.md, log.md)."""
|
|
258
|
+
return path.name in _RESERVED_FILENAMES
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def path_to_concept_id(bundle_root: Path, path: Path) -> str:
|
|
262
|
+
"""Convert a file path to a slash-separated concept ID."""
|
|
263
|
+
rel = path.relative_to(bundle_root).with_suffix("")
|
|
264
|
+
return "/".join(rel.parts)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def parse_bundle(bundle_root: Path) -> list[OKFConcept]:
|
|
268
|
+
"""Walk an OKF bundle directory and parse all concept documents.
|
|
269
|
+
|
|
270
|
+
Skips reserved files (``index.md``, ``log.md``) per the spec.
|
|
271
|
+
"""
|
|
272
|
+
root = Path(bundle_root)
|
|
273
|
+
concepts: list[OKFConcept] = []
|
|
274
|
+
|
|
275
|
+
for md_file in sorted(root.rglob("*.md")):
|
|
276
|
+
if is_reserved(md_file):
|
|
277
|
+
continue
|
|
278
|
+
text = md_file.read_text(encoding="utf-8")
|
|
279
|
+
fm, body = parse_frontmatter(text)
|
|
280
|
+
concept_id = path_to_concept_id(root, md_file)
|
|
281
|
+
concepts.append(
|
|
282
|
+
OKFConcept(
|
|
283
|
+
concept_id=concept_id,
|
|
284
|
+
frontmatter=fm,
|
|
285
|
+
body=body,
|
|
286
|
+
source_path=str(md_file),
|
|
287
|
+
)
|
|
288
|
+
)
|
|
289
|
+
return concepts
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""OKF Bundle Document Loader for LangChain — OKF v0.2.
|
|
2
|
+
|
|
3
|
+
Loads an OKF v0.2 knowledge bundle into LangChain ``Document`` objects,
|
|
4
|
+
mapping frontmatter fields to metadata and preserving concept identity.
|
|
5
|
+
|
|
6
|
+
v0.2 additions: ``generated``, ``verified``, ``sources``, ``status``,
|
|
7
|
+
``stale_after``, and ``trust_tier`` are mapped to Document metadata so
|
|
8
|
+
retrievers can filter on them.
|
|
9
|
+
|
|
10
|
+
See the `OKF spec <https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md>`_
|
|
11
|
+
for format details.
|
|
12
|
+
|
|
13
|
+
Example:
|
|
14
|
+
.. code-block:: python
|
|
15
|
+
|
|
16
|
+
from langchain_okf import OKFBundleLoader
|
|
17
|
+
|
|
18
|
+
loader = OKFBundleLoader("path/to/bundle/")
|
|
19
|
+
docs = loader.load()
|
|
20
|
+
|
|
21
|
+
# Each doc has OKF v0.2 frontmatter as metadata
|
|
22
|
+
docs[0].metadata["type"] # "BigQuery Table"
|
|
23
|
+
docs[0].metadata["concept_id"] # "tables/orders"
|
|
24
|
+
docs[0].metadata["tags"] # ["sales", "orders"]
|
|
25
|
+
docs[0].metadata["trust_tier"] # "human-reviewed"
|
|
26
|
+
docs[0].metadata["status"] # "stable"
|
|
27
|
+
docs[0].metadata["generated_at"] # "2026-06-20T22:53:05Z"
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import json
|
|
33
|
+
from collections.abc import Iterator
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
|
|
36
|
+
from langchain_core.document_loaders import BaseLoader
|
|
37
|
+
from langchain_core.documents import Document
|
|
38
|
+
|
|
39
|
+
from langchain_okf._parser import OKFConcept, parse_bundle
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class OKFBundleLoader(BaseLoader):
|
|
43
|
+
"""Load an OKF v0.2 knowledge bundle as LangChain Documents.
|
|
44
|
+
|
|
45
|
+
Each concept document in the bundle becomes one ``Document``, with
|
|
46
|
+
YAML frontmatter fields mapped to ``Document.metadata``. The
|
|
47
|
+
concept's markdown body becomes ``page_content``.
|
|
48
|
+
|
|
49
|
+
v0.2 metadata fields included in every document:
|
|
50
|
+
- ``trust_tier``: ``"human-reviewed"`` | ``"machine-confirmed"`` | ``"unverified"``
|
|
51
|
+
- ``status``: ``"draft"`` | ``"stable"`` | ``"deprecated"``
|
|
52
|
+
- ``stale_after``: ISO 8601 datetime (if set)
|
|
53
|
+
- ``generated_at`` / ``generated_by``: authorship info
|
|
54
|
+
- ``sources``: list of provenance entries
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
bundle_path: Path to the OKF bundle root directory.
|
|
58
|
+
include_types: If set, only load concepts matching these types.
|
|
59
|
+
exclude_types: If set, skip concepts matching these types.
|
|
60
|
+
include_tags: If set, only load concepts having at least one
|
|
61
|
+
of these tags.
|
|
62
|
+
include_status: If set, only load concepts with this status.
|
|
63
|
+
Common values: ``"stable"``, ``"draft"``, ``"deprecated"``.
|
|
64
|
+
exclude_stale: If True (default), exclude concepts whose
|
|
65
|
+
``stale_after`` has passed.
|
|
66
|
+
|
|
67
|
+
Example:
|
|
68
|
+
.. code-block:: python
|
|
69
|
+
|
|
70
|
+
from langchain_okf import OKFBundleLoader
|
|
71
|
+
|
|
72
|
+
loader = OKFBundleLoader(
|
|
73
|
+
"./bundles/ga4/",
|
|
74
|
+
include_status="stable",
|
|
75
|
+
exclude_stale=True,
|
|
76
|
+
)
|
|
77
|
+
docs = loader.load()
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
bundle_path: str | Path,
|
|
83
|
+
*,
|
|
84
|
+
include_types: list[str] | None = None,
|
|
85
|
+
exclude_types: list[str] | None = None,
|
|
86
|
+
include_tags: list[str] | None = None,
|
|
87
|
+
include_status: str | None = None,
|
|
88
|
+
exclude_stale: bool = True,
|
|
89
|
+
) -> None:
|
|
90
|
+
self.bundle_path = Path(bundle_path)
|
|
91
|
+
self.include_types = set(include_types) if include_types else None
|
|
92
|
+
self.exclude_types = set(exclude_types) if exclude_types else None
|
|
93
|
+
self.include_tags = set(include_tags) if include_tags else None
|
|
94
|
+
self.include_status = include_status
|
|
95
|
+
self.exclude_stale = exclude_stale
|
|
96
|
+
|
|
97
|
+
def _should_include(self, concept: OKFConcept) -> bool:
|
|
98
|
+
if self.include_types and concept.type not in self.include_types:
|
|
99
|
+
return False
|
|
100
|
+
if self.exclude_types and concept.type in self.exclude_types:
|
|
101
|
+
return False
|
|
102
|
+
if self.include_tags and not (set(concept.tags) & self.include_tags):
|
|
103
|
+
return False
|
|
104
|
+
if self.include_status and concept.status != self.include_status:
|
|
105
|
+
return False
|
|
106
|
+
return not (self.exclude_stale and concept.is_stale)
|
|
107
|
+
|
|
108
|
+
def _concept_to_document(self, concept: OKFConcept) -> Document:
|
|
109
|
+
metadata: dict = {
|
|
110
|
+
"concept_id": concept.concept_id,
|
|
111
|
+
"source": concept.source_path,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# ── Core fields ───────────────────────────────────────────────────────
|
|
115
|
+
if concept.type:
|
|
116
|
+
metadata["type"] = concept.type
|
|
117
|
+
if concept.title:
|
|
118
|
+
metadata["title"] = concept.title
|
|
119
|
+
if concept.description:
|
|
120
|
+
metadata["description"] = concept.description
|
|
121
|
+
if concept.resource:
|
|
122
|
+
metadata["resource"] = concept.resource
|
|
123
|
+
if concept.tags:
|
|
124
|
+
metadata["tags"] = concept.tags
|
|
125
|
+
|
|
126
|
+
# ── v0.2 trust ────────────────────────────────────────────────────────
|
|
127
|
+
metadata["trust_tier"] = concept.trust_tier
|
|
128
|
+
metadata["status"] = concept.status
|
|
129
|
+
if concept.stale_after:
|
|
130
|
+
metadata["stale_after"] = concept.stale_after
|
|
131
|
+
if concept.generated_at:
|
|
132
|
+
metadata["generated_at"] = concept.generated_at
|
|
133
|
+
if concept.generated_by:
|
|
134
|
+
metadata["generated_by"] = concept.generated_by
|
|
135
|
+
|
|
136
|
+
# ── v0.2 provenance ───────────────────────────────────────────────────
|
|
137
|
+
if concept.sources:
|
|
138
|
+
# Store as JSON string so vector stores (e.g. Chroma) accept it
|
|
139
|
+
metadata["sources"] = json.dumps(concept.sources, default=str)
|
|
140
|
+
first_resource = concept.sources[0].get("resource", "")
|
|
141
|
+
if first_resource:
|
|
142
|
+
metadata["primary_source"] = first_resource
|
|
143
|
+
|
|
144
|
+
# ── v0.1 compat ───────────────────────────────────────────────────────
|
|
145
|
+
if concept.timestamp:
|
|
146
|
+
metadata["timestamp"] = concept.timestamp
|
|
147
|
+
|
|
148
|
+
# ── Cross-links ───────────────────────────────────────────────────────
|
|
149
|
+
cross_links = concept.cross_links()
|
|
150
|
+
if cross_links:
|
|
151
|
+
metadata["cross_links"] = cross_links
|
|
152
|
+
|
|
153
|
+
# ── Attested Computation ──────────────────────────────────────────────
|
|
154
|
+
if concept.runtime:
|
|
155
|
+
metadata["runtime"] = concept.runtime
|
|
156
|
+
|
|
157
|
+
# ── Remaining frontmatter (extension fields) ──────────────────────────
|
|
158
|
+
skip = {
|
|
159
|
+
"type",
|
|
160
|
+
"title",
|
|
161
|
+
"description",
|
|
162
|
+
"resource",
|
|
163
|
+
"tags",
|
|
164
|
+
"generated",
|
|
165
|
+
"verified",
|
|
166
|
+
"sources",
|
|
167
|
+
"status",
|
|
168
|
+
"stale_after",
|
|
169
|
+
"timestamp",
|
|
170
|
+
"runtime",
|
|
171
|
+
"parameters",
|
|
172
|
+
"executor",
|
|
173
|
+
"attester",
|
|
174
|
+
}
|
|
175
|
+
for key, value in concept.frontmatter.items():
|
|
176
|
+
if key not in skip and key not in metadata:
|
|
177
|
+
metadata[key] = value
|
|
178
|
+
|
|
179
|
+
return Document(page_content=concept.body, metadata=metadata)
|
|
180
|
+
|
|
181
|
+
def lazy_load(self) -> Iterator[Document]:
|
|
182
|
+
"""Lazily load OKF concepts as Documents.
|
|
183
|
+
|
|
184
|
+
Yields:
|
|
185
|
+
One ``Document`` per concept, with frontmatter as metadata.
|
|
186
|
+
"""
|
|
187
|
+
for concept in parse_bundle(self.bundle_path):
|
|
188
|
+
if self._should_include(concept):
|
|
189
|
+
yield self._concept_to_document(concept)
|