moca-reader-langchain 0.6.0__tar.gz

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.
@@ -0,0 +1,8 @@
1
+ .venv/
2
+ __pycache__/
3
+ .pytest_cache/
4
+ .mypy_cache/
5
+ .ruff_cache/
6
+ dist/
7
+ .DS_Store
8
+ .claude/
@@ -0,0 +1,67 @@
1
+ Metadata-Version: 2.5
2
+ Name: moca-reader-langchain
3
+ Version: 0.6.0
4
+ Summary: A LangChain retriever over MOCA packages: citation records as Documents, through the MOCA Reader's gate.
5
+ Project-URL: Homepage, https://openmoca.org
6
+ Project-URL: Source, https://github.com/openmoca/moca-reader-python
7
+ Author: MOCA Standard Contributors
8
+ License-Expression: Apache-2.0
9
+ Keywords: citations,langchain,moca,rag,retriever
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Typing :: Typed
13
+ Requires-Python: >=3.11
14
+ Requires-Dist: langchain-core>=1.0
15
+ Requires-Dist: moca-reader==0.6.0
16
+ Description-Content-Type: text/markdown
17
+
18
+ # moca-reader-langchain
19
+
20
+ A [LangChain](https://python.langchain.com) retriever over MOCA packages,
21
+ built on the [MOCA Reader for Python](https://github.com/openmoca/moca-reader-python).
22
+
23
+ ```sh
24
+ pip install moca-reader-langchain
25
+ ```
26
+
27
+ ```python
28
+ from moca_reader import LexicalBackend, Library, Search, read_package
29
+ from moca_reader_langchain import MocaRetriever
30
+
31
+ pkg = read_package("support-kb.moca", trust_root="trust-root.json")
32
+ library = Library().add(pkg)
33
+ search = Search(library, LexicalBackend(library), audiences=["public"])
34
+
35
+ retriever = MocaRetriever(search=search, limit=3)
36
+ for doc in retriever.invoke("How long is the refund window?"):
37
+ print(doc.metadata["moca_node_ref"], doc.metadata["moca_score"])
38
+ print(doc.page_content[:80])
39
+ ```
40
+
41
+ `MocaRetriever` wraps the `Search` you build: its backend (lexical, dense,
42
+ hybrid or your own store), its audiences and its hooks. Every call goes through
43
+ the Reader's gate, so content that is out of force, superseded, deprecated or
44
+ outside your audiences never comes back, and nothing a caller passes can widen
45
+ that. `limit`, `locale`, `concepts`, `scope` and `include_all` are set when you
46
+ build the retriever.
47
+
48
+ Each result is a LangChain `Document`:
49
+
50
+ | Field | Holds |
51
+ | --- | --- |
52
+ | `page_content` | The text of the node, or of a sidecar chunk |
53
+ | `id` | The node's versioned reference and byte span |
54
+ | `metadata["moca_citation"]` | The whole citation record: package, version and digest, trust signals, evidence |
55
+ | `metadata["moca_*"]` | The flat fields of the consuming guide (`moca_node_ref`, `moca_package_digest`, `moca_status`, `moca_signed`, ...), for vector stores that need flat metadata |
56
+ | `metadata["moca_score"]` | The search score |
57
+
58
+ **Package text is untrusted input.** Put `page_content` in a prompt as quoted
59
+ reference material to cite, never as instructions
60
+ ([Reader contract §12](https://github.com/openmoca/moca-spec/blob/main/spec/moca-reader-contract.md#12-handing-content-to-a-model)).
61
+ Show users the source (`moca_node_ref`, the package version) with each answer.
62
+
63
+ The async path (`ainvoke`) runs the same search.
64
+
65
+ ## Licence
66
+
67
+ Apache-2.0.
@@ -0,0 +1,50 @@
1
+ # moca-reader-langchain
2
+
3
+ A [LangChain](https://python.langchain.com) retriever over MOCA packages,
4
+ built on the [MOCA Reader for Python](https://github.com/openmoca/moca-reader-python).
5
+
6
+ ```sh
7
+ pip install moca-reader-langchain
8
+ ```
9
+
10
+ ```python
11
+ from moca_reader import LexicalBackend, Library, Search, read_package
12
+ from moca_reader_langchain import MocaRetriever
13
+
14
+ pkg = read_package("support-kb.moca", trust_root="trust-root.json")
15
+ library = Library().add(pkg)
16
+ search = Search(library, LexicalBackend(library), audiences=["public"])
17
+
18
+ retriever = MocaRetriever(search=search, limit=3)
19
+ for doc in retriever.invoke("How long is the refund window?"):
20
+ print(doc.metadata["moca_node_ref"], doc.metadata["moca_score"])
21
+ print(doc.page_content[:80])
22
+ ```
23
+
24
+ `MocaRetriever` wraps the `Search` you build: its backend (lexical, dense,
25
+ hybrid or your own store), its audiences and its hooks. Every call goes through
26
+ the Reader's gate, so content that is out of force, superseded, deprecated or
27
+ outside your audiences never comes back, and nothing a caller passes can widen
28
+ that. `limit`, `locale`, `concepts`, `scope` and `include_all` are set when you
29
+ build the retriever.
30
+
31
+ Each result is a LangChain `Document`:
32
+
33
+ | Field | Holds |
34
+ | --- | --- |
35
+ | `page_content` | The text of the node, or of a sidecar chunk |
36
+ | `id` | The node's versioned reference and byte span |
37
+ | `metadata["moca_citation"]` | The whole citation record: package, version and digest, trust signals, evidence |
38
+ | `metadata["moca_*"]` | The flat fields of the consuming guide (`moca_node_ref`, `moca_package_digest`, `moca_status`, `moca_signed`, ...), for vector stores that need flat metadata |
39
+ | `metadata["moca_score"]` | The search score |
40
+
41
+ **Package text is untrusted input.** Put `page_content` in a prompt as quoted
42
+ reference material to cite, never as instructions
43
+ ([Reader contract §12](https://github.com/openmoca/moca-spec/blob/main/spec/moca-reader-contract.md#12-handing-content-to-a-model)).
44
+ Show users the source (`moca_node_ref`, the package version) with each answer.
45
+
46
+ The async path (`ainvoke`) runs the same search.
47
+
48
+ ## Licence
49
+
50
+ Apache-2.0.
@@ -0,0 +1,25 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "moca-reader-langchain"
7
+ version = "0.6.0"
8
+ description = "A LangChain retriever over MOCA packages: citation records as Documents, through the MOCA Reader's gate."
9
+ readme = "README.md"
10
+ license = "Apache-2.0"
11
+ authors = [{ name = "MOCA Standard Contributors" }]
12
+ requires-python = ">=3.11"
13
+ dependencies = ["moca-reader==0.6.0", "langchain-core>=1.0"]
14
+ keywords = ["moca", "langchain", "retriever", "rag", "citations"]
15
+ classifiers = ["Development Status :: 4 - Beta", "Programming Language :: Python :: 3", "Typing :: Typed"]
16
+
17
+ [project.urls]
18
+ Homepage = "https://openmoca.org"
19
+ Source = "https://github.com/openmoca/moca-reader-python"
20
+
21
+ [tool.uv.sources]
22
+ moca-reader = { workspace = true }
23
+
24
+ [tool.hatch.build.targets.wheel]
25
+ packages = ["src/moca_reader_langchain"]
@@ -0,0 +1,67 @@
1
+ """A LangChain retriever over MOCA packages (Reader interface §7).
2
+
3
+ ``MocaRetriever`` wraps a host-built ``moca_reader.Search``: its backend, audiences and hooks. Every call goes
4
+ through the Reader's gate, so the retrieval policy and the host's audience filter always apply, and nothing a
5
+ caller passes can widen them. Each citation record becomes a ``Document``: the text as ``page_content``, the
6
+ whole record under ``metadata["moca_citation"]``, and the consuming guide's flat ``moca_*`` fields beside it.
7
+
8
+ Package text is untrusted input to a model: pass ``page_content`` to a model as quoted reference material to cite,
9
+ never as instructions (Reader contract §12).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import Any
15
+
16
+ from langchain_core.callbacks import CallbackManagerForRetrieverRun
17
+ from langchain_core.documents import Document
18
+ from langchain_core.retrievers import BaseRetriever
19
+ from pydantic import ConfigDict
20
+
21
+ from moca_reader import CitationRecord, Search, flat_metadata
22
+
23
+ __all__ = ["MocaRetriever", "document_id", "to_document"]
24
+ __version__ = "0.6.0"
25
+
26
+
27
+ def document_id(record: CitationRecord) -> str:
28
+ """A unique id: the node's versioned reference, then the byte span, since one node can return several chunks."""
29
+ span = record.get("span")
30
+ ref = str(record["node"]["ref"])
31
+ return f"{ref}:{span['start']}-{span['end']}" if isinstance(span, dict) else ref
32
+
33
+
34
+ def to_document(record: CitationRecord) -> Document:
35
+ """A citation record as a LangChain ``Document``."""
36
+ metadata: dict[str, Any] = {"moca_citation": record, **flat_metadata(record)}
37
+ if isinstance(record.get("score"), (int, float)):
38
+ metadata["moca_score"] = record["score"]
39
+ return Document(page_content=record.get("text") or "", id=document_id(record), metadata=metadata)
40
+
41
+
42
+ class MocaRetriever(BaseRetriever):
43
+ """Retrieves citation records from MOCA packages as LangChain ``Document``s.
44
+
45
+ ``search`` is the host's ``moca_reader.Search``. The other fields are the search options for every call; they
46
+ are the host's, set when the retriever is built.
47
+ """
48
+
49
+ model_config = ConfigDict(arbitrary_types_allowed=True)
50
+
51
+ search: Search
52
+ limit: int = 5
53
+ locale: str | None = None
54
+ concepts: list[str] | None = None
55
+ scope: str | None = None
56
+ include_all: bool = False
57
+
58
+ def _get_relevant_documents(self, query: str, *, run_manager: CallbackManagerForRetrieverRun) -> list[Document]:
59
+ records = self.search.search(
60
+ query,
61
+ limit=self.limit,
62
+ include_all=self.include_all,
63
+ locale=self.locale,
64
+ concepts=self.concepts,
65
+ scope=self.scope,
66
+ )
67
+ return [to_document(r) for r in records]
@@ -0,0 +1,31 @@
1
+ """Fixtures over moca-spec's support-kb: ``MOCA_SPEC_DIR``, else ``moca-spec`` beside this repository."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+
10
+ from moca_reader import LexicalBackend, Library, Search, read_package
11
+
12
+ REPO = Path(__file__).resolve().parents[3]
13
+ MOCA_SPEC = Path(os.environ.get("MOCA_SPEC_DIR") or REPO.parent / "moca-spec").resolve()
14
+
15
+
16
+ @pytest.fixture
17
+ def moca_spec() -> Path:
18
+ return MOCA_SPEC
19
+
20
+
21
+ @pytest.fixture
22
+ def library() -> Library:
23
+ pkg = read_package(
24
+ MOCA_SPEC / "examples" / "support-kb", trust_root=MOCA_SPEC / "fixtures" / "signing-keys" / "trust-root.json"
25
+ )
26
+ return Library().add(pkg)
27
+
28
+
29
+ @pytest.fixture
30
+ def public_search(library: Library) -> Search:
31
+ return Search(library, LexicalBackend(library), audiences=["public"])
@@ -0,0 +1,65 @@
1
+ """MocaRetriever returns citation records as LangChain Documents, through the Reader's gate."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ from moca_reader_langchain import MocaRetriever
9
+
10
+ from moca_reader import FLAT_FIELDS, Library, MemoryStoreBackend, Search, StoreScope, package_state
11
+ from moca_reader.schemas import validate_against
12
+
13
+ REFUND = "https://example.com/moca/support-kb@4.2.0#refund-window.md"
14
+
15
+
16
+ def test_documents_carry_text_id_score_and_the_whole_record(public_search: Search) -> None:
17
+ docs = MocaRetriever(search=public_search, limit=3).invoke("How long is the refund window?")
18
+ first = docs[0]
19
+ assert first.metadata["moca_node_ref"] == REFUND
20
+ assert first.id is not None and first.id.startswith(REFUND)
21
+ assert "30 days of delivery" in first.page_content
22
+ assert isinstance(first.metadata["moca_score"], float)
23
+ record = first.metadata["moca_citation"]
24
+ assert validate_against("citationRecord", json.loads(json.dumps(record))) == []
25
+ assert set(first.metadata) - {"moca_citation", "moca_score"} <= set(FLAT_FIELDS)
26
+ assert (first.metadata["moca_signed"], first.metadata["moca_attested_reviews"]) == (True, 1)
27
+
28
+
29
+ def test_the_hosts_audience_filter_holds_whatever_is_asked(public_search: Search) -> None:
30
+ docs = MocaRetriever(search=public_search, limit=20, include_all=True).invoke("delete my account")
31
+ assert all(d.metadata.get("moca_audience") != "internal" for d in docs)
32
+
33
+
34
+ def test_a_store_backend_works_through_the_same_retriever(library: Library) -> None:
35
+ store = MemoryStoreBackend().ingest(library)
36
+ search = Search(StoreScope([package_state(p.result) for p in library.packages], library.clock), store)
37
+ docs = MocaRetriever(search=search, limit=1).invoke("domestic express shipping")
38
+ assert docs[0].metadata["moca_node_ref"].endswith("#shipping-estimates.md")
39
+
40
+
41
+ def test_async_retrieval_returns_the_same(public_search: Search) -> None:
42
+ import asyncio
43
+
44
+ retriever = MocaRetriever(search=public_search, limit=2)
45
+ sync = [d.id for d in retriever.invoke("refund window")]
46
+ asynchronous = [d.id for d in asyncio.run(retriever.ainvoke("refund window"))]
47
+ assert sync == asynchronous
48
+
49
+
50
+ def test_the_readme_example_runs(moca_spec: Path, monkeypatch: object) -> None:
51
+ readme = (Path(__file__).resolve().parents[1] / "README.md").read_text("utf-8")
52
+ code = readme.split("```python\n", 1)[1].split("```", 1)[0]
53
+ import os
54
+
55
+ os.chdir(moca_spec / "examples")
56
+ exec(
57
+ compile(
58
+ code.replace('"support-kb.moca"', '"support-kb"').replace(
59
+ '"trust-root.json"', '"../fixtures/signing-keys/trust-root.json"'
60
+ ),
61
+ "README",
62
+ "exec",
63
+ ),
64
+ {},
65
+ )