LangChain and LangGraph¶
The optional adapter, installed with pip install "openbtk[langchain]". See the
guide for worked examples.
openbtk.integrations.langchain.embeddings.OpenBTKEmbeddings ¶
OpenBTKEmbeddings(provider: BaseEmbeddingProvider)
Bases: Embeddings
Expose a BaseEmbeddingProvider to LangChain.
Documents are embedded in slices of the provider's own batch_size so a
long list is never sent as one oversized request. The provider's
sends_data_offsite flag is unchanged by wrapping: an offsite provider
stays offsite, and a local-only policy is enforced where the provider is
constructed -- see :meth:from_registry.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
provider
|
BaseEmbeddingProvider
|
The provider to wrap. |
required |
Example
import numpy as np from openbtk.core.base import BaseEmbeddingProvider class Fixed(BaseEmbeddingProvider): ... sends_data_offsite = False ... dimension = 2 ... def embed(self, texts): ... return np.ones((len(texts), 2), dtype=np.float32) OpenBTKEmbeddings(Fixed()).embed_query("hello") [1.0, 1.0]
Source code in src/openbtk/integrations/langchain/embeddings.py
40 41 | |
from_registry
classmethod
¶
from_registry(key: str, *, policy: PolicyConfig | None = None, **params: Any) -> OpenBTKEmbeddings
Build the provider registered under key and wrap it.
policy is forwarded to the registry, which refuses an offsite
provider unless policy.allow_offsite_providers is true -- wrapping
a provider for LangChain does not bypass the local-only guard.
Raises:
| Type | Description |
|---|---|
PolicyError
|
If the provider sends data offsite and |
Source code in src/openbtk/integrations/langchain/embeddings.py
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | |
openbtk.integrations.langchain.chat_models.OpenBTKChatModel ¶
Bases: BaseChatModel
Expose a BaseLLMProvider as a LangChain chat model.
Only system, user and assistant messages with plain-text content are
supported -- anything else raises ProviderError rather than being
flattened into a lossy prompt. Streaming is not overridden: Openbtk's
BaseLLMProvider.stream takes a prompt string, not a message list, so
.stream() here falls back to LangChain's default of yielding the
complete response as a single chunk.
Construct it with provider= -- the BaseLLMProvider to wrap (a
pydantic field, as every LangChain chat model's parameters are).
Example
from openbtk.core.base import BaseLLMProvider from openbtk.core.schemas import LLMResponse class Echo(BaseLLMProvider): ... sends_data_offsite = False ... def generate(self, prompt, kw): return LLMResponse(text=prompt) ... def stream(self, prompt, kw): yield prompt ... def chat(self, messages, **kw): ... return LLMResponse(text=messages[-1].content.upper()) OpenBTKChatModel(provider=Echo()).invoke("hi").content 'HI'
openbtk.integrations.langchain.vectorstores.OpenBTKVectorStore ¶
OpenBTKVectorStore(store: BaseVectorStore, embedding: Embeddings, *, text_key: str = _DEFAULT_TEXT_KEY)
Bases: VectorStore
Expose a BaseVectorStore and an Embeddings as a LangChain
VectorStore.
Openbtk stores hold vectors and metadata, not documents, so each text is
kept in the metadata under text_key ("text" -- the same key
:class:~openbtk.pipelines.rag.RAGPipeline reads, so one index serves both
APIs). A caller's own metadata may not use that key.
Scores are the underlying store's own: their scale and direction (L2
distance falls as similarity rises; inner product rises) depend on how the
store was built, which this adapter cannot know. For that reason
similarity_search_with_relevance_scores (a normalised 0-1 score) is
unsupported rather than guessed at.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
store
|
BaseVectorStore
|
The Openbtk vector store. |
required |
embedding
|
Embeddings
|
Embeds documents and queries. Wrap an Openbtk provider with
:class: |
required |
text_key
|
str
|
Metadata key holding each document's text. |
_DEFAULT_TEXT_KEY
|
Example
import numpy as np from openbtk.core.base import BaseVectorStore from openbtk.core.schemas import SearchResult from langchain_core.embeddings import DeterministicFakeEmbedding class Mem(BaseVectorStore): ... def init(self): self.rows = {} ... def upsert(self, ids, vectors, metadata): ... self.rows.update(zip(ids, metadata)) ... def query(self, vector, top_k=5, filter=None): ... return [SearchResult(id=i, score=1.0, metadata=m) ... for i, m in list(self.rows.items())[:top_k]] ... def delete(self, ids): pass vs = OpenBTKVectorStore(Mem(), DeterministicFakeEmbedding(size=4)) _ = vs.add_texts(["note A"], ids=["a"]) vs.similarity_search("anything")[0].page_content 'note A'
Source code in src/openbtk/integrations/langchain/vectorstores.py
65 66 67 68 69 70 71 72 73 74 | |
delete ¶
delete(ids: list[str] | None = None, **kwargs: Any) -> bool | None
Delete by id. Deleting everything (ids=None) is unsupported:
BaseVectorStore has no clear operation.
Source code in src/openbtk/integrations/langchain/vectorstores.py
119 120 121 122 123 124 125 126 127 128 129 130 131 132 | |
from_texts
classmethod
¶
from_texts(texts: list[str], embedding: Embeddings, metadatas: list[dict[str, Any]] | None = None, *, ids: list[str] | None = None, store: BaseVectorStore | None = None, **kwargs: Any) -> OpenBTKVectorStore
Build over store and index texts.
store is required: this adapter does not choose a backend (FAISS,
Chroma, Qdrant...) on the caller's behalf.
Source code in src/openbtk/integrations/langchain/vectorstores.py
174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 | |
openbtk.integrations.langchain.documents.chunk_to_document ¶
chunk_to_document(chunk: ClinicalTextChunk) -> Document
Convert a chunk to a LangChain Document.
Raises:
| Type | Description |
|---|---|
ProcessingError
|
If |
Example
from openbtk.core.schemas import TextSpan chunk = ClinicalTextChunk( ... chunk_id="n1:0", record_id="n1", text="Plan: rest.", ... span=TextSpan(start=0, end=11, label="chunk", confidence=1.0), ... token_count=4, ... ) doc = chunk_to_document(chunk) doc.page_content, doc.id, doc.metadata["record_id"] ('Plan: rest.', 'n1:0', 'n1')
Source code in src/openbtk/integrations/langchain/documents.py
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | |
openbtk.integrations.langchain.documents.document_to_chunk ¶
document_to_chunk(document: Document, *, record_id: str | None = None, token_counter: Callable[[str], int] | None = None) -> ClinicalTextChunk
Convert a Document back to a chunk.
A Document produced by :func:chunk_to_document round-trips exactly.
For any other Document, record_id and token_counter fill in what
the Document cannot say; the chunk id falls back to Document.id, and
the span to the whole text.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
document
|
Document
|
The Document to convert. |
required |
record_id
|
str | None
|
Overrides/fills |
None
|
token_counter
|
Callable[[str], int] | None
|
Returns the exact subword count of a text. Required
unless the Document carries |
None
|
Raises:
| Type | Description |
|---|---|
ProcessingError
|
If the chunk id, record id or token count cannot be determined, or the metadata is not valid for a chunk. |
Source code in src/openbtk/integrations/langchain/documents.py
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | |
openbtk.integrations.langchain.runnables.as_runnable ¶
as_runnable(component: Component | DeidEngine) -> Runnable[Any, Any]
Wrap component as a LangChain Runnable (see the module table).
Raises:
| Type | Description |
|---|---|
ConfigError
|
If |
Example
import contextlib, io with contextlib.redirect_stdout(io.StringIO()): # registry debug logs ... from openbtk.guardrails.phi_leakage import PHILeakageGuardrail ... result = as_runnable(PHILeakageGuardrail()).invoke("Plan: rest.") result.passed True
Source code in src/openbtk/integrations/langchain/runnables.py
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | |
openbtk.integrations.langchain.runnables.from_runnable ¶
from_runnable(runnable: Runnable[str, str]) -> RunnablePreprocessor
Wrap a text-to-text Runnable as an Openbtk record preprocessor.
Source code in src/openbtk/integrations/langchain/runnables.py
135 136 137 | |
openbtk.integrations.langchain.graph.as_langgraph_node ¶
as_langgraph_node(component: Component | DeidEngine, *, input_key: str, output_key: str, name: str | None = None) -> Callable[[Mapping[str, Any]], dict[str, Any]]
Wrap component as a LangGraph node.
The node reads state[input_key], runs the component exactly as
:func:~openbtk.integrations.langchain.as_runnable documents, and returns
{output_key: result} -- a partial update LangGraph merges into state.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
component
|
Component | DeidEngine
|
Any component |
required |
input_key
|
str
|
State key holding the component's input. |
required |
output_key
|
str
|
State key the result is written to. |
required |
name
|
str | None
|
The node's |
None
|
Raises:
| Type | Description |
|---|---|
ConfigError
|
At call time, if |
Example
import contextlib, io with contextlib.redirect_stdout(io.StringIO()): # registry debug logs ... from openbtk.guardrails.phi_leakage import PHILeakageGuardrail ... node = as_langgraph_node( ... PHILeakageGuardrail(), input_key="draft", output_key="check" ... ) ... update = node({"draft": "Plan: rest."}) update["check"].passed True
Source code in src/openbtk/integrations/langchain/graph.py
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | |