mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-08-29 04:26:20 +00:00
* feat(integrations): add LangChain integration — retriever, vectorstore, tools Co-authored-by: Cursor <cursoragent@cursor.com> * fix(langchain): address Qodo review on HybridSearch hits and tools Read nested HybridSearch metadata so retriever/vectorstore Documents are not empty, make the agent tools real BaseTool subclasses, and stop slicing tool JSON into invalid payloads. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
144 lines
4.7 KiB
Python
144 lines
4.7 KiB
Python
"""
|
|
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
|