Runnables
zenpyre.runnables ¶
Contain runnables.
zenpyre.runnables.CachingRunnable ¶
Bases: Runnable[Input, Output], MultilineDisplayMixin
Wrap a Runnable to cache its output, keyed by a hash of the
input.
On each call, key_fn(input) is used to look up a previously
cached result in cache. On a cache hit, the cached result is
returned without calling the wrapped runnable. On a cache miss,
the wrapped runnable is invoked and its result is stored in
cache before being returned. If cache is None, caching
is disabled entirely and every call goes straight to the wrapped
runnable.
batch/abatch look up each input's cache entry individually,
then call the wrapped runnable's own batch/abatch for only
the inputs that missed -- so a partially-cached batch still benefits
from the wrapped runnable's batching, rather than falling back to
one call per miss.
Unlike subclassing a caching base class, this wrapper works with any
Runnable — including third-party ones you don't control — since
caching is composed around the runnable rather than baked into its
class hierarchy.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
runnable
|
Runnable[Input, Output]
|
The runnable whose output should be cached. |
required |
cache
|
Cache | None
|
The :class: |
None
|
key_fn
|
Callable[[Input], str] | None
|
A function that derives a cache key from an input. The
returned string is used directly as the |
None
|
Example
>>> from langchain_core.runnables import RunnableLambda
>>> from persista.cache import Cache
>>> from zenpyre.runnables import CachingRunnable
>>> runnable = RunnableLambda(lambda x: x.upper())
>>> with Cache() as cache:
... cached = CachingRunnable(runnable=runnable, cache=cache)
... cached.invoke("hello")
...
'HELLO'
zenpyre.runnables.InputOutputPair
dataclass
¶
Bases: Generic[Input, Output]
A frozen pair holding a runnable's input alongside the output it produced.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
input
|
Input
|
The input value passed to the wrapped runnable. |
required |
output
|
Output
|
The output value produced by the wrapped runnable for
|
required |
zenpyre.runnables.InputOutputRunnable ¶
Bases: Runnable[Input, InputOutputPair[Input, Output]], Generic[Input, Output]
Wrap a runnable so invoking it returns an
:class:InputOutputPair of its input and output, instead of just
the output.
This is useful whenever downstream code needs to know which input produced a given output -- e.g. logging, evaluation harnesses, or building a dataset of (input, output) examples -- without having to thread the input through the wrapped runnable itself or zip inputs and outputs back together by hand afterwards.
.batch()/.abatch() delegate to the wrapped runnable's own
batch/abatch implementation (rather than falling back to the
default per-item invoke loop that :class:Runnable provides),
so any batching optimizations the inner runnable implements (e.g.
batched LLM calls) are preserved. When return_exceptions=True
and a given input's call fails, the corresponding entry in the
returned list is the raw exception, not an :class:InputOutputPair
-- exactly as :meth:Runnable.batch behaves for the wrapped
runnable itself.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
runnable
|
Runnable[Input, Output]
|
The runnable to wrap. |
required |
Example
>>> from langchain_core.runnables import RunnableLambda
>>> from zenpyre.runnables import InputOutputRunnable
>>> inner = RunnableLambda(lambda x: x.upper())
>>> wrapped = InputOutputRunnable(inner)
>>> result = wrapped.invoke("hello")
>>> result.input
'hello'
>>> result.output
'HELLO'
>>> [pair.output for pair in wrapped.batch(["a", "b"])]
['A', 'B']
zenpyre.runnables.RecordingRunnable ¶
Bases: Runnable[Input, Output], MultilineDisplayMixin, Generic[Input, Output]
Wrap a Runnable to record the input and output of each invocation to a record store.
This is a transparent passthrough wrapper: calling invoke,
ainvoke, batch, or abatch behaves exactly like calling
the wrapped runnable directly (same return value, same
exceptions), with the side effect of writing one
:class:~zenpyre.records.Record per invocation to record_store
via :meth:~zenpyre.record_stores.base.BaseRecordStore.add_records.
Each record gets a fresh, randomly generated ID (not derived from
its content), so that two calls with identical input/output/extra
are still both recorded rather than one silently overwriting the
other via the store's upsert semantics.
Note
If the wrapped runnable raises during invoke/
ainvoke, the exception propagates immediately and no
record is written for that call -- unlike batch/abatch
with return_exceptions=True, which does record failed
items (see "error" below). If you want failed single calls
recorded too for a fully consistent audit trail, wrap the
self._runnable.invoke(...)/ainvoke(...) calls in
invoke/ainvoke in a try/except that builds an
error record before re-raising, mirroring
:meth:_record_batch's handling.
Each record's metadata is assembled as a plain dict with the
following keys, then passed through serializer (see below) as
a whole before being stored:
"input"/"output": the invocation's raw input and output."timestamp": an ISO 8601 UTC timestamp of when the call completed."run_id": therun_idfrom the call'sRunnableConfig, if the caller supplied one explicitly; otherwiseNone. This is not LangChain's internally auto-generated run ID (that isn't accessible from a plain wrapper like this one), only one explicitly passed in by the caller."error":Noneon success. On a batch item that failed withreturn_exceptions=True, this holdsstr(exception)and"output"isNone.- Any additional keys from
extra(fixed for this wrapper's lifetime, e.g. an experiment ID) and/or from the call'sconfig["metadata"](varies per invocation, e.g. a session or user ID). If the same key appears in both, the per-callconfig["metadata"]value wins. Neither may use one of the reserved keys above; doing so raises :exc:ValueError.
stream/astream are supported on a best-effort basis: each
chunk is yielded to the caller immediately (true streaming isn't
delayed), while chunks are accumulated internally (via +, as
LangChain message chunks support) to reconstruct a final output
for recording once the stream is exhausted. If chunks don't
support +, or the stream raises before completing, no record
is written for that call (a warning is logged) rather than raising
an error into the caller's stream.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
runnable
|
Runnable[Input, Output]
|
The inner Runnable to wrap. |
required |
record_store
|
BaseRecordStore
|
The store to write input/output records to. |
required |
extra
|
dict[str, Any] | None
|
Additional metadata merged into every record written by
this wrapper, fixed for its lifetime (e.g. an experiment
ID). For metadata that varies per call, pass it via
|
None
|
serializer
|
Callable[[dict[str, Any]], dict[str, Any]] | None
|
A function applied to the whole assembled metadata
dict before it's stored, to make it JSON-friendly (or to
apply any other custom transform). Defaults to
:func: |
None
|
Example
>>> from zenpyre.record_stores import DuckDBRecordStore
>>> from zenpyre.runnables import RecordingRunnable
>>> store = DuckDBRecordStore(":memory:")
>>> recorded = RecordingRunnable(
... chat_model, store, extra={"experiment_id": "exp-42"}
... ) # doctest: +SKIP
>>> recorded.invoke("Hello!", config={"metadata": {"session_id": "s-1"}}) # doctest: +SKIP
AIMessage(content='Hi there!')
>>> store.all()[0].metadata["experiment_id"], store.all()[0].metadata[
... "session_id"
... ] # doctest: +SKIP
('exp-42', 's-1')
zenpyre.runnables.RecordingRunnable.reserved_metadata_keys
property
¶
reserved_metadata_keys: frozenset[str]
The metadata keys reserved for this class's own use.
Neither extra nor a call's config["metadata"] may use
one of these; doing so raises :exc:ValueError. A subclass may
override this property to change the reserved set.
zenpyre.runnables.RecordingRunnable.__init__ ¶
__init__(
runnable: Runnable[Input, Output],
record_store: BaseRecordStore,
*,
extra: dict[str, Any] | None = None,
serializer: (
Callable[[dict[str, Any]], dict[str, Any]] | None
) = None
) -> None
Initialize the wrapper.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
runnable
|
Runnable[Input, Output]
|
The inner Runnable to wrap and record calls of. |
required |
record_store
|
BaseRecordStore
|
The store to write input/output records to. |
required |
extra
|
dict[str, Any] | None
|
Additional metadata merged into every record
written by this wrapper, fixed for its lifetime (e.g.
an experiment ID). Must not contain a key from
:attr: |
None
|
serializer
|
Callable[[dict[str, Any]], dict[str, Any]] | None
|
A function applied to the whole assembled
metadata dict before it's stored. Defaults to
:func: |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
zenpyre.runnables.RecordingRunnable.abatch
async
¶
abatch(
inputs: list[Input],
config: (
RunnableConfig | list[RunnableConfig] | None
) = None,
*,
return_exceptions: bool = False,
**kwargs: Any
) -> list[Output]
Asynchronously invoke the wrapped runnable on a list of inputs and record each call.
The async counterpart of :meth:batch: calls
self.runnable.abatch(inputs, config, return_exceptions,
**kwargs), then writes one :class:~zenpyre.records.Record
per (input, result) pair to the record store in a single
:meth:~zenpyre.record_stores.base.BaseRecordStore.add_records
call, before returning the results unchanged. Unlike
:meth:ainvoke, a failed item (when return_exceptions=True)
is still recorded, with "error" set to str(exception)
and "output" set to None.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
inputs
|
list[Input]
|
The list of inputs to pass to the wrapped runnable. |
required |
config
|
RunnableConfig | list[RunnableConfig] | None
|
Optional run configuration, either a single config
shared by every item or a list with one config per
item (matching |
None
|
return_exceptions
|
bool
|
If |
False
|
**kwargs
|
Any
|
Additional keyword arguments forwarded to the
wrapped runnable's |
{}
|
Returns:
| Type | Description |
|---|---|
list[Output]
|
One result per input, in the same order as |
zenpyre.runnables.RecordingRunnable.ainvoke
async
¶
ainvoke(
input: Input,
config: RunnableConfig | None = None,
**kwargs: Any
) -> Output
Asynchronously invoke the wrapped runnable and record the call.
The async counterpart of :meth:invoke: calls
self.runnable.ainvoke(input, config, **kwargs), writes one
:class:~zenpyre.records.Record to the record store capturing
input, the returned output, a timestamp, and any
run_id/extra metadata, then returns that output
unchanged.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
input
|
Input
|
The input to pass to the wrapped runnable. |
required |
config
|
RunnableConfig | None
|
Optional run configuration. If it has a
|
None
|
**kwargs
|
Any
|
Additional keyword arguments forwarded to the
wrapped runnable's |
{}
|
Returns:
| Type | Description |
|---|---|
Output
|
The wrapped runnable's output, unchanged. |
Raises:
| Type | Description |
|---|---|
Exception
|
Whatever the wrapped runnable itself raises. In that case, this method does not catch it, so no record is written for the failed call (see the class docstring's Note). |
zenpyre.runnables.RecordingRunnable.astream
async
¶
astream(
input: Input,
config: RunnableConfig | None = None,
**kwargs: Any
) -> AsyncIterator[Output]
Asynchronously stream the wrapped runnable's output chunks, recording an accumulated final result once the stream completes.
The async counterpart of :meth:stream: each chunk from
self.runnable.astream(input, config, **kwargs) is yielded
to the caller immediately, with no added latency. In parallel,
chunks are accumulated via + (see :func:_try_add) to
reconstruct a final output. Once the stream is exhausted (in a
finally block, so this also runs if the caller stops
iterating early or the stream raises), one
:class:~zenpyre.records.Record is written for the
accumulated result, unless no chunk could be accumulated at
all (in which case a warning is logged and nothing is
recorded for this call).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
input
|
Input
|
The input to pass to the wrapped runnable. |
required |
config
|
RunnableConfig | None
|
Optional run configuration. If it has a
|
None
|
**kwargs
|
Any
|
Additional keyword arguments forwarded to the
wrapped runnable's |
{}
|
Yields:
| Type | Description |
|---|---|
AsyncIterator[Output]
|
Each output chunk from the wrapped runnable, unchanged and in the same order, as soon as it's produced. |
zenpyre.runnables.RecordingRunnable.batch ¶
batch(
inputs: list[Input],
config: (
RunnableConfig | list[RunnableConfig] | None
) = None,
*,
return_exceptions: bool = False,
**kwargs: Any
) -> list[Output]
Invoke the wrapped runnable on a list of inputs and record each call.
Calls self.runnable.batch(inputs, config, return_exceptions,
**kwargs), then writes one :class:~zenpyre.records.Record
per (input, result) pair to the record store in a single
:meth:~zenpyre.record_stores.base.BaseRecordStore.add_records
call, before returning the results unchanged. Unlike
:meth:invoke, a failed item (when return_exceptions=True)
is still recorded, with "error" set to str(exception)
and "output" set to None.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
inputs
|
list[Input]
|
The list of inputs to pass to the wrapped runnable. |
required |
config
|
RunnableConfig | list[RunnableConfig] | None
|
Optional run configuration, either a single config
shared by every item or a list with one config per
item (matching |
None
|
return_exceptions
|
bool
|
If |
False
|
**kwargs
|
Any
|
Additional keyword arguments forwarded to the
wrapped runnable's |
{}
|
Returns:
| Type | Description |
|---|---|
list[Output]
|
One result per input, in the same order as |
zenpyre.runnables.RecordingRunnable.invoke ¶
invoke(
input: Input,
config: RunnableConfig | None = None,
**kwargs: Any
) -> Output
Invoke the wrapped runnable and record the call.
Calls self.runnable.invoke(input, config, **kwargs),
writes one :class:~zenpyre.records.Record to the record
store capturing input, the returned output, a timestamp,
and any run_id/extra metadata, then returns that
output unchanged.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
input
|
Input
|
The input to pass to the wrapped runnable. |
required |
config
|
RunnableConfig | None
|
Optional run configuration. If it has a
|
None
|
**kwargs
|
Any
|
Additional keyword arguments forwarded to the
wrapped runnable's |
{}
|
Returns:
| Type | Description |
|---|---|
Output
|
The wrapped runnable's output, unchanged. |
Raises:
| Type | Description |
|---|---|
Exception
|
Whatever the wrapped runnable itself raises. In that case, this method does not catch it, so no record is written for the failed call (see the class docstring's Note). |
zenpyre.runnables.RecordingRunnable.stream ¶
stream(
input: Input,
config: RunnableConfig | None = None,
**kwargs: Any
) -> Iterator[Output]
Stream the wrapped runnable's output chunks, recording an accumulated final result once the stream completes.
Each chunk from self.runnable.stream(input, config,
**kwargs) is yielded to the caller immediately, with no
added latency. In parallel, chunks are accumulated via +
(see :func:_try_add) to reconstruct a final output. Once the
stream is exhausted (in a finally block, so this also runs
if the caller stops iterating early or the stream raises), one
:class:~zenpyre.records.Record is written for the
accumulated result, unless no chunk could be accumulated at
all (in which case a warning is logged and nothing is
recorded for this call).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
input
|
Input
|
The input to pass to the wrapped runnable. |
required |
config
|
RunnableConfig | None
|
Optional run configuration. If it has a
|
None
|
**kwargs
|
Any
|
Additional keyword arguments forwarded to the
wrapped runnable's |
{}
|
Yields:
| Type | Description |
|---|---|
Output
|
Each output chunk from the wrapped runnable, unchanged and in the same order, as soon as it's produced. |
zenpyre.runnables.resolve_runnable ¶
resolve_runnable(
runnable: (
Runnable[Input, Output]
| dict[str, Any]
| BaseConfig
),
) -> Runnable[Input, Output]
Resolve a LangChain :class:~langchain_core.runnables.Runnable
instance from an existing object, a configuration dictionary, or a
:class:~zenpyre.utils.config.BaseConfig.
If runnable is already a
:class:~langchain_core.runnables.Runnable instance it is
returned as-is. If it is a :class:dict or a
:class:~zenpyre.utils.config.BaseConfig, it is treated as an
objectory factory configuration and instantiated via
:func:objectory.factory. See
:func:~zenpyre.utils.resolve.resolve_object for details.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
runnable
|
Runnable[Input, Output] | dict[str, Any] | BaseConfig
|
Either a fully configured
:class: |
required |
Returns:
| Type | Description |
|---|---|
Runnable[Input, Output]
|
A configured :class: |
Runnable[Input, Output]
|
instance. |
Raises:
| Type | Description |
|---|---|
TypeError
|
If the resolved object is not a
:class: |
Example
>>> from langchain_core.runnables import Runnable
>>> from zenpyre.runnables import resolve_runnable
>>> class MyRunnable(Runnable):
... def invoke(self, input: Any, config: Any = None, **kwargs: Any) -> Any:
... return input
...
>>> # From an existing instance:
>>> runnable = resolve_runnable(MyRunnable())
>>> # From a configuration dictionary:
>>> runnable = resolve_runnable(
... {"_target_": "langchain_core.runnables.RunnablePassthrough"}
... )
zenpyre.runnables.structured_output_runnable ¶
structured_output_runnable(
chat_model: BaseChatModel,
output_type: type[T],
*,
include_raw: Literal[False] = False,
**kwargs: Any
) -> Runnable[LanguageModelInput, T]
structured_output_runnable(
chat_model: BaseChatModel,
output_type: type[T],
*,
include_raw: Literal[True],
**kwargs: Any
) -> Runnable[LanguageModelInput, dict[str, Any]]
structured_output_runnable(
chat_model: BaseChatModel,
output_type: type[T],
*,
include_raw: bool = False,
**kwargs: Any
) -> (
Runnable[LanguageModelInput, T]
| Runnable[LanguageModelInput, dict[str, Any]]
)
Build a Runnable that returns validated, structured output, with a JSON-parsing fallback.
This composes chat_model.with_structured_output(output_type,
include_raw=True) -- itself a
:class:~langchain_core.runnables.Runnable returning
{"raw": AIMessage, "parsed": T | None, "parsing_error":
Exception | None} -- with a small unwrapping step piped after it
via |.
If the chat model's native structured-output parsing fails (e.g. the model didn't emit a proper tool call, which is common with small or local models), the unwrap step falls back to manually parsing the raw message content as JSON, without making a second LLM call.
include_raw controls both the output shape and the failure
behavior, mirroring with_structured_output's own contract:
include_raw=False(default): invoking returnsTdirectly. If both native parsing and the JSON fallback fail, this raises :class:StructuredOutputError.include_raw=True: invoking returns a dict with the same"raw"/"parsed"/"parsing_error"keys aswith_structured_output(..., include_raw=True), plus a"used_fallback": boolkey. This mode never raises on parse failure, matching the underlying method's own fail-open contract:"parsed"is populated whenever native parsing or the JSON fallback succeeds, and"parsing_error"is only set if both fail.
Because the result is a plain |-composed
:class:~langchain_core.runnables.RunnableSequence, it already
implements invoke, ainvoke, batch, abatch,
stream, astream, and config propagation -- nothing here
reimplements the Runnable interface. batch/abatch in
particular delegate to each step's own batch implementation (so
the chat model's native batching is preserved), and correctly
skip re-processing items that already failed when
return_exceptions=True.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
chat_model
|
BaseChatModel
|
The chat model to wrap. |
required |
output_type
|
type[T]
|
The type (e.g. a Pydantic model) that the LLM output should be parsed into. |
required |
include_raw
|
bool
|
If |
False
|
**kwargs
|
Any
|
Additional keyword arguments forwarded to
|
{}
|
Returns:
| Type | Description |
|---|---|
Runnable[LanguageModelInput, T] | Runnable[LanguageModelInput, dict[str, Any]]
|
A |
Runnable[LanguageModelInput, T] | Runnable[LanguageModelInput, dict[str, Any]]
|
or a |
Runnable[LanguageModelInput, T] | Runnable[LanguageModelInput, dict[str, Any]]
|
|
Example
>>> from pydantic import BaseModel
>>> class Answer(BaseModel):
... value: int
...
>>> chain = structured_output_runnable(chat_model, Answer) # doctest: +SKIP
>>> chain.invoke("What is 2+2?") # doctest: +SKIP
Answer(value=4)
zenpyre.runnables.factory ¶
Contain factories for runnables.
zenpyre.runnables.factory.BaseRunnableFactory ¶
Bases: ABC, Generic[Input, Output]
Abstract base class for Runnable factories.
Subclasses implement :meth:make_runnable to instantiate and
return a configured
:class:~langchain_core.runnables.Runnable object. This
pattern decouples Runnable creation from the rest of the
codebase, making it easy to swap implementations without
changing call sites.
Example
>>> from typing import Any
>>> from langchain_core.runnables import Runnable
>>> from zenpyre.runnables.factory import BaseRunnableFactory
>>> class MyRunnableFactory(BaseRunnableFactory):
... def make_runnable(self) -> Runnable[Any, Any]:
... class MyRunnable(Runnable[Any, Any]):
... def invoke(self, input: Any, config: Any = None, **kwargs: Any) -> Any:
... return input
... return MyRunnable()
...
>>> factory = MyRunnableFactory()
>>> runnable = factory.make_runnable()
zenpyre.runnables.factory.BaseRunnableFactory.make_runnable
abstractmethod
¶
make_runnable() -> Runnable[Input, Output]
Create and return a configured Runnable instance.
Returns:
| Name | Type | Description |
|---|---|---|
A |
Runnable[Input, Output]
|
class: |
Runnable[Input, Output]
|
instance ready for use. |
zenpyre.runnables.factory.ConfigurableRunnableFactory ¶
Bases: BaseRunnableFactory[Input, Output], MultilineDisplayMixin
A concrete Runnable factory that accepts either a pre-built
:class:~langchain_core.runnables.Runnable instance or a
configuration dictionary.
When a dict is provided it is resolved at each :meth:make_runnable
call via :func:~zenpyre.runnables.resolve_runnable, which uses
objectory to instantiate the configured class. When an
instance is provided it is returned as-is.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
runnable
|
Runnable[Input, Output] | dict[str, Any]
|
A fully configured
:class: |
required |
Example
>>> from typing import Any
>>> from langchain_core.runnables import Runnable
>>> from zenpyre.runnables.factory import ConfigurableRunnableFactory
>>> class MyRunnable(Runnable[Any, Any]):
... def invoke(self, input: Any, config: Any = None, **kwargs: Any) -> Any:
... return input
...
>>> factory = ConfigurableRunnableFactory(MyRunnable())
>>> runnable = factory.make_runnable()
zenpyre.runnables.factory.RunnableFactory ¶
Bases: BaseRunnableFactory[Input, Output], MultilineDisplayMixin
A concrete Runnable factory that wraps a pre-built
:class:~langchain_core.runnables.Runnable instance.
Use this when the runnable is already instantiated and you
simply want to wrap it in the :class:~BaseRunnableFactory
interface — for example, when injecting a fixed runnable into a
component that expects a factory.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
runnable
|
Runnable[Input, Output]
|
A fully configured
:class: |
required |
Example
>>> from typing import Any
>>> from langchain_core.runnables import Runnable
>>> from zenpyre.runnables.factory import RunnableFactory
>>> class MyRunnable(Runnable[Any, Any]):
... def invoke(self, input: Any, config: Any = None, **kwargs: Any) -> Any:
... return input
...
>>> factory = RunnableFactory(MyRunnable())
>>> runnable = factory.make_runnable()