mirror of
https://github.com/langchain-ai/langserve.git
synced 2026-07-25 13:15:44 -04:00
Compare commits
14 Commits
v0.2.3
...
v0.3.0dev2
| Author | SHA1 | Date | |
|---|---|---|---|
| 59b3c81189 | |||
| 36e9919c17 | |||
| ff94f96dc8 | |||
| 8c852935e5 | |||
| 04236b0cf2 | |||
| 54eee64faf | |||
| 72c200ff81 | |||
| 21c2e3da2a | |||
| 6e7a9ee3f5 | |||
| 81c0285af2 | |||
| b62b925825 | |||
| b528955b60 | |||
| 5aedbf7083 | |||
| 41a9d798aa |
@@ -1,94 +0,0 @@
|
||||
name: pydantic v1/v2 compatibility
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
working-directory:
|
||||
required: true
|
||||
type: string
|
||||
description: "From which folder this pipeline executes"
|
||||
|
||||
env:
|
||||
POETRY_VERSION: "1.5.1"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
timeout-minutes: 10
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version:
|
||||
- "3.8"
|
||||
- "3.9"
|
||||
- "3.10"
|
||||
- "3.11"
|
||||
name: Pydantic v1/v2 compatibility - Python ${{ matrix.python-version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }} + Poetry ${{ env.POETRY_VERSION }}
|
||||
uses: "./.github/actions/poetry_setup"
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
poetry-version: ${{ env.POETRY_VERSION }}
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
cache-key: pydantic-cross-compat
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: poetry install
|
||||
|
||||
- name: Install the opposite major version of pydantic
|
||||
# If normal tests use pydantic v1, here we'll use v2, and vice versa.
|
||||
shell: bash
|
||||
run: |
|
||||
# Determine the major part of pydantic version
|
||||
REGULAR_VERSION=$(poetry run python -c "import pydantic; print(pydantic.__version__)" | cut -d. -f1)
|
||||
|
||||
if [[ "$REGULAR_VERSION" == "1" ]]; then
|
||||
PYDANTIC_DEP=">=2.1,<3"
|
||||
TEST_WITH_VERSION="2"
|
||||
elif [[ "$REGULAR_VERSION" == "2" ]]; then
|
||||
PYDANTIC_DEP="<2"
|
||||
TEST_WITH_VERSION="1"
|
||||
else
|
||||
echo "Unexpected pydantic major version '$REGULAR_VERSION', cannot determine which version to use for cross-compatibility test."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Install via `pip` instead of `poetry add` to avoid changing lockfile,
|
||||
# which would prevent caching from working: the cache would get saved
|
||||
# to a different key than where it gets loaded from.
|
||||
poetry run pip install "pydantic${PYDANTIC_DEP}"
|
||||
|
||||
# Ensure that the correct pydantic is installed now.
|
||||
echo "Checking pydantic version... Expecting ${TEST_WITH_VERSION}"
|
||||
|
||||
# Determine the major part of pydantic version
|
||||
CURRENT_VERSION=$(poetry run python -c "import pydantic; print(pydantic.__version__)" | cut -d. -f1)
|
||||
|
||||
# Check that the major part of pydantic version is as expected, if not
|
||||
# raise an error
|
||||
if [[ "$CURRENT_VERSION" != "$TEST_WITH_VERSION" ]]; then
|
||||
echo "Error: expected pydantic version ${CURRENT_VERSION} to have been installed, but found: ${TEST_WITH_VERSION}"
|
||||
exit 1
|
||||
fi
|
||||
echo "Found pydantic version ${CURRENT_VERSION}, as expected"
|
||||
- name: Run pydantic compatibility tests
|
||||
shell: bash
|
||||
run: make test
|
||||
|
||||
- name: Ensure the tests did not create any additional files
|
||||
shell: bash
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
STATUS="$(git status)"
|
||||
echo "$STATUS"
|
||||
|
||||
# grep will exit non-zero if the target message isn't found,
|
||||
# and `set -e` above will cause the step to fail.
|
||||
echo "$STATUS" | grep 'nothing to commit, working tree clean'
|
||||
@@ -39,13 +39,6 @@ jobs:
|
||||
with:
|
||||
working-directory: .
|
||||
secrets: inherit
|
||||
|
||||
pydantic-compatibility:
|
||||
uses:
|
||||
./.github/workflows/_pydantic_compatibility.yml
|
||||
with:
|
||||
working-directory: .
|
||||
secrets: inherit
|
||||
test:
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -380,7 +380,7 @@ prompt = ChatPromptTemplate.from_messages(
|
||||
]
|
||||
)
|
||||
|
||||
chain = prompt | ChatAnthropic(model="claude-2")
|
||||
chain = prompt | ChatAnthropic(model="claude-2.1")
|
||||
|
||||
|
||||
class InputChat(BaseModel):
|
||||
@@ -772,7 +772,7 @@ prompt = ChatPromptTemplate.from_messages(
|
||||
]
|
||||
)
|
||||
|
||||
chain = prompt | ChatAnthropic(model="claude-2")
|
||||
chain = prompt | ChatAnthropic(model="claude-2.1")
|
||||
|
||||
|
||||
class MessageListInput(BaseModel):
|
||||
|
||||
@@ -23,12 +23,12 @@ from fastapi import FastAPI
|
||||
from langchain.agents import AgentExecutor
|
||||
from langchain.agents.format_scratchpad import format_to_openai_functions
|
||||
from langchain.agents.output_parsers import OpenAIFunctionsAgentOutputParser
|
||||
from langchain.pydantic_v1 import BaseModel
|
||||
from langchain_community.vectorstores import FAISS
|
||||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||
from langchain_core.tools import tool
|
||||
from langchain_core.utils.function_calling import format_tool_to_openai_function
|
||||
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
|
||||
from pydantic import BaseModel
|
||||
|
||||
from langserve import add_routes
|
||||
|
||||
|
||||
@@ -57,9 +57,9 @@ from langchain_core.runnables import RunnableLambda
|
||||
from langchain_core.tools import tool
|
||||
from langchain_core.utils.function_calling import format_tool_to_openai_tool
|
||||
from langchain_openai import ChatOpenAI
|
||||
from pydantic import BaseModel
|
||||
|
||||
from langserve import add_routes
|
||||
from langserve.pydantic_v1 import BaseModel
|
||||
|
||||
prompt = ChatPromptTemplate.from_messages(
|
||||
[
|
||||
|
||||
@@ -36,9 +36,9 @@ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||
from langchain_core.tools import tool
|
||||
from langchain_core.utils.function_calling import format_tool_to_openai_tool
|
||||
from langchain_openai import ChatOpenAI
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from langserve import add_routes
|
||||
from langserve.pydantic_v1 import BaseModel, Field
|
||||
|
||||
prompt = ChatPromptTemplate.from_messages(
|
||||
[
|
||||
|
||||
@@ -44,10 +44,10 @@ from langchain_core.runnables import (
|
||||
)
|
||||
from langchain_core.vectorstores import VectorStore
|
||||
from langchain_openai import OpenAIEmbeddings
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from langserve import APIHandler
|
||||
from langserve.pydantic_v1 import BaseModel
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
@@ -150,10 +150,9 @@ class PerUserVectorstore(RunnableSerializable):
|
||||
user_id: Optional[str]
|
||||
vectorstore: VectorStore
|
||||
|
||||
class Config:
|
||||
# Allow arbitrary types since VectorStore is an abstract interface
|
||||
# and not a pydantic model
|
||||
arbitrary_types_allowed = True
|
||||
model_config = ConfigDict(
|
||||
arbitrary_types_allowed=True,
|
||||
)
|
||||
|
||||
def _invoke(
|
||||
self, input: str, config: Optional[RunnableConfig] = None, **kwargs: Any
|
||||
|
||||
@@ -45,10 +45,10 @@ from langchain_core.runnables import (
|
||||
)
|
||||
from langchain_core.vectorstores import VectorStore
|
||||
from langchain_openai import OpenAIEmbeddings
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from langserve import add_routes
|
||||
from langserve.pydantic_v1 import BaseModel
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
@@ -147,10 +147,9 @@ class PerUserVectorstore(RunnableSerializable):
|
||||
user_id: Optional[str]
|
||||
vectorstore: VectorStore
|
||||
|
||||
class Config:
|
||||
# Allow arbitrary types since VectorStore is an abstract interface
|
||||
# and not a pydantic model
|
||||
arbitrary_types_allowed = True
|
||||
model_config = ConfigDict(
|
||||
arbitrary_types_allowed=True,
|
||||
)
|
||||
|
||||
def _invoke(
|
||||
self, input: str, config: Optional[RunnableConfig] = None, **kwargs: Any
|
||||
|
||||
@@ -8,9 +8,9 @@ from fastapi import FastAPI
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
|
||||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from langserve import add_routes
|
||||
from langserve.pydantic_v1 import BaseModel, Field
|
||||
|
||||
app = FastAPI(
|
||||
title="LangChain Server",
|
||||
@@ -28,7 +28,7 @@ prompt = ChatPromptTemplate.from_messages(
|
||||
]
|
||||
)
|
||||
|
||||
chain = prompt | ChatAnthropic(model="claude-2")
|
||||
chain = prompt | ChatAnthropic(model="claude-2.1")
|
||||
|
||||
|
||||
class InputChat(BaseModel):
|
||||
|
||||
@@ -8,9 +8,9 @@ from fastapi import FastAPI
|
||||
from langchain_anthropic.chat_models import ChatAnthropic
|
||||
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
|
||||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from langserve import add_routes
|
||||
from langserve.pydantic_v1 import BaseModel, Field
|
||||
|
||||
app = FastAPI(
|
||||
title="LangChain Server",
|
||||
|
||||
@@ -18,9 +18,9 @@ from langchain_community.chat_message_histories import FileChatMessageHistory
|
||||
from langchain_core.chat_history import BaseChatMessageHistory
|
||||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||
from langchain_core.runnables.history import RunnableWithMessageHistory
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from langserve import add_routes
|
||||
from langserve.pydantic_v1 import BaseModel, Field
|
||||
|
||||
|
||||
def _is_valid_identifier(value: str) -> bool:
|
||||
@@ -76,7 +76,7 @@ prompt = ChatPromptTemplate.from_messages(
|
||||
]
|
||||
)
|
||||
|
||||
chain = prompt | ChatAnthropic(model="claude-2")
|
||||
chain = prompt | ChatAnthropic(model="claude-2.1")
|
||||
|
||||
|
||||
class InputChat(BaseModel):
|
||||
|
||||
@@ -20,7 +20,6 @@ from fastapi import FastAPI
|
||||
from langchain.agents import AgentExecutor
|
||||
from langchain.agents.format_scratchpad import format_to_openai_functions
|
||||
from langchain.agents.output_parsers import OpenAIFunctionsAgentOutputParser
|
||||
from langchain.pydantic_v1 import BaseModel
|
||||
from langchain_community.vectorstores import FAISS
|
||||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||
from langchain_core.runnables import (
|
||||
@@ -33,6 +32,7 @@ from langchain_core.runnables.utils import Input, Output
|
||||
from langchain_core.tools import tool
|
||||
from langchain_core.utils.function_calling import format_tool_to_openai_function
|
||||
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
|
||||
from pydantic import BaseModel
|
||||
|
||||
from langserve import add_routes
|
||||
|
||||
|
||||
@@ -15,9 +15,9 @@ from langchain_core.runnables import (
|
||||
)
|
||||
from langchain_core.vectorstores import VectorStore
|
||||
from langchain_openai import OpenAIEmbeddings
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from langserve import add_routes
|
||||
from langserve.pydantic_v1 import BaseModel, Field
|
||||
|
||||
vectorstore1 = FAISS.from_texts(
|
||||
["cats like fish", "dogs like sticks"], embedding=OpenAIEmbeddings()
|
||||
|
||||
@@ -18,9 +18,9 @@ from langchain_core.output_parsers import StrOutputParser
|
||||
from langchain_core.prompts import ChatPromptTemplate, PromptTemplate, format_document
|
||||
from langchain_core.runnables import RunnableMap, RunnablePassthrough
|
||||
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from langserve import add_routes
|
||||
from langserve.pydantic_v1 import BaseModel, Field
|
||||
|
||||
_TEMPLATE = """Given the following conversation and a follow up question, rephrase the
|
||||
follow up question to be a standalone question, in its original language.
|
||||
|
||||
@@ -15,10 +15,10 @@ allowing one to upload a binary file using the langserve playground UI.
|
||||
import base64
|
||||
|
||||
from fastapi import FastAPI
|
||||
from langchain.pydantic_v1 import Field
|
||||
from langchain_community.document_loaders.parsers.pdf import PDFMinerParser
|
||||
from langchain_core.document_loaders import Blob
|
||||
from langchain_core.runnables import RunnableLambda
|
||||
from pydantic import Field
|
||||
|
||||
from langserve import CustomUserType, add_routes
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ from langchain_anthropic import ChatAnthropic
|
||||
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
|
||||
from langchain_core.output_parsers import StrOutputParser
|
||||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from langserve import add_routes
|
||||
from langserve.pydantic_v1 import BaseModel, Field
|
||||
|
||||
app = FastAPI(
|
||||
title="LangChain Server",
|
||||
@@ -40,7 +40,7 @@ prompt = ChatPromptTemplate.from_messages(
|
||||
]
|
||||
)
|
||||
|
||||
chain = prompt | ChatAnthropic(model="claude-2") | StrOutputParser()
|
||||
chain = prompt | ChatAnthropic(model="claude-2.1") | StrOutputParser()
|
||||
|
||||
|
||||
class InputChat(BaseModel):
|
||||
|
||||
@@ -6,7 +6,6 @@ from typing import Any, Dict, List, Tuple
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from langchain.pydantic_v1 import BaseModel, Field
|
||||
from langchain_community.document_loaders.parsers.pdf import PDFMinerParser
|
||||
from langchain_core.document_loaders import Blob
|
||||
from langchain_core.messages import (
|
||||
@@ -17,6 +16,7 @@ from langchain_core.messages import (
|
||||
)
|
||||
from langchain_core.runnables import RunnableLambda, RunnableParallel
|
||||
from langchain_openai import ChatOpenAI
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from langserve import CustomUserType
|
||||
from langserve.server import add_routes
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
from typing import Any, Dict, Type, cast
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, RootModel
|
||||
from pydantic.json_schema import (
|
||||
DEFAULT_REF_TEMPLATE,
|
||||
GenerateJsonSchema,
|
||||
JsonSchemaMode,
|
||||
)
|
||||
|
||||
|
||||
def _create_root_model(name: str, type_: Any) -> Type[RootModel]:
|
||||
"""Create a base class."""
|
||||
|
||||
def schema(
|
||||
cls: Type[BaseModel],
|
||||
by_alias: bool = True,
|
||||
ref_template: str = DEFAULT_REF_TEMPLATE,
|
||||
) -> Dict[str, Any]:
|
||||
# Complains about schema not being defined in superclass
|
||||
schema_ = super(cls, cls).schema( # type: ignore[misc]
|
||||
by_alias=by_alias, ref_template=ref_template
|
||||
)
|
||||
schema_["title"] = name
|
||||
return schema_
|
||||
|
||||
def model_json_schema(
|
||||
cls: Type[BaseModel],
|
||||
by_alias: bool = True,
|
||||
ref_template: str = DEFAULT_REF_TEMPLATE,
|
||||
schema_generator: type[GenerateJsonSchema] = GenerateJsonSchema,
|
||||
mode: JsonSchemaMode = "validation",
|
||||
) -> Dict[str, Any]:
|
||||
# Complains about model_json_schema not being defined in superclass
|
||||
schema_ = super(cls, cls).model_json_schema( # type: ignore[misc]
|
||||
by_alias=by_alias,
|
||||
ref_template=ref_template,
|
||||
schema_generator=schema_generator,
|
||||
mode=mode,
|
||||
)
|
||||
schema_["title"] = name
|
||||
return schema_
|
||||
|
||||
base_class_attributes = {
|
||||
"__annotations__": {"root": type_},
|
||||
"model_config": ConfigDict(arbitrary_types_allowed=True),
|
||||
"schema": classmethod(schema),
|
||||
"model_json_schema": classmethod(model_json_schema),
|
||||
# Should replace __module__ with caller based on stack frame.
|
||||
"__module__": "langserve._pydantic",
|
||||
}
|
||||
|
||||
custom_root_type = type(name, (RootModel,), base_class_attributes)
|
||||
return cast(Type[RootModel], custom_root_type)
|
||||
+51
-44
@@ -29,6 +29,7 @@ from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from langchain_core._api.beta_decorator import warn_beta
|
||||
from langchain_core.callbacks.base import AsyncCallbackHandler
|
||||
from langchain_core.callbacks.manager import BaseCallbackManager
|
||||
from langchain_core.load.serializable import Serializable
|
||||
from langchain_core.runnables import Runnable, RunnableConfig
|
||||
from langchain_core.runnables.config import (
|
||||
@@ -40,14 +41,15 @@ from langchain_core.tracers import RunLogPatch
|
||||
from langsmith import client as ls_client
|
||||
from langsmith.schemas import FeedbackIngestToken
|
||||
from langsmith.utils import tracing_is_enabled
|
||||
from pydantic import BaseModel, Field, RootModel, ValidationError, create_model
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import JSONResponse, Response
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langserve._pydantic import _create_root_model
|
||||
from langserve.callbacks import AsyncEventAggregatorCallback, CallbackEventDict
|
||||
from langserve.lzstring import LZString
|
||||
from langserve.playground import serve_playground
|
||||
from langserve.pydantic_v1 import BaseModel, Field, ValidationError, create_model
|
||||
from langserve.schema import (
|
||||
BatchResponseMetadata,
|
||||
CustomUserType,
|
||||
@@ -184,11 +186,11 @@ async def _unpack_request_config(
|
||||
config_dicts = []
|
||||
for config in client_sent_configs:
|
||||
if isinstance(config, str):
|
||||
config_dicts.append(model(**_config_from_hash(config)).dict())
|
||||
config_dicts.append(model(**_config_from_hash(config)).model_dump())
|
||||
elif isinstance(config, BaseModel):
|
||||
config_dicts.append(config.dict())
|
||||
config_dicts.append(config.model_dump())
|
||||
elif isinstance(config, Mapping):
|
||||
config_dicts.append(model(**config).dict())
|
||||
config_dicts.append(model(**config).model_dump())
|
||||
else:
|
||||
raise TypeError(f"Expected a string, dict or BaseModel got {type(config)}")
|
||||
config = merge_configs(*config_dicts)
|
||||
@@ -255,10 +257,12 @@ def _update_config_with_defaults(
|
||||
}
|
||||
metadata.update(hosted_metadata)
|
||||
|
||||
non_overridable_default_config = RunnableConfig(
|
||||
run_name=run_name,
|
||||
metadata=metadata,
|
||||
)
|
||||
non_overridable_default_config: RunnableConfig = {
|
||||
"metadata": metadata,
|
||||
}
|
||||
|
||||
if run_name:
|
||||
non_overridable_default_config["run_name"] = run_name
|
||||
|
||||
# merge_configs is last-writer-wins, so we specifically pass in the
|
||||
# overridable configs first, then the user provided configs, then
|
||||
@@ -279,8 +283,8 @@ def _update_config_with_defaults(
|
||||
|
||||
def _unpack_input(validated_model: BaseModel) -> Any:
|
||||
"""Unpack the decoded input from the validated model."""
|
||||
if hasattr(validated_model, "__root__"):
|
||||
model = validated_model.__root__
|
||||
if isinstance(validated_model, RootModel):
|
||||
model = validated_model.root
|
||||
else:
|
||||
model = validated_model
|
||||
|
||||
@@ -294,7 +298,7 @@ def _unpack_input(validated_model: BaseModel) -> Any:
|
||||
# This logic should be applied recursively to nested models.
|
||||
return {
|
||||
fieldname: _unpack_input(getattr(model, fieldname))
|
||||
for fieldname in model.__fields__.keys()
|
||||
for fieldname in model.model_fields.keys()
|
||||
}
|
||||
|
||||
return model
|
||||
@@ -304,7 +308,7 @@ def _rename_pydantic_model(model: Type[BaseModel], prefix: str) -> Type[BaseMode
|
||||
"""Rename the given pydantic model to the given name."""
|
||||
return create_model(
|
||||
prefix + model.__name__,
|
||||
__config__=model.__config__,
|
||||
__config__=model.model_config,
|
||||
**{
|
||||
fieldname: (
|
||||
_rename_pydantic_model(field.annotation, prefix)
|
||||
@@ -313,10 +317,10 @@ def _rename_pydantic_model(model: Type[BaseModel], prefix: str) -> Type[BaseMode
|
||||
Field(
|
||||
field.default,
|
||||
title=fieldname,
|
||||
description=field.field_info.description,
|
||||
description=field.description,
|
||||
),
|
||||
)
|
||||
for fieldname, field in model.__fields__.items()
|
||||
for fieldname, field in model.model_fields.items()
|
||||
},
|
||||
)
|
||||
|
||||
@@ -326,6 +330,11 @@ def _replace_non_alphanumeric_with_underscores(s: str) -> str:
|
||||
return re.sub(r"[^a-zA-Z0-9]", "_", s)
|
||||
|
||||
|
||||
def _schema_json(model: Type[BaseModel]) -> str:
|
||||
"""Return the JSON representation of the model schema."""
|
||||
return json.dumps(model.model_json_schema(), sort_keys=True, indent=False)
|
||||
|
||||
|
||||
def _resolve_model(
|
||||
type_: Union[Type, BaseModel], default_name: str, namespace: str
|
||||
) -> Type[BaseModel]:
|
||||
@@ -333,15 +342,15 @@ def _resolve_model(
|
||||
if isclass(type_) and issubclass(type_, BaseModel):
|
||||
model = type_
|
||||
else:
|
||||
model = create_model(default_name, __root__=(type_, ...))
|
||||
model = _create_root_model(default_name, type_)
|
||||
|
||||
hash_ = model.schema_json()
|
||||
hash_ = _schema_json(model)
|
||||
|
||||
if model.__name__ in _SEEN_NAMES and hash_ not in _MODEL_REGISTRY:
|
||||
# If the model name has been seen before, but the model itself is different
|
||||
# generate a new name for the model.
|
||||
model_to_use = _rename_pydantic_model(model, namespace)
|
||||
hash_ = model_to_use.schema_json()
|
||||
hash_ = _schema_json(model_to_use)
|
||||
else:
|
||||
model_to_use = model
|
||||
|
||||
@@ -366,11 +375,7 @@ def _add_namespace_to_model(namespace: str, model: Type[BaseModel]) -> Type[Base
|
||||
A new model with name prepended with the given namespace.
|
||||
"""
|
||||
model_with_unique_name = _rename_pydantic_model(model, namespace)
|
||||
if "run_id" in model_with_unique_name.__annotations__:
|
||||
# Help resolve reference by providing namespace references
|
||||
model_with_unique_name.update_forward_refs(uuid=uuid)
|
||||
else:
|
||||
model_with_unique_name.update_forward_refs()
|
||||
model_with_unique_name.model_rebuild()
|
||||
return model_with_unique_name
|
||||
|
||||
|
||||
@@ -403,7 +408,7 @@ def _with_validation_error_translation() -> Generator[None, None, None]:
|
||||
try:
|
||||
yield
|
||||
except ValidationError as e:
|
||||
raise RequestValidationError(e.errors(), body=e.model)
|
||||
raise RequestValidationError(e.errors())
|
||||
|
||||
|
||||
def _json_encode_response(model: BaseModel) -> JSONResponse:
|
||||
@@ -423,27 +428,27 @@ def _json_encode_response(model: BaseModel) -> JSONResponse:
|
||||
|
||||
if isinstance(model, InvokeBaseResponse):
|
||||
# Invoke Response
|
||||
# Collapse '__root__' from output field if it exists. This is done
|
||||
# Collapse 'root' from output field if it exists. This is done
|
||||
# automatically by fastapi when annotating request and response with
|
||||
# We need to do this manually since we're using vanilla JSONResponse
|
||||
if isinstance(obj["output"], dict) and "__root__" in obj["output"]:
|
||||
obj["output"] = obj["output"]["__root__"]
|
||||
if isinstance(obj["output"], dict) and "root" in obj["output"]:
|
||||
obj["output"] = obj["output"]["root"]
|
||||
|
||||
if "callback_events" in obj:
|
||||
for idx, callback_event in enumerate(obj["callback_events"]):
|
||||
if isinstance(callback_event, dict) and "__root__" in callback_event:
|
||||
obj["callback_events"][idx] = callback_event["__root__"]
|
||||
if isinstance(callback_event, dict) and "root" in callback_event:
|
||||
obj["callback_events"][idx] = callback_event["root"]
|
||||
elif isinstance(model, BatchBaseResponse):
|
||||
if not isinstance(obj["output"], list):
|
||||
raise AssertionError("Expected output to be a list")
|
||||
|
||||
# Collapse '__root__' from output field if it exists. This is done
|
||||
# Collapse 'root' from output field if it exists. This is done
|
||||
# automatically by fastapi when annotating request and response with
|
||||
# We need to do this manually since we're using vanilla JSONResponse
|
||||
outputs = obj["output"]
|
||||
for idx, output in enumerate(outputs):
|
||||
if isinstance(output, dict) and "__root__" in output:
|
||||
outputs[idx] = output["__root__"]
|
||||
if isinstance(output, dict) and "root" in output:
|
||||
outputs[idx] = output["root"]
|
||||
|
||||
if "callback_events" in obj:
|
||||
if not isinstance(obj["callback_events"], list):
|
||||
@@ -451,11 +456,8 @@ def _json_encode_response(model: BaseModel) -> JSONResponse:
|
||||
|
||||
for callback_events in obj["callback_events"]:
|
||||
for idx, callback_event in enumerate(callback_events):
|
||||
if (
|
||||
isinstance(callback_event, dict)
|
||||
and "__root__" in callback_event
|
||||
):
|
||||
callback_events[idx] = callback_event["__root__"]
|
||||
if isinstance(callback_event, dict) and "root" in callback_event:
|
||||
callback_events[idx] = callback_event["root"]
|
||||
else:
|
||||
raise AssertionError(
|
||||
f"Expected a InvokeBaseResponse or BatchBaseResponse got: {type(model)}"
|
||||
@@ -470,7 +472,12 @@ def _add_callbacks(
|
||||
"""Add the callback aggregator to the config."""
|
||||
if "callbacks" not in config:
|
||||
config["callbacks"] = []
|
||||
config["callbacks"].extend(callbacks)
|
||||
if "callbacks" in config:
|
||||
if isinstance(config["callbacks"], list):
|
||||
config["callbacks"].extend(callbacks)
|
||||
elif isinstance(config["callbacks"], BaseCallbackManager):
|
||||
for callback in callbacks:
|
||||
config["callbacks"].add_handler(callback, inherit=True)
|
||||
|
||||
|
||||
_MODEL_REGISTRY = {}
|
||||
@@ -753,7 +760,7 @@ class APIHandler:
|
||||
except json.JSONDecodeError:
|
||||
raise RequestValidationError(errors=["Invalid JSON body"])
|
||||
try:
|
||||
body = InvokeRequestShallowValidator.validate(body)
|
||||
body = InvokeRequestShallowValidator.model_validate(body)
|
||||
|
||||
# Merge the config from the path with the config from the body.
|
||||
user_provided_config = await _unpack_request_config(
|
||||
@@ -775,7 +782,7 @@ class APIHandler:
|
||||
# This takes into account changes in the input type when
|
||||
# using configuration.
|
||||
schema = self._runnable.with_config(config).input_schema
|
||||
input_ = schema.validate(body.input)
|
||||
input_ = schema.model_validate(body.input)
|
||||
return config, _unpack_input(input_)
|
||||
except ValidationError as e:
|
||||
raise RequestValidationError(e.errors(), body=body)
|
||||
@@ -885,7 +892,7 @@ class APIHandler:
|
||||
raise RequestValidationError(errors=["Invalid JSON body"])
|
||||
|
||||
with _with_validation_error_translation():
|
||||
body = BatchRequestShallowValidator.validate(body)
|
||||
body = BatchRequestShallowValidator.model_validate(body)
|
||||
config = body.config
|
||||
|
||||
# First unpack the config
|
||||
@@ -936,7 +943,7 @@ class APIHandler:
|
||||
|
||||
inputs = [
|
||||
_unpack_input(
|
||||
self._runnable.with_config(config_).input_schema.validate(input_)
|
||||
self._runnable.with_config(config_).input_schema.model_validate(input_)
|
||||
)
|
||||
for config_, input_ in zip(configs_, inputs_)
|
||||
]
|
||||
@@ -1405,7 +1412,7 @@ class APIHandler:
|
||||
self._run_name, user_provided_config, request
|
||||
)
|
||||
|
||||
return self._runnable.get_input_schema(config).schema()
|
||||
return self._runnable.get_input_schema(config).model_json_schema()
|
||||
|
||||
async def output_schema(
|
||||
self,
|
||||
@@ -1432,7 +1439,7 @@ class APIHandler:
|
||||
config = _update_config_with_defaults(
|
||||
self._run_name, user_provided_config, request
|
||||
)
|
||||
return self._runnable.get_output_schema(config).schema()
|
||||
return self._runnable.get_output_schema(config).model_json_schema()
|
||||
|
||||
async def config_schema(
|
||||
self,
|
||||
@@ -1462,7 +1469,7 @@ class APIHandler:
|
||||
return (
|
||||
self._runnable.with_config(config)
|
||||
.config_schema(include=self._config_keys)
|
||||
.schema()
|
||||
.model_json_schema()
|
||||
)
|
||||
|
||||
async def playground(
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -5,7 +5,7 @@
|
||||
<link rel="icon" href="/____LANGSERVE_BASE_URL/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Chat Playground</title>
|
||||
<script type="module" crossorigin src="/____LANGSERVE_BASE_URL/assets/index-86d4d9c0.js"></script>
|
||||
<script type="module" crossorigin src="/____LANGSERVE_BASE_URL/assets/index-53ad47d4.js"></script>
|
||||
<link rel="stylesheet" href="/____LANGSERVE_BASE_URL/assets/index-434ff580.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -30,6 +30,7 @@ export function App() {
|
||||
);
|
||||
const outputSchemaSupported = (
|
||||
outputDataSchema?.anyOf?.find((option) => option.properties?.type?.enum?.includes("ai")) ||
|
||||
outputDataSchema?.oneOf?.find((option) => option.properties?.type?.enum?.includes("ai")) ||
|
||||
outputDataSchema?.type === "string"
|
||||
);
|
||||
const isSupported = isLoading || (inputSchemaSupported && outputSchemaSupported);
|
||||
|
||||
+6
-2
@@ -431,11 +431,15 @@ class RemoteRunnable(Runnable[Input, Output]):
|
||||
self,
|
||||
inputs: List[Input],
|
||||
config: Optional[RunnableConfig] = None,
|
||||
*,
|
||||
return_exceptions: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> List[Output]:
|
||||
if kwargs:
|
||||
raise NotImplementedError("kwargs not implemented yet.")
|
||||
return self._batch_with_config(self._batch, inputs, config)
|
||||
raise NotImplementedError(f"kwargs not implemented yet. Got {kwargs}")
|
||||
return self._batch_with_config(
|
||||
self._batch, inputs, config, return_exceptions=return_exceptions
|
||||
)
|
||||
|
||||
async def _abatch(
|
||||
self,
|
||||
|
||||
@@ -6,8 +6,7 @@ from typing import Literal, Sequence, Type
|
||||
|
||||
from fastapi.responses import Response
|
||||
from langchain_core.runnables import Runnable
|
||||
|
||||
from langserve.pydantic_v1 import BaseModel
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class PlaygroundTemplate(Template):
|
||||
@@ -90,10 +89,12 @@ async def serve_playground(
|
||||
if base_url.startswith("/")
|
||||
else base_url,
|
||||
LANGSERVE_CONFIG_SCHEMA=json.dumps(
|
||||
runnable.config_schema(include=config_keys).schema()
|
||||
runnable.config_schema(include=config_keys).model_json_schema()
|
||||
),
|
||||
LANGSERVE_INPUT_SCHEMA=json.dumps(input_schema.model_json_schema()),
|
||||
LANGSERVE_OUTPUT_SCHEMA=json.dumps(
|
||||
output_schema.model_json_schema()
|
||||
),
|
||||
LANGSERVE_INPUT_SCHEMA=json.dumps(input_schema.schema()),
|
||||
LANGSERVE_OUTPUT_SCHEMA=json.dumps(output_schema.schema()),
|
||||
LANGSERVE_FEEDBACK_ENABLED=json.dumps(
|
||||
"true" if feedback_enabled else "false"
|
||||
),
|
||||
|
||||
+47
-47
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -5,7 +5,7 @@
|
||||
<link rel="icon" href="/____LANGSERVE_BASE_URL/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Playground</title>
|
||||
<script type="module" crossorigin src="/____LANGSERVE_BASE_URL/assets/index-dbc96538.js"></script>
|
||||
<script type="module" crossorigin src="/____LANGSERVE_BASE_URL/assets/index-400979f0.js"></script>
|
||||
<link rel="stylesheet" href="/____LANGSERVE_BASE_URL/assets/index-52e8ab2f.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -6,12 +6,30 @@ import {
|
||||
schemaMatches,
|
||||
Paths,
|
||||
isControl,
|
||||
JsonSchema,
|
||||
} from "@jsonforms/core";
|
||||
import { useStreamCallback } from "../useStreamCallback";
|
||||
import { isJsonSchemaExtra } from "../utils/schema";
|
||||
import { MessageFields, ChatMessageInput } from "./ChatMessageInput";
|
||||
import { useEffect } from "react";
|
||||
|
||||
function checkItemSchema(schema: JsonSchema) {
|
||||
const isObjectMessage =
|
||||
schema.type === "object" &&
|
||||
(schema.title?.endsWith("Message") ||
|
||||
schema.title?.endsWith("MessageChunk"));
|
||||
|
||||
const isTupleMessage =
|
||||
schema.type === "array" &&
|
||||
schema.minItems === 2 &&
|
||||
schema.maxItems === 2 &&
|
||||
Array.isArray(schema.items) &&
|
||||
schema.items.length === 2 &&
|
||||
schema.items.every((schema) => schema.type === "string");
|
||||
|
||||
return isObjectMessage || isTupleMessage;
|
||||
}
|
||||
|
||||
export const chatMessagesTester = rankWith(
|
||||
12,
|
||||
and(
|
||||
@@ -34,22 +52,11 @@ export const chatMessagesTester = rankWith(
|
||||
}
|
||||
|
||||
if ("anyOf" in schema.items && schema.items.anyOf != null) {
|
||||
return schema.items.anyOf.every((schema) => {
|
||||
const isObjectMessage =
|
||||
schema.type === "object" &&
|
||||
(schema.title?.endsWith("Message") ||
|
||||
schema.title?.endsWith("MessageChunk"));
|
||||
return schema.items.anyOf.every(checkItemSchema);
|
||||
}
|
||||
|
||||
const isTupleMessage =
|
||||
schema.type === "array" &&
|
||||
schema.minItems === 2 &&
|
||||
schema.maxItems === 2 &&
|
||||
Array.isArray(schema.items) &&
|
||||
schema.items.length === 2 &&
|
||||
schema.items.every((schema) => schema.type === "string");
|
||||
|
||||
return isObjectMessage || isTupleMessage;
|
||||
});
|
||||
if ("oneOf" in schema.items && schema.items.oneOf != null) {
|
||||
return schema.items.oneOf.every(checkItemSchema);
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -64,10 +71,14 @@ export const ChatMessagesControlRenderer = withJsonFormsControlProps(
|
||||
useEffect(() => {
|
||||
if (!isJsonSchemaExtra(props.schema)) return;
|
||||
if (props.schema.extra.widget.type !== "chat") return;
|
||||
setTimeout(() => props.handleChange(props.path, [
|
||||
...data,
|
||||
{ content: "", type: "human" },
|
||||
]), 10);
|
||||
setTimeout(
|
||||
() =>
|
||||
props.handleChange(props.path, [
|
||||
...data,
|
||||
{ content: "", type: "human" },
|
||||
]),
|
||||
10
|
||||
);
|
||||
}, []);
|
||||
|
||||
useStreamCallback("onStart", () => {
|
||||
@@ -81,7 +92,10 @@ export const ChatMessagesControlRenderer = withJsonFormsControlProps(
|
||||
if (props.schema.extra.widget.type !== "chat") return;
|
||||
if (aggregatedState?.final_output !== undefined) {
|
||||
const msgPath = Paths.compose(props.path, `${data.length - 1}`);
|
||||
if ((aggregatedState.final_output as MessageFields)?.type === "AIMessageChunk") {
|
||||
if (
|
||||
(aggregatedState.final_output as MessageFields)?.type ===
|
||||
"AIMessageChunk"
|
||||
) {
|
||||
props.handleChange(
|
||||
Paths.compose(msgPath, "content"),
|
||||
(aggregatedState.final_output as MessageFields)?.content
|
||||
@@ -140,7 +154,7 @@ export const ChatMessagesControlRenderer = withJsonFormsControlProps(
|
||||
props.path,
|
||||
data.filter((_, i) => i !== index)
|
||||
);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<ChatMessageInput
|
||||
message={message}
|
||||
@@ -148,7 +162,7 @@ export const ChatMessagesControlRenderer = withJsonFormsControlProps(
|
||||
handleRemoval={handleChatMessageRemoval}
|
||||
path={props.path}
|
||||
key={index}
|
||||
></ChatMessageInput>
|
||||
></ChatMessageInput>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
from importlib import metadata
|
||||
|
||||
## Create namespaces for pydantic v1 and v2.
|
||||
# This code must stay at the top of the file before other modules may
|
||||
# attempt to import pydantic since it adds pydantic_v1 and pydantic_v2 to sys.modules.
|
||||
#
|
||||
# This hack is done for the following reasons:
|
||||
# * Langchain will attempt to remain compatible with both pydantic v1 and v2 since
|
||||
# both dependencies and dependents may be stuck on either version of v1 or v2.
|
||||
# * Creating namespaces for pydantic v1 and v2 should allow us to write code that
|
||||
# unambiguously uses either v1 or v2 API.
|
||||
# * This change is easier to roll out and roll back.
|
||||
|
||||
try:
|
||||
# F401: imported but unused
|
||||
from pydantic.v1 import ( # noqa: F401
|
||||
BaseModel,
|
||||
Field,
|
||||
ValidationError,
|
||||
create_model,
|
||||
)
|
||||
except ImportError:
|
||||
from pydantic import BaseModel, Field, ValidationError, create_model # noqa: F401
|
||||
|
||||
|
||||
# This is not a pydantic v1 thing, but it feels too small to create a new module for.
|
||||
|
||||
PYDANTIC_VERSION = metadata.version("pydantic")
|
||||
|
||||
try:
|
||||
_PYDANTIC_MAJOR_VERSION: int = int(PYDANTIC_VERSION.split(".")[0])
|
||||
except metadata.PackageNotFoundError:
|
||||
_PYDANTIC_MAJOR_VERSION = -1
|
||||
+5
-4
@@ -2,10 +2,11 @@ from datetime import datetime
|
||||
from typing import Dict, List, Optional, Union
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel # Floats between v1 and v2
|
||||
|
||||
from langserve.pydantic_v1 import BaseModel as BaseModelV1
|
||||
from langserve.pydantic_v1 import Field
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
Field,
|
||||
)
|
||||
from pydantic import BaseModel as BaseModelV1
|
||||
|
||||
|
||||
class CustomUserType(BaseModelV1):
|
||||
|
||||
+57
-41
@@ -13,7 +13,7 @@ sensitive information from the server to the client.
|
||||
import abc
|
||||
import logging
|
||||
from functools import lru_cache
|
||||
from typing import Any, Dict, List, Union
|
||||
from typing import Annotated, Any, Dict, List, Union
|
||||
|
||||
import orjson
|
||||
from langchain_core.agents import AgentAction, AgentActionMessageLog, AgentFinish
|
||||
@@ -29,6 +29,8 @@ from langchain_core.messages import (
|
||||
HumanMessageChunk,
|
||||
SystemMessage,
|
||||
SystemMessageChunk,
|
||||
ToolMessage,
|
||||
ToolMessageChunk,
|
||||
)
|
||||
from langchain_core.outputs import (
|
||||
ChatGeneration,
|
||||
@@ -38,8 +40,8 @@ from langchain_core.outputs import (
|
||||
)
|
||||
from langchain_core.prompt_values import ChatPromptValueConcrete
|
||||
from langchain_core.prompts.base import StringPromptValue
|
||||
from pydantic import BaseModel, Discriminator, Field, RootModel, Tag, ValidationError
|
||||
|
||||
from langserve.pydantic_v1 import BaseModel, ValidationError
|
||||
from langserve.validation import CallbackEvent
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -51,42 +53,58 @@ def _log_error_message_once(error_message: str) -> None:
|
||||
logger.error(error_message)
|
||||
|
||||
|
||||
class WellKnownLCObject(BaseModel):
|
||||
"""A well known LangChain object.
|
||||
def _get_type(v: Any) -> str:
|
||||
"""Get the type associated with the object for serialization purposes."""
|
||||
if isinstance(v, dict) and "type" in v:
|
||||
return v["type"]
|
||||
elif hasattr(v, "type"):
|
||||
return v.type
|
||||
else:
|
||||
raise TypeError(
|
||||
f"Expected either a dictionary with a 'type' key or an object "
|
||||
f"with a 'type' attribute. Instead got type {type(v)}."
|
||||
)
|
||||
|
||||
A pydantic model that defines what constitutes a well known LangChain object.
|
||||
|
||||
All well-known objects are allowed to be serialized and de-serialized.
|
||||
"""
|
||||
# A well known LangChain object.
|
||||
# A pydantic model that defines what constitutes a well known LangChain object.
|
||||
# All well-known objects are allowed to be serialized and de-serialized.
|
||||
|
||||
__root__: Union[
|
||||
Document,
|
||||
HumanMessage,
|
||||
SystemMessage,
|
||||
ChatMessage,
|
||||
FunctionMessage,
|
||||
AIMessage,
|
||||
HumanMessageChunk,
|
||||
SystemMessageChunk,
|
||||
ChatMessageChunk,
|
||||
FunctionMessageChunk,
|
||||
AIMessageChunk,
|
||||
StringPromptValue,
|
||||
ChatPromptValueConcrete,
|
||||
AgentAction,
|
||||
AgentFinish,
|
||||
AgentActionMessageLog,
|
||||
LLMResult,
|
||||
ChatGeneration,
|
||||
Generation,
|
||||
ChatGenerationChunk,
|
||||
WellKnownLCObject = RootModel[
|
||||
Annotated[
|
||||
Union[
|
||||
Annotated[AIMessage, Tag(tag="ai")],
|
||||
Annotated[HumanMessage, Tag(tag="human")],
|
||||
Annotated[ChatMessage, Tag(tag="chat")],
|
||||
Annotated[SystemMessage, Tag(tag="system")],
|
||||
Annotated[FunctionMessage, Tag(tag="function")],
|
||||
Annotated[ToolMessage, Tag(tag="tool")],
|
||||
Annotated[AIMessageChunk, Tag(tag="AIMessageChunk")],
|
||||
Annotated[HumanMessageChunk, Tag(tag="HumanMessageChunk")],
|
||||
Annotated[ChatMessageChunk, Tag(tag="ChatMessageChunk")],
|
||||
Annotated[SystemMessageChunk, Tag(tag="SystemMessageChunk")],
|
||||
Annotated[FunctionMessageChunk, Tag(tag="FunctionMessageChunk")],
|
||||
Annotated[ToolMessageChunk, Tag(tag="ToolMessageChunk")],
|
||||
Annotated[Document, Tag(tag="Document")],
|
||||
Annotated[StringPromptValue, Tag(tag="StringPromptValue")],
|
||||
Annotated[ChatPromptValueConcrete, Tag(tag="ChatPromptValueConcrete")],
|
||||
Annotated[AgentAction, Tag(tag="AgentAction")],
|
||||
Annotated[AgentFinish, Tag(tag="AgentFinish")],
|
||||
Annotated[AgentActionMessageLog, Tag(tag="AgentActionMessageLog")],
|
||||
Annotated[ChatGeneration, Tag(tag="ChatGeneration")],
|
||||
Annotated[Generation, Tag(tag="Generation")],
|
||||
Annotated[ChatGenerationChunk, Tag(tag="ChatGenerationChunk")],
|
||||
Annotated[LLMResult, Tag(tag="LLMResult")],
|
||||
],
|
||||
Field(discriminator=Discriminator(_get_type)),
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
def default(obj) -> Any:
|
||||
"""Default serialization for well known objects."""
|
||||
if isinstance(obj, BaseModel):
|
||||
return obj.dict()
|
||||
return obj.model_dump()
|
||||
return super().default(obj)
|
||||
|
||||
|
||||
@@ -96,12 +114,10 @@ def _decode_lc_objects(value: Any) -> Any:
|
||||
v = {key: _decode_lc_objects(v) for key, v in value.items()}
|
||||
|
||||
try:
|
||||
obj = WellKnownLCObject.parse_obj(v)
|
||||
parsed = obj.__root__
|
||||
if set(parsed.dict()) != set(value):
|
||||
raise ValueError("Invalid object")
|
||||
obj = WellKnownLCObject.model_validate(v)
|
||||
parsed = obj.root
|
||||
return parsed
|
||||
except (ValidationError, ValueError):
|
||||
except (ValidationError, ValueError, TypeError):
|
||||
return v
|
||||
elif isinstance(value, list):
|
||||
return [_decode_lc_objects(item) for item in value]
|
||||
@@ -123,12 +139,12 @@ def _decode_event_data(value: Any) -> Any:
|
||||
"""Decode the event data from a JSON object representation."""
|
||||
if isinstance(value, dict):
|
||||
try:
|
||||
obj = CallbackEvent.parse_obj(value)
|
||||
return obj.__root__
|
||||
obj = CallbackEvent.model_validate(value)
|
||||
return obj.root
|
||||
except ValidationError:
|
||||
try:
|
||||
obj = WellKnownLCObject.parse_obj(value)
|
||||
return obj.__root__
|
||||
obj = WellKnownLCObject.model_validate(value)
|
||||
return obj.root
|
||||
except ValidationError:
|
||||
return {key: _decode_event_data(v) for key, v in value.items()}
|
||||
elif isinstance(value, list):
|
||||
@@ -178,7 +194,7 @@ class WellKnownLCSerializer(Serializer):
|
||||
|
||||
def _project_top_level(model: BaseModel) -> Dict[str, Any]:
|
||||
"""Project the top level of the model as dict."""
|
||||
return {key: getattr(model, key) for key in model.__fields__}
|
||||
return {key: getattr(model, key) for key in model.model_fields}
|
||||
|
||||
|
||||
def load_events(events: Any) -> List[Dict[str, Any]]:
|
||||
@@ -209,7 +225,7 @@ def load_events(events: Any) -> List[Dict[str, Any]]:
|
||||
|
||||
# Then validate the event
|
||||
try:
|
||||
full_event = CallbackEvent.parse_obj(decoded_event_data)
|
||||
full_event = CallbackEvent.model_validate(decoded_event_data)
|
||||
except ValidationError as e:
|
||||
msg = f"Encountered an invalid event: {e}"
|
||||
if "type" in decoded_event_data:
|
||||
@@ -217,7 +233,7 @@ def load_events(events: Any) -> List[Dict[str, Any]]:
|
||||
_log_error_message_once(msg)
|
||||
continue
|
||||
|
||||
decoded_event_data = _project_top_level(full_event.__root__)
|
||||
decoded_event_data = _project_top_level(full_event.root)
|
||||
|
||||
if decoded_event_data["type"].endswith("_error"):
|
||||
# Data is validated by this point, so we can assume that the shape
|
||||
|
||||
+296
-336
@@ -5,6 +5,7 @@ This code contains integration for langchain runnables with FastAPI.
|
||||
The main entry point is the `add_routes` function which adds the routes to an existing
|
||||
FastAPI app or APIRouter.
|
||||
"""
|
||||
import warnings
|
||||
import weakref
|
||||
from typing import (
|
||||
Any,
|
||||
@@ -16,6 +17,7 @@ from typing import (
|
||||
)
|
||||
|
||||
from langchain_core.runnables import Runnable
|
||||
from pydantic import BaseModel
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from langserve.api_handler import (
|
||||
@@ -24,11 +26,6 @@ from langserve.api_handler import (
|
||||
TokenFeedbackConfig,
|
||||
_is_hosted,
|
||||
)
|
||||
from langserve.pydantic_v1 import (
|
||||
_PYDANTIC_MAJOR_VERSION,
|
||||
PYDANTIC_VERSION,
|
||||
BaseModel,
|
||||
)
|
||||
|
||||
try:
|
||||
from fastapi import APIRouter, Depends, FastAPI, Request, Response
|
||||
@@ -205,51 +202,43 @@ def _register_path_for_app(
|
||||
def _setup_global_app_handlers(
|
||||
app: Union[FastAPI, APIRouter], endpoint_configuration: _EndpointConfiguration
|
||||
) -> None:
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
LANGSERVE = r"""
|
||||
__ ___ .__ __. _______ _______. _______ .______ ____ ____ _______
|
||||
| | / \ | \ | | / _____| / || ____|| _ \ \ \ / / | ____|
|
||||
| | / ^ \ | \| | | | __ | (----`| |__ | |_) | \ \/ / | |__
|
||||
| | / /_\ \ | . ` | | | |_ | \ \ | __| | / \ / | __|
|
||||
| `----./ _____ \ | |\ | | |__| | .----) | | |____ | |\ \----. \ / | |____
|
||||
|_______/__/ \__\ |__| \__| \______| |_______/ |_______|| _| `._____| \__/ |_______|
|
||||
""" # noqa: E501
|
||||
with warnings.catch_warnings():
|
||||
# We are using deprecated functionality here.
|
||||
# We should re-write to use lifetime events at some point, and yielding
|
||||
# an APIRouter instance to the caller.
|
||||
warnings.filterwarnings(
|
||||
"ignore",
|
||||
"[\\s.]*on_event is deprecated[\\s.]*",
|
||||
category=DeprecationWarning,
|
||||
)
|
||||
|
||||
def green(text: str) -> str:
|
||||
"""Return the given text in green."""
|
||||
return "\x1b[1;32;40m" + text + "\x1b[0m"
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
LANGSERVE = r"""
|
||||
__ ___ .__ __. _______ _______. _______ .______ ____ ____ _______
|
||||
| | / \ | \ | | / _____| / || ____|| _ \ \ \ / / | ____|
|
||||
| | / ^ \ | \| | | | __ | (----`| |__ | |_) | \ \/ / | |__
|
||||
| | / /_\ \ | . ` | | | |_ | \ \ | __| | / \ / | __|
|
||||
| `----./ _____ \ | |\ | | |__| | .----) | | |____ | |\ \----. \ / | |____
|
||||
|_______/__/ \__\ |__| \__| \______| |_______/ |_______|| _| `._____| \__/ |_______|
|
||||
""" # noqa: E501
|
||||
|
||||
def orange(text: str) -> str:
|
||||
"""Return the given text in orange."""
|
||||
return "\x1b[1;31;40m" + text + "\x1b[0m"
|
||||
def green(text: str) -> str:
|
||||
"""Return the given text in green."""
|
||||
return "\x1b[1;32;40m" + text + "\x1b[0m"
|
||||
|
||||
paths = _APP_TO_PATHS[app]
|
||||
print(LANGSERVE)
|
||||
for path in paths:
|
||||
if endpoint_configuration.is_playground_enabled:
|
||||
print(
|
||||
f'{green("LANGSERVE:")} Playground for chain "{path or ""}/" is '
|
||||
f"live at:"
|
||||
)
|
||||
print(f'{green("LANGSERVE:")} │')
|
||||
print(f'{green("LANGSERVE:")} └──> {path}/playground/')
|
||||
print(f'{green("LANGSERVE:")}')
|
||||
print(f'{green("LANGSERVE:")} See all available routes at {app.docs_url}/')
|
||||
|
||||
if _PYDANTIC_MAJOR_VERSION == 2:
|
||||
print()
|
||||
print(f'{orange("LANGSERVE:")} ', end="")
|
||||
print(
|
||||
f"⚠️ Using pydantic {PYDANTIC_VERSION}. "
|
||||
f"OpenAPI docs for invoke, batch, stream, stream_log "
|
||||
f"endpoints will not be generated. API endpoints and playground "
|
||||
f"should work as expected. "
|
||||
f"If you need to see the docs, you can downgrade to pydantic 1. "
|
||||
"For example, `pip install pydantic==1.10.13`. "
|
||||
f"See https://github.com/tiangolo/fastapi/issues/10360 for details."
|
||||
)
|
||||
print()
|
||||
paths = _APP_TO_PATHS[app]
|
||||
print(LANGSERVE)
|
||||
for path in paths:
|
||||
if endpoint_configuration.is_playground_enabled:
|
||||
print(
|
||||
f'{green("LANGSERVE:")} Playground for chain "{path or ""}/" '
|
||||
f'is live at:'
|
||||
)
|
||||
print(f'{green("LANGSERVE:")} │')
|
||||
print(f'{green("LANGSERVE:")} └──> {path}/playground/')
|
||||
print(f'{green("LANGSERVE:")}')
|
||||
print(f'{green("LANGSERVE:")} See all available routes at {app.docs_url}/')
|
||||
|
||||
|
||||
# PUBLIC API
|
||||
@@ -473,35 +462,9 @@ def add_routes(
|
||||
if hasattr(app, "openapi_tags") and (path or (app not in _APP_SEEN)):
|
||||
if not path:
|
||||
_APP_SEEN.add(app)
|
||||
|
||||
if _PYDANTIC_MAJOR_VERSION == 1:
|
||||
# Documentation for the default endpoints
|
||||
default_endpoint_tags = {
|
||||
"name": route_tags[0] if route_tags else "default",
|
||||
}
|
||||
elif _PYDANTIC_MAJOR_VERSION == 2:
|
||||
# When using pydantic v2, we cannot generate openapi docs for
|
||||
# the invoke/batch/stream/stream_log endpoints since the underlying
|
||||
# models are from the pydantic.v1 namespace and cannot be supported
|
||||
# by FastAPI's.
|
||||
# https://github.com/tiangolo/fastapi/issues/10360
|
||||
default_endpoint_tags = {
|
||||
"name": route_tags[0] if route_tags else "default",
|
||||
"description": (
|
||||
f"⚠️ Using pydantic {PYDANTIC_VERSION}. "
|
||||
f"OpenAPI docs for `invoke`, `batch`, `stream`, `stream_log` "
|
||||
f"endpoints will not be generated. API endpoints and playground "
|
||||
f"should work as expected. "
|
||||
f"If you need to see the docs, you can downgrade to pydantic 1. "
|
||||
"For example, `pip install pydantic==1.10.13`"
|
||||
f"See https://github.com/tiangolo/fastapi/issues/10360 for details."
|
||||
),
|
||||
}
|
||||
else:
|
||||
raise AssertionError(
|
||||
f"Expected pydantic major version 1 or 2, got {_PYDANTIC_MAJOR_VERSION}"
|
||||
)
|
||||
|
||||
default_endpoint_tags = {
|
||||
"name": route_tags[0] if route_tags else "default",
|
||||
}
|
||||
if endpoint_configuration.is_config_hash_enabled:
|
||||
app.openapi_tags = [
|
||||
*(getattr(app, "openapi_tags", []) or []),
|
||||
@@ -778,329 +741,326 @@ def add_routes(
|
||||
# Documentation variants of end points.
|
||||
#######################################
|
||||
# At the moment, we only support pydantic 1.x for documentation
|
||||
if _PYDANTIC_MAJOR_VERSION == 1:
|
||||
InvokeRequest = api_handler.InvokeRequest
|
||||
InvokeResponse = api_handler.InvokeResponse
|
||||
BatchRequest = api_handler.BatchRequest
|
||||
BatchResponse = api_handler.BatchResponse
|
||||
StreamRequest = api_handler.StreamRequest
|
||||
StreamLogRequest = api_handler.StreamLogRequest
|
||||
StreamEventsRequest = api_handler.StreamEventsRequest
|
||||
InvokeRequest = api_handler.InvokeRequest
|
||||
InvokeResponse = api_handler.InvokeResponse
|
||||
BatchRequest = api_handler.BatchRequest
|
||||
BatchResponse = api_handler.BatchResponse
|
||||
StreamRequest = api_handler.StreamRequest
|
||||
StreamLogRequest = api_handler.StreamLogRequest
|
||||
StreamEventsRequest = api_handler.StreamEventsRequest
|
||||
|
||||
if endpoint_configuration.is_invoke_enabled:
|
||||
if endpoint_configuration.is_invoke_enabled:
|
||||
|
||||
async def _invoke_docs(
|
||||
invoke_request: Annotated[InvokeRequest, InvokeRequest],
|
||||
config_hash: str = "",
|
||||
) -> InvokeResponse:
|
||||
"""Invoke the runnable with the given input and config."""
|
||||
raise AssertionError("This endpoint should not be reachable.")
|
||||
async def _invoke_docs(
|
||||
invoke_request: Annotated[InvokeRequest, InvokeRequest],
|
||||
config_hash: str = "",
|
||||
) -> InvokeResponse:
|
||||
"""Invoke the runnable with the given input and config."""
|
||||
raise AssertionError("This endpoint should not be reachable.")
|
||||
|
||||
invoke_docs = app.post(
|
||||
f"{namespace}/invoke",
|
||||
invoke_docs = app.post(
|
||||
f"{namespace}/invoke",
|
||||
response_model=api_handler.InvokeResponse,
|
||||
tags=route_tags,
|
||||
name=_route_name("invoke"),
|
||||
dependencies=dependencies,
|
||||
)(_invoke_docs)
|
||||
|
||||
if endpoint_configuration.is_config_hash_enabled:
|
||||
app.post(
|
||||
namespace + "/c/{config_hash}/invoke",
|
||||
response_model=api_handler.InvokeResponse,
|
||||
tags=route_tags,
|
||||
name=_route_name("invoke"),
|
||||
tags=route_tags_with_config,
|
||||
name=_route_name_with_config("invoke"),
|
||||
dependencies=dependencies,
|
||||
)(_invoke_docs)
|
||||
description=(
|
||||
"This endpoint is to be used with share links generated by the "
|
||||
"LangServe playground. "
|
||||
"The hash is an LZString compressed JSON string. "
|
||||
"For regular use cases, use the /invoke endpoint without "
|
||||
"the `c/{config_hash}` path parameter."
|
||||
),
|
||||
)(invoke_docs)
|
||||
|
||||
if endpoint_configuration.is_config_hash_enabled:
|
||||
app.post(
|
||||
namespace + "/c/{config_hash}/invoke",
|
||||
response_model=api_handler.InvokeResponse,
|
||||
tags=route_tags_with_config,
|
||||
name=_route_name_with_config("invoke"),
|
||||
dependencies=dependencies,
|
||||
description=(
|
||||
"This endpoint is to be used with share links generated by the "
|
||||
"LangServe playground. "
|
||||
"The hash is an LZString compressed JSON string. "
|
||||
"For regular use cases, use the /invoke endpoint without "
|
||||
"the `c/{config_hash}` path parameter."
|
||||
),
|
||||
)(invoke_docs)
|
||||
if endpoint_configuration.is_batch_enabled:
|
||||
|
||||
if endpoint_configuration.is_batch_enabled:
|
||||
async def _batch_docs(
|
||||
batch_request: Annotated[BatchRequest, BatchRequest],
|
||||
config_hash: str = "",
|
||||
) -> BatchResponse:
|
||||
"""Batch invoke the runnable with the given inputs and config."""
|
||||
raise AssertionError("This endpoint should not be reachable.")
|
||||
|
||||
async def _batch_docs(
|
||||
batch_request: Annotated[BatchRequest, BatchRequest],
|
||||
config_hash: str = "",
|
||||
) -> BatchResponse:
|
||||
"""Batch invoke the runnable with the given inputs and config."""
|
||||
raise AssertionError("This endpoint should not be reachable.")
|
||||
batch_docs = app.post(
|
||||
f"{namespace}/batch",
|
||||
response_model=BatchResponse,
|
||||
tags=route_tags,
|
||||
name=_route_name("batch"),
|
||||
dependencies=dependencies,
|
||||
)(_batch_docs)
|
||||
|
||||
batch_docs = app.post(
|
||||
f"{namespace}/batch",
|
||||
if endpoint_configuration.is_config_hash_enabled:
|
||||
app.post(
|
||||
namespace + "/c/{config_hash}/batch",
|
||||
response_model=BatchResponse,
|
||||
tags=route_tags,
|
||||
name=_route_name("batch"),
|
||||
tags=route_tags_with_config,
|
||||
name=_route_name_with_config("batch"),
|
||||
dependencies=dependencies,
|
||||
)(_batch_docs)
|
||||
description=(
|
||||
"This endpoint is to be used with share links generated by the "
|
||||
"LangServe playground. "
|
||||
"The hash is an LZString compressed JSON string. "
|
||||
"For regular use cases, use the /batch endpoint without "
|
||||
"the `c/{config_hash}` path parameter."
|
||||
),
|
||||
)(batch_docs)
|
||||
|
||||
if endpoint_configuration.is_config_hash_enabled:
|
||||
app.post(
|
||||
namespace + "/c/{config_hash}/batch",
|
||||
response_model=BatchResponse,
|
||||
tags=route_tags_with_config,
|
||||
name=_route_name_with_config("batch"),
|
||||
dependencies=dependencies,
|
||||
description=(
|
||||
"This endpoint is to be used with share links generated by the "
|
||||
"LangServe playground. "
|
||||
"The hash is an LZString compressed JSON string. "
|
||||
"For regular use cases, use the /batch endpoint without "
|
||||
"the `c/{config_hash}` path parameter."
|
||||
),
|
||||
)(batch_docs)
|
||||
if endpoint_configuration.is_stream_enabled:
|
||||
|
||||
if endpoint_configuration.is_stream_enabled:
|
||||
async def _stream_docs(
|
||||
stream_request: Annotated[StreamRequest, StreamRequest],
|
||||
config_hash: str = "",
|
||||
) -> EventSourceResponse:
|
||||
"""Invoke the runnable stream the output.
|
||||
|
||||
async def _stream_docs(
|
||||
stream_request: Annotated[StreamRequest, StreamRequest],
|
||||
config_hash: str = "",
|
||||
) -> EventSourceResponse:
|
||||
"""Invoke the runnable stream the output.
|
||||
This endpoint allows to stream the output of the runnable.
|
||||
|
||||
This endpoint allows to stream the output of the runnable.
|
||||
The endpoint uses a server sent event stream to stream the output.
|
||||
|
||||
The endpoint uses a server sent event stream to stream the output.
|
||||
https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events
|
||||
|
||||
https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events
|
||||
Important: Set the "text/event-stream" media type for request headers if
|
||||
not using an existing SDK.
|
||||
|
||||
Important: Set the "text/event-stream" media type for request headers if
|
||||
not using an existing SDK.
|
||||
The events that the endpoint uses are the following:
|
||||
* "data" -- used for streaming the output of the runnale
|
||||
* "error" -- signaling an error while streaming and ends the stream.
|
||||
* "end" -- used for signaling the end of the stream
|
||||
* "metadata" -- used for sending metadata about the run; e.g., run id.
|
||||
|
||||
The events that the endpoint uses are the following:
|
||||
* "data" -- used for streaming the output of the runnale
|
||||
* "error" -- signaling an error while streaming and ends the stream.
|
||||
* "end" -- used for signaling the end of the stream
|
||||
* "metadata" -- used for sending metadata about the run; e.g., run id.
|
||||
|
||||
The event type is in the "event" field of the event.
|
||||
The payload associated with the event is in the "data" field
|
||||
of the event, and it is JSON encoded.
|
||||
The event type is in the "event" field of the event.
|
||||
The payload associated with the event is in the "data" field
|
||||
of the event, and it is JSON encoded.
|
||||
|
||||
|
||||
Here are some examples of events that the endpoint can send:
|
||||
Here are some examples of events that the endpoint can send:
|
||||
|
||||
Regular streaming event:
|
||||
{
|
||||
"event": "data",
|
||||
"data": {
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
Internal server error:
|
||||
{
|
||||
"event": "error",
|
||||
"data": {
|
||||
"status_code": 500,
|
||||
"message": "Internal Server Error"
|
||||
}
|
||||
}
|
||||
|
||||
Streaming ended so client should stop listening for events:
|
||||
{
|
||||
"event": "end",
|
||||
}
|
||||
"""
|
||||
raise AssertionError("This endpoint should not be reachable.")
|
||||
|
||||
stream_docs = app.post(
|
||||
f"{namespace}/stream",
|
||||
include_in_schema=True,
|
||||
tags=route_tags,
|
||||
name=_route_name("stream"),
|
||||
dependencies=dependencies,
|
||||
description=(
|
||||
"This endpoint allows to stream the output of the runnable. "
|
||||
"The endpoint uses a server sent event stream to stream the "
|
||||
"output. "
|
||||
"https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events"
|
||||
),
|
||||
)(_stream_docs)
|
||||
|
||||
if endpoint_configuration.is_config_hash_enabled:
|
||||
app.post(
|
||||
namespace + "/c/{config_hash}/stream",
|
||||
include_in_schema=True,
|
||||
tags=route_tags_with_config,
|
||||
name=_route_name_with_config("stream"),
|
||||
dependencies=dependencies,
|
||||
description=(
|
||||
"This endpoint is to be used with share links generated by the "
|
||||
"LangServe playground. "
|
||||
"The hash is an LZString compressed JSON string. "
|
||||
"For regular use cases, use the /stream endpoint without "
|
||||
"the `c/{config_hash}` path parameter."
|
||||
),
|
||||
)(stream_docs)
|
||||
|
||||
if endpoint_configuration.is_stream_log_enabled:
|
||||
|
||||
async def _stream_log_docs(
|
||||
stream_log_request: Annotated[StreamLogRequest, StreamLogRequest],
|
||||
config_hash: str = "",
|
||||
) -> EventSourceResponse:
|
||||
"""Invoke the runnable stream_log the output.
|
||||
|
||||
This endpoint allows to stream the output of the runnable, including
|
||||
the output of all intermediate steps.
|
||||
|
||||
The endpoint uses a server sent event stream to stream the output.
|
||||
|
||||
https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events
|
||||
|
||||
Important: Set the "text/event-stream" media type for request headers if
|
||||
not using an existing SDK.
|
||||
|
||||
This endpoint uses two different types of events:
|
||||
|
||||
* data - for streaming the output of the runnable
|
||||
|
||||
Regular streaming event:
|
||||
{
|
||||
"event": "data",
|
||||
"data": {
|
||||
...
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
Internal server error:
|
||||
{
|
||||
"event": "error",
|
||||
"data": {
|
||||
"status_code": 500,
|
||||
"message": "Internal Server Error"
|
||||
}
|
||||
}
|
||||
* error - for signaling an error in the stream, also ends the stream.
|
||||
|
||||
{
|
||||
"event": "error",
|
||||
"data": {
|
||||
"status_code": 500,
|
||||
"message": "Internal Server Error"
|
||||
}
|
||||
}
|
||||
|
||||
* end - for signaling the end of the stream.
|
||||
|
||||
This helps the client to know when to stop listening for events and
|
||||
know that the streaming has ended successfully.
|
||||
|
||||
Streaming ended so client should stop listening for events:
|
||||
{
|
||||
"event": "end",
|
||||
}
|
||||
"""
|
||||
raise AssertionError("This endpoint should not be reachable.")
|
||||
"""
|
||||
raise AssertionError("This endpoint should not be reachable.")
|
||||
|
||||
stream_docs = app.post(
|
||||
f"{namespace}/stream",
|
||||
include_in_schema=True,
|
||||
tags=route_tags,
|
||||
name=_route_name("stream"),
|
||||
dependencies=dependencies,
|
||||
description=(
|
||||
"This endpoint allows to stream the output of the runnable. "
|
||||
"The endpoint uses a server sent event stream to stream the "
|
||||
"output. "
|
||||
"https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events"
|
||||
),
|
||||
)(_stream_docs)
|
||||
|
||||
if endpoint_configuration.is_config_hash_enabled:
|
||||
app.post(
|
||||
namespace + "/c/{config_hash}/stream",
|
||||
include_in_schema=True,
|
||||
tags=route_tags_with_config,
|
||||
name=_route_name_with_config("stream"),
|
||||
dependencies=dependencies,
|
||||
description=(
|
||||
"This endpoint is to be used with share links generated by the "
|
||||
"LangServe playground. "
|
||||
"The hash is an LZString compressed JSON string. "
|
||||
"For regular use cases, use the /stream endpoint without "
|
||||
"the `c/{config_hash}` path parameter."
|
||||
),
|
||||
)(stream_docs)
|
||||
|
||||
if endpoint_configuration.is_stream_log_enabled:
|
||||
|
||||
async def _stream_log_docs(
|
||||
stream_log_request: Annotated[StreamLogRequest, StreamLogRequest],
|
||||
config_hash: str = "",
|
||||
) -> EventSourceResponse:
|
||||
"""Invoke the runnable stream_log the output.
|
||||
|
||||
This endpoint allows to stream the output of the runnable, including
|
||||
the output of all intermediate steps.
|
||||
|
||||
The endpoint uses a server sent event stream to stream the output.
|
||||
|
||||
https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events
|
||||
|
||||
Important: Set the "text/event-stream" media type for request headers if
|
||||
not using an existing SDK.
|
||||
|
||||
This endpoint uses two different types of events:
|
||||
|
||||
* data - for streaming the output of the runnable
|
||||
|
||||
{
|
||||
"event": "data",
|
||||
"data": {
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
* error - for signaling an error in the stream, also ends the stream.
|
||||
|
||||
{
|
||||
"event": "error",
|
||||
"data": {
|
||||
"status_code": 500,
|
||||
"message": "Internal Server Error"
|
||||
}
|
||||
}
|
||||
|
||||
* end - for signaling the end of the stream.
|
||||
|
||||
This helps the client to know when to stop listening for events and
|
||||
know that the streaming has ended successfully.
|
||||
|
||||
{
|
||||
"event": "end",
|
||||
}
|
||||
"""
|
||||
raise AssertionError("This endpoint should not be reachable.")
|
||||
app.post(
|
||||
f"{namespace}/stream_log",
|
||||
include_in_schema=True,
|
||||
tags=route_tags,
|
||||
name=_route_name("stream_log"),
|
||||
dependencies=dependencies,
|
||||
)(_stream_log_docs)
|
||||
|
||||
if endpoint_configuration.is_config_hash_enabled:
|
||||
app.post(
|
||||
f"{namespace}/stream_log",
|
||||
namespace + "/c/{config_hash}/stream_log",
|
||||
include_in_schema=True,
|
||||
tags=route_tags,
|
||||
name=_route_name("stream_log"),
|
||||
tags=route_tags_with_config,
|
||||
name=_route_name_with_config("stream_log"),
|
||||
description=(
|
||||
"This endpoint is to be used with share links generated by the "
|
||||
"LangServe playground. "
|
||||
"The hash is an LZString compressed JSON string. "
|
||||
"For regular use cases, use the /stream_log endpoint without "
|
||||
"the `c/{config_hash}` path parameter."
|
||||
),
|
||||
dependencies=dependencies,
|
||||
)(_stream_log_docs)
|
||||
|
||||
if endpoint_configuration.is_config_hash_enabled:
|
||||
app.post(
|
||||
namespace + "/c/{config_hash}/stream_log",
|
||||
include_in_schema=True,
|
||||
tags=route_tags_with_config,
|
||||
name=_route_name_with_config("stream_log"),
|
||||
description=(
|
||||
"This endpoint is to be used with share links generated by the "
|
||||
"LangServe playground. "
|
||||
"The hash is an LZString compressed JSON string. "
|
||||
"For regular use cases, use the /stream_log endpoint without "
|
||||
"the `c/{config_hash}` path parameter."
|
||||
),
|
||||
dependencies=dependencies,
|
||||
)(_stream_log_docs)
|
||||
if has_astream_events and endpoint_configuration.is_stream_events_enabled:
|
||||
|
||||
if has_astream_events and endpoint_configuration.is_stream_events_enabled:
|
||||
async def _stream_events_docs(
|
||||
stream_events_request: Annotated[StreamEventsRequest, StreamEventsRequest],
|
||||
config_hash: str = "",
|
||||
) -> EventSourceResponse:
|
||||
"""Stream events from the given runnable.
|
||||
|
||||
async def _stream_events_docs(
|
||||
stream_events_request: Annotated[
|
||||
StreamEventsRequest, StreamEventsRequest
|
||||
],
|
||||
config_hash: str = "",
|
||||
) -> EventSourceResponse:
|
||||
"""Stream events from the given runnable.
|
||||
This endpoint allows to stream events from the runnable, including
|
||||
events from all intermediate steps.
|
||||
|
||||
This endpoint allows to stream events from the runnable, including
|
||||
events from all intermediate steps.
|
||||
**Attention**
|
||||
|
||||
**Attention**
|
||||
This is a new endpoint that only works for langchain-core >= 0.1.14.
|
||||
|
||||
This is a new endpoint that only works for langchain-core >= 0.1.14.
|
||||
It belongs to a Beta API that may change in the future.
|
||||
|
||||
It belongs to a Beta API that may change in the future.
|
||||
**Important**
|
||||
Specify filters to the events you want to receive by setting
|
||||
the appropriate filters in the request body.
|
||||
|
||||
**Important**
|
||||
Specify filters to the events you want to receive by setting
|
||||
the appropriate filters in the request body.
|
||||
This will help avoid sending too much data over the network.
|
||||
|
||||
This will help avoid sending too much data over the network.
|
||||
It will also prevent serialization issues with
|
||||
any unsupported types since it won't need to serialize events
|
||||
that aren't transmitted.
|
||||
|
||||
It will also prevent serialization issues with
|
||||
any unsupported types since it won't need to serialize events
|
||||
that aren't transmitted.
|
||||
The endpoint uses a server sent event stream to stream the output.
|
||||
|
||||
The endpoint uses a server sent event stream to stream the output.
|
||||
https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events
|
||||
|
||||
https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events
|
||||
The encoding of events follows the following format:
|
||||
|
||||
The encoding of events follows the following format:
|
||||
|
||||
* data - for streaming the output of the runnable
|
||||
|
||||
{
|
||||
"event": "data",
|
||||
"data": {
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
* error - for signaling an error in the stream, also ends the stream.
|
||||
* data - for streaming the output of the runnable
|
||||
|
||||
{
|
||||
"event": "error",
|
||||
"event": "data",
|
||||
"data": {
|
||||
"status_code": 500,
|
||||
"message": "Internal Server Error"
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
* end - for signaling the end of the stream.
|
||||
* error - for signaling an error in the stream, also ends the stream.
|
||||
|
||||
This helps the client to know when to stop listening for events and
|
||||
know that the streaming has ended successfully.
|
||||
{
|
||||
"event": "error",
|
||||
"data": {
|
||||
"status_code": 500,
|
||||
"message": "Internal Server Error"
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"event": "end",
|
||||
}
|
||||
* end - for signaling the end of the stream.
|
||||
|
||||
`data` for the `data` event is a JSON object that corresponds
|
||||
to a serialized representation of a StreamEvent.
|
||||
This helps the client to know when to stop listening for events and
|
||||
know that the streaming has ended successfully.
|
||||
|
||||
See LangChain documentation for more information about astream_events.
|
||||
"""
|
||||
raise AssertionError("This endpoint should not be reachable.")
|
||||
{
|
||||
"event": "end",
|
||||
}
|
||||
|
||||
`data` for the `data` event is a JSON object that corresponds
|
||||
to a serialized representation of a StreamEvent.
|
||||
|
||||
See LangChain documentation for more information about astream_events.
|
||||
"""
|
||||
raise AssertionError("This endpoint should not be reachable.")
|
||||
|
||||
app.post(
|
||||
f"{namespace}/stream_events",
|
||||
include_in_schema=True,
|
||||
tags=route_tags,
|
||||
name=_route_name("stream_events"),
|
||||
dependencies=dependencies,
|
||||
)(_stream_events_docs)
|
||||
|
||||
if endpoint_configuration.is_config_hash_enabled:
|
||||
app.post(
|
||||
f"{namespace}/stream_events",
|
||||
namespace + "/c/{config_hash}/stream_events",
|
||||
include_in_schema=True,
|
||||
tags=route_tags,
|
||||
name=_route_name("stream_events"),
|
||||
tags=route_tags_with_config,
|
||||
name=_route_name_with_config("stream_events"),
|
||||
description=(
|
||||
"This endpoint is to be used with share links generated by the "
|
||||
"LangServe playground. "
|
||||
"The hash is an LZString compressed JSON string. "
|
||||
"For regular use cases, use the /stream_events endpoint "
|
||||
"without the `c/{config_hash}` path parameter."
|
||||
),
|
||||
dependencies=dependencies,
|
||||
)(_stream_events_docs)
|
||||
|
||||
if endpoint_configuration.is_config_hash_enabled:
|
||||
app.post(
|
||||
namespace + "/c/{config_hash}/stream_events",
|
||||
include_in_schema=True,
|
||||
tags=route_tags_with_config,
|
||||
name=_route_name_with_config("stream_events"),
|
||||
description=(
|
||||
"This endpoint is to be used with share links generated by the "
|
||||
"LangServe playground. "
|
||||
"The hash is an LZString compressed JSON string. "
|
||||
"For regular use cases, use the /stream_events endpoint "
|
||||
"without the `c/{config_hash}` path parameter."
|
||||
),
|
||||
dependencies=dependencies,
|
||||
)(_stream_events_docs)
|
||||
|
||||
+11
-15
@@ -22,16 +22,11 @@ from uuid import UUID
|
||||
from langchain_core.documents import Document
|
||||
from langchain_core.messages import BaseMessage
|
||||
from langchain_core.outputs import ChatGeneration, Generation, RunInfo
|
||||
from pydantic import BaseModel, Field, RootModel, create_model
|
||||
from typing_extensions import Type
|
||||
|
||||
from langserve.schema import BatchResponseMetadata, InvokeResponseMetadata
|
||||
|
||||
try:
|
||||
from pydantic.v1 import BaseModel, Field, create_model
|
||||
except ImportError:
|
||||
from pydantic import BaseModel, Field, create_model
|
||||
|
||||
|
||||
# Type that is either a python annotation or a pydantic model that can be
|
||||
# used to validate the input or output of a runnable.
|
||||
Validator = Union[Type[BaseModel], type]
|
||||
@@ -66,7 +61,7 @@ def create_invoke_request_model(
|
||||
),
|
||||
),
|
||||
)
|
||||
invoke_request_type.update_forward_refs()
|
||||
invoke_request_type.model_rebuild()
|
||||
return invoke_request_type
|
||||
|
||||
|
||||
@@ -97,7 +92,7 @@ def create_stream_request_model(
|
||||
),
|
||||
),
|
||||
)
|
||||
stream_request_model.update_forward_refs()
|
||||
stream_request_model.model_rebuild()
|
||||
return stream_request_model
|
||||
|
||||
|
||||
@@ -129,7 +124,7 @@ def create_batch_request_model(
|
||||
),
|
||||
),
|
||||
)
|
||||
batch_request_type.update_forward_refs()
|
||||
batch_request_type.model_rebuild()
|
||||
return batch_request_type
|
||||
|
||||
|
||||
@@ -187,7 +182,7 @@ def create_stream_log_request_model(
|
||||
),
|
||||
kwargs=(dict, Field(default_factory=dict)),
|
||||
)
|
||||
stream_log_request.update_forward_refs()
|
||||
stream_log_request.model_rebuild()
|
||||
return stream_log_request
|
||||
|
||||
|
||||
@@ -245,7 +240,7 @@ def create_stream_events_request_model(
|
||||
),
|
||||
kwargs=(dict, Field(default_factory=dict)),
|
||||
)
|
||||
stream_events_request.update_forward_refs()
|
||||
stream_events_request.model_rebuild()
|
||||
return stream_events_request
|
||||
|
||||
|
||||
@@ -297,7 +292,7 @@ def create_invoke_response_model(
|
||||
__base__=InvokeBaseResponse,
|
||||
**fields,
|
||||
)
|
||||
invoke_response_type.update_forward_refs()
|
||||
invoke_response_type.model_rebuild()
|
||||
return invoke_response_type
|
||||
|
||||
|
||||
@@ -347,7 +342,7 @@ def create_batch_response_model(
|
||||
__base__=BatchBaseResponse,
|
||||
**fields,
|
||||
)
|
||||
batch_response_type.update_forward_refs()
|
||||
batch_response_type.model_rebuild()
|
||||
return batch_response_type
|
||||
|
||||
|
||||
@@ -566,8 +561,8 @@ class OnRetrieverEnd(BaseModel):
|
||||
type: Literal["on_retriever_end"] = "on_retriever_end"
|
||||
|
||||
|
||||
class CallbackEvent(BaseModel):
|
||||
__root__: Union[
|
||||
CallbackEvent = RootModel[
|
||||
Union[
|
||||
OnChainStart,
|
||||
OnChainEnd,
|
||||
OnChainError,
|
||||
@@ -581,3 +576,4 @@ class CallbackEvent(BaseModel):
|
||||
OnRetrieverEnd,
|
||||
OnRetrieverError,
|
||||
]
|
||||
]
|
||||
|
||||
Generated
+1337
-1503
File diff suppressed because it is too large
Load Diff
+8
-4
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langserve"
|
||||
version = "0.2.3"
|
||||
version = "0.3.0dev2"
|
||||
description = ""
|
||||
readme = "README.md"
|
||||
authors = ["LangChain"]
|
||||
@@ -10,14 +10,14 @@ exclude = ["langserve/playground,langserve/chat_playground"]
|
||||
include = ["langserve/playground/dist/**/*", "langserve/chat_playground/dist/**/*"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.8.1"
|
||||
python = "^3.9"
|
||||
httpx = ">=0.23.0" # May be able to decrease this version
|
||||
fastapi = {version = ">=0.90.1,<1", optional = true}
|
||||
sse-starlette = {version = "^1.3.0", optional = true}
|
||||
pydantic = ">=1"
|
||||
langchain-core = ">=0.1,<0.3"
|
||||
langchain-core = "0.3.0dev5"
|
||||
orjson = ">=2"
|
||||
pyproject-toml = "^0.0.10"
|
||||
pydantic = "^2.7"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
jupyterlab = "^3.6.1"
|
||||
@@ -95,3 +95,7 @@ addopts = "--strict-markers --strict-config --durations=5 -vv"
|
||||
# take more than 5 seconds
|
||||
timeout = 5
|
||||
asyncio_mode = "auto"
|
||||
filterwarnings = [
|
||||
"ignore::langchain_core._api.beta_decorator.LangChainBetaWarning",
|
||||
]
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Test the playground API."""
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from httpx import AsyncClient
|
||||
from langchain_core.runnables import RunnableLambda
|
||||
@@ -15,7 +16,9 @@ async def test_serve_playground() -> None:
|
||||
RunnableLambda(lambda foo: "hello"),
|
||||
)
|
||||
|
||||
async with AsyncClient(app=app, base_url="http://localhost:9999") as client:
|
||||
async with AsyncClient(
|
||||
base_url="http://localhost:9999", transport=httpx.ASGITransport(app=app)
|
||||
) as client:
|
||||
response = await client.get("/playground/index.html")
|
||||
assert response.status_code == 200
|
||||
# Test that we can't access files that do not exist.
|
||||
@@ -42,7 +45,9 @@ async def test_serve_playground_with_api_router() -> None:
|
||||
|
||||
app.include_router(router)
|
||||
|
||||
async with AsyncClient(app=app, base_url="http://localhost:9999") as client:
|
||||
async with AsyncClient(
|
||||
base_url="http://localhost:9999", transport=httpx.ASGITransport(app=app)
|
||||
) as client:
|
||||
response = await client.get("/langserve_runnables/chat/playground/index.html")
|
||||
assert response.status_code == 200
|
||||
|
||||
@@ -64,7 +69,9 @@ async def test_serve_playground_with_api_router_in_api_router() -> None:
|
||||
# Now add parent router to the app
|
||||
app.include_router(parent_router)
|
||||
|
||||
async with AsyncClient(app=app, base_url="http://localhost:9999") as client:
|
||||
async with AsyncClient(
|
||||
base_url="http://localhost:9999", transport=httpx.ASGITransport(app=app)
|
||||
) as client:
|
||||
response = await client.get("/parent/bar/foo/playground/index.html")
|
||||
assert response.status_code == 200
|
||||
|
||||
@@ -88,7 +95,9 @@ async def test_root_path_on_playground() -> None:
|
||||
)
|
||||
app.include_router(router)
|
||||
|
||||
async_client = AsyncClient(app=app, base_url="http://localhost:9999")
|
||||
async_client = AsyncClient(
|
||||
base_url="http://localhost:9999", transport=httpx.ASGITransport(app=app)
|
||||
)
|
||||
|
||||
response = await async_client.get("/chat/playground/index.html")
|
||||
assert response.status_code == 200
|
||||
|
||||
@@ -4,15 +4,23 @@ from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from langchain_core.documents.base import Document
|
||||
from langchain_core.messages import HumanMessage, HumanMessageChunk, SystemMessage
|
||||
from langchain_core.outputs import ChatGeneration
|
||||
from pydantic import BaseModel
|
||||
|
||||
try:
|
||||
from pydantic.v1 import BaseModel
|
||||
except ImportError:
|
||||
from pydantic import BaseModel
|
||||
from langserve.serialization import (
|
||||
WellKnownLCObject,
|
||||
WellKnownLCSerializer,
|
||||
load_events,
|
||||
)
|
||||
|
||||
from langserve.serialization import WellKnownLCSerializer, load_events
|
||||
|
||||
def test_document_serialization() -> None:
|
||||
"""Simple test. Exhaustive tests follow below."""
|
||||
doc = Document(page_content="hello")
|
||||
d = doc.model_dump()
|
||||
WellKnownLCObject.model_validate(d)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -23,6 +31,8 @@ from langserve.serialization import WellKnownLCSerializer, load_events
|
||||
[],
|
||||
{},
|
||||
{"a": 1},
|
||||
Document(page_content="Hello"),
|
||||
[Document(page_content="Hello")],
|
||||
{"output": [HumanMessage(content="hello")]},
|
||||
# Test with a single message (HumanMessage)
|
||||
HumanMessage(content="Hello"),
|
||||
@@ -77,7 +87,7 @@ def _get_full_representation(data: Any) -> Any:
|
||||
elif isinstance(data, list):
|
||||
return [_get_full_representation(value) for value in data]
|
||||
elif isinstance(data, BaseModel):
|
||||
return data.schema()
|
||||
return data.model_json_schema()
|
||||
else:
|
||||
return data
|
||||
|
||||
@@ -179,3 +189,9 @@ def test_encoding_of_well_known_types(obj: Any, expected: str) -> None:
|
||||
"""
|
||||
lc_serializer = WellKnownLCSerializer()
|
||||
assert lc_serializer.dumpd(obj) == expected
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason="0.3")
|
||||
def test_fail_03() -> None:
|
||||
"""This test will fail on purposes. It contains a TODO list for 0.3 release."""
|
||||
assert "CHatGeneration_Deserialized correct" == "UNcomment test above"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,14 +5,9 @@ import pytest
|
||||
from fastapi import Request
|
||||
from langchain_core.prompts import PromptTemplate
|
||||
from langchain_core.runnables import ConfigurableField
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
from langserve.api_handler import _unpack_request_config
|
||||
|
||||
try:
|
||||
from pydantic.v1 import BaseModel, ValidationError
|
||||
except ImportError:
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
from langserve.validation import (
|
||||
create_batch_request_model,
|
||||
create_invoke_request_model,
|
||||
@@ -175,11 +170,11 @@ async def test_invoke_request_with_runnables() -> None:
|
||||
"configurable": {"template": "goodbye {name}"},
|
||||
},
|
||||
)
|
||||
assert request.input == {"name": "bob"}
|
||||
assert dict(request.input) == {"name": "bob"}
|
||||
assert request.config.tags == ["hello"]
|
||||
assert request.config.run_name == "run"
|
||||
assert isinstance(request.config.configurable, BaseModel)
|
||||
assert request.config.configurable.dict() == {
|
||||
assert request.config.configurable.model_dump() == {
|
||||
"template": "goodbye {name}",
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
from typing import Any
|
||||
|
||||
|
||||
def recursive_dump(obj: Any) -> Any:
|
||||
"""Recursively dump the object if encountering any pydantic models."""
|
||||
if isinstance(obj, dict):
|
||||
return {
|
||||
k: recursive_dump(v)
|
||||
for k, v in obj.items()
|
||||
if k != "id" # Remove the id field for testing purposes
|
||||
}
|
||||
if isinstance(obj, list):
|
||||
return [recursive_dump(v) for v in obj]
|
||||
if hasattr(obj, "model_dump"):
|
||||
# if the object contains an ID field, we'll remove it for testing purposes
|
||||
d = obj.model_dump()
|
||||
if "id" in d:
|
||||
d.pop("id")
|
||||
return recursive_dump(d)
|
||||
if hasattr(obj, "dict"):
|
||||
# if the object contains an ID field, we'll remove it for testing purposes
|
||||
if hasattr(obj, "id"):
|
||||
d = obj.dict()
|
||||
d.pop("id")
|
||||
return recursive_dump(d)
|
||||
return recursive_dump(obj.dict())
|
||||
return obj
|
||||
@@ -1,6 +1,22 @@
|
||||
from typing import Any
|
||||
|
||||
from langchain_core.messages import AIMessage, AIMessageChunk
|
||||
|
||||
|
||||
class AnyStr(str):
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return isinstance(other, str)
|
||||
|
||||
|
||||
def _AnyIdAIMessage(**kwargs: Any) -> AIMessage:
|
||||
"""Create ai message with an any id field."""
|
||||
message = AIMessage(**kwargs)
|
||||
message.id = AnyStr()
|
||||
return message
|
||||
|
||||
|
||||
def _AnyIdAIMessageChunk(**kwargs: Any) -> AIMessageChunk:
|
||||
"""Create ai message with an any id field."""
|
||||
message = AIMessageChunk(**kwargs)
|
||||
message.id = AnyStr()
|
||||
return message
|
||||
|
||||
@@ -4,11 +4,11 @@ from typing import Any, Dict, List, Optional, Union
|
||||
from uuid import UUID
|
||||
|
||||
from langchain_core.callbacks.base import AsyncCallbackHandler
|
||||
from langchain_core.messages import AIMessage, AIMessageChunk, BaseMessage
|
||||
from langchain_core.messages import AIMessage, BaseMessage
|
||||
from langchain_core.outputs import ChatGenerationChunk, GenerationChunk
|
||||
|
||||
from tests.unit_tests.utils.llms import GenericFakeChatModel
|
||||
from tests.unit_tests.utils.stubs import AnyStr
|
||||
from tests.unit_tests.utils.stubs import _AnyIdAIMessage, _AnyIdAIMessageChunk
|
||||
|
||||
|
||||
def test_generic_fake_chat_model_invoke() -> None:
|
||||
@@ -16,11 +16,11 @@ def test_generic_fake_chat_model_invoke() -> None:
|
||||
infinite_cycle = cycle([AIMessage(content="hello"), AIMessage(content="goodbye")])
|
||||
model = GenericFakeChatModel(messages=infinite_cycle)
|
||||
response = model.invoke("meow")
|
||||
assert response == AIMessage(content="hello", id=AnyStr())
|
||||
assert response == _AnyIdAIMessage(content="hello")
|
||||
response = model.invoke("kitty")
|
||||
assert response == AIMessage(content="goodbye", id=AnyStr())
|
||||
assert response == _AnyIdAIMessage(content="goodbye")
|
||||
response = model.invoke("meow")
|
||||
assert response == AIMessage(content="hello", id=AnyStr())
|
||||
assert response == _AnyIdAIMessage(content="hello")
|
||||
|
||||
|
||||
async def test_generic_fake_chat_model_ainvoke() -> None:
|
||||
@@ -28,11 +28,11 @@ async def test_generic_fake_chat_model_ainvoke() -> None:
|
||||
infinite_cycle = cycle([AIMessage(content="hello"), AIMessage(content="goodbye")])
|
||||
model = GenericFakeChatModel(messages=infinite_cycle)
|
||||
response = await model.ainvoke("meow")
|
||||
assert response == AIMessage(content="hello", id=AnyStr())
|
||||
assert response == _AnyIdAIMessage(content="hello")
|
||||
response = await model.ainvoke("kitty")
|
||||
assert response == AIMessage(content="goodbye", id=AnyStr())
|
||||
assert response == _AnyIdAIMessage(content="goodbye")
|
||||
response = await model.ainvoke("meow")
|
||||
assert response == AIMessage(content="hello", id=AnyStr())
|
||||
assert response == _AnyIdAIMessage(content="hello")
|
||||
|
||||
|
||||
async def test_generic_fake_chat_model_stream() -> None:
|
||||
@@ -45,28 +45,26 @@ async def test_generic_fake_chat_model_stream() -> None:
|
||||
model = GenericFakeChatModel(messages=infinite_cycle)
|
||||
chunks = [chunk async for chunk in model.astream("meow")]
|
||||
assert chunks == [
|
||||
AIMessageChunk(content="hello", id=AnyStr()),
|
||||
AIMessageChunk(content=" ", id=AnyStr()),
|
||||
AIMessageChunk(content="goodbye", id=AnyStr()),
|
||||
_AnyIdAIMessageChunk(content="hello"),
|
||||
_AnyIdAIMessageChunk(content=" "),
|
||||
_AnyIdAIMessageChunk(content="goodbye"),
|
||||
]
|
||||
|
||||
chunks = [chunk for chunk in model.stream("meow")]
|
||||
assert chunks == [
|
||||
AIMessageChunk(content="hello", id=AnyStr()),
|
||||
AIMessageChunk(content=" ", id=AnyStr()),
|
||||
AIMessageChunk(content="goodbye", id=AnyStr()),
|
||||
_AnyIdAIMessageChunk(content="hello"),
|
||||
_AnyIdAIMessageChunk(content=" "),
|
||||
_AnyIdAIMessageChunk(content="goodbye"),
|
||||
]
|
||||
|
||||
# Test streaming of additional kwargs.
|
||||
# Relying on insertion order of the additional kwargs dict
|
||||
message = AIMessage(
|
||||
content="", additional_kwargs={"foo": 42, "bar": 24}, id=AnyStr()
|
||||
)
|
||||
message = AIMessage(content="", additional_kwargs={"foo": 42, "bar": 24}, id="1")
|
||||
model = GenericFakeChatModel(messages=cycle([message]))
|
||||
chunks = [chunk async for chunk in model.astream("meow")]
|
||||
assert chunks == [
|
||||
AIMessageChunk(content="", additional_kwargs={"foo": 42}, id=AnyStr()),
|
||||
AIMessageChunk(content="", additional_kwargs={"bar": 24}, id=AnyStr()),
|
||||
_AnyIdAIMessageChunk(content="", additional_kwargs={"foo": 42}),
|
||||
_AnyIdAIMessageChunk(content="", additional_kwargs={"bar": 24}),
|
||||
]
|
||||
|
||||
message = AIMessage(
|
||||
@@ -83,29 +81,25 @@ async def test_generic_fake_chat_model_stream() -> None:
|
||||
chunks = [chunk async for chunk in model.astream("meow")]
|
||||
|
||||
assert chunks == [
|
||||
AIMessageChunk(
|
||||
_AnyIdAIMessageChunk(
|
||||
content="",
|
||||
additional_kwargs={"function_call": {"name": "move_file"}},
|
||||
id=AnyStr(),
|
||||
),
|
||||
AIMessageChunk(
|
||||
_AnyIdAIMessageChunk(
|
||||
content="",
|
||||
additional_kwargs={
|
||||
"function_call": {"arguments": '{\n "source_path": "foo"'}
|
||||
},
|
||||
id=AnyStr(),
|
||||
),
|
||||
AIMessageChunk(
|
||||
_AnyIdAIMessageChunk(
|
||||
content="",
|
||||
additional_kwargs={"function_call": {"arguments": ","}},
|
||||
id=AnyStr(),
|
||||
),
|
||||
AIMessageChunk(
|
||||
_AnyIdAIMessageChunk(
|
||||
content="",
|
||||
additional_kwargs={
|
||||
"function_call": {"arguments": '\n "destination_path": "bar"\n}'}
|
||||
},
|
||||
id=AnyStr(),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -116,7 +110,7 @@ async def test_generic_fake_chat_model_stream() -> None:
|
||||
else:
|
||||
accumulate_chunks += chunk
|
||||
|
||||
assert accumulate_chunks == AIMessageChunk(
|
||||
assert accumulate_chunks == _AnyIdAIMessageChunk(
|
||||
content="",
|
||||
additional_kwargs={
|
||||
"function_call": {
|
||||
@@ -125,7 +119,6 @@ async def test_generic_fake_chat_model_stream() -> None:
|
||||
'destination_path": "bar"\n}',
|
||||
}
|
||||
},
|
||||
id=AnyStr(),
|
||||
)
|
||||
|
||||
|
||||
@@ -138,9 +131,9 @@ async def test_generic_fake_chat_model_astream_log() -> None:
|
||||
]
|
||||
final = log_patches[-1]
|
||||
assert final.state["streamed_output"] == [
|
||||
AIMessageChunk(content="hello", id=AnyStr()),
|
||||
AIMessageChunk(content=" ", id=AnyStr()),
|
||||
AIMessageChunk(content="goodbye", id=AnyStr()),
|
||||
_AnyIdAIMessageChunk(content="hello"),
|
||||
_AnyIdAIMessageChunk(content=" "),
|
||||
_AnyIdAIMessageChunk(content="goodbye"),
|
||||
]
|
||||
|
||||
|
||||
@@ -188,8 +181,8 @@ async def test_callback_handlers() -> None:
|
||||
# New model
|
||||
results = list(model.stream("meow", {"callbacks": [MyCustomAsyncHandler(tokens)]}))
|
||||
assert results == [
|
||||
AIMessageChunk(content="hello", id=AnyStr()),
|
||||
AIMessageChunk(content=" ", id=AnyStr()),
|
||||
AIMessageChunk(content="goodbye", id=AnyStr()),
|
||||
_AnyIdAIMessageChunk(content="hello"),
|
||||
_AnyIdAIMessageChunk(content=" "),
|
||||
_AnyIdAIMessageChunk(content="goodbye"),
|
||||
]
|
||||
assert tokens == ["hello", " ", "goodbye"]
|
||||
|
||||
Reference in New Issue
Block a user