diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5c1ec4d8..ffbaa7be 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Added
+
+- **First-class LangChain integration** (closes #963; recreates #969)
+ - New `pip install semantica[langchain]` extra (`langchain-core>=0.3.0`), included in the `all` bundle
+ - `integrations/langchain/SemanticaRetriever` — LangChain `BaseRetriever` that seeds from `HybridSearch` then walks graph edges (`hops=2` default) for GraphRAG-style retrieval; falls back to `ContextGraph.query` when hybrid search is unavailable
+ - `integrations/langchain/SemanticaVectorStore` — LangChain `VectorStore` adapter over `HybridSearch` (`add_texts`, `similarity_search`, `similarity_search_with_score`, `from_texts`)
+ - `integrations/langchain/SemanticaKGTool` / `SemanticaDecisionTool` — `BaseTool` subclasses with Pydantic `args_schema` (`semantica_query_graph`, `semantica_query_decisions`); `build()` returns the tool, or `None` when langchain-core is absent
+ - Retriever and VectorStore read HybridSearch nested `metadata` (`content`, `node_id`, `node_type`) rather than top-level fields that HybridSearch does not set
+ - All adapters remain importable without langchain-core (`LANGCHAIN_AVAILABLE` flag)
+ - Docs: `docs/integrations/langchain.md`, README native-integration matrix, and `docs.json` nav entry
+
## [0.6.6] - 2026-08-20
### Added
diff --git a/README.md b/README.md
index c9ea191b..85833b2c 100644
--- a/README.md
+++ b/README.md
@@ -87,7 +87,7 @@ Semantica sits underneath your LLM, vector store, and agent framework as a deter
- **Graph Analytics:** Centrality, community detection, link prediction, and shortest-path queries over the graph you just built
- **Polyglot Graph Storage:** Native RDF (embedded Oxigraph, Blazegraph, Apache Jena, Eclipse RDF4J via SPARQL) and Labeled Property Graphs (Neo4j, FalkorDB, Apache AGE, AWS Neptune via Cypher), plus vector stores, all swappable without touching your code
- **Visualization:** Explore any graph, ontology, or timeline in an interactive browser workbench
-- **Drop-in Integrations:** Native Agno and CrewAI support, a full-featured MCP server, a comprehensive CLI, a REST API, and plugins across major editors
+- **Drop-in Integrations:** Native Agno, CrewAI, and LangChain support, a full-featured MCP server, a comprehensive CLI, a REST API, and plugins across major editors
---
@@ -1188,7 +1188,7 @@ Start with `semantica`, verify with `doctor`, build a graph, and explore the com
## Integrations
-Native plugin bundles for Claude Code, Cursor, Codex, Windsurf, Cline, Continue, VS Code, and OpenClaw; a full-featured MCP server for any MCP-compatible client; a comprehensive REST API; and first-class Agno and CrewAI support for agentic frameworks. Every major LLM provider is already supported via `semantica.llms` and LiteLLM: OpenAI, Anthropic, Gemini, Mistral, Llama, Groq, Cohere, Azure, Bedrock, Ollama, DeepSeek, HuggingFace, and more.
+Native plugin bundles for Claude Code, Cursor, Codex, Windsurf, Cline, Continue, VS Code, and OpenClaw; a full-featured MCP server for any MCP-compatible client; a comprehensive REST API; and first-class Agno, CrewAI, and LangChain support for agentic frameworks. Every major LLM provider is already supported via `semantica.llms` and LiteLLM: OpenAI, Anthropic, Gemini, Mistral, Llama, Groq, Cohere, Azure, Bedrock, Ollama, DeepSeek, HuggingFace, and more.
MCP setup takes 30 seconds — see [MCP Server](#mcp-server) below.
@@ -1307,17 +1307,17 @@ MCP setup takes 30 seconds — see [MCP Server](#mcp-server) below.
CrewAI
First-class · pip install semantica[crewai]
+
-
-LangChain
-Dedicated toolkit
- |
-

LlamaIndex
Dedicated toolkit
@@ -1511,6 +1506,7 @@ pip install semantica[all] # everything
```bash
pip install semantica[agno] # Agno multi-agent integration
pip install semantica[crewai] # CrewAI integration
+pip install semantica[langchain] # LangChain / LangGraph integration
pip install semantica[llm-litellm] # OpenAI, Anthropic, Gemini, Mistral, Llama, Groq, Cohere, Bedrock, Ollama, DeepSeek, and more
pip install semantica[graph-neo4j] # Neo4j graph store (LPG)
pip install semantica[graph-falkordb] # FalkorDB graph store (LPG)
diff --git a/docs/docs.json b/docs/docs.json
index d2ad5da2..d5713f4d 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -103,6 +103,7 @@
"pages": [
"integrations/agno",
"integrations/crewai",
+ "integrations/langchain",
"integrations/docling",
"integrations/snowflake",
"integrations/databricks"
diff --git a/docs/integrations/langchain.md b/docs/integrations/langchain.md
new file mode 100644
index 00000000..e10fdc0b
--- /dev/null
+++ b/docs/integrations/langchain.md
@@ -0,0 +1,81 @@
+---
+title: "LangChain Integration"
+description: "Drop Semantica into LangChain / LangGraph pipelines via a GraphRAG retriever, VectorStore adapter, and agent tools."
+icon: "link"
+---
+
+> Three drop-in adapters that bring Semantica's context graph and hybrid search into LangChain chains and LangGraph agents.
+
+## Installation
+
+```bash
+pip install "semantica[langchain]"
+```
+
+Requires `langchain-core >= 0.3`. If langchain-core is not installed, the integration still imports — every class carries the full Semantica API and degrades gracefully (`build()` returns `None`; branch on `LANGCHAIN_AVAILABLE`).
+
+## Components at a Glance
+
+- **SemanticaRetriever** — `BaseRetriever`: hybrid-search seeds retrieval, then graph edges are walked `hops` steps (default 2) for GraphRAG-style results.
+- **SemanticaVectorStore** — `VectorStore`: `add_texts` / `similarity_search` / `similarity_search_with_score` / `from_texts` over `HybridSearch`.
+- **SemanticaKGTool** / **SemanticaDecisionTool** — `BaseTool` subclasses: `semantica_query_graph` and `semantica_query_decisions` for LangGraph / tool-calling agents.
+
+## Component Details
+
+
+
+ Hybrid search seeds retrieval; then graph edges are walked `hops` steps so results go beyond flat vector similarity. If hybrid search is omitted or fails, the retriever falls back to a `ContextGraph.query` keyword scan.
+
+ ```python
+ from integrations.langchain import SemanticaRetriever
+ from semantica.context import ContextGraph
+ from semantica.vector_store import HybridSearch
+
+ graph = ContextGraph()
+ hybrid = HybridSearch()
+
+ retriever = SemanticaRetriever(graph=graph, hybrid=hybrid, hops=2, top_k=10)
+
+ from langchain.chains import RetrievalQA
+
+ qa = RetrievalQA.from_chain_type(llm=llm, retriever=retriever)
+ ```
+
+
+ Drop-in `VectorStore` for RetrievalQA / LCEL chains. `from_texts` requires a pre-configured `hybrid` instance.
+
+ ```python
+ from integrations.langchain import SemanticaVectorStore
+
+ store = SemanticaVectorStore(hybrid=hybrid)
+ store.add_texts(
+ ["document one", "document two"],
+ metadatas=[{"source": "a"}, {"source": "b"}],
+ )
+ docs = store.similarity_search("document", k=2)
+ docs, scores = store.similarity_search_with_score("document", k=2)
+ ```
+
+ `add_texts` delegates to a Semantica vector store with `add_documents` (pass `vector_store=` to `HybridSearch` or to `SemanticaVectorStore`).
+
+
+ Instances are LangChain `BaseTool`s and can be passed to an agent directly.
+ `.build()` returns the tool, or `None` when langchain-core is absent.
+
+ ```python
+ from integrations.langchain import SemanticaKGTool, SemanticaDecisionTool
+ from langgraph.prebuilt import create_react_agent
+
+ tools = [
+ SemanticaKGTool(graph),
+ SemanticaDecisionTool(graph),
+ ]
+ agent = create_react_agent(model, tools)
+ ```
+
+ | Tool | Description |
+ | :------ | :------------- |
+ | `semantica_query_graph` | Keyword / NL query over the shared context graph |
+ | `semantica_query_decisions` | Search the recorded decision log |
+
+
diff --git a/integrations/__init__.py b/integrations/__init__.py
index 06219ff5..080383fc 100644
--- a/integrations/__init__.py
+++ b/integrations/__init__.py
@@ -1,7 +1,7 @@
"""
Semantica Framework Integrations
-Optional integration packages for agentic frameworks (Google ADK, Claude Agent SDK, Agno, etc.).
+Optional integration packages for agentic frameworks (Google ADK, Claude Agent SDK, Agno, CrewAI, LangChain, etc.).
Each integration is self-contained, independently installable via extras_require, and maintains
zero impact on core Semantica - keeping the semantic layer lean while maximizing ecosystem reach.
"""
diff --git a/integrations/langchain/README.md b/integrations/langchain/README.md
new file mode 100644
index 00000000..d34b002b
--- /dev/null
+++ b/integrations/langchain/README.md
@@ -0,0 +1,67 @@
+# Semantica × LangChain
+
+Drop Semantica into existing LangChain / LangGraph pipelines: GraphRAG-style
+retrieval, a `VectorStore` adapter, and agent tools.
+
+## Install
+
+```bash
+pip install semantica[langchain]
+# or just the core adapter dependency:
+pip install langchain-core
+```
+
+## Retriever (GraphRAG)
+
+```python
+from integrations.langchain import SemanticaRetriever
+from semantica.context import ContextGraph
+from semantica.vector_store import HybridSearch
+
+graph = ContextGraph()
+hybrid = HybridSearch()
+
+retriever = SemanticaRetriever(graph=graph, hybrid=hybrid, hops=2, top_k=10)
+
+# Use with any LangChain chain that accepts a retriever:
+from langchain.chains import RetrievalQA
+
+qa = RetrievalQA.from_chain_type(llm=llm, retriever=retriever)
+```
+
+Hybrid search seeds retrieval; then graph edges are walked `hops` steps so
+results go beyond flat vector similarity.
+
+## VectorStore
+
+```python
+from integrations.langchain import SemanticaVectorStore
+
+store = SemanticaVectorStore(hybrid=hybrid)
+store.add_texts(["document one", "document two"], metadatas=[{"source": "a"}, {"source": "b"}])
+docs = store.similarity_search("document", k=2)
+docs, scores = store.similarity_search_with_score("document", k=2)
+```
+
+## Agent tools (LangGraph / tool-calling agents)
+
+```python
+from integrations.langchain import SemanticaKGTool, SemanticaDecisionTool
+from langgraph.prebuilt import create_react_agent
+
+tools = [
+ SemanticaKGTool(graph),
+ SemanticaDecisionTool(graph),
+]
+agent = create_react_agent(model, tools)
+```
+
+- `semantica_query_graph` — query the shared context graph (keyword / NL)
+- `semantica_query_decisions` — search the recorded decision log
+
+## Compatibility
+
+- Requires `langchain-core >= 0.3`.
+- All classes degrade gracefully when `langchain-core` is absent: they remain
+ importable (carrying the full Semantica API), and `build()` returns `None`,
+ so agents can branch on `LANGCHAIN_AVAILABLE`.
diff --git a/integrations/langchain/__init__.py b/integrations/langchain/__init__.py
new file mode 100644
index 00000000..875b5f66
--- /dev/null
+++ b/integrations/langchain/__init__.py
@@ -0,0 +1,48 @@
+"""
+Semantica × LangChain Integration
+=================================
+
+First-class integration between the Semantica semantic intelligence stack and
+the `LangChain `_ / LangGraph
+ecosystem.
+
+Public surface
+--------------
+SemanticaRetriever — ``BaseRetriever`` with multi-hop GraphRAG (walks graph
+ edges from hybrid-search hits)
+SemanticaVectorStore — ``VectorStore`` adapter over Semantica's hybrid search
+ (drop-in for RetrievalQA / LCEL chains)
+SemanticaKGTool — ``BaseTool`` for querying the context graph
+SemanticaDecisionTool — ``BaseTool`` exposing the recorded decision log
+
+Quick start
+-----------
+ pip install semantica[langchain]
+
+ >>> from integrations.langchain import (
+ ... SemanticaRetriever,
+ ... SemanticaVectorStore,
+ ... SemanticaKGTool,
+ ... SemanticaDecisionTool,
+ ... )
+
+Compatibility
+-------------
+Requires ``langchain-core >= 0.3``. All classes degrade gracefully when
+``langchain-core`` is not installed — they are still importable and carry the
+full Semantica API, but cannot be bound to LangChain chains/agents.
+"""
+
+from .retriever import LANGCHAIN_AVAILABLE, SemanticaRetriever
+from .tools import SemanticaDecisionTool, SemanticaKGTool
+from .vectorstore import SemanticaVectorStore
+
+__all__ = [
+ "SemanticaRetriever",
+ "SemanticaVectorStore",
+ "SemanticaKGTool",
+ "SemanticaDecisionTool",
+ "LANGCHAIN_AVAILABLE",
+]
+
+__version__ = "0.1.0"
diff --git a/integrations/langchain/retriever.py b/integrations/langchain/retriever.py
new file mode 100644
index 00000000..b2489725
--- /dev/null
+++ b/integrations/langchain/retriever.py
@@ -0,0 +1,216 @@
+"""
+SemanticaRetriever — LangChain ``BaseRetriever`` with multi-hop GraphRAG.
+
+Hybrid search seeds the retrieval, then graph edges are walked for ``hops``
+steps so results go beyond flat vector similarity.
+"""
+
+from __future__ import annotations
+
+from typing import Any, Dict, List, Optional, Tuple
+
+from semantica.utils.logging import get_logger
+
+logger = get_logger(__name__)
+
+# ---------------------------------------------------------------------------
+# Optional: LangChain core
+# ---------------------------------------------------------------------------
+LANGCHAIN_AVAILABLE = False
+LANGCHAIN_IMPORT_ERROR: Optional[str] = None
+
+_BaseRetriever: Any = object
+_Document: Any = None
+
+
+def _get_document(**kwargs: Any) -> Any:
+ """Instantiate a langchain Document lazily (keeps the import optional)."""
+ if _Document is None: # pragma: no cover - exercised only with langchain
+ raise RuntimeError(LANGCHAIN_IMPORT_ERROR or "langchain-core not installed")
+ return _Document(**kwargs)
+
+
+try:
+ from langchain_core.documents import Document as _Document # type: ignore
+ from langchain_core.retrievers import (
+ BaseRetriever as _BaseRetriever, # type: ignore
+ )
+
+ LANGCHAIN_AVAILABLE = True
+except ImportError: # pragma: no cover - exercised only without langchain
+ LANGCHAIN_IMPORT_ERROR = (
+ "langchain-core is not installed. Install with: pip install langchain-core"
+ )
+ logger.debug(LANGCHAIN_IMPORT_ERROR)
+
+
+def _hit_layers(hit: Dict[str, Any]) -> Tuple[Dict[str, Any], Dict[str, Any]]:
+ """Nested HybridSearch metadata and ContextGraph.query node, if present."""
+ metadata = hit.get("metadata") if isinstance(hit.get("metadata"), dict) else {}
+ node = hit.get("node") if isinstance(hit.get("node"), dict) else {}
+ return metadata, node
+
+
+def _hit_id(hit: Dict[str, Any]) -> Optional[str]:
+ """Graph node id, preferring metadata over a HybridSearch vector id."""
+ metadata, node = _hit_layers(hit)
+ return (
+ hit.get("node_id")
+ or metadata.get("node_id")
+ or node.get("id")
+ or node.get("node_id")
+ or hit.get("id")
+ )
+
+
+def _hit_content(hit: Dict[str, Any], fallback: str = "") -> str:
+ metadata, node = _hit_layers(hit)
+ props = node.get("properties") if isinstance(node.get("properties"), dict) else {}
+ return (
+ hit.get("content")
+ or hit.get("text")
+ or metadata.get("content")
+ or metadata.get("text")
+ or props.get("content")
+ or fallback
+ )
+
+
+def _hit_type(hit: Dict[str, Any]) -> str:
+ metadata, node = _hit_layers(hit)
+ return (
+ hit.get("node_type")
+ or hit.get("type")
+ or metadata.get("node_type")
+ or metadata.get("type")
+ or node.get("type")
+ or node.get("node_type")
+ or "node"
+ )
+
+
+def _hit_score(hit: Dict[str, Any], default: float = 1.0) -> float:
+ return float(hit.get("score") if hit.get("score") is not None else hit.get("distance") or default)
+
+
+class SemanticaRetriever(_BaseRetriever): # type: ignore[misc]
+ """GraphRAG-style retriever over a Semantica ``ContextGraph``.
+
+ Args:
+ graph: A semantica.context.ContextGraph instance.
+ hybrid: A semantica.vector_store.HybridSearch instance used to seed
+ retrieval. If omitted, a best-effort keyword search on the graph
+ is used.
+ hops: Number of graph-edge expansion hops (default 2).
+ top_k: Number of seed hits (default 10).
+ """
+
+ graph: Any
+ hybrid: Any = None
+ hops: int = 2
+ top_k: int = 10
+
+ def __init__(
+ self,
+ graph: Any,
+ hybrid: Any = None,
+ hops: int = 2,
+ top_k: int = 10,
+ **kwargs: Any,
+ ) -> None:
+ """Explicit init so the retriever works with and without langchain."""
+ if LANGCHAIN_AVAILABLE:
+ # BaseRetriever is a Pydantic model: pass the declared fields
+ # through so validation succeeds.
+ super().__init__(
+ graph=graph,
+ hybrid=hybrid,
+ hops=hops,
+ top_k=top_k,
+ **kwargs,
+ )
+ else:
+ # Without langchain-core, BaseRetriever is a plain object
+ super().__init__() # type: ignore[call-arg]
+ self.graph = graph
+ self.hybrid = hybrid
+ self.hops = hops
+ self.top_k = top_k
+
+ def _get_relevant_documents(self, query: str, **kwargs: Any) -> List[Any]:
+ """LangChain BaseRetriever entry point."""
+ seed = self._seed_results(query)
+ if not seed:
+ return []
+
+ # Expand each seed node through the graph
+ expanded: Dict[str, Dict[str, Any]] = {}
+ for hit in seed:
+ node_id = _hit_id(hit)
+ if not node_id:
+ continue
+ metadata, _ = _hit_layers(hit)
+ expanded[node_id] = {
+ "content": _hit_content(hit, fallback=str(node_id)),
+ "node_type": _hit_type(hit),
+ "score": _hit_score(hit),
+ "metadata": metadata,
+ }
+ try:
+ neighbors = self.graph.get_neighbors(node_id, hops=self.hops)
+ for neighbor in neighbors:
+ nid = neighbor.get("node_id") or neighbor.get("id")
+ if nid and nid not in expanded:
+ expanded[nid] = {
+ "content": neighbor.get("content")
+ or neighbor.get("text")
+ or neighbor.get("name")
+ or str(nid),
+ "node_type": neighbor.get("node_type")
+ or neighbor.get("type")
+ or "node",
+ "score": float(neighbor.get("weight") or 0.5),
+ "metadata": {},
+ }
+ except Exception as exc: # graph expansion is best-effort
+ logger.debug("graph expansion failed for %s: %s", node_id, exc)
+
+ # Order: seed hits first (they have real scores), then neighbors.
+ # Keep a deterministic id->payload list (sets are unordered — see Qodo).
+ ordered_pairs: List[tuple] = []
+ seen_ids = set()
+ for hit in seed:
+ nid = _hit_id(hit)
+ if nid and nid in expanded and nid not in seen_ids:
+ ordered_pairs.append((nid, expanded[nid]))
+ seen_ids.add(nid)
+ for nid, item in expanded.items():
+ if nid not in seen_ids:
+ ordered_pairs.append((nid, item))
+ seen_ids.add(nid)
+
+ return [
+ _get_document(
+ page_content=item["content"],
+ metadata={
+ **item["metadata"],
+ "node_id": nid,
+ "node_type": item["node_type"],
+ "score": item["score"],
+ },
+ )
+ for nid, item in ordered_pairs
+ ]
+
+ def _seed_results(self, query: str) -> List[Dict[str, Any]]:
+ """Get seed results from hybrid search or a graph keyword scan."""
+ if self.hybrid is not None:
+ try:
+ return self.hybrid.search(query, k=self.top_k)
+ except Exception as exc:
+ logger.debug("hybrid search failed, falling back: %s", exc)
+ # Best-effort keyword scan over graph nodes (ContextGraph.query)
+ try:
+ return self.graph.query(query, limit=self.top_k)
+ except Exception:
+ return []
diff --git a/integrations/langchain/tools.py b/integrations/langchain/tools.py
new file mode 100644
index 00000000..9fa23f62
--- /dev/null
+++ b/integrations/langchain/tools.py
@@ -0,0 +1,133 @@
+"""
+SemanticaKGTool / SemanticaDecisionTool — LangChain ``BaseTool`` adapters
+for LangChain / LangGraph agents.
+"""
+
+from __future__ import annotations
+
+import json
+from typing import Any, Optional, Type
+
+from pydantic import BaseModel, ConfigDict, Field
+
+from semantica.utils.logging import get_logger
+
+logger = get_logger(__name__)
+
+# ---------------------------------------------------------------------------
+# Optional: LangChain core
+# ---------------------------------------------------------------------------
+LANGCHAIN_AVAILABLE = False
+LANGCHAIN_IMPORT_ERROR: Optional[str] = None
+
+_BaseTool: Any = object
+
+
+try:
+ from langchain_core.tools import BaseTool as _BaseTool # type: ignore
+
+ LANGCHAIN_AVAILABLE = True
+except ImportError: # pragma: no cover
+ LANGCHAIN_IMPORT_ERROR = (
+ "langchain-core is not installed. Install with: pip install langchain-core"
+ )
+ logger.debug(LANGCHAIN_IMPORT_ERROR)
+
+
+def _json(payload: Any) -> str:
+ return json.dumps(payload, default=str, ensure_ascii=False)
+
+
+class QueryGraphInput(BaseModel):
+ query: str = Field(..., description="Natural-language or keyword graph query")
+ limit: int = Field(10, description="Maximum matching nodes to return")
+
+
+class QueryDecisionsInput(BaseModel):
+ category: str = Field(
+ "",
+ description="Keyword to search recorded decisions; empty returns insights",
+ )
+ limit: int = Field(10, description="Maximum results when searching by keyword")
+
+
+class SemanticaKGTool(_BaseTool): # type: ignore[misc]
+ """LangChain tool for querying a Semantica ``ContextGraph``.
+
+ Args:
+ graph: A semantica.context.ContextGraph instance.
+
+ Example:
+ >>> tool = SemanticaKGTool(graph)
+ >>> agent = create_react_agent(model, tools=[tool])
+ """
+
+ model_config = ConfigDict(arbitrary_types_allowed=True)
+
+ name: str = "semantica_query_graph"
+ description: str = (
+ "Query Semantica's shared context graph with a natural-language "
+ "keyword query. Returns matching entities and relationships."
+ )
+ args_schema: Type[BaseModel] = QueryGraphInput
+ graph: Any = None
+
+ def __init__(self, graph: Any = None, **kwargs: Any) -> None:
+ if LANGCHAIN_AVAILABLE:
+ super().__init__(graph=graph, **kwargs)
+ else:
+ super().__init__()
+ self.graph = graph
+
+ def build(self) -> Any:
+ """Return this tool, or None if langchain-core is missing."""
+ return self if LANGCHAIN_AVAILABLE else None
+
+ def _run(self, query: str, limit: int = 10, **kwargs: Any) -> str:
+ try:
+ return _json(self.graph.query(query, limit=limit))
+ except Exception as exc:
+ return _json({"error": str(exc)})
+
+ async def _arun(self, query: str, limit: int = 10, **kwargs: Any) -> str:
+ return self._run(query, limit=limit)
+
+
+class SemanticaDecisionTool(_BaseTool): # type: ignore[misc]
+ """LangChain tool for searching Semantica's recorded decision log.
+
+ Args:
+ graph: A semantica.context.ContextGraph instance.
+ """
+
+ model_config = ConfigDict(arbitrary_types_allowed=True)
+
+ name: str = "semantica_query_decisions"
+ description: str = (
+ "Search Semantica's recorded decision log with a keyword query. "
+ "Returns decisions, rationale, and context."
+ )
+ args_schema: Type[BaseModel] = QueryDecisionsInput
+ graph: Any = None
+
+ def __init__(self, graph: Any = None, **kwargs: Any) -> None:
+ if LANGCHAIN_AVAILABLE:
+ super().__init__(graph=graph, **kwargs)
+ else:
+ super().__init__()
+ self.graph = graph
+
+ def build(self) -> Any:
+ """Return this tool, or None if langchain-core is missing."""
+ return self if LANGCHAIN_AVAILABLE else None
+
+ def _run(self, category: str = "", limit: int = 10, **kwargs: Any) -> str:
+ try:
+ if category:
+ return _json(self.graph.query(category, limit=limit))
+ return _json(self.graph.get_decision_insights())
+ except Exception as exc:
+ return _json({"error": str(exc)})
+
+ async def _arun(self, category: str = "", limit: int = 10, **kwargs: Any) -> str:
+ return self._run(category=category, limit=limit)
diff --git a/integrations/langchain/vectorstore.py b/integrations/langchain/vectorstore.py
new file mode 100644
index 00000000..49473cce
--- /dev/null
+++ b/integrations/langchain/vectorstore.py
@@ -0,0 +1,143 @@
+"""
+SemanticaVectorStore — LangChain ``VectorStore`` adapter over Semantica's
+hybrid search (``semantica.vector_store.HybridSearch``).
+"""
+
+from __future__ import annotations
+
+from typing import Any, Dict, Iterable, List, Optional
+
+from semantica.utils.logging import get_logger
+
+from .retriever import _hit_content, _hit_id, _hit_score, _hit_type, _hit_layers
+
+logger = get_logger(__name__)
+
+# ---------------------------------------------------------------------------
+# Optional: LangChain core
+# ---------------------------------------------------------------------------
+LANGCHAIN_AVAILABLE = False
+LANGCHAIN_IMPORT_ERROR: Optional[str] = None
+
+_VectorStoreBase: Any = object
+_Document: Any = None
+
+
+def _make_document(**kwargs: Any) -> Any:
+ if _Document is None: # pragma: no cover
+ raise RuntimeError(LANGCHAIN_IMPORT_ERROR or "langchain-core not installed")
+ return _Document(**kwargs)
+
+
+try:
+ from langchain_core.documents import Document as _Document # type: ignore
+ from langchain_core.vectorstores import (
+ VectorStore as _VectorStoreBase, # type: ignore
+ )
+
+ LANGCHAIN_AVAILABLE = True
+except ImportError: # pragma: no cover
+ LANGCHAIN_IMPORT_ERROR = (
+ "langchain-core is not installed. Install with: pip install langchain-core"
+ )
+ logger.debug(LANGCHAIN_IMPORT_ERROR)
+
+
+def _document_from_hit(hit: Dict[str, Any], include_score: bool = True) -> Any:
+ metadata, _ = _hit_layers(hit)
+ node_id = _hit_id(hit)
+ doc_meta = {
+ **metadata,
+ "node_id": node_id,
+ "node_type": _hit_type(hit),
+ }
+ if include_score:
+ doc_meta["score"] = _hit_score(hit, default=0.0)
+ return _make_document(
+ page_content=_hit_content(hit),
+ metadata=doc_meta,
+ )
+
+
+class SemanticaVectorStore(_VectorStoreBase): # type: ignore[misc]
+ """Wrap Semantica hybrid search as a LangChain ``VectorStore``.
+
+ Args:
+ hybrid: A semantica.vector_store.HybridSearch instance.
+ vector_store: Optional Semantica vector store passed through to
+ ``HybridSearch.add_texts``.
+ """
+
+ hybrid: Any
+ vector_store: Any = None
+
+ def __init__(self, hybrid: Any, vector_store: Any = None, **kwargs: Any) -> None:
+ if LANGCHAIN_AVAILABLE:
+ super().__init__(**kwargs)
+ else:
+ super().__init__()
+ self.hybrid = hybrid
+ self.vector_store = vector_store
+
+ # -- required VectorStore API ------------------------------------------
+ def add_texts(
+ self,
+ texts: Iterable[str],
+ metadatas: Optional[List[Dict[str, Any]]] = None,
+ **kwargs: Any,
+ ) -> List[str]:
+ """Embed and store texts; return the generated IDs.
+
+ Delegates to the Semantica ``VectorStore.add_documents`` backing the
+ HybridSearch instance (or to ``hybrid.vector_store`` if provided).
+ """
+ if self.vector_store is not None:
+ return self.vector_store.add_documents(
+ list(texts), metadata=metadatas, **kwargs
+ )
+ vs = getattr(self.hybrid, "vector_store", None)
+ if vs is not None and hasattr(vs, "add_documents"):
+ return vs.add_documents(list(texts), metadata=metadatas, **kwargs)
+ raise ValueError(
+ "SemanticaVectorStore requires a Semantica vector store with "
+ "add_documents (pass vector_store=... to the HybridSearch or to "
+ "SemanticaVectorStore)"
+ )
+
+ def similarity_search(self, query: str, k: int = 4, **kwargs: Any) -> List[Any]:
+ """Return documents most similar to the query."""
+ return [_document_from_hit(hit) for hit in self.hybrid.search(query, k=k)]
+
+ def similarity_search_with_score(
+ self, query: str, k: int = 4, **kwargs: Any
+ ) -> List[Any]:
+ """Return (document, score) pairs."""
+ return [
+ (
+ _document_from_hit(hit, include_score=False),
+ _hit_score(hit, default=0.0),
+ )
+ for hit in self.hybrid.search(query, k=k)
+ ]
+
+ @classmethod
+ def from_texts(
+ cls,
+ texts: List[str],
+ embedding: Any = None,
+ metadatas: Optional[List[Dict[str, Any]]] = None,
+ **kwargs: Any,
+ ) -> "SemanticaVectorStore":
+ """Build a store from a list of texts (LangChain convention).
+
+ Requires a pre-configured ``hybrid`` instance passed via kwargs.
+ """
+ hybrid = kwargs.pop("hybrid", None)
+ if hybrid is None:
+ raise ValueError(
+ "SemanticaVectorStore.from_texts requires a 'hybrid' "
+ "HybridSearch instance as a keyword argument"
+ )
+ store = cls(hybrid=hybrid, **kwargs)
+ store.add_texts(texts, metadatas=metadatas)
+ return store
diff --git a/pyproject.toml b/pyproject.toml
index aa2ce518..278b4c52 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -206,6 +206,7 @@ agno = ["agno>=1.0.0"]
# needed (it pulls vulnerable transitive deps like chromadb) and would only
# duplicate the prebuilt tooling users can install separately.
crewai = ["crewai>=0.80.0"]
+langchain = ["langchain-core>=0.3.0"]
# ---- File Watching ----
watch = ["watchdog>=6.0.0"]
@@ -253,7 +254,7 @@ explorer-lite = [
# dependency-audit/security gates. Install it explicitly via ``semantica[crewai]``.
all = [
"semantica[dev,viz,infra,cloud,monitoring,watch,llm-all,models-huggingface,split-all,graph-all,tripletstore-oxigraph,vectorstore-all,parse-docling,ingest-parquet,ingest-arrow,shacl,explorer]",
- "semantica[dev,viz,infra,cloud,monitoring,watch,llm-all,models-huggingface,split-all,graph-all,tripletstore-oxigraph,vectorstore-all,parse-docling,ingest-parquet,ingest-arrow,shacl,agno]"
+ "semantica[dev,viz,infra,cloud,monitoring,watch,llm-all,models-huggingface,split-all,graph-all,tripletstore-oxigraph,vectorstore-all,parse-docling,ingest-parquet,ingest-arrow,shacl,agno,langchain]"
]
# ---------------- ENTRYPOINTS ----------------
diff --git a/requirements-ci.txt b/requirements-ci.txt
index 5c99f201..ad725115 100644
--- a/requirements-ci.txt
+++ b/requirements-ci.txt
@@ -2191,6 +2191,10 @@ jsonlines==4.0.0 \
--hash=sha256:0c6d2c09117550c089995247f605ae4cf77dd1533041d366351f6f298822ea74 \
--hash=sha256:185b334ff2ca5a91362993f42e83588a360cf95ce4b71a73548502bda52a7c55
# via docling-ibm-models
+jsonpatch==1.33 \
+ --hash=sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade \
+ --hash=sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c
+ # via langchain-core
jsonpickle==4.1.2 \
--hash=sha256:7ffe34426bc797684dbf1dc84185558bd864cd25b1ff5fb01b7405e392d0a937 \
--hash=sha256:8afed18aa189fd81e2e833b426bb4af485594921f0b1d36c2001fc5637a2f210
@@ -2419,6 +2423,18 @@ kombu==5.6.2 \
--hash=sha256:8060497058066c6f5aed7c26d7cd0d3b574990b09de842a8c5aaed0b92cc5a55 \
--hash=sha256:efcfc559da324d41d61ca311b0c64965ea35b4c55cc04ee36e55386145dace93
# via celery
+langchain-core==1.5.6 \
+ --hash=sha256:b5f73bd9688c457b31ec73657a0ad56948f889fae27acee79286e9c285632ee6 \
+ --hash=sha256:d6cf37bf695ecc22cddeb8461a684e353190b2ce430d99eb22bc11c0c7c00ea5
+ # via semantica (pyproject.toml)
+langchain-protocol==0.0.18 \
+ --hash=sha256:70b53a86fbf9cedc863555effe44da192ab02d556ddbf2cf95b8873adcf41b5a \
+ --hash=sha256:ec3e11782f1ed0c9db38e5a9ed01b0e7a0d3fba406faa8aef6594b73c56a63e6
+ # via langchain-core
+langsmith==0.11.0 \
+ --hash=sha256:7339f90e6fd9a1a009445b5084a7a0e56a8b6f17305ee5d7e8c5e7582217854f \
+ --hash=sha256:e87a3929915936c066b3fa3283ec3f3f0013e2ef7f98a443a7fbe3fab8e784a3
+ # via langchain-core
lark==1.3.1 \
--hash=sha256:b426a7a6d6d53189d318f2b6236ab5d6429eaf09259f1ca33eb716eed10d2905 \
--hash=sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12
@@ -5483,6 +5499,10 @@ requests==2.34.2 \
# rapidocr
# spacy
# tiktoken
+requests-toolbelt==1.0.0 \
+ --hash=sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6 \
+ --hash=sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06
+ # via langsmith
rfc3339-validator==0.1.4 \
--hash=sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b \
--hash=sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa
@@ -6634,6 +6654,104 @@ urllib3==2.7.0 \
# pinecone-client
# qdrant-client
# requests
+uuid-utils==0.17.0 \
+ --hash=sha256:03815cea572c8a693cab5475b9d750cc161470961c7defa27e9286cad62f38f5 \
+ --hash=sha256:04452640d8b6920c480c16e5afe91ff896d236e0c972830f9247e0898d38c803 \
+ --hash=sha256:09a55b7a5ae764985cb46467496a1787678d0a1400356157a080ad95b1a36869 \
+ --hash=sha256:0ab4a66e7a035ad6625cfc1fbdb34f5c2d25a80ae1ef4bfee458ea2036333c6d \
+ --hash=sha256:0bc4c431ccd59c764080ceb43b126043325fe17861b87759d026a0cdd8423bb2 \
+ --hash=sha256:0f3729e839209f3457d0d8b6a35a376fdf65577a5aecaf4cc3587d3305759ba6 \
+ --hash=sha256:0fcca4e838af9ac9243b3358d7c14afa4dca286a87781124c272d6c4cad9c968 \
+ --hash=sha256:1019476b6bdc047216ef7414be5babe0fa5ccfde977c0cac4fd6c75ddec66ff7 \
+ --hash=sha256:14dc2f46abb1091260c0d203fcbdf4e045042cc07e49183fd3b255904b95eb70 \
+ --hash=sha256:1776a80d16369999b21627028cc5dbce819be83e1e079fdd7a51b587d2916db9 \
+ --hash=sha256:1edf2f8732e4ed95bd7b65f2658f4aa072efaaff321144f4e0d4bf6a22709263 \
+ --hash=sha256:1fd6f0e8a162dc0e9255b6aebe3cd175e76c33202f1bf39da9e6294b93db0099 \
+ --hash=sha256:21c79b61ff750abcf057163dd764ccb6196cde7a26cda1b31b45cd97769e03b3 \
+ --hash=sha256:220b52746d99e11964badac3c0869016e0c24bafb70a7dd5c2c072a6be3da9cc \
+ --hash=sha256:237722b6581bb5b4eb4cefbcbe5c6e2980a440aabe781fbe50ebf1cb71eee4cc \
+ --hash=sha256:239d8a281fe10bae33205b5d43185834d556b18434e0a113b5dc1dfb2fd97e91 \
+ --hash=sha256:29179ffb7b317239b6d6afb100d14c439c728770460718280b9c0a42d2561ec2 \
+ --hash=sha256:2db386941cfdecdd0b5a8ceeed5cf7479c83d1730dcf64a48d43cfa018cc3310 \
+ --hash=sha256:2dd4a21baaac9a88486f0dd166c5793feb101a0bb9f006f2c401657fff5a1343 \
+ --hash=sha256:309a35f12d99dde19032bc2259cda6431c85eeac0879134dc777cc3087d7e1cb \
+ --hash=sha256:3150d836290c88f1d26eb59c4db280d87417dd3bfaadd2889c77416c8f0ff6fa \
+ --hash=sha256:32abaafc8e91928b3d9f4d82e42d2094041e38ad6bb964066faadff28e4162f1 \
+ --hash=sha256:32df1944808877702ceea398c103881c09a679bb672a215e01c2a84231266bf9 \
+ --hash=sha256:344f7c755e280ea0ba6aeb08022190d867a80000b1715cacded54fc4b5633607 \
+ --hash=sha256:351462debd866f1f25e4d4f5c7fac89525b52151f0102a1bdfe94a999b046f5f \
+ --hash=sha256:375cde148430d60a4a07c03abaa0774c4fddfdd90de99b4ba02f24088bc9d750 \
+ --hash=sha256:387cf7437c94ddec08651a0f1081381299c7075bc48a6251d8922bf39973378a \
+ --hash=sha256:3dac0ad0cd9a2818d1775215365a4e8c2f8ada215529dd26f3f8cceeb67a6988 \
+ --hash=sha256:405233a5f625b3d995648f4647fa6befa4567cf3f74e1f6b9837e16f7310f0e0 \
+ --hash=sha256:4134353bfe3026ddab8e886002dc52bc5a0ab04611aabb0eaae23c32e6e57f64 \
+ --hash=sha256:42275ebd0e8e74e32cdbfb8bd88fc99576567d51d54a508020611fd8f4f463a0 \
+ --hash=sha256:4441600447d340ae103a353f01dbcd22ff680e5ee1a22988efe8d7b791d8fdb3 \
+ --hash=sha256:46a73cacdf512f473a81f65dbf84186e08cfe6e9118fa582b6c6b33a8288a30d \
+ --hash=sha256:4bf4d9cd1e80e73922073b9b27c143bedeb109d65f94cd12712e2c87118f2b7d \
+ --hash=sha256:4e2ac1c0b56f2c91b6f158e29ed96b1503223fe8aa6e79b1be1dc55bd8a5131c \
+ --hash=sha256:52db0e471d3d2632d35445af352591f40a8f32959a412981d9f51e068bb9514b \
+ --hash=sha256:53ce348ef4c6e98c02c19c522af01334fe94476ce9af0db8c4482f9f142ae9c1 \
+ --hash=sha256:5641071337eb11d61a001ea08793bf72216f3241f0a433ed2764804b2a3e3cc7 \
+ --hash=sha256:5670c52a438e21483ce715776144914a4e2a2a5c62d9dee15f8a3e90cf128ae6 \
+ --hash=sha256:56aa6488b931246fae11924e4bd0e2b32677e63945eecb71c29e3c2ca0dc3131 \
+ --hash=sha256:570db214f6d8507587a8faa968a3fe65e957daeb7bc48b27dc7f69bc3ecdd6f1 \
+ --hash=sha256:58838921e377791ef22c64cc92141bfae030f43651ff9272f0f28a208a9e6a5a \
+ --hash=sha256:589d9da7de8fa7f739bb970ac4632c9a268213117d634e1c4a58c1c1e821ca05 \
+ --hash=sha256:5a4370089c8b2e42f1db51d76408c7fa8eaa2934bf854d17983d16179c07c098 \
+ --hash=sha256:622cdde768300591ac79bfcd7bb3468e4b191b1105d5dbfe8d87c39d8f63dd46 \
+ --hash=sha256:673d89cc434cc9b97a0b4cf61272f6fca70a81f64eb0afbface2a0d9f77f06cd \
+ --hash=sha256:6a019a31bc4db89a0903a3e4f6b218571f3a6ff0ad4b3d3fe1c8f91a05ff6e3e \
+ --hash=sha256:6c142bd0cb4dba31c10babe00d59f7ef6460f0ef55eaa9c1a9da270684af996a \
+ --hash=sha256:6f29689a76fe7a49cbd629a794d0ec1eab48814e323a00a146a741b0195bde68 \
+ --hash=sha256:75d7411e8eb9259764dd60310738540649057cda4509b4af14b36b7f663bfeb0 \
+ --hash=sha256:793229621e1ad6cac55f015cfa9f4eff102accbc3da25d607b91c6b0bec167fb \
+ --hash=sha256:7a49f47ac26df3e431c56b825c1bae8e6d3d591fdbb7438c227cc9845a7e3d73 \
+ --hash=sha256:7b9044ce4acbf392d4b3a503fe377641f4deff82e6c341c36ef27af0dea76cdf \
+ --hash=sha256:7c89359affecebe2e39e6a116d069b363c936511a9572b308402489a26957d89 \
+ --hash=sha256:84ed3a2d5cd3ae6db87af20bfed3331116195ba4757ad7177fc8f12c1bbce2a9 \
+ --hash=sha256:89a0980d49683c00539c59cd9f46b1908c538e6b5b0a48ad12187bb856d0f391 \
+ --hash=sha256:8b72c2002202038666bf647f9a790906214c7c11cd0d6efef77b7d07bef3034a \
+ --hash=sha256:8eb3e5caca8d3a6f72ea4cce024583f989f6f2e9186f98800213fff0176e8bcc \
+ --hash=sha256:9082e709014946b1f6e96ae6ecd93652efca2d2a6a3ab67dbe151c8b4bf193a4 \
+ --hash=sha256:9205068badf453d2f0821fd5d340389b4679992d7ff79d4f3e5608996dd1b287 \
+ --hash=sha256:9472a8de37faf8bd216c628e0e68c8f6bef730d3ba0a5060f3b0fa460c992ac2 \
+ --hash=sha256:967955620df45e6cffe2e9950cb9903cb455649396f896b26b04363a91a5054b \
+ --hash=sha256:975c17da26c5b9d46c336b03c52a057ac28378d6f9d98b58d32a038589bb3912 \
+ --hash=sha256:981cc10163988defea96e8d6c507df151eab8f483e7df9ae543d5a41a4be073b \
+ --hash=sha256:98c88d3edd08e7245562e9815996dbc6f0bd4745e1c76462f24af5ae4e187dd1 \
+ --hash=sha256:9a91c4814c7150a4d798da691b7804eacd78c4b84fb392a60fa0de21341861eb \
+ --hash=sha256:9e311f908d2f842fca4c7dcebc4f10306b8089b204ef04cf6704b4332c9ff6ff \
+ --hash=sha256:9e753e81457241e2200c56a898e268e8fa25796271af0489c608f24d8e631eed \
+ --hash=sha256:a46bedc273b6f58f11dee816ff74999625ef8d007890f411b7a4975bf1c89330 \
+ --hash=sha256:abb5667a36119019b3fa320c4d10c21ebccfcc87c8a739e6a0056cee7f48dde2 \
+ --hash=sha256:b3131a82d0c7611f0aa480a6d36929e001a3f54ba0fc029a8118a5863cce513c \
+ --hash=sha256:b5d11cccba076a32321ef1380dea956821f0b51794ef59df64e58fb1cd543aae \
+ --hash=sha256:b6c5d2d71e1f17329150ad9427d27f4a3f29a01792e7ecdc64a98ac5368fc4d5 \
+ --hash=sha256:b776c7fc8755c7de06dd5a22b47c40ae84f67d13277ebb233cc84933ba4dcbcd \
+ --hash=sha256:c00d182e31034250690f417b9068b78eab423c10d76766664e82d9860c340479 \
+ --hash=sha256:c351737e2e65497c7200ab4ffb8af97e9f48be6488309abdd265fe08d66ee92f \
+ --hash=sha256:c4f845166b09acc65c5213a35551a7f81c17fa010ab467229b5813f79d17fe13 \
+ --hash=sha256:c589f5023d471ce75dd2cce61acb25ed6347e562041588a1a366808f22d7176c \
+ --hash=sha256:cee808b405e9095506f4e4e89924bec7ea77eac3129b6fe36eda04364b3b343b \
+ --hash=sha256:d11a7bc1e02da8984d32e6de9e0826c6edac00eac17de270f372bf32f9a0af63 \
+ --hash=sha256:d27c531edb8d1f38ca2eddaa1fa24913a460aeb721f2efd4ef42a124ce94e354 \
+ --hash=sha256:d2d9a63a9e6f2416ace8c109043a9280d6b34f34bb2e5421903e149403db40a6 \
+ --hash=sha256:d561a4c5747a1e6c7fa7c49a0292e78b4e8c456332caa084fc7abad8de828652 \
+ --hash=sha256:d63010803d7c368963bbe6f7ec379593e76dd581d7db0f29118d88713c9e0354 \
+ --hash=sha256:dd741c73440b328f937dc53b344ecadc46bc4f0cec0333a8f42b55f3468ce7ec \
+ --hash=sha256:de1064663aa7c839286488a319d2b3b478ca5ab5b2091ade888ed0eeca11a98a \
+ --hash=sha256:e252db239eb41c32248e096e0d170bce5896a4fd3405556362bc3dd83d912206 \
+ --hash=sha256:e288a06cbbbcd01b44386e767985c9e21d2ad9bf59829aa7058d9a2a494804ab \
+ --hash=sha256:e59b60a0a4cb7541480e02090d37dc2df3b72df4c2e776fff64ce3a4e3dd4637 \
+ --hash=sha256:e671b2322ef09106ecb1ca0f4c398b134d5e2c1f80d7a4f3336847a3072c0e94 \
+ --hash=sha256:e7b04935a79c03c41ad08d0a5f390aac968bfb561f1268897bc5b0f077971efd \
+ --hash=sha256:f7e9b8728ba07a3cb2f29d5aa1a266c2664eb8ef0fd43afa34627c92f7fac8f0 \
+ --hash=sha256:f9b093cb3b6c9d6233ef45a05cab064d2aa0a8cb3c5777084c9e20fcb77c2371 \
+ --hash=sha256:fae8b282f0cb22a5de222999f7723f4e5ec04f6fcdf4aaef879b5b36625ae2b0
+ # via
+ # langchain-core
+ # langsmith
uvicorn==0.52.1 \
--hash=sha256:112ec661814189acbccd3f7b86460147cc065fc92c0821afa78918780e4354dd \
--hash=sha256:e4403f9d93188cf9d1088e9f40e3acd12630e2df8675316704379a7fc20fff6a
@@ -7097,6 +7215,222 @@ xlsxwriter==3.2.9 \
--hash=sha256:254b1c37a368c444eac6e2f867405cc9e461b0ed97a3233b2ac1e574efb4140c \
--hash=sha256:9a5db42bc5dff014806c58a20b9eae7322a134abb6fce3c92c181bfb275ec5b3
# via python-pptx
+xxhash==4.0.1 \
+ --hash=sha256:0163b5d259de23ae9e07b7eabf435ce4704f6f205589a2b154e6af4be985ce1b \
+ --hash=sha256:03600a8987849b2bef7be795a60a6052b635c63fa98b718b08ca5ee823691cfc \
+ --hash=sha256:04f9a24de11a6647666d5302fd73d6a5224ce50ddc965fb0bb44cee736e6bd7c \
+ --hash=sha256:06713a5aaf1d0905c5579416c020c02e42b3ceb931e86c7d3b7fb85403dee3f3 \
+ --hash=sha256:06d7fbd609503c3be5e65cdb6bb2f040d6a98574404e2e1d5c60815c97fff4aa \
+ --hash=sha256:0718ad66f4ded2411f8e62bdba549ee71e313a2d26ef5060ca3fdbf29897dd3c \
+ --hash=sha256:08ed8da18cd4fd0a6a5d6a444852d8fbd0e565388a74a4937085451b5f1a312a \
+ --hash=sha256:09f9feb118966cc6650e1806205d577eae7ca394aa6acf349a0b62a94bbeb329 \
+ --hash=sha256:0ab851b45c70d4992be7cdeeee16f97a0b677408c758c4b1efb1cfe8030bfd37 \
+ --hash=sha256:0b1082fd0f089ce9098ed77aad8b777b5d156f8ac601c69cab73811822b8ef07 \
+ --hash=sha256:0b20a06454b34f1531fc677c54efe2ecdec691ef9224f7fa919bf2c1363f7ff1 \
+ --hash=sha256:0b42a5a26607e4b2409fea174773a66f2dff9dfdbf2c1a851bb7b804e2c97535 \
+ --hash=sha256:101aa300de6ceef3d9c77569706330d8921fc45dd82bceed2084f1e9f2557a24 \
+ --hash=sha256:1216f7ba5683f17a89eb7dcb4bc50a0b743dfe1902278d7b3d0786f538118433 \
+ --hash=sha256:1642907941ee4b75aacc3db688af52ea02ca2305ab22af7ee686ed726b332684 \
+ --hash=sha256:168dd6b51725a222abc722832e56624d15a63fc2e8249021509c93f1063913f6 \
+ --hash=sha256:1749f0688020209fe0d357ce1e1cd9ec9c6161ed0405ea949d24581c4c43fa91 \
+ --hash=sha256:1b3cccf75eeb5b01639b2feadb042a8e07889293b7ca72fa2985e7dcb64763cf \
+ --hash=sha256:1b50223d92df94d54e1a31469335a2c74b16692e6c1cb726f1e6949514458706 \
+ --hash=sha256:1bc591533fc975614f7e13594daee76af96b8e1fbcf8de76c8773858fa9e7cea \
+ --hash=sha256:1c2200b98a805351cb3142ae4e1fdcc9e91b5e20f5d30d4862b0b96f92558f4e \
+ --hash=sha256:1c7c642a0f79c3e3cf2965475507574d3d1a50ec71060039d60cb87358667cb2 \
+ --hash=sha256:1ee523f51718e41753f04f7102bb4dc55a18d2ea5cbaceef8ec7ca08571bd428 \
+ --hash=sha256:1f3346c5c287ac3c7f38b20380f55e8768230e7252af59fabcf3b87ab21e4256 \
+ --hash=sha256:2194bf96d5f3d4e0cb65deba370ec83dda3edfba42155f9384190ed5e51ea5e2 \
+ --hash=sha256:237b8f63a2a0fcfb1ffc06e21dad23add44e6d354b2b014364a1d41e419a4dee \
+ --hash=sha256:23a4376b4a3183cb50d4d2a3179f887a7773cc695eb2c908e551bec3221b8c60 \
+ --hash=sha256:247ece770647c0aef080561fa996f9774b4dadce2d0c42eeb98229db7dcf820d \
+ --hash=sha256:2696bbac613f6880fed60316c298bf3091d4f8eee3ae2e9466f70bb76204fb0c \
+ --hash=sha256:26fe6238c2d5b11ed5063b9bf4eb290624b004fd074688da6bb079bd564f10d7 \
+ --hash=sha256:2d52dc7c33c1b83082b707f6b7814dc76d2faaa2ea62bd9c5fab4b36f83c087f \
+ --hash=sha256:2df3ca8757dc381e75e90a4d7995a6324f58a923c7145220a7b2c0231f66fddc \
+ --hash=sha256:303121aab4b7f898058582d7962ea79d9e26e2379d7b6d8743f70f2671674481 \
+ --hash=sha256:3088dadbffa33c29e0518578430a7dff2e901a212e487aefa5faaa0dc06dad34 \
+ --hash=sha256:31d86f9e81f3e84e00131ac7c54caf5119ae4ddd82c09c31cff597c813ce1ee2 \
+ --hash=sha256:3358097d333d40657569ec1121e21043dd7d0efa10aead1b50e8b4fa83077d7b \
+ --hash=sha256:33e270d302c95ec426dfa0f5a4e16bff2ab8d7b8a46faa4746affb05e684ac77 \
+ --hash=sha256:33fd538191f47071deef6b1f676535e2aa770f1fd150ae4cc75a34c9e930be3d \
+ --hash=sha256:348c8f288dc961d6bbd1985c8152a3ed7a85c95df00e82320f0c5215d922a399 \
+ --hash=sha256:349775ac30372b344d2338b2a168c0a1312a644194da25b8bec476d55761a128 \
+ --hash=sha256:34ed93e20bfd98d722b902121643791eeb4b1641871e2dc63d0d4c2d93f187df \
+ --hash=sha256:37f667dee0f867c42894b34e2a6fe26bf195c0ea4683d9d2b713db023f242c3a \
+ --hash=sha256:3891efe3d7a531ce6da0a4a50a99dd41c75b8fd4ca19d73c86431b4db5c305f0 \
+ --hash=sha256:38c3d22129a6958846a3098d68bc8e661704461c0be4793ae28836e4690c8478 \
+ --hash=sha256:3c2445edafc300cc40feb6a25a8356a971c30cd0bf47b5349c2ad74c508343b1 \
+ --hash=sha256:3f68fe400ceec235f3e4a4b02a28c2fd2d283584a193223c921dd4c48f1d0754 \
+ --hash=sha256:3fb1d30d4b6d6e2c4a08e5ac6fffdb2b572d2cfcca15a5509cf4e7a1350f955c \
+ --hash=sha256:41e579025a6e13a99e6d71e39c9cfc621a0dcdbbf19106325e145fa858f2d794 \
+ --hash=sha256:421b94f3ba7067958d02e38960d987756347aa150df06df11aa68ae1af78c619 \
+ --hash=sha256:427b62d62d4f967fbb10b82a3813e4875c2a6e7e7634739f17265b650c7f65a6 \
+ --hash=sha256:436e11b4dd966afe5f7f665e4cc4c5485ffe3ceb42f25a22e1701d236abf1853 \
+ --hash=sha256:43bcf2a871f28f16135545415cab3ec43904d4c80425a64598a9e6cebfb2b5ba \
+ --hash=sha256:43e5f9169e73d0f0db33b5f6b8554bcce69ac278c966daf83d5eb4eb2f13829f \
+ --hash=sha256:440c401e146ce64bdb3beb8ff0c84677b6f21307c28a34779071cecee5d4d70c \
+ --hash=sha256:44ab12e8cd17d4f001769f00ad465208b4bcb897ed29e65f058f74466b57a98f \
+ --hash=sha256:4528cf80ebbbf57d40edfb31521ae265daa6dd636d615b1cf0ac86209579e59d \
+ --hash=sha256:45e88111ebe331de478ef8d4293efbe88f3cf8b863386c9a2357136b838e1af0 \
+ --hash=sha256:4741d42d59e4e5fa1a86c17ab9c27dc8ea459c700d91b6742fdb9138d9a516cb \
+ --hash=sha256:4751f1d7eecae6b2d2a773630f1a7248f125c9a92a456694d03c15bceffc9d68 \
+ --hash=sha256:488ca5c5e28ef56ec4bbb12f835b3f1cbecc5f3510062e70117bc6594851932a \
+ --hash=sha256:4972332c079d6aad69c4620a68d015a4ecb33141583f70d642cf9edf6a713763 \
+ --hash=sha256:4a252fb862b0ae2590587e625f47a0e03da05cf0205e8830b67b6596c06038b1 \
+ --hash=sha256:4a76345f5aceb4ec404918edf9c7f2b5507db864dc0d7455982009ac0890b57b \
+ --hash=sha256:4af350bc3f329970c0e3a59af84a8a30998bf8a9167eb50cd48e59baaa1d7bec \
+ --hash=sha256:4bbf3ff651e0f1a19beb5d0f48e0874a9bad2482a588c9d214c96ef1fff1cd9c \
+ --hash=sha256:4e5141543c7f7fe3087500bbb4ac2845cb528a980aa91f8f1e661e2292ff4a5d \
+ --hash=sha256:4f5e5c6df4b703afcbe9352d238a51efd97c3b91fdc3a2052e40fdacb1e7505f \
+ --hash=sha256:515a822c73abbf6a0b7c70976d9662be342835c9d78b8dc7c023411f39c35dbc \
+ --hash=sha256:554f87034635bcec47c5d72447bf3db7e02da1bf493a0ada010db28a76f891c6 \
+ --hash=sha256:567cbc630302a46a8ecfd943b309ccf5372bb3718f1f3762d452df30f033bcf0 \
+ --hash=sha256:57d7fa8f23908d173001c21a9e82bfc6ad997d1b6c270fb121812b7ed158891c \
+ --hash=sha256:5adf927dca8c47fde7e683fe69efdd81bc865c4db1fb6bb00b391e2b6185207b \
+ --hash=sha256:5b7875ac1a2edcb691f27642b8b94b904baa6bcecb7d79c72df2228ba8cb5c51 \
+ --hash=sha256:5b7979f71d06ae45a769de0699900a246d8cb632db1e8bfdc79ec019063a503c \
+ --hash=sha256:5c2d525a3afabcd8e3549d85fc7e111fde6bc302d06a1893fe73adb79823415e \
+ --hash=sha256:5dc434c946012e6d8a72b10f970ea30755b718251dd7591dbfdabafd3bcb21bc \
+ --hash=sha256:5f1ea31d61bcd2cd2f3ec4ca80a64187bbd7948f490b63cf0dcbc6e717b4c1e9 \
+ --hash=sha256:62198213fc3e0c56e567894b318ba45834e007d065f84ba6dc9165d21546fc56 \
+ --hash=sha256:63aa52659bc32bb9bd7cb5caf523b4d14429a477762cfac886132d687c1f80fc \
+ --hash=sha256:649f2682c090cca1ac4037866381f3652eaacbd56e5178030f4ce1325b8f945b \
+ --hash=sha256:67e57b834e07ed973cee7b6da1548ff28a56458d77696fd2a5f397f340694848 \
+ --hash=sha256:684160b3c0a9b62c6f0de90f44e11dc5d8643dcfa18a5856b45fb1c47478bb71 \
+ --hash=sha256:6a8c5ce76b94ba49f3be8a8f2611abc6564210702c72ac9e237ca2bebfd17794 \
+ --hash=sha256:6a9f98af872355e0c02439e48583958eee00e60b928bb20476460d9d40cb7b4e \
+ --hash=sha256:6c45258a37fc22721395c09927cb982d3e7a83607cab15be7e2416501bd3a330 \
+ --hash=sha256:6cbf4e21ef0890804b5bb9ad25c48f9c127758d7f6c66bef374efcacc63c738a \
+ --hash=sha256:6cf633df84d80a1668fcf61e330791dae46825e395549e7d34f376411e75088a \
+ --hash=sha256:6efb8f21cc136c79b3e5bb747c8682d37916fb202cdbbc32182de5c4e47f821f \
+ --hash=sha256:70129ebb8f20e1ac1da58b78ed381624bd689a43a9a7366560bd8fabea145105 \
+ --hash=sha256:704381264b36a18b9c62ecbabe2e71d0fc58c77c129c15355c989b10bf05b6b0 \
+ --hash=sha256:7236be540d6be9ce448d98b940dd26ddf70ca41012e8a14a53fd9354cefe4e8d \
+ --hash=sha256:72f34834518157a75e7090f328ee7a16c70c804cfc7c694fa069cc888e9fc03e \
+ --hash=sha256:74379a577a9f3b6afbdedf1b90e5c7764467051977f18a326d7d607336d743bd \
+ --hash=sha256:74a164e8b63f1e9cf35c9a7809d082b033d1a00e7375d5d814415436e7867e57 \
+ --hash=sha256:760de77279e9cf9c81d012ce0705cba13afccee9b09c480f17d778c8c5cefae8 \
+ --hash=sha256:764b32d52d15b8b95ac8160e540772fa1adeb611fe40bffaeb42e7bf98279e44 \
+ --hash=sha256:79a3203aadf39637869dfea1185227d8452844d78b837e54fb1117b4d34ba5c3 \
+ --hash=sha256:7c343ee174d417a44d0c3355602c0cbbfa52a04d1bbbf1723378c7d2c8f60626 \
+ --hash=sha256:7e27dbed5c4ba033919e4b4ed8dc14e029e91d14a93cd9f920d25277c7df6781 \
+ --hash=sha256:81507a68ba84c55241fb61cce1469f473a5da4205fc8ef6f698e5948eea8dd88 \
+ --hash=sha256:81664268dba92e037b740ecf37fa02f1cab4a391f93f28e35792b3341c60648f \
+ --hash=sha256:839f58c5bd9989875be0fd28446dbf32cace2c2cd8bf2f6762acdc38a95cd1aa \
+ --hash=sha256:83b8c2013edb5dc1f9e7268b6496130705bc48d79c86bb8817b3d210b81a5513 \
+ --hash=sha256:84df5f8da574caadbc0cb1b8866ecc2368cc941f0cd05f677756c802f370dafa \
+ --hash=sha256:8580aab306888224074c7edeec734de0c3c5ccde65b2da4e6c9a5e28f7c0a1bd \
+ --hash=sha256:85bdd40cb505a11e0ca04191711266c5fd696ed786ae83849955e457774edc96 \
+ --hash=sha256:85e402dab0f9acd3604539747c6fcc57dc188a18af6ab07eb8189351cd32466c \
+ --hash=sha256:863f3d3b44110f7243e86cf994aa5c5d88f2348b6e84ab4402fadadfbf9f7da7 \
+ --hash=sha256:86b2b12bec60c678ed8f5cca0258ad93a8928ebddb6ca7732f0875afe1451d1a \
+ --hash=sha256:87aa309a93bd5ec13f14309a305ff4e9bf74c5363fc46c264c0a22edfd5b0670 \
+ --hash=sha256:87cbdec1a7dd930079671a60b249f3ca4e773e6fbd0676e21e36fdc9dd0f3b00 \
+ --hash=sha256:87da13df72c5612771cd905a8b121e0bfea62d7659b1c92198736eb722220e83 \
+ --hash=sha256:88d87719fe6bddf117238b341c5db851f8e96ba68ad9832b450e4a43dc60b37f \
+ --hash=sha256:8b4477edc03091f51f5309406d230851c23cf4822029e3bf40b8df53093fff1c \
+ --hash=sha256:8b99ebaf9e816ac5069423b1367ee7e8078fbcebcf62545506bb0608d2f4f468 \
+ --hash=sha256:8ba782ca3bf1e81492611152b9a0d5264971339e95e34d69de0ac2c926be496d \
+ --hash=sha256:8bcba9456242ebf180a04d9443812fd85ffe6bd12bda464dd116fcece8886ff3 \
+ --hash=sha256:8c9fe122444e129881afd1d4d1c7ac0d3ce2d91b68c2b40173b6025ff1c31f9a \
+ --hash=sha256:8ec4777d92fd61a5c8fdeddab894fd65bea301a8092fb5419ec6472aa4d458d7 \
+ --hash=sha256:90cb2a1c9cc503a054a19612b48ff6e8e47805f618bdb3224a07568aad03a37e \
+ --hash=sha256:9283d9dd6b44acad35118e2976fc763a065509e4118debdb61916ec322ed17b9 \
+ --hash=sha256:94ac8a6b8c47951173f0b67bf862bcb971bf24e493b9fbbdb0e010cbbc7d9f54 \
+ --hash=sha256:96d8de55029d42251945531f6aa7590c32b48163c66a43bf29d8657d7446a377 \
+ --hash=sha256:96dedccfb09a73a25751053a183159b88f4ee75f388df8166040c152ac0531c6 \
+ --hash=sha256:9761ff4a0ffa583fe850731ad24fe82c88cccb7a2294727db0955f3279a4cb3f \
+ --hash=sha256:97b455de3e8b1b0b1e4594cb61a468992563f03ca264062fbb0a66b393c01d90 \
+ --hash=sha256:97b94fb29abf21f5f0bde15f7dbdd3a4aa2dc59f37026adc7b4bee8563b84375 \
+ --hash=sha256:99054b838b74d8d3995ea0d410976ae967c46207ae22d6ddfc535e809197dab9 \
+ --hash=sha256:99166cc98637e8bf550cda2aab07f4f1d5f899c45fbd721801aeabcc9d404824 \
+ --hash=sha256:9a51b061d54cda8b83e62c44458bfbf0dabbef9b975dd9649952ba5076b9f349 \
+ --hash=sha256:9b1dddc257279417d93c9e59420d49ef90aece90d7a01996db3aade74b0281b1 \
+ --hash=sha256:9c3c4b9aa9a27196b921197f7daf9e6c1412739df06a99cfa6e923879362eff6 \
+ --hash=sha256:a14578102a6081465aec9cf73c76c3cd3f79f0709bdb3b8ae7ab0b54c9d8b089 \
+ --hash=sha256:a16a3fa6936e36bb1414d16a6bd012c9033e5161b68b426805b61d895392437d \
+ --hash=sha256:a33de7633c948ab2dc144af370a66e7e7af29b425dcd0f7e4f59689fb9391b53 \
+ --hash=sha256:a43418e1a90b4809a9caf64aeb8b0696e3e1f300a323acc1e6ee2f93ae319fcf \
+ --hash=sha256:a4553d36cc0b7fce1f35ba8a94dfd775aa3ed12f5eab2dc3b46ac75a0706b0bb \
+ --hash=sha256:a5b21b42a01a343096a1c018d35e9b7aec9c7065dda53ae8da071e37478b2cea \
+ --hash=sha256:a65785e653573fcd1e33062760ab4c3c3440e8e910765018e4b6ed4ad07b54a0 \
+ --hash=sha256:a6671a8f6ea4f2101ce11fab5023a2e59391cff249fc3928cecb69d971525fd5 \
+ --hash=sha256:a69e8946e4902ea11fc1c557740cdbfe7d75c78fcc5e4324ff89a696a634357d \
+ --hash=sha256:a6e3653df1a70b8ac4191216324242e4be2bca18c9a7c10934e1bd56dc7ca15e \
+ --hash=sha256:a865d2d470220e659220fdb59d5b6c4422802d8d6098e1324bc4d12444798914 \
+ --hash=sha256:a949b072ea59c6eca0811ccd9e95133cc50d2afda8d464b5b077c78f78efa269 \
+ --hash=sha256:aa6ccc7f31018484d652cf52db020003433f3c9fa83189c028bd807d2adde503 \
+ --hash=sha256:ac0f291ab6485bd71f33941f9b92771318332a05d505460b41e893a549caadc0 \
+ --hash=sha256:acb31ecdd1a97fab5cd39a84ee9f515e727d319f796fec48703b8339b9998360 \
+ --hash=sha256:acf52474b2494ef66dc7e0fb6d5e2b50c18313039ad4d275fbf9f9907c804bc5 \
+ --hash=sha256:ad889d58361a26ba75f5d6a1a0da08ed4950ec4ac8a6da86e1c5ce1b95ccb43f \
+ --hash=sha256:adbd48b30e3f82c89fb2b3e6a87cdd28d113b190a5ed0ee2dee286323ee9a621 \
+ --hash=sha256:af05a3f650220a6c59fa0ad2410249f2d2470a05225807c378fb67458693f8df \
+ --hash=sha256:b3662719007e059abde7eddacf8517142ba076ddc7b30c807260e57d28c3c191 \
+ --hash=sha256:b3bece52127ac20044311ee73567f9f0893b5de64f9028aecc90cc740cfd525a \
+ --hash=sha256:b4c8842fb19d78b5e8c2a52baf4c8357658cc56c62bc822b86ce0f942f28e286 \
+ --hash=sha256:b659fad79c99b0238c7ad7e9d7dbf4eebfea9097c2dba65fa0a4d18a25b29a2f \
+ --hash=sha256:b6c1f9c59bbe593f88a0aad30be4150f15bd57bd64efb95feeabcb8e563f1ecd \
+ --hash=sha256:bdd16718b63aa3ebd68aabb79021a40e47c81374852d41a306b9453141bbcbee \
+ --hash=sha256:bf430c587f447a554c53768ad76b9846fe7c5632180ef6f69c4fce8b0552fbd0 \
+ --hash=sha256:bfed61996d618eb90d6eaae0178002e3466a28b06bfc557a7a3a7266378d8c5a \
+ --hash=sha256:c09ada495567c9c9a8156c5ebcfb93be7fece0755062d738c972dcbecd0d84b5 \
+ --hash=sha256:c0e6ccc2b19ec8a726b2e26062ac71ea63e15500d6bf85910e42481844fdffc1 \
+ --hash=sha256:c101180495cb4ba3617b279a944345c53a5e73b0c150053d1fa8d8af32de9579 \
+ --hash=sha256:c10b9206753b64aa791b35b201485477525b26fdec5bf86e8364c388a03e2592 \
+ --hash=sha256:c3074db513c81f764053e3da079312ecf85a50d8350c71f4cc0105d9662a9e6c \
+ --hash=sha256:c30dd1af66a820820398b26e0d74e7a9aa43cae705924f23ed828cd8e5c26c3d \
+ --hash=sha256:c57963970d359a72262f7fe6be88f945e2334d4bc41462b7f08c37b0abf35ca6 \
+ --hash=sha256:c6301d92545c591ad31c3e050aa40a5f8a4c16413f1f9e6f9322c6f0f9d2b736 \
+ --hash=sha256:c6370189e8e66b7e608f533b939a9de092ddca6cce084ca0d3d414d2ed5b5d59 \
+ --hash=sha256:c6fc415b5568bd9accc7187f1729a99707330c0a67a8b9f93c1149ed573ed75d \
+ --hash=sha256:c7484fea54964edd417cc3a104d5180562514aa7c4e2a2bc26d776ef0c4cb4a1 \
+ --hash=sha256:cba763d84b06bda2c38d5185dee76f1b9dfdc0789e96e476d9e10005526d0788 \
+ --hash=sha256:cd878d32f5c6cbce9783f8d6897561fb772211edba9dde49d85672b88ed45276 \
+ --hash=sha256:ce6d5cc94a50291d080259a126cbf1e9ba4ac861e6429d2f3cdbb1474f51945d \
+ --hash=sha256:d0d24a4f3fb63852cd09af46ae4b7a4d00cc8b8615a046dca543786e728d1056 \
+ --hash=sha256:d1e0d1ea6e44f51808a9e8469c8afdebcdf6fa23d1ea524a0303d57d23919712 \
+ --hash=sha256:d54b8ae068af532c8cdf56abb9e09a60fbe7b10792444c9c27987bb6d3b450fa \
+ --hash=sha256:d55bf4ef10eb09b8b6866790e083d26d087d84caa3cc0946ba87c3ca7ecaf7b7 \
+ --hash=sha256:d9f3848ffaf010bdbabdbf4c25641fa258b6227ff27bc74a4d06edef521a4873 \
+ --hash=sha256:da0264844a09b538c894e5eff25313d941deb4dedec2131b98418a71a3c9944e \
+ --hash=sha256:da544672efd9ad76077928a3e6c5d894e52ce82d3bf14002db4a1bf17d1a36a2 \
+ --hash=sha256:daade8936c4deaaf7b01561324ce438ba4f885d717e9adc62b4d67212ad7d7bd \
+ --hash=sha256:dd649663ddeafbfd4734eb8abae921dd5baa1242f20bda54e8bc927369ccded4 \
+ --hash=sha256:deca2a30d983d240b8375ec2ee0a4288e72042827fc61df2f7671f8467e4cb2f \
+ --hash=sha256:e259bb7e1e2d8de6b35f430f5c7220b1c0ebf3962d1ba7ec7545980d5931edb8 \
+ --hash=sha256:e3996ff9b6f99180357024336bf5749a8ad6476a9a2523e535c5212b995b12a2 \
+ --hash=sha256:e3eba72f9bb84fe696516f4cbca68d3d74a376157e68bacddbb7f2516af61523 \
+ --hash=sha256:e4296fcc790876a8b0f297edc83d3b088457b774d8f67b4636807f8a2ec69a79 \
+ --hash=sha256:e53926e76131a74e79cc0b39fa712c227875f180afc68646bd1e1d8a17e60313 \
+ --hash=sha256:e681a6fc7e4f715252b9b5acfb30536ec7dd1f75033a32dc617e6fa95af1a3fd \
+ --hash=sha256:e71b34978e77868cbf2d18c5206a4603f9c644dd7181bec5643bd40141d3b8c5 \
+ --hash=sha256:e8cda075b10bb3917b002c74a04f9e02b7d13b5bf732571404d51c52b11c7329 \
+ --hash=sha256:e90b4bcf1d9eb1010fdaee7c9209fb667e74c0684f3ba17f9032bd7319da90c9 \
+ --hash=sha256:e961093277ff9d42addb9dad5614dfb7800ccba07c245c39c8e9b4daa35d160c \
+ --hash=sha256:e9701c073bd062fb6bf6be51b47186ad15f1e87feedf4ea07198e0333ec068dc \
+ --hash=sha256:e998cb3685b92101ec5de0fb4d9485cf01e50bc418211955c55d98064664cf4c \
+ --hash=sha256:ea5ecf800b45bdb34afe05a1d0dae1f8ea02a290e50636dccd399063f6b180f8 \
+ --hash=sha256:ec1a470c6db94ac4589c203921e89ac1bc13e796a8b1784d8135e1893559cd3b \
+ --hash=sha256:edccc2ec58435a580f96a48a3ccae8cd0a480824119165dd90108718ad81ae6e \
+ --hash=sha256:f00330ac7e24769e2032203f2b01794d670916b0c1799fd261340f1af9499875 \
+ --hash=sha256:f09ee747e2a5f876cc5ad56947734811828335e13b403dd8ea1e06d77a9dd48d \
+ --hash=sha256:f18732adcc271741bd651c3e56fa519d8a237d2cccda01fe3afb226bf87f783b \
+ --hash=sha256:f1b603d0686c99fa0879f104a74e7db58367634c6e50ba827bee9aa095e23205 \
+ --hash=sha256:f33cf0baa91eccd2cb7b62bf00f10c2264ef578b71dd33a12962e71a36eb4d32 \
+ --hash=sha256:f3e1a44af01b6692de0ec6caba5f0bf93ceb36896e02b7fc00952c6ea7ef39e1 \
+ --hash=sha256:f484ed57bb3e4142f9d6439568658c38be5f94b702ba00a1ff32c69783b6c66d \
+ --hash=sha256:f5d031f35962e5483a613214e61f09fe24ab523062c3646d592dc16c4a217451 \
+ --hash=sha256:f6247f5e23ee94f2557ac9dab738a336f607c6ff476fcf66ca70c3aef5eee15a \
+ --hash=sha256:f7db035447a0ac8959aa230c5d36545ecf9f547413eb1711c0ca6f0ba1418925 \
+ --hash=sha256:f83295394d34e1287e5b30fcc496c13b92cf886a131f3dae5444e38da8757efb \
+ --hash=sha256:fac4832b638000106207bc44e44b9616a6a416aaee56c62b01d61f3705e49f58 \
+ --hash=sha256:fb59a0dd61fb2ad481c03fda399d78ce57dab6bb62c2c8fdb446a7ba4754b89a \
+ --hash=sha256:fc737c05ca2d48e5dcdbbb249314df3fc6c2a0be6da8b0aa28e13d72afaad7cd \
+ --hash=sha256:ff48915bf1871a1f19f74c11834c6329443d306cedc0c05fe7fe617810422a80 \
+ --hash=sha256:ffa44b4c7c5d0ffa31356b4428659516c0e47647825c74079a296b3857b6d99d
+ # via langsmith
yarl==1.24.5 \
--hash=sha256:0055afc45e864b92729ac7600e2d102c17bef060647e74bca75fa84d66b9ff36 \
--hash=sha256:0465ec8cedc2349b97a6b595ace64084a50c6e839eca40aa0626f38b8350e331 \
@@ -7207,3 +7541,104 @@ zipp==4.1.0 \
--hash=sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f \
--hash=sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602
# via importlib-metadata
+zstandard==0.25.0 \
+ --hash=sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64 \
+ --hash=sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a \
+ --hash=sha256:05353cef599a7b0b98baca9b068dd36810c3ef0f42bf282583f438caf6ddcee3 \
+ --hash=sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f \
+ --hash=sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6 \
+ --hash=sha256:07b527a69c1e1c8b5ab1ab14e2afe0675614a09182213f21a0717b62027b5936 \
+ --hash=sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431 \
+ --hash=sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250 \
+ --hash=sha256:106281ae350e494f4ac8a80470e66d1fe27e497052c8d9c3b95dc4cf1ade81aa \
+ --hash=sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f \
+ --hash=sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851 \
+ --hash=sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3 \
+ --hash=sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9 \
+ --hash=sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6 \
+ --hash=sha256:19796b39075201d51d5f5f790bf849221e58b48a39a5fc74837675d8bafc7362 \
+ --hash=sha256:1cd5da4d8e8ee0e88be976c294db744773459d51bb32f707a0f166e5ad5c8649 \
+ --hash=sha256:1f3689581a72eaba9131b1d9bdbfe520ccd169999219b41000ede2fca5c1bfdb \
+ --hash=sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5 \
+ --hash=sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439 \
+ --hash=sha256:22a06c5df3751bb7dc67406f5374734ccee8ed37fc5981bf1ad7041831fa1137 \
+ --hash=sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa \
+ --hash=sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd \
+ --hash=sha256:25f8f3cd45087d089aef5ba3848cd9efe3ad41163d3400862fb42f81a3a46701 \
+ --hash=sha256:2b6bd67528ee8b5c5f10255735abc21aa106931f0dbaf297c7be0c886353c3d0 \
+ --hash=sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043 \
+ --hash=sha256:3756b3e9da9b83da1796f8809dd57cb024f838b9eeafde28f3cb472012797ac1 \
+ --hash=sha256:37daddd452c0ffb65da00620afb8e17abd4adaae6ce6310702841760c2c26860 \
+ --hash=sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611 \
+ --hash=sha256:3b870ce5a02d4b22286cf4944c628e0f0881b11b3f14667c1d62185a99e04f53 \
+ --hash=sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b \
+ --hash=sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088 \
+ --hash=sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e \
+ --hash=sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa \
+ --hash=sha256:4b14abacf83dfb5c25eb4e4a79520de9e7e205f72c9ee7702f91233ae57d33a2 \
+ --hash=sha256:4b6d83057e713ff235a12e73916b6d356e3084fd3d14ced499d84240f3eecee0 \
+ --hash=sha256:4d441506e9b372386a5271c64125f72d5df6d2a8e8a2a45a0ae09b03cb781ef7 \
+ --hash=sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf \
+ --hash=sha256:51526324f1b23229001eb3735bc8c94f9c578b1bd9e867a0a646a3b17109f388 \
+ --hash=sha256:53e08b2445a6bc241261fea89d065536f00a581f02535f8122eba42db9375530 \
+ --hash=sha256:53f94448fe5b10ee75d246497168e5825135d54325458c4bfffbaafabcc0a577 \
+ --hash=sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902 \
+ --hash=sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc \
+ --hash=sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98 \
+ --hash=sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a \
+ --hash=sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097 \
+ --hash=sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea \
+ --hash=sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09 \
+ --hash=sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb \
+ --hash=sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7 \
+ --hash=sha256:75ffc32a569fb049499e63ce68c743155477610532da1eb38e7f24bf7cd29e74 \
+ --hash=sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b \
+ --hash=sha256:78228d8a6a1c177a96b94f7e2e8d012c55f9c760761980da16ae7546a15a8e9b \
+ --hash=sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b \
+ --hash=sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91 \
+ --hash=sha256:81dad8d145d8fd981b2962b686b2241d3a1ea07733e76a2f15435dfb7fb60150 \
+ --hash=sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049 \
+ --hash=sha256:89c4b48479a43f820b749df49cd7ba2dbc2b1b78560ecb5ab52985574fd40b27 \
+ --hash=sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a \
+ --hash=sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00 \
+ --hash=sha256:9174f4ed06f790a6869b41cba05b43eeb9a35f8993c4422ab853b705e8112bbd \
+ --hash=sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072 \
+ --hash=sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c \
+ --hash=sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c \
+ --hash=sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065 \
+ --hash=sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512 \
+ --hash=sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1 \
+ --hash=sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f \
+ --hash=sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2 \
+ --hash=sha256:a51ff14f8017338e2f2e5dab738ce1ec3b5a851f23b18c1ae1359b1eecbee6df \
+ --hash=sha256:a5a419712cf88862a45a23def0ae063686db3d324cec7edbe40509d1a79a0aab \
+ --hash=sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7 \
+ --hash=sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b \
+ --hash=sha256:ab85470ab54c2cb96e176f40342d9ed41e58ca5733be6a893b730e7af9c40550 \
+ --hash=sha256:b9af1fe743828123e12b41dd8091eca1074d0c1569cc42e6e1eee98027f2bbd0 \
+ --hash=sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea \
+ --hash=sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277 \
+ --hash=sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2 \
+ --hash=sha256:c2ba942c94e0691467ab901fc51b6f2085ff48f2eea77b1a48240f011e8247c7 \
+ --hash=sha256:c8e167d5adf59476fa3e37bee730890e389410c354771a62e3c076c86f9f7778 \
+ --hash=sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859 \
+ --hash=sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d \
+ --hash=sha256:d8c56bb4e6c795fc77d74d8e8b80846e1fb8292fc0b5060cd8131d522974b751 \
+ --hash=sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12 \
+ --hash=sha256:daab68faadb847063d0c56f361a289c4f268706b598afbf9ad113cbe5c38b6b2 \
+ --hash=sha256:e05ab82ea7753354bb054b92e2f288afb750e6b439ff6ca78af52939ebbc476d \
+ --hash=sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0 \
+ --hash=sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3 \
+ --hash=sha256:e59fdc271772f6686e01e1b3b74537259800f57e24280be3f29c8a0deb1904dd \
+ --hash=sha256:e7360eae90809efd19b886e59a09dad07da4ca9ba096752e61a2e03c8aca188e \
+ --hash=sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f \
+ --hash=sha256:ea9d54cc3d8064260114a0bbf3479fc4a98b21dffc89b3459edd506b69262f6e \
+ --hash=sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94 \
+ --hash=sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708 \
+ --hash=sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313 \
+ --hash=sha256:f5aeea11ded7320a84dcdd62a3d95b5186834224a9e55b92ccae35d21a8b63d4 \
+ --hash=sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c \
+ --hash=sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344 \
+ --hash=sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551 \
+ --hash=sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01
+ # via langsmith
diff --git a/tests/integrations/langchain/test_degradation.py b/tests/integrations/langchain/test_degradation.py
new file mode 100644
index 00000000..b50ce76c
--- /dev/null
+++ b/tests/integrations/langchain/test_degradation.py
@@ -0,0 +1,92 @@
+"""
+Graceful-degradation tests for the LangChain integration.
+
+Runs the adapters in a fresh subprocess with langchain-core hidden, so the
+object-base path is proven even when this env has langchain-core installed.
+"""
+
+from __future__ import annotations
+
+import os
+import subprocess
+import sys
+from types import SimpleNamespace
+
+import pytest
+
+from integrations.langchain import (
+ LANGCHAIN_AVAILABLE,
+ SemanticaDecisionTool,
+ SemanticaKGTool,
+)
+
+REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
+
+_SCRIPT = r"""
+import sys
+from types import SimpleNamespace
+
+class _BlockLangchain:
+ def find_spec(self, fullname, path=None, target=None):
+ if fullname == "langchain_core" or fullname.startswith("langchain_core."):
+ raise ImportError("langchain_core blocked for degradation test")
+ return None
+
+sys.meta_path.insert(0, _BlockLangchain())
+for name in list(sys.modules):
+ if name == "langchain_core" or name.startswith("langchain_core."):
+ del sys.modules[name]
+
+from integrations.langchain.retriever import LANGCHAIN_AVAILABLE as RET_AVAIL
+from integrations.langchain.vectorstore import (
+ LANGCHAIN_AVAILABLE as VS_AVAIL,
+ SemanticaVectorStore,
+)
+from integrations.langchain.tools import (
+ LANGCHAIN_AVAILABLE as TOOL_AVAIL,
+ SemanticaKGTool,
+ SemanticaDecisionTool,
+)
+from integrations.langchain.retriever import SemanticaRetriever, _get_document
+
+assert RET_AVAIL is False and VS_AVAIL is False and TOOL_AVAIL is False
+
+retriever = SemanticaRetriever(graph=SimpleNamespace(), hops=2)
+assert retriever.hops == 2
+
+store = SemanticaVectorStore(hybrid=SimpleNamespace(), tags=["x"])
+assert store.hybrid is not None
+
+graph = SimpleNamespace(query=lambda q, limit=10: [{"q": q, "limit": limit}])
+assert SemanticaKGTool(graph).build() is None
+assert SemanticaDecisionTool(graph).build() is None
+
+try:
+ _get_document(page_content="x")
+ raise SystemExit("expected RuntimeError from _get_document")
+except RuntimeError as exc:
+ assert "langchain-core" in str(exc)
+
+print("DEGRADATION_OK")
+"""
+
+
+def test_importable_and_functional_without_langchain():
+ result = subprocess.run(
+ [sys.executable, "-c", _SCRIPT],
+ cwd=REPO_ROOT,
+ capture_output=True,
+ text=True,
+ timeout=60,
+ )
+ assert result.returncode == 0, (
+ f"subprocess failed:\nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}"
+ )
+ assert "DEGRADATION_OK" in result.stdout
+
+
+@pytest.mark.skipif(LANGCHAIN_AVAILABLE, reason="langchain-core is installed")
+def test_tools_build_returns_none_without_langchain():
+ graph = SimpleNamespace()
+ assert SemanticaKGTool(graph).build() is None
+ assert SemanticaDecisionTool(graph).build() is None
diff --git a/tests/integrations/langchain/test_langchain_integration.py b/tests/integrations/langchain/test_langchain_integration.py
new file mode 100644
index 00000000..88812e06
--- /dev/null
+++ b/tests/integrations/langchain/test_langchain_integration.py
@@ -0,0 +1,231 @@
+"""
+Tests for integrations/langchain.
+
+Adapter behavior is always exercised (hit parsing, seed/fallback, tool JSON).
+LangChain-present paths use pytest.importorskip; degradation without
+langchain-core is covered in test_degradation.py via a subprocess so it still
+runs when langchain-core is installed in this env.
+"""
+
+from __future__ import annotations
+
+import json
+from types import SimpleNamespace
+from unittest.mock import MagicMock
+
+import pytest
+
+from integrations.langchain import (
+ LANGCHAIN_AVAILABLE,
+ SemanticaDecisionTool,
+ SemanticaKGTool,
+ SemanticaRetriever,
+ SemanticaVectorStore,
+)
+from integrations.langchain.retriever import _hit_content, _hit_id, _hit_type
+from integrations.langchain.tools import QueryDecisionsInput, QueryGraphInput
+
+# HybridSearch.search() returns {id, score, distance, metadata} — content lives
+# inside metadata, and id is a vector id, not a graph node id.
+_HYBRID_HIT = {
+ "id": "vec_0",
+ "score": 0.91,
+ "distance": 0.09,
+ "metadata": {
+ "node_id": "alice",
+ "content": "Alice is a developer",
+ "node_type": "person",
+ "source": "graph",
+ },
+}
+
+
+def test_exports_exist():
+ assert callable(SemanticaRetriever)
+ assert callable(SemanticaVectorStore)
+ assert callable(SemanticaKGTool)
+ assert callable(SemanticaDecisionTool)
+
+
+def test_version():
+ from integrations.langchain import __version__
+
+ assert __version__ == "0.1.0"
+
+
+# ---------------------------------------------------------------------------
+# Hit parsing (the Qodo high-severity finding)
+# ---------------------------------------------------------------------------
+def test_hit_id_prefers_metadata_node_id_over_vector_id():
+ assert _hit_id(_HYBRID_HIT) == "alice"
+ assert _hit_id({"node_id": "n1"}) == "n1"
+ assert _hit_id({"id": "n2"}) == "n2"
+
+
+def test_hit_id_unwraps_context_graph_query_shape():
+ hit = {
+ "node": {
+ "id": "alice",
+ "type": "person",
+ "properties": {"content": "Alice"},
+ },
+ "score": 1.0,
+ "content": "Alice is a developer",
+ }
+ assert _hit_id(hit) == "alice"
+ assert _hit_content(hit) == "Alice is a developer"
+ assert _hit_type(hit) == "person"
+
+
+def test_hit_content_and_type_read_nested_metadata():
+ assert _hit_content(_HYBRID_HIT) == "Alice is a developer"
+ assert _hit_type(_HYBRID_HIT) == "person"
+ assert _hit_content({"id": "x"}) == ""
+
+
+# ---------------------------------------------------------------------------
+# Retriever
+# ---------------------------------------------------------------------------
+def test_empty_seed_returns_empty():
+ graph = MagicMock()
+ graph.query.return_value = []
+ retriever = SemanticaRetriever(graph=graph, top_k=5)
+ assert retriever._seed_results("query") == []
+ assert retriever.hops == 2
+
+
+def test_seed_uses_hybrid_when_provided():
+ graph = MagicMock()
+ hybrid = MagicMock()
+ hybrid.search.return_value = [_HYBRID_HIT]
+ retriever = SemanticaRetriever(graph=graph, hybrid=hybrid)
+ results = retriever._seed_results("query")
+ assert len(results) == 1
+ hybrid.search.assert_called_once_with("query", k=10)
+
+
+def test_graph_fallback_when_hybrid_fails():
+ graph = MagicMock()
+ graph.query.return_value = [{"node_id": "n1", "content": "c1"}]
+ hybrid = MagicMock()
+ hybrid.search.side_effect = RuntimeError("down")
+ retriever = SemanticaRetriever(graph=graph, hybrid=hybrid)
+ results = retriever._seed_results("query")
+ assert len(results) == 1
+ graph.query.assert_called_once()
+
+
+def test_retriever_reads_hybrid_metadata_and_expands_by_node_id():
+ pytest.importorskip("langchain_core")
+ graph = MagicMock()
+ graph.get_neighbors.return_value = [
+ {
+ "id": "bob",
+ "type": "person",
+ "content": "Bob reports to Alice",
+ "weight": 0.8,
+ }
+ ]
+ hybrid = MagicMock()
+ hybrid.search.return_value = [_HYBRID_HIT]
+ retriever = SemanticaRetriever(graph=graph, hybrid=hybrid)
+ docs = retriever._get_relevant_documents("Alice")
+ assert docs[0].page_content == "Alice is a developer"
+ assert docs[0].metadata["node_id"] == "alice"
+ assert docs[0].metadata["source"] == "graph"
+ graph.get_neighbors.assert_called_once_with("alice", hops=2)
+ assert [d.metadata["node_id"] for d in docs] == ["alice", "bob"]
+
+
+# ---------------------------------------------------------------------------
+# VectorStore
+# ---------------------------------------------------------------------------
+def test_add_texts_delegates_to_vector_store():
+ vs = MagicMock()
+ vs.add_documents.return_value = ["id1"]
+ store = SemanticaVectorStore(hybrid=MagicMock(), vector_store=vs)
+ assert store.add_texts(["hello"]) == ["id1"]
+ vs.add_documents.assert_called_once()
+
+
+def test_add_texts_raises_without_vector_store():
+ store = SemanticaVectorStore(hybrid=SimpleNamespace(vector_store=None))
+ with pytest.raises(ValueError):
+ store.add_texts(["hello"])
+
+
+def test_from_texts_requires_hybrid_kwarg():
+ with pytest.raises(ValueError):
+ SemanticaVectorStore.from_texts(["hello"], embedding=None)
+
+
+def test_vectorstore_reads_hybrid_metadata():
+ pytest.importorskip("langchain_core")
+ hybrid = MagicMock()
+ hybrid.search.return_value = [_HYBRID_HIT]
+ store = SemanticaVectorStore(hybrid=hybrid)
+ docs = store.similarity_search("Alice", k=1)
+ assert docs[0].page_content == "Alice is a developer"
+ assert docs[0].metadata["node_id"] == "alice"
+ assert docs[0].metadata["source"] == "graph"
+ pairs = store.similarity_search_with_score("Alice", k=1)
+ assert pairs[0][0].page_content == "Alice is a developer"
+ assert pairs[0][1] == pytest.approx(0.91)
+
+
+# ---------------------------------------------------------------------------
+# Tools — JSON payload + BaseTool contract
+# ---------------------------------------------------------------------------
+def test_kg_tool_returns_full_valid_json():
+ graph = MagicMock()
+ graph.query.return_value = [{"content": "x" * 5000, "id": i} for i in range(3)]
+ raw = SemanticaKGTool(graph)._run("q", limit=3)
+ parsed = json.loads(raw)
+ assert len(parsed) == 3
+ assert len(parsed[0]["content"]) == 5000
+ graph.query.assert_called_once_with("q", limit=3)
+
+
+def test_tool_errors_are_json():
+ graph = MagicMock()
+ graph.query.side_effect = RuntimeError("boom")
+ assert json.loads(SemanticaKGTool(graph)._run("q")) == {"error": "boom"}
+ assert json.loads(SemanticaDecisionTool(graph)._run("q")) == {"error": "boom"}
+
+
+def test_decision_tool_empty_category_uses_insights():
+ graph = MagicMock()
+ graph.get_decision_insights.return_value = {"n": 0}
+ assert json.loads(SemanticaDecisionTool(graph)._run("")) == {"n": 0}
+
+
+def test_tools_are_base_tools_with_args_schema():
+ pytest.importorskip("langchain_core")
+ from langchain_core.tools import BaseTool
+
+ graph = MagicMock()
+ graph.query.return_value = [{"hit": True}]
+ kg = SemanticaKGTool(graph)
+ dec = SemanticaDecisionTool(graph)
+ assert isinstance(kg, BaseTool)
+ assert isinstance(dec, BaseTool)
+ assert kg.args_schema is QueryGraphInput
+ assert dec.args_schema is QueryDecisionsInput
+ assert kg.build() is kg
+ parsed = json.loads(kg.invoke({"query": "Alice", "limit": 5}))
+ assert parsed == [{"hit": True}]
+
+
+@pytest.mark.skipif(not LANGCHAIN_AVAILABLE, reason="langchain-core not installed")
+def test_kg_tool_invoke_with_context_graph():
+ pytest.importorskip("langchain_core")
+ try:
+ from semantica.context import ContextGraph
+ except ImportError:
+ pytest.skip("ContextGraph import requires optional core deps")
+
+ graph = ContextGraph()
+ graph.add_node(node_id="alice", node_type="person", content="Alice is a developer")
+ result = SemanticaKGTool(graph).invoke({"query": "Alice", "limit": 5})
+ assert "Alice" in result
+ json.loads(result)
|