mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-19 06:05:44 +02:00
Compare commits
30
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcc068b38d | ||
|
|
50291be532 | ||
|
|
5336a52925 | ||
|
|
ed45b88591 | ||
|
|
29ffaa0e0b | ||
|
|
45cd4e1928 | ||
|
|
480271f753 | ||
|
|
66fdf60e47 | ||
|
|
0894daf3fc | ||
|
|
850c55d630 | ||
|
|
c0d65ff409 | ||
|
|
be7b60a722 | ||
|
|
d467ec6556 | ||
|
|
b8683ab67a | ||
|
|
470b9a4b97 | ||
|
|
516175780d | ||
|
|
571780f74c | ||
|
|
d719438307 | ||
|
|
85c809a651 | ||
|
|
0441fd156f | ||
|
|
37b5d3886c | ||
|
|
b95267a3cc | ||
|
|
2e33c520a5 | ||
|
|
67b1dc602e | ||
|
|
1519b90414 | ||
|
|
0035ab9825 | ||
|
|
c42cd57a32 | ||
|
|
acc56e094a | ||
|
|
6b30d4fd8f | ||
|
|
fcc37cd06b |
@@ -1,6 +1,6 @@
|
||||
name: "\U0001F41B Bug Report"
|
||||
description: Report a bug in LangGraph. To report a security issue, please instead use the security option below. For questions, please use the GitHub Discussions.
|
||||
labels: ["02 Bug Report"]
|
||||
labels: [pending,bug]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: Documentation
|
||||
description: Report an issue related to the LangGraph documentation.
|
||||
title: "DOC: <Please write a comprehensive title after the 'DOC: ' prefix>"
|
||||
labels: [03 - Documentation]
|
||||
labels: [documentation]
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# AGENTS Instructions
|
||||
|
||||
This repository is a monorepo. Each library lives in a subdirectory under `libs/`.
|
||||
|
||||
When you modify code in any library, run the following commands in that library's directory before creating a pull request:
|
||||
|
||||
- `make format` – run code formatters
|
||||
- `make lint` – run the linter
|
||||
- `make test` – execute the test suite
|
||||
|
||||
To run a particular test file or to pass additional pytest options you can specify the `TEST` variable:
|
||||
|
||||
```
|
||||
TEST=path/to/test.py make test
|
||||
```
|
||||
|
||||
Other pytest arguments can also be supplied inside the `TEST` variable.
|
||||
|
||||
## Libraries
|
||||
|
||||
The repository contains several Python and JavaScript/TypeScript libraries.
|
||||
Below is a high-level overview:
|
||||
|
||||
- **checkpoint** – base interfaces for LangGraph checkpointers.
|
||||
- **checkpoint-postgres** – Postgres implementation of the checkpoint saver.
|
||||
- **checkpoint-sqlite** – SQLite implementation of the checkpoint saver.
|
||||
- **cli** – official command-line interface for LangGraph.
|
||||
- **langgraph** – core framework for building stateful, multi-actor agents.
|
||||
- **prebuilt** – high-level APIs for creating and running agents and tools.
|
||||
- **sdk-js** – JS/TS SDK for interacting with the LangGraph REST API.
|
||||
- **sdk-py** – Python SDK for the LangGraph Platform API.
|
||||
|
||||
### Dependency map
|
||||
|
||||
The diagram below lists downstream libraries for each production dependency as
|
||||
declared in that library's `pyproject.toml` (or `package.json`).
|
||||
|
||||
```text
|
||||
checkpoint
|
||||
├── checkpoint-postgres
|
||||
├── checkpoint-sqlite
|
||||
├── prebuilt
|
||||
└── langgraph
|
||||
|
||||
prebuilt
|
||||
└── langgraph
|
||||
|
||||
sdk-py
|
||||
├── langgraph
|
||||
└── cli
|
||||
|
||||
sdk-js (standalone)
|
||||
```
|
||||
|
||||
Changes to a library may impact all of its dependents shown above.
|
||||
@@ -12,7 +12,6 @@
|
||||
[](https://pepy.tech/project/langgraph)
|
||||
[](https://github.com/langchain-ai/langgraph/issues)
|
||||
[](https://langchain-ai.github.io/langgraph/)
|
||||
[](https://gitmcp.io/langchain-ai/langgraph)
|
||||
|
||||
Trusted by companies shaping the future of agents – including Klarna, Replit, Elastic, and more – LangGraph is a low-level orchestration framework for building, managing, and deploying long-running, stateful agents.
|
||||
|
||||
|
||||
@@ -30,18 +30,16 @@ Before deploying, review the [conceptual guide for the Self-Hosted Control Plane
|
||||
1. `LangGraphPlatform CRD`: A CRD for LangGraph Platform deployments. This contains the spec for managing an instance of a LangGraph platform deployment.
|
||||
1. `operator`: This operator handles changes to your LangGraph Platform CRDs.
|
||||
1. `host-backend`: This is the [control plane](../../concepts/langgraph_control_plane.md).
|
||||
1. Two additional images will be used by the chart.
|
||||
1. Two additional images will be used by the chart. Use the images that are specified in the latest release.
|
||||
|
||||
hostBackendImage:
|
||||
repository: "docker.io/langchain/hosted-langserve-backend"
|
||||
pullPolicy: IfNotPresent
|
||||
tag: "0.9.80"
|
||||
operatorImage:
|
||||
repository: "docker.io/langchain/langgraph-operator"
|
||||
pullPolicy: IfNotPresent
|
||||
tag: "aa9dff4"
|
||||
|
||||
1. In your `langsmith_config.yaml` file, enable the `langgraphPlatform` option. Note that you must also have a valid ingress setup:
|
||||
1. In your config file for langsmith (usually `langsmith_config.yaml`, enable the `langgraphPlatform` option. Note that you must also have a valid ingress setup:
|
||||
|
||||
config:
|
||||
langgraphPlatform:
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# Run experiments over a dataset
|
||||
|
||||
LangGraph Studio supports evaluations by allowing you to run your assistant over a pre-defined LangSmith dataset. This enables you to understand how your application performs over a variety of inputs, compare the results to reference outputs, and score the results using [evaluators](../../../agents/evals.md).
|
||||
|
||||
This guide shows you how to run an experiment end-to-end from Studio.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before running an experiment, ensure you have the following:
|
||||
|
||||
1. **A LangSmith dataset**: Your dataset should contain the inputs you want to test and optionally, reference outputs for comparison.
|
||||
|
||||
- The schema for the inputs must match the required input schema for the assistant. For more information on schemas, see [here](../../../concepts/low_level.md#schema).
|
||||
- For more on creating datasets, see [How to Manage Datasets](https://docs.smith.langchain.com/evaluation/how_to_guides/manage_datasets_in_application#set-up-your-dataset).
|
||||
|
||||
2. **(Optional) Evaluators**: You can attach evaluators (e.g., LLM-as-a-Judge, heuristics, or custom functions) to your dataset in LangSmith. These will run automatically after the graph has processed all inputs.
|
||||
|
||||
- To learn more, read about [Evaluation Concepts](https://docs.smith.langchain.com/evaluation/concepts#evaluators).
|
||||
|
||||
3. **A running application**: The experiment can be run against:
|
||||
- An application deployed on [LangGraph Platform](../../quick_start.md).
|
||||
- A locally running application started via the [langgraph-cli](../../../tutorials/langgraph-platform/local-server.md).
|
||||
|
||||
---
|
||||
|
||||
## Step-by-step guide
|
||||
|
||||
### 1. Launch the experiment
|
||||
|
||||
Click the **Run experiment** button in the top right corner of the Studio page.
|
||||
|
||||
### 2. Select your dataset
|
||||
|
||||
In the modal that appears, select the dataset (or a specific dataset split) to use for the experiment and click **Start**.
|
||||
|
||||
### 3. Monitor the progress
|
||||
|
||||
All of the inputs in the dataset will now be run against the active assistant. Monitor the experiment's progress via the badge in the top right corner.
|
||||
|
||||
You can continue to work in Studio while the experiment runs in the background. Click the arrow icon button at any time to navigate to LangSmith and view the detailed experiment results.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Run experiment" button is disabled
|
||||
|
||||
If the "Run experiment" button is disabled, check the following:
|
||||
|
||||
- **Deployed application**: If your application is deployed on LangGraph Platform, you may need to create a new revision to enable this feature.
|
||||
- **Local development server**: If you are running your application locally, make sure you have upgraded to the latest version of the `langgraph-cli` (`pip install -U langgraph-cli`). Additionally, ensure you have tracing enabled by setting the `LANGSMITH_API_KEY` in your project's `.env` file.
|
||||
|
||||
### Evaluator results are missing
|
||||
|
||||
When you run an experiment, any attached evaluators are scheduled for execution in a queue. If you don't see results immediately, it likely means they are still pending.
|
||||
@@ -3818,6 +3818,14 @@
|
||||
"title": "Filter",
|
||||
"description": "Optional dictionary of key-value pairs to filter results."
|
||||
},
|
||||
"query": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"title": "Query",
|
||||
"description": "Query string for semantic/vector search."
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"default": 10,
|
||||
|
||||
@@ -123,3 +123,12 @@ Defaults to `''`.
|
||||
Set `REDIS_CLUSTER` to `True` to enable Redis Cluster mode. When enabled, the system will connect to Redis using cluster mode. This is useful when connecting to a Redis Cluster deployment.
|
||||
|
||||
Defaults to `False`.
|
||||
|
||||
## `MOUNT_PREFIX`
|
||||
|
||||
!!! info "Only Allowed in Self-Hosted Deployments"
|
||||
The `MOUNT_PREFIX` environment variable is only allowed in Self-Hosted Deployment models, LangGraph Platform SaaS will not allow this environment variable.
|
||||
|
||||
Set `MOUNT_PREFIX` to serve the LangGraph Server under a specific path prefix. This is useful for deployments where the server is behind a reverse proxy or load balancer that requires a specific path prefix.
|
||||
|
||||
For example, if the server is to be served under `https://example.com/langgraph`, set `MOUNT_PREFIX` to `/langgraph`.
|
||||
|
||||
@@ -24,6 +24,7 @@ Key features of LangGraph Studio:
|
||||
- [Manage assistants](../cloud/how-tos/studio/manage_assistants.md)
|
||||
- [Manage threads](../cloud/how-tos/threads_studio.md)
|
||||
- [Iterate on prompts](../cloud/how-tos/iterate_graph_studio.md)
|
||||
- [Run experiments over a dataset](../cloud/how-tos/studio/run_evals.md)
|
||||
- Manage [long term memory](memory.md)
|
||||
- Debug agent state via [time travel](time-travel.md)
|
||||
|
||||
@@ -41,4 +42,4 @@ Chat mode is a simpler UI for iterating on and testing chat-specific agents. It
|
||||
|
||||
## Learn more
|
||||
|
||||
- See this guide on how to [get started](../cloud/how-tos/studio/quick_start.md) with LangGraph Studio.
|
||||
- See this guide on how to [get started](../cloud/how-tos/studio/quick_start.md) with LangGraph Studio.
|
||||
|
||||
@@ -470,9 +470,34 @@ If the checkpointer is used with asynchronous graph execution (i.e. executing th
|
||||
|
||||
### Serializer
|
||||
|
||||
When checkpointers save the graph state, they need to serialize the channel values in the state. This is done using serializer objects.
|
||||
When checkpointers save the graph state, they need to serialize the channel values in the state. This is done using serializer objects.
|
||||
`langgraph_checkpoint` defines [protocol][langgraph.checkpoint.serde.base.SerializerProtocol] for implementing serializers provides a default implementation ([JsonPlusSerializer][langgraph.checkpoint.serde.jsonplus.JsonPlusSerializer]) that handles a wide variety of types, including LangChain and LangGraph primitives, datetimes, enums and more.
|
||||
|
||||
#### Encryption
|
||||
|
||||
Checkpointers can optionally encrypt all persisted state. To enable this, pass an instance of [`EncryptedSerializer`][langgraph.checkpoint.serde.encrypted.EncryptedSerializer] to the `serde` argument of any `BaseCheckpointSaver` implementation. The easiest way to create an encrypted serializer is via [`from_pycryptodome_aes`][langgraph.checkpoint.serde.encrypted.EncryptedSerializer.from_pycryptodome_aes], which reads the AES key from the `LANGGRAPH_AES_KEY` environment variable (or accepts a `key` argument):
|
||||
|
||||
```python
|
||||
import sqlite3
|
||||
|
||||
from langgraph.checkpoint.serde.encrypted import EncryptedSerializer
|
||||
from langgraph.checkpoint.sqlite import SqliteSaver
|
||||
|
||||
serde = EncryptedSerializer.from_pycryptodome_aes() # reads LANGGRAPH_AES_KEY
|
||||
checkpointer = SqliteSaver(sqlite3.connect("checkpoint.db"), serde=serde)
|
||||
```
|
||||
|
||||
```python
|
||||
from langgraph.checkpoint.serde.encrypted import EncryptedSerializer
|
||||
from langgraph.checkpoint.postgres import PostgresSaver
|
||||
|
||||
serde = EncryptedSerializer.from_pycryptodome_aes()
|
||||
checkpointer = PostgresSaver.from_conn_string("postgresql://...", serde=serde)
|
||||
checkpointer.setup()
|
||||
```
|
||||
|
||||
When running on LangGraph Platform, encryption is automatically enabled whenever `LANGGRAPH_AES_KEY` is present, so you only need to provide the environment variable. Other encryption schemes can be used by implementing [`CipherProtocol`][langgraph.checkpoint.serde.base.CipherProtocol] and supplying it to `EncryptedSerializer`.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### Human-in-the-loop
|
||||
|
||||
@@ -59,8 +59,9 @@ The main question when adding subgraphs is how the parent graph and subgraph com
|
||||
response = model.invoke(state["subgraph_messages"])
|
||||
return {"subgraph_messages": response}
|
||||
|
||||
subgraph_builder = StateGraph(State)
|
||||
subgraph_builder.add_node(call_model)
|
||||
subgraph_builder = StateGraph(SubgraphMessagesState)
|
||||
subgraph_builder.add_node("call_model_from_subgraph", call_model)
|
||||
subgraph_builder.add_edge(START, "call_model_from_subgraph")
|
||||
...
|
||||
# highlight-next-line
|
||||
subgraph = subgraph_builder.compile()
|
||||
|
||||
@@ -1107,10 +1107,10 @@
|
||||
"source": [
|
||||
"### Use in production\n",
|
||||
"\n",
|
||||
"In production, you would want to use a checkpointer backed by a database:\n",
|
||||
"In production, you would want to use a store backed by a database:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"from langgraph.checkpoint.postgres import PostgresSaver\n",
|
||||
"from langgraph.store.postgres import PostgresStore\n",
|
||||
"\n",
|
||||
"DB_URI = \"postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable\"\n",
|
||||
"# highlight-next-line\n",
|
||||
|
||||
@@ -12,12 +12,18 @@
|
||||
options:
|
||||
members:
|
||||
- SerializerProtocol
|
||||
- CipherProtocol
|
||||
|
||||
::: langgraph.checkpoint.serde.jsonplus
|
||||
options:
|
||||
members:
|
||||
- JsonPlusSerializer
|
||||
|
||||
::: langgraph.checkpoint.serde.encrypted
|
||||
options:
|
||||
members:
|
||||
- EncryptedSerializer
|
||||
|
||||
::: langgraph.checkpoint.memory
|
||||
|
||||
::: langgraph.checkpoint.sqlite
|
||||
@@ -32,4 +38,4 @@
|
||||
::: langgraph.checkpoint.postgres.aio
|
||||
options:
|
||||
members:
|
||||
- AsyncPostgresSaver
|
||||
- AsyncPostgresSaver
|
||||
|
||||
+1
-3
@@ -580,9 +580,7 @@
|
||||
" ]\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"evaluator = prompt | ChatOpenAI(model=\"gpt-4-turbo-preview\").with_structured_output(\n",
|
||||
" RedTeamingResult, method=\"function_calling\"\n",
|
||||
")\n",
|
||||
"evaluator = prompt | ChatOpenAI(model=\"gpt-4o\").with_structured_output(RedTeamingResult)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def did_resist(run, example):\n",
|
||||
|
||||
@@ -471,7 +471,7 @@
|
||||
"\n",
|
||||
"_get_pass(\"TAVILY_API_KEY\")\n",
|
||||
"\n",
|
||||
"calculate = get_math_tool(ChatOpenAI(model=\"gpt-4-turbo-preview\"))\n",
|
||||
"calculate = get_math_tool(ChatOpenAI(model=\"gpt-4o\"))\n",
|
||||
"search = TavilySearchResults(\n",
|
||||
" max_results=1,\n",
|
||||
" description='tavily_search_results_json(query=\"the search query\") - a search engine.',\n",
|
||||
@@ -540,11 +540,11 @@
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"================================\u001b[1m System Message \u001b[0m================================\n",
|
||||
"================================\u001B[1m System Message \u001B[0m================================\n",
|
||||
"\n",
|
||||
"Given a user query, create a plan to solve it with the utmost parallelizability. Each plan should comprise an action from the following \u001b[33;1m\u001b[1;3m{num_tools}\u001b[0m types:\n",
|
||||
"\u001b[33;1m\u001b[1;3m{tool_descriptions}\u001b[0m\n",
|
||||
"\u001b[33;1m\u001b[1;3m{num_tools}\u001b[0m. join(): Collects and combines results from prior actions.\n",
|
||||
"Given a user query, create a plan to solve it with the utmost parallelizability. Each plan should comprise an action from the following \u001B[33;1m\u001B[1;3m{num_tools}\u001B[0m types:\n",
|
||||
"\u001B[33;1m\u001B[1;3m{tool_descriptions}\u001B[0m\n",
|
||||
"\u001B[33;1m\u001B[1;3m{num_tools}\u001B[0m. join(): Collects and combines results from prior actions.\n",
|
||||
"\n",
|
||||
" - An LLM agent is called upon invoking join() to either finalize the user query or wait until the plans are executed.\n",
|
||||
" - join should always be the last action in the plan, and will be called in two scenarios:\n",
|
||||
@@ -561,11 +561,11 @@
|
||||
" - Only use the provided action types. If a query cannot be addressed using these, invoke the join action for the next steps.\n",
|
||||
" - Never introduce new actions other than the ones provided.\n",
|
||||
"\n",
|
||||
"=============================\u001b[1m Messages Placeholder \u001b[0m=============================\n",
|
||||
"=============================\u001B[1m Messages Placeholder \u001B[0m=============================\n",
|
||||
"\n",
|
||||
"\u001b[33;1m\u001b[1;3m{messages}\u001b[0m\n",
|
||||
"\u001B[33;1m\u001B[1;3m{messages}\u001B[0m\n",
|
||||
"\n",
|
||||
"================================\u001b[1m System Message \u001b[0m================================\n",
|
||||
"================================\u001B[1m System Message \u001B[0m================================\n",
|
||||
"\n",
|
||||
"Remember, ONLY respond with the task list in the correct format! E.g.:\n",
|
||||
"idx. tool(arg_name=args)\n",
|
||||
@@ -1030,7 +1030,7 @@
|
||||
"joiner_prompt = hub.pull(\"wfh/llm-compiler-joiner\").partial(\n",
|
||||
" examples=\"\"\n",
|
||||
") # You can optionally add examples\n",
|
||||
"llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")\n",
|
||||
"llm = ChatOpenAI(model=\"gpt-4o\")\n",
|
||||
"\n",
|
||||
"runnable = joiner_prompt | llm.with_structured_output(\n",
|
||||
" JoinOutputs, method=\"function_calling\"\n",
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
" if not os.environ.get(var):\n",
|
||||
" os.environ[var] = getpass.getpass(f\"Please provide your {var}\")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"_set_if_undefined(\"OPENAI_API_KEY\")\n",
|
||||
"_set_if_undefined(\"TAVILY_API_KEY\")"
|
||||
]
|
||||
|
||||
@@ -135,7 +135,6 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain import hub\n",
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"\n",
|
||||
"from langgraph.prebuilt import create_react_agent\n",
|
||||
|
||||
@@ -90,7 +90,11 @@
|
||||
"id": "9ac1c2cd-81fb-40eb-8ba1-e9197800cba6",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Create Index"
|
||||
"## Create Index\n",
|
||||
"\n",
|
||||
"Set up a vector database using OpenAI Embeddings and the Chroma vector database. \n",
|
||||
"Input URLs of blog posts related to agents, prompt engineering, and large language models (LLMs). \n",
|
||||
"Generate vector indices for use in Retrieval-Augmented Generation (RAG)."
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -159,6 +163,21 @@
|
||||
"</div>"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "6cdd5ac0-fa18-4ee9-8051-062a0c56268f",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Router for Query Analysis\n",
|
||||
"\n",
|
||||
"Let’s start with Routing. First, assign the query analysis to the LLM.\n",
|
||||
"\n",
|
||||
"Create a RouteQuery data model and specify it in a structured format for the LLM. The decision for routing should be embedded in the prompt. You need to clearly define which parts of the document should be directed to RAG based on the topic.\n",
|
||||
"\n",
|
||||
"While you could automate this process by having the LLM summarize the RAG documents again, it’s more cost-effective to manually manage this when dealing with large documents, as automation could become expensive.\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
@@ -219,6 +238,18 @@
|
||||
"print(question_router.invoke({\"question\": \"What are the types of agent memory?\"}))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "cb248c94-0b0c-4d86-8565-32aa8d7424e4",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Retrieval Grader\n",
|
||||
"\n",
|
||||
"After performing retrieval, evaluate the results. Although you initially decided to use RAG based on the query, the retrieved documents might not be satisfactory. Assess whether the retrieved documents are sufficiently relevant to the query.\n",
|
||||
"\n",
|
||||
"For this, rely on the LLM to evaluate the relevance, providing a binary ‘yes’ or ‘no’ decision."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
@@ -309,6 +340,17 @@
|
||||
"print(generation)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "cb0ab54a-4a4f-45fa-b1c5-cea1bf4c59d5",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Hallucination Grader\n",
|
||||
"\n",
|
||||
"Verify if the LLM produced any hallucinations by comparing its output to the retrieved facts. \n",
|
||||
"Provide the LLM’s evaluation in a binary ‘yes’ or ‘no’ format.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
@@ -357,6 +399,16 @@
|
||||
"hallucination_grader.invoke({\"documents\": docs, \"generation\": generation})"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "4f58502a-c25f-4d80-a402-5583b0cd3e41",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Answer Grader\n",
|
||||
"\n",
|
||||
"Evaluate the answer finally."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
@@ -405,6 +457,18 @@
|
||||
"answer_grader.invoke({\"question\": question, \"generation\": generation})"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "af77946c-2646-4039-86b0-e2fde1ab7459",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Question Rewriting\n",
|
||||
"\n",
|
||||
"The original question from user was directly used in RAG. \n",
|
||||
"However, the user’s question might not be in a form suitable for RAG. \n",
|
||||
"To improve retrieval, rephrase the question to ensure it aligns better with vector similarity search."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 9,
|
||||
@@ -450,7 +514,9 @@
|
||||
"id": "d07c0b31-b919-4498-869f-9673125c2473",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Web Search Tool"
|
||||
"## Web Search Tool\n",
|
||||
"\n",
|
||||
"Use Tavily Search tool to get information from the web."
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -185,7 +185,7 @@
|
||||
"\n",
|
||||
"\n",
|
||||
"# LLM with function call\n",
|
||||
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
|
||||
"llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)\n",
|
||||
"structured_llm_grader = llm.with_structured_output(GradeDocuments)\n",
|
||||
"\n",
|
||||
"# Prompt\n",
|
||||
|
||||
+1
-19
@@ -179,6 +179,7 @@ nav:
|
||||
- cloud/how-tos/studio/manage_assistants.md
|
||||
- cloud/how-tos/threads_studio.md
|
||||
- cloud/how-tos/iterate_graph_studio.md
|
||||
- cloud/how-tos/studio/run_evals.md
|
||||
- cloud/how-tos/clone_traces_studio.md
|
||||
- cloud/how-tos/datasets_studio.md
|
||||
- LangGraph SDK: concepts/sdk.md
|
||||
@@ -381,25 +382,6 @@ extra:
|
||||
link: https://github.com/langchain-ai/langgraph
|
||||
- icon: fontawesome/brands/twitter
|
||||
link: https://twitter.com/LangChainAI
|
||||
analytics:
|
||||
provider: google
|
||||
property: G-G8X6ELZYE0
|
||||
feedback:
|
||||
title: Was this page helpful?
|
||||
ratings:
|
||||
- icon: material/emoticon-happy-outline
|
||||
name: This page was helpful
|
||||
data: 1
|
||||
note: >-
|
||||
Thanks for your feedback!
|
||||
- icon: material/emoticon-sad-outline
|
||||
name: This page could be improved
|
||||
data: 0
|
||||
note: >-
|
||||
Thanks for your feedback! Please help us improve this page by adding to the discussion below.
|
||||
shared_analytics:
|
||||
provider: google
|
||||
property: G-47WX3HKKY2
|
||||
validation:
|
||||
# https://www.mkdocs.org/user-guide/configuration/
|
||||
# We are still raising for omitted files because they determine the breadcrumbs for pages.
|
||||
|
||||
Generated
+3060
-3059
File diff suppressed because it is too large
Load Diff
@@ -184,7 +184,7 @@
|
||||
"\n",
|
||||
"\n",
|
||||
"# LLM with function call\n",
|
||||
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
|
||||
"llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)\n",
|
||||
"structured_llm_router = llm.with_structured_output(RouteQuery)\n",
|
||||
"\n",
|
||||
"# Prompt\n",
|
||||
@@ -235,7 +235,7 @@
|
||||
"\n",
|
||||
"\n",
|
||||
"# LLM with function call\n",
|
||||
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
|
||||
"llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)\n",
|
||||
"structured_llm_grader = llm.with_structured_output(GradeDocuments)\n",
|
||||
"\n",
|
||||
"# Prompt\n",
|
||||
@@ -328,7 +328,7 @@
|
||||
"\n",
|
||||
"\n",
|
||||
"# LLM with function call\n",
|
||||
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
|
||||
"llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)\n",
|
||||
"structured_llm_grader = llm.with_structured_output(GradeHallucinations)\n",
|
||||
"\n",
|
||||
"# Prompt\n",
|
||||
@@ -376,7 +376,7 @@
|
||||
"\n",
|
||||
"\n",
|
||||
"# LLM with function call\n",
|
||||
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
|
||||
"llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)\n",
|
||||
"structured_llm_grader = llm.with_structured_output(GradeAnswer)\n",
|
||||
"\n",
|
||||
"# Prompt\n",
|
||||
|
||||
@@ -200,11 +200,11 @@
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"********************Prompt[rlm/rag-prompt]********************\n",
|
||||
"================================\u001b[1m Human Message \u001b[0m=================================\n",
|
||||
"================================\u001B[1m Human Message \u001B[0m=================================\n",
|
||||
"\n",
|
||||
"You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. Use three sentences maximum and keep the answer concise.\n",
|
||||
"Question: \u001b[33;1m\u001b[1;3m{question}\u001b[0m \n",
|
||||
"Context: \u001b[33;1m\u001b[1;3m{context}\u001b[0m \n",
|
||||
"Question: \u001B[33;1m\u001B[1;3m{question}\u001B[0m \n",
|
||||
"Context: \u001B[33;1m\u001B[1;3m{context}\u001B[0m \n",
|
||||
"Answer:\n"
|
||||
]
|
||||
}
|
||||
@@ -244,7 +244,7 @@
|
||||
" binary_score: str = Field(description=\"Relevance score 'yes' or 'no'\")\n",
|
||||
"\n",
|
||||
" # LLM\n",
|
||||
" model = ChatOpenAI(temperature=0, model=\"gpt-4-0125-preview\", streaming=True)\n",
|
||||
" model = ChatOpenAI(temperature=0, model=\"gpt-4o\", streaming=True)\n",
|
||||
"\n",
|
||||
" # LLM with tool and validation\n",
|
||||
" llm_with_tool = model.with_structured_output(grade)\n",
|
||||
|
||||
@@ -171,7 +171,7 @@
|
||||
"\n",
|
||||
"\n",
|
||||
"# LLM with function call\n",
|
||||
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
|
||||
"llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)\n",
|
||||
"structured_llm_grader = llm.with_structured_output(GradeDocuments)\n",
|
||||
"\n",
|
||||
"# Prompt\n",
|
||||
|
||||
@@ -191,7 +191,7 @@
|
||||
"\n",
|
||||
"\n",
|
||||
"# LLM with function call\n",
|
||||
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
|
||||
"llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)\n",
|
||||
"structured_llm_grader = llm.with_structured_output(GradeDocuments)\n",
|
||||
"\n",
|
||||
"# Prompt\n",
|
||||
@@ -284,7 +284,7 @@
|
||||
"\n",
|
||||
"\n",
|
||||
"# LLM with function call\n",
|
||||
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
|
||||
"llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)\n",
|
||||
"structured_llm_grader = llm.with_structured_output(GradeHallucinations)\n",
|
||||
"\n",
|
||||
"# Prompt\n",
|
||||
@@ -332,7 +332,7 @@
|
||||
"\n",
|
||||
"\n",
|
||||
"# LLM with function call\n",
|
||||
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
|
||||
"llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)\n",
|
||||
"structured_llm_grader = llm.with_structured_output(GradeAnswer)\n",
|
||||
"\n",
|
||||
"# Prompt\n",
|
||||
|
||||
@@ -33,7 +33,9 @@
|
||||
"id": "a384cc48-0425-4e8f-aafc-cfb8e56025c9",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["%pip install -qU langchain-pinecone langchain-openai langchainhub langgraph"]
|
||||
"source": [
|
||||
"%pip install -qU langchain-pinecone langchain-openai langchainhub langgraph"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -51,7 +53,9 @@
|
||||
"id": "ccc3dae5-1df6-48ca-af8a-50f0e6128876",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["import os\n\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\nos.environ[\"LANGCHAIN_API_KEY\"] = \"<your-api-key>\""]
|
||||
"source": [
|
||||
"import os\n\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\nos.environ[\"LANGCHAIN_API_KEY\"] = \"<your-api-key>\""
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -59,7 +63,9 @@
|
||||
"id": "88637820",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["import os\n\nos.environ[\"LANGCHAIN_PROJECT\"] = \"pinecone-devconnect\""]
|
||||
"source": [
|
||||
"import os\n\nos.environ[\"LANGCHAIN_PROJECT\"] = \"pinecone-devconnect\""
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -77,7 +83,9 @@
|
||||
"id": "565a6d44-2c9f-4fff-b1ec-eea05df9350d",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from langchain_openai import OpenAIEmbeddings\nfrom langchain_pinecone import PineconeVectorStore\n\n# use pinecone movies database\n\n# Add to vectorDB\nvectorstore = PineconeVectorStore(\n embedding=OpenAIEmbeddings(),\n index_name=\"sample-movies\",\n text_key=\"summary\",\n)\nretriever = vectorstore.as_retriever()"]
|
||||
"source": [
|
||||
"from langchain_openai import OpenAIEmbeddings\nfrom langchain_pinecone import PineconeVectorStore\n\n# use pinecone movies database\n\n# Add to vectorDB\nvectorstore = PineconeVectorStore(\n embedding=OpenAIEmbeddings(),\n index_name=\"sample-movies\",\n text_key=\"summary\",\n)\nretriever = vectorstore.as_retriever()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -104,7 +112,9 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": ["docs = retriever.invoke(\"James Cameron\")\nfor doc in docs:\n print(\"# \" + doc.metadata[\"title\"])\n print(doc.page_content)\n print()"]
|
||||
"source": [
|
||||
"docs = retriever.invoke(\"James Cameron\")\nfor doc in docs:\n print(\"# \" + doc.metadata[\"title\"])\n print(doc.page_content)\n print()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -120,7 +130,32 @@
|
||||
"id": "1fafad21-60cc-483e-92a3-6a7edb1838e3",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["### Retrieval Grader\n\nfrom langchain import hub\nfrom langchain_core.pydantic_v1 import BaseModel, Field\nfrom langchain_openai import ChatOpenAI\n\n\n# Data model\nclass GradeDocuments(BaseModel):\n \"\"\"Binary score for relevance check on retrieved documents.\"\"\"\n\n binary_score: str = Field(\n description=\"Documents are relevant to the question, 'yes' or 'no'\"\n )\n\n\n# https://smith.langchain.com/hub/efriis/self-rag-retrieval-grader\ngrade_prompt = hub.pull(\"efriis/self-rag-retrieval-grader\")\n\n# LLM with function call\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(GradeDocuments)\n\nretrieval_grader = grade_prompt | structured_llm_grader"]
|
||||
"source": [
|
||||
"### Retrieval Grader\n",
|
||||
"\n",
|
||||
"from langchain import hub\n",
|
||||
"from langchain_core.pydantic_v1 import BaseModel, Field\n",
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Data model\n",
|
||||
"class GradeDocuments(BaseModel):\n",
|
||||
" \"\"\"Binary score for relevance check on retrieved documents.\"\"\"\n",
|
||||
"\n",
|
||||
" binary_score: str = Field(\n",
|
||||
" description=\"Documents are relevant to the question, 'yes' or 'no'\"\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# https://smith.langchain.com/hub/efriis/self-rag-retrieval-grader\n",
|
||||
"grade_prompt = hub.pull(\"efriis/self-rag-retrieval-grader\")\n",
|
||||
"\n",
|
||||
"# LLM with function call\n",
|
||||
"llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)\n",
|
||||
"structured_llm_grader = llm.with_structured_output(GradeDocuments)\n",
|
||||
"\n",
|
||||
"retrieval_grader = grade_prompt | structured_llm_grader"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -137,7 +172,9 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": ["# Test the retrieval grader\nquestion = \"movies starring jason momoa\"\ndocs = retriever.invoke(question)\ndoc_txt = docs[0].page_content\nprint(doc_txt)\nprint(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))"]
|
||||
"source": [
|
||||
"# Test the retrieval grader\nquestion = \"movies starring jason momoa\"\ndocs = retriever.invoke(question)\ndoc_txt = docs[0].page_content\nprint(doc_txt)\nprint(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -163,7 +200,9 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": ["### Generate\n\nfrom langchain import hub\nfrom langchain_core.output_parsers import StrOutputParser\n\n# Prompt\nprompt = hub.pull(\"rlm/rag-prompt\")\n\n# LLM\nllm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n\n# Chain\nrag_chain = prompt | llm | StrOutputParser()\n\n# Run\ngeneration = rag_chain.invoke({\"context\": docs, \"question\": question})\nprint(generation)"]
|
||||
"source": [
|
||||
"### Generate\n\nfrom langchain import hub\nfrom langchain_core.output_parsers import StrOutputParser\n\n# Prompt\nprompt = hub.pull(\"rlm/rag-prompt\")\n\n# LLM\nllm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n\n# Chain\nrag_chain = prompt | llm | StrOutputParser()\n\n# Run\ngeneration = rag_chain.invoke({\"context\": docs, \"question\": question})\nprint(generation)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -189,7 +228,30 @@
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": ["### Hallucination Grader\n\n\n# Data model\nclass GradeHallucinations(BaseModel):\n \"\"\"Binary score for hallucination present in generation answer.\"\"\"\n\n binary_score: str = Field(\n description=\"Answer is grounded in the facts, 'yes' or 'no'\"\n )\n\n\n# LLM with function call\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(GradeHallucinations)\n\n# https://smith.langchain.com/hub/efriis/self-rag-hallucination-grader\nhallucination_prompt = hub.pull(\"efriis/self-rag-hallucination-grader\")\n\nhallucination_grader = hallucination_prompt | structured_llm_grader\nprint(generation)\nhallucination_grader.invoke({\"documents\": docs, \"generation\": generation})"]
|
||||
"source": [
|
||||
"### Hallucination Grader\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Data model\n",
|
||||
"class GradeHallucinations(BaseModel):\n",
|
||||
" \"\"\"Binary score for hallucination present in generation answer.\"\"\"\n",
|
||||
"\n",
|
||||
" binary_score: str = Field(\n",
|
||||
" description=\"Answer is grounded in the facts, 'yes' or 'no'\"\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# LLM with function call\n",
|
||||
"llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)\n",
|
||||
"structured_llm_grader = llm.with_structured_output(GradeHallucinations)\n",
|
||||
"\n",
|
||||
"# https://smith.langchain.com/hub/efriis/self-rag-hallucination-grader\n",
|
||||
"hallucination_prompt = hub.pull(\"efriis/self-rag-hallucination-grader\")\n",
|
||||
"\n",
|
||||
"hallucination_grader = hallucination_prompt | structured_llm_grader\n",
|
||||
"print(generation)\n",
|
||||
"hallucination_grader.invoke({\"documents\": docs, \"generation\": generation})"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -216,7 +278,31 @@
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": ["### Answer Grader\n\n\n# Data model\nclass GradeAnswer(BaseModel):\n \"\"\"Binary score to assess answer addresses question.\"\"\"\n\n binary_score: str = Field(\n description=\"Answer addresses the question, 'yes' or 'no'\"\n )\n\n\n# LLM with function call\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(GradeAnswer)\n\n# Prompt\nanswer_prompt = hub.pull(\"efriis/self-rag-answer-grader\")\n\nanswer_grader = answer_prompt | structured_llm_grader\nprint(question)\nprint(generation)\nanswer_grader.invoke({\"question\": question, \"generation\": generation})"]
|
||||
"source": [
|
||||
"### Answer Grader\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Data model\n",
|
||||
"class GradeAnswer(BaseModel):\n",
|
||||
" \"\"\"Binary score to assess answer addresses question.\"\"\"\n",
|
||||
"\n",
|
||||
" binary_score: str = Field(\n",
|
||||
" description=\"Answer addresses the question, 'yes' or 'no'\"\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# LLM with function call\n",
|
||||
"llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)\n",
|
||||
"structured_llm_grader = llm.with_structured_output(GradeAnswer)\n",
|
||||
"\n",
|
||||
"# Prompt\n",
|
||||
"answer_prompt = hub.pull(\"efriis/self-rag-answer-grader\")\n",
|
||||
"\n",
|
||||
"answer_grader = answer_prompt | structured_llm_grader\n",
|
||||
"print(question)\n",
|
||||
"print(generation)\n",
|
||||
"answer_grader.invoke({\"question\": question, \"generation\": generation})"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -242,7 +328,9 @@
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": ["### Question Re-writer\n\n# LLM\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n\n# Prompt\nre_write_prompt = hub.pull(\"efriis/self-rag-question-rewriter\")\n\nquestion_rewriter = re_write_prompt | llm | StrOutputParser()\nprint(question)\nquestion_rewriter.invoke({\"question\": question})"]
|
||||
"source": [
|
||||
"### Question Re-writer\n\n# LLM\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n\n# Prompt\nre_write_prompt = hub.pull(\"efriis/self-rag-question-rewriter\")\n\nquestion_rewriter = re_write_prompt | llm | StrOutputParser()\nprint(question)\nquestion_rewriter.invoke({\"question\": question})"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -262,7 +350,9 @@
|
||||
"id": "f1617e9e-66a8-4c1a-a1fe-cc936284c085",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from typing import List\n\nfrom typing_extensions import TypedDict\n\n\nclass GraphState(TypedDict):\n \"\"\"\n Represents the state of our graph.\n\n Attributes:\n question: question\n generation: LLM generation\n documents: list of documents\n \"\"\"\n\n question: str\n generation: str\n documents: List[str]"]
|
||||
"source": [
|
||||
"from typing import List\n\nfrom typing_extensions import TypedDict\n\n\nclass GraphState(TypedDict):\n \"\"\"\n Represents the state of our graph.\n\n Attributes:\n question: question\n generation: LLM generation\n documents: list of documents\n \"\"\"\n\n question: str\n generation: str\n documents: List[str]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -270,7 +360,9 @@
|
||||
"id": "add509d8-6682-4127-8d95-13dd37d79702",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["### Nodes\n\n\ndef retrieve(state):\n \"\"\"\n Retrieve documents\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, documents, that contains retrieved documents\n \"\"\"\n print(\"---RETRIEVE---\")\n question = state[\"question\"]\n\n # Retrieval\n documents = retriever.invoke(question)\n return {\"documents\": documents, \"question\": question}\n\n\ndef generate(state):\n \"\"\"\n Generate answer\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation, that contains LLM generation\n \"\"\"\n print(\"---GENERATE---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # RAG generation\n generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n return {\"documents\": documents, \"question\": question, \"generation\": generation}\n\n\ndef grade_documents(state):\n \"\"\"\n Determines whether the retrieved documents are relevant to the question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with only filtered relevant documents\n \"\"\"\n\n print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Score each doc\n filtered_docs = []\n for d in documents:\n score = retrieval_grader.invoke(\n {\"question\": question, \"document\": d.page_content}\n )\n grade = score.binary_score\n if grade == \"yes\":\n print(\"---GRADE: DOCUMENT RELEVANT---\")\n filtered_docs.append(d)\n else:\n print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n continue\n return {\"documents\": filtered_docs, \"question\": question}\n\n\ndef transform_query(state):\n \"\"\"\n Transform the query to produce a better question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates question key with a re-phrased question\n \"\"\"\n\n print(\"---TRANSFORM QUERY---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Re-write question\n better_question = question_rewriter.invoke({\"question\": question})\n return {\"documents\": documents, \"question\": better_question}"]
|
||||
"source": [
|
||||
"### Nodes\n\n\ndef retrieve(state):\n \"\"\"\n Retrieve documents\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, documents, that contains retrieved documents\n \"\"\"\n print(\"---RETRIEVE---\")\n question = state[\"question\"]\n\n # Retrieval\n documents = retriever.invoke(question)\n return {\"documents\": documents, \"question\": question}\n\n\ndef generate(state):\n \"\"\"\n Generate answer\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation, that contains LLM generation\n \"\"\"\n print(\"---GENERATE---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # RAG generation\n generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n return {\"documents\": documents, \"question\": question, \"generation\": generation}\n\n\ndef grade_documents(state):\n \"\"\"\n Determines whether the retrieved documents are relevant to the question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with only filtered relevant documents\n \"\"\"\n\n print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Score each doc\n filtered_docs = []\n for d in documents:\n score = retrieval_grader.invoke(\n {\"question\": question, \"document\": d.page_content}\n )\n grade = score.binary_score\n if grade == \"yes\":\n print(\"---GRADE: DOCUMENT RELEVANT---\")\n filtered_docs.append(d)\n else:\n print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n continue\n return {\"documents\": filtered_docs, \"question\": question}\n\n\ndef transform_query(state):\n \"\"\"\n Transform the query to produce a better question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates question key with a re-phrased question\n \"\"\"\n\n print(\"---TRANSFORM QUERY---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Re-write question\n better_question = question_rewriter.invoke({\"question\": question})\n return {\"documents\": documents, \"question\": better_question}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -278,7 +370,9 @@
|
||||
"id": "09fc91b4",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["### Edges\n\n\ndef decide_to_generate(state):\n \"\"\"\n Determines whether to generate an answer, or re-generate a question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Binary decision for next node to call\n \"\"\"\n\n print(\"---ASSESS GRADED DOCUMENTS---\")\n state[\"question\"]\n filtered_documents = state[\"documents\"]\n\n if not filtered_documents:\n # All documents have been filtered check_relevance\n # We will re-generate a new query\n print(\n \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n )\n return \"transform_query\"\n else:\n # We have relevant documents, so generate answer\n print(\"---DECISION: GENERATE---\")\n return \"generate\"\n\n\ndef grade_generation_v_documents_and_question(state):\n \"\"\"\n Determines whether the generation is grounded in the document and answers question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Decision for next node to call\n \"\"\"\n\n print(\"---CHECK HALLUCINATIONS---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n generation = state[\"generation\"]\n\n score = hallucination_grader.invoke(\n {\"documents\": documents, \"generation\": generation}\n )\n grade = score.binary_score\n\n # Check hallucination\n if grade == \"yes\":\n print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n # Check question-answering\n print(\"---GRADE GENERATION vs QUESTION---\")\n score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n grade = score.binary_score\n if grade == \"yes\":\n print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n return \"useful\"\n else:\n print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n return \"not useful\"\n else:\n pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n return \"not supported\""]
|
||||
"source": [
|
||||
"### Edges\n\n\ndef decide_to_generate(state):\n \"\"\"\n Determines whether to generate an answer, or re-generate a question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Binary decision for next node to call\n \"\"\"\n\n print(\"---ASSESS GRADED DOCUMENTS---\")\n state[\"question\"]\n filtered_documents = state[\"documents\"]\n\n if not filtered_documents:\n # All documents have been filtered check_relevance\n # We will re-generate a new query\n print(\n \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n )\n return \"transform_query\"\n else:\n # We have relevant documents, so generate answer\n print(\"---DECISION: GENERATE---\")\n return \"generate\"\n\n\ndef grade_generation_v_documents_and_question(state):\n \"\"\"\n Determines whether the generation is grounded in the document and answers question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Decision for next node to call\n \"\"\"\n\n print(\"---CHECK HALLUCINATIONS---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n generation = state[\"generation\"]\n\n score = hallucination_grader.invoke(\n {\"documents\": documents, \"generation\": generation}\n )\n grade = score.binary_score\n\n # Check hallucination\n if grade == \"yes\":\n print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n # Check question-answering\n print(\"---GRADE GENERATION vs QUESTION---\")\n score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n grade = score.binary_score\n if grade == \"yes\":\n print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n return \"useful\"\n else:\n print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n return \"not useful\"\n else:\n pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n return \"not supported\""
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -331,7 +425,9 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": ["from pprint import pprint\n\n# Run\ninputs = {\"question\": \"Movies that star Daniel Craig\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint(f\"Node '{key}':\")\n pprint(\"\\n---\\n\")\n\n# Final generation\npprint(value[\"generation\"])"]
|
||||
"source": [
|
||||
"from pprint import pprint\n\n# Run\ninputs = {\"question\": \"Movies that star Daniel Craig\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint(f\"Node '{key}':\")\n pprint(\"\\n---\\n\")\n\n# Final generation\npprint(value[\"generation\"])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -339,7 +435,9 @@
|
||||
"id": "4138bc51-8c84-4b8a-8d24-f7f470721f6f",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["inputs = {\"question\": \"Which movies are about aliens?\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint(f\"Node '{key}':\")\n pprint(\"\\n---\\n\")\n\n# Final generation\npprint(value[\"generation\"])"]
|
||||
"source": [
|
||||
"inputs = {\"question\": \"Which movies are about aliens?\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint(f\"Node '{key}':\")\n pprint(\"\\n---\\n\")\n\n# Final generation\npprint(value[\"generation\"])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -347,7 +445,9 @@
|
||||
"id": "42369ab8-322d-434a-b5dd-2266e4cb2903",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [""]
|
||||
"source": [
|
||||
""
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
|
||||
@@ -13,6 +13,20 @@ By default `langgraph-checkpoint-postgres` installs `psycopg` (Psycopg 3) withou
|
||||
|
||||
> [!IMPORTANT]
|
||||
> When manually creating Postgres connections and passing them to `PostgresSaver` or `AsyncPostgresSaver`, make sure to include `autocommit=True` and `row_factory=dict_row` (`from psycopg.rows import dict_row`). See a full example in this [how-to guide](https://langchain-ai.github.io/langgraph/how-tos/persistence_postgres/).
|
||||
>
|
||||
> **Why these parameters are required:**
|
||||
> - `autocommit=True`: Required for the `.setup()` method to properly commit the checkpoint tables to the database. Without this, table creation may not be persisted.
|
||||
> - `row_factory=dict_row`: Required because the PostgresSaver implementation accesses database rows using dictionary-style syntax (e.g., `row["column_name"]`). The default `tuple_row` factory returns tuples that only support index-based access (e.g., `row[0]`), which will cause `TypeError` exceptions when the checkpointer tries to access columns by name.
|
||||
>
|
||||
> **Example of incorrect usage:**
|
||||
> ```python
|
||||
> # ❌ This will fail with TypeError during checkpointer operations
|
||||
> with psycopg.connect(DB_URI) as conn: # Missing autocommit=True and row_factory=dict_row
|
||||
> checkpointer = PostgresSaver(conn)
|
||||
> checkpointer.setup() # May not persist tables properly
|
||||
> # Any operation that reads from database will fail with:
|
||||
> # TypeError: tuple indices must be integers or slices, not str
|
||||
> ```
|
||||
|
||||
```python
|
||||
from langgraph.checkpoint.postgres import PostgresSaver
|
||||
|
||||
@@ -175,32 +175,7 @@ class PostgresSaver(BasePostgresSaver):
|
||||
value["channel_values"],
|
||||
)
|
||||
for value in values:
|
||||
yield CheckpointTuple(
|
||||
{
|
||||
"configurable": {
|
||||
"thread_id": value["thread_id"],
|
||||
"checkpoint_ns": value["checkpoint_ns"],
|
||||
"checkpoint_id": value["checkpoint_id"],
|
||||
}
|
||||
},
|
||||
{
|
||||
**value["checkpoint"],
|
||||
"channel_values": self._load_blobs(value["channel_values"]),
|
||||
},
|
||||
value["metadata"],
|
||||
(
|
||||
{
|
||||
"configurable": {
|
||||
"thread_id": value["thread_id"],
|
||||
"checkpoint_ns": value["checkpoint_ns"],
|
||||
"checkpoint_id": value["parent_checkpoint_id"],
|
||||
}
|
||||
}
|
||||
if value["parent_checkpoint_id"]
|
||||
else None
|
||||
),
|
||||
self._load_writes(value["pending_writes"]),
|
||||
)
|
||||
yield self._load_checkpoint_tuple(value)
|
||||
|
||||
def get_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
|
||||
"""Get a checkpoint tuple from the database.
|
||||
@@ -271,32 +246,7 @@ class PostgresSaver(BasePostgresSaver):
|
||||
value["channel_values"],
|
||||
)
|
||||
|
||||
return CheckpointTuple(
|
||||
{
|
||||
"configurable": {
|
||||
"thread_id": thread_id,
|
||||
"checkpoint_ns": checkpoint_ns,
|
||||
"checkpoint_id": value["checkpoint_id"],
|
||||
}
|
||||
},
|
||||
{
|
||||
**value["checkpoint"],
|
||||
"channel_values": self._load_blobs(value["channel_values"]),
|
||||
},
|
||||
value["metadata"],
|
||||
(
|
||||
{
|
||||
"configurable": {
|
||||
"thread_id": thread_id,
|
||||
"checkpoint_ns": checkpoint_ns,
|
||||
"checkpoint_id": value["parent_checkpoint_id"],
|
||||
}
|
||||
}
|
||||
if value["parent_checkpoint_id"]
|
||||
else None
|
||||
),
|
||||
self._load_writes(value["pending_writes"]),
|
||||
)
|
||||
return self._load_checkpoint_tuple(value)
|
||||
|
||||
def put(
|
||||
self,
|
||||
@@ -466,5 +416,44 @@ class PostgresSaver(BasePostgresSaver):
|
||||
with conn.cursor(binary=True, row_factory=dict_row) as cur:
|
||||
yield cur
|
||||
|
||||
def _load_checkpoint_tuple(self, value: DictRow) -> CheckpointTuple:
|
||||
"""
|
||||
Convert a database row into a CheckpointTuple object.
|
||||
|
||||
Args:
|
||||
value: A row from the database containing checkpoint data.
|
||||
|
||||
Returns:
|
||||
CheckpointTuple: A structured representation of the checkpoint,
|
||||
including its configuration, metadata, parent checkpoint (if any),
|
||||
and pending writes.
|
||||
"""
|
||||
return CheckpointTuple(
|
||||
{
|
||||
"configurable": {
|
||||
"thread_id": value["thread_id"],
|
||||
"checkpoint_ns": value["checkpoint_ns"],
|
||||
"checkpoint_id": value["checkpoint_id"],
|
||||
}
|
||||
},
|
||||
{
|
||||
**value["checkpoint"],
|
||||
"channel_values": self._load_blobs(value["channel_values"]),
|
||||
},
|
||||
value["metadata"],
|
||||
(
|
||||
{
|
||||
"configurable": {
|
||||
"thread_id": value["thread_id"],
|
||||
"checkpoint_ns": value["checkpoint_ns"],
|
||||
"checkpoint_id": value["parent_checkpoint_id"],
|
||||
}
|
||||
}
|
||||
if value["parent_checkpoint_id"]
|
||||
else None
|
||||
),
|
||||
self._load_writes(value["pending_writes"]),
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["PostgresSaver", "BasePostgresSaver", "Conn"]
|
||||
|
||||
@@ -162,32 +162,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
value["channel_values"],
|
||||
)
|
||||
for value in values:
|
||||
yield CheckpointTuple(
|
||||
{
|
||||
"configurable": {
|
||||
"thread_id": value["thread_id"],
|
||||
"checkpoint_ns": value["checkpoint_ns"],
|
||||
"checkpoint_id": value["checkpoint_id"],
|
||||
}
|
||||
},
|
||||
{
|
||||
**value["checkpoint"],
|
||||
"channel_values": self._load_blobs(value["channel_values"]),
|
||||
},
|
||||
value["metadata"],
|
||||
(
|
||||
{
|
||||
"configurable": {
|
||||
"thread_id": value["thread_id"],
|
||||
"checkpoint_ns": value["checkpoint_ns"],
|
||||
"checkpoint_id": value["parent_checkpoint_id"],
|
||||
}
|
||||
}
|
||||
if value["parent_checkpoint_id"]
|
||||
else None
|
||||
),
|
||||
await asyncio.to_thread(self._load_writes, value["pending_writes"]),
|
||||
)
|
||||
yield await self._load_checkpoint_tuple(value)
|
||||
|
||||
async def aget_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
|
||||
"""Get a checkpoint tuple from the database asynchronously.
|
||||
@@ -238,32 +213,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
value["channel_values"],
|
||||
)
|
||||
|
||||
return CheckpointTuple(
|
||||
{
|
||||
"configurable": {
|
||||
"thread_id": thread_id,
|
||||
"checkpoint_ns": checkpoint_ns,
|
||||
"checkpoint_id": value["checkpoint_id"],
|
||||
}
|
||||
},
|
||||
{
|
||||
**value["checkpoint"],
|
||||
"channel_values": self._load_blobs(value["channel_values"]),
|
||||
},
|
||||
value["metadata"],
|
||||
(
|
||||
{
|
||||
"configurable": {
|
||||
"thread_id": thread_id,
|
||||
"checkpoint_ns": checkpoint_ns,
|
||||
"checkpoint_id": value["parent_checkpoint_id"],
|
||||
}
|
||||
}
|
||||
if value["parent_checkpoint_id"]
|
||||
else None
|
||||
),
|
||||
await asyncio.to_thread(self._load_writes, value["pending_writes"]),
|
||||
)
|
||||
return await self._load_checkpoint_tuple(value)
|
||||
|
||||
async def aput(
|
||||
self,
|
||||
@@ -424,6 +374,45 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
async with conn.cursor(binary=True, row_factory=dict_row) as cur:
|
||||
yield cur
|
||||
|
||||
async def _load_checkpoint_tuple(self, value: DictRow) -> CheckpointTuple:
|
||||
"""
|
||||
Convert a database row into a CheckpointTuple object.
|
||||
|
||||
Args:
|
||||
value: A row from the database containing checkpoint data.
|
||||
|
||||
Returns:
|
||||
CheckpointTuple: A structured representation of the checkpoint,
|
||||
including its configuration, metadata, parent checkpoint (if any),
|
||||
and pending writes.
|
||||
"""
|
||||
return CheckpointTuple(
|
||||
{
|
||||
"configurable": {
|
||||
"thread_id": value["thread_id"],
|
||||
"checkpoint_ns": value["checkpoint_ns"],
|
||||
"checkpoint_id": value["checkpoint_id"],
|
||||
}
|
||||
},
|
||||
{
|
||||
**value["checkpoint"],
|
||||
"channel_values": self._load_blobs(value["channel_values"]),
|
||||
},
|
||||
value["metadata"],
|
||||
(
|
||||
{
|
||||
"configurable": {
|
||||
"thread_id": value["thread_id"],
|
||||
"checkpoint_ns": value["checkpoint_ns"],
|
||||
"checkpoint_id": value["parent_checkpoint_id"],
|
||||
}
|
||||
}
|
||||
if value["parent_checkpoint_id"]
|
||||
else None
|
||||
),
|
||||
await asyncio.to_thread(self._load_writes, value["pending_writes"]),
|
||||
)
|
||||
|
||||
def list(
|
||||
self,
|
||||
config: RunnableConfig | None,
|
||||
|
||||
@@ -1317,12 +1317,12 @@ def _ensure_index_config(
|
||||
index_config = index_config.copy()
|
||||
tokenized: list[tuple[str, Literal["$"] | list[str]]] = []
|
||||
tot = 0
|
||||
text_fields = index_config.get("fields") or ["$"]
|
||||
if isinstance(text_fields, str):
|
||||
text_fields = [text_fields]
|
||||
if not isinstance(text_fields, list):
|
||||
raise ValueError(f"Text fields must be a list or a string. Got {text_fields}")
|
||||
for p in text_fields:
|
||||
fields = index_config.get("fields") or ["$"]
|
||||
if isinstance(fields, str):
|
||||
fields = [fields]
|
||||
if not isinstance(fields, list):
|
||||
raise ValueError(f"Text fields must be a list or a string. Got {fields}")
|
||||
for p in fields:
|
||||
if p == "$":
|
||||
tokenized.append((p, "$"))
|
||||
tot += 1
|
||||
|
||||
Generated
+704
-703
File diff suppressed because it is too large
Load Diff
Generated
+652
-650
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@ import json
|
||||
import pathlib
|
||||
import pickle
|
||||
import re
|
||||
import sys
|
||||
from collections import deque
|
||||
from collections.abc import Sequence
|
||||
from datetime import date, datetime, time, timedelta, timezone
|
||||
@@ -251,6 +252,7 @@ EXT_CONSTRUCTOR_KW_ARGS = 2
|
||||
EXT_METHOD_SINGLE_ARG = 3
|
||||
EXT_PYDANTIC_V1 = 4
|
||||
EXT_PYDANTIC_V2 = 5
|
||||
EXT_NUMPY_ARRAY = 6
|
||||
|
||||
|
||||
def _msgpack_default(obj: Any) -> str | ormsgpack.Ext:
|
||||
@@ -320,13 +322,6 @@ def _msgpack_default(obj: Any) -> str | ormsgpack.Ext:
|
||||
(obj.__class__.__module__, obj.__class__.__name__, obj.hex),
|
||||
),
|
||||
)
|
||||
elif isinstance(obj, bytearray):
|
||||
return ormsgpack.Ext(
|
||||
EXT_CONSTRUCTOR_SINGLE_ARG,
|
||||
_msgpack_enc(
|
||||
(obj.__class__.__module__, obj.__class__.__name__, bytes(obj)),
|
||||
),
|
||||
)
|
||||
elif isinstance(obj, decimal.Decimal):
|
||||
return ormsgpack.Ext(
|
||||
EXT_CONSTRUCTOR_SINGLE_ARG,
|
||||
@@ -465,6 +460,21 @@ def _msgpack_default(obj: Any) -> str | ormsgpack.Ext:
|
||||
),
|
||||
),
|
||||
)
|
||||
elif (np_mod := sys.modules.get("numpy")) is not None and isinstance(
|
||||
obj, np_mod.ndarray
|
||||
):
|
||||
order = "F" if obj.flags.f_contiguous and not obj.flags.c_contiguous else "C"
|
||||
if obj.flags.c_contiguous:
|
||||
mv = memoryview(obj)
|
||||
try:
|
||||
meta = (obj.dtype.str, obj.shape, order, mv)
|
||||
return ormsgpack.Ext(EXT_NUMPY_ARRAY, _msgpack_enc(meta))
|
||||
finally:
|
||||
mv.release()
|
||||
else:
|
||||
buf = obj.tobytes(order="A")
|
||||
meta = (obj.dtype.str, obj.shape, order, buf)
|
||||
return ormsgpack.Ext(EXT_NUMPY_ARRAY, _msgpack_enc(meta))
|
||||
elif isinstance(obj, BaseException):
|
||||
return repr(obj)
|
||||
else:
|
||||
@@ -546,6 +556,17 @@ def _msgpack_ext_hook(code: int, data: bytes) -> Any:
|
||||
return tup[2]
|
||||
except NameError:
|
||||
return
|
||||
elif code == EXT_NUMPY_ARRAY:
|
||||
try:
|
||||
import numpy as _np
|
||||
|
||||
dtype_str, shape, order, buf = ormsgpack.unpackb(
|
||||
data, ext_hook=_msgpack_ext_hook, option=ormsgpack.OPT_NON_STR_KEYS
|
||||
)
|
||||
arr = _np.frombuffer(buf, dtype=_np.dtype(dtype_str))
|
||||
return arr.reshape(shape, order=order)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
|
||||
def _msgpack_ext_hook_to_json(code: int, data: bytes) -> Any:
|
||||
@@ -626,6 +647,19 @@ def _msgpack_ext_hook_to_json(code: int, data: bytes) -> Any:
|
||||
return tup[2]
|
||||
except Exception:
|
||||
return
|
||||
elif code == EXT_NUMPY_ARRAY:
|
||||
try:
|
||||
import numpy as _np
|
||||
|
||||
dtype_str, shape, order, buf = ormsgpack.unpackb(
|
||||
data,
|
||||
ext_hook=_msgpack_ext_hook_to_json,
|
||||
option=ormsgpack.OPT_NON_STR_KEYS,
|
||||
)
|
||||
arr = _np.frombuffer(buf, dtype=_np.dtype(dtype_str))
|
||||
return arr.reshape(shape, order=order).tolist()
|
||||
except Exception:
|
||||
return
|
||||
|
||||
|
||||
_option = (
|
||||
|
||||
@@ -496,7 +496,7 @@ def _cosine_similarity(X: list[float], Y: list[list[float]]) -> list[float]:
|
||||
if not Y:
|
||||
return []
|
||||
if _check_numpy():
|
||||
import numpy as np # type: ignore[import-not-found]
|
||||
import numpy as np
|
||||
|
||||
X_arr = np.array(X) if not isinstance(X, np.ndarray) else X
|
||||
Y_arr = np.array(Y) if not isinstance(Y, np.ndarray) else Y
|
||||
|
||||
@@ -13,7 +13,7 @@ license = "MIT"
|
||||
license-files = ['LICENSE']
|
||||
dependencies = [
|
||||
"langchain-core>=0.2.38",
|
||||
"ormsgpack>=1.8.0",
|
||||
"ormsgpack>=1.10.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
@@ -29,6 +29,7 @@ dev = [
|
||||
"pytest-watcher",
|
||||
"mypy",
|
||||
"dataclasses-json",
|
||||
"numpy",
|
||||
]
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
|
||||
@@ -11,6 +11,8 @@ from ipaddress import IPv4Address
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import dataclasses_json
|
||||
import numpy as np
|
||||
import pytest
|
||||
from pydantic import BaseModel, SecretStr
|
||||
from pydantic.v1 import BaseModel as BaseModelV1
|
||||
from pydantic.v1 import SecretStr as SecretStrV1
|
||||
@@ -295,6 +297,41 @@ def test_serde_jsonplus_bytearray() -> None:
|
||||
assert serde.loads_typed(dumped) == some_bytearray
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"arr",
|
||||
[
|
||||
np.arange(9, dtype=np.int32).reshape(3, 3),
|
||||
np.asfortranarray(np.arange(9, dtype=np.float64).reshape(3, 3)),
|
||||
np.arange(12, dtype=np.int16)[::2].reshape(3, 2),
|
||||
],
|
||||
)
|
||||
def test_serde_jsonplus_numpy_array(arr: np.ndarray) -> None:
|
||||
serde = JsonPlusSerializer()
|
||||
|
||||
dumped = serde.dumps_typed(arr)
|
||||
assert dumped[0] == "msgpack"
|
||||
result = serde.loads_typed(dumped)
|
||||
assert isinstance(result, np.ndarray)
|
||||
assert result.dtype == arr.dtype
|
||||
assert np.array_equal(result, arr)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"arr",
|
||||
[
|
||||
np.arange(6, dtype=np.float32).reshape(2, 3),
|
||||
np.asfortranarray(np.arange(4, dtype=np.complex128).reshape(2, 2)),
|
||||
],
|
||||
)
|
||||
def test_serde_jsonplus_numpy_array_json_hook(arr: np.ndarray) -> None:
|
||||
serde = JsonPlusSerializer(__unpack_ext_hook__=_msgpack_ext_hook_to_json)
|
||||
dumped = serde.dumps_typed(arr)
|
||||
assert dumped[0] == "msgpack"
|
||||
result = serde.loads_typed(dumped)
|
||||
assert isinstance(result, list)
|
||||
assert result == arr.tolist()
|
||||
|
||||
|
||||
def test_loads_cannot_find() -> None:
|
||||
serde = JsonPlusSerializer()
|
||||
|
||||
|
||||
Generated
+838
-650
File diff suppressed because it is too large
Load Diff
@@ -22,10 +22,10 @@ from langgraph.graph import END, StateGraph
|
||||
from pydantic import BaseModel, Field
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
fast_llm = ChatOpenAI(model="gpt-3.5-turbo")
|
||||
fast_llm = ChatOpenAI(model="gpt-4o-mini")
|
||||
# Uncomment for a Fireworks model
|
||||
# fast_llm = ChatFireworks(model="accounts/fireworks/models/firefunction-v1", max_tokens=32_000)
|
||||
long_context_llm = ChatOpenAI(model="gpt-4-turbo-preview")
|
||||
long_context_llm = ChatOpenAI(model="gpt-4o")
|
||||
|
||||
|
||||
direct_gen_outline_prompt = ChatPromptTemplate.from_messages(
|
||||
@@ -144,7 +144,7 @@ gen_perspectives_prompt = ChatPromptTemplate.from_messages(
|
||||
)
|
||||
|
||||
gen_perspectives_chain = gen_perspectives_prompt | ChatOpenAI(
|
||||
model="gpt-3.5-turbo"
|
||||
model="gpt-4o-mini"
|
||||
).with_structured_output(Perspectives)
|
||||
|
||||
|
||||
@@ -270,7 +270,7 @@ gen_queries_prompt = ChatPromptTemplate.from_messages(
|
||||
]
|
||||
)
|
||||
gen_queries_chain = gen_queries_prompt | ChatOpenAI(
|
||||
model="gpt-3.5-turbo"
|
||||
model="gpt-4o-mini"
|
||||
).with_structured_output(Queries, include_raw=True)
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
[](https://pepy.tech/project/langgraph)
|
||||
[](https://github.com/langchain-ai/langgraph/issues)
|
||||
[](https://langchain-ai.github.io/langgraph/)
|
||||
[](https://gitmcp.io/langchain-ai/langgraph)
|
||||
|
||||
Trusted by companies shaping the future of agents – including Klarna, Replit, Elastic, and more – LangGraph is a low-level orchestration framework for building, managing, and deploying long-running, stateful agents.
|
||||
|
||||
|
||||
@@ -499,7 +499,7 @@ class entrypoint:
|
||||
func.__name__: PregelNode(
|
||||
bound=bound,
|
||||
triggers=[START],
|
||||
channels=[START],
|
||||
channels=START,
|
||||
writers=[
|
||||
ChannelWrite(
|
||||
[
|
||||
|
||||
@@ -996,18 +996,18 @@ class CompiledStateGraph(
|
||||
self.nodes[key] = PregelNode(
|
||||
tags=[TAG_HIDDEN],
|
||||
triggers=[START],
|
||||
channels=[START],
|
||||
channels=START,
|
||||
writers=[ChannelWrite(write_entries)],
|
||||
)
|
||||
elif node is not None:
|
||||
input_schema = node.input if node else self.builder._state_schema
|
||||
input_values = {k: k for k in self.builder.schemas[input_schema]}
|
||||
is_single_input = len(input_values) == 1 and "__root__" in input_values
|
||||
input_channels = list(self.builder.schemas[input_schema])
|
||||
is_single_input = len(input_channels) == 1 and "__root__" in input_channels
|
||||
if input_schema in self.schema_to_mapper:
|
||||
mapper = self.schema_to_mapper[input_schema]
|
||||
else:
|
||||
mapper = _pick_mapper(
|
||||
list(input_values),
|
||||
input_channels,
|
||||
input_schema,
|
||||
)
|
||||
self.schema_to_mapper[input_schema] = mapper
|
||||
@@ -1021,7 +1021,7 @@ class CompiledStateGraph(
|
||||
self.nodes[key] = PregelNode(
|
||||
triggers=[branch_channel],
|
||||
# read state keys and managed values
|
||||
channels=(list(input_values) if is_single_input else input_values),
|
||||
channels=("__root__" if is_single_input else input_channels),
|
||||
# coerce state dict to schema class (eg. pydantic model)
|
||||
mapper=mapper,
|
||||
# publish to state keys
|
||||
|
||||
@@ -145,7 +145,7 @@ class NodeBuilder:
|
||||
"_cache_policy",
|
||||
)
|
||||
|
||||
_channels: list[str] | dict[str, str]
|
||||
_channels: str | list[str]
|
||||
_triggers: list[str]
|
||||
_tags: list[str]
|
||||
_metadata: dict[str, Any]
|
||||
@@ -157,7 +157,7 @@ class NodeBuilder:
|
||||
def __init__(
|
||||
self,
|
||||
) -> None:
|
||||
self._channels = {}
|
||||
self._channels = []
|
||||
self._triggers = []
|
||||
self._tags = []
|
||||
self._metadata = {}
|
||||
@@ -171,10 +171,8 @@ class NodeBuilder:
|
||||
channel: str,
|
||||
) -> Self:
|
||||
"""Subscribe to a single channel."""
|
||||
if isinstance(self._channels, list):
|
||||
self._channels.append(channel)
|
||||
elif not self._channels:
|
||||
self._channels = [channel]
|
||||
if not self._channels:
|
||||
self._channels = channel
|
||||
else:
|
||||
raise ValueError(
|
||||
"Cannot subscribe to single channels when other channels are already subscribed to"
|
||||
@@ -200,15 +198,15 @@ class NodeBuilder:
|
||||
Returns:
|
||||
Self for chaining
|
||||
"""
|
||||
if isinstance(self._channels, list):
|
||||
if isinstance(self._channels, str):
|
||||
raise ValueError(
|
||||
"Cannot subscribe to channels when subscribed to a single channel"
|
||||
)
|
||||
if read:
|
||||
if not self._channels:
|
||||
self._channels = {chan: chan for chan in channels}
|
||||
self._channels = list(channels)
|
||||
else:
|
||||
self._channels.update({chan: chan for chan in channels})
|
||||
self._channels.extend(channels)
|
||||
|
||||
if isinstance(channels, str):
|
||||
self._triggers.append(channels)
|
||||
@@ -222,11 +220,10 @@ class NodeBuilder:
|
||||
*channels: str,
|
||||
) -> Self:
|
||||
"""Adds the specified channels to read from, without subscribing to them."""
|
||||
assert self._channels, "Channels must be specified first"
|
||||
assert isinstance(self._channels, dict), (
|
||||
assert isinstance(self._channels, list), (
|
||||
"Cannot read additional channels when subscribed to single channels"
|
||||
)
|
||||
self._channels.update({c: c for c in channels})
|
||||
self._channels.extend(channels)
|
||||
return self
|
||||
|
||||
def do(
|
||||
|
||||
@@ -922,18 +922,18 @@ def _triggers(
|
||||
seen: ChannelVersions | None,
|
||||
null_version: V,
|
||||
proc: PregelNode,
|
||||
) -> Sequence[str]:
|
||||
) -> bool:
|
||||
if seen is None:
|
||||
for chan in proc.triggers:
|
||||
if channels[chan].is_available():
|
||||
return (chan,)
|
||||
return True
|
||||
else:
|
||||
for chan in proc.triggers:
|
||||
if channels[chan].is_available() and versions.get( # type: ignore[operator]
|
||||
chan, null_version
|
||||
) > seen.get(chan, null_version):
|
||||
return (chan,)
|
||||
return EMPTY_SEQ
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _scratchpad(
|
||||
@@ -1019,25 +1019,22 @@ def _proc_input(
|
||||
return copy(input_cache[proc.input_cache_key])
|
||||
# If all trigger channels subscribed by this process are not empty
|
||||
# then invoke the process with the values of all non-empty channels
|
||||
if isinstance(proc.channels, dict):
|
||||
if isinstance(proc.channels, list):
|
||||
val: dict[str, Any] = {}
|
||||
for k, chan in proc.channels.items():
|
||||
if chan in channels:
|
||||
if channels[chan].is_available():
|
||||
val[k] = channels[chan].get()
|
||||
else:
|
||||
val[k] = managed[k].get(scratchpad)
|
||||
elif isinstance(proc.channels, list):
|
||||
for chan in proc.channels:
|
||||
if chan in channels:
|
||||
if channels[chan].is_available():
|
||||
val = channels[chan].get()
|
||||
break
|
||||
val[chan] = channels[chan].get()
|
||||
else:
|
||||
val = managed[chan].get(scratchpad)
|
||||
break
|
||||
val[chan] = managed[chan].get(scratchpad)
|
||||
elif isinstance(proc.channels, str):
|
||||
if proc.channels in channels:
|
||||
if channels[proc.channels].is_available():
|
||||
val = channels[proc.channels].get()
|
||||
else:
|
||||
return MISSING
|
||||
else:
|
||||
return MISSING
|
||||
val = managed[proc.channels].get(scratchpad)
|
||||
else:
|
||||
raise RuntimeError(
|
||||
f"Invalid channels type, expected list or dict, got {proc.channels}"
|
||||
|
||||
@@ -101,11 +101,10 @@ class PregelNode(Runnable):
|
||||
itself, but instead acts as a container for the components necessary to make
|
||||
a PregelExecutableTask for a node."""
|
||||
|
||||
channels: list[str] | Mapping[str, str]
|
||||
channels: str | list[str]
|
||||
"""The channels that will be passed as input to `bound`.
|
||||
If a list, the node will be invoked with the first of that isn't empty.
|
||||
If a dict, the keys are the names of the channels, and the values are the keys
|
||||
to use in the input to `bound`."""
|
||||
If a str, the node will be invoked with its value if it isn't empty.
|
||||
If a list, the node will be invoked with a dict of those channels' values."""
|
||||
|
||||
triggers: list[str]
|
||||
"""If any of these channels is written to, this node will be triggered in
|
||||
@@ -140,7 +139,7 @@ class PregelNode(Runnable):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
channels: list[str] | Mapping[str, str],
|
||||
channels: str | list[str],
|
||||
triggers: Sequence[str],
|
||||
mapper: Callable[[Any], Any] | None = None,
|
||||
writers: list[Runnable] | None = None,
|
||||
@@ -223,25 +222,9 @@ class PregelNode(Runnable):
|
||||
This is used to avoid calculating the same input multiple times."""
|
||||
return (
|
||||
self.mapper,
|
||||
tuple(f"{key}:{value}" for key, value in self.channels.items())
|
||||
if isinstance(self.channels, dict)
|
||||
else tuple(self.channels),
|
||||
)
|
||||
|
||||
def join(self, channels: Sequence[str]) -> PregelNode:
|
||||
assert isinstance(channels, list) or isinstance(channels, tuple), (
|
||||
"channels must be a list or tuple"
|
||||
)
|
||||
assert isinstance(self.channels, dict), (
|
||||
"all channels must be named when using .join()"
|
||||
)
|
||||
return self.copy(
|
||||
update=dict(
|
||||
channels={
|
||||
**self.channels,
|
||||
**{chan: chan for chan in channels},
|
||||
}
|
||||
),
|
||||
tuple(self.channels)
|
||||
if isinstance(self.channels, list)
|
||||
else (self.channels,),
|
||||
)
|
||||
|
||||
def __or__(
|
||||
|
||||
Generated
+1587
-1586
File diff suppressed because it is too large
Load Diff
@@ -850,7 +850,7 @@ def _get_store_arg(tool: BaseTool) -> Optional[str]:
|
||||
if _is_injection(type_arg, InjectedStore)
|
||||
]
|
||||
if len(injections) > 1:
|
||||
ValueError(
|
||||
raise ValueError(
|
||||
"A tool argument should not be annotated with InjectedStore more than "
|
||||
f"once. Received arg {name} with annotations {injections}."
|
||||
)
|
||||
|
||||
Generated
+744
-743
File diff suppressed because it is too large
Load Diff
@@ -39,6 +39,75 @@ import { getEnvironmentVariable } from "./utils/env.js";
|
||||
import { mergeSignals } from "./utils/signals.js";
|
||||
import { BytesLineDecoder, SSEDecoder } from "./utils/sse.js";
|
||||
import { IterableReadableStream } from "./utils/stream.js";
|
||||
|
||||
type HeaderValue = string | undefined | null;
|
||||
|
||||
function* iterateHeaders(
|
||||
headers: HeadersInit | Record<string, HeaderValue>,
|
||||
): IterableIterator<[string, string | null]> {
|
||||
let iter: Iterable<(HeaderValue | HeaderValue | null[])[]>;
|
||||
let shouldClear = false;
|
||||
|
||||
if (headers instanceof Headers) {
|
||||
const entries: [string, string][] = [];
|
||||
headers.forEach((value, name) => {
|
||||
entries.push([name, value]);
|
||||
});
|
||||
iter = entries;
|
||||
} else if (Array.isArray(headers)) {
|
||||
iter = headers;
|
||||
} else {
|
||||
shouldClear = true;
|
||||
iter = Object.entries(headers ?? {});
|
||||
}
|
||||
|
||||
for (let item of iter) {
|
||||
const name = item[0];
|
||||
if (typeof name !== "string")
|
||||
throw new TypeError(
|
||||
`Expected header name to be a string, got ${typeof name}`,
|
||||
);
|
||||
const values = Array.isArray(item[1]) ? item[1] : [item[1]];
|
||||
let didClear = false;
|
||||
|
||||
for (const value of values) {
|
||||
if (value === undefined) continue;
|
||||
|
||||
// New object keys should always overwrite older headers
|
||||
// Yield a null to clear the header in the headers object
|
||||
// before adding the new value
|
||||
if (shouldClear && !didClear) {
|
||||
didClear = true;
|
||||
yield [name, null];
|
||||
}
|
||||
yield [name, value];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mergeHeaders(
|
||||
...headerObjects: (
|
||||
| HeadersInit
|
||||
| Record<string, HeaderValue>
|
||||
| undefined
|
||||
| null
|
||||
)[]
|
||||
) {
|
||||
const outputHeaders = new Headers();
|
||||
for (const headers of headerObjects) {
|
||||
if (!headers) continue;
|
||||
for (const [name, value] of iterateHeaders(headers)) {
|
||||
if (value === null) outputHeaders.delete(name);
|
||||
else outputHeaders.append(name, value);
|
||||
}
|
||||
}
|
||||
const headerEntries: [string, string][] = [];
|
||||
outputHeaders.forEach((value, name) => {
|
||||
headerEntries.push([name, value]);
|
||||
});
|
||||
return Object.fromEntries(headerEntries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the API key from the environment.
|
||||
* Precedence:
|
||||
@@ -96,7 +165,7 @@ export interface ClientConfig {
|
||||
apiKey?: string;
|
||||
callerOptions?: AsyncCallerParams;
|
||||
timeoutMs?: number;
|
||||
defaultHeaders?: Record<string, string | null | undefined>;
|
||||
defaultHeaders?: Record<string, HeaderValue>;
|
||||
onRequest?: RequestHook;
|
||||
}
|
||||
|
||||
@@ -107,7 +176,7 @@ class BaseClient {
|
||||
|
||||
protected apiUrl: string;
|
||||
|
||||
protected defaultHeaders: Record<string, string | null | undefined>;
|
||||
protected defaultHeaders: Record<string, HeaderValue>;
|
||||
|
||||
protected onRequest?: RequestHook;
|
||||
|
||||
@@ -147,7 +216,7 @@ class BaseClient {
|
||||
this.onRequest = config?.onRequest;
|
||||
const apiKey = getApiKey(config?.apiKey);
|
||||
if (apiKey) {
|
||||
this.defaultHeaders["X-Api-Key"] = apiKey;
|
||||
this.defaultHeaders["x-api-key"] = apiKey;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,15 +231,14 @@ class BaseClient {
|
||||
): [url: URL, init: RequestInit] {
|
||||
const mutatedOptions = {
|
||||
...options,
|
||||
headers: { ...this.defaultHeaders, ...options?.headers },
|
||||
headers: mergeHeaders(this.defaultHeaders, options?.headers),
|
||||
};
|
||||
|
||||
if (mutatedOptions.json) {
|
||||
mutatedOptions.body = JSON.stringify(mutatedOptions.json);
|
||||
mutatedOptions.headers = {
|
||||
...mutatedOptions.headers,
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
mutatedOptions.headers = mergeHeaders(mutatedOptions.headers, {
|
||||
"content-type": "application/json",
|
||||
});
|
||||
delete mutatedOptions.json;
|
||||
}
|
||||
|
||||
@@ -693,7 +761,6 @@ export class ThreadsClient<
|
||||
offset?: number;
|
||||
/**
|
||||
* Thread status to filter on.
|
||||
* Must be one of 'idle', 'busy', 'interrupted' or 'error'.
|
||||
*/
|
||||
status?: ThreadStatus;
|
||||
/**
|
||||
|
||||
@@ -74,5 +74,128 @@ describe.each([["global"], ["mocked"]])(
|
||||
expect(unexpectedFetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("header coalescing", () => {
|
||||
it("should properly merge headers with conflicting name casing", async () => {
|
||||
const client = new Client({ apiKey: "test-api-key" });
|
||||
await (client.threads as any).fetch("/test", {
|
||||
headers: { "X-Api-Key": "custom-value" },
|
||||
});
|
||||
expect(expectedFetchMock).toHaveBeenCalledWith(
|
||||
expect.any(URL),
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
"x-api-key": "custom-value",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should properly merge headers from multiple sources", async () => {
|
||||
const client = new Client({
|
||||
apiKey: "test-api-key",
|
||||
defaultHeaders: {
|
||||
"x-default": "default-value",
|
||||
"x-override": "default-value",
|
||||
},
|
||||
});
|
||||
|
||||
await (client.threads as any).fetch("/test", {
|
||||
headers: {
|
||||
"x-custom": "custom-value",
|
||||
"x-override": "custom-value",
|
||||
},
|
||||
});
|
||||
|
||||
expect(expectedFetchMock).toHaveBeenCalledWith(
|
||||
expect.any(URL),
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
"x-api-key": "test-api-key",
|
||||
"x-default": "default-value",
|
||||
"x-custom": "custom-value",
|
||||
"x-override": "custom-value",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Test with null/undefined values
|
||||
await (client.threads as any).fetch("/test", {
|
||||
headers: {
|
||||
"x-null": null,
|
||||
"x-undefined": undefined,
|
||||
"x-empty": "",
|
||||
},
|
||||
});
|
||||
|
||||
expect(expectedFetchMock).toHaveBeenCalledWith(
|
||||
expect.any(URL),
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
"x-api-key": "test-api-key",
|
||||
"x-default": "default-value",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(expectedFetchMock).not.toHaveBeenCalledWith(
|
||||
expect.any(URL),
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
"x-null": null,
|
||||
"x-undefined": undefined,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle Headers object input", async () => {
|
||||
const client = new Client({ apiKey: "test-api-key" });
|
||||
const headers = new Headers();
|
||||
headers.append("x-custom", "custom-value");
|
||||
headers.append("x-multi", "value1");
|
||||
headers.append("x-multi", "value2");
|
||||
|
||||
await (client.threads as any).fetch("/test", { headers });
|
||||
|
||||
expect(expectedFetchMock).toHaveBeenCalledWith(
|
||||
expect.any(URL),
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
"x-api-key": "test-api-key",
|
||||
"x-custom": "custom-value",
|
||||
"x-multi": "value1, value2",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle array of header tuples", async () => {
|
||||
const client = new Client({
|
||||
apiKey: "test-api-key",
|
||||
defaultHeaders: {
|
||||
"x-custom": "custom-value",
|
||||
},
|
||||
});
|
||||
const headers = [
|
||||
["x-multi", "value1"],
|
||||
["x-multi", "value2"],
|
||||
];
|
||||
|
||||
await (client.threads as any).fetch("/test", { headers });
|
||||
|
||||
expect(expectedFetchMock).toHaveBeenCalledWith(
|
||||
expect.any(URL),
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
"x-api-key": "test-api-key",
|
||||
"x-custom": "custom-value",
|
||||
"x-multi": "value1, value2",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user