Compare commits

...

28 Commits

Author SHA1 Message Date
joaquin-borggio-lc f0d86287ab ci: drop python 3.8 (#825) 2025-09-17 15:38:01 -04:00
joaquin-borggio-lc 168c9ff90e chore: bump patch version (#823)
bump patched version so i can release
2025-09-17 15:31:09 -04:00
joaquin-borggio-lc a87125d0b8 chore: bump some package versions (#821)
Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com>
2025-09-17 15:03:09 -04:00
Eugene Yurtsev 1de17fa799 Add new contributor to .clabot configuration (#822) 2025-09-17 15:01:18 -04:00
ResearchAI 1a08f2740c Update Chroma import to avoid deprecation warning. (#741) 2025-07-09 17:19:18 -04:00
Yash Jivani 06c3c3691e Update README.md Grammatical Errors (#733) 2025-07-09 17:15:01 -04:00
Eugene Yurtsev d20eab45f2 ci: add workflow dispatch to _release (#805) 2024-12-26 21:37:14 -05:00
Eugene Yurtsev 321b7aa3b1 Release 0.3.1 (#801) 2024-12-19 10:33:17 -05:00
Eugene Yurtsev aa4aea4a81 lint (#803) 2024-12-19 10:19:16 -05:00
Eugene Yurtsev 1aaec1189c Update _lint.yml (#802) 2024-12-18 22:10:43 -05:00
アリス 0d7601781d update ssl verifying part to work with httpx 0.28.0 (#798)
As the new httpx 0.28.0 removes VerifyTypes, we should update to follow
their change
See [issue](https://github.com/langchain-ai/langserve/issues/796)

---------

Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com>
2024-12-18 22:08:58 -05:00
Eugene Yurtsev 0ad075fb67 Update .clabot (#800) 2024-12-18 22:04:39 -05:00
Mingqi Hu b007300b06 docs: Remove templates in readme (#792)
docs: Remove templates in readme

Issue: 404 when you click the [LangChain
Templates](https://github.com/langchain-ai/langchain/blob/master/templates/README.md)
in readme. As templates dir has been removed on latest langchain master.
<img width="368" alt="image"
src="https://github.com/user-attachments/assets/8dfa7e82-c6df-458c-ba42-2b9f0fd67d8d">


Fix: Just remove it to avoid 404 as example dirs is enough, look like
(my local vs code)
<img width="423" alt="image"
src="https://github.com/user-attachments/assets/b65fc053-ed62-47e4-8404-fbfbcb4b17d5">

Signed-off-by: Mingqi <mingqi.hu@intel.com>
Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com>
2024-11-22 10:21:02 -05:00
Eugene Yurtsev 9df7e88bb9 Update .clabot (#793) 2024-11-22 10:20:11 -05:00
Eugene Yurtsev c626cde08c Update MIGRATION.md (#789) 2024-11-18 11:06:37 -05:00
ccurme c4a8925b00 docs: update local link in readme (#788)
LangChain doc builds are currently
[broken](https://vercel.com/langchain/langchain/ExxE3a6SSZVa7RSRUDB3mzHDpsvV?filter=errors).

- LangChain docs page hosts LangServe readme:
https://python.langchain.com/docs/langserve/;
- The readme is [downloaded from
github](https://github.com/langchain-ai/langchain/blob/22a8652eccd9023cd41ea0f6924575ce87c28756/docs/Makefile#L49)
when docs are built;
- A
[script](https://github.com/langchain-ai/langchain/blob/master/docs/scripts/resolve_local_links.py)
resolves local links in the readme to point to the LangServe github.

This script assumes local links are prefixed with `./`
2024-11-18 11:05:19 -05:00
Eugene Yurtsev 80f949b62e migration guide (#787) 2024-11-16 22:36:31 -05:00
Eugene Yurtsev c27923a7d1 Update README.md (#773) 2024-09-18 13:33:52 -04:00
Eugene Yurtsev f3b9c43106 Update README.md (#771) 2024-09-16 13:22:26 -04:00
Eugene Yurtsev 6b1a0d97ef Update README (#769) 2024-09-16 11:56:41 -04:00
Eugene Yurtsev dc04672537 examples: update clients (#768) 2024-09-14 14:32:41 -04:00
Eugene Yurtsev 42b61a664b v0.3 release (#767) 2024-09-14 14:14:45 -04:00
Eugene Yurtsev 1e24edce08 Add ability to specify custom serializer (#764)
Allow users to define a custom serializer
2024-09-14 14:06:08 -04:00
Eugene Yurtsev c747e20c1e Fix issue with callback events sent from server (#765)
This properly propagates the name of the runs from the server to the client if one enables callbacks.
2024-09-14 13:46:10 -04:00
Eugene Yurtsev 8b4d8dff6c propagate astream events from add_route (#763)
Propagate the parameter to APIHandler
2024-09-12 17:02:35 -04:00
Eugene Yurtsev 2c957bdd78 mark include callback events as a beta api (#761) 2024-09-12 14:58:33 -04:00
Eugene Yurtsev 36f945e494 improve error messages when models fail to hash (#762) 2024-09-12 14:58:25 -04:00
Eugene Yurtsev 43683b3671 add ability to control version of astream events API (#760)
add ability to control version of astream events API server side
2024-09-12 14:33:21 -04:00
29 changed files with 862 additions and 551 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
{
"contributors": ["eyurtsev", "hwchase17", "nfcampos", "efriis", "jacoblee93", "dqbd", "kreneskyp", "adarsh-jha-dev", "harris", "baskaryan", "hinthornw", "bracesproul", "jakerachleff", "craigsdennis", "anhi", "169", "LarchLiu", "PaulLockett", "RCMatthias", "jwynia", "majiayu000", "mpskex", "shivachittamuru", "sinashaloudegi", "sowsan", "akira", "lucianotonet", "JGalego", "nat-n", "dirien", "donbr", "rahilvora", "WarrenTheRabbit", "StreetLamb", "ccurme", "dennisrall"],
"contributors": ["eyurtsev", "hwchase17", "nfcampos", "efriis", "jacoblee93", "dqbd", "kreneskyp", "adarsh-jha-dev", "harris", "baskaryan", "hinthornw", "bracesproul", "jakerachleff", "craigsdennis", "anhi", "169", "LarchLiu", "PaulLockett", "RCMatthias", "jwynia", "majiayu000", "mpskex", "shivachittamuru", "sinashaloudegi", "sowsan", "akira", "lucianotonet", "JGalego", "nat-n", "dirien", "donbr", "rahilvora", "WarrenTheRabbit", "StreetLamb", "ccurme", "dennisrall", "Mingqi2", "xxsl", "joaquin-borggio-lc"],
"message": "Thank you for your pull request and welcome to our community. We require contributors to sign our Contributor License Agreement, and we don't seem to have the username {{usersWithoutCLA}} on file. In order for us to review and merge your code, please complete the Individual Contributor License Agreement here https://forms.gle/AQFbtkWRoHXUgipM6 .\n\nThis process is done manually on our side, so after signing the form one of the maintainers will add you to the contributors list.\n\nFor more details about why we have a CLA and other contribution guidelines please see: https://github.com/langchain-ai/langserve/blob/main/CONTRIBUTING.md."
}
+1 -1
View File
@@ -31,7 +31,7 @@ jobs:
# Starting new jobs is also relatively slow,
# so linting on fewer versions makes CI faster.
python-version:
- "3.8"
- "3.9"
- "3.11"
steps:
- uses: actions/checkout@v3
+6
View File
@@ -7,6 +7,12 @@ on:
required: true
type: string
description: "From which folder this pipeline executes"
workflow_dispatch: # Allows to trigger the workflow manually in GitHub UI
inputs:
working-directory:
required: true
type: string
description: "From which folder this pipeline executes"
env:
POETRY_VERSION: "1.5.1"
-1
View File
@@ -20,7 +20,6 @@ jobs:
strategy:
matrix:
python-version:
- "3.8"
- "3.9"
- "3.10"
- "3.11"
-1
View File
@@ -48,7 +48,6 @@ jobs:
strategy:
matrix:
python-version:
- "3.8"
- "3.9"
- "3.10"
- "3.11"
+222
View File
@@ -0,0 +1,222 @@
# LangGraph Platform Migration Guide
We have [recently announced](https://blog.langchain.dev/langgraph-platform-announce/) LangGraph Platform, a ***significantly*** enhanced solution for deploying agentic applications at scale.
LangGraph Platform incorporates [key design patterns and capabilities](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform/#option-2-leveraging-langgraph-platform-for-complex-deployments) essential for production-level deployment of large language model (LLM) applications.
In contrast to LangServe, LangGraph Platform provides comprehensive, out-of-the-box support for [persistence](https://langchain-ai.github.io/langgraph/concepts/application_structure/), [memory](https://langchain-ai.github.io/langgraph/concepts/assistants/), [double-texting handling](https://langchain-ai.github.io/langgraph/concepts/double_texting/), [human-in-the-loop workflows](https://langchain-ai.github.io/langgraph/concepts/assistants/), [cron job scheduling](https://langchain-ai.github.io/langgraph/concepts/langgraph_server/#cron-jobs), [webhooks](https://langchain-ai.github.io/langgraph/concepts/langgraph_server/#webhooks), high-load management, advanced streaming, support for long-running tasks, background task processing, and much more.
The LangGraph Platform ecosystem includes the following components:
- [LangGraph Server](https://langchain-ai.github.io/langgraph/concepts/langgraph_server/): Provides an [Assistants API](https://langchain-ai.github.io/langgraph/cloud/reference/api/api_ref.html) for LLM applications (graphs) built with [LangGraph](https://langchain-ai.github.io/langgraph/). Available in both Python and JavaScript/TypeScript.
- [LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio/): A specialized IDE for real-time visualization, debugging, and interaction via a graphical interface. Available as a web application or macOS desktop app, it's a substantial improvement over LangServe's playground.
- [SDK](https://langchain-ai.github.io/langgraph/concepts/sdk/): Enables programmatic interaction with the server, available in Python and JavaScript/TypeScript.
- [RemoteGraph](https://langchain-ai.github.io/langgraph/how-tos/use-remote-graph/): Allows interaction with a remote graph as if it were running locally, serving as LangGraph's equivalent to LangServe's RemoteRunnable. Available in both Python and JavaScript/TypeScript.
## Context
LangServe was built as a deployment solution for LangChain Runnables created using the [LangChain Expression Language (LCEL)](https://python.langchain.com/docs/concepts/lcel). In LangServe, the LCEL was the orchestration layer that managed the execution of the Runnable.
[LangGraph](https://langchain-ai.github.io/langgraph/) is an open source library created by the LangChain team that provides a more flexible orchestration layer that's better suited for creating more complex LLM applications. LangGraph Platform
is the deployment solution for LangGraph applications.
## LangServe Support
We recommend using LangGraph Platform rather than LangServe for new projects.
We will continue to accept bug fixes for LangServe from the community; however, we will not be accepting new feature contributions.
## Migration
If you would like to migrate an existing LangServe application to LangGraph Platform, you have two options:
1. You can wrap the existing `Runnable` that you expose in the LangServe application via `add_routes` in a `LangGraph` node. This is the quickest way to migrate your application to LangGraph Platform.
2. You can do a larger refactor to break up the existing LCEL into appropriate `LangGraph` nodes. This is recommended if you want to take advantage of more advanced features in LangGraph Platform.
### Option 1: Wrap Runnable in LangGraph Node
This option is the quickest way to migrate your application to LangGraph Platform. You can wrap the existing `Runnable` that you expose in the LangServe application via `add_routes` in a `LangGraph` node.
Original LangServe code:
```python
from langserve import add_routes
app = FastAPI()
# Some input schema
class Input(BaseModel):
input: str
foo: Optional[str]
# Some output schema
class Output(BaseModel):
output: Any
runnable = .... # Your existing Runnable
runnable_with_types = runnable.with_types(input_type=Input, output_type=Output)
# Adds routes to the app for using the chain under:
add_routes(
app,
runnable_with_types,
)
```
Migrated LangGraph Platform code:
```python
@dataclass
class InputState: # Equivalent to Input in the original code
"""Defines the input state, representing a narrower interface to the outside world.
This class is used to define the initial state and structure of incoming data.
See: https://langchain-ai.github.io/langgraph/concepts/low_level/#state
for more information.
"""
input: str
foo: Optional[str] = None
@dataclass
class OutputState: # Equivalent to Output in the original code
"""Defines the output state, representing a narrower interface to the outside world.
https://langchain-ai.github.io/langgraph/concepts/low_level/#state
"""
output: Any
@dataclass
class SharedState:
"""The full graph state.
https://langchain-ai.github.io/langgraph/concepts/low_level/#state
"""
input: str
foo: Optional[str] = None
output: Any
runnable = ... # Same code as before
async def my_node(state: InputState, config: RunnableConfig) -> OutputState:
"""Each node does work."""
return await runnable.ainvoke({"input": state.input, "foo": state.foo})
# Define a new graph
builder = StateGraph(
SharedState, config_schema=Configuration, input=InputState, output=OutputState
)
# Add the node to the graph
builder.add_node("my_node", my_node)
# Set the entrypoint as `call_model`
builder.add_edge("__start__", "my_node")
# Compile the workflow into an executable graph
graph = builder.compile()
graph.name = "New Graph" # This defines the custom name in LangSmith
```
### 2. Refactor LCEL into LangGraph Nodes
This option is recommended if you want to take advantage of more advanced features in LangGraph Platform.
#### Memory (alternative to `RunnableWithMessageHistory`)
For example, LangGraph comes with built-in persistence that is more general than LangChain's `RunnableWithMessageHistory`.
Please refer to the guide on [upgrading to LangGraph memory](https://python.langchain.com/docs/versions/migrating_memory/) for more details.
#### Agents
If you're relying on legacy LangChain agents, you can migrate them into the pre-built
LangGraph agents. Please refer to the guide on [migrating agents](https://python.langchain.com/docs/how_to/migrate_agent/) for more details.
#### Custom Chains
If you created a custom chain and used LCEL to orchestrate it, you will usually be able to refactor it into a LangGraph without too much difficulty.
There isn't a one-size-fits-all guide for this, but generally speaking, consider creating
a separate node for any long-running step in your LCEL chain or any step that you would
want to be able to monitor or debug separately.
For example, if you have a simple Retrieval Augmented Generation (RAG) pipeline, you might have a node for the retrieval step and a node for the generation step.
Original LCEL code:
```python
...
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
rag_chain.with_types(input_type=Input, output_type=Output)
```
Using LangGraph for the same pipeline:
```python
@dataclass
class InputState: # Equivalent to Input in the original code
"""Input question from the user."""
question: str
@dataclass
class OutputState: # Equivalent to Output in the original code
"""The output from the graph."""
answer: str
@dataclass
class SharedState:
question: str
docs: List[str]
response: str
async def retriever_node(state: InputState) -> SharedState:
"""Rettrieve documents based on the user's question."""
documents = await retriever.ainvoke({"context": state.question})
return {
"docs": documents
}
async def generator_node(state: SharedState) -> OutputState:
"""Generate an answer using an LLM based on the retrieved documents and question."""
context = " -- DOCUMENT -- ".join(state.docs)
prompt = [
SystemMessage(
content=(
"Answer the user's question based on the list of documents "
"that were retrieved. Here are the documents: \n\n"
f"{context}"
)
),
HumanMessage(content=state.question),
]
ai_message = await llm.ainvoke(prompt)
return {"answer": ai_message.content}
# Define a new graph
builder = StateGraph(
SharedState, config_schema=Configuration, input=InputState, output=OutputState
)
builder.add_node("retriever", retriever_node)
builder.add_node("generator", generator_node)
builder.add_edge("__start__", "retriever")
builder.add_edge("retriever", "generator")
graph = builder.compile()
graph.name = "RAG Graph"
```
Please see the [LangGraph tutorials](https://langchain-ai.github.io/langgraph/tutorials/)
for tutorials and examples that will help you get started with LangGraph
and LangGraph Platform.
+19 -13
View File
@@ -5,6 +5,15 @@
[![Open Issues](https://img.shields.io/github/issues-raw/langchain-ai/langserve)](https://github.com/langchain-ai/langserve/issues)
[![](https://dcbadge.vercel.app/api/server/6adMQxSpJS?compact=true&style=flat)](https://discord.com/channels/1038097195422978059/1170024642245832774)
> [!WARNING]
> We recommend using LangGraph Platform rather than LangServe for new projects.
>
> Please see the [LangGraph Platform Migration Guide](./MIGRATION.md) for more information.
>
> We will continue to accept bug fixes for LangServe from the community; however, we
> will not be accepting new feature contributions.
## Overview
[LangServe](https://github.com/langchain-ai/langserve) helps developers
@@ -40,7 +49,7 @@ in [LangChain.js](https://js.langchain.com/docs/ecosystem/langserve).
## ⚠️ LangGraph Compatibility
LangServe is designed to primarily deploy simple Runnables and wok with well-known primitives in langchain-core.
LangServe is designed to primarily deploy simple Runnables and work with well-known primitives in langchain-core.
If you need a deployment option for LangGraph, you should instead be looking at [LangGraph Cloud (beta)](https://langchain-ai.github.io/langgraph/cloud/) which will
be better suited for deploying LangGraph applications.
@@ -48,9 +57,8 @@ be better suited for deploying LangGraph applications.
## Limitations
- Client callbacks are not yet supported for events that originate on the server
- OpenAPI docs will not be generated when using Pydantic V2. Fast API does not
support [mixing pydantic v1 and v2 namespaces](https://github.com/tiangolo/fastapi/issues/10360).
See section below for more details.
- Versions of LangServe <= 0.2.0, will not generate OpenAPI docs properly when using Pydantic V2 as Fast API does not support [mixing pydantic v1 and v2 namespaces](https://github.com/tiangolo/fastapi/issues/10360).
See section below for more details. Either upgrade to LangServe>=0.3.0 or downgrade Pydantic to pydantic 1.
## Security
@@ -92,7 +100,7 @@ langchain app new my-app
add_routes(app. NotImplemented)
```
### 3. Use `poetry` to add 3rd party packages (e.g., langchain-openai, langchain-anthropic, langchain-mistral etc).
### 3. Use `poetry` to add 3rd party packages (e.g., langchain-openai, langchain-anthropic, langchain-mistral, etc).
```sh
poetry add [package-name] // e.g `poetry add langchain-openai`
@@ -112,12 +120,7 @@ poetry run langchain serve --port=8100
## Examples
Get your LangServe instance started quickly with
[LangChain Templates](https://github.com/langchain-ai/langchain/blob/master/templates/README.md).
For more examples, see the templates
[index](https://github.com/langchain-ai/langchain/blob/master/templates/docs/INDEX.md)
or the [examples](https://github.com/langchain-ai/langserve/tree/main/examples)
Get your LangServe instances started quickly with the [examples](https://github.com/langchain-ai/langserve/tree/main/examples)
directory.
| Description | Links |
@@ -208,8 +211,9 @@ app.add_middleware(
If you've deployed the server above, you can view the generated OpenAPI docs using:
> ⚠️ If using pydantic v2, docs will not be generated for _invoke_, _batch_, _stream_,
> ⚠️ If using LangServe <= 0.2.0 and pydantic v2, docs will not be generated for _invoke_, _batch_, _stream_,
> _stream_log_. See [Pydantic](#pydantic) section below for more details.
> To resolve please upgrade to LangServe 0.3.0.
```sh
curl localhost:8000/docs
@@ -472,7 +476,9 @@ gcloud run deploy [your-service-name] --source . --port 8001 --allow-unauthentic
## Pydantic
LangServe provides support for Pydantic 2 with some limitations.
LangServe>=0.3 fully supports Pydantic 2.
If you're using an earlier version of LangServe (<= 0.2), then please note that support for Pydantic 2 has the following limitations:
1. OpenAPI docs will not be generated for invoke/batch/stream/stream_log when using
Pydantic V2. Fast API does not support [mixing pydantic v1 and v2 namespaces]. To fix this, use `pip install pydantic==1.10.17`.
+1 -1
View File
@@ -35,7 +35,7 @@ from typing import Any, List, Optional, Union
from fastapi import Depends, FastAPI, HTTPException, Request, Response, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from langchain_community.vectorstores.chroma import Chroma
from langchain_chroma import Chroma
from langchain_core.documents import Document
from langchain_core.runnables import (
ConfigurableField,
@@ -36,7 +36,7 @@ from typing import Any, Dict, List, Optional, Union
from fastapi import Depends, FastAPI, HTTPException, Request, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from langchain_community.vectorstores.chroma import Chroma
from langchain_chroma import Chroma
from langchain_core.documents import Document
from langchain_core.runnables import (
ConfigurableField,
+9 -53
View File
@@ -23,14 +23,7 @@
"tags": []
},
"outputs": [],
"source": [
"import requests\n",
"\n",
"inputs = {\"input\": {\"topic\": \"sports\"}}\n",
"response = requests.post(\"http://localhost:8000/configurable_temp/invoke\", json=inputs)\n",
"\n",
"response.json()"
]
"source": ["import requests\n\ninputs = {\"input\": {\"topic\": \"sports\"}}\nresponse = requests.post(\"http://localhost:8000/configurable_temp/invoke\", json=inputs)\n\nresponse.json()"]
},
{
"cell_type": "markdown",
@@ -46,11 +39,7 @@
"tags": []
},
"outputs": [],
"source": [
"from langserve import RemoteRunnable\n",
"\n",
"remote_runnable = RemoteRunnable(\"http://localhost:8000/configurable_temp\")"
]
"source": ["from langserve import RemoteRunnable\n\nremote_runnable = RemoteRunnable(\"http://localhost:8000/configurable_temp\")"]
},
{
"cell_type": "markdown",
@@ -66,9 +55,7 @@
"tags": []
},
"outputs": [],
"source": [
"response = await remote_runnable.ainvoke({\"topic\": \"sports\"})"
]
"source": ["response = await remote_runnable.ainvoke({\"topic\": \"sports\"})"]
},
{
"cell_type": "markdown",
@@ -84,11 +71,7 @@
"tags": []
},
"outputs": [],
"source": [
"from langchain.schema.runnable.config import RunnableConfig\n",
"\n",
"remote_runnable.batch([{\"topic\": \"sports\"}, {\"topic\": \"cars\"}])"
]
"source": ["from langchain_core.runnables import RunnableConfig\n\nremote_runnable.batch([{\"topic\": \"sports\"}, {\"topic\": \"cars\"}])"]
},
{
"cell_type": "markdown",
@@ -104,10 +87,7 @@
"tags": []
},
"outputs": [],
"source": [
"async for chunk in remote_runnable.astream({\"topic\": \"bears, but a bit verbose\"}):\n",
" print(chunk, end=\"\", flush=True)"
]
"source": ["async for chunk in remote_runnable.astream({\"topic\": \"bears, but a bit verbose\"}):\n print(chunk, end=\"\", flush=True)"]
},
{
"cell_type": "markdown",
@@ -157,14 +137,7 @@
"tags": []
},
"outputs": [],
"source": [
"await remote_runnable.ainvoke(\n",
" {\"topic\": \"sports\"},\n",
" config={\n",
" \"configurable\": {\"prompt\": \"how to say {topic} in french\", \"llm\": \"low_temp\"}\n",
" },\n",
")"
]
"source": ["await remote_runnable.ainvoke(\n {\"topic\": \"sports\"},\n config={\n \"configurable\": {\"prompt\": \"how to say {topic} in french\", \"llm\": \"low_temp\"}\n },\n)"]
},
{
"cell_type": "markdown",
@@ -221,13 +194,7 @@
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# The model will fail with an auth error\n",
"unauthenticated_response = requests.post(\n",
" \"http://localhost:8000/auth_from_header/invoke\", json={\"input\": \"hello\"}\n",
")\n",
"unauthenticated_response.json()"
]
"source": ["# The model will fail with an auth error\nunauthenticated_response = requests.post(\n \"http://localhost:8000/auth_from_header/invoke\", json={\"input\": \"hello\"}\n)\nunauthenticated_response.json()"]
},
{
"cell_type": "markdown",
@@ -244,25 +211,14 @@
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# The model will succeed as long as the above shell script is run previously\n",
"import os\n",
"\n",
"test_key = os.environ[\"TEST_API_KEY\"]\n",
"authenticated_response = requests.post(\n",
" \"http://localhost:8000/auth_from_header/invoke\",\n",
" json={\"input\": \"hello\"},\n",
" headers={\"x-api-key\": test_key},\n",
")\n",
"authenticated_response.json()"
]
"source": ["# The model will succeed as long as the above shell script is run previously\nimport os\n\ntest_key = os.environ[\"TEST_API_KEY\"]\nauthenticated_response = requests.post(\n \"http://localhost:8000/auth_from_header/invoke\",\n json={\"input\": \"hello\"},\n headers={\"x-api-key\": test_key},\n)\nauthenticated_response.json()"]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
"source": [""]
}
],
"metadata": {
+13 -81
View File
@@ -16,9 +16,7 @@
"tags": []
},
"outputs": [],
"source": [
"from langchain.prompts.chat import ChatPromptTemplate"
]
"source": ["from langchain_core.prompts import ChatPromptTemplate"]
},
{
"cell_type": "code",
@@ -27,12 +25,7 @@
"tags": []
},
"outputs": [],
"source": [
"from langserve import RemoteRunnable\n",
"\n",
"openai_llm = RemoteRunnable(\"http://localhost:8000/openai/\")\n",
"anthropic = RemoteRunnable(\"http://localhost:8000/anthropic/\")"
]
"source": ["from langserve import RemoteRunnable\n\nopenai_llm = RemoteRunnable(\"http://localhost:8000/openai/\")\nanthropic = RemoteRunnable(\"http://localhost:8000/anthropic/\")"]
},
{
"cell_type": "markdown",
@@ -48,18 +41,7 @@
"tags": []
},
"outputs": [],
"source": [
"prompt = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\n",
" \"system\",\n",
" \"You are a highly educated person who loves to use big words. \"\n",
" + \"You are also concise. Never answer in more than three sentences.\",\n",
" ),\n",
" (\"human\", \"Tell me about your favorite novel\"),\n",
" ]\n",
").format_messages()"
]
"source": ["prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are a highly educated person who loves to use big words. \"\n + \"You are also concise. Never answer in more than three sentences.\",\n ),\n (\"human\", \"Tell me about your favorite novel\"),\n ]\n).format_messages()"]
},
{
"cell_type": "markdown",
@@ -86,9 +68,7 @@
"output_type": "execute_result"
}
],
"source": [
"anthropic.invoke(prompt)"
]
"source": ["anthropic.invoke(prompt)"]
},
{
"cell_type": "code",
@@ -97,9 +77,7 @@
"tags": []
},
"outputs": [],
"source": [
"openai_llm.invoke(prompt)"
]
"source": ["openai_llm.invoke(prompt)"]
},
{
"cell_type": "markdown",
@@ -126,9 +104,7 @@
"output_type": "execute_result"
}
],
"source": [
"await openai_llm.ainvoke(prompt)"
]
"source": ["await openai_llm.ainvoke(prompt)"]
},
{
"cell_type": "code",
@@ -149,9 +125,7 @@
"output_type": "execute_result"
}
],
"source": [
"anthropic.batch([prompt, prompt])"
]
"source": ["anthropic.batch([prompt, prompt])"]
},
{
"cell_type": "code",
@@ -172,9 +146,7 @@
"output_type": "execute_result"
}
],
"source": [
"await anthropic.abatch([prompt, prompt])"
]
"source": ["await anthropic.abatch([prompt, prompt])"]
},
{
"cell_type": "markdown",
@@ -198,10 +170,7 @@
]
}
],
"source": [
"for chunk in anthropic.stream(prompt):\n",
" print(chunk.content, end=\"\", flush=True)"
]
"source": ["for chunk in anthropic.stream(prompt):\n print(chunk.content, end=\"\", flush=True)"]
},
{
"cell_type": "code",
@@ -218,19 +187,14 @@
]
}
],
"source": [
"async for chunk in anthropic.astream(prompt):\n",
" print(chunk.content, end=\"\", flush=True)"
]
"source": ["async for chunk in anthropic.astream(prompt):\n print(chunk.content, end=\"\", flush=True)"]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {},
"outputs": [],
"source": [
"from langchain.schema.runnable import RunnablePassthrough"
]
"source": ["from langchain_core.runnables import RunnablePassthrough"]
},
{
"cell_type": "code",
@@ -239,37 +203,7 @@
"tags": []
},
"outputs": [],
"source": [
"comedian_chain = (\n",
" ChatPromptTemplate.from_messages(\n",
" [\n",
" (\n",
" \"system\",\n",
" \"You are a comedian that sometimes tells funny jokes and other times you just state facts that are not funny. Please either tell a joke or state fact now but only output one.\",\n",
" ),\n",
" ]\n",
" )\n",
" | openai_llm\n",
")\n",
"\n",
"joke_classifier_chain = (\n",
" ChatPromptTemplate.from_messages(\n",
" [\n",
" (\n",
" \"system\",\n",
" \"Please determine if the joke is funny. Say `funny` if it's funny and `not funny` if not funny. Then repeat the first five words of the joke for reference...\",\n",
" ),\n",
" (\"human\", \"{joke}\"),\n",
" ]\n",
" )\n",
" | anthropic\n",
")\n",
"\n",
"\n",
"chain = {\"joke\": comedian_chain} | RunnablePassthrough.assign(\n",
" classification=joke_classifier_chain\n",
")"
]
"source": ["comedian_chain = (\n ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are a comedian that sometimes tells funny jokes and other times you just state facts that are not funny. Please either tell a joke or state fact now but only output one.\",\n ),\n ]\n )\n | openai_llm\n)\n\njoke_classifier_chain = (\n ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"Please determine if the joke is funny. Say `funny` if it's funny and `not funny` if not funny. Then repeat the first five words of the joke for reference...\",\n ),\n (\"human\", \"{joke}\"),\n ]\n )\n | anthropic\n)\n\n\nchain = {\"joke\": comedian_chain} | RunnablePassthrough.assign(\n classification=joke_classifier_chain\n)"]
},
{
"cell_type": "code",
@@ -290,9 +224,7 @@
"output_type": "execute_result"
}
],
"source": [
"chain.invoke({})"
]
"source": ["chain.invoke({})"]
}
],
"metadata": {
+11 -46
View File
@@ -18,9 +18,7 @@
"tags": []
},
"outputs": [],
"source": [
"from langchain.prompts.chat import ChatPromptTemplate"
]
"source": ["from langchain_core.prompts import ChatPromptTemplate"]
},
{
"cell_type": "code",
@@ -29,11 +27,7 @@
"tags": []
},
"outputs": [],
"source": [
"from langserve import RemoteRunnable\n",
"\n",
"model = RemoteRunnable(\"http://localhost:8000/ollama/\")"
]
"source": ["from langserve import RemoteRunnable\n\nmodel = RemoteRunnable(\"http://localhost:8000/ollama/\")"]
},
{
"cell_type": "markdown",
@@ -49,9 +43,7 @@
"tags": []
},
"outputs": [],
"source": [
"prompt = \"Tell me a 3 sentence story about a cat.\""
]
"source": ["prompt = \"Tell me a 3 sentence story about a cat.\""]
},
{
"cell_type": "code",
@@ -71,9 +63,7 @@
"output_type": "execute_result"
}
],
"source": [
"model.invoke(prompt)"
]
"source": ["model.invoke(prompt)"]
},
{
"cell_type": "code",
@@ -93,9 +83,7 @@
"output_type": "execute_result"
}
],
"source": [
"await model.ainvoke(prompt)"
]
"source": ["await model.ainvoke(prompt)"]
},
{
"cell_type": "markdown",
@@ -131,10 +119,7 @@
"output_type": "execute_result"
}
],
"source": [
"%%time\n",
"model.batch([prompt, prompt])"
]
"source": ["%%time\nmodel.batch([prompt, prompt])"]
},
{
"cell_type": "code",
@@ -152,11 +137,7 @@
]
}
],
"source": [
"%%time\n",
"for _ in range(2):\n",
" model.invoke(prompt)"
]
"source": ["%%time\nfor _ in range(2):\n model.invoke(prompt)"]
},
{
"cell_type": "code",
@@ -177,9 +158,7 @@
"output_type": "execute_result"
}
],
"source": [
"await model.abatch([prompt, prompt])"
]
"source": ["await model.abatch([prompt, prompt])"]
},
{
"cell_type": "markdown",
@@ -206,10 +185,7 @@
]
}
],
"source": [
"for chunk in model.stream(prompt):\n",
" print(chunk.content, end=\"|\", flush=True)"
]
"source": ["for chunk in model.stream(prompt):\n print(chunk.content, end=\"|\", flush=True)"]
},
{
"cell_type": "code",
@@ -227,10 +203,7 @@
]
}
],
"source": [
"async for chunk in model.astream(prompt):\n",
" print(chunk.content, end=\"|\", flush=True)"
]
"source": ["async for chunk in model.astream(prompt):\n print(chunk.content, end=\"|\", flush=True)"]
},
{
"cell_type": "markdown",
@@ -266,15 +239,7 @@
]
}
],
"source": [
"i = 0\n",
"async for event in model.astream_events(prompt, version='v1'):\n",
" print(event)\n",
" if i > 10:\n",
" print('...')\n",
" break\n",
" i += 1"
]
"source": ["i = 0\nasync for event in model.astream_events(prompt, version='v1'):\n print(event)\n if i > 10:\n print('...')\n break\n i += 1"]
}
],
"metadata": {
+6 -23
View File
@@ -16,9 +16,7 @@
"tags": []
},
"outputs": [],
"source": [
"from langchain.prompts.chat import ChatPromptTemplate"
]
"source": ["from langchain_core.prompts import ChatPromptTemplate"]
},
{
"cell_type": "code",
@@ -27,11 +25,7 @@
"tags": []
},
"outputs": [],
"source": [
"from langserve import RemoteRunnable\n",
"\n",
"chain = RemoteRunnable(\"http://localhost:8000/v1/\")"
]
"source": ["from langserve import RemoteRunnable\n\nchain = RemoteRunnable(\"http://localhost:8000/v1/\")"]
},
{
"cell_type": "markdown",
@@ -59,9 +53,7 @@
"output_type": "execute_result"
}
],
"source": [
"chain.invoke({'thing': 'apple', 'language': 'italian', 'info': {\"user_id\": 42, \"user_info\": {\"address\": 42}}})"
]
"source": ["chain.invoke({'thing': 'apple', 'language': 'italian', 'info': {\"user_id\": 42, \"user_info\": {\"address\": 42}}})"]
},
{
"cell_type": "code",
@@ -82,10 +74,7 @@
]
}
],
"source": [
"for chunk in chain.stream({'thing': 'apple', 'language': 'italian', 'info': {\"user_id\": 42, \"user_info\": {\"address\": 42}}}):\n",
" print(chunk)"
]
"source": ["for chunk in chain.stream({'thing': 'apple', 'language': 'italian', 'info': {\"user_id\": 42, \"user_info\": {\"address\": 42}}}):\n print(chunk)"]
},
{
"cell_type": "code",
@@ -94,11 +83,7 @@
"tags": []
},
"outputs": [],
"source": [
"from langserve import RemoteRunnable\n",
"\n",
"chain = RemoteRunnable(\"http://localhost:8000/v2/\")"
]
"source": ["from langserve import RemoteRunnable\n\nchain = RemoteRunnable(\"http://localhost:8000/v2/\")"]
},
{
"cell_type": "code",
@@ -119,9 +104,7 @@
"output_type": "execute_result"
}
],
"source": [
"chain.invoke({'thing': 'apple', 'language': 'italian', 'info': {\"user_id\": 42, \"user_info\": {\"address\": 42}}})"
]
"source": ["chain.invoke({'thing': 'apple', 'language': 'italian', 'info': {\"user_id\": 42, \"user_info\": {\"address\": 42}}})"]
}
],
"metadata": {
+67 -11
View File
@@ -42,6 +42,7 @@ 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 pydantic.v1 import BaseModel as BaseModelV1
from starlette.requests import Request
from starlette.responses import JSONResponse, Response
from typing_extensions import TypedDict
@@ -61,7 +62,7 @@ from langserve.schema import (
PublicTraceLink,
PublicTraceLinkCreateRequest,
)
from langserve.serialization import WellKnownLCSerializer
from langserve.serialization import Serializer, WellKnownLCSerializer
from langserve.validation import (
BatchBaseResponse,
BatchRequestShallowValidator,
@@ -534,6 +535,8 @@ class APIHandler:
per_req_config_modifier: Optional[PerRequestConfigModifier] = None,
stream_log_name_allow_list: Optional[Sequence[str]] = None,
playground_type: Literal["default", "chat"] = "default",
astream_events_version: Literal["v1", "v2"] = "v2",
serializer: Optional[Serializer] = None,
) -> None:
"""Create an API handler for the given runnable.
@@ -568,6 +571,7 @@ class APIHandler:
If true, the client will be able to show trace information
including events that occurred on the server side.
Be sure not to include any sensitive information in the callback events.
This is a **beta** API.
enable_feedback_endpoint: Whether to enable an endpoint for logging feedback
to LangSmith. Disabled by default. If this flag is disabled or LangSmith
tracing is not enabled for the runnable, then 4xx errors will be thrown
@@ -595,6 +599,10 @@ class APIHandler:
If not provided, then all logs will be allowed to be streamed.
Use to also limit the events that can be streamed by the stream_events.
TODO: Introduce deprecation for this parameter to rename it
astream_events_version: version of the stream events endpoint to use.
By default "v2".
serializer: optional serializer to use for serializing the output.
If not provided, the default serializer will be used.
"""
if importlib.util.find_spec("sse_starlette") is None:
raise ImportError(
@@ -626,12 +634,18 @@ class APIHandler:
# and when tracing information is logged, we'll be able to see
# traces for the path /foo/bar.
self._run_name = self._base_url
if include_callback_events:
warn_beta(
message="Including callback events in the response is in beta. "
"This API may change in the future."
)
self._include_callback_events = include_callback_events
self._per_req_config_modifier = per_req_config_modifier
self._serializer = WellKnownLCSerializer()
self._serializer = serializer or WellKnownLCSerializer()
self._enable_feedback_endpoint = enable_feedback_endpoint
self._enable_public_trace_link_endpoint = enable_public_trace_link_endpoint
self._names_in_stream_allow_list = stream_log_name_allow_list
self._astream_events_version = astream_events_version
if token_feedback_config:
if len(token_feedback_config["key_configs"]) != 1:
@@ -671,15 +685,57 @@ class APIHandler:
model_namespace = _replace_non_alphanumeric_with_underscores(path.strip("/"))
input_type_ = _resolve_model(
runnable.get_input_schema(), "Input", model_namespace
)
try:
input_type_ = _resolve_model(
runnable.get_input_schema(), "Input", model_namespace
)
except Exception as e:
# Attempt to surface a more informative user facing error
raise_original_error = True
try:
if isinstance(runnable.get_input_schema(), BaseModelV1):
raise_original_error = False
raise ValueError(
"Found an input type which is a pydantic v1 model."
"Please use pydantic.BaseModel rather than "
"pydantic.v1.BaseModel."
)
finally: # noqa
if raise_original_error:
print(
"Encountered an error while resolving the inputs of "
"the Runnable. Try specifying the input type explicitly "
"using the `with_types` method on the runnable.\n"
"See https://api.python.langchain.com/en/latest/runnables/langchain_core.runnables.base.Runnable.html " # noqa: E501
)
raise e
output_type_ = _resolve_model(
runnable.get_output_schema(),
"Output",
model_namespace,
)
try:
output_type_ = _resolve_model(
runnable.get_output_schema(),
"Output",
model_namespace,
)
except Exception as e:
# Attempt to surface a more informative user facing error
raise_original_error = True
try:
if isinstance(runnable.get_output_schema(), BaseModelV1):
raise_original_error = False
raise ValueError(
"Found an output type which is a pydantic v1 model."
"Please use pydantic.BaseModel rather than "
"pydantic.v1.BaseModel."
)
finally: # noqa
if raise_original_error:
print(
"Encountered an error while resolving the inputs of "
"the Runnable. Try specifying the output type explicitly "
"using the `with_types` method on the runnable.\n"
"See https://api.python.langchain.com/en/latest/runnables/langchain_core.runnables.base.Runnable.html " # noqa: E501
)
raise e
self._ConfigPayload = _add_namespace_to_model(
model_namespace, runnable.config_schema(include=config_keys)
@@ -1343,7 +1399,7 @@ class APIHandler:
exclude_names=stream_events_request.exclude_names,
exclude_types=stream_events_request.exclude_types,
exclude_tags=stream_events_request.exclude_tags,
version="v1",
version=self._astream_events_version,
):
if (
self._names_in_stream_allow_list is None
+21 -7
View File
@@ -48,7 +48,7 @@ class AsyncEventAggregatorCallback(AsyncCallbackHandler):
async def on_chat_model_start(
self,
serialized: Dict[str, Any],
serialized: Optional[Dict[str, Any]],
messages: List[List[BaseMessage]],
*,
run_id: UUID,
@@ -73,7 +73,7 @@ class AsyncEventAggregatorCallback(AsyncCallbackHandler):
async def on_chain_start(
self,
serialized: Dict[str, Any],
serialized: Optional[Dict[str, Any]],
inputs: Dict[str, Any],
*,
run_id: UUID,
@@ -138,7 +138,7 @@ class AsyncEventAggregatorCallback(AsyncCallbackHandler):
async def on_retriever_start(
self,
serialized: Dict[str, Any],
serialized: Optional[Dict[str, Any]],
query: str,
*,
run_id: UUID,
@@ -202,7 +202,7 @@ class AsyncEventAggregatorCallback(AsyncCallbackHandler):
async def on_tool_start(
self,
serialized: Dict[str, Any],
serialized: Optional[Dict[str, Any]],
input_str: str,
*,
run_id: UUID,
@@ -306,7 +306,7 @@ class AsyncEventAggregatorCallback(AsyncCallbackHandler):
async def on_llm_start(
self,
serialized: Dict[str, Any],
serialized: Optional[Dict[str, Any]],
prompts: List[str],
*,
run_id: UUID,
@@ -445,7 +445,14 @@ async def ahandle_callbacks(
if event["parent_run_id"] is None: # How do we make sure it's None!?
event["parent_run_id"] = callback_manager.run_id
event_data = {key: value for key, value in event.items() if key != "type"}
event_data = {
key: value
for key, value in event.items()
if key != "type" and key != "kwargs"
}
if "kwargs" in event:
event_data.update(event["kwargs"])
await ahandle_event(
# Unpacking like this may not work
@@ -467,7 +474,14 @@ def handle_callbacks(
if event["parent_run_id"] is None: # How do we make sure it's None!?
event["parent_run_id"] = callback_manager.run_id
event_data = {key: value for key, value in event.items() if key != "type"}
event_data = {
key: value
for key, value in event.items()
if key != "type" and key != "kwargs"
}
if "kwargs" in event:
event_data.update(event["kwargs"])
handle_event(
# Unpacking like this may not work
+6 -1
View File
@@ -46,7 +46,12 @@
"postcss": "^8.4.31",
"tailwindcss": "^3.3.3",
"typescript": "^5.0.2",
"vite": "^4.4.5",
"vite": "^4.5.2",
"vite-plugin-svgr": "^4.1.0"
},
"resolutions": {
"braces": "^3.0.3",
"cross-spawn": "^7.0.5",
"rollup": "^3.29.5"
}
}
+21 -21
View File
@@ -1311,12 +1311,12 @@ brace-expansion@^1.1.7:
balanced-match "^1.0.0"
concat-map "0.0.1"
braces@^3.0.2, braces@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
braces@^3.0.2, braces@^3.0.3, braces@~3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
dependencies:
fill-range "^7.0.1"
fill-range "^7.1.1"
browserslist@^4.21.10, browserslist@^4.21.9:
version "4.22.1"
@@ -1460,10 +1460,10 @@ cosmiconfig@^8.1.3:
parse-json "^5.2.0"
path-type "^4.0.0"
cross-spawn@^7.0.2:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
cross-spawn@^7.0.2, cross-spawn@^7.0.5:
version "7.0.6"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
dependencies:
path-key "^3.1.0"
shebang-command "^2.0.0"
@@ -1750,10 +1750,10 @@ file-entry-cache@^6.0.1:
dependencies:
flat-cache "^3.0.4"
fill-range@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
fill-range@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
dependencies:
to-regex-range "^5.0.1"
@@ -2483,10 +2483,10 @@ rimraf@^3.0.2:
dependencies:
glob "^7.1.3"
rollup@^3.27.1:
version "3.29.4"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.4.tgz#4d70c0f9834146df8705bfb69a9a19c9e1109981"
integrity sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==
rollup@^3.27.1, rollup@^3.29.5:
version "3.29.5"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.5.tgz#8a2e477a758b520fb78daf04bca4c522c1da8a54"
integrity sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==
optionalDependencies:
fsevents "~2.3.2"
@@ -2770,10 +2770,10 @@ vite-plugin-svgr@^4.1.0:
"@svgr/core" "^8.1.0"
"@svgr/plugin-jsx" "^8.1.0"
vite@^4.4.5:
version "4.5.0"
resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.0.tgz#ec406295b4167ac3bc23e26f9c8ff559287cff26"
integrity sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==
vite@^4.5.2:
version "4.5.14"
resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.14.tgz#2e652bc1d898265d987d6543ce866ecd65fa4086"
integrity sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==
dependencies:
esbuild "^0.18.10"
postcss "^8.4.27"
+28 -8
View File
@@ -8,6 +8,7 @@ import weakref
from concurrent.futures import ThreadPoolExecutor
from functools import lru_cache
from typing import (
TYPE_CHECKING,
Any,
AsyncIterator,
Dict,
@@ -21,7 +22,7 @@ from typing import (
from urllib.parse import urljoin
import httpx
from httpx._types import AuthTypes, CertTypes, CookieTypes, HeaderTypes, VerifyTypes
from httpx._types import AuthTypes, CertTypes, CookieTypes, HeaderTypes
from langchain_core.callbacks import (
AsyncCallbackManagerForChainRun,
CallbackManagerForChainRun,
@@ -49,6 +50,10 @@ from langserve.server_sent_events import aconnect_sse, connect_sse
logger = logging.getLogger(__name__)
if TYPE_CHECKING:
# For type checking httpx types
import ssl
def _is_json_serializable(obj: Any) -> bool:
"""Return True if the object is json serializable."""
@@ -120,6 +125,12 @@ def _log_error_message_once(error_message: str) -> None:
logger.error(error_message)
@lru_cache(maxsize=1_000) # Will accommodate up to 1_000 different error messages
def _log_info_message_once(error_message: str) -> None:
"""Log an error message once."""
logger.info(error_message)
def _sanitize_request(request: httpx.Request) -> httpx.Request:
"""Remove sensitive headers from the request."""
accept_headers = {
@@ -275,10 +286,11 @@ class RemoteRunnable(Runnable[Input, Output]):
auth: Optional[AuthTypes] = None,
headers: Optional[HeaderTypes] = None,
cookies: Optional[CookieTypes] = None,
verify: VerifyTypes = True,
verify: ssl.SSLContext | str | bool = True,
cert: Optional[CertTypes] = None,
client_kwargs: Optional[Dict[str, Any]] = None,
use_server_callback_events: bool = True,
serializer: Optional[Serializer] = None,
) -> None:
"""Initialize the client.
@@ -294,6 +306,8 @@ class RemoteRunnable(Runnable[Input, Output]):
and async httpx clients
use_server_callback_events: Whether to invoke callbacks on any
callback events returned by the server.
serializer: The serializer to use for serializing and deserializing
data. If not provided, a default serializer will be used.
"""
_client_kwargs = client_kwargs or {}
# Enforce trailing slash
@@ -321,7 +335,7 @@ class RemoteRunnable(Runnable[Input, Output]):
# Register cleanup handler once RemoteRunnable is garbage collected
weakref.finalize(self, _close_clients, self.sync_client, self.async_client)
self._lc_serializer = WellKnownLCSerializer()
self._lc_serializer = serializer or WellKnownLCSerializer()
self._use_server_callback_events = use_server_callback_events
def _invoke(
@@ -752,7 +766,7 @@ class RemoteRunnable(Runnable[Input, Output]):
input: Any,
config: Optional[RunnableConfig] = None,
*,
version: Literal["v1"],
version: Literal["v1", "v2", None] = None,
include_names: Optional[Sequence[str]] = None,
include_types: Optional[Sequence[str]] = None,
include_tags: Optional[Sequence[str]] = None,
@@ -775,7 +789,8 @@ class RemoteRunnable(Runnable[Input, Output]):
input: The input to the runnable
config: The config to use for the runnable
version: The version of the astream_events to use.
Currently only "v1" is supported.
Currently, this input is IGNORED on the client.
The server will return whatever format it's configured with.
include_names: The names of the events to include
include_types: The types of the events to include
include_tags: The tags of the events to include
@@ -783,13 +798,18 @@ class RemoteRunnable(Runnable[Input, Output]):
exclude_types: The types of the events to exclude
exclude_tags: The tags of the events to exclude
"""
if version != "v1":
raise ValueError(f"Unsupported version: {version}. Use 'v1'")
# Create a stream handler that will emit Log objects
config = ensure_config(config)
callback_manager = get_async_callback_manager_for_config(config)
if version is not None:
_log_info_message_once(
"Versioning of the astream_events API is not supported on the client "
"side currently. The server will return events in whatever format "
"it was configured with in add_routes or APIHandler. "
"To stop seeing this message, remove the `version` argument."
)
events = []
run_manager = await callback_manager.on_chain_start(
+6 -1
View File
@@ -48,7 +48,12 @@
"postcss": "^8.4.31",
"tailwindcss": "^3.3.3",
"typescript": "^5.0.2",
"vite": "^4.4.5",
"vite": "^4.5.2",
"vite-plugin-svgr": "^4.1.0"
},
"resolutions": {
"braces": "^3.0.3",
"cross-spawn": "^7.0.5",
"rollup": "^3.29.5"
}
}
+21 -21
View File
@@ -1338,12 +1338,12 @@ brace-expansion@^1.1.7:
balanced-match "^1.0.0"
concat-map "0.0.1"
braces@^3.0.2, braces@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
braces@^3.0.2, braces@^3.0.3, braces@~3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
dependencies:
fill-range "^7.0.1"
fill-range "^7.1.1"
browserslist@^4.21.10, browserslist@^4.21.9:
version "4.22.1"
@@ -1482,10 +1482,10 @@ cosmiconfig@^8.1.3:
parse-json "^5.2.0"
path-type "^4.0.0"
cross-spawn@^7.0.2:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
cross-spawn@^7.0.2, cross-spawn@^7.0.5:
version "7.0.6"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
dependencies:
path-key "^3.1.0"
shebang-command "^2.0.0"
@@ -1777,10 +1777,10 @@ file-entry-cache@^6.0.1:
dependencies:
flat-cache "^3.0.4"
fill-range@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
fill-range@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
dependencies:
to-regex-range "^5.0.1"
@@ -2503,10 +2503,10 @@ rimraf@^3.0.2:
dependencies:
glob "^7.1.3"
rollup@^3.27.1:
version "3.29.4"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.4.tgz#4d70c0f9834146df8705bfb69a9a19c9e1109981"
integrity sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==
rollup@^3.27.1, rollup@^3.29.5:
version "3.29.5"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.5.tgz#8a2e477a758b520fb78daf04bca4c522c1da8a54"
integrity sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==
optionalDependencies:
fsevents "~2.3.2"
@@ -2790,10 +2790,10 @@ vite-plugin-svgr@^4.1.0:
"@svgr/core" "^8.1.0"
"@svgr/plugin-jsx" "^8.1.0"
vite@^4.4.5:
version "4.5.0"
resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.0.tgz#ec406295b4167ac3bc23e26f9c8ff559287cff26"
integrity sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==
vite@^4.5.2:
version "4.5.14"
resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.14.tgz#2e652bc1d898265d987d6543ce866ecd65fa4086"
integrity sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==
dependencies:
esbuild "^0.18.10"
postcss "^8.4.27"
+37 -27
View File
@@ -157,40 +157,50 @@ def _decode_event_data(value: Any) -> Any:
class Serializer(abc.ABC):
@abc.abstractmethod
def dumpd(self, obj: Any) -> Any:
"""Convert the given object to a JSON serializable object."""
@abc.abstractmethod
def dumps(self, obj: Any) -> bytes:
"""Dump the given object as a JSON string."""
@abc.abstractmethod
def loads(self, s: bytes) -> Any:
"""Load the given JSON string."""
@abc.abstractmethod
def loadd(self, obj: Any) -> Any:
"""Load the given object."""
class WellKnownLCSerializer(Serializer):
def dumpd(self, obj: Any) -> Any:
"""Convert the given object to a JSON serializable object."""
return orjson.loads(orjson.dumps(obj, default=default))
def dumps(self, obj: Any) -> bytes:
"""Dump the given object as a JSON string."""
return orjson.dumps(obj, default=default)
def loadd(self, obj: Any) -> Any:
"""Load the given object."""
return _decode_lc_objects(obj)
return orjson.loads(self.dumps(obj))
def loads(self, s: bytes) -> Any:
"""Load the given JSON string."""
return self.loadd(orjson.loads(s))
@abc.abstractmethod
def dumps(self, obj: Any) -> bytes:
"""Dump the given object to a JSON byte string."""
@abc.abstractmethod
def loadd(self, s: bytes) -> Any:
"""Given a python object, load it into a well known object.
The obj represents content that was json loaded from a string, but
not yet validated or converted into a well known object.
"""
class WellKnownLCSerializer(Serializer):
"""A pre-defined serializer for well known LangChain objects.
This is the default serialized used by LangServe for serializing and
de-serializing well known LangChain objects.
If you need to extend the serialization capabilities for your own application,
feel free to create a new instance of the Serializer class and implement
the abstract methods dumps and loadd.
"""
def dumps(self, obj: Any) -> bytes:
"""Dump the given object to a JSON byte string."""
return orjson.dumps(obj, default=default)
def loadd(self, obj: Any) -> Any:
"""Given a python object, load it into a well known object.
The obj represents content that was json loaded from a string, but
not yet validated or converted into a well known object.
"""
return _decode_lc_objects(obj)
def _project_top_level(model: BaseModel) -> Dict[str, Any]:
"""Project the top level of the model as dict."""
+14 -4
View File
@@ -26,6 +26,7 @@ from langserve.api_handler import (
TokenFeedbackConfig,
_is_hosted,
)
from langserve.serialization import Serializer
try:
from fastapi import APIRouter, Depends, FastAPI, Request, Response
@@ -262,6 +263,8 @@ def add_routes(
enabled_endpoints: Optional[Sequence[EndpointName]] = None,
dependencies: Optional[Sequence[Depends]] = None,
playground_type: Literal["default", "chat"] = "default",
astream_events_version: Literal["v1", "v2"] = "v2",
serializer: Optional[Serializer] = None,
) -> None:
"""Register the routes on the given FastAPI app or APIRouter.
@@ -380,14 +383,18 @@ def add_routes(
- chat: UX is optimized for chat-like interactions. Please review
the README in langserve for more details about constraints (e.g.,
which message types are supported etc.)
astream_events_version: version of the stream events endpoint to use.
By default "v2".
serializer: The serializer to use for serializing the output. If not provided,
the default serializer will be used.
""" # noqa: E501
if not isinstance(runnable, Runnable):
raise TypeError(
f"Expected a Runnable, got {type(runnable)}. "
f"The second argument to add_routes should be a Runnable instance."
f"add_route(app, runnable, ...) is the correct usage."
f"Please make sure that you are using a runnable which is an instance of "
f"langchain_core.runnables.Runnable."
"The second argument to add_routes should be a Runnable instance."
"add_route(app, runnable, ...) is the correct usage."
"Please make sure that you are using a runnable which is an instance of "
"langchain_core.runnables.Runnable."
)
endpoint_configuration = _EndpointConfiguration(
@@ -443,7 +450,10 @@ def add_routes(
per_req_config_modifier=per_req_config_modifier,
stream_log_name_allow_list=stream_log_name_allow_list,
playground_type=playground_type,
astream_events_version=astream_events_version,
serializer=serializer,
)
namespace = path or ""
route_tags = [path.strip("/")] if path else None
+2 -1
View File
@@ -1,8 +1,9 @@
"""Adapted from https://github.com/florimondmanca/httpx-sse"""
from contextlib import asynccontextmanager, contextmanager
from typing import Any, AsyncIterator, Iterator, List, Optional, TypedDict
from typing import Any, AsyncIterator, Iterator, List, Optional
import httpx
from typing_extensions import TypedDict
class ServerSentEvent(TypedDict):
+35 -53
View File
@@ -396,27 +396,29 @@ class StreamEventsParameters(BaseModel):
# status code and a message.
class OnChainStart(BaseModel):
"""On Chain Start Callback Event."""
class BaseCallback(BaseModel):
"""Base class for all callback events."""
serialized: Dict[str, Any]
inputs: Any
run_id: UUID
parent_run_id: Optional[UUID] = None
tags: Optional[List[str]] = None
metadata: Optional[Dict[str, Any]] = None
kwargs: Any = None
class OnChainStart(BaseCallback):
"""On Chain Start Callback Event."""
serialized: Optional[Dict[str, Any]] = None
inputs: Any
kwargs: Optional[Dict[str, Any]] = None
type: Literal["on_chain_start"] = "on_chain_start"
class OnChainEnd(BaseModel):
class OnChainEnd(BaseCallback):
"""On Chain End Callback Event."""
outputs: Any
run_id: UUID
parent_run_id: Optional[UUID] = None
tags: Optional[List[str]] = None
kwargs: Any = None
kwargs: Optional[Dict[str, Any]] = None
type: Literal["on_chain_end"] = "on_chain_end"
@@ -428,38 +430,35 @@ class Error(BaseModel):
type: Literal["error"] = "error"
class OnChainError(BaseModel):
class OnChainError(BaseCallback):
"""On Chain Error Callback Event."""
error: Error
run_id: UUID
parent_run_id: Optional[UUID] = None
tags: Optional[List[str]] = None
kwargs: Any = None
kwargs: Optional[Dict[str, Any]] = None
type: Literal["on_chain_error"] = "on_chain_error"
class OnToolStart(BaseModel):
class OnToolStart(BaseCallback):
"""On Tool Start Callback Event."""
serialized: Dict[str, Any]
serialized: Optional[Dict[str, Any]] = None
input_str: str
run_id: UUID
parent_run_id: Optional[UUID] = None
tags: Optional[List[str]] = None
metadata: Optional[Dict[str, Any]] = None
kwargs: Any = None
kwargs: Optional[Dict[str, Any]] = None
type: Literal["on_tool_start"] = "on_tool_start"
class OnToolEnd(BaseModel):
class OnToolEnd(BaseCallback):
"""On Tool End Callback Event."""
output: str
run_id: UUID
parent_run_id: Optional[UUID] = None
tags: Optional[List[str]] = None
kwargs: Any = None
kwargs: Optional[Dict[str, Any]] = None
type: Literal["on_tool_end"] = "on_tool_end"
@@ -467,36 +466,29 @@ class OnToolError(BaseModel):
"""On Tool Error Callback Event."""
error: Error
run_id: UUID
parent_run_id: Optional[UUID] = None
tags: Optional[List[str]] = None
kwargs: Any = None
kwargs: Optional[Dict[str, Any]] = None
type: Literal["on_tool_error"] = "on_tool_error"
class OnChatModelStart(BaseModel):
class OnChatModelStart(BaseCallback):
"""On Chat Model Start Callback Event."""
serialized: Dict[str, Any]
serialized: Optional[Dict[str, Any]] = None
messages: List[List[BaseMessage]]
run_id: UUID
parent_run_id: Optional[UUID] = None
tags: Optional[List[str]] = None
metadata: Optional[Dict[str, Any]] = None
kwargs: Any = None
kwargs: Optional[Dict[str, Any]] = None
type: Literal["on_chat_model_start"] = "on_chat_model_start"
class OnLLMStart(BaseModel):
class OnLLMStart(BaseCallback):
"""On LLM Start Callback Event."""
serialized: Dict[str, Any]
serialized: Optional[Dict[str, Any]] = None
prompts: List[str]
run_id: UUID
parent_run_id: Optional[UUID] = None
tags: Optional[List[str]] = None
metadata: Optional[Dict[str, Any]] = None
kwargs: Any = None
kwargs: Optional[Dict[str, Any]] = None
type: Literal["on_llm_start"] = "on_llm_start"
@@ -515,49 +507,39 @@ class LLMResult(BaseModel):
"""List of metadata info for model call for each input."""
class OnLLMEnd(BaseModel):
class OnLLMEnd(BaseCallback):
"""On LLM End Callback Event."""
response: LLMResult
run_id: UUID
parent_run_id: Optional[UUID] = None
tags: Optional[List[str]] = None
kwargs: Any = None
kwargs: Optional[Dict[str, Any]] = None
type: Literal["on_llm_end"] = "on_llm_end"
class OnRetrieverStart(BaseModel):
class OnRetrieverStart(BaseCallback):
"""On Retriever Start Callback Event."""
serialized: Dict[str, Any]
serialized: Optional[Dict[str, Any]] = None
query: str
run_id: UUID
parent_run_id: Optional[UUID] = None
tags: Optional[List[str]] = None
metadata: Optional[Dict[str, Any]] = None
kwargs: Any = None
kwargs: Optional[Dict[str, Any]] = None
type: Literal["on_retriever_start"] = "on_retriever_start"
class OnRetrieverError(BaseModel):
class OnRetrieverError(BaseCallback):
"""On Retriever Error Callback Event."""
error: Error
run_id: UUID
parent_run_id: Optional[UUID] = None
tags: Optional[List[str]] = None
kwargs: Any = None
kwargs: Optional[Dict[str, Any]] = None
type: Literal["on_retriever_error"] = "on_retriever_error"
class OnRetrieverEnd(BaseModel):
class OnRetrieverEnd(BaseCallback):
"""On Retriever End Callback Event."""
documents: Sequence[Document]
run_id: UUID
parent_run_id: Optional[UUID] = None
tags: Optional[List[str]] = None
kwargs: Any = None
kwargs: Optional[Dict[str, Any]] = None
type: Literal["on_retriever_end"] = "on_retriever_end"
Generated
+31 -87
View File
@@ -826,13 +826,13 @@ tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipyth
[[package]]
name = "fastapi"
version = "0.114.1"
version = "0.114.2"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
optional = false
python-versions = ">=3.8"
files = [
{file = "fastapi-0.114.1-py3-none-any.whl", hash = "sha256:5d4746f6e4b7dff0b4f6b6c6d5445645285f662fe75886e99af7ee2d6b58bb3e"},
{file = "fastapi-0.114.1.tar.gz", hash = "sha256:1d7bbbeabbaae0acb0c22f0ab0b040f642d3093ca3645f8c876b6f91391861d8"},
{file = "fastapi-0.114.2-py3-none-any.whl", hash = "sha256:44474a22913057b1acb973ab90f4b671ba5200482e7622816d79105dcece1ac5"},
{file = "fastapi-0.114.2.tar.gz", hash = "sha256:0adb148b62edb09e8c6eeefa3ea934e8f276dabc038c5a82989ea6346050c3da"},
]
[package.dependencies]
@@ -1062,15 +1062,18 @@ zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "idna"
version = "3.8"
version = "3.9"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
python-versions = ">=3.6"
files = [
{file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"},
{file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"},
{file = "idna-3.9-py3-none-any.whl", hash = "sha256:69297d5da0cc9281c77efffb4e730254dd45943f45bbfb461de5991713989b1e"},
{file = "idna-3.9.tar.gz", hash = "sha256:e5c5dafde284f26e9e0f28f6ea2d6400abd5ca099864a67f576f3981c6476124"},
]
[package.extras]
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
[[package]]
name = "importlib-metadata"
version = "8.5.0"
@@ -1564,41 +1567,44 @@ test = ["hatch", "ipykernel", "openapi-core (>=0.18.0,<0.19.0)", "openapi-spec-v
[[package]]
name = "langchain-core"
version = "0.3.0.dev5"
version = "0.3.0"
description = "Building applications with LLMs through composability"
optional = false
python-versions = "<4.0,>=3.9"
files = [
{file = "langchain_core-0.3.0.dev5-py3-none-any.whl", hash = "sha256:8de38ea9dd15f9705d20ee44af7160216052467d47893d5473e0e878861685be"},
{file = "langchain_core-0.3.0.dev5.tar.gz", hash = "sha256:d0986b3e2810522d90eb09d048ee4a92b079a334b313f4b7429624b4a062adff"},
{file = "langchain_core-0.3.0-py3-none-any.whl", hash = "sha256:bee6dae2366d037ef0c5b87401fed14b5497cad26f97724e8c9ca7bc9239e847"},
{file = "langchain_core-0.3.0.tar.gz", hash = "sha256:1249149ea3ba24c9c761011483c14091573a5eb1a773aa0db9c8ad155dd4a69d"},
]
[package.dependencies]
jsonpatch = ">=1.33,<2.0"
langsmith = ">=0.1.117,<0.2.0"
packaging = ">=23.2,<25"
pydantic = ">=2.7.4,<3.0.0"
pydantic = [
{version = ">=2.5.2,<3.0.0", markers = "python_full_version < \"3.12.4\""},
{version = ">=2.7.4,<3.0.0", markers = "python_full_version >= \"3.12.4\""},
]
PyYAML = ">=5.3"
tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<9.0.0"
typing-extensions = ">=4.7"
[[package]]
name = "langsmith"
version = "0.1.117"
version = "0.1.120"
description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform."
optional = false
python-versions = "<4.0,>=3.8.1"
files = [
{file = "langsmith-0.1.117-py3-none-any.whl", hash = "sha256:e936ee9bcf8293b0496df7ba462a3702179fbe51f9dc28744b0fbec0dbf206ae"},
{file = "langsmith-0.1.117.tar.gz", hash = "sha256:a1b532f49968b9339bcaff9118d141846d52ed3d803f342902e7448edf1d662b"},
{file = "langsmith-0.1.120-py3-none-any.whl", hash = "sha256:54d2785e301646c0988e0a69ebe4d976488c87b41928b358cb153b6ddd8db62b"},
{file = "langsmith-0.1.120.tar.gz", hash = "sha256:25499ca187b41bd89d784b272b97a8d76f60e0e21bdf20336e8a2aa6a9b23ac9"},
]
[package.dependencies]
httpx = ">=0.23.0,<1"
orjson = ">=3.9.14,<4.0.0"
pydantic = [
{version = ">=2.7.4,<3.0.0", markers = "python_full_version >= \"3.12.4\""},
{version = ">=1,<3", markers = "python_full_version < \"3.12.4\""},
{version = ">=2.7.4,<3.0.0", markers = "python_full_version >= \"3.12.4\""},
]
requests = ">=2,<3"
@@ -2117,13 +2123,13 @@ ptyprocess = ">=0.5"
[[package]]
name = "platformdirs"
version = "4.3.2"
version = "4.3.3"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
optional = false
python-versions = ">=3.8"
files = [
{file = "platformdirs-4.3.2-py3-none-any.whl", hash = "sha256:eb1c8582560b34ed4ba105009a4badf7f6f85768b30126f351328507b2beb617"},
{file = "platformdirs-4.3.2.tar.gz", hash = "sha256:9e5e27a08aa095dd127b9f2e764d74254f482fef22b0970773bfba79d091ab8c"},
{file = "platformdirs-4.3.3-py3-none-any.whl", hash = "sha256:50a5450e2e84f44539718293cbb1da0a0885c9d14adf21b77bae4e66fc99d9b5"},
{file = "platformdirs-4.3.3.tar.gz", hash = "sha256:d4e0b7d8ec176b341fb03cb11ca12d0276faa8c485f9cd218f613840463fc2c0"},
]
[package.extras]
@@ -2254,8 +2260,8 @@ files = [
annotated-types = ">=0.6.0"
pydantic-core = "2.23.3"
typing-extensions = [
{version = ">=4.12.2", markers = "python_version >= \"3.13\""},
{version = ">=4.6.1", markers = "python_version < \"3.13\""},
{version = ">=4.12.2", markers = "python_version >= \"3.13\""},
]
[package.extras]
@@ -2377,23 +2383,6 @@ files = [
[package.extras]
windows-terminal = ["colorama (>=0.4.6)"]
[[package]]
name = "pyproject-toml"
version = "0.0.10"
description = "Project intend to implement PEP 517, 518, 621, 631 and so on."
optional = false
python-versions = "*"
files = [
{file = "pyproject-toml-0.0.10.tar.gz", hash = "sha256:f0ce0e9934ecb00c0e529b4a1c380edd3034c4be65516769c5f080bdb23dfcb3"},
{file = "pyproject_toml-0.0.10-py3-none-any.whl", hash = "sha256:257a7070617e1a0bcfd8f790817b30bd9193876023a9b9e7a6b4fc976acf4c3e"},
]
[package.dependencies]
jsonschema = "*"
setuptools = ">=42"
toml = "*"
wheel = "*"
[[package]]
name = "pytest"
version = "7.4.4"
@@ -2988,26 +2977,6 @@ nativelib = ["pyobjc-framework-Cocoa", "pywin32"]
objc = ["pyobjc-framework-Cocoa"]
win32 = ["pywin32"]
[[package]]
name = "setuptools"
version = "74.1.2"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
optional = false
python-versions = ">=3.8"
files = [
{file = "setuptools-74.1.2-py3-none-any.whl", hash = "sha256:5f4c08aa4d3ebcb57a50c33b1b07e94315d7fc7230f7115e47fc99776c8ce308"},
{file = "setuptools-74.1.2.tar.gz", hash = "sha256:95b40ed940a1c67eb70fc099094bd6e99c6ee7c23aa2306f4d2697ba7916f9c6"},
]
[package.extras]
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.5.2)"]
core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"]
cover = ["pytest-cov"]
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"]
enabler = ["pytest-enabler (>=2.2)"]
test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"]
type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.11.*)", "pytest-mypy"]
[[package]]
name = "six"
version = "1.16.0"
@@ -3149,17 +3118,6 @@ webencodings = ">=0.4"
doc = ["sphinx", "sphinx_rtd_theme"]
test = ["pytest", "ruff"]
[[package]]
name = "toml"
version = "0.10.2"
description = "Python Library for Tom's Obvious, Minimal Language"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
files = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
[[package]]
name = "tomli"
version = "2.0.1"
@@ -3264,13 +3222,13 @@ dev = ["flake8", "flake8-annotations", "flake8-bandit", "flake8-bugbear", "flake
[[package]]
name = "urllib3"
version = "2.2.2"
version = "2.2.3"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
python-versions = ">=3.8"
files = [
{file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"},
{file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"},
{file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"},
{file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"},
]
[package.extras]
@@ -3634,20 +3592,6 @@ files = [
{file = "websockets-13.0.1.tar.gz", hash = "sha256:4d6ece65099411cfd9a48d13701d7438d9c34f479046b34c50ff60bb8834e43e"},
]
[[package]]
name = "wheel"
version = "0.44.0"
description = "A built-package format for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "wheel-0.44.0-py3-none-any.whl", hash = "sha256:2376a90c98cc337d18623527a97c31797bd02bad0033d41547043a1cbfbe448f"},
{file = "wheel-0.44.0.tar.gz", hash = "sha256:a29c3f2817e95ab89aa4660681ad547c0e9547f20e75b0562fe7723c9a2a9d49"},
]
[package.extras]
test = ["pytest (>=6.0.0)", "setuptools (>=65)"]
[[package]]
name = "y-py"
version = "0.6.2"
@@ -3857,13 +3801,13 @@ test = ["mypy", "pre-commit", "pytest", "pytest-asyncio", "websockets (>=10.0)"]
[[package]]
name = "zipp"
version = "3.20.1"
version = "3.20.2"
description = "Backport of pathlib-compatible object wrapper for zip files"
optional = false
python-versions = ">=3.8"
files = [
{file = "zipp-3.20.1-py3-none-any.whl", hash = "sha256:9960cd8967c8f85a56f920d5d507274e74f9ff813a0ab8889a5b5be2daf44064"},
{file = "zipp-3.20.1.tar.gz", hash = "sha256:c22b14cc4763c5a5b04134207736c107db42e9d3ef2d9779d465f5f1bcba572b"},
{file = "zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350"},
{file = "zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29"},
]
[package.extras]
@@ -3882,4 +3826,4 @@ server = ["fastapi", "sse-starlette"]
[metadata]
lock-version = "2.0"
python-versions = "^3.9"
content-hash = "bee76de01384e265c807d785959226cdf9c788b4d5dfc3772dcce5d70a8f6121"
content-hash = "8bb5ca68199c071d2d95d1bec5d4cf3e36a0e0535853b2370cfb594ba0808c0d"
+4 -5
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langserve"
version = "0.3.0dev2"
version = "0.3.2"
description = ""
readme = "README.md"
authors = ["LangChain"]
@@ -11,12 +11,11 @@ include = ["langserve/playground/dist/**/*", "langserve/chat_playground/dist/**/
[tool.poetry.dependencies]
python = "^3.9"
httpx = ">=0.23.0" # May be able to decrease this version
httpx = ">=0.23.0,<1.0"
fastapi = {version = ">=0.90.1,<1", optional = true}
sse-starlette = {version = "^1.3.0", optional = true}
langchain-core = "0.3.0dev5"
orjson = ">=2"
pyproject-toml = "^0.0.10"
langchain-core = "^0.3"
orjson = ">=2,<4"
pydantic = "^2.7"
[tool.poetry.group.dev.dependencies]
-6
View File
@@ -189,9 +189,3 @@ 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"
+250 -75
View File
@@ -56,6 +56,7 @@ from langchain_core.tracers import RunLog, RunLogPatch
from langsmith import schemas as ls_schemas
from langsmith.client import Client
from langsmith.schemas import FeedbackIngestToken
from orjson import orjson
from pydantic import BaseModel, Field, __version__
from pytest import MonkeyPatch
from pytest_mock import MockerFixture
@@ -122,6 +123,19 @@ def _replace_run_id_in_stream_resp(streamed_resp: str) -> str:
return streamed_resp.replace(uuid, "<REPLACED>")
def _null_run_id_and_metadata_recursively(decoded_response: Any) -> None:
"""Recursively traverse the object and delete any keys called run_id"""
if isinstance(decoded_response, dict):
for key, value in decoded_response.items():
if key in {"run_id", "__langserve_version"}:
decoded_response[key] = None
else:
_null_run_id_and_metadata_recursively(value)
elif isinstance(decoded_response, list):
for item in decoded_response:
_null_run_id_and_metadata_recursively(item)
@pytest.fixture(scope="module")
def event_loop():
"""Create an instance of the default event loop for each test case."""
@@ -231,13 +245,17 @@ async def get_async_test_client(
@asynccontextmanager
async def get_async_remote_runnable(
server: FastAPI, *, path: Optional[str] = None, raise_app_exceptions: bool = True
server: FastAPI,
*,
path: Optional[str] = None,
raise_app_exceptions: bool = True,
**kwargs: Any,
) -> RemoteRunnable:
"""Get an async client."""
url = "http://localhost:9999"
if path:
url += path
remote_runnable_client = RemoteRunnable(url=url)
remote_runnable_client = RemoteRunnable(url=url, **kwargs)
async with get_async_test_client(
server, path=path, raise_app_exceptions=raise_app_exceptions
@@ -523,6 +541,15 @@ def test_invoke(sync_remote_runnable: RemoteRunnable) -> None:
assert remote_runnable_run.child_runs[0].name == "add_one_or_passthrough"
def test_batch_tracer_with_single_input(sync_remote_runnable: RemoteRunnable) -> None:
"""Test passing a single tracer to batch."""
tracer = FakeTracer()
assert sync_remote_runnable.batch([1], config={"callbacks": [tracer]}) == [2]
assert len(tracer.runs) == 1
assert len(tracer.runs[0].child_runs) == 1
assert tracer.runs[0].child_runs[0].name == "add_one_or_passthrough"
def test_batch(sync_remote_runnable: RemoteRunnable) -> None:
"""Test sync batch."""
assert sync_remote_runnable.batch([]) == []
@@ -536,17 +563,9 @@ def test_batch(sync_remote_runnable: RemoteRunnable) -> None:
tracer = FakeTracer()
assert sync_remote_runnable.batch([1, 2], config={"callbacks": [tracer]}) == [2, 3]
assert len(tracer.runs) == 2
# Light test to verify that we're picking up information about the server side
# function being invoked via a callback.
assert tracer.runs[0].child_runs[0].name == "RunnableLambda"
assert (
tracer.runs[0].child_runs[0].extra["kwargs"]["name"] == "add_one_or_passthrough"
)
assert tracer.runs[1].child_runs[0].name == "RunnableLambda"
assert (
tracer.runs[1].child_runs[0].extra["kwargs"]["name"] == "add_one_or_passthrough"
)
assert tracer.runs[0].child_runs[0].name == "add_one_or_passthrough"
assert tracer.runs[1].child_runs[0].name == "add_one_or_passthrough"
# Verify that each tracer gets its own run
tracer1 = FakeTracer()
@@ -558,17 +577,8 @@ def test_batch(sync_remote_runnable: RemoteRunnable) -> None:
assert len(tracer2.runs) == 1
# Light test to verify that we're picking up information about the server side
# function being invoked via a callback.
assert tracer1.runs[0].child_runs[0].name == "RunnableLambda"
assert (
tracer1.runs[0].child_runs[0].extra["kwargs"]["name"]
== "add_one_or_passthrough"
)
assert tracer2.runs[0].child_runs[0].name == "RunnableLambda"
assert (
tracer2.runs[0].child_runs[0].extra["kwargs"]["name"]
== "add_one_or_passthrough"
)
assert tracer1.runs[0].child_runs[0].name == "add_one_or_passthrough"
assert tracer2.runs[0].child_runs[0].name == "add_one_or_passthrough"
async def test_ainvoke(async_remote_runnable: RemoteRunnable) -> None:
@@ -601,10 +611,7 @@ async def test_ainvoke(async_remote_runnable: RemoteRunnable) -> None:
elif sys.version_info < (3, 11):
assert len(tracer.runs) == 1, "Failed for python < 3.11"
remote_runnable = tracer.runs[0]
assert (
remote_runnable.child_runs[0].extra["kwargs"]["name"]
== "add_one_or_passthrough"
)
assert remote_runnable.name == "RemoteRunnable"
else:
raise AssertionError(f"Unsupported python version {sys.version_info}")
@@ -624,17 +631,9 @@ async def test_abatch(async_remote_runnable: RemoteRunnable) -> None:
[1, 2], config={"callbacks": [tracer]}
) == [2, 3]
assert len(tracer.runs) == 2
# Light test to verify that we're picking up information about the server side
# function being invoked via a callback.
assert tracer.runs[0].child_runs[0].name == "RunnableLambda"
assert (
tracer.runs[0].child_runs[0].extra["kwargs"]["name"] == "add_one_or_passthrough"
)
assert tracer.runs[1].child_runs[0].name == "RunnableLambda"
assert (
tracer.runs[1].child_runs[0].extra["kwargs"]["name"] == "add_one_or_passthrough"
)
assert tracer.runs[0].child_runs[0].name == "add_one_or_passthrough"
assert tracer.runs[1].child_runs[0].name == "add_one_or_passthrough"
# Verify that each tracer gets its own run
tracer1 = FakeTracer()
@@ -644,19 +643,9 @@ async def test_abatch(async_remote_runnable: RemoteRunnable) -> None:
) == [2, 3]
assert len(tracer1.runs) == 1
assert len(tracer2.runs) == 1
# Light test to verify that we're picking up information about the server side
# function being invoked via a callback.
assert tracer1.runs[0].child_runs[0].name == "RunnableLambda"
assert (
tracer1.runs[0].child_runs[0].extra["kwargs"]["name"]
== "add_one_or_passthrough"
)
assert tracer2.runs[0].child_runs[0].name == "RunnableLambda"
assert (
tracer2.runs[0].child_runs[0].extra["kwargs"]["name"]
== "add_one_or_passthrough"
)
assert tracer1.runs[0].child_runs[0].name == "add_one_or_passthrough"
assert tracer2.runs[0].child_runs[0].name == "add_one_or_passthrough"
async def test_astream(async_remote_runnable: RemoteRunnable) -> None:
@@ -1128,6 +1117,156 @@ async def test_config_keys_validation(mocker: MockerFixture) -> None:
assert config_seen["metadata"]["__langserve_endpoint"] == "invoke"
async def test_include_callback_events(mocker: MockerFixture) -> None:
"""This test should not use a RemoteRunnable.
Check if callback events are being sent back from the server.
Do so using the raw client.
"""
async def add_one(x: int) -> int:
"""Add one to simulate a valid function"""
return x + 1
server_runnable = RunnableLambda(func=add_one)
app = FastAPI()
add_routes(app, server_runnable, input_type=int, include_callback_events=True)
async with AsyncClient(
base_url="http://localhost:9999", transport=httpx.ASGITransport(app=app)
) as async_client:
response = await async_client.post("/invoke", json={"input": 1})
# Config should be ignored but default debug information
# will still be added
assert response.status_code == 200
decoded_response = response.json()
# Remove any run_id from the response recursively
_null_run_id_and_metadata_recursively(decoded_response)
assert decoded_response == {
"callback_events": [
{
"inputs": 1,
"kwargs": {"name": "add_one", "run_type": None},
"metadata": {
"__langserve_endpoint": "invoke",
"__langserve_version": None,
"__useragent": "python-httpx/0.27.2",
},
"parent_run_id": None,
"serialized": None,
"run_id": None,
"tags": [],
"type": "on_chain_start",
},
{
"kwargs": {},
"outputs": 2,
"metadata": None,
"parent_run_id": None,
"tags": [],
"run_id": None,
"type": "on_chain_end",
},
],
"metadata": {
"feedback_tokens": [],
"run_id": None,
},
"output": 2,
}
async def test_include_callback_events_batch() -> None:
"""This test should not use a RemoteRunnable.
Check if callback events are being sent back from the server.
Do so using the raw client.
"""
async def add_one(x: int) -> int:
"""Add one to simulate a valid function"""
return x + 1
server_runnable = RunnableLambda(func=add_one)
app = FastAPI()
add_routes(app, server_runnable, input_type=int, include_callback_events=True)
async with AsyncClient(
base_url="http://localhost:9999", transport=httpx.ASGITransport(app=app)
) as async_client:
response = await async_client.post("/batch", json={"inputs": [1, 2]})
# Config should be ignored but default debug information
# will still be added
assert response.status_code == 200
decoded_response = response.json()
# Remove any run_id from the response recursively
_null_run_id_and_metadata_recursively(decoded_response)
del decoded_response["metadata"]["run_ids"]
assert decoded_response == {
"callback_events": [
[
{
"inputs": 1,
"kwargs": {"name": "add_one", "run_type": None},
"metadata": {
"__langserve_endpoint": "batch",
"__langserve_version": None,
"__useragent": "python-httpx/0.27.2",
},
"parent_run_id": None,
"run_id": None,
"serialized": None,
"tags": [],
"type": "on_chain_start",
},
{
"kwargs": {},
"outputs": 2,
"parent_run_id": None,
"metadata": None,
"run_id": None,
"tags": [],
"type": "on_chain_end",
},
],
[
{
"inputs": 2,
"kwargs": {"name": "add_one", "run_type": None},
"metadata": {
"__langserve_endpoint": "batch",
"__langserve_version": None,
"__useragent": "python-httpx/0.27.2",
},
"parent_run_id": None,
"run_id": None,
"serialized": None,
"tags": [],
"type": "on_chain_start",
},
{
"kwargs": {},
"outputs": 3,
"parent_run_id": None,
"metadata": None,
"run_id": None,
"tags": [],
"type": "on_chain_end",
},
],
],
"metadata": {
"responses": [
{"feedback_tokens": [], "run_id": None},
{"feedback_tokens": [], "run_id": None},
],
},
"output": [2, 3],
}
async def test_input_validation(mocker: MockerFixture) -> None:
"""Test client side and server side exceptions."""
@@ -2146,6 +2285,49 @@ async def test_uuid_serialization() -> None:
)
async def test_custom_serialization() -> None:
"""Test updating the config based on the raw request object."""
from langserve.serialization import Serializer
class CustomObject:
def __init__(self, x: int) -> None:
self.x = x
def __eq__(self, other) -> bool:
return self.x == other.x
class CustomSerializer(Serializer):
def dumps(self, obj: Any) -> bytes:
"""Dump the given object as a JSON string."""
if isinstance(obj, CustomObject):
return orjson.dumps({"x": obj.x})
else:
return orjson.dumps(obj)
def loadd(self, obj: Any) -> Any:
"""Load the given object."""
if isinstance(obj, bytes):
obj = obj.decode("utf-8")
if obj.get("x"):
return CustomObject(x=obj["x"])
return obj
def foo(x: int) -> Any:
"""Add one to simulate a valid function."""
return CustomObject(x=5)
app = FastAPI()
server_runnable = RunnableLambda(foo)
add_routes(app, server_runnable, serializer=CustomSerializer())
async with get_async_remote_runnable(
app, raise_app_exceptions=True, serializer=CustomSerializer()
) as runnable:
result = await runnable.ainvoke(5)
assert isinstance(result, CustomObject)
assert result == CustomObject(x=5)
async def test_endpoint_configurations() -> None:
"""Test enabling/disabling endpoints."""
app = FastAPI()
@@ -2306,7 +2488,7 @@ async def test_astream_events_simple(async_remote_runnable: RemoteRunnable) -> N
# test client side error
with pytest.raises(httpx.HTTPStatusError) as cb:
# Invalid input type (expected string but got int)
async for _ in runnable.astream_events("foo", version="v1"):
async for _ in runnable.astream_events("foo", version="v2"):
pass
# Verify that this is a 422 error
@@ -2315,7 +2497,7 @@ async def test_astream_events_simple(async_remote_runnable: RemoteRunnable) -> N
with pytest.raises(httpx.HTTPStatusError) as cb:
# Invalid input type (expected string but got int)
# include names should not be a list of lists
async for _ in runnable.astream_events(1, include_names=[[]], version="v1"):
async for _ in runnable.astream_events(1, include_names=[[]], version="v2"):
pass
# Verify that this is a 422 error
@@ -2324,7 +2506,7 @@ async def test_astream_events_simple(async_remote_runnable: RemoteRunnable) -> N
# Test good requests
events = []
async for event in runnable.astream_events(1, version="v1"):
async for event in runnable.astream_events(1, version="v2"):
events.append(event)
# validate events
@@ -2337,6 +2519,7 @@ async def test_astream_events_simple(async_remote_runnable: RemoteRunnable) -> N
assert not k.startswith("__")
assert "metadata" in event
del event["metadata"]
event["parent_ids"] = []
assert events == [
{
@@ -2416,6 +2599,7 @@ def _clean_up_events(events: List[Dict[str, Any]]) -> None:
assert not k.startswith("__")
assert "metadata" in event
del event["metadata"]
event["parent_ids"] = []
async def test_astream_events_with_serialization(
@@ -2488,7 +2672,7 @@ async def test_astream_events_with_serialization(
app, raise_app_exceptions=False, path="/doc_types"
) as runnable:
# Test good requests
events = [event async for event in runnable.astream_events("foo", version="v1")]
events = [event async for event in runnable.astream_events("foo", version="v2")]
_clean_up_events(events)
assert events == [
@@ -2578,7 +2762,7 @@ async def test_astream_events_with_serialization(
app, raise_app_exceptions=False, path="/get_pets"
) as runnable:
# Test good requests
events = [event async for event in runnable.astream_events("foo", version="v1")]
events = [event async for event in runnable.astream_events("foo", version="v2")]
_clean_up_events(events)
assert events == [
{
@@ -2613,7 +2797,7 @@ async def test_astream_events_with_serialization(
) as runnable:
# Test good requests
with pytest.raises(httpx.HTTPStatusError) as cb:
async for event in runnable.astream_events("foo", version="v1"):
async for event in runnable.astream_events("foo", version="v2"):
pass
assert cb.value.response.status_code == 500
@@ -2641,7 +2825,7 @@ async def test_astream_events_with_prompt_model_parser_chain(
events = [
event
async for event in runnable.astream_events(
{"question": "hello"}, version="v1"
{"question": "hello"}, version="v2"
)
]
_clean_up_events(events)
@@ -2850,25 +3034,16 @@ async def test_astream_events_with_prompt_model_parser_chain(
]
},
"output": {
"generations": [
[
{
"generation_info": None,
"message": {
"additional_kwargs": {},
"content": "Hello World!",
"name": None,
"response_metadata": {},
"type": "AIMessageChunk",
},
"text": "Hello World!",
"type": "ChatGenerationChunk",
}
]
],
"llm_output": None,
"run": None,
"type": "LLMResult",
"additional_kwargs": {},
"content": "Hello World!",
"example": False,
"invalid_tool_calls": [],
"name": None,
"response_metadata": {},
"tool_call_chunks": [],
"tool_calls": [],
"type": "AIMessageChunk",
"usage_metadata": None,
},
},
"event": "on_chat_model_end",
+29 -1
View File
@@ -1,4 +1,4 @@
from typing import Dict, List
from typing import Any, Dict, List, Optional
from uuid import UUID
from langchain_core.tracers import BaseTracer
@@ -39,6 +39,34 @@ class FakeTracer(BaseTracer):
}
)
def _create_chain_run(
self,
serialized: Dict[str, Any],
inputs: Dict[str, Any],
run_id: UUID,
tags: Optional[List[str]] = None,
parent_run_id: Optional[UUID] = None,
metadata: Optional[Dict[str, Any]] = None,
run_type: Optional[str] = None,
name: Optional[str] = None,
**kwargs: Any,
) -> Run:
if name is None:
# can't raise an exception from here, but can get a breakpoint
# import pdb; pdb.set_trace()
pass
return super()._create_chain_run(
serialized,
inputs,
run_id,
tags,
parent_run_id,
metadata,
run_type,
name,
**kwargs,
)
def _persist_run(self, run: Run) -> None:
"""Persist a run."""
self.runs.append(self._copy_run(run))