libs/core/tests/unit_tests/runnables/test_runnable.py PYTHON 6,013 lines View on github.com → Search inside
File is large — showing lines 1–2,000 of 6,013.
1import asyncio2import re3import sys4import time5import uuid6import warnings7from collections.abc import (8    AsyncIterator,9    Awaitable,10    Callable,11    Iterator,12    Sequence,13)14from functools import partial15from operator import itemgetter16from typing import Any, cast17from uuid import UUID1819import pytest20from freezegun import freeze_time21from packaging import version22from pydantic import BaseModel, Field, ValidationError23from pydantic.v1 import BaseModel as BaseModelV124from pydantic.v1 import Field as FieldV125from pydantic.v1 import ValidationError as ValidationErrorV126from pytest_mock import MockerFixture27from syrupy.assertion import SnapshotAssertion28from typing_extensions import TypedDict, override2930from langchain_core.callbacks import BaseCallbackHandler31from langchain_core.callbacks.manager import (32    AsyncCallbackManagerForRetrieverRun,33    CallbackManagerForRetrieverRun,34    atrace_as_chain_group,35    trace_as_chain_group,36)37from langchain_core.documents import Document38from langchain_core.language_models import (39    FakeListChatModel,40    FakeListLLM,41    FakeStreamingListLLM,42)43from langchain_core.language_models.fake_chat_models import GenericFakeChatModel44from langchain_core.load import dumpd, dumps45from langchain_core.load.load import loads46from langchain_core.messages import AIMessageChunk, HumanMessage, SystemMessage47from langchain_core.messages.base import BaseMessage48from langchain_core.output_parsers import (49    BaseOutputParser,50    CommaSeparatedListOutputParser,51    StrOutputParser,52)53from langchain_core.outputs.chat_generation import ChatGeneration54from langchain_core.outputs.llm_result import LLMResult55from langchain_core.prompt_values import ChatPromptValue, StringPromptValue56from langchain_core.prompts import (57    ChatPromptTemplate,58    HumanMessagePromptTemplate,59    MessagesPlaceholder,60    PromptTemplate,61    SystemMessagePromptTemplate,62)63from langchain_core.retrievers import BaseRetriever64from langchain_core.runnables import (65    AddableDict,66    ConfigurableField,67    ConfigurableFieldMultiOption,68    ConfigurableFieldSingleOption,69    RouterRunnable,70    Runnable,71    RunnableAssign,72    RunnableBinding,73    RunnableBranch,74    RunnableConfig,75    RunnableGenerator,76    RunnableLambda,77    RunnableParallel,78    RunnablePassthrough,79    RunnablePick,80    RunnableSequence,81    add,82    chain,83)84from langchain_core.runnables.base import RunnableMap, RunnableSerializable85from langchain_core.runnables.utils import Input, Output86from langchain_core.tools import BaseTool, tool87from langchain_core.tracers import (88    BaseTracer,89    ConsoleCallbackHandler,90    Run,91    RunLog,92    RunLogPatch,93)94from langchain_core.tracers._compat import pydantic_copy95from langchain_core.tracers.context import collect_runs96from langchain_core.utils.pydantic import (97    PYDANTIC_VERSION,98    TypeBaseModel,99    model_validate,100)101from langchain_core.version import VERSION102from tests.unit_tests.pydantic_utils import (103    _normalize_schema,104    _schema,105    skip_if_no_pydantic_v1,106)107from tests.unit_tests.stubs import AnyStr, _any_id_ai_message, _any_id_ai_message_chunk108109# Several tests assert the legacy `RunLog` / `RunLogPatch` output produced by110# `astream_log`, which cannot be replaced by `astream` without losing coverage.111pytestmark = pytest.mark.filterwarnings(112    "ignore:astream_log is deprecated. Use astream instead.:"113    "langchain_core._api.deprecation.LangChainDeprecationWarning"114)115116PYDANTIC_VERSION_AT_LEAST_29 = version.parse("2.9") <= PYDANTIC_VERSION117PYDANTIC_VERSION_AT_LEAST_210 = version.parse("2.10") <= PYDANTIC_VERSION118119120def _normalize_lc_version(value: str) -> str:121    return value.replace(122        f"'langchain-core': '{VERSION}'",123        "'langchain-core': '<version>'",124    )125126127class FakeTracer(BaseTracer):128    """Fake tracer that records LangChain execution.129130    It replaces run IDs with deterministic UUIDs for snapshotting.131    """132133    def __init__(self) -> None:134        """Initialize the tracer."""135        super().__init__()136        self.runs: list[Run] = []137        self.uuids_map: dict[UUID, UUID] = {}138        self.uuids_generator = (139            UUID(f"00000000-0000-4000-8000-{i:012}", version=4) for i in range(10000)140        )141142    def _replace_uuid(self, uuid: UUID) -> UUID:143        if uuid not in self.uuids_map:144            self.uuids_map[uuid] = next(self.uuids_generator)145        return self.uuids_map[uuid]146147    def _replace_message_id(self, maybe_message: Any) -> Any:148        if isinstance(maybe_message, BaseMessage):149            maybe_message.id = str(next(self.uuids_generator))150        if isinstance(maybe_message, ChatGeneration):151            maybe_message.message.id = str(next(self.uuids_generator))152        if isinstance(maybe_message, LLMResult):153            for i, gen_list in enumerate(maybe_message.generations):154                for j, gen in enumerate(gen_list):155                    maybe_message.generations[i][j] = self._replace_message_id(gen)156        if isinstance(maybe_message, dict):157            for k, v in maybe_message.items():158                maybe_message[k] = self._replace_message_id(v)159        if isinstance(maybe_message, list):160            for i, v in enumerate(maybe_message):161                maybe_message[i] = self._replace_message_id(v)162163        return maybe_message164165    def _copy_run(self, run: Run) -> Run:166        if run.dotted_order:167            levels = run.dotted_order.split(".")168            processed_levels = []169            for level in levels:170                timestamp, run_id = level.split("Z")171                new_run_id = self._replace_uuid(UUID(run_id))172                processed_level = f"{timestamp}Z{new_run_id}"173                processed_levels.append(processed_level)174            new_dotted_order = ".".join(processed_levels)175        else:176            new_dotted_order = None177        update_dict = {178            "id": self._replace_uuid(run.id),179            "parent_run_id": (180                self.uuids_map[run.parent_run_id] if run.parent_run_id else None181            ),182            "child_runs": [self._copy_run(child) for child in run.child_runs],183            "trace_id": self._replace_uuid(run.trace_id) if run.trace_id else None,184            "dotted_order": new_dotted_order,185            "inputs": self._replace_message_id(run.inputs),186            "outputs": self._replace_message_id(run.outputs),187        }188        return pydantic_copy(run, update=update_dict)189190    def _persist_run(self, run: Run) -> None:191        """Persist a run."""192        self.runs.append(self._copy_run(run))193194    def flattened_runs(self) -> list[Run]:195        q = [*self.runs]196        result = []197        while q:198            parent = q.pop()199            result.append(parent)200            if parent.child_runs:201                q.extend(parent.child_runs)202        return result203204    @property205    def run_ids(self) -> list[uuid.UUID | None]:206        runs = self.flattened_runs()207        uuids_map = {v: k for k, v in self.uuids_map.items()}208        return [uuids_map.get(r.id) for r in runs]209210211class FakeRunnable(Runnable[str, int]):212    @override213    def invoke(214        self,215        input: str,216        config: RunnableConfig | None = None,217        **kwargs: Any,218    ) -> int:219        return len(input)220221222class FakeRunnableSerializable(RunnableSerializable[str, int]):223    hello: str = ""224225    @override226    def invoke(227        self,228        input: str,229        config: RunnableConfig | None = None,230        **kwargs: Any,231    ) -> int:232        return len(input)233234235class FakeRetriever(BaseRetriever):236    @override237    def _get_relevant_documents(238        self, query: str, *, run_manager: CallbackManagerForRetrieverRun239    ) -> list[Document]:240        return [Document(page_content="foo"), Document(page_content="bar")]241242    @override243    async def _aget_relevant_documents(244        self, query: str, *, run_manager: AsyncCallbackManagerForRetrieverRun245    ) -> list[Document]:246        return [Document(page_content="foo"), Document(page_content="bar")]247248249@pytest.mark.skipif(250    PYDANTIC_VERSION_AT_LEAST_210,251    reason=(252        "Only test with most recent version of pydantic. "253        "Pydantic introduced small fixes to generated JSONSchema on minor versions."254    ),255)256def test_schemas(snapshot: SnapshotAssertion) -> None:257    fake = FakeRunnable()  # str -> int258259    assert fake.get_input_jsonschema() == {260        "title": "FakeRunnableInput",261        "type": "string",262    }263    assert fake.get_output_jsonschema() == {264        "title": "FakeRunnableOutput",265        "type": "integer",266    }267    assert fake.get_config_jsonschema(include=["tags", "metadata", "run_name"]) == {268        "properties": {269            "metadata": {270                "default": None,271                "title": "Metadata",272                "type": "object",273            },274            "run_name": {"default": None, "title": "Run Name", "type": "string"},275            "tags": {276                "default": None,277                "items": {"type": "string"},278                "title": "Tags",279                "type": "array",280            },281        },282        "title": "FakeRunnableConfig",283        "type": "object",284    }285286    fake_bound = FakeRunnable().bind(a="b")  # str -> int287288    assert fake_bound.get_input_jsonschema() == {289        "title": "FakeRunnableInput",290        "type": "string",291    }292    assert fake_bound.get_output_jsonschema() == {293        "title": "FakeRunnableOutput",294        "type": "integer",295    }296297    fake_w_fallbacks = FakeRunnable().with_fallbacks((fake,))  # str -> int298299    assert fake_w_fallbacks.get_input_jsonschema() == {300        "title": "FakeRunnableInput",301        "type": "string",302    }303    assert fake_w_fallbacks.get_output_jsonschema() == {304        "title": "FakeRunnableOutput",305        "type": "integer",306    }307308    def typed_lambda_impl(x: str) -> int:309        return len(x)310311    typed_lambda = RunnableLambda(typed_lambda_impl)  # str -> int312313    assert typed_lambda.get_input_jsonschema() == {314        "title": "typed_lambda_impl_input",315        "type": "string",316    }317    assert typed_lambda.get_output_jsonschema() == {318        "title": "typed_lambda_impl_output",319        "type": "integer",320    }321322    async def typed_async_lambda_impl(x: str) -> int:323        return len(x)324325    typed_async_lambda = RunnableLambda(typed_async_lambda_impl)  # str -> int326327    assert typed_async_lambda.get_input_jsonschema() == {328        "title": "typed_async_lambda_impl_input",329        "type": "string",330    }331    assert typed_async_lambda.get_output_jsonschema() == {332        "title": "typed_async_lambda_impl_output",333        "type": "integer",334    }335336    fake_ret = FakeRetriever()  # str -> list[Document]337338    assert fake_ret.get_input_jsonschema() == {339        "title": "FakeRetrieverInput",340        "type": "string",341    }342    assert _normalize_schema(fake_ret.get_output_jsonschema()) == {343        "$defs": {344            "Document": {345                "description": "Class for storing a piece of text and "346                "associated metadata.\n"347                "\n"348                "!!! note\n"349                "\n"350                "    `Document` is for **retrieval workflows**, not chat I/O. For "351                "sending text\n"352                "    to an LLM in a conversation, use message types from "353                "`langchain.messages`.\n"354                "\n"355                "Example:\n"356                "    ```python\n"357                "    from langchain_core.documents import Document\n"358                "\n"359                "    document = Document(\n"360                '        page_content="Hello, world!", '361                'metadata={"source": "https://example.com"}\n'362                "    )\n"363                "    ```",364                "properties": {365                    "id": {366                        "anyOf": [{"type": "string"}, {"type": "null"}],367                        "default": None,368                        "title": "Id",369                    },370                    "metadata": {"title": "Metadata", "type": "object"},371                    "page_content": {"title": "Page Content", "type": "string"},372                    "type": {373                        "const": "Document",374                        "default": "Document",375                        "title": "Type",376                    },377                },378                "required": ["page_content"],379                "title": "Document",380                "type": "object",381            }382        },383        "items": {"$ref": "#/$defs/Document"},384        "title": "FakeRetrieverOutput",385        "type": "array",386    }387388    fake_llm = FakeListLLM(responses=["a"])  # str -> list[list[str]]389390    assert _schema(fake_llm.input_schema) == snapshot(name="fake_llm_input_schema")391    assert _schema(fake_llm.output_schema) == {392        "title": "FakeListLLMOutput",393        "type": "string",394    }395396    fake_chat = FakeListChatModel(responses=["a"])  # str -> list[list[str]]397398    assert _schema(fake_chat.input_schema) == snapshot(name="fake_chat_input_schema")399    assert _schema(fake_chat.output_schema) == snapshot(name="fake_chat_output_schema")400401    chat_prompt = ChatPromptTemplate.from_messages(402        [403            MessagesPlaceholder(variable_name="history"),404            ("human", "Hello, how are you?"),405        ]406    )407408    assert _normalize_schema(chat_prompt.get_input_jsonschema()) == snapshot(409        name="chat_prompt_input_schema"410    )411    assert _normalize_schema(chat_prompt.get_output_jsonschema()) == snapshot(412        name="chat_prompt_output_schema"413    )414415    prompt = PromptTemplate.from_template("Hello, {name}!")416417    assert prompt.get_input_jsonschema() == {418        "title": "PromptInput",419        "type": "object",420        "properties": {"name": {"title": "Name", "type": "string"}},421        "required": ["name"],422    }423    assert _schema(prompt.output_schema) == snapshot(name="prompt_output_schema")424425    prompt_mapper = PromptTemplate.from_template("Hello, {name}!").map()426427    assert _normalize_schema(prompt_mapper.get_input_jsonschema()) == {428        "$defs": {429            "PromptInput": {430                "properties": {"name": {"title": "Name", "type": "string"}},431                "required": ["name"],432                "title": "PromptInput",433                "type": "object",434            }435        },436        "default": None,437        "items": {"$ref": "#/$defs/PromptInput"},438        "title": "RunnableEach<PromptTemplate>Input",439        "type": "array",440    }441    assert _schema(prompt_mapper.output_schema) == snapshot(442        name="prompt_mapper_output_schema"443    )444445    list_parser = CommaSeparatedListOutputParser()446447    assert _schema(list_parser.input_schema) == snapshot(448        name="list_parser_input_schema"449    )450    assert _schema(list_parser.output_schema) == {451        "title": "CommaSeparatedListOutputParserOutput",452        "type": "array",453        "items": {"type": "string"},454    }455456    seq = prompt | fake_llm | list_parser457458    assert seq.get_input_jsonschema() == {459        "title": "PromptInput",460        "type": "object",461        "properties": {"name": {"title": "Name", "type": "string"}},462        "required": ["name"],463    }464    assert seq.get_output_jsonschema() == {465        "type": "array",466        "items": {"type": "string"},467        "title": "CommaSeparatedListOutputParserOutput",468    }469470    router = RouterRunnable[Any]({})471472    assert _schema(router.input_schema) == {473        "$ref": "#/definitions/RouterInput",474        "definitions": {475            "RouterInput": {476                "description": "Router input.",477                "properties": {478                    "input": {"title": "Input"},479                    "key": {"title": "Key", "type": "string"},480                },481                "required": ["key", "input"],482                "title": "RouterInput",483                "type": "object",484            }485        },486        "title": "RouterRunnableInput",487    }488    assert router.get_output_jsonschema() == {"title": "RouterRunnableOutput"}489490    seq_w_map = (491        prompt492        | fake_llm493        | {494            "original": RunnablePassthrough(input_type=str),495            "as_list": list_parser,496            "length": typed_lambda_impl,497        }498    )499500    assert seq_w_map.get_input_jsonschema() == {501        "title": "PromptInput",502        "type": "object",503        "properties": {"name": {"title": "Name", "type": "string"}},504        "required": ["name"],505    }506    assert seq_w_map.get_output_jsonschema() == {507        "title": "RunnableParallel<original,as_list,length>Output",508        "type": "object",509        "properties": {510            "original": {"title": "Original", "type": "string"},511            "length": {"title": "Length", "type": "integer"},512            "as_list": {513                "title": "As List",514                "type": "array",515                "items": {"type": "string"},516            },517        },518        "required": ["original", "as_list", "length"],519    }520521    # Add a test for schema of runnable assign522    def foo(x: int) -> int:523        return x524525    foo_ = RunnableLambda(foo)526527    assert foo_.assign(bar=lambda _: "foo").get_output_jsonschema() == {528        "properties": {"bar": {"title": "Bar"}, "root": {"title": "Root"}},529        "required": ["root", "bar"],530        "title": "RunnableAssignOutput",531        "type": "object",532    }533534535def test_passthrough_assign_schema() -> None:536    retriever = FakeRetriever()  # str -> list[Document]537    prompt = PromptTemplate.from_template("{context} {question}")538    fake_llm = FakeListLLM(responses=["a"])  # str -> list[list[str]]539540    seq_w_assign = (541        RunnablePassthrough.assign(context=itemgetter("question") | retriever)542        | prompt543        | fake_llm544    )545546    assert seq_w_assign.get_input_jsonschema() == {547        "properties": {"question": {"title": "Question", "type": "string"}},548        "title": "RunnableSequenceInput",549        "type": "object",550        "required": ["question"],551    }552    assert seq_w_assign.get_output_jsonschema() == {553        "title": "FakeListLLMOutput",554        "type": "string",555    }556557    invalid_seq_w_assign = (558        RunnablePassthrough.assign(context=itemgetter("question") | retriever)559        | fake_llm  # type: ignore[operator]560    )561562    # fallback to RunnableAssign.input_schema if next runnable doesn't have563    # expected dict input_schema564    assert invalid_seq_w_assign.get_input_jsonschema() == {565        "properties": {"question": {"title": "Question"}},566        "title": "RunnableParallel<context>Input",567        "type": "object",568        "required": ["question"],569    }570571572def test_lambda_schemas(snapshot: SnapshotAssertion) -> None:573    first_lambda = lambda x: x["hello"]  # noqa: E731574    assert RunnableLambda(first_lambda).get_input_jsonschema() == {575        "title": "RunnableLambdaInput",576        "type": "object",577        "properties": {"hello": {"title": "Hello"}},578        "required": ["hello"],579    }580581    second_lambda = lambda x, y: (x["hello"], x["bye"], y["bah"])  # noqa: E731582    assert RunnableLambda(second_lambda).get_input_jsonschema() == {583        "title": "RunnableLambdaInput",584        "type": "object",585        "properties": {"hello": {"title": "Hello"}, "bye": {"title": "Bye"}},586        "required": ["bye", "hello"],587    }588589    def get_value(value):  # type: ignore[no-untyped-def] # noqa: ANN001,ANN202590        return value["variable_name"]591592    assert RunnableLambda(get_value).get_input_jsonschema() == {593        "title": "get_value_input",594        "type": "object",595        "properties": {"variable_name": {"title": "Variable Name"}},596        "required": ["variable_name"],597    }598599    async def aget_value(value):  # type: ignore[no-untyped-def] # noqa: ANN001,ANN202600        return (value["variable_name"], value.get("another"))601602    assert RunnableLambda(aget_value).get_input_jsonschema() == {603        "title": "aget_value_input",604        "type": "object",605        "properties": {606            "another": {"title": "Another"},607            "variable_name": {"title": "Variable Name"},608        },609        "required": ["another", "variable_name"],610    }611612    async def aget_values(value):  # type: ignore[no-untyped-def] # noqa: ANN001,ANN202613        return {614            "hello": value["variable_name"],615            "bye": value["variable_name"],616            "byebye": value["yo"],617        }618619    assert RunnableLambda(aget_values).get_input_jsonschema() == {620        "title": "aget_values_input",621        "type": "object",622        "properties": {623            "variable_name": {"title": "Variable Name"},624            "yo": {"title": "Yo"},625        },626        "required": ["variable_name", "yo"],627    }628629    class InputType(TypedDict):630        variable_name: str631        yo: int632633    class OutputType(TypedDict):634        hello: str635        bye: str636        byebye: int637638    async def aget_values_typed(value: InputType) -> OutputType:639        return {640            "hello": value["variable_name"],641            "bye": value["variable_name"],642            "byebye": value["yo"],643        }644645    assert _normalize_schema(646        RunnableLambda(aget_values_typed).get_input_jsonschema()647    ) == _normalize_schema(648        {649            "$defs": {650                "InputType": {651                    "properties": {652                        "variable_name": {653                            "title": "Variable Name",654                            "type": "string",655                        },656                        "yo": {"title": "Yo", "type": "integer"},657                    },658                    "required": ["variable_name", "yo"],659                    "title": "InputType",660                    "type": "object",661                }662            },663            "allOf": [{"$ref": "#/$defs/InputType"}],664            "title": "aget_values_typed_input",665        }666    )667668    if PYDANTIC_VERSION_AT_LEAST_29:669        assert _normalize_schema(670            RunnableLambda(aget_values_typed).get_output_jsonschema()671        ) == snapshot(name="schema8")672673674def test_with_types_with_type_generics() -> None:675    """Verify that with_types works if we use things like list[int]."""676677    def foo(x: int) -> None:678        """Add one to the input."""679        raise NotImplementedError680681    # Try specifying some682    RunnableLambda(foo).with_types(683        output_type=list[int],  # type: ignore[arg-type]684        input_type=list[int],  # type: ignore[arg-type]685    )686    RunnableLambda(foo).with_types(687        output_type=Sequence[int],  # type: ignore[arg-type]688        input_type=Sequence[int],  # type: ignore[arg-type]689    )690691692def test_schema_with_itemgetter() -> None:693    """Test runnable with itemgetter."""694    foo = RunnableLambda(itemgetter("hello"))695    assert _schema(foo.input_schema) == {696        "properties": {"hello": {"title": "Hello"}},697        "required": ["hello"],698        "title": "RunnableLambdaInput",699        "type": "object",700    }701    prompt = ChatPromptTemplate.from_template("what is {language}?")702    chain = {"language": itemgetter("language")} | prompt703    assert _schema(chain.input_schema) == {704        "properties": {"language": {"title": "Language"}},705        "required": ["language"],706        "title": "RunnableParallel<language>Input",707        "type": "object",708    }709710711def test_schema_complex_seq() -> None:712    prompt1 = ChatPromptTemplate.from_template("what is the city {person} is from?")713    prompt2 = ChatPromptTemplate.from_template(714        "what country is the city {city} in? respond in {language}"715    )716717    model = FakeListChatModel(responses=[""])718719    chain1 = RunnableSequence[dict[str, Any], str](720        prompt1, model, StrOutputParser(), name="city_chain"721    )722723    assert chain1.name == "city_chain"724725    chain2 = (726        {"city": chain1, "language": itemgetter("language")}727        | prompt2728        | model729        | StrOutputParser()730    )731732    assert chain2.get_input_jsonschema() == {733        "title": "RunnableParallel<city,language>Input",734        "type": "object",735        "properties": {736            "person": {"title": "Person", "type": "string"},737            "language": {"title": "Language"},738        },739        "required": ["person", "language"],740    }741742    assert chain2.get_output_jsonschema() == {743        "title": "StrOutputParserOutput",744        "type": "string",745    }746747    assert chain2.with_types(input_type=str).get_input_jsonschema() == {748        "title": "RunnableSequenceInput",749        "type": "string",750    }751752    assert chain2.with_types(input_type=int).get_output_jsonschema() == {753        "title": "StrOutputParserOutput",754        "type": "string",755    }756757    class InputType(BaseModel):758        person: str759760    assert chain2.with_types(input_type=InputType).get_input_jsonschema() == {761        "title": "InputType",762        "type": "object",763        "properties": {"person": {"title": "Person", "type": "string"}},764        "required": ["person"],765    }766767768def test_configurable_fields(snapshot: SnapshotAssertion) -> None:769    fake_llm = FakeListLLM(responses=["a"])  # str -> list[list[str]]770771    assert fake_llm.invoke("...") == "a"772773    fake_llm_configurable = fake_llm.configurable_fields(774        responses=ConfigurableField(775            id="llm_responses",776            name="LLM Responses",777            description="A list of fake responses for this LLM",778        )779    )780781    assert fake_llm_configurable.invoke("...") == "a"782783    if PYDANTIC_VERSION_AT_LEAST_29:784        assert _normalize_schema(785            fake_llm_configurable.get_config_jsonschema()786        ) == snapshot(name="schema2")787788    fake_llm_configured = fake_llm_configurable.with_config(789        configurable={"llm_responses": ["b"]}790    )791792    assert fake_llm_configured.invoke("...") == "b"793794    prompt = PromptTemplate.from_template("Hello, {name}!")795796    assert prompt.invoke({"name": "John"}) == StringPromptValue(text="Hello, John!")797798    prompt_configurable = prompt.configurable_fields(799        template=ConfigurableField(800            id="prompt_template",801            name="Prompt Template",802            description="The prompt template for this chain",803        )804    )805806    assert prompt_configurable.invoke({"name": "John"}) == StringPromptValue(807        text="Hello, John!"808    )809810    if PYDANTIC_VERSION_AT_LEAST_29:811        assert _normalize_schema(812            prompt_configurable.get_config_jsonschema()813        ) == snapshot(name="schema3")814815    prompt_configured = prompt_configurable.with_config(816        configurable={"prompt_template": "Hello, {name}! {name}!"}817    )818819    assert prompt_configured.invoke({"name": "John"}) == StringPromptValue(820        text="Hello, John! John!"821    )822823    assert prompt_configurable.with_config(824        configurable={"prompt_template": "Hello {name} in {lang}"}825    ).get_input_jsonschema() == {826        "title": "PromptInput",827        "type": "object",828        "properties": {829            "lang": {"title": "Lang", "type": "string"},830            "name": {"title": "Name", "type": "string"},831        },832        "required": ["lang", "name"],833    }834835    chain_configurable = prompt_configurable | fake_llm_configurable | StrOutputParser()836837    assert chain_configurable.invoke({"name": "John"}) == "a"838839    if PYDANTIC_VERSION_AT_LEAST_29:840        assert _normalize_schema(841            chain_configurable.get_config_jsonschema()842        ) == snapshot(name="schema4")843844    assert (845        chain_configurable.with_config(846            configurable={847                "prompt_template": "A very good morning to you, {name} {lang}!",848                "llm_responses": ["c"],849            }850        ).invoke({"name": "John", "lang": "en"})851        == "c"852    )853854    assert chain_configurable.with_config(855        configurable={856            "prompt_template": "A very good morning to you, {name} {lang}!",857            "llm_responses": ["c"],858        }859    ).get_input_jsonschema() == {860        "title": "PromptInput",861        "type": "object",862        "properties": {863            "lang": {"title": "Lang", "type": "string"},864            "name": {"title": "Name", "type": "string"},865        },866        "required": ["lang", "name"],867    }868869    chain_with_map_configurable = prompt_configurable | {870        "llm1": fake_llm_configurable | StrOutputParser(),871        "llm2": fake_llm_configurable | StrOutputParser(),872        "llm3": fake_llm.configurable_fields(873            responses=ConfigurableField("other_responses")874        )875        | StrOutputParser(),876    }877878    assert chain_with_map_configurable.invoke({"name": "John"}) == {879        "llm1": "a",880        "llm2": "a",881        "llm3": "a",882    }883884    if PYDANTIC_VERSION_AT_LEAST_29:885        assert _normalize_schema(886            chain_with_map_configurable.get_config_jsonschema()887        ) == snapshot(name="schema5")888889    assert chain_with_map_configurable.with_config(890        configurable={891            "prompt_template": "A very good morning to you, {name}!",892            "llm_responses": ["c"],893            "other_responses": ["d"],894        }895    ).invoke({"name": "John"}) == {"llm1": "c", "llm2": "c", "llm3": "d"}896897898def test_configurable_alts_factory() -> None:899    fake_llm = FakeListLLM(responses=["a"]).configurable_alternatives(900        ConfigurableField(id="llm", name="LLM"),901        chat=partial(FakeListLLM, responses=["b"]),902    )903904    assert fake_llm.invoke("...") == "a"905906    assert fake_llm.with_config(configurable={"llm": "chat"}).invoke("...") == "b"907908909def test_configurable_fields_prefix_keys(snapshot: SnapshotAssertion) -> None:910    fake_chat = FakeListChatModel(responses=["b"]).configurable_fields(911        responses=ConfigurableFieldMultiOption(912            id="responses",913            name="Chat Responses",914            options={915                "hello": "A good morning to you!",916                "bye": "See you later!",917                "helpful": "How can I help you?",918            },919            default=["hello", "bye"],920        ),921        # (sleep is a configurable field in FakeListChatModel)922        sleep=ConfigurableField(923            id="chat_sleep",924            is_shared=True,925        ),926    )927    fake_llm = (928        FakeListLLM(responses=["a"])929        .configurable_fields(930            responses=ConfigurableField(931                id="responses",932                name="LLM Responses",933                description="A list of fake responses for this LLM",934            )935        )936        .configurable_alternatives(937            ConfigurableField(id="llm", name="LLM"),938            chat=fake_chat | StrOutputParser(),939            prefix_keys=True,940        )941    )942    prompt = PromptTemplate.from_template("Hello, {name}!").configurable_fields(943        template=ConfigurableFieldSingleOption(944            id="prompt_template",945            name="Prompt Template",946            description="The prompt template for this chain",947            options={948                "hello": "Hello, {name}!",949                "good_morning": "A very good morning to you, {name}!",950            },951            default="hello",952        )953    )954955    chain = prompt | fake_llm956957    if PYDANTIC_VERSION_AT_LEAST_29:958        assert _normalize_schema(_schema(chain.config_schema())) == snapshot(959            name="schema6"960        )961962963def test_configurable_fields_example(snapshot: SnapshotAssertion) -> None:964    fake_chat = FakeListChatModel(responses=["b"]).configurable_fields(965        responses=ConfigurableFieldMultiOption(966            id="chat_responses",967            name="Chat Responses",968            options={969                "hello": "A good morning to you!",970                "bye": "See you later!",971                "helpful": "How can I help you?",972            },973            default=["hello", "bye"],974        )975    )976    fake_llm = (977        FakeListLLM(responses=["a"])978        .configurable_fields(979            responses=ConfigurableField(980                id="llm_responses",981                name="LLM Responses",982                description="A list of fake responses for this LLM",983            )984        )985        .configurable_alternatives(986            ConfigurableField(id="llm", name="LLM"),987            chat=fake_chat | StrOutputParser(),988        )989    )990991    prompt = PromptTemplate.from_template("Hello, {name}!").configurable_fields(992        template=ConfigurableFieldSingleOption(993            id="prompt_template",994            name="Prompt Template",995            description="The prompt template for this chain",996            options={997                "hello": "Hello, {name}!",998                "good_morning": "A very good morning to you, {name}!",999            },1000            default="hello",1001        )1002    )10031004    # deduplication of configurable fields1005    chain_configurable = prompt | fake_llm | (lambda x: {"name": x}) | prompt | fake_llm10061007    assert chain_configurable.invoke({"name": "John"}) == "a"10081009    if PYDANTIC_VERSION_AT_LEAST_29:1010        assert _normalize_schema(1011            chain_configurable.get_config_jsonschema()1012        ) == snapshot(name="schema7")10131014    assert (1015        chain_configurable.with_config(configurable={"llm": "chat"}).invoke(1016            {"name": "John"}1017        )1018        == "A good morning to you!"1019    )10201021    assert (1022        chain_configurable.with_config(1023            configurable={"llm": "chat", "chat_responses": ["helpful"]}1024        ).invoke({"name": "John"})1025        == "How can I help you?"1026    )102710281029def test_passthrough_tap(mocker: MockerFixture) -> None:1030    fake = FakeRunnable()1031    mock = mocker.Mock()10321033    seq = RunnablePassthrough[Any](mock) | fake | RunnablePassthrough[Any](mock)10341035    assert seq.invoke("hello", my_kwarg="value") == 51036    assert mock.call_args_list == [1037        mocker.call("hello", my_kwarg="value"),1038        mocker.call(5),1039    ]1040    mock.reset_mock()10411042    assert seq.batch(["hello", "byebye"], my_kwarg="value") == [5, 6]1043    assert len(mock.call_args_list) == 41044    for call in [1045        mocker.call("hello", my_kwarg="value"),1046        mocker.call("byebye", my_kwarg="value"),1047        mocker.call(5),1048        mocker.call(6),1049    ]:1050        assert call in mock.call_args_list1051    mock.reset_mock()10521053    assert seq.batch(["hello", "byebye"], my_kwarg="value", return_exceptions=True) == [1054        5,1055        6,1056    ]1057    assert len(mock.call_args_list) == 41058    for call in [1059        mocker.call("hello", my_kwarg="value"),1060        mocker.call("byebye", my_kwarg="value"),1061        mocker.call(5),1062        mocker.call(6),1063    ]:1064        assert call in mock.call_args_list1065    mock.reset_mock()10661067    assert sorted(1068        a1069        for a in seq.batch_as_completed(1070            ["hello", "byebye"], my_kwarg="value", return_exceptions=True1071        )1072    ) == [1073        (0, 5),1074        (1, 6),1075    ]1076    assert len(mock.call_args_list) == 41077    for call in [1078        mocker.call("hello", my_kwarg="value"),1079        mocker.call("byebye", my_kwarg="value"),1080        mocker.call(5),1081        mocker.call(6),1082    ]:1083        assert call in mock.call_args_list1084    mock.reset_mock()10851086    assert list(1087        seq.stream("hello", {"metadata": {"key": "value"}}, my_kwarg="value")1088    ) == [5]1089    assert mock.call_args_list == [1090        mocker.call("hello", my_kwarg="value"),1091        mocker.call(5),1092    ]1093    mock.reset_mock()109410951096async def test_passthrough_tap_async(mocker: MockerFixture) -> None:1097    fake = FakeRunnable()1098    mock = mocker.Mock()10991100    seq = RunnablePassthrough[Any](mock) | fake | RunnablePassthrough[Any](mock)11011102    assert await seq.ainvoke("hello", my_kwarg="value") == 51103    assert mock.call_args_list == [1104        mocker.call("hello", my_kwarg="value"),1105        mocker.call(5),1106    ]1107    mock.reset_mock()11081109    assert await seq.abatch(["hello", "byebye"], my_kwarg="value") == [5, 6]1110    assert len(mock.call_args_list) == 41111    for call in [1112        mocker.call("hello", my_kwarg="value"),1113        mocker.call("byebye", my_kwarg="value"),1114        mocker.call(5),1115        mocker.call(6),1116    ]:1117        assert call in mock.call_args_list1118    mock.reset_mock()11191120    assert await seq.abatch(1121        ["hello", "byebye"], my_kwarg="value", return_exceptions=True1122    ) == [1123        5,1124        6,1125    ]1126    assert len(mock.call_args_list) == 41127    for call in [1128        mocker.call("hello", my_kwarg="value"),1129        mocker.call("byebye", my_kwarg="value"),1130        mocker.call(5),1131        mocker.call(6),1132    ]:1133        assert call in mock.call_args_list1134    mock.reset_mock()11351136    assert sorted(1137        [1138            a1139            async for a in seq.abatch_as_completed(1140                ["hello", "byebye"], my_kwarg="value", return_exceptions=True1141            )1142        ]1143    ) == [1144        (0, 5),1145        (1, 6),1146    ]1147    assert len(mock.call_args_list) == 41148    for call in [1149        mocker.call("hello", my_kwarg="value"),1150        mocker.call("byebye", my_kwarg="value"),1151        mocker.call(5),1152        mocker.call(6),1153    ]:1154        assert call in mock.call_args_list1155    mock.reset_mock()11561157    assert [1158        part1159        async for part in seq.astream(1160            "hello", {"metadata": {"key": "value"}}, my_kwarg="value"1161        )1162    ] == [5]1163    assert mock.call_args_list == [1164        mocker.call("hello", my_kwarg="value"),1165        mocker.call(5),1166    ]116711681169async def test_with_config_metadata_passthrough(mocker: MockerFixture) -> None:1170    fake = FakeRunnableSerializable()1171    spy = mocker.spy(fake.__class__, "invoke")1172    fakew = fake.configurable_fields(hello=ConfigurableField(id="hello", name="Hello"))11731174    assert (1175        fakew.with_config(tags=["a-tag"]).invoke(1176            "hello",1177            {1178                "configurable": {"hello": "there", "__secret_key": "nahnah"},1179                "metadata": {"bye": "now"},1180            },1181        )1182        == 51183    )1184    assert spy.call_args_list[0].args[1:] == (1185        "hello",1186        {1187            "tags": ["a-tag"],1188            "callbacks": None,1189            "recursion_limit": 25,1190            "configurable": {"hello": "there", "__secret_key": "nahnah"},1191            "metadata": {"bye": "now"},1192        },1193    )1194    spy.reset_mock()119511961197def test_with_config(mocker: MockerFixture) -> None:1198    fake = FakeRunnable()1199    spy = mocker.spy(fake, "invoke")12001201    assert fake.with_config(tags=["a-tag"]).invoke("hello") == 51202    assert spy.call_args_list == [1203        mocker.call(1204            "hello",1205            {"tags": ["a-tag"], "metadata": {}, "configurable": {}},1206        ),1207    ]1208    spy.reset_mock()12091210    fake_1 = RunnablePassthrough[Any]()1211    fake_2 = RunnablePassthrough[Any]()1212    spy_seq_step = mocker.spy(fake_1.__class__, "invoke")12131214    sequence = fake_1.with_config(tags=["a-tag"]) | fake_2.with_config(1215        tags=["b-tag"], max_concurrency=51216    )1217    assert sequence.invoke("hello") == "hello"1218    assert len(spy_seq_step.call_args_list) == 21219    for i, call in enumerate(spy_seq_step.call_args_list):1220        assert call.args[1] == "hello"1221        if i == 0:1222            assert call.args[2].get("tags") == ["a-tag"]1223            assert call.args[2].get("max_concurrency") is None1224        else:1225            assert call.args[2].get("tags") == ["b-tag"]1226            assert call.args[2].get("max_concurrency") == 51227    mocker.stop(spy_seq_step)12281229    assert [1230        *fake.with_config(tags=["a-tag"]).stream(1231            "hello", {"metadata": {"key": "value"}}1232        )1233    ] == [5]1234    assert spy.call_args_list == [1235        mocker.call(1236            "hello",1237            {"tags": ["a-tag"], "metadata": {"key": "value"}, "configurable": {}},1238        ),1239    ]1240    spy.reset_mock()12411242    assert fake.with_config(recursion_limit=5).batch(1243        ["hello", "wooorld"], [{"tags": ["a-tag"]}, {"metadata": {"key": "value"}}]1244    ) == [5, 7]12451246    assert len(spy.call_args_list) == 21247    for i, call in enumerate(1248        sorted(spy.call_args_list, key=lambda x: 0 if x.args[0] == "hello" else 1)1249    ):1250        assert call.args[0] == ("hello" if i == 0 else "wooorld")1251        if i == 0:1252            assert call.args[1].get("recursion_limit") == 51253            assert call.args[1].get("tags") == ["a-tag"]1254            assert call.args[1].get("metadata") == {}1255        else:1256            assert call.args[1].get("recursion_limit") == 51257            assert call.args[1].get("tags") == []1258            assert call.args[1].get("metadata") == {"key": "value"}12591260    spy.reset_mock()12611262    assert sorted(1263        c1264        for c in fake.with_config(recursion_limit=5).batch_as_completed(1265            ["hello", "wooorld"],1266            [{"tags": ["a-tag"]}, {"metadata": {"key": "value"}}],1267        )1268    ) == [(0, 5), (1, 7)]12691270    assert len(spy.call_args_list) == 21271    for i, call in enumerate(1272        sorted(spy.call_args_list, key=lambda x: 0 if x.args[0] == "hello" else 1)1273    ):1274        assert call.args[0] == ("hello" if i == 0 else "wooorld")1275        if i == 0:1276            assert call.args[1].get("recursion_limit") == 51277            assert call.args[1].get("tags") == ["a-tag"]1278            assert call.args[1].get("metadata") == {}1279        else:1280            assert call.args[1].get("recursion_limit") == 51281            assert call.args[1].get("tags") == []1282            assert call.args[1].get("metadata") == {"key": "value"}12831284    spy.reset_mock()12851286    assert fake.with_config(metadata={"a": "b"}).batch(1287        ["hello", "wooorld"], {"tags": ["a-tag"]}1288    ) == [5, 7]1289    assert len(spy.call_args_list) == 21290    for i, call in enumerate(spy.call_args_list):1291        assert call.args[0] == ("hello" if i == 0 else "wooorld")1292        assert call.args[1].get("tags") == ["a-tag"]1293        assert call.args[1].get("metadata") == {"a": "b"}1294    spy.reset_mock()12951296    assert sorted(1297        c for c in fake.batch_as_completed(["hello", "wooorld"], {"tags": ["a-tag"]})1298    ) == [(0, 5), (1, 7)]1299    assert len(spy.call_args_list) == 21300    for i, call in enumerate(spy.call_args_list):1301        assert call.args[0] == ("hello" if i == 0 else "wooorld")1302        assert call.args[1].get("tags") == ["a-tag"]130313041305async def test_with_config_async(mocker: MockerFixture) -> None:1306    fake = FakeRunnable()1307    spy = mocker.spy(fake, "invoke")13081309    handler = ConsoleCallbackHandler()1310    assert (1311        await fake.with_config(metadata={"a": "b"}).ainvoke(1312            "hello", config={"callbacks": [handler]}1313        )1314        == 51315    )1316    assert spy.call_args_list == [1317        mocker.call(1318            "hello",1319            {1320                "callbacks": [handler],1321                "metadata": {"a": "b"},1322                "configurable": {},1323                "tags": [],1324            },1325        ),1326    ]1327    spy.reset_mock()13281329    assert [1330        part async for part in fake.with_config(metadata={"a": "b"}).astream("hello")1331    ] == [5]1332    assert spy.call_args_list == [1333        mocker.call("hello", {"metadata": {"a": "b"}, "tags": [], "configurable": {}}),1334    ]1335    spy.reset_mock()13361337    assert await fake.with_config(recursion_limit=5, tags=["c"]).abatch(1338        ["hello", "wooorld"], {"metadata": {"key": "value"}}1339    ) == [1340        5,1341        7,1342    ]1343    assert sorted(spy.call_args_list) == [1344        mocker.call(1345            "hello",1346            {1347                "metadata": {"key": "value"},1348                "tags": ["c"],1349                "callbacks": None,1350                "recursion_limit": 5,1351                "configurable": {},1352            },1353        ),1354        mocker.call(1355            "wooorld",1356            {1357                "metadata": {"key": "value"},1358                "tags": ["c"],1359                "callbacks": None,1360                "recursion_limit": 5,1361                "configurable": {},1362            },1363        ),1364    ]1365    spy.reset_mock()13661367    assert sorted(1368        [1369            c1370            async for c in fake.with_config(1371                recursion_limit=5, tags=["c"]1372            ).abatch_as_completed(["hello", "wooorld"], {"metadata": {"key": "value"}})1373        ]1374    ) == [1375        (0, 5),1376        (1, 7),1377    ]1378    assert len(spy.call_args_list) == 21379    first_call = next(call for call in spy.call_args_list if call.args[0] == "hello")1380    assert first_call == mocker.call(1381        "hello",1382        {1383            "metadata": {"key": "value"},1384            "tags": ["c"],1385            "callbacks": None,1386            "recursion_limit": 5,1387            "configurable": {},1388        },1389    )1390    second_call = next(call for call in spy.call_args_list if call.args[0] == "wooorld")1391    assert second_call == mocker.call(1392        "wooorld",1393        {1394            "metadata": {"key": "value"},1395            "tags": ["c"],1396            "callbacks": None,1397            "recursion_limit": 5,1398            "configurable": {},1399        },1400    )140114021403def test_default_method_implementations(mocker: MockerFixture) -> None:1404    fake = FakeRunnable()1405    spy = mocker.spy(fake, "invoke")14061407    assert fake.invoke("hello", {"tags": ["a-tag"]}) == 51408    assert spy.call_args_list == [1409        mocker.call("hello", {"tags": ["a-tag"]}),1410    ]1411    spy.reset_mock()14121413    assert [*fake.stream("hello", {"metadata": {"key": "value"}})] == [5]1414    assert spy.call_args_list == [1415        mocker.call("hello", {"metadata": {"key": "value"}}),1416    ]1417    spy.reset_mock()14181419    assert fake.batch(1420        ["hello", "wooorld"], [{"tags": ["a-tag"]}, {"metadata": {"key": "value"}}]1421    ) == [5, 7]14221423    assert len(spy.call_args_list) == 21424    for call in spy.call_args_list:1425        call_arg = call.args[0]14261427        if call_arg == "hello":1428            assert call_arg == "hello"1429            assert call.args[1].get("tags") == ["a-tag"]1430            assert call.args[1].get("metadata") == {}1431        else:1432            assert call_arg == "wooorld"1433            assert call.args[1].get("tags") == []1434            assert call.args[1].get("metadata") == {"key": "value"}14351436    spy.reset_mock()14371438    assert fake.batch(["hello", "wooorld"], {"tags": ["a-tag"]}) == [5, 7]1439    assert len(spy.call_args_list) == 21440    assert {call.args[0] for call in spy.call_args_list} == {"hello", "wooorld"}1441    for call in spy.call_args_list:1442        assert call.args[1].get("tags") == ["a-tag"]1443        assert call.args[1].get("metadata") == {}144414451446async def test_default_method_implementations_async(mocker: MockerFixture) -> None:1447    fake = FakeRunnable()1448    spy = mocker.spy(fake, "invoke")14491450    assert await fake.ainvoke("hello", config={"callbacks": []}) == 51451    assert spy.call_args_list == [1452        mocker.call("hello", {"callbacks": []}),1453    ]1454    spy.reset_mock()14551456    assert [part async for part in fake.astream("hello")] == [5]1457    assert spy.call_args_list == [1458        mocker.call("hello", None),1459    ]1460    spy.reset_mock()14611462    assert await fake.abatch(["hello", "wooorld"], {"metadata": {"key": "value"}}) == [1463        5,1464        7,1465    ]1466    assert {call.args[0] for call in spy.call_args_list} == {"hello", "wooorld"}1467    for call in spy.call_args_list:1468        assert call.args[1] == {1469            "metadata": {"key": "value"},1470            "tags": [],1471            "callbacks": None,1472            "recursion_limit": 25,1473            "configurable": {},1474        }147514761477def test_prompt() -> None:1478    prompt = ChatPromptTemplate.from_messages(1479        messages=[1480            SystemMessage(content="You are a nice assistant."),1481            HumanMessagePromptTemplate.from_template("{question}"),1482        ]1483    )1484    expected = ChatPromptValue(1485        messages=[1486            SystemMessage(content="You are a nice assistant."),1487            HumanMessage(content="What is your name?"),1488        ]1489    )14901491    assert prompt.invoke({"question": "What is your name?"}) == expected14921493    assert prompt.batch(1494        [1495            {"question": "What is your name?"},1496            {"question": "What is your favorite color?"},1497        ]1498    ) == [1499        expected,1500        ChatPromptValue(1501            messages=[1502                SystemMessage(content="You are a nice assistant."),1503                HumanMessage(content="What is your favorite color?"),1504            ]1505        ),1506    ]15071508    assert [*prompt.stream({"question": "What is your name?"})] == [expected]150915101511async def test_prompt_async() -> None:1512    prompt = ChatPromptTemplate.from_messages(1513        messages=[1514            SystemMessage(content="You are a nice assistant."),1515            HumanMessagePromptTemplate.from_template("{question}"),1516        ]1517    )1518    expected = ChatPromptValue(1519        messages=[1520            SystemMessage(content="You are a nice assistant."),1521            HumanMessage(content="What is your name?"),1522        ]1523    )15241525    assert await prompt.ainvoke({"question": "What is your name?"}) == expected15261527    assert await prompt.abatch(1528        [1529            {"question": "What is your name?"},1530            {"question": "What is your favorite color?"},1531        ]1532    ) == [1533        expected,1534        ChatPromptValue(1535            messages=[1536                SystemMessage(content="You are a nice assistant."),1537                HumanMessage(content="What is your favorite color?"),1538            ]1539        ),1540    ]15411542    assert [1543        part async for part in prompt.astream({"question": "What is your name?"})1544    ] == [expected]15451546    stream_log = [1547        part async for part in prompt.astream_log({"question": "What is your name?"})1548    ]15491550    assert len(stream_log[0].ops) == 11551    assert stream_log[0].ops[0]["op"] == "replace"1552    assert stream_log[0].ops[0]["path"] == ""1553    assert stream_log[0].ops[0]["value"]["logs"] == {}1554    assert stream_log[0].ops[0]["value"]["final_output"] is None1555    assert stream_log[0].ops[0]["value"]["streamed_output"] == []1556    assert isinstance(stream_log[0].ops[0]["value"]["id"], str)15571558    assert stream_log[1:] == [1559        RunLogPatch(1560            {"op": "add", "path": "/streamed_output/-", "value": expected},1561            {1562                "op": "replace",1563                "path": "/final_output",1564                "value": ChatPromptValue(1565                    messages=[1566                        SystemMessage(content="You are a nice assistant."),1567                        HumanMessage(content="What is your name?"),1568                    ]1569                ),1570            },1571        ),1572    ]15731574    stream_log_state = [1575        part1576        async for part in prompt.astream_log(1577            {"question": "What is your name?"}, diff=False1578        )1579    ]15801581    # remove random id1582    stream_log[0].ops[0]["value"]["id"] = "00000000-0000-0000-0000-000000000000"1583    stream_log_state[-1].ops[0]["value"]["id"] = "00000000-0000-0000-0000-000000000000"1584    stream_log_state[-1].state["id"] = "00000000-0000-0000-0000-000000000000"15851586    # assert output with diff=False matches output with diff=True1587    assert stream_log_state[-1].ops == [op for chunk in stream_log for op in chunk.ops]1588    assert stream_log_state[-1] == RunLog(1589        *[op for chunk in stream_log for op in chunk.ops],1590        state={1591            "final_output": ChatPromptValue(1592                messages=[1593                    SystemMessage(content="You are a nice assistant."),1594                    HumanMessage(content="What is your name?"),1595                ]1596            ),1597            "id": "00000000-0000-0000-0000-000000000000",1598            "logs": {},1599            "streamed_output": [1600                ChatPromptValue(1601                    messages=[1602                        SystemMessage(content="You are a nice assistant."),1603                        HumanMessage(content="What is your name?"),1604                    ]1605                )1606            ],1607            "type": "prompt",1608            "name": "ChatPromptTemplate",1609        },1610    )16111612    # nested inside trace_with_chain_group16131614    async with atrace_as_chain_group("a_group") as manager:1615        stream_log_nested = [1616            part1617            async for part in prompt.astream_log(1618                {"question": "What is your name?"}, config={"callbacks": manager}1619            )1620        ]16211622    assert len(stream_log_nested[0].ops) == 11623    assert stream_log_nested[0].ops[0]["op"] == "replace"1624    assert stream_log_nested[0].ops[0]["path"] == ""1625    assert stream_log_nested[0].ops[0]["value"]["logs"] == {}1626    assert stream_log_nested[0].ops[0]["value"]["final_output"] is None1627    assert stream_log_nested[0].ops[0]["value"]["streamed_output"] == []1628    assert isinstance(stream_log_nested[0].ops[0]["value"]["id"], str)16291630    assert stream_log_nested[1:] == [1631        RunLogPatch(1632            {"op": "add", "path": "/streamed_output/-", "value": expected},1633            {1634                "op": "replace",1635                "path": "/final_output",1636                "value": ChatPromptValue(1637                    messages=[1638                        SystemMessage(content="You are a nice assistant."),1639                        HumanMessage(content="What is your name?"),1640                    ]1641                ),1642            },1643        ),1644    ]164516461647def test_prompt_template_params() -> None:1648    prompt = ChatPromptTemplate.from_template(1649        "Respond to the following question: {question}"1650    )1651    result = prompt.invoke(1652        {1653            "question": "test",1654            "topic": "test",1655        }1656    )1657    assert result == ChatPromptValue(1658        messages=[HumanMessage(content="Respond to the following question: test")]1659    )16601661    with pytest.raises(KeyError):1662        prompt.invoke({})166316641665def test_with_listeners(mocker: MockerFixture) -> None:1666    prompt = (1667        SystemMessagePromptTemplate.from_template("You are a nice assistant.")1668        + "{question}"1669    )1670    chat = FakeListChatModel(responses=["foo"])16711672    chain = prompt | chat16731674    mock_start = mocker.Mock()1675    mock_end = mocker.Mock()16761677    chain.with_listeners(on_start=mock_start, on_end=mock_end).invoke(1678        {"question": "Who are you?"}1679    )16801681    assert mock_start.call_count == 11682    assert mock_start.call_args[0][0].name == "RunnableSequence"1683    assert mock_end.call_count == 116841685    mock_start.reset_mock()1686    mock_end.reset_mock()16871688    with trace_as_chain_group("hello") as manager:1689        chain.with_listeners(on_start=mock_start, on_end=mock_end).invoke(1690            {"question": "Who are you?"}, {"callbacks": manager}1691        )16921693    assert mock_start.call_count == 11694    assert mock_start.call_args[0][0].name == "RunnableSequence"1695    assert mock_end.call_count == 1169616971698async def test_with_listeners_async(mocker: MockerFixture) -> None:1699    prompt = (1700        SystemMessagePromptTemplate.from_template("You are a nice assistant.")1701        + "{question}"1702    )1703    chat = FakeListChatModel(responses=["foo"])17041705    chain = prompt | chat17061707    mock_start = mocker.Mock()1708    mock_end = mocker.Mock()17091710    await chain.with_listeners(on_start=mock_start, on_end=mock_end).ainvoke(1711        {"question": "Who are you?"}1712    )17131714    assert mock_start.call_count == 11715    assert mock_start.call_args[0][0].name == "RunnableSequence"1716    assert mock_end.call_count == 117171718    mock_start.reset_mock()1719    mock_end.reset_mock()17201721    async with atrace_as_chain_group("hello") as manager:1722        await chain.with_listeners(on_start=mock_start, on_end=mock_end).ainvoke(1723            {"question": "Who are you?"}, {"callbacks": manager}1724        )17251726    assert mock_start.call_count == 11727    assert mock_start.call_args[0][0].name == "RunnableSequence"1728    assert mock_end.call_count == 1172917301731def test_with_listener_propagation(mocker: MockerFixture) -> None:1732    prompt = (1733        SystemMessagePromptTemplate.from_template("You are a nice assistant.")1734        + "{question}"1735    )1736    chat = FakeListChatModel(responses=["foo"])1737    chain = prompt | chat1738    mock_start = mocker.Mock()1739    mock_end = mocker.Mock()1740    chain_with_listeners = chain.with_listeners(on_start=mock_start, on_end=mock_end)17411742    chain_with_listeners.with_retry().invoke({"question": "Who are you?"})17431744    assert mock_start.call_count == 11745    assert mock_start.call_args[0][0].name == "RunnableSequence"1746    assert mock_end.call_count == 117471748    mock_start.reset_mock()1749    mock_end.reset_mock()17501751    chain_with_listeners.invoke({"question": "Who are you?"})17521753    assert mock_start.call_count == 11754    assert mock_start.call_args[0][0].name == "RunnableSequence"1755    assert mock_end.call_count == 117561757    mock_start.reset_mock()1758    mock_end.reset_mock()17591760    chain_with_listeners.with_config({"tags": ["foo"]}).invoke(1761        {"question": "Who are you?"}1762    )17631764    assert mock_start.call_count == 11765    assert mock_start.call_args[0][0].name == "RunnableSequence"1766    assert mock_end.call_count == 117671768    mock_start.reset_mock()1769    mock_end.reset_mock()17701771    chain_with_listeners.bind(stop=["foo"]).invoke({"question": "Who are you?"})17721773    assert mock_start.call_count == 11774    assert mock_start.call_args[0][0].name == "RunnableSequence"1775    assert mock_end.call_count == 117761777    mock_start.reset_mock()1778    mock_end.reset_mock()17791780    mock_start_inner = mocker.Mock()1781    mock_end_inner = mocker.Mock()17821783    chain_with_listeners.with_listeners(1784        on_start=mock_start_inner, on_end=mock_end_inner1785    ).invoke({"question": "Who are you?"})17861787    assert mock_start.call_count == 11788    assert mock_start.call_args[0][0].name == "RunnableSequence"1789    assert mock_end.call_count == 11790    assert mock_start_inner.call_count == 11791    assert mock_start_inner.call_args[0][0].name == "RunnableSequence"1792    assert mock_end_inner.call_count == 1179317941795@freeze_time("2023-01-01")1796@pytest.mark.usefixtures("deterministic_uuids")1797def test_prompt_with_chat_model(1798    mocker: MockerFixture,1799    snapshot: SnapshotAssertion,1800) -> None:1801    prompt = (1802        SystemMessagePromptTemplate.from_template("You are a nice assistant.")1803        + "{question}"1804    )1805    chat = FakeListChatModel(responses=["foo"])18061807    chain = prompt | chat18081809    assert _normalize_lc_version(repr(chain)) == snapshot1810    assert isinstance(chain, RunnableSequence)1811    assert chain.first == prompt1812    assert chain.middle == []1813    assert chain.last == chat1814    assert _normalize_lc_version(dumps(chain, pretty=True)) == snapshot18151816    # Test invoke1817    prompt_spy = mocker.spy(prompt.__class__, "invoke")1818    chat_spy = mocker.spy(chat.__class__, "invoke")1819    tracer = FakeTracer()1820    assert chain.invoke(1821        {"question": "What is your name?"}, {"callbacks": [tracer]}1822    ) == _any_id_ai_message(content="foo")1823    assert prompt_spy.call_args.args[1] == {"question": "What is your name?"}1824    assert chat_spy.call_args.args[1] == ChatPromptValue(1825        messages=[1826            SystemMessage(content="You are a nice assistant."),1827            HumanMessage(content="What is your name?"),1828        ]1829    )18301831    assert tracer.runs == snapshot18321833    mocker.stop(prompt_spy)1834    mocker.stop(chat_spy)18351836    # Test batch1837    prompt_spy = mocker.spy(prompt.__class__, "batch")1838    chat_spy = mocker.spy(chat.__class__, "batch")1839    tracer = FakeTracer()1840    assert chain.batch(1841        [1842            {"question": "What is your name?"},1843            {"question": "What is your favorite color?"},1844        ],1845        {"callbacks": [tracer]},1846    ) == [1847        _any_id_ai_message(content="foo"),1848        _any_id_ai_message(content="foo"),1849    ]1850    assert prompt_spy.call_args.args[1] == [1851        {"question": "What is your name?"},1852        {"question": "What is your favorite color?"},1853    ]1854    assert chat_spy.call_args.args[1] == [1855        ChatPromptValue(1856            messages=[1857                SystemMessage(content="You are a nice assistant."),1858                HumanMessage(content="What is your name?"),1859            ]1860        ),1861        ChatPromptValue(1862            messages=[1863                SystemMessage(content="You are a nice assistant."),1864                HumanMessage(content="What is your favorite color?"),1865            ]1866        ),1867    ]1868    assert (1869        len(1870            [1871                r1872                for r in tracer.runs1873                if r.parent_run_id is None and len(r.child_runs) == 21874            ]1875        )1876        == 21877    ), "Each of 2 outer runs contains exactly two inner runs (1 prompt, 1 chat)"1878    mocker.stop(prompt_spy)1879    mocker.stop(chat_spy)18801881    # Test stream1882    prompt_spy = mocker.spy(prompt.__class__, "invoke")1883    chat_spy = mocker.spy(chat.__class__, "stream")1884    tracer = FakeTracer()1885    assert [1886        *chain.stream({"question": "What is your name?"}, {"callbacks": [tracer]})1887    ] == [1888        _any_id_ai_message_chunk(content="f"),1889        _any_id_ai_message_chunk(content="o"),1890        _any_id_ai_message_chunk(content="o", chunk_position="last"),1891    ]1892    assert prompt_spy.call_args.args[1] == {"question": "What is your name?"}1893    assert chat_spy.call_args.args[1] == ChatPromptValue(1894        messages=[1895            SystemMessage(content="You are a nice assistant."),1896            HumanMessage(content="What is your name?"),1897        ]1898    )189919001901@freeze_time("2023-01-01")1902@pytest.mark.usefixtures("deterministic_uuids")1903async def test_prompt_with_chat_model_async(1904    mocker: MockerFixture,1905    snapshot: SnapshotAssertion,1906) -> None:1907    prompt = (1908        SystemMessagePromptTemplate.from_template("You are a nice assistant.")1909        + "{question}"1910    )1911    chat = FakeListChatModel(responses=["foo"])19121913    chain = prompt | chat19141915    assert _normalize_lc_version(repr(chain)) == snapshot1916    assert isinstance(chain, RunnableSequence)1917    assert chain.first == prompt1918    assert chain.middle == []1919    assert chain.last == chat1920    assert _normalize_lc_version(dumps(chain, pretty=True)) == snapshot19211922    # Test invoke1923    prompt_spy = mocker.spy(prompt.__class__, "ainvoke")1924    chat_spy = mocker.spy(chat.__class__, "ainvoke")1925    tracer = FakeTracer()1926    assert await chain.ainvoke(1927        {"question": "What is your name?"}, {"callbacks": [tracer]}1928    ) == _any_id_ai_message(content="foo")1929    assert prompt_spy.call_args.args[1] == {"question": "What is your name?"}1930    assert chat_spy.call_args.args[1] == ChatPromptValue(1931        messages=[1932            SystemMessage(content="You are a nice assistant."),1933            HumanMessage(content="What is your name?"),1934        ]1935    )19361937    assert tracer.runs == snapshot19381939    mocker.stop(prompt_spy)1940    mocker.stop(chat_spy)19411942    # Test batch1943    prompt_spy = mocker.spy(prompt.__class__, "abatch")1944    chat_spy = mocker.spy(chat.__class__, "abatch")1945    tracer = FakeTracer()1946    assert await chain.abatch(1947        [1948            {"question": "What is your name?"},1949            {"question": "What is your favorite color?"},1950        ],1951        {"callbacks": [tracer]},1952    ) == [1953        _any_id_ai_message(content="foo"),1954        _any_id_ai_message(content="foo"),1955    ]1956    assert prompt_spy.call_args.args[1] == [1957        {"question": "What is your name?"},1958        {"question": "What is your favorite color?"},1959    ]1960    assert chat_spy.call_args.args[1] == [1961        ChatPromptValue(1962            messages=[1963                SystemMessage(content="You are a nice assistant."),1964                HumanMessage(content="What is your name?"),1965            ]1966        ),1967        ChatPromptValue(1968            messages=[1969                SystemMessage(content="You are a nice assistant."),1970                HumanMessage(content="What is your favorite color?"),1971            ]1972        ),1973    ]1974    assert (1975        len(1976            [1977                r1978                for r in tracer.runs1979                if r.parent_run_id is None and len(r.child_runs) == 21980            ]1981        )1982        == 21983    ), "Each of 2 outer runs contains exactly two inner runs (1 prompt, 1 chat)"1984    mocker.stop(prompt_spy)1985    mocker.stop(chat_spy)19861987    # Test stream1988    prompt_spy = mocker.spy(prompt.__class__, "ainvoke")1989    chat_spy = mocker.spy(chat.__class__, "astream")1990    tracer = FakeTracer()1991    assert [1992        a1993        async for a in chain.astream(1994            {"question": "What is your name?"}, {"callbacks": [tracer]}1995        )1996    ] == [1997        _any_id_ai_message_chunk(content="f"),1998        _any_id_ai_message_chunk(content="o"),1999        _any_id_ai_message_chunk(content="o", chunk_position="last"),2000    ]

Findings

✓ No findings reported for this file.

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.