mirror of
https://github.com/hwchase17/langchain.git
synced 2025-08-25 04:23:05 +00:00
Merge branch 'master' into pprados/07-zeroxpdf
This commit is contained in:
commit
c032aa7499
108
.github/workflows/_release.yml
vendored
108
.github/workflows/_release.yml
vendored
@ -100,15 +100,32 @@ jobs:
|
||||
PKG_NAME: ${{ needs.build.outputs.pkg-name }}
|
||||
VERSION: ${{ needs.build.outputs.version }}
|
||||
run: |
|
||||
PREV_TAG="$PKG_NAME==${VERSION%.*}.$(( ${VERSION##*.} - 1 ))"; [[ "${VERSION##*.}" -eq 0 ]] && PREV_TAG=""
|
||||
# Handle regular versions and pre-release versions differently
|
||||
if [[ "$VERSION" == *"-"* ]]; then
|
||||
# This is a pre-release version (contains a hyphen)
|
||||
# Extract the base version without the pre-release suffix
|
||||
BASE_VERSION=${VERSION%%-*}
|
||||
# Look for the latest release of the same base version
|
||||
REGEX="^$PKG_NAME==$BASE_VERSION\$"
|
||||
PREV_TAG=$(git tag --sort=-creatordate | (grep -P "$REGEX" || true) | head -1)
|
||||
|
||||
# If no exact base version match, look for the latest release of any kind
|
||||
if [ -z "$PREV_TAG" ]; then
|
||||
REGEX="^$PKG_NAME==\\d+\\.\\d+\\.\\d+\$"
|
||||
PREV_TAG=$(git tag --sort=-creatordate | (grep -P "$REGEX" || true) | head -1)
|
||||
fi
|
||||
else
|
||||
# Regular version handling
|
||||
PREV_TAG="$PKG_NAME==${VERSION%.*}.$(( ${VERSION##*.} - 1 ))"; [[ "${VERSION##*.}" -eq 0 ]] && PREV_TAG=""
|
||||
|
||||
# backup case if releasing e.g. 0.3.0, looks up last release
|
||||
# note if last release (chronologically) was e.g. 0.1.47 it will get
|
||||
# that instead of the last 0.2 release
|
||||
if [ -z "$PREV_TAG" ]; then
|
||||
REGEX="^$PKG_NAME==\\d+\\.\\d+\\.\\d+\$"
|
||||
echo $REGEX
|
||||
PREV_TAG=$(git tag --sort=-creatordate | (grep -P $REGEX || true) | head -1)
|
||||
# backup case if releasing e.g. 0.3.0, looks up last release
|
||||
# note if last release (chronologically) was e.g. 0.1.47 it will get
|
||||
# that instead of the last 0.2 release
|
||||
if [ -z "$PREV_TAG" ]; then
|
||||
REGEX="^$PKG_NAME==\\d+\\.\\d+\\.\\d+\$"
|
||||
echo $REGEX
|
||||
PREV_TAG=$(git tag --sort=-creatordate | (grep -P $REGEX || true) | head -1)
|
||||
fi
|
||||
fi
|
||||
|
||||
# if PREV_TAG is empty, let it be empty
|
||||
@ -312,12 +329,87 @@ jobs:
|
||||
run: make integration_tests
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
|
||||
# Test select published packages against new core
|
||||
test-prior-published-packages-against-new-core:
|
||||
needs:
|
||||
- build
|
||||
- release-notes
|
||||
- test-pypi-publish
|
||||
- pre-release-checks
|
||||
if: ${{ startsWith(inputs.working-directory, 'libs/core') }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
partner: [openai, anthropic]
|
||||
fail-fast: false # Continue testing other partners if one fails
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
AZURE_OPENAI_API_VERSION: ${{ secrets.AZURE_OPENAI_API_VERSION }}
|
||||
AZURE_OPENAI_API_BASE: ${{ secrets.AZURE_OPENAI_API_BASE }}
|
||||
AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }}
|
||||
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ secrets.AZURE_OPENAI_CHAT_DEPLOYMENT_NAME }}
|
||||
AZURE_OPENAI_LEGACY_CHAT_DEPLOYMENT_NAME: ${{ secrets.AZURE_OPENAI_LEGACY_CHAT_DEPLOYMENT_NAME }}
|
||||
AZURE_OPENAI_LLM_DEPLOYMENT_NAME: ${{ secrets.AZURE_OPENAI_LLM_DEPLOYMENT_NAME }}
|
||||
AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT_NAME: ${{ secrets.AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT_NAME }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python + uv
|
||||
uses: "./.github/actions/uv_setup"
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
path: ${{ inputs.working-directory }}/dist/
|
||||
|
||||
- name: Test against ${{ matrix.partner }}
|
||||
run: |
|
||||
# Identify latest tag
|
||||
LATEST_PACKAGE_TAG="$(
|
||||
git ls-remote --tags origin "langchain-${{ matrix.partner }}*" \
|
||||
| awk '{print $2}' \
|
||||
| sed 's|refs/tags/||' \
|
||||
| sort -Vr \
|
||||
| head -n 1
|
||||
)"
|
||||
echo "Latest package tag: $LATEST_PACKAGE_TAG"
|
||||
|
||||
# Shallow-fetch just that single tag
|
||||
git fetch --depth=1 origin tag "$LATEST_PACKAGE_TAG"
|
||||
|
||||
# Checkout the latest package files
|
||||
rm -rf $GITHUB_WORKSPACE/libs/partners/${{ matrix.partner }}/*
|
||||
cd $GITHUB_WORKSPACE/libs/partners/${{ matrix.partner }}
|
||||
git checkout "$LATEST_PACKAGE_TAG" -- .
|
||||
|
||||
# Print as a sanity check
|
||||
echo "Version number from pyproject.toml: "
|
||||
cat pyproject.toml | grep "version = "
|
||||
|
||||
# Run tests
|
||||
uv sync --group test --group test_integration
|
||||
uv pip install ../../core/dist/*.whl
|
||||
make integration_tests
|
||||
|
||||
publish:
|
||||
needs:
|
||||
- build
|
||||
- release-notes
|
||||
- test-pypi-publish
|
||||
- pre-release-checks
|
||||
- test-prior-published-packages-against-new-core
|
||||
if: >
|
||||
always() &&
|
||||
needs.build.result == 'success' &&
|
||||
needs.release-notes.result == 'success' &&
|
||||
needs.test-pypi-publish.result == 'success' &&
|
||||
needs.pre-release-checks.result == 'success' && (
|
||||
(startsWith(inputs.working-directory, 'libs/core') && needs.test-prior-published-packages-against-new-core.result == 'success')
|
||||
|| (!startsWith(inputs.working-directory, 'libs/core'))
|
||||
)
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# This permission is used for trusted publishing:
|
||||
|
176
README.md
176
README.md
@ -1,6 +1,12 @@
|
||||
# 🦜️🔗 LangChain
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: light)" srcset="docs/static/img/logo-dark.svg">
|
||||
<source media="(prefers-color-scheme: dark)" srcset="docs/static/img/logo-light.svg">
|
||||
<img alt="LangChain Logo" src="docs/static/img/logo-dark.svg" width="80%">
|
||||
</picture>
|
||||
|
||||
⚡ Build context-aware reasoning applications ⚡
|
||||
<div>
|
||||
<br>
|
||||
</div>
|
||||
|
||||
[](https://github.com/langchain-ai/langchain/releases)
|
||||
[](https://github.com/langchain-ai/langchain/actions/workflows/check_diffs.yml)
|
||||
@ -12,131 +18,65 @@
|
||||
[](https://codespaces.new/langchain-ai/langchain)
|
||||
[](https://twitter.com/langchainai)
|
||||
|
||||
Looking for the JS/TS library? Check out [LangChain.js](https://github.com/langchain-ai/langchainjs).
|
||||
> [!NOTE]
|
||||
> Looking for the JS/TS library? Check out [LangChain.js](https://github.com/langchain-ai/langchainjs).
|
||||
|
||||
To help you ship LangChain apps to production faster, check out [LangSmith](https://smith.langchain.com).
|
||||
[LangSmith](https://smith.langchain.com) is a unified developer platform for building, testing, and monitoring LLM applications.
|
||||
Fill out [this form](https://www.langchain.com/contact-sales) to speak with our sales team.
|
||||
|
||||
## Quick Install
|
||||
|
||||
With pip:
|
||||
LangChain is a framework for building LLM-powered applications. It helps you chain
|
||||
together interoperable components and third-party integrations to simplify AI
|
||||
application development — all while future-proofing decisions as the underlying
|
||||
technology evolves.
|
||||
|
||||
```bash
|
||||
pip install langchain
|
||||
pip install -U langchain
|
||||
```
|
||||
|
||||
With conda:
|
||||
To learn more about LangChain, check out
|
||||
[the docs](https://python.langchain.com/docs/introduction/). If you’re looking for more
|
||||
advanced customization or agent orchestration, check out
|
||||
[LangGraph](https://langchain-ai.github.io/langgraph/), our framework for building
|
||||
controllable agent workflows.
|
||||
|
||||
```bash
|
||||
conda install langchain -c conda-forge
|
||||
```
|
||||
## Why use LangChain?
|
||||
|
||||
## 🤔 What is LangChain?
|
||||
LangChain helps developers build applications powered by LLMs through a standard
|
||||
interface for models, embeddings, vector stores, and more.
|
||||
|
||||
**LangChain** is a framework for developing applications powered by large language models (LLMs).
|
||||
Use LangChain for:
|
||||
- **Real-time data augmentation**. Easily connect LLMs to diverse data sources and
|
||||
external / internal systems, drawing from LangChain’s vast library of integrations with
|
||||
model providers, tools, vector stores, retrievers, and more.
|
||||
- **Model interoperability**. Swap models in and out as your engineering team
|
||||
experiments to find the best choice for your application’s needs. As the industry
|
||||
frontier evolves, adapt quickly — LangChain’s abstractions keep you moving without
|
||||
losing momentum.
|
||||
|
||||
For these applications, LangChain simplifies the entire application lifecycle:
|
||||
## LangChain’s ecosystem
|
||||
While the LangChain framework can be used standalone, it also integrates seamlessly
|
||||
with any LangChain product, giving developers a full suite of tools when building LLM
|
||||
applications.
|
||||
|
||||
To improve your LLM application development, pair LangChain with:
|
||||
|
||||
- **Open-source libraries**: Build your applications using LangChain's open-source
|
||||
[components](https://python.langchain.com/docs/concepts/) and
|
||||
[third-party integrations](https://python.langchain.com/docs/integrations/providers/).
|
||||
Use [LangGraph](https://langchain-ai.github.io/langgraph/) to build stateful agents with first-class streaming and human-in-the-loop support.
|
||||
- **Productionization**: Inspect, monitor, and evaluate your apps with [LangSmith](https://docs.smith.langchain.com/) so that you can constantly optimize and deploy with confidence.
|
||||
- **Deployment**: Turn your LangGraph applications into production-ready APIs and Assistants with [LangGraph Platform](https://langchain-ai.github.io/langgraph/cloud/).
|
||||
- [LangSmith](http://www.langchain.com/langsmith) - Helpful for agent evals and
|
||||
observability. Debug poor-performing LLM app runs, evaluate agent trajectories, gain
|
||||
visibility in production, and improve performance over time.
|
||||
- [LangGraph](https://langchain-ai.github.io/langgraph/) - Build agents that can
|
||||
reliably handle complex tasks with LangGraph, our low-level agent orchestration
|
||||
framework. LangGraph offers customizable architecture, long-term memory, and
|
||||
human-in-the-loop workflows — and is trusted in production by companies like LinkedIn,
|
||||
Uber, Klarna, and GitLab.
|
||||
- [LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/#langgraph-platform) - Deploy
|
||||
and scale agents effortlessly with a purpose-built deployment platform for long
|
||||
running, stateful workflows. Discover, reuse, configure, and share agents across
|
||||
teams — and iterate quickly with visual prototyping in
|
||||
[LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio/).
|
||||
|
||||
### Open-source libraries
|
||||
|
||||
- **`langchain-core`**: Base abstractions.
|
||||
- **Integration packages** (e.g. **`langchain-openai`**, **`langchain-anthropic`**, etc.): Important integrations have been split into lightweight packages that are co-maintained by the LangChain team and the integration developers.
|
||||
- **`langchain`**: Chains, agents, and retrieval strategies that make up an application's cognitive architecture.
|
||||
- **`langchain-community`**: Third-party integrations that are community maintained.
|
||||
- **[LangGraph](https://langchain-ai.github.io/langgraph)**: LangGraph powers production-grade agents, trusted by Linkedin, Uber, Klarna, GitLab, and many more. Build robust and stateful multi-actor applications with LLMs by modeling steps as edges and nodes in a graph. Integrates smoothly with LangChain, but can be used without it. To learn more about LangGraph, check out our first LangChain Academy course, *Introduction to LangGraph*, available [here](https://academy.langchain.com/courses/intro-to-langgraph).
|
||||
|
||||
### Productionization:
|
||||
|
||||
- **[LangSmith](https://docs.smith.langchain.com/)**: A developer platform that lets you debug, test, evaluate, and monitor chains built on any LLM framework and seamlessly integrates with LangChain.
|
||||
|
||||
### Deployment:
|
||||
|
||||
- **[LangGraph Platform](https://langchain-ai.github.io/langgraph/cloud/)**: Turn your LangGraph applications into production-ready APIs and Assistants.
|
||||
|
||||

|
||||

|
||||
|
||||
## 🧱 What can you build with LangChain?
|
||||
|
||||
**❓ Question answering with RAG**
|
||||
|
||||
- [Documentation](https://python.langchain.com/docs/tutorials/rag/)
|
||||
- End-to-end Example: [Chat LangChain](https://chat.langchain.com) and [repo](https://github.com/langchain-ai/chat-langchain)
|
||||
|
||||
**🧱 Extracting structured output**
|
||||
|
||||
- [Documentation](https://python.langchain.com/docs/tutorials/extraction/)
|
||||
- End-to-end Example: [LangChain Extract](https://github.com/langchain-ai/langchain-extract/)
|
||||
|
||||
**🤖 Chatbots**
|
||||
|
||||
- [Documentation](https://python.langchain.com/docs/tutorials/chatbot/)
|
||||
- End-to-end Example: [Web LangChain (web researcher chatbot)](https://weblangchain.vercel.app) and [repo](https://github.com/langchain-ai/weblangchain)
|
||||
|
||||
And much more! Head to the [Tutorials](https://python.langchain.com/docs/tutorials/) section of the docs for more.
|
||||
|
||||
## 🚀 How does LangChain help?
|
||||
|
||||
The main value props of the LangChain libraries are:
|
||||
|
||||
1. **Components**: composable building blocks, tools and integrations for working with language models. Components are modular and easy-to-use, whether you are using the rest of the LangChain framework or not.
|
||||
2. **Easy orchestration with LangGraph**: [LangGraph](https://langchain-ai.github.io/langgraph/),
|
||||
built on top of `langchain-core`, has built-in support for [messages](https://python.langchain.com/docs/concepts/messages/), [tools](https://python.langchain.com/docs/concepts/tools/),
|
||||
and other LangChain abstractions. This makes it easy to combine components into
|
||||
production-ready applications with persistence, streaming, and other key features.
|
||||
Check out the LangChain [tutorials page](https://python.langchain.com/docs/tutorials/#orchestration) for examples.
|
||||
|
||||
## Components
|
||||
|
||||
Components fall into the following **modules**:
|
||||
|
||||
**📃 Model I/O**
|
||||
|
||||
This includes [prompt management](https://python.langchain.com/docs/concepts/prompt_templates/)
|
||||
and a generic interface for [chat models](https://python.langchain.com/docs/concepts/chat_models/), including a consistent interface for [tool-calling](https://python.langchain.com/docs/concepts/tool_calling/) and [structured output](https://python.langchain.com/docs/concepts/structured_outputs/) across model providers.
|
||||
|
||||
**📚 Retrieval**
|
||||
|
||||
Retrieval Augmented Generation involves [loading data](https://python.langchain.com/docs/concepts/document_loaders/) from a variety of sources, [preparing it](https://python.langchain.com/docs/concepts/text_splitters/), then [searching over (a.k.a. retrieving from)](https://python.langchain.com/docs/concepts/retrievers/) it for use in the generation step.
|
||||
|
||||
**🤖 Agents**
|
||||
|
||||
Agents allow an LLM autonomy over how a task is accomplished. Agents make decisions about which Actions to take, then take that Action, observe the result, and repeat until the task is complete. [LangGraph](https://langchain-ai.github.io/langgraph/) makes it easy to use
|
||||
LangChain components to build both [custom](https://langchain-ai.github.io/langgraph/tutorials/)
|
||||
and [built-in](https://langchain-ai.github.io/langgraph/how-tos/create-react-agent/)
|
||||
LLM agents.
|
||||
|
||||
## 📖 Documentation
|
||||
|
||||
Please see [here](https://python.langchain.com) for full documentation, which includes:
|
||||
|
||||
- [Introduction](https://python.langchain.com/docs/introduction/): Overview of the framework and the structure of the docs.
|
||||
- [Tutorials](https://python.langchain.com/docs/tutorials/): If you're looking to build something specific or are more of a hands-on learner, check out our tutorials. This is the best place to get started.
|
||||
- [How-to guides](https://python.langchain.com/docs/how_to/): Answers to “How do I….?” type questions. These guides are goal-oriented and concrete; they're meant to help you complete a specific task.
|
||||
- [Conceptual guide](https://python.langchain.com/docs/concepts/): Conceptual explanations of the key parts of the framework.
|
||||
- [API Reference](https://python.langchain.com/api_reference/): Thorough documentation of every class and method.
|
||||
|
||||
## 🌐 Ecosystem
|
||||
|
||||
- [🦜🛠️ LangSmith](https://docs.smith.langchain.com/): Trace and evaluate your language model applications and intelligent agents to help you move from prototype to production.
|
||||
- [🦜🕸️ LangGraph](https://langchain-ai.github.io/langgraph/): Create stateful, multi-actor applications with LLMs. Integrates smoothly with LangChain, but can be used without it.
|
||||
- [🦜🕸️ LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/#langgraph-platform): Deploy LLM applications built with LangGraph into production.
|
||||
|
||||
## 💁 Contributing
|
||||
|
||||
As an open-source project in a rapidly developing field, we are extremely open to contributions, whether it be in the form of a new feature, improved infrastructure, or better documentation.
|
||||
|
||||
For detailed information on how to contribute, see [here](https://python.langchain.com/docs/contributing/).
|
||||
|
||||
## 🌟 Contributors
|
||||
|
||||
[](https://github.com/langchain-ai/langchain/graphs/contributors)
|
||||
## Additional resources
|
||||
- [Tutorials](https://python.langchain.com/docs/tutorials/): Simple walkthroughs with
|
||||
guided examples on getting started with LangChain.
|
||||
- [How-to Guides](https://python.langchain.com/docs/how_to/): Quick, actionable code
|
||||
snippets for topics such as tool calling, RAG use cases, and more.
|
||||
- [Conceptual Guides](https://python.langchain.com/docs/concepts/): Explanations of key
|
||||
concepts behind the LangChain framework.
|
||||
- [API Reference](https://python.langchain.com/api_reference/): Detailed reference on
|
||||
navigating base packages and integrations for LangChain.
|
||||
|
@ -30,7 +30,7 @@ At a high-level, the basic ways to generate examples are:
|
||||
- User feedback: users (or labelers) leave feedback on interactions with the application and examples are generated based on that feedback (for example, all interactions with positive feedback could be turned into examples).
|
||||
- LLM feedback: same as user feedback but the process is automated by having models evaluate themselves.
|
||||
|
||||
Which approach is best depends on your task. For tasks where a small number core principles need to be understood really well, it can be valuable hand-craft a few really good examples.
|
||||
Which approach is best depends on your task. For tasks where a small number of core principles need to be understood really well, it can be valuable hand-craft a few really good examples.
|
||||
For tasks where the space of correct behaviors is broader and more nuanced, it can be useful to generate many examples in a more automated fashion so that there's a higher likelihood of there being some highly relevant examples for any runtime input.
|
||||
|
||||
**Single-turn v.s. multi-turn examples**
|
||||
@ -39,8 +39,8 @@ Another dimension to think about when generating examples is what the example is
|
||||
|
||||
The simplest types of examples just have a user input and an expected model output. These are single-turn examples.
|
||||
|
||||
One more complex type if example is where the example is an entire conversation, usually in which a model initially responds incorrectly and a user then tells the model how to correct its answer.
|
||||
This is called a multi-turn example. Multi-turn examples can be useful for more nuanced tasks where its useful to show common errors and spell out exactly why they're wrong and what should be done instead.
|
||||
One more complex type of example is where the example is an entire conversation, usually in which a model initially responds incorrectly and a user then tells the model how to correct its answer.
|
||||
This is called a multi-turn example. Multi-turn examples can be useful for more nuanced tasks where it's useful to show common errors and spell out exactly why they're wrong and what should be done instead.
|
||||
|
||||
## 2. Number of examples
|
||||
|
||||
@ -77,7 +77,7 @@ If we insert our examples as messages, where each example is represented as a se
|
||||
One area where formatting examples as messages can be tricky is when our example outputs have tool calls. This is because different models have different constraints on what types of message sequences are allowed when any tool calls are generated.
|
||||
- Some models require that any AIMessage with tool calls be immediately followed by ToolMessages for every tool call,
|
||||
- Some models additionally require that any ToolMessages be immediately followed by an AIMessage before the next HumanMessage,
|
||||
- Some models require that tools are passed in to the model if there are any tool calls / ToolMessages in the chat history.
|
||||
- Some models require that tools are passed into the model if there are any tool calls / ToolMessages in the chat history.
|
||||
|
||||
These requirements are model-specific and should be checked for the model you are using. If your model requires ToolMessages after tool calls and/or AIMessages after ToolMessages and your examples only include expected tool calls and not the actual tool outputs, you can try adding dummy ToolMessages / AIMessages to the end of each example with generic contents to satisfy the API constraints.
|
||||
In these cases it's especially worth experimenting with inserting your examples as strings versus messages, as having dummy messages can adversely affect certain models.
|
||||
|
@ -91,7 +91,7 @@ For more information, please see:
|
||||
|
||||
#### Usage with LCEL
|
||||
|
||||
If you compose multiple Runnables using [LangChain’s Expression Language (LCEL)](/docs/concepts/lcel), the `stream()` and `astream()` methods will, by convention, stream the output of the last step in the chain. This allows the final processed result to be streamed incrementally. **LCEL** tries to optimize streaming latency in pipelines such that the streaming results from the last step are available as soon as possible.
|
||||
If you compose multiple Runnables using [LangChain’s Expression Language (LCEL)](/docs/concepts/lcel), the `stream()` and `astream()` methods will, by convention, stream the output of the last step in the chain. This allows the final processed result to be streamed incrementally. **LCEL** tries to optimize streaming latency in pipelines so that the streaming results from the last step are available as soon as possible.
|
||||
|
||||
|
||||
|
||||
@ -104,7 +104,7 @@ Use the `astream_events` API to access custom data and intermediate outputs from
|
||||
While this API is available for use with [LangGraph](/docs/concepts/architecture#langgraph) as well, it is usually not necessary when working with LangGraph, as the `stream` and `astream` methods provide comprehensive streaming capabilities for LangGraph graphs.
|
||||
:::
|
||||
|
||||
For chains constructed using **LCEL**, the `.stream()` method only streams the output of the final step from te chain. This might be sufficient for some applications, but as you build more complex chains of several LLM calls together, you may want to use the intermediate values of the chain alongside the final output. For example, you may want to return sources alongside the final generation when building a chat-over-documents app.
|
||||
For chains constructed using **LCEL**, the `.stream()` method only streams the output of the final step from the chain. This might be sufficient for some applications, but as you build more complex chains of several LLM calls together, you may want to use the intermediate values of the chain alongside the final output. For example, you may want to return sources alongside the final generation when building a chat-over-documents app.
|
||||
|
||||
There are ways to do this [using callbacks](/docs/concepts/callbacks), or by constructing your chain in such a way that it passes intermediate
|
||||
values to the end with something like chained [`.assign()`](/docs/how_to/passthrough/) calls, but LangChain also includes an
|
||||
|
@ -38,6 +38,12 @@
|
||||
"| :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: |\n",
|
||||
"| ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | ❌ | \n",
|
||||
"\n",
|
||||
":::note\n",
|
||||
"\n",
|
||||
"DeepSeek-R1, specified via `model=\"deepseek-reasoner\"`, does not support tool calling or structured output. Those features [are supported](https://api-docs.deepseek.com/guides/function_calling) by DeepSeek-V3 (specified via `model=\"deepseek-chat\"`).\n",
|
||||
"\n",
|
||||
":::\n",
|
||||
"\n",
|
||||
"## Setup\n",
|
||||
"\n",
|
||||
"To access DeepSeek models you'll need to create a/an DeepSeek account, get an API key, and install the `langchain-deepseek` integration package.\n",
|
||||
|
@ -322,7 +322,7 @@
|
||||
"source": [
|
||||
"### ``strict=True``\n",
|
||||
"\n",
|
||||
":::info Requires ``langchain-openai>=0.1.21rc1``\n",
|
||||
":::info Requires ``langchain-openai>=0.1.21``\n",
|
||||
"\n",
|
||||
":::\n",
|
||||
"\n",
|
||||
@ -397,6 +397,405 @@
|
||||
"For more on binding tools and tool call outputs, head to the [tool calling](/docs/how_to/function_calling) docs."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "84833dd0-17e9-4269-82ed-550639d65751",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Responses API\n",
|
||||
"\n",
|
||||
":::info Requires ``langchain-openai>=0.3.9-rc.1``\n",
|
||||
"\n",
|
||||
":::\n",
|
||||
"\n",
|
||||
"OpenAI supports a [Responses](https://platform.openai.com/docs/guides/responses-vs-chat-completions) API that is oriented toward building [agentic](/docs/concepts/agents/) applications. It includes a suite of [built-in tools](https://platform.openai.com/docs/guides/tools?api-mode=responses), including web and file search. It also supports management of [conversation state](https://platform.openai.com/docs/guides/conversation-state?api-mode=responses), allowing you to continue a conversational thread without explicitly passing in previous messages.\n",
|
||||
"\n",
|
||||
"`ChatOpenAI` will route to the Responses API if one of these features is used. You can also specify `use_responses_api=True` when instantiating `ChatOpenAI`.\n",
|
||||
"\n",
|
||||
"### Built-in tools\n",
|
||||
"\n",
|
||||
"Equipping `ChatOpenAI` with built-in tools will ground its responses with outside information, such as via context in files or the web. The [AIMessage](/docs/concepts/messages/#aimessage) generated from the model will include information about the built-in tool invocation.\n",
|
||||
"\n",
|
||||
"#### Web search\n",
|
||||
"\n",
|
||||
"To trigger a web search, pass `{\"type\": \"web_search_preview\"}` to the model as you would another tool.\n",
|
||||
"\n",
|
||||
":::tip\n",
|
||||
"\n",
|
||||
"You can also pass built-in tools as invocation params:\n",
|
||||
"```python\n",
|
||||
"llm.invoke(\"...\", tools=[{\"type\": \"web_search_preview\"}])\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
":::"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "0d8bfe89-948b-42d4-beac-85ef2a72491d",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"\n",
|
||||
"llm = ChatOpenAI(model=\"gpt-4o-mini\")\n",
|
||||
"\n",
|
||||
"tool = {\"type\": \"web_search_preview\"}\n",
|
||||
"llm_with_tools = llm.bind_tools([tool])\n",
|
||||
"\n",
|
||||
"response = llm_with_tools.invoke(\"What was a positive news story from today?\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "c9fe67c6-38ff-40a5-93b3-a4b7fca76372",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Note that the response includes structured [content blocks](/docs/concepts/messages/#content-1) that include both the text of the response and OpenAI [annotations](https://platform.openai.com/docs/guides/tools-web-search?api-mode=responses#output-and-citations) citing its sources:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"id": "3ea5a4b1-f57a-4c8a-97f4-60ab8330a804",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[{'type': 'text',\n",
|
||||
" 'text': 'Today, a heartwarming story emerged from Minnesota, where a group of high school robotics students built a custom motorized wheelchair for a 2-year-old boy named Cillian Jackson. Born with a genetic condition that limited his mobility, Cillian\\'s family couldn\\'t afford the $20,000 wheelchair he needed. The students at Farmington High School\\'s Rogue Robotics team took it upon themselves to modify a Power Wheels toy car into a functional motorized wheelchair for Cillian, complete with a joystick, safety bumpers, and a harness. One team member remarked, \"I think we won here more than we do in our competitions. Instead of completing a task, we\\'re helping change someone\\'s life.\" ([boredpanda.com](https://www.boredpanda.com/wholesome-global-positive-news/?utm_source=openai))\\n\\nThis act of kindness highlights the profound impact that community support and innovation can have on individuals facing challenges. ',\n",
|
||||
" 'annotations': [{'end_index': 778,\n",
|
||||
" 'start_index': 682,\n",
|
||||
" 'title': '“Global Positive News”: 40 Posts To Remind Us There’s Good In The World',\n",
|
||||
" 'type': 'url_citation',\n",
|
||||
" 'url': 'https://www.boredpanda.com/wholesome-global-positive-news/?utm_source=openai'}]}]"
|
||||
]
|
||||
},
|
||||
"execution_count": 7,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"response.content"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "95fbc34c-2f12-4d51-92c5-bf62a2f8900c",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
":::tip\n",
|
||||
"\n",
|
||||
"You can recover just the text content of the response as a string by using `response.text()`. For example, to stream response text:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"for token in llm_with_tools.stream(\"...\"):\n",
|
||||
" print(token.text(), end=\"|\")\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"See the [streaming guide](/docs/how_to/chat_streaming/) for more detail.\n",
|
||||
"\n",
|
||||
":::"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "2a332940-d409-41ee-ac36-2e9bee900e83",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"The output message will also contain information from any tool invocations:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 14,
|
||||
"id": "a8011049-6c90-4fcb-82d4-850c72b46941",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'tool_outputs': [{'id': 'ws_67d192aeb6cc81918e736ad4a57937570d6f8507990d9d71',\n",
|
||||
" 'status': 'completed',\n",
|
||||
" 'type': 'web_search_call'}]}"
|
||||
]
|
||||
},
|
||||
"execution_count": 14,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"response.additional_kwargs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "288d47bb-3ccb-412f-a3d3-9f6cee0e6214",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"#### File search\n",
|
||||
"\n",
|
||||
"To trigger a file search, pass a [file search tool](https://platform.openai.com/docs/guides/tools-file-search) to the model as you would another tool. You will need to populate an OpenAI-managed vector store and include the vector store ID in the tool definition. See [OpenAI documentation](https://platform.openai.com/docs/guides/tools-file-search) for more detail."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 24,
|
||||
"id": "1f758726-33ef-4c04-8a54-49adb783bbb3",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Deep Research by OpenAI is a new capability integrated into ChatGPT that allows for the execution of multi-step research tasks independently. It can synthesize extensive amounts of online information and produce comprehensive reports similar to what a research analyst would do, significantly speeding up processes that would typically take hours for a human.\n",
|
||||
"\n",
|
||||
"### Key Features:\n",
|
||||
"- **Independent Research**: Users simply provide a prompt, and the model can find, analyze, and synthesize information from hundreds of online sources.\n",
|
||||
"- **Multi-Modal Capabilities**: The model is also able to browse user-uploaded files, plot graphs using Python, and embed visualizations in its outputs.\n",
|
||||
"- **Training**: Deep Research has been trained using reinforcement learning on real-world tasks that require extensive browsing and reasoning.\n",
|
||||
"\n",
|
||||
"### Applications:\n",
|
||||
"- Useful for professionals in sectors like finance, science, policy, and engineering, enabling them to obtain accurate and thorough research quickly.\n",
|
||||
"- It can also be beneficial for consumers seeking personalized recommendations on complex purchases.\n",
|
||||
"\n",
|
||||
"### Limitations:\n",
|
||||
"Although Deep Research presents significant advancements, it has some limitations, such as the potential to hallucinate facts or struggle with authoritative information. \n",
|
||||
"\n",
|
||||
"Deep Research aims to facilitate access to thorough and documented information, marking a significant step toward the broader goal of developing artificial general intelligence (AGI).\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"llm = ChatOpenAI(model=\"gpt-4o-mini\")\n",
|
||||
"\n",
|
||||
"openai_vector_store_ids = [\n",
|
||||
" \"vs_...\", # your IDs here\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"tool = {\n",
|
||||
" \"type\": \"file_search\",\n",
|
||||
" \"vector_store_ids\": openai_vector_store_ids,\n",
|
||||
"}\n",
|
||||
"llm_with_tools = llm.bind_tools([tool])\n",
|
||||
"\n",
|
||||
"response = llm_with_tools.invoke(\"What is deep research by OpenAI?\")\n",
|
||||
"print(response.text())"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "f88bbd71-83b0-45a6-9141-46ec9da93df6",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"As with [web search](#web-search), the response will include content blocks with citations:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 22,
|
||||
"id": "865bc14e-1599-438e-be44-857891004979",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[{'file_id': 'file-3UzgX7jcC8Dt9ZAFzywg5k',\n",
|
||||
" 'index': 346,\n",
|
||||
" 'type': 'file_citation',\n",
|
||||
" 'filename': 'deep_research_blog.pdf'},\n",
|
||||
" {'file_id': 'file-3UzgX7jcC8Dt9ZAFzywg5k',\n",
|
||||
" 'index': 575,\n",
|
||||
" 'type': 'file_citation',\n",
|
||||
" 'filename': 'deep_research_blog.pdf'}]"
|
||||
]
|
||||
},
|
||||
"execution_count": 22,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"response.content[0][\"annotations\"][:2]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "dd00f6be-2862-4634-a0c3-14ee39915c90",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"It will also include information from the built-in tool invocations:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 20,
|
||||
"id": "e16a7110-d2d8-45fa-b372-5109f330540b",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'tool_outputs': [{'id': 'fs_67d196fbb83c8191ba20586175331687089228ce932eceb1',\n",
|
||||
" 'queries': ['What is deep research by OpenAI?'],\n",
|
||||
" 'status': 'completed',\n",
|
||||
" 'type': 'file_search_call'}]}"
|
||||
]
|
||||
},
|
||||
"execution_count": 20,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"response.additional_kwargs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "6fda05f0-4b81-4709-9407-f316d760ad50",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Managing conversation state\n",
|
||||
"\n",
|
||||
"The Responses API supports management of [conversation state](https://platform.openai.com/docs/guides/conversation-state?api-mode=responses).\n",
|
||||
"\n",
|
||||
"#### Manually manage state\n",
|
||||
"\n",
|
||||
"You can manage the state manually or using [LangGraph](/docs/tutorials/chatbot/), as with other chat models:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"id": "51d3e4d3-ea78-426c-9205-aecb0937fca7",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"As of March 12, 2025, here are some positive news stories that highlight recent uplifting events:\n",
|
||||
"\n",
|
||||
"*... exemplify positive developments in health, environmental sustainability, and community well-being. \n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"\n",
|
||||
"llm = ChatOpenAI(model=\"gpt-4o-mini\")\n",
|
||||
"\n",
|
||||
"tool = {\"type\": \"web_search_preview\"}\n",
|
||||
"llm_with_tools = llm.bind_tools([tool])\n",
|
||||
"\n",
|
||||
"first_query = \"What was a positive news story from today?\"\n",
|
||||
"messages = [{\"role\": \"user\", \"content\": first_query}]\n",
|
||||
"\n",
|
||||
"response = llm_with_tools.invoke(messages)\n",
|
||||
"response_text = response.text()\n",
|
||||
"print(f\"{response_text[:100]}... {response_text[-100:]}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"id": "5da9d20f-9712-46f4-a395-5be5a7c1bc62",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Your question was: \"What was a positive news story from today?\"\n",
|
||||
"\n",
|
||||
"The last sentence of my answer was: \"These stories exemplify positive developments in health, environmental sustainability, and community well-being.\"\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"second_query = (\n",
|
||||
" \"Repeat my question back to me, as well as the last sentence of your answer.\"\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"messages.extend(\n",
|
||||
" [\n",
|
||||
" response,\n",
|
||||
" {\"role\": \"user\", \"content\": second_query},\n",
|
||||
" ]\n",
|
||||
")\n",
|
||||
"second_response = llm_with_tools.invoke(messages)\n",
|
||||
"print(second_response.text())"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "5fd8ca21-8a5e-4294-af32-11f26a040171",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
":::tip\n",
|
||||
"\n",
|
||||
"You can use [LangGraph](https://langchain-ai.github.io/langgraph/) to manage conversational threads for you in a variety of backends, including in-memory and Postgres. See [this tutorial](/docs/tutorials/chatbot/) to get started.\n",
|
||||
"\n",
|
||||
":::\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"#### Passing `previous_response_id`\n",
|
||||
"\n",
|
||||
"When using the Responses API, LangChain messages will include an `\"id\"` field in its metadata. Passing this ID to subsequent invocations will continue the conversation. Note that this is [equivalent](https://platform.openai.com/docs/guides/conversation-state?api-mode=responses#openai-apis-for-conversation-state) to manually passing in messages from a billing perspective."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"id": "009e541a-b372-410e-b9dd-608a8052ce09",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Hi Bob! How can I assist you today?\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"\n",
|
||||
"llm = ChatOpenAI(\n",
|
||||
" model=\"gpt-4o-mini\",\n",
|
||||
" use_responses_api=True,\n",
|
||||
")\n",
|
||||
"response = llm.invoke(\"Hi, I'm Bob.\")\n",
|
||||
"print(response.text())"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"id": "393a443a-4c5f-4a07-bc0e-c76e529b35e3",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Your name is Bob. How can I help you today, Bob?\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"second_response = llm.invoke(\n",
|
||||
" \"What is my name?\",\n",
|
||||
" previous_response_id=response.response_metadata[\"id\"],\n",
|
||||
")\n",
|
||||
"print(second_response.text())"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "57e27714",
|
||||
|
265
docs/docs/integrations/document_loaders/agentql.ipynb
Normal file
265
docs/docs/integrations/document_loaders/agentql.ipynb
Normal file
@ -0,0 +1,265 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "wkUAAcGZNSJ3"
|
||||
},
|
||||
"source": [
|
||||
"# AgentQLLoader\n",
|
||||
"\n",
|
||||
"[AgentQL](https://www.agentql.com/)'s document loader provides structured data extraction from any web page using an [AgentQL query](https://docs.agentql.com/agentql-query). AgentQL can be used across multiple languages and web pages without breaking over time and change.\n",
|
||||
"\n",
|
||||
"## Overview\n",
|
||||
"\n",
|
||||
"`AgentQLLoader` requires the following two parameters:\n",
|
||||
"- `url`: The URL of the web page you want to extract data from.\n",
|
||||
"- `query`: The AgentQL query to execute. Learn more about [how to write an AgentQL query in the docs](https://docs.agentql.com/agentql-query) or test one out in the [AgentQL Playground](https://dev.agentql.com/playground).\n",
|
||||
"\n",
|
||||
"Setting the following parameters are optional:\n",
|
||||
"- `api_key`: Your AgentQL API key from [dev.agentql.com](https://dev.agentql.com). **`Optional`.**\n",
|
||||
"- `timeout`: The number of seconds to wait for a request before timing out. **Defaults to `900`.**\n",
|
||||
"- `is_stealth_mode_enabled`: Whether to enable experimental anti-bot evasion strategies. This feature may not work for all websites at all times. Data extraction may take longer to complete with this mode enabled. **Defaults to `False`.**\n",
|
||||
"- `wait_for`: The number of seconds to wait for the page to load before extracting data. **Defaults to `0`.**\n",
|
||||
"- `is_scroll_to_bottom_enabled`: Whether to scroll to bottom of the page before extracting data. **Defaults to `False`.**\n",
|
||||
"- `mode`: `\"standard\"` uses deep data analysis, while `\"fast\"` trades some depth of analysis for speed and is adequate for most usecases. [Learn more about the modes in this guide.](https://docs.agentql.com/accuracy/standard-mode) **Defaults to `\"fast\"`.**\n",
|
||||
"- `is_screenshot_enabled`: Whether to take a screenshot before extracting data. Returned in 'metadata' as a Base64 string. **Defaults to `False`.**\n",
|
||||
"\n",
|
||||
"AgentQLLoader is implemented with AgentQL's [REST API](https://docs.agentql.com/rest-api/api-reference)\n",
|
||||
"\n",
|
||||
"### Integration details\n",
|
||||
"\n",
|
||||
"| Class | Package | Local | Serializable | JS support |\n",
|
||||
"| :--- | :--- | :---: | :---: | :---: |\n",
|
||||
"| AgentQLLoader| langchain-agentql | ✅ | ❌ | ❌ |\n",
|
||||
"\n",
|
||||
"### Loader features\n",
|
||||
"| Source | Document Lazy Loading | Native Async Support\n",
|
||||
"| :---: | :---: | :---: |\n",
|
||||
"| AgentQLLoader | ✅ | ❌ |"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "CaKa2QrnwPXq"
|
||||
},
|
||||
"source": [
|
||||
"## Setup\n",
|
||||
"\n",
|
||||
"To use the AgentQL Document Loader, you will need to configure the `AGENTQL_API_KEY` environment variable, or use the `api_key` parameter. You can acquire an API key from our [Dev Portal](https://dev.agentql.com)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "mZNJvUQBNSJ5"
|
||||
},
|
||||
"source": [
|
||||
"### Installation\n",
|
||||
"\n",
|
||||
"Install **langchain-agentql**."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "IblRoJJDNSJ5"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%pip install -qU langchain_agentql"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "SNsUT60YvfCm"
|
||||
},
|
||||
"source": [
|
||||
"### Set Credentials"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"metadata": {
|
||||
"id": "2D1EN7Egvk1c"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import os\n",
|
||||
"\n",
|
||||
"os.environ[\"AGENTQL_API_KEY\"] = \"YOUR_AGENTQL_API_KEY\""
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "D4hnJV_6NSJ5"
|
||||
},
|
||||
"source": [
|
||||
"## Initialization\n",
|
||||
"\n",
|
||||
"Next instantiate your model object:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"metadata": {
|
||||
"id": "oMJdxL_KNSJ5"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_agentql.document_loaders import AgentQLLoader\n",
|
||||
"\n",
|
||||
"loader = AgentQLLoader(\n",
|
||||
" url=\"https://www.agentql.com/blog\",\n",
|
||||
" query=\"\"\"\n",
|
||||
" {\n",
|
||||
" posts[] {\n",
|
||||
" title\n",
|
||||
" url\n",
|
||||
" date\n",
|
||||
" author\n",
|
||||
" }\n",
|
||||
" }\n",
|
||||
" \"\"\",\n",
|
||||
" is_scroll_to_bottom_enabled=True,\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "SRxIOx90NSJ5"
|
||||
},
|
||||
"source": [
|
||||
"## Load"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/"
|
||||
},
|
||||
"id": "bNnnCZ1oNSJ5",
|
||||
"outputId": "d0eb8cb4-9742-4f0c-80f1-0509a3af1808"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"Document(metadata={'request_id': 'bdb9dbe7-8a7f-427f-bc16-839ccc02cae6', 'generated_query': None, 'screenshot': None}, page_content=\"{'posts': [{'title': 'Launch Week Recap—make the web AI-ready', 'url': 'https://www.agentql.com/blog/2024-launch-week-recap', 'date': 'Nov 18, 2024', 'author': 'Rachel-Lee Nabors'}, {'title': 'Accurate data extraction from PDFs and images with AgentQL', 'url': 'https://www.agentql.com/blog/accurate-data-extraction-pdfs-images', 'date': 'Feb 1, 2025', 'author': 'Rachel-Lee Nabors'}, {'title': 'Introducing Scheduled Scraping Workflows', 'url': 'https://www.agentql.com/blog/scheduling', 'date': 'Dec 2, 2024', 'author': 'Rachel-Lee Nabors'}, {'title': 'Updates to Our Pricing Model', 'url': 'https://www.agentql.com/blog/2024-pricing-update', 'date': 'Nov 19, 2024', 'author': 'Rachel-Lee Nabors'}, {'title': 'Get data from any page: AgentQL’s REST API Endpoint—Launch week day 5', 'url': 'https://www.agentql.com/blog/data-rest-api', 'date': 'Nov 15, 2024', 'author': 'Rachel-Lee Nabors'}]}\")"
|
||||
]
|
||||
},
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"docs = loader.load()\n",
|
||||
"docs[0]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/"
|
||||
},
|
||||
"id": "wtPMNh72NSJ5",
|
||||
"outputId": "59d529a4-3c22-445c-f5cf-dc7b24168906"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"{'request_id': 'bdb9dbe7-8a7f-427f-bc16-839ccc02cae6', 'generated_query': None, 'screenshot': None}\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"print(docs[0].metadata)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "7RMuEwl4NSJ5"
|
||||
},
|
||||
"source": [
|
||||
"## Lazy Load\n",
|
||||
"\n",
|
||||
"`AgentQLLoader` currently only loads one `Document` at a time. Therefore, `load()` and `lazy_load()` behave the same:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/"
|
||||
},
|
||||
"id": "FIYddZBONSJ5",
|
||||
"outputId": "c39a7a6d-bc52-4ef9-b36f-e1d138590b79"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[Document(metadata={'request_id': '06273abd-b2ef-4e15-b0ec-901cba7b4825', 'generated_query': None, 'screenshot': None}, page_content=\"{'posts': [{'title': 'Launch Week Recap—make the web AI-ready', 'url': 'https://www.agentql.com/blog/2024-launch-week-recap', 'date': 'Nov 18, 2024', 'author': 'Rachel-Lee Nabors'}, {'title': 'Accurate data extraction from PDFs and images with AgentQL', 'url': 'https://www.agentql.com/blog/accurate-data-extraction-pdfs-images', 'date': 'Feb 1, 2025', 'author': 'Rachel-Lee Nabors'}, {'title': 'Introducing Scheduled Scraping Workflows', 'url': 'https://www.agentql.com/blog/scheduling', 'date': 'Dec 2, 2024', 'author': 'Rachel-Lee Nabors'}, {'title': 'Updates to Our Pricing Model', 'url': 'https://www.agentql.com/blog/2024-pricing-update', 'date': 'Nov 19, 2024', 'author': 'Rachel-Lee Nabors'}, {'title': 'Get data from any page: AgentQL’s REST API Endpoint—Launch week day 5', 'url': 'https://www.agentql.com/blog/data-rest-api', 'date': 'Nov 15, 2024', 'author': 'Rachel-Lee Nabors'}]}\")]"
|
||||
]
|
||||
},
|
||||
"execution_count": 7,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"pages = [doc for doc in loader.lazy_load()]\n",
|
||||
"pages"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## API reference\n",
|
||||
"\n",
|
||||
"For more information on how to use this integration, please refer to the [git repo](https://github.com/tinyfish-io/agentql-integrations/tree/main/langchain) or the [langchain integration documentation](https://docs.agentql.com/integrations/langchain)"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"provenance": []
|
||||
},
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.9"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 0
|
||||
}
|
35
docs/docs/integrations/providers/agentql.mdx
Normal file
35
docs/docs/integrations/providers/agentql.mdx
Normal file
@ -0,0 +1,35 @@
|
||||
# AgentQL
|
||||
|
||||
[AgentQL](https://www.agentql.com/) provides web interaction and structured data extraction from any web page using an [AgentQL query](https://docs.agentql.com/agentql-query) or a Natural Language prompt. AgentQL can be used across multiple languages and web pages without breaking over time and change.
|
||||
|
||||
## Installation and Setup
|
||||
|
||||
Install the integration package:
|
||||
|
||||
```bash
|
||||
pip install langchain-agentql
|
||||
```
|
||||
|
||||
## API Key
|
||||
|
||||
Get an API Key from our [Dev Portal](https://dev.agentql.com/) and add it to your environment variables:
|
||||
```
|
||||
export AGENTQL_API_KEY="your-api-key-here"
|
||||
```
|
||||
|
||||
## DocumentLoader
|
||||
AgentQL's document loader provides structured data extraction from any web page using an AgentQL query.
|
||||
|
||||
```python
|
||||
from langchain_agentql.document_loaders import AgentQLLoader
|
||||
```
|
||||
See our [document loader documentation and usage example](/docs/integrations/document_loaders/agentql).
|
||||
|
||||
## Tools and Toolkits
|
||||
AgentQL tools provides web interaction and structured data extraction from any web page using an AgentQL query or a Natural Language prompt.
|
||||
|
||||
```python
|
||||
from langchain_agentql.tools import ExtractWebDataTool, ExtractWebDataBrowserTool, GetWebElementBrowserTool
|
||||
from langchain_agentql import AgentQLBrowserToolkit
|
||||
```
|
||||
See our [tools documentation and usage example](/docs/integrations/tools/agentql).
|
1077
docs/docs/integrations/tools/agentql.ipynb
Normal file
1077
docs/docs/integrations/tools/agentql.ipynb
Normal file
File diff suppressed because it is too large
Load Diff
@ -147,6 +147,11 @@ WEBBROWSING_TOOL_FEAT_TABLE = {
|
||||
"interactions": True,
|
||||
"pricing": "40 free requests/day",
|
||||
},
|
||||
"AgentQL Toolkit": {
|
||||
"link": "/docs/integrations/tools/agentql",
|
||||
"interactions": True,
|
||||
"pricing": "Free trial, with pay-as-you-go and flat rate plans after",
|
||||
},
|
||||
}
|
||||
|
||||
DATABASE_TOOL_FEAT_TABLE = {
|
||||
|
@ -819,6 +819,13 @@ const FEATURE_TABLES = {
|
||||
source: "Platform for running and scaling headless browsers, can be used to scrape/crawl any site",
|
||||
api: "API",
|
||||
apiLink: "https://python.langchain.com/docs/integrations/document_loaders/hyperbrowser/"
|
||||
},
|
||||
{
|
||||
name: "AgentQL",
|
||||
link: "agentql",
|
||||
source: "Web interaction and structured data extraction from any web page using an AgentQL query or a Natural Language prompt",
|
||||
api: "API",
|
||||
apiLink: "https://python.langchain.com/docs/integrations/document_loaders/agentql/"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
25
docs/static/img/logo-dark.svg
vendored
Normal file
25
docs/static/img/logo-dark.svg
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1584.81 250">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #1c3c3c;
|
||||
stroke-width: 0px;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="LanChain-logo">
|
||||
<g id="LangChain-logotype">
|
||||
<polygon class="cls-1" points="596.33 49.07 596.33 200.67 700.76 200.67 700.76 177.78 620.04 177.78 620.04 49.07 596.33 49.07"/>
|
||||
<path class="cls-1" d="M1126.83,49.07c-20.53,0-37.95,7.4-50.38,21.41-12.32,13.88-18.82,33.36-18.82,56.33,0,47.23,27.25,77.75,69.41,77.75,29.71,0,52.71-15.54,61.54-41.56l2.14-6.31-23.53-8.94-2.17,7.03c-5.26,17.01-18.75,26.38-37.99,26.38-27.48,0-44.55-20.82-44.55-54.34s17.23-54.34,44.97-54.34c19.23,0,30.31,7.54,35.95,24.44l2.46,7.37,22.91-10.75-2.1-5.9c-8.96-25.22-29.65-38.56-59.85-38.56Z"/>
|
||||
<path class="cls-1" d="M756.43,85.05c-22.76,0-39.78,10.67-46.69,29.27-.44,1.19-1.77,4.78-1.77,4.78l19.51,12.62,2.65-6.91c4.52-11.78,12.88-17.27,26.3-17.27s21.1,6.51,20.96,19.33c0,.52-.04,2.09-.04,2.09,0,0-17.76,2.88-25.08,4.43-31.23,6.6-44.31,18.52-44.31,38.02,0,10.39,5.77,21.64,16.3,27.95,6.32,3.78,14.57,5.21,23.68,5.21,5.99,0,11.81-.89,17.2-2.53,12.25-4.07,15.67-12.07,15.67-12.07v10.46h20.29v-74.78c0-25.42-16.7-40.6-44.67-40.6ZM777.46,164.85c0,7.86-8.56,18.93-28.5,18.93-5.63,0-9.62-1.49-12.28-3.71-3.56-2.97-4.73-7.24-4.24-11.01.21-1.64,1.2-5.17,4.87-8.23,3.75-3.13,10.38-5.37,20.62-7.6,8.42-1.83,19.54-3.85,19.54-3.85v15.48Z"/>
|
||||
<path class="cls-1" d="M876.11,85.04c-2.82,0-5.57.2-8.24.57-18.17,2.73-23.49,11.96-23.49,11.96l.02-9.31h-22.74s0,112.19,0,112.19h23.71v-62.18c0-21.13,15.41-30.75,29.73-30.75,15.48,0,23,8.32,23,25.45v67.48h23.71v-70.74c0-27.56-17.51-44.67-45.69-44.67Z"/>
|
||||
<path class="cls-1" d="M1539.12,85.04c-2.82,0-5.57.2-8.24.57-18.17,2.73-23.49,11.96-23.49,11.96v-9.32h-22.72v112.2h23.71v-62.18c0-21.13,15.41-30.75,29.73-30.75,15.48,0,23,8.32,23,25.45v67.48h23.71v-70.74c0-27.56-17.51-44.67-45.69-44.67Z"/>
|
||||
<path class="cls-1" d="M1020.76,88.26v11.55s-5.81-14.77-32.24-14.77c-32.84,0-53.24,22.66-53.24,59.15,0,20.59,6.58,36.8,18.19,47.04,9.03,7.96,21.09,12.04,35.45,12.32,9.99.19,16.46-2.53,20.5-5.1,7.76-4.94,10.64-9.63,10.64-9.63,0,0-.33,3.67-.93,8.64-.43,3.6-1.24,6.13-1.24,6.13h0c-3.61,12.85-14.17,20.28-29.57,20.28s-24.73-5.07-26.58-15.06l-23.05,6.88c3.98,19.2,22,30.66,48.2,30.66,17.81,0,31.77-4.84,41.5-14.4,9.81-9.64,14.79-23.53,14.79-41.29v-102.41h-22.42ZM1019.26,145.21c0,22.44-10.96,35.84-29.32,35.84-19.67,0-30.95-13.44-30.95-36.86s11.28-36.66,30.95-36.66c17.92,0,29.15,13.34,29.32,34.82v2.86Z"/>
|
||||
<path class="cls-1" d="M1259.01,85.04c-2.6,0-5.13.17-7.59.49-17.88,2.79-23.14,11.9-23.14,11.9v-2.67h-.01s0-45.69,0-45.69h-23.71v151.39h23.71v-62.18c0-21.27,15.41-30.95,29.73-30.95,15.48,0,23,8.32,23,25.45v67.68h23.71v-70.94c0-27.01-17.94-44.47-45.69-44.47Z"/>
|
||||
<circle class="cls-1" cx="1450.93" cy="64.47" r="15.37"/>
|
||||
<path class="cls-1" d="M1439.14,88.2v56.94h0c-6.75-5.56-14.6-9.75-23.5-12.26v-7.23c0-25.42-16.7-40.6-44.67-40.6-22.76,0-39.78,10.67-46.69,29.27-.44,1.19-1.77,4.78-1.77,4.78l19.51,12.62,2.65-6.91c4.52-11.78,12.88-17.27,26.3-17.27s21.1,6.51,20.96,19.33c0,.08,0,1.15,0,2.86-10.04-.28-19.38.69-27.77,2.66,0,0,0,0,0,0-11.06,2.5-31.6,8.85-38.94,25.36-.05.11-1.13,2.96-1.13,2.96-1.06,3.28-1.59,6.84-1.59,10.7,0,10.39,5.77,21.64,16.3,27.95,6.32,3.78,14.57,5.21,23.68,5.21,5.88,0,11.6-.86,16.91-2.44,12.49-4.04,15.96-12.16,15.96-12.16v10.47h20.29v-34.27c-5.7-3.56-14.26-5.66-23.65-5.64,0,2.65,0,4.33,0,4.33,0,7.86-8.56,18.93-28.5,18.93-5.63,0-9.62-1.49-12.28-3.71-3.56-2.97-4.73-7.24-4.24-11.01.21-1.64,1.2-5.17,4.87-8.23l-.04-.11c8.42-6.89,24.97-9.64,40.17-9.04v.03c12.94.47,22.62,3.01,29.53,7.77,1.88,1.19,3.65,2.52,5.28,3.98,6.94,6.23,9.73,13.9,10.93,18.38,1.95,7.31,1.43,18.57,1.43,18.57h23.59v-112.2h-23.59Z"/>
|
||||
</g>
|
||||
<path id="LangChain-symbol" class="cls-1" d="M393.52,75.2c9.66,9.66,9.66,25.38,0,35.04l-21.64,21.29-.22-1.22c-1.58-8.75-5.74-16.69-12.02-22.97-4.73-4.72-10.32-8.21-16.62-10.37-3.91,3.93-6.06,9.08-6.06,14.5,0,1.1.1,2.24.3,3.38,3.47,1.25,6.54,3.18,9.12,5.76,9.66,9.66,9.66,25.38,0,35.04l-18.84,18.84c-4.83,4.83-11.17,7.24-17.52,7.24s-12.69-2.41-17.52-7.24c-9.66-9.66-9.66-25.38,0-35.04l21.64-21.28.22,1.22c1.57,8.73,5.73,16.67,12.03,22.96,4.74,4.74,9.99,7.89,16.28,10.04l1.16-1.16c3.52-3.52,5.45-8.2,5.45-13.19,0-1.11-.1-2.22-.29-3.31-3.63-1.2-6.62-2.91-9.34-5.63-3.92-3.92-6.36-8.93-7.04-14.48-.05-.4-.08-.79-.12-1.19-.54-7.23,2.07-14.29,7.16-19.37l18.84-18.84c4.67-4.67,10.89-7.25,17.52-7.25s12.85,2.57,17.52,7.25ZM491.9,125c0,68.93-56.08,125-125,125H125C56.08,250,0,193.93,0,125S56.08,0,125,0h241.9c68.93,0,125,56.08,125,125ZM240.9,187.69c1.97-2.39-7.13-9.12-8.99-11.59-3.78-4.1-3.8-10-6.35-14.79-6.24-14.46-13.41-28.81-23.44-41.05-10.6-13.39-23.68-24.47-35.17-37.04-8.53-8.77-10.81-21.26-18.34-30.69-10.38-15.33-43.2-19.51-48.01,2.14.02.68-.19,1.11-.78,1.54-2.66,1.93-5.03,4.14-7.02,6.81-4.87,6.78-5.62,18.28.46,24.37.2-3.21.31-6.24,2.85-8.54,4.7,4.03,11.8,5.46,17.25,2.45,12.04,17.19,9.04,40.97,18.6,59.49,2.64,4.38,5.3,8.85,8.69,12.69,2.75,4.28,12.25,9.33,12.81,13.29.1,6.8-.7,14.23,3.76,19.92,2.1,4.26-3.06,8.54-7.22,8.01-5.4.74-11.99-3.63-16.72-.94-1.67,1.81-4.94-.19-6.38,2.32-.5,1.3-3.2,3.13-1.59,4.38,1.79-1.36,3.45-2.78,5.86-1.97-.36,1.96,1.19,2.24,2.42,2.81-.04,1.33-.82,2.69.2,3.82,1.19-1.2,1.9-2.9,3.79-3.4,6.28,8.37,12.67-8.47,26.26-.89-2.76-.14-5.21.21-7.07,2.48-.46.51-.85,1.11-.04,1.77,7.33-4.73,7.29,1.62,12.05-.33,3.66-1.91,7.3-4.3,11.65-3.62-4.23,1.22-4.4,4.62-6.88,7.49-.42.44-.62.94-.13,1.67,8.78-.74,9.5-3.66,16.59-7.24,5.29-3.23,10.56,4.6,15.14.14,1.01-.97,2.39-.64,3.64-.77-1.6-8.53-19.19,1.56-18.91-9.88,5.66-3.85,4.36-11.22,4.74-17.17,6.51,3.61,13.75,5.71,20.13,9.16,3.22,5.2,8.27,12.07,15,11.62.18-.52.34-.98.53-1.51,2.04.35,4.66,1.7,5.78-.88,3.05,3.19,7.53,3.03,11.52,2.21,2.95-2.4-5.55-5.82-6.69-8.29ZM419.51,92.72c0-11.64-4.52-22.57-12.73-30.78-8.21-8.21-19.14-12.73-30.79-12.73s-22.58,4.52-30.79,12.73l-18.84,18.84c-4.4,4.4-7.74,9.57-9.93,15.36l-.13.33-.34.1c-6.84,2.11-12.87,5.73-17.92,10.78l-18.84,18.84c-16.97,16.98-16.97,44.6,0,61.57,8.21,8.21,19.14,12.73,30.78,12.73h0c11.64,0,22.58-4.52,30.79-12.73l18.84-18.84c4.38-4.38,7.7-9.53,9.89-15.31l.13-.33.34-.11c6.72-2.06,12.92-5.8,17.95-10.82l18.84-18.84c8.21-8.21,12.73-19.14,12.73-30.79ZM172.38,173.6c-1.62,6.32-2.15,17.09-10.37,17.4-.68,3.65,2.53,5.02,5.44,3.85,2.89-1.33,4.26,1.05,5.23,3.42,4.46.65,11.06-1.49,11.31-6.77-6.66-3.84-8.72-11.14-11.62-17.9Z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 6.4 KiB |
25
docs/static/img/logo-light.svg
vendored
Normal file
25
docs/static/img/logo-light.svg
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1584.81 250">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #fff;
|
||||
stroke-width: 0px;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="LanChain-logo">
|
||||
<g id="LangChain-logotype">
|
||||
<polygon class="cls-1" points="596.33 49.07 596.33 200.67 700.76 200.67 700.76 177.78 620.04 177.78 620.04 49.07 596.33 49.07"/>
|
||||
<path class="cls-1" d="M1126.83,49.07c-20.53,0-37.95,7.4-50.38,21.41-12.32,13.88-18.82,33.36-18.82,56.33,0,47.23,27.25,77.75,69.41,77.75,29.71,0,52.71-15.54,61.54-41.56l2.14-6.31-23.53-8.94-2.17,7.03c-5.26,17.01-18.75,26.38-37.99,26.38-27.48,0-44.55-20.82-44.55-54.34s17.23-54.34,44.97-54.34c19.23,0,30.31,7.54,35.95,24.44l2.46,7.37,22.91-10.75-2.1-5.9c-8.96-25.22-29.65-38.56-59.85-38.56Z"/>
|
||||
<path class="cls-1" d="M756.43,85.05c-22.76,0-39.78,10.67-46.69,29.27-.44,1.19-1.77,4.78-1.77,4.78l19.51,12.62,2.65-6.91c4.52-11.78,12.88-17.27,26.3-17.27s21.1,6.51,20.96,19.33c0,.52-.04,2.09-.04,2.09,0,0-17.76,2.88-25.08,4.43-31.23,6.6-44.31,18.52-44.31,38.02,0,10.39,5.77,21.64,16.3,27.95,6.32,3.78,14.57,5.21,23.68,5.21,5.99,0,11.81-.89,17.2-2.53,12.25-4.07,15.67-12.07,15.67-12.07v10.46h20.29v-74.78c0-25.42-16.7-40.6-44.67-40.6ZM777.46,164.85c0,7.86-8.56,18.93-28.5,18.93-5.63,0-9.62-1.49-12.28-3.71-3.56-2.97-4.73-7.24-4.24-11.01.21-1.64,1.2-5.17,4.87-8.23,3.75-3.13,10.38-5.37,20.62-7.6,8.42-1.83,19.54-3.85,19.54-3.85v15.48Z"/>
|
||||
<path class="cls-1" d="M876.11,85.04c-2.82,0-5.57.2-8.24.57-18.17,2.73-23.49,11.96-23.49,11.96l.02-9.31h-22.74s0,112.19,0,112.19h23.71v-62.18c0-21.13,15.41-30.75,29.73-30.75,15.48,0,23,8.32,23,25.45v67.48h23.71v-70.74c0-27.56-17.51-44.67-45.69-44.67Z"/>
|
||||
<path class="cls-1" d="M1539.12,85.04c-2.82,0-5.57.2-8.24.57-18.17,2.73-23.49,11.96-23.49,11.96v-9.32h-22.72v112.2h23.71v-62.18c0-21.13,15.41-30.75,29.73-30.75,15.48,0,23,8.32,23,25.45v67.48h23.71v-70.74c0-27.56-17.51-44.67-45.69-44.67Z"/>
|
||||
<path class="cls-1" d="M1020.76,88.26v11.55s-5.81-14.77-32.24-14.77c-32.84,0-53.24,22.66-53.24,59.15,0,20.59,6.58,36.8,18.19,47.04,9.03,7.96,21.09,12.04,35.45,12.32,9.99.19,16.46-2.53,20.5-5.1,7.76-4.94,10.64-9.63,10.64-9.63,0,0-.33,3.67-.93,8.64-.43,3.6-1.24,6.13-1.24,6.13h0c-3.61,12.85-14.17,20.28-29.57,20.28s-24.73-5.07-26.58-15.06l-23.05,6.88c3.98,19.2,22,30.66,48.2,30.66,17.81,0,31.77-4.84,41.5-14.4,9.81-9.64,14.79-23.53,14.79-41.29v-102.41h-22.42ZM1019.26,145.21c0,22.44-10.96,35.84-29.32,35.84-19.67,0-30.95-13.44-30.95-36.86s11.28-36.66,30.95-36.66c17.92,0,29.15,13.34,29.32,34.82v2.86Z"/>
|
||||
<path class="cls-1" d="M1259.01,85.04c-2.6,0-5.13.17-7.59.49-17.88,2.79-23.14,11.9-23.14,11.9v-2.67h-.01s0-45.69,0-45.69h-23.71v151.39h23.71v-62.18c0-21.27,15.41-30.95,29.73-30.95,15.48,0,23,8.32,23,25.45v67.68h23.71v-70.94c0-27.01-17.94-44.47-45.69-44.47Z"/>
|
||||
<circle class="cls-1" cx="1450.93" cy="64.47" r="15.37"/>
|
||||
<path class="cls-1" d="M1439.14,88.2v56.94h0c-6.75-5.56-14.6-9.75-23.5-12.26v-7.23c0-25.42-16.7-40.6-44.67-40.6-22.76,0-39.78,10.67-46.69,29.27-.44,1.19-1.77,4.78-1.77,4.78l19.51,12.62,2.65-6.91c4.52-11.78,12.88-17.27,26.3-17.27s21.1,6.51,20.96,19.33c0,.08,0,1.15,0,2.86-10.04-.28-19.38.69-27.77,2.66,0,0,0,0,0,0-11.06,2.5-31.6,8.85-38.94,25.36-.05.11-1.13,2.96-1.13,2.96-1.06,3.28-1.59,6.84-1.59,10.7,0,10.39,5.77,21.64,16.3,27.95,6.32,3.78,14.57,5.21,23.68,5.21,5.88,0,11.6-.86,16.91-2.44,12.49-4.04,15.96-12.16,15.96-12.16v10.47h20.29v-34.27c-5.7-3.56-14.26-5.66-23.65-5.64,0,2.65,0,4.33,0,4.33,0,7.86-8.56,18.93-28.5,18.93-5.63,0-9.62-1.49-12.28-3.71-3.56-2.97-4.73-7.24-4.24-11.01.21-1.64,1.2-5.17,4.87-8.23l-.04-.11c8.42-6.89,24.97-9.64,40.17-9.04v.03c12.94.47,22.62,3.01,29.53,7.77,1.88,1.19,3.65,2.52,5.28,3.98,6.94,6.23,9.73,13.9,10.93,18.38,1.95,7.31,1.43,18.57,1.43,18.57h23.59v-112.2h-23.59Z"/>
|
||||
</g>
|
||||
<path id="LangChain-symbol" class="cls-1" d="M393.52,75.2c9.66,9.66,9.66,25.38,0,35.04l-21.64,21.29-.22-1.22c-1.58-8.75-5.74-16.69-12.02-22.97-4.73-4.72-10.32-8.21-16.62-10.37-3.91,3.93-6.06,9.08-6.06,14.5,0,1.1.1,2.24.3,3.38,3.47,1.25,6.54,3.18,9.12,5.76,9.66,9.66,9.66,25.38,0,35.04l-18.84,18.84c-4.83,4.83-11.17,7.24-17.52,7.24s-12.69-2.41-17.52-7.24c-9.66-9.66-9.66-25.38,0-35.04l21.64-21.28.22,1.22c1.57,8.73,5.73,16.67,12.03,22.96,4.74,4.74,9.99,7.89,16.28,10.04l1.16-1.16c3.52-3.52,5.45-8.2,5.45-13.19,0-1.11-.1-2.22-.29-3.31-3.63-1.2-6.62-2.91-9.34-5.63-3.92-3.92-6.36-8.93-7.04-14.48-.05-.4-.08-.79-.12-1.19-.54-7.23,2.07-14.29,7.16-19.37l18.84-18.84c4.67-4.67,10.89-7.25,17.52-7.25s12.85,2.57,17.52,7.25ZM491.9,125c0,68.93-56.08,125-125,125H125C56.08,250,0,193.93,0,125S56.08,0,125,0h241.9C435.82,0,491.9,56.08,491.9,125ZM240.9,187.69c1.97-2.39-7.13-9.12-8.99-11.59-3.78-4.1-3.8-10-6.35-14.79-6.24-14.46-13.41-28.81-23.44-41.05-10.6-13.39-23.68-24.47-35.17-37.04-8.53-8.77-10.81-21.26-18.34-30.69-10.38-15.33-43.2-19.51-48.01,2.14.02.68-.19,1.11-.78,1.54-2.66,1.93-5.03,4.14-7.02,6.81-4.87,6.78-5.62,18.28.46,24.37.2-3.21.31-6.24,2.85-8.54,4.7,4.03,11.8,5.46,17.25,2.45,12.04,17.19,9.04,40.97,18.6,59.49,2.64,4.38,5.3,8.85,8.69,12.69,2.75,4.28,12.25,9.33,12.81,13.29.1,6.8-.7,14.23,3.76,19.92,2.1,4.26-3.06,8.54-7.22,8.01-5.4.74-11.99-3.63-16.72-.94-1.67,1.81-4.94-.19-6.38,2.32-.5,1.3-3.2,3.13-1.59,4.38,1.79-1.36,3.45-2.78,5.86-1.97-.36,1.96,1.19,2.24,2.42,2.81-.04,1.33-.82,2.69.2,3.82,1.19-1.2,1.9-2.9,3.79-3.4,6.28,8.37,12.67-8.47,26.26-.89-2.76-.14-5.21.21-7.07,2.48-.46.51-.85,1.11-.04,1.77,7.33-4.73,7.29,1.62,12.05-.33,3.66-1.91,7.3-4.3,11.65-3.62-4.23,1.22-4.4,4.62-6.88,7.49-.42.44-.62.94-.13,1.67,8.78-.74,9.5-3.66,16.59-7.24,5.29-3.23,10.56,4.6,15.14.14,1.01-.97,2.39-.64,3.64-.77-1.6-8.53-19.19,1.56-18.91-9.88,5.66-3.85,4.36-11.22,4.74-17.17,6.51,3.61,13.75,5.71,20.13,9.16,3.22,5.2,8.27,12.07,15,11.62.18-.52.34-.98.53-1.51,2.04.35,4.66,1.7,5.78-.88,3.05,3.19,7.53,3.03,11.52,2.21,2.95-2.4-5.55-5.82-6.69-8.29ZM419.51,92.72c0-11.64-4.52-22.57-12.73-30.78-8.21-8.21-19.14-12.73-30.79-12.73s-22.58,4.52-30.79,12.73l-18.84,18.84c-4.4,4.4-7.74,9.57-9.93,15.36l-.13.33-.34.1c-6.84,2.11-12.87,5.73-17.92,10.78l-18.84,18.84c-16.97,16.98-16.97,44.6,0,61.57,8.21,8.21,19.14,12.73,30.78,12.73h0c11.64,0,22.58-4.52,30.79-12.73l18.84-18.84c4.38-4.38,7.7-9.53,9.89-15.31l.13-.33.34-.11c6.72-2.06,12.92-5.8,17.95-10.82l18.84-18.84c8.21-8.21,12.73-19.14,12.73-30.79ZM172.38,173.6c-1.62,6.32-2.15,17.09-10.37,17.4-.68,3.65,2.53,5.02,5.44,3.85,2.89-1.33,4.26,1.05,5.23,3.42,4.46.65,11.06-1.49,11.31-6.77-6.66-3.84-8.72-11.14-11.62-17.9Z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 6.4 KiB |
@ -783,8 +783,6 @@ class ChatTongyi(BaseChatModel):
|
||||
]
|
||||
if len(system_message_indices) == 1 and system_message_indices[0] != 0:
|
||||
raise ValueError("System message can only be the first message.")
|
||||
elif len(system_message_indices) > 1:
|
||||
raise ValueError("There can be only one system message at most.")
|
||||
|
||||
params["messages"] = message_dicts
|
||||
|
||||
|
@ -443,6 +443,11 @@ def add_ai_message_chunks(
|
||||
else:
|
||||
usage_metadata = None
|
||||
|
||||
id = None
|
||||
for id_ in [left.id] + [o.id for o in others]:
|
||||
if id_:
|
||||
id = id_
|
||||
break
|
||||
return left.__class__(
|
||||
example=left.example,
|
||||
content=content,
|
||||
@ -450,7 +455,7 @@ def add_ai_message_chunks(
|
||||
tool_call_chunks=tool_call_chunks,
|
||||
response_metadata=response_metadata,
|
||||
usage_metadata=usage_metadata,
|
||||
id=left.id,
|
||||
id=id,
|
||||
)
|
||||
|
||||
|
||||
|
@ -56,37 +56,50 @@ def draw_mermaid(
|
||||
if with_styles
|
||||
else "graph TD;\n"
|
||||
)
|
||||
# Group nodes by subgraph
|
||||
subgraph_nodes: dict[str, dict[str, Node]] = {}
|
||||
regular_nodes: dict[str, Node] = {}
|
||||
|
||||
if with_styles:
|
||||
# Node formatting templates
|
||||
default_class_label = "default"
|
||||
format_dict = {default_class_label: "{0}({1})"}
|
||||
if first_node is not None:
|
||||
format_dict[first_node] = "{0}([{1}]):::first"
|
||||
if last_node is not None:
|
||||
format_dict[last_node] = "{0}([{1}]):::last"
|
||||
for key, node in nodes.items():
|
||||
if ":" in key:
|
||||
# For nodes with colons, add them only to their deepest subgraph level
|
||||
prefix = ":".join(key.split(":")[:-1])
|
||||
subgraph_nodes.setdefault(prefix, {})[key] = node
|
||||
else:
|
||||
regular_nodes[key] = node
|
||||
|
||||
# Add nodes to the graph
|
||||
for key, node in nodes.items():
|
||||
node_name = node.name.split(":")[-1]
|
||||
# Node formatting templates
|
||||
default_class_label = "default"
|
||||
format_dict = {default_class_label: "{0}({1})"}
|
||||
if first_node is not None:
|
||||
format_dict[first_node] = "{0}([{1}]):::first"
|
||||
if last_node is not None:
|
||||
format_dict[last_node] = "{0}([{1}]):::last"
|
||||
|
||||
def render_node(key: str, node: Node, indent: str = "\t") -> str:
|
||||
"""Helper function to render a node with consistent formatting."""
|
||||
node_name = node.name.split(":")[-1]
|
||||
label = (
|
||||
f"<p>{node_name}</p>"
|
||||
if node_name.startswith(tuple(MARKDOWN_SPECIAL_CHARS))
|
||||
and node_name.endswith(tuple(MARKDOWN_SPECIAL_CHARS))
|
||||
else node_name
|
||||
)
|
||||
if node.metadata:
|
||||
label = (
|
||||
f"<p>{node_name}</p>"
|
||||
if node_name.startswith(tuple(MARKDOWN_SPECIAL_CHARS))
|
||||
and node_name.endswith(tuple(MARKDOWN_SPECIAL_CHARS))
|
||||
else node_name
|
||||
f"{label}<hr/><small><em>"
|
||||
+ "\n".join(f"{k} = {value}" for k, value in node.metadata.items())
|
||||
+ "</em></small>"
|
||||
)
|
||||
if node.metadata:
|
||||
label = (
|
||||
f"{label}<hr/><small><em>"
|
||||
+ "\n".join(
|
||||
f"{key} = {value}" for key, value in node.metadata.items()
|
||||
)
|
||||
+ "</em></small>"
|
||||
)
|
||||
node_label = format_dict.get(key, format_dict[default_class_label]).format(
|
||||
_escape_node_label(key), label
|
||||
)
|
||||
mermaid_graph += f"\t{node_label}\n"
|
||||
node_label = format_dict.get(key, format_dict[default_class_label]).format(
|
||||
_escape_node_label(key), label
|
||||
)
|
||||
return f"{indent}{node_label}\n"
|
||||
|
||||
# Add non-subgraph nodes to the graph
|
||||
if with_styles:
|
||||
for key, node in regular_nodes.items():
|
||||
mermaid_graph += render_node(key, node)
|
||||
|
||||
# Group edges by their common prefixes
|
||||
edge_groups: dict[str, list[Edge]] = {}
|
||||
@ -116,6 +129,11 @@ def draw_mermaid(
|
||||
seen_subgraphs.add(subgraph)
|
||||
mermaid_graph += f"\tsubgraph {subgraph}\n"
|
||||
|
||||
# Add nodes that belong to this subgraph
|
||||
if with_styles and prefix in subgraph_nodes:
|
||||
for key, node in subgraph_nodes[prefix].items():
|
||||
mermaid_graph += render_node(key, node)
|
||||
|
||||
for edge in edges:
|
||||
source, target = edge.source, edge.target
|
||||
|
||||
@ -156,11 +174,25 @@ def draw_mermaid(
|
||||
# Start with the top-level edges (no common prefix)
|
||||
add_subgraph(edge_groups.get("", []), "")
|
||||
|
||||
# Add remaining subgraphs
|
||||
# Add remaining subgraphs with edges
|
||||
for prefix in edge_groups:
|
||||
if ":" in prefix or prefix == "":
|
||||
continue
|
||||
add_subgraph(edge_groups[prefix], prefix)
|
||||
seen_subgraphs.add(prefix)
|
||||
|
||||
# Add empty subgraphs (subgraphs with no internal edges)
|
||||
if with_styles:
|
||||
for prefix in subgraph_nodes:
|
||||
if ":" not in prefix and prefix not in seen_subgraphs:
|
||||
mermaid_graph += f"\tsubgraph {prefix}\n"
|
||||
|
||||
# Add nodes that belong to this subgraph
|
||||
for key, node in subgraph_nodes[prefix].items():
|
||||
mermaid_graph += render_node(key, node)
|
||||
|
||||
mermaid_graph += "\tend\n"
|
||||
seen_subgraphs.add(prefix)
|
||||
|
||||
# Add custom styles for nodes
|
||||
if with_styles:
|
||||
|
@ -531,9 +531,19 @@ def convert_to_openai_tool(
|
||||
|
||||
'description' and 'parameters' keys are now optional. Only 'name' is
|
||||
required and guaranteed to be part of the output.
|
||||
|
||||
.. versionchanged:: 0.3.44
|
||||
|
||||
Return OpenAI Responses API-style tools unchanged. This includes
|
||||
any dict with "type" in "file_search", "function", "computer_use_preview",
|
||||
"web_search_preview".
|
||||
"""
|
||||
if isinstance(tool, dict) and tool.get("type") == "function" and "function" in tool:
|
||||
return tool
|
||||
if isinstance(tool, dict):
|
||||
if tool.get("type") in ("function", "file_search", "computer_use_preview"):
|
||||
return tool
|
||||
# As of 03.12.25 can be "web_search_preview" or "web_search_preview_2025_03_11"
|
||||
if (tool.get("type") or "").startswith("web_search_preview"):
|
||||
return tool
|
||||
oai_function = convert_to_openai_function(tool, strict=strict)
|
||||
return {"type": "function", "function": oai_function}
|
||||
|
||||
|
@ -17,7 +17,7 @@ dependencies = [
|
||||
"pydantic<3.0.0,>=2.7.4; python_full_version >= \"3.12.4\"",
|
||||
]
|
||||
name = "langchain-core"
|
||||
version = "0.3.43"
|
||||
version = "0.3.45-rc.1"
|
||||
description = "Building applications with LLMs through composability"
|
||||
readme = "README.md"
|
||||
|
||||
|
@ -5,9 +5,6 @@
|
||||
graph TD;
|
||||
__start__([<p>__start__</p>]):::first
|
||||
parent_1(parent_1)
|
||||
child_child_1_grandchild_1(grandchild_1)
|
||||
child_child_1_grandchild_2(grandchild_2<hr/><small><em>__interrupt = before</em></small>)
|
||||
child_child_2(child_2)
|
||||
parent_2(parent_2)
|
||||
__end__([<p>__end__</p>]):::last
|
||||
__start__ --> parent_1;
|
||||
@ -15,8 +12,11 @@
|
||||
parent_1 --> child_child_1_grandchild_1;
|
||||
parent_2 --> __end__;
|
||||
subgraph child
|
||||
child_child_2(child_2)
|
||||
child_child_1_grandchild_2 --> child_child_2;
|
||||
subgraph child_1
|
||||
child_child_1_grandchild_1(grandchild_1)
|
||||
child_child_1_grandchild_2(grandchild_2<hr/><small><em>__interrupt = before</em></small>)
|
||||
child_child_1_grandchild_1 --> child_child_1_grandchild_2;
|
||||
end
|
||||
end
|
||||
@ -32,10 +32,6 @@
|
||||
graph TD;
|
||||
__start__([<p>__start__</p>]):::first
|
||||
parent_1(parent_1)
|
||||
child_child_1_grandchild_1(grandchild_1)
|
||||
child_child_1_grandchild_1_greatgrandchild(greatgrandchild)
|
||||
child_child_1_grandchild_2(grandchild_2<hr/><small><em>__interrupt = before</em></small>)
|
||||
child_child_2(child_2)
|
||||
parent_2(parent_2)
|
||||
__end__([<p>__end__</p>]):::last
|
||||
__start__ --> parent_1;
|
||||
@ -43,10 +39,14 @@
|
||||
parent_1 --> child_child_1_grandchild_1;
|
||||
parent_2 --> __end__;
|
||||
subgraph child
|
||||
child_child_2(child_2)
|
||||
child_child_1_grandchild_2 --> child_child_2;
|
||||
subgraph child_1
|
||||
child_child_1_grandchild_1(grandchild_1)
|
||||
child_child_1_grandchild_2(grandchild_2<hr/><small><em>__interrupt = before</em></small>)
|
||||
child_child_1_grandchild_1_greatgrandchild --> child_child_1_grandchild_2;
|
||||
subgraph grandchild_1
|
||||
child_child_1_grandchild_1_greatgrandchild(greatgrandchild)
|
||||
child_child_1_grandchild_1 --> child_child_1_grandchild_1_greatgrandchild;
|
||||
end
|
||||
end
|
||||
@ -1996,10 +1996,6 @@
|
||||
graph TD;
|
||||
__start__([<p>__start__</p>]):::first
|
||||
outer_1(outer_1)
|
||||
inner_1_inner_1(inner_1)
|
||||
inner_1_inner_2(inner_2<hr/><small><em>__interrupt = before</em></small>)
|
||||
inner_2_inner_1(inner_1)
|
||||
inner_2_inner_2(inner_2)
|
||||
outer_2(outer_2)
|
||||
__end__([<p>__end__</p>]):::last
|
||||
__start__ --> outer_1;
|
||||
@ -2009,9 +2005,13 @@
|
||||
outer_1 --> inner_2_inner_1;
|
||||
outer_2 --> __end__;
|
||||
subgraph inner_1
|
||||
inner_1_inner_1(inner_1)
|
||||
inner_1_inner_2(inner_2<hr/><small><em>__interrupt = before</em></small>)
|
||||
inner_1_inner_1 --> inner_1_inner_2;
|
||||
end
|
||||
subgraph inner_2
|
||||
inner_2_inner_1(inner_1)
|
||||
inner_2_inner_2(inner_2)
|
||||
inner_2_inner_1 --> inner_2_inner_2;
|
||||
end
|
||||
classDef default fill:#f2f0ff,line-height:1.2
|
||||
@ -2020,6 +2020,23 @@
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_single_node_subgraph_mermaid[mermaid]
|
||||
'''
|
||||
%%{init: {'flowchart': {'curve': 'linear'}}}%%
|
||||
graph TD;
|
||||
__start__([<p>__start__</p>]):::first
|
||||
__end__([<p>__end__</p>]):::last
|
||||
__start__ --> sub_meow;
|
||||
sub_meow --> __end__;
|
||||
subgraph sub
|
||||
sub_meow(meow)
|
||||
end
|
||||
classDef default fill:#f2f0ff,line-height:1.2
|
||||
classDef first fill-opacity:0
|
||||
classDef last fill:#bfb6fc
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_trim
|
||||
dict({
|
||||
'edges': list([
|
||||
|
@ -448,6 +448,23 @@ def test_triple_nested_subgraph_mermaid(snapshot: SnapshotAssertion) -> None:
|
||||
assert graph.draw_mermaid() == snapshot(name="mermaid")
|
||||
|
||||
|
||||
def test_single_node_subgraph_mermaid(snapshot: SnapshotAssertion) -> None:
|
||||
empty_data = BaseModel
|
||||
nodes = {
|
||||
"__start__": Node(
|
||||
id="__start__", name="__start__", data=empty_data, metadata=None
|
||||
),
|
||||
"sub:meow": Node(id="sub:meow", name="meow", data=empty_data, metadata=None),
|
||||
"__end__": Node(id="__end__", name="__end__", data=empty_data, metadata=None),
|
||||
}
|
||||
edges = [
|
||||
Edge(source="__start__", target="sub:meow", data=None, conditional=False),
|
||||
Edge(source="sub:meow", target="__end__", data=None, conditional=False),
|
||||
]
|
||||
graph = Graph(nodes, edges)
|
||||
assert graph.draw_mermaid() == snapshot(name="mermaid")
|
||||
|
||||
|
||||
def test_runnable_get_graph_with_invalid_input_type() -> None:
|
||||
"""Test that error isn't raised when getting graph with invalid input type."""
|
||||
|
||||
|
@ -935,7 +935,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-core"
|
||||
version = "0.3.43"
|
||||
version = "0.3.44"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "jsonpatch" },
|
||||
|
@ -133,6 +133,7 @@ def test_configurable() -> None:
|
||||
"extra_body": None,
|
||||
"include_response_headers": False,
|
||||
"stream_usage": False,
|
||||
"use_responses_api": None,
|
||||
},
|
||||
"kwargs": {
|
||||
"tools": [
|
||||
|
@ -513,3 +513,6 @@ packages:
|
||||
- name: langchain-opengradient
|
||||
path: .
|
||||
repo: OpenGradient/og-langchain
|
||||
- name: langchain-agentql
|
||||
path: langchain
|
||||
repo: tinyfish-io/agentql-integrations
|
||||
|
@ -26,7 +26,7 @@ test = [
|
||||
"pytest-asyncio<1.0.0,>=0.23.2",
|
||||
"pytest-socket<1.0.0,>=0.7.0",
|
||||
"pytest-watcher<1.0.0,>=0.3.4",
|
||||
"langchain-tests<1.0.0,>=0.3.5",
|
||||
"langchain-tests",
|
||||
"langchain-openai",
|
||||
"pytest-timeout<3.0.0,>=2.3.1",
|
||||
]
|
||||
@ -40,6 +40,7 @@ typing = ["mypy<2.0,>=1.10"]
|
||||
[tool.uv.sources]
|
||||
langchain-openai = { path = "../openai", editable = true }
|
||||
langchain-core = { path = "../../core", editable = true }
|
||||
langchain-tests = { path = "../../standard-tests", editable = true }
|
||||
|
||||
[tool.mypy]
|
||||
disallow_untyped_defs = "True"
|
||||
|
@ -367,7 +367,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-core"
|
||||
version = "0.3.35"
|
||||
version = "0.3.43"
|
||||
source = { editable = "../../core" }
|
||||
dependencies = [
|
||||
{ name = "jsonpatch" },
|
||||
@ -399,7 +399,7 @@ dev = [
|
||||
]
|
||||
lint = [{ name = "ruff", specifier = ">=0.9.2,<1.0.0" }]
|
||||
test = [
|
||||
{ name = "blockbuster", specifier = "~=1.5.11" },
|
||||
{ name = "blockbuster", specifier = "~=1.5.18" },
|
||||
{ name = "freezegun", specifier = ">=1.2.2,<2.0.0" },
|
||||
{ name = "grandalf", specifier = ">=0.8,<1.0" },
|
||||
{ name = "langchain-tests", directory = "../../standard-tests" },
|
||||
@ -464,7 +464,7 @@ dev = []
|
||||
lint = [{ name = "ruff", specifier = ">=0.5,<1.0" }]
|
||||
test = [
|
||||
{ name = "langchain-openai", editable = "../openai" },
|
||||
{ name = "langchain-tests", specifier = ">=0.3.5,<1.0.0" },
|
||||
{ name = "langchain-tests", editable = "../../standard-tests" },
|
||||
{ name = "pytest", specifier = ">=7.4.3,<8.0.0" },
|
||||
{ name = "pytest-asyncio", specifier = ">=0.23.2,<1.0.0" },
|
||||
{ name = "pytest-socket", specifier = ">=0.7.0,<1.0.0" },
|
||||
@ -476,7 +476,7 @@ typing = [{ name = "mypy", specifier = ">=1.10,<2.0" }]
|
||||
|
||||
[[package]]
|
||||
name = "langchain-openai"
|
||||
version = "0.3.5"
|
||||
version = "0.3.8"
|
||||
source = { editable = "../openai" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@ -524,8 +524,8 @@ typing = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-tests"
|
||||
version = "0.3.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
version = "0.3.14"
|
||||
source = { editable = "../../standard-tests" }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
{ name = "langchain-core" },
|
||||
@ -536,9 +536,26 @@ dependencies = [
|
||||
{ name = "pytest-socket" },
|
||||
{ name = "syrupy" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/80/24/b1ef0d74222d04c4196e673e3ae8bac9f89481c17c4e6a72c67f61b403c7/langchain_tests-0.3.10.tar.gz", hash = "sha256:ba0ce038cb633e906961efc85591dd86b28d5c84a7880e7e0cd4dcb833d604a8", size = 31022 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/c3/2f2f2e919bbb9f8608389ac926c6cf8f717c3965956f0e5f139372742fb9/langchain_tests-0.3.10-py3-none-any.whl", hash = "sha256:393e15990b9d1d12b52ee832257e874beb4299891d98ec7682b7fba12c0f8fe1", size = 37521 },
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "httpx", specifier = ">=0.25.0,<1" },
|
||||
{ name = "langchain-core", editable = "../../core" },
|
||||
{ name = "numpy", specifier = ">=1.26.2,<3" },
|
||||
{ name = "pytest", specifier = ">=7,<9" },
|
||||
{ name = "pytest-asyncio", specifier = ">=0.20,<1" },
|
||||
{ name = "pytest-socket", specifier = ">=0.6.0,<1" },
|
||||
{ name = "syrupy", specifier = ">=4,<5" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
codespell = [{ name = "codespell", specifier = ">=2.2.0,<3.0.0" }]
|
||||
lint = [{ name = "ruff", specifier = ">=0.9.2,<1.0.0" }]
|
||||
test = [{ name = "langchain-core", editable = "../../core" }]
|
||||
test-integration = []
|
||||
typing = [
|
||||
{ name = "langchain-core", editable = "../../core" },
|
||||
{ name = "mypy", specifier = ">=1,<2" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -12,9 +12,11 @@ import sys
|
||||
import warnings
|
||||
from functools import partial
|
||||
from io import BytesIO
|
||||
from json import JSONDecodeError
|
||||
from math import ceil
|
||||
from operator import itemgetter
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
AsyncIterator,
|
||||
Callable,
|
||||
@ -89,6 +91,7 @@ from langchain_core.runnables import (
|
||||
)
|
||||
from langchain_core.runnables.config import run_in_executor
|
||||
from langchain_core.tools import BaseTool
|
||||
from langchain_core.tools.base import _stringify
|
||||
from langchain_core.utils import get_pydantic_field_names
|
||||
from langchain_core.utils.function_calling import (
|
||||
convert_to_openai_function,
|
||||
@ -104,12 +107,17 @@ from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator
|
||||
from pydantic.v1 import BaseModel as BaseModelV1
|
||||
from typing_extensions import Self
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from openai.types.responses import Response
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# This SSL context is equivelent to the default `verify=True`.
|
||||
# https://www.python-httpx.org/advanced/ssl/#configuring-client-instances
|
||||
global_ssl_context = ssl.create_default_context(cafile=certifi.where())
|
||||
|
||||
_FUNCTION_CALL_IDS_MAP_KEY = "__openai_function_call_ids__"
|
||||
|
||||
|
||||
def _convert_dict_to_message(_dict: Mapping[str, Any]) -> BaseMessage:
|
||||
"""Convert a dictionary to a LangChain message.
|
||||
@ -528,6 +536,14 @@ class BaseChatOpenAI(BaseChatModel):
|
||||
invocation.
|
||||
"""
|
||||
|
||||
use_responses_api: Optional[bool] = None
|
||||
"""Whether to use the Responses API instead of the Chat API.
|
||||
|
||||
If not specified then will be inferred based on invocation params.
|
||||
|
||||
.. versionadded:: 0.3.9
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@ -654,7 +670,7 @@ class BaseChatOpenAI(BaseChatModel):
|
||||
if output is None:
|
||||
# Happens in streaming
|
||||
continue
|
||||
token_usage = output["token_usage"]
|
||||
token_usage = output.get("token_usage")
|
||||
if token_usage is not None:
|
||||
for k, v in token_usage.items():
|
||||
if v is None:
|
||||
@ -725,6 +741,50 @@ class BaseChatOpenAI(BaseChatModel):
|
||||
)
|
||||
return generation_chunk
|
||||
|
||||
def _stream_responses(
|
||||
self,
|
||||
messages: List[BaseMessage],
|
||||
stop: Optional[List[str]] = None,
|
||||
run_manager: Optional[CallbackManagerForLLMRun] = None,
|
||||
**kwargs: Any,
|
||||
) -> Iterator[ChatGenerationChunk]:
|
||||
kwargs["stream"] = True
|
||||
payload = self._get_request_payload(messages, stop=stop, **kwargs)
|
||||
context_manager = self.root_client.responses.create(**payload)
|
||||
|
||||
with context_manager as response:
|
||||
for chunk in response:
|
||||
if generation_chunk := _convert_responses_chunk_to_generation_chunk(
|
||||
chunk
|
||||
):
|
||||
if run_manager:
|
||||
run_manager.on_llm_new_token(
|
||||
generation_chunk.text, chunk=generation_chunk
|
||||
)
|
||||
yield generation_chunk
|
||||
|
||||
async def _astream_responses(
|
||||
self,
|
||||
messages: List[BaseMessage],
|
||||
stop: Optional[List[str]] = None,
|
||||
run_manager: Optional[AsyncCallbackManagerForLLMRun] = None,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterator[ChatGenerationChunk]:
|
||||
kwargs["stream"] = True
|
||||
payload = self._get_request_payload(messages, stop=stop, **kwargs)
|
||||
context_manager = await self.root_async_client.responses.create(**payload)
|
||||
|
||||
async with context_manager as response:
|
||||
async for chunk in response:
|
||||
if generation_chunk := _convert_responses_chunk_to_generation_chunk(
|
||||
chunk
|
||||
):
|
||||
if run_manager:
|
||||
await run_manager.on_llm_new_token(
|
||||
generation_chunk.text, chunk=generation_chunk
|
||||
)
|
||||
yield generation_chunk
|
||||
|
||||
def _stream(
|
||||
self,
|
||||
messages: List[BaseMessage],
|
||||
@ -819,10 +879,19 @@ class BaseChatOpenAI(BaseChatModel):
|
||||
raw_response = self.client.with_raw_response.create(**payload)
|
||||
response = raw_response.parse()
|
||||
generation_info = {"headers": dict(raw_response.headers)}
|
||||
elif self._use_responses_api(payload):
|
||||
response = self.root_client.responses.create(**payload)
|
||||
return _construct_lc_result_from_responses_api(response)
|
||||
else:
|
||||
response = self.client.create(**payload)
|
||||
return self._create_chat_result(response, generation_info)
|
||||
|
||||
def _use_responses_api(self, payload: dict) -> bool:
|
||||
if isinstance(self.use_responses_api, bool):
|
||||
return self.use_responses_api
|
||||
else:
|
||||
return _use_responses_api(payload)
|
||||
|
||||
def _get_request_payload(
|
||||
self,
|
||||
input_: LanguageModelInput,
|
||||
@ -834,11 +903,12 @@ class BaseChatOpenAI(BaseChatModel):
|
||||
if stop is not None:
|
||||
kwargs["stop"] = stop
|
||||
|
||||
return {
|
||||
"messages": [_convert_message_to_dict(m) for m in messages],
|
||||
**self._default_params,
|
||||
**kwargs,
|
||||
}
|
||||
payload = {**self._default_params, **kwargs}
|
||||
if self._use_responses_api(payload):
|
||||
payload = _construct_responses_api_payload(messages, payload)
|
||||
else:
|
||||
payload["messages"] = [_convert_message_to_dict(m) for m in messages]
|
||||
return payload
|
||||
|
||||
def _create_chat_result(
|
||||
self,
|
||||
@ -877,6 +947,8 @@ class BaseChatOpenAI(BaseChatModel):
|
||||
"model_name": response_dict.get("model", self.model_name),
|
||||
"system_fingerprint": response_dict.get("system_fingerprint", ""),
|
||||
}
|
||||
if "id" in response_dict:
|
||||
llm_output["id"] = response_dict["id"]
|
||||
|
||||
if isinstance(response, openai.BaseModel) and getattr(
|
||||
response, "choices", None
|
||||
@ -989,6 +1061,9 @@ class BaseChatOpenAI(BaseChatModel):
|
||||
raw_response = await self.async_client.with_raw_response.create(**payload)
|
||||
response = raw_response.parse()
|
||||
generation_info = {"headers": dict(raw_response.headers)}
|
||||
elif self._use_responses_api(payload):
|
||||
response = await self.root_async_client.responses.create(**payload)
|
||||
return _construct_lc_result_from_responses_api(response)
|
||||
else:
|
||||
response = await self.async_client.create(**payload)
|
||||
return await run_in_executor(
|
||||
@ -1258,33 +1333,38 @@ class BaseChatOpenAI(BaseChatModel):
|
||||
formatted_tools = [
|
||||
convert_to_openai_tool(tool, strict=strict) for tool in tools
|
||||
]
|
||||
tool_names = []
|
||||
for tool in formatted_tools:
|
||||
if "function" in tool:
|
||||
tool_names.append(tool["function"]["name"])
|
||||
elif "name" in tool:
|
||||
tool_names.append(tool["name"])
|
||||
else:
|
||||
pass
|
||||
if tool_choice:
|
||||
if isinstance(tool_choice, str):
|
||||
# tool_choice is a tool/function name
|
||||
if tool_choice not in ("auto", "none", "any", "required"):
|
||||
if tool_choice in tool_names:
|
||||
tool_choice = {
|
||||
"type": "function",
|
||||
"function": {"name": tool_choice},
|
||||
}
|
||||
elif tool_choice in (
|
||||
"file_search",
|
||||
"web_search_preview",
|
||||
"computer_use_preview",
|
||||
):
|
||||
tool_choice = {"type": tool_choice}
|
||||
# 'any' is not natively supported by OpenAI API.
|
||||
# We support 'any' since other models use this instead of 'required'.
|
||||
if tool_choice == "any":
|
||||
elif tool_choice == "any":
|
||||
tool_choice = "required"
|
||||
else:
|
||||
pass
|
||||
elif isinstance(tool_choice, bool):
|
||||
tool_choice = "required"
|
||||
elif isinstance(tool_choice, dict):
|
||||
tool_names = [
|
||||
formatted_tool["function"]["name"]
|
||||
for formatted_tool in formatted_tools
|
||||
]
|
||||
if not any(
|
||||
tool_name == tool_choice["function"]["name"]
|
||||
for tool_name in tool_names
|
||||
):
|
||||
raise ValueError(
|
||||
f"Tool choice {tool_choice} was specified, but the only "
|
||||
f"provided tools were {tool_names}."
|
||||
)
|
||||
pass
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unrecognized tool_choice type. Expected str, bool or dict. "
|
||||
@ -1562,6 +1642,8 @@ class ChatOpenAI(BaseChatOpenAI): # type: ignore[override]
|
||||
stream_options: Dict
|
||||
Configure streaming outputs, like whether to return token usage when
|
||||
streaming (``{"include_usage": True}``).
|
||||
use_responses_api: Optional[bool]
|
||||
Whether to use the responses API.
|
||||
|
||||
See full list of supported init args and their descriptions in the params section.
|
||||
|
||||
@ -1805,6 +1887,79 @@ class ChatOpenAI(BaseChatOpenAI): # type: ignore[override]
|
||||
|
||||
See ``ChatOpenAI.bind_tools()`` method for more.
|
||||
|
||||
.. dropdown:: Built-in tools
|
||||
|
||||
.. versionadded:: 0.3.9
|
||||
|
||||
You can access `built-in tools <https://platform.openai.com/docs/guides/tools?api-mode=responses>`_
|
||||
supported by the OpenAI Responses API. See LangChain
|
||||
`docs <https://python.langchain.com/docs/integrations/chat/openai/>`_ for more
|
||||
detail.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from langchain_openai import ChatOpenAI
|
||||
|
||||
llm = ChatOpenAI(model="gpt-4o-mini")
|
||||
|
||||
tool = {"type": "web_search_preview"}
|
||||
llm_with_tools = llm.bind_tools([tool])
|
||||
|
||||
response = llm_with_tools.invoke("What was a positive news story from today?")
|
||||
response.content
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
[
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Today, a heartwarming story emerged from ...",
|
||||
"annotations": [
|
||||
{
|
||||
"end_index": 778,
|
||||
"start_index": 682,
|
||||
"title": "Title of story",
|
||||
"type": "url_citation",
|
||||
"url": "<url of story>",
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
.. dropdown:: Managing conversation state
|
||||
|
||||
.. versionadded:: 0.3.9
|
||||
|
||||
OpenAI's Responses API supports management of
|
||||
`conversation state <https://platform.openai.com/docs/guides/conversation-state?api-mode=responses>`_.
|
||||
Passing in response IDs from previous messages will continue a conversational
|
||||
thread. See LangChain
|
||||
`docs <https://python.langchain.com/docs/integrations/chat/openai/>`_ for more
|
||||
detail.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from langchain_openai import ChatOpenAI
|
||||
|
||||
llm = ChatOpenAI(model="gpt-4o-mini", use_responses_api=True)
|
||||
response = llm.invoke("Hi, I'm Bob.")
|
||||
response.text()
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
"Hi Bob! How can I assist you today?"
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
second_response = llm.invoke(
|
||||
"What is my name?", previous_response_id=response.response_metadata["id"]
|
||||
)
|
||||
second_response.text()
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
"Your name is Bob. How can I help you today, Bob?"
|
||||
|
||||
.. dropdown:: Structured output
|
||||
|
||||
.. code-block:: python
|
||||
@ -2082,27 +2237,34 @@ class ChatOpenAI(BaseChatOpenAI): # type: ignore[override]
|
||||
self, *args: Any, stream_usage: Optional[bool] = None, **kwargs: Any
|
||||
) -> Iterator[ChatGenerationChunk]:
|
||||
"""Set default stream_options."""
|
||||
stream_usage = self._should_stream_usage(stream_usage, **kwargs)
|
||||
# Note: stream_options is not a valid parameter for Azure OpenAI.
|
||||
# To support users proxying Azure through ChatOpenAI, here we only specify
|
||||
# stream_options if include_usage is set to True.
|
||||
# See https://learn.microsoft.com/en-us/azure/ai-services/openai/whats-new
|
||||
# for release notes.
|
||||
if stream_usage:
|
||||
kwargs["stream_options"] = {"include_usage": stream_usage}
|
||||
if self._use_responses_api(kwargs):
|
||||
return super()._stream_responses(*args, **kwargs)
|
||||
else:
|
||||
stream_usage = self._should_stream_usage(stream_usage, **kwargs)
|
||||
# Note: stream_options is not a valid parameter for Azure OpenAI.
|
||||
# To support users proxying Azure through ChatOpenAI, here we only specify
|
||||
# stream_options if include_usage is set to True.
|
||||
# See https://learn.microsoft.com/en-us/azure/ai-services/openai/whats-new
|
||||
# for release notes.
|
||||
if stream_usage:
|
||||
kwargs["stream_options"] = {"include_usage": stream_usage}
|
||||
|
||||
return super()._stream(*args, **kwargs)
|
||||
return super()._stream(*args, **kwargs)
|
||||
|
||||
async def _astream(
|
||||
self, *args: Any, stream_usage: Optional[bool] = None, **kwargs: Any
|
||||
) -> AsyncIterator[ChatGenerationChunk]:
|
||||
"""Set default stream_options."""
|
||||
stream_usage = self._should_stream_usage(stream_usage, **kwargs)
|
||||
if stream_usage:
|
||||
kwargs["stream_options"] = {"include_usage": stream_usage}
|
||||
if self._use_responses_api(kwargs):
|
||||
async for chunk in super()._astream_responses(*args, **kwargs):
|
||||
yield chunk
|
||||
else:
|
||||
stream_usage = self._should_stream_usage(stream_usage, **kwargs)
|
||||
if stream_usage:
|
||||
kwargs["stream_options"] = {"include_usage": stream_usage}
|
||||
|
||||
async for chunk in super()._astream(*args, **kwargs):
|
||||
yield chunk
|
||||
async for chunk in super()._astream(*args, **kwargs):
|
||||
yield chunk
|
||||
|
||||
def with_structured_output(
|
||||
self,
|
||||
@ -2617,3 +2779,355 @@ def _create_usage_metadata(oai_token_usage: dict) -> UsageMetadata:
|
||||
**{k: v for k, v in output_token_details.items() if v is not None}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _create_usage_metadata_responses(oai_token_usage: dict) -> UsageMetadata:
|
||||
input_tokens = oai_token_usage.get("input_tokens", 0)
|
||||
output_tokens = oai_token_usage.get("output_tokens", 0)
|
||||
total_tokens = oai_token_usage.get("total_tokens", input_tokens + output_tokens)
|
||||
|
||||
output_token_details: dict = {
|
||||
"audio": (oai_token_usage.get("completion_tokens_details") or {}).get(
|
||||
"audio_tokens"
|
||||
),
|
||||
"reasoning": (oai_token_usage.get("output_token_details") or {}).get(
|
||||
"reasoning_tokens"
|
||||
),
|
||||
}
|
||||
return UsageMetadata(
|
||||
input_tokens=input_tokens,
|
||||
output_tokens=output_tokens,
|
||||
total_tokens=total_tokens,
|
||||
output_token_details=OutputTokenDetails(
|
||||
**{k: v for k, v in output_token_details.items() if v is not None}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _is_builtin_tool(tool: dict) -> bool:
|
||||
return "type" in tool and tool["type"] != "function"
|
||||
|
||||
|
||||
def _use_responses_api(payload: dict) -> bool:
|
||||
uses_builtin_tools = "tools" in payload and any(
|
||||
_is_builtin_tool(tool) for tool in payload["tools"]
|
||||
)
|
||||
responses_only_args = {"previous_response_id", "text", "truncation", "include"}
|
||||
return bool(uses_builtin_tools or responses_only_args.intersection(payload))
|
||||
|
||||
|
||||
def _construct_responses_api_payload(
|
||||
messages: Sequence[BaseMessage], payload: dict
|
||||
) -> dict:
|
||||
payload["input"] = _construct_responses_api_input(messages)
|
||||
if tools := payload.pop("tools", None):
|
||||
new_tools: list = []
|
||||
for tool in tools:
|
||||
# chat api: {"type": "function", "function": {"name": "...", "description": "...", "parameters": {...}, "strict": ...}} # noqa: E501
|
||||
# responses api: {"type": "function", "name": "...", "description": "...", "parameters": {...}, "strict": ...} # noqa: E501
|
||||
if tool["type"] == "function" and "function" in tool:
|
||||
new_tools.append({"type": "function", **tool["function"]})
|
||||
else:
|
||||
new_tools.append(tool)
|
||||
payload["tools"] = new_tools
|
||||
if tool_choice := payload.pop("tool_choice", None):
|
||||
# chat api: {"type": "function", "function": {"name": "..."}}
|
||||
# responses api: {"type": "function", "name": "..."}
|
||||
if tool_choice["type"] == "function" and "function" in tool_choice:
|
||||
payload["tool_choice"] = {"type": "function", **tool_choice["function"]}
|
||||
else:
|
||||
payload["tool_choice"] = tool_choice
|
||||
if response_format := payload.pop("response_format", None):
|
||||
if payload.get("text"):
|
||||
text = payload["text"]
|
||||
raise ValueError(
|
||||
"Can specify at most one of 'response_format' or 'text', received both:"
|
||||
f"\n{response_format=}\n{text=}"
|
||||
)
|
||||
# chat api: {"type": "json_schema, "json_schema": {"schema": {...}, "name": "...", "description": "...", "strict": ...}} # noqa: E501
|
||||
# responses api: {"type": "json_schema, "schema": {...}, "name": "...", "description": "...", "strict": ...} # noqa: E501
|
||||
if response_format["type"] == "json_schema":
|
||||
payload["text"] = {"type": "json_schema", **response_format["json_schema"]}
|
||||
else:
|
||||
payload["text"] = response_format
|
||||
return payload
|
||||
|
||||
|
||||
def _construct_responses_api_input(messages: Sequence[BaseMessage]) -> list:
|
||||
input_ = []
|
||||
for lc_msg in messages:
|
||||
msg = _convert_message_to_dict(lc_msg)
|
||||
if msg["role"] == "tool":
|
||||
tool_output = msg["content"]
|
||||
if not isinstance(tool_output, str):
|
||||
tool_output = _stringify(tool_output)
|
||||
function_call_output = {
|
||||
"type": "function_call_output",
|
||||
"output": tool_output,
|
||||
"call_id": msg["tool_call_id"],
|
||||
}
|
||||
input_.append(function_call_output)
|
||||
elif msg["role"] == "assistant":
|
||||
function_calls = []
|
||||
if tool_calls := msg.pop("tool_calls", None):
|
||||
# TODO: should you be able to preserve the function call object id on
|
||||
# the langchain tool calls themselves?
|
||||
if not lc_msg.additional_kwargs.get(_FUNCTION_CALL_IDS_MAP_KEY):
|
||||
raise ValueError("")
|
||||
function_call_ids = lc_msg.additional_kwargs[_FUNCTION_CALL_IDS_MAP_KEY]
|
||||
for tool_call in tool_calls:
|
||||
function_call = {
|
||||
"type": "function_call",
|
||||
"name": tool_call["function"]["name"],
|
||||
"arguments": tool_call["function"]["arguments"],
|
||||
"call_id": tool_call["id"],
|
||||
"id": function_call_ids[tool_call["id"]],
|
||||
}
|
||||
function_calls.append(function_call)
|
||||
|
||||
msg["content"] = msg.get("content") or []
|
||||
if lc_msg.additional_kwargs.get("refusal"):
|
||||
if isinstance(msg["content"], str):
|
||||
msg["content"] = [
|
||||
{
|
||||
"type": "output_text",
|
||||
"text": msg["content"],
|
||||
"annotations": [],
|
||||
}
|
||||
]
|
||||
msg["content"] = msg["content"] + [
|
||||
{"type": "refusal", "refusal": lc_msg.additional_kwargs["refusal"]}
|
||||
]
|
||||
if isinstance(msg["content"], list):
|
||||
new_blocks = []
|
||||
for block in msg["content"]:
|
||||
# chat api: {"type": "text", "text": "..."}
|
||||
# responses api: {"type": "output_text", "text": "...", "annotations": [...]} # noqa: E501
|
||||
if block["type"] == "text":
|
||||
new_blocks.append(
|
||||
{
|
||||
"type": "output_text",
|
||||
"text": block["text"],
|
||||
"annotations": block.get("annotations") or [],
|
||||
}
|
||||
)
|
||||
elif block["type"] in ("output_text", "refusal"):
|
||||
new_blocks.append(block)
|
||||
else:
|
||||
pass
|
||||
msg["content"] = new_blocks
|
||||
if msg["content"]:
|
||||
input_.append(msg)
|
||||
input_.extend(function_calls)
|
||||
elif msg["role"] == "user":
|
||||
if isinstance(msg["content"], list):
|
||||
new_blocks = []
|
||||
for block in msg["content"]:
|
||||
# chat api: {"type": "text", "text": "..."}
|
||||
# responses api: {"type": "input_text", "text": "..."}
|
||||
if block["type"] == "text":
|
||||
new_blocks.append({"type": "input_text", "text": block["text"]})
|
||||
# chat api: {"type": "image_url", "image_url": {"url": "...", "detail": "..."}} # noqa: E501
|
||||
# responses api: {"type": "image_url", "image_url": "...", "detail": "...", "file_id": "..."} # noqa: E501
|
||||
elif block["type"] == "image_url":
|
||||
new_block = {
|
||||
"type": "input_image",
|
||||
"image_url": block["image_url"]["url"],
|
||||
}
|
||||
if block["image_url"].get("detail"):
|
||||
new_block["detail"] = block["image_url"]["detail"]
|
||||
new_blocks.append(new_block)
|
||||
elif block["type"] in ("input_text", "input_image", "input_file"):
|
||||
new_blocks.append(block)
|
||||
else:
|
||||
pass
|
||||
msg["content"] = new_blocks
|
||||
input_.append(msg)
|
||||
else:
|
||||
input_.append(msg)
|
||||
|
||||
return input_
|
||||
|
||||
|
||||
def _construct_lc_result_from_responses_api(response: Response) -> ChatResult:
|
||||
"""Construct ChatResponse from OpenAI Response API response."""
|
||||
if response.error:
|
||||
raise ValueError(response.error)
|
||||
|
||||
response_metadata = {
|
||||
k: v
|
||||
for k, v in response.model_dump(exclude_none=True, mode="json").items()
|
||||
if k
|
||||
in (
|
||||
"created_at",
|
||||
"id",
|
||||
"incomplete_details",
|
||||
"metadata",
|
||||
"object",
|
||||
"status",
|
||||
"user",
|
||||
"model",
|
||||
)
|
||||
}
|
||||
# for compatibility with chat completion calls.
|
||||
response_metadata["model_name"] = response_metadata.get("model")
|
||||
if response.usage:
|
||||
usage_metadata = _create_usage_metadata_responses(response.usage.model_dump())
|
||||
else:
|
||||
usage_metadata = None
|
||||
|
||||
content_blocks: list = []
|
||||
tool_calls = []
|
||||
invalid_tool_calls = []
|
||||
additional_kwargs: dict = {}
|
||||
msg_id = None
|
||||
for output in response.output:
|
||||
if output.type == "message":
|
||||
for content in output.content:
|
||||
if content.type == "output_text":
|
||||
block = {
|
||||
"type": "text",
|
||||
"text": content.text,
|
||||
"annotations": [
|
||||
annotation.model_dump()
|
||||
for annotation in content.annotations
|
||||
],
|
||||
}
|
||||
content_blocks.append(block)
|
||||
if content.type == "refusal":
|
||||
additional_kwargs["refusal"] = content.refusal
|
||||
msg_id = output.id
|
||||
elif output.type == "function_call":
|
||||
try:
|
||||
args = json.loads(output.arguments, strict=False)
|
||||
error = None
|
||||
except JSONDecodeError as e:
|
||||
args = output.arguments
|
||||
error = str(e)
|
||||
if error is None:
|
||||
tool_call = {
|
||||
"type": "tool_call",
|
||||
"name": output.name,
|
||||
"args": args,
|
||||
"id": output.call_id,
|
||||
}
|
||||
tool_calls.append(tool_call)
|
||||
else:
|
||||
tool_call = {
|
||||
"type": "invalid_tool_call",
|
||||
"name": output.name,
|
||||
"args": args,
|
||||
"id": output.call_id,
|
||||
"error": error,
|
||||
}
|
||||
invalid_tool_calls.append(tool_call)
|
||||
if _FUNCTION_CALL_IDS_MAP_KEY not in additional_kwargs:
|
||||
additional_kwargs[_FUNCTION_CALL_IDS_MAP_KEY] = {}
|
||||
additional_kwargs[_FUNCTION_CALL_IDS_MAP_KEY][output.call_id] = output.id
|
||||
elif output.type == "reasoning":
|
||||
additional_kwargs["reasoning"] = output.model_dump(
|
||||
exclude_none=True, mode="json"
|
||||
)
|
||||
else:
|
||||
tool_output = output.model_dump(exclude_none=True, mode="json")
|
||||
if "tool_outputs" in additional_kwargs:
|
||||
additional_kwargs["tool_outputs"].append(tool_output)
|
||||
else:
|
||||
additional_kwargs["tool_outputs"] = [tool_output]
|
||||
message = AIMessage(
|
||||
content=content_blocks,
|
||||
id=msg_id,
|
||||
usage_metadata=usage_metadata,
|
||||
response_metadata=response_metadata,
|
||||
additional_kwargs=additional_kwargs,
|
||||
tool_calls=tool_calls,
|
||||
invalid_tool_calls=invalid_tool_calls,
|
||||
)
|
||||
return ChatResult(generations=[ChatGeneration(message=message)])
|
||||
|
||||
|
||||
def _convert_responses_chunk_to_generation_chunk(
|
||||
chunk: Any,
|
||||
) -> Optional[ChatGenerationChunk]:
|
||||
content = []
|
||||
tool_call_chunks: list = []
|
||||
additional_kwargs: dict = {}
|
||||
response_metadata = {}
|
||||
usage_metadata = None
|
||||
id = None
|
||||
if chunk.type == "response.output_text.delta":
|
||||
content.append(
|
||||
{"type": "text", "text": chunk.delta, "index": chunk.content_index}
|
||||
)
|
||||
elif chunk.type == "response.output_text.annotation.added":
|
||||
content.append(
|
||||
{
|
||||
"annotations": [
|
||||
chunk.annotation.model_dump(exclude_none=True, mode="json")
|
||||
],
|
||||
"index": chunk.content_index,
|
||||
}
|
||||
)
|
||||
elif chunk.type == "response.created":
|
||||
response_metadata["id"] = chunk.response.id
|
||||
elif chunk.type == "response.completed":
|
||||
msg = cast(
|
||||
AIMessage,
|
||||
(
|
||||
_construct_lc_result_from_responses_api(chunk.response)
|
||||
.generations[0]
|
||||
.message
|
||||
),
|
||||
)
|
||||
usage_metadata = msg.usage_metadata
|
||||
response_metadata = {
|
||||
k: v for k, v in msg.response_metadata.items() if k != "id"
|
||||
}
|
||||
elif chunk.type == "response.output_item.added" and chunk.item.type == "message":
|
||||
id = chunk.item.id
|
||||
elif (
|
||||
chunk.type == "response.output_item.added"
|
||||
and chunk.item.type == "function_call"
|
||||
):
|
||||
tool_call_chunks.append(
|
||||
{
|
||||
"type": "tool_call_chunk",
|
||||
"name": chunk.item.name,
|
||||
"args": chunk.item.arguments,
|
||||
"id": chunk.item.call_id,
|
||||
"index": chunk.output_index,
|
||||
}
|
||||
)
|
||||
additional_kwargs[_FUNCTION_CALL_IDS_MAP_KEY] = {
|
||||
chunk.item.call_id: chunk.item.id
|
||||
}
|
||||
elif chunk.type == "response.output_item.done" and chunk.item.type in (
|
||||
"web_search_call",
|
||||
"file_search_call",
|
||||
):
|
||||
additional_kwargs["tool_outputs"] = [
|
||||
chunk.item.model_dump(exclude_none=True, mode="json")
|
||||
]
|
||||
elif chunk.type == "response.function_call_arguments.delta":
|
||||
tool_call_chunks.append(
|
||||
{
|
||||
"type": "tool_call_chunk",
|
||||
"args": chunk.delta,
|
||||
"index": chunk.output_index,
|
||||
}
|
||||
)
|
||||
elif chunk.type == "response.refusal.done":
|
||||
additional_kwargs["refusal"] = chunk.refusal
|
||||
else:
|
||||
return None
|
||||
|
||||
return ChatGenerationChunk(
|
||||
message=AIMessageChunk(
|
||||
content=content, # type: ignore[arg-type]
|
||||
tool_call_chunks=tool_call_chunks,
|
||||
usage_metadata=usage_metadata,
|
||||
response_metadata=response_metadata,
|
||||
additional_kwargs=additional_kwargs,
|
||||
id=id,
|
||||
)
|
||||
)
|
||||
|
@ -7,12 +7,12 @@ authors = []
|
||||
license = { text = "MIT" }
|
||||
requires-python = "<4.0,>=3.9"
|
||||
dependencies = [
|
||||
"langchain-core<1.0.0,>=0.3.42",
|
||||
"openai<2.0.0,>=1.58.1",
|
||||
"langchain-core<1.0.0,>=0.3.45-rc.1",
|
||||
"openai<2.0.0,>=1.66.0",
|
||||
"tiktoken<1,>=0.7",
|
||||
]
|
||||
name = "langchain-openai"
|
||||
version = "0.3.8"
|
||||
version = "0.3.9-rc.1"
|
||||
description = "An integration package connecting OpenAI and LangChain"
|
||||
readme = "README.md"
|
||||
|
||||
|
@ -0,0 +1,168 @@
|
||||
"""Test Responses API usage."""
|
||||
|
||||
import os
|
||||
from typing import Any, Optional, cast
|
||||
|
||||
import pytest
|
||||
from langchain_core.messages import (
|
||||
AIMessage,
|
||||
AIMessageChunk,
|
||||
BaseMessage,
|
||||
BaseMessageChunk,
|
||||
)
|
||||
|
||||
from langchain_openai import ChatOpenAI
|
||||
|
||||
|
||||
def _check_response(response: Optional[BaseMessage]) -> None:
|
||||
assert isinstance(response, AIMessage)
|
||||
assert isinstance(response.content, list)
|
||||
for block in response.content:
|
||||
assert isinstance(block, dict)
|
||||
if block["type"] == "text":
|
||||
assert isinstance(block["text"], str)
|
||||
for annotation in block["annotations"]:
|
||||
if annotation["type"] == "file_citation":
|
||||
assert all(
|
||||
key in annotation
|
||||
for key in ["file_id", "filename", "index", "type"]
|
||||
)
|
||||
elif annotation["type"] == "web_search":
|
||||
assert all(
|
||||
key in annotation
|
||||
for key in ["end_index", "start_index", "title", "type", "url"]
|
||||
)
|
||||
|
||||
text_content = response.text()
|
||||
assert isinstance(text_content, str)
|
||||
assert text_content
|
||||
assert response.usage_metadata
|
||||
assert response.usage_metadata["input_tokens"] > 0
|
||||
assert response.usage_metadata["output_tokens"] > 0
|
||||
assert response.usage_metadata["total_tokens"] > 0
|
||||
assert response.response_metadata["model_name"]
|
||||
for tool_output in response.additional_kwargs["tool_outputs"]:
|
||||
assert tool_output["id"]
|
||||
assert tool_output["status"]
|
||||
assert tool_output["type"]
|
||||
|
||||
|
||||
def test_web_search() -> None:
|
||||
llm = ChatOpenAI(model="gpt-4o-mini")
|
||||
first_response = llm.invoke(
|
||||
"What was a positive news story from today?",
|
||||
tools=[{"type": "web_search_preview"}],
|
||||
)
|
||||
_check_response(first_response)
|
||||
|
||||
# Test streaming
|
||||
full: Optional[BaseMessageChunk] = None
|
||||
for chunk in llm.stream(
|
||||
"What was a positive news story from today?",
|
||||
tools=[{"type": "web_search_preview"}],
|
||||
):
|
||||
assert isinstance(chunk, AIMessageChunk)
|
||||
full = chunk if full is None else full + chunk
|
||||
_check_response(full)
|
||||
|
||||
# Use OpenAI's stateful API
|
||||
response = llm.invoke(
|
||||
"what about a negative one",
|
||||
tools=[{"type": "web_search_preview"}],
|
||||
previous_response_id=first_response.response_metadata["id"],
|
||||
)
|
||||
_check_response(response)
|
||||
|
||||
# Manually pass in chat history
|
||||
response = llm.invoke(
|
||||
[
|
||||
first_response,
|
||||
{
|
||||
"role": "user",
|
||||
"content": [{"type": "text", "text": "what about a negative one"}],
|
||||
},
|
||||
],
|
||||
tools=[{"type": "web_search_preview"}],
|
||||
)
|
||||
_check_response(response)
|
||||
|
||||
# Bind tool
|
||||
response = llm.bind_tools([{"type": "web_search_preview"}]).invoke(
|
||||
"What was a positive news story from today?"
|
||||
)
|
||||
_check_response(response)
|
||||
|
||||
|
||||
async def test_web_search_async() -> None:
|
||||
llm = ChatOpenAI(model="gpt-4o-mini")
|
||||
response = await llm.ainvoke(
|
||||
"What was a positive news story from today?",
|
||||
tools=[{"type": "web_search_preview"}],
|
||||
)
|
||||
_check_response(response)
|
||||
assert response.response_metadata["status"]
|
||||
|
||||
# Test streaming
|
||||
full: Optional[BaseMessageChunk] = None
|
||||
async for chunk in llm.astream(
|
||||
"What was a positive news story from today?",
|
||||
tools=[{"type": "web_search_preview"}],
|
||||
):
|
||||
assert isinstance(chunk, AIMessageChunk)
|
||||
full = chunk if full is None else full + chunk
|
||||
assert isinstance(full, AIMessageChunk)
|
||||
_check_response(full)
|
||||
|
||||
|
||||
def test_function_calling() -> None:
|
||||
def multiply(x: int, y: int) -> int:
|
||||
"""return x * y"""
|
||||
return x * y
|
||||
|
||||
llm = ChatOpenAI(model="gpt-4o-mini")
|
||||
bound_llm = llm.bind_tools([multiply, {"type": "web_search_preview"}])
|
||||
ai_msg = cast(AIMessage, bound_llm.invoke("whats 5 * 4"))
|
||||
assert len(ai_msg.tool_calls) == 1
|
||||
assert ai_msg.tool_calls[0]["name"] == "multiply"
|
||||
assert set(ai_msg.tool_calls[0]["args"]) == {"x", "y"}
|
||||
|
||||
full: Any = None
|
||||
for chunk in bound_llm.stream("whats 5 * 4"):
|
||||
assert isinstance(chunk, AIMessageChunk)
|
||||
full = chunk if full is None else full + chunk
|
||||
assert len(full.tool_calls) == 1
|
||||
assert full.tool_calls[0]["name"] == "multiply"
|
||||
assert set(full.tool_calls[0]["args"]) == {"x", "y"}
|
||||
|
||||
response = bound_llm.invoke("whats some good news from today")
|
||||
_check_response(response)
|
||||
|
||||
|
||||
def test_stateful_api() -> None:
|
||||
llm = ChatOpenAI(model="gpt-4o-mini", use_responses_api=True)
|
||||
response = llm.invoke("how are you, my name is Bobo")
|
||||
assert "id" in response.response_metadata
|
||||
|
||||
second_response = llm.invoke(
|
||||
"what's my name", previous_response_id=response.response_metadata["id"]
|
||||
)
|
||||
assert isinstance(second_response.content, list)
|
||||
assert "bobo" in second_response.content[0]["text"].lower() # type: ignore
|
||||
|
||||
|
||||
def test_file_search() -> None:
|
||||
pytest.skip() # TODO: set up infra
|
||||
llm = ChatOpenAI(model="gpt-4o-mini")
|
||||
tool = {
|
||||
"type": "file_search",
|
||||
"vector_store_ids": [os.environ["OPENAI_VECTOR_STORE_ID"]],
|
||||
}
|
||||
response = llm.invoke("What is deep research by OpenAI?", tools=[tool])
|
||||
_check_response(response)
|
||||
|
||||
full: Optional[BaseMessageChunk] = None
|
||||
for chunk in llm.stream("What is deep research by OpenAI?", tools=[tool]):
|
||||
assert isinstance(chunk, AIMessageChunk)
|
||||
full = chunk if full is None else full + chunk
|
||||
assert isinstance(full, AIMessageChunk)
|
||||
_check_response(full)
|
@ -3,7 +3,7 @@
|
||||
import json
|
||||
from functools import partial
|
||||
from types import TracebackType
|
||||
from typing import Any, Dict, List, Literal, Optional, Type, Union
|
||||
from typing import Any, Dict, List, Literal, Optional, Type, Union, cast
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
@ -19,13 +19,30 @@ from langchain_core.messages import (
|
||||
ToolMessage,
|
||||
)
|
||||
from langchain_core.messages.ai import UsageMetadata
|
||||
from langchain_core.outputs import ChatGeneration
|
||||
from langchain_core.outputs import ChatGeneration, ChatResult
|
||||
from langchain_core.runnables import RunnableLambda
|
||||
from openai.types.responses import ResponseOutputMessage
|
||||
from openai.types.responses.response import IncompleteDetails, Response, ResponseUsage
|
||||
from openai.types.responses.response_error import ResponseError
|
||||
from openai.types.responses.response_file_search_tool_call import (
|
||||
ResponseFileSearchToolCall,
|
||||
Result,
|
||||
)
|
||||
from openai.types.responses.response_function_tool_call import ResponseFunctionToolCall
|
||||
from openai.types.responses.response_function_web_search import (
|
||||
ResponseFunctionWebSearch,
|
||||
)
|
||||
from openai.types.responses.response_output_refusal import ResponseOutputRefusal
|
||||
from openai.types.responses.response_output_text import ResponseOutputText
|
||||
from openai.types.responses.response_usage import OutputTokensDetails
|
||||
from pydantic import BaseModel, Field
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langchain_openai.chat_models.base import (
|
||||
_FUNCTION_CALL_IDS_MAP_KEY,
|
||||
_construct_lc_result_from_responses_api,
|
||||
_construct_responses_api_input,
|
||||
_convert_dict_to_message,
|
||||
_convert_message_to_dict,
|
||||
_convert_to_openai_response_format,
|
||||
@ -862,7 +879,7 @@ def test_nested_structured_output_strict() -> None:
|
||||
|
||||
setup: str
|
||||
punchline: str
|
||||
self_evaluation: SelfEvaluation
|
||||
_evaluation: SelfEvaluation
|
||||
|
||||
llm.with_structured_output(JokeWithEvaluation, method="json_schema")
|
||||
|
||||
@ -936,3 +953,731 @@ def test_structured_outputs_parser() -> None:
|
||||
assert isinstance(deserialized, ChatGeneration)
|
||||
result = output_parser.invoke(deserialized.message)
|
||||
assert result == parsed_response
|
||||
|
||||
|
||||
def test__construct_lc_result_from_responses_api_error_handling() -> None:
|
||||
"""Test that errors in the response are properly raised."""
|
||||
response = Response(
|
||||
id="resp_123",
|
||||
created_at=1234567890,
|
||||
model="gpt-4o",
|
||||
object="response",
|
||||
error=ResponseError(message="Test error", code="server_error"),
|
||||
parallel_tool_calls=True,
|
||||
tools=[],
|
||||
tool_choice="auto",
|
||||
output=[],
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
_construct_lc_result_from_responses_api(response)
|
||||
|
||||
assert "Test error" in str(excinfo.value)
|
||||
|
||||
|
||||
def test__construct_lc_result_from_responses_api_basic_text_response() -> None:
|
||||
"""Test a basic text response with no tools or special features."""
|
||||
response = Response(
|
||||
id="resp_123",
|
||||
created_at=1234567890,
|
||||
model="gpt-4o",
|
||||
object="response",
|
||||
parallel_tool_calls=True,
|
||||
tools=[],
|
||||
tool_choice="auto",
|
||||
output=[
|
||||
ResponseOutputMessage(
|
||||
type="message",
|
||||
id="msg_123",
|
||||
content=[
|
||||
ResponseOutputText(
|
||||
type="output_text", text="Hello, world!", annotations=[]
|
||||
)
|
||||
],
|
||||
role="assistant",
|
||||
status="completed",
|
||||
)
|
||||
],
|
||||
usage=ResponseUsage(
|
||||
input_tokens=10,
|
||||
output_tokens=3,
|
||||
total_tokens=13,
|
||||
output_tokens_details=OutputTokensDetails(reasoning_tokens=0),
|
||||
),
|
||||
)
|
||||
|
||||
result = _construct_lc_result_from_responses_api(response)
|
||||
|
||||
assert isinstance(result, ChatResult)
|
||||
assert len(result.generations) == 1
|
||||
assert isinstance(result.generations[0], ChatGeneration)
|
||||
assert isinstance(result.generations[0].message, AIMessage)
|
||||
assert result.generations[0].message.content == [
|
||||
{"type": "text", "text": "Hello, world!", "annotations": []}
|
||||
]
|
||||
assert result.generations[0].message.id == "msg_123"
|
||||
assert result.generations[0].message.usage_metadata
|
||||
assert result.generations[0].message.usage_metadata["input_tokens"] == 10
|
||||
assert result.generations[0].message.usage_metadata["output_tokens"] == 3
|
||||
assert result.generations[0].message.usage_metadata["total_tokens"] == 13
|
||||
assert result.generations[0].message.response_metadata["id"] == "resp_123"
|
||||
assert result.generations[0].message.response_metadata["model_name"] == "gpt-4o"
|
||||
|
||||
|
||||
def test__construct_lc_result_from_responses_api_multiple_text_blocks() -> None:
|
||||
"""Test a response with multiple text blocks."""
|
||||
response = Response(
|
||||
id="resp_123",
|
||||
created_at=1234567890,
|
||||
model="gpt-4o",
|
||||
object="response",
|
||||
parallel_tool_calls=True,
|
||||
tools=[],
|
||||
tool_choice="auto",
|
||||
output=[
|
||||
ResponseOutputMessage(
|
||||
type="message",
|
||||
id="msg_123",
|
||||
content=[
|
||||
ResponseOutputText(
|
||||
type="output_text", text="First part", annotations=[]
|
||||
),
|
||||
ResponseOutputText(
|
||||
type="output_text", text="Second part", annotations=[]
|
||||
),
|
||||
],
|
||||
role="assistant",
|
||||
status="completed",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
result = _construct_lc_result_from_responses_api(response)
|
||||
|
||||
assert len(result.generations[0].message.content) == 2
|
||||
assert result.generations[0].message.content[0]["text"] == "First part" # type: ignore
|
||||
assert result.generations[0].message.content[1]["text"] == "Second part" # type: ignore
|
||||
|
||||
|
||||
def test__construct_lc_result_from_responses_api_refusal_response() -> None:
|
||||
"""Test a response with a refusal."""
|
||||
response = Response(
|
||||
id="resp_123",
|
||||
created_at=1234567890,
|
||||
model="gpt-4o",
|
||||
object="response",
|
||||
parallel_tool_calls=True,
|
||||
tools=[],
|
||||
tool_choice="auto",
|
||||
output=[
|
||||
ResponseOutputMessage(
|
||||
type="message",
|
||||
id="msg_123",
|
||||
content=[
|
||||
ResponseOutputRefusal(
|
||||
type="refusal", refusal="I cannot assist with that request."
|
||||
)
|
||||
],
|
||||
role="assistant",
|
||||
status="completed",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
result = _construct_lc_result_from_responses_api(response)
|
||||
|
||||
assert result.generations[0].message.content == []
|
||||
assert (
|
||||
result.generations[0].message.additional_kwargs["refusal"]
|
||||
== "I cannot assist with that request."
|
||||
)
|
||||
|
||||
|
||||
def test__construct_lc_result_from_responses_api_function_call_valid_json() -> None:
|
||||
"""Test a response with a valid function call."""
|
||||
response = Response(
|
||||
id="resp_123",
|
||||
created_at=1234567890,
|
||||
model="gpt-4o",
|
||||
object="response",
|
||||
parallel_tool_calls=True,
|
||||
tools=[],
|
||||
tool_choice="auto",
|
||||
output=[
|
||||
ResponseFunctionToolCall(
|
||||
type="function_call",
|
||||
id="func_123",
|
||||
call_id="call_123",
|
||||
name="get_weather",
|
||||
arguments='{"location": "New York", "unit": "celsius"}',
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
result = _construct_lc_result_from_responses_api(response)
|
||||
|
||||
msg: AIMessage = cast(AIMessage, result.generations[0].message)
|
||||
assert len(msg.tool_calls) == 1
|
||||
assert msg.tool_calls[0]["type"] == "tool_call"
|
||||
assert msg.tool_calls[0]["name"] == "get_weather"
|
||||
assert msg.tool_calls[0]["id"] == "call_123"
|
||||
assert msg.tool_calls[0]["args"] == {"location": "New York", "unit": "celsius"}
|
||||
assert _FUNCTION_CALL_IDS_MAP_KEY in result.generations[0].message.additional_kwargs
|
||||
assert (
|
||||
result.generations[0].message.additional_kwargs[_FUNCTION_CALL_IDS_MAP_KEY][
|
||||
"call_123"
|
||||
]
|
||||
== "func_123"
|
||||
)
|
||||
|
||||
|
||||
def test__construct_lc_result_from_responses_api_function_call_invalid_json() -> None:
|
||||
"""Test a response with an invalid JSON function call."""
|
||||
response = Response(
|
||||
id="resp_123",
|
||||
created_at=1234567890,
|
||||
model="gpt-4o",
|
||||
object="response",
|
||||
parallel_tool_calls=True,
|
||||
tools=[],
|
||||
tool_choice="auto",
|
||||
output=[
|
||||
ResponseFunctionToolCall(
|
||||
type="function_call",
|
||||
id="func_123",
|
||||
call_id="call_123",
|
||||
name="get_weather",
|
||||
arguments='{"location": "New York", "unit": "celsius"',
|
||||
# Missing closing brace
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
result = _construct_lc_result_from_responses_api(response)
|
||||
|
||||
msg: AIMessage = cast(AIMessage, result.generations[0].message)
|
||||
assert len(msg.invalid_tool_calls) == 1
|
||||
assert msg.invalid_tool_calls[0]["type"] == "invalid_tool_call"
|
||||
assert msg.invalid_tool_calls[0]["name"] == "get_weather"
|
||||
assert msg.invalid_tool_calls[0]["id"] == "call_123"
|
||||
assert (
|
||||
msg.invalid_tool_calls[0]["args"]
|
||||
== '{"location": "New York", "unit": "celsius"'
|
||||
)
|
||||
assert "error" in msg.invalid_tool_calls[0]
|
||||
assert _FUNCTION_CALL_IDS_MAP_KEY in result.generations[0].message.additional_kwargs
|
||||
|
||||
|
||||
def test__construct_lc_result_from_responses_api_complex_response() -> None:
|
||||
"""Test a complex response with multiple output types."""
|
||||
response = Response(
|
||||
id="resp_123",
|
||||
created_at=1234567890,
|
||||
model="gpt-4o",
|
||||
object="response",
|
||||
parallel_tool_calls=True,
|
||||
tools=[],
|
||||
tool_choice="auto",
|
||||
output=[
|
||||
ResponseOutputMessage(
|
||||
type="message",
|
||||
id="msg_123",
|
||||
content=[
|
||||
ResponseOutputText(
|
||||
type="output_text",
|
||||
text="Here's the information you requested:",
|
||||
annotations=[],
|
||||
)
|
||||
],
|
||||
role="assistant",
|
||||
status="completed",
|
||||
),
|
||||
ResponseFunctionToolCall(
|
||||
type="function_call",
|
||||
id="func_123",
|
||||
call_id="call_123",
|
||||
name="get_weather",
|
||||
arguments='{"location": "New York"}',
|
||||
),
|
||||
],
|
||||
metadata=dict(key1="value1", key2="value2"),
|
||||
incomplete_details=IncompleteDetails(reason="max_output_tokens"),
|
||||
status="completed",
|
||||
user="user_123",
|
||||
)
|
||||
|
||||
result = _construct_lc_result_from_responses_api(response)
|
||||
|
||||
# Check message content
|
||||
assert result.generations[0].message.content == [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Here's the information you requested:",
|
||||
"annotations": [],
|
||||
}
|
||||
]
|
||||
|
||||
# Check tool calls
|
||||
msg: AIMessage = cast(AIMessage, result.generations[0].message)
|
||||
assert len(msg.tool_calls) == 1
|
||||
assert msg.tool_calls[0]["name"] == "get_weather"
|
||||
|
||||
# Check metadata
|
||||
assert result.generations[0].message.response_metadata["id"] == "resp_123"
|
||||
assert result.generations[0].message.response_metadata["metadata"] == {
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
}
|
||||
assert result.generations[0].message.response_metadata["incomplete_details"] == {
|
||||
"reason": "max_output_tokens"
|
||||
}
|
||||
assert result.generations[0].message.response_metadata["status"] == "completed"
|
||||
assert result.generations[0].message.response_metadata["user"] == "user_123"
|
||||
|
||||
|
||||
def test__construct_lc_result_from_responses_api_no_usage_metadata() -> None:
|
||||
"""Test a response without usage metadata."""
|
||||
response = Response(
|
||||
id="resp_123",
|
||||
created_at=1234567890,
|
||||
model="gpt-4o",
|
||||
object="response",
|
||||
parallel_tool_calls=True,
|
||||
tools=[],
|
||||
tool_choice="auto",
|
||||
output=[
|
||||
ResponseOutputMessage(
|
||||
type="message",
|
||||
id="msg_123",
|
||||
content=[
|
||||
ResponseOutputText(
|
||||
type="output_text", text="Hello, world!", annotations=[]
|
||||
)
|
||||
],
|
||||
role="assistant",
|
||||
status="completed",
|
||||
)
|
||||
],
|
||||
# No usage field
|
||||
)
|
||||
|
||||
result = _construct_lc_result_from_responses_api(response)
|
||||
|
||||
assert cast(AIMessage, result.generations[0].message).usage_metadata is None
|
||||
|
||||
|
||||
def test__construct_lc_result_from_responses_api_web_search_response() -> None:
|
||||
"""Test a response with web search output."""
|
||||
from openai.types.responses.response_function_web_search import (
|
||||
ResponseFunctionWebSearch,
|
||||
)
|
||||
|
||||
response = Response(
|
||||
id="resp_123",
|
||||
created_at=1234567890,
|
||||
model="gpt-4o",
|
||||
object="response",
|
||||
parallel_tool_calls=True,
|
||||
tools=[],
|
||||
tool_choice="auto",
|
||||
output=[
|
||||
ResponseFunctionWebSearch(
|
||||
id="websearch_123", type="web_search_call", status="completed"
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
result = _construct_lc_result_from_responses_api(response)
|
||||
|
||||
assert "tool_outputs" in result.generations[0].message.additional_kwargs
|
||||
assert len(result.generations[0].message.additional_kwargs["tool_outputs"]) == 1
|
||||
assert (
|
||||
result.generations[0].message.additional_kwargs["tool_outputs"][0]["type"]
|
||||
== "web_search_call"
|
||||
)
|
||||
assert (
|
||||
result.generations[0].message.additional_kwargs["tool_outputs"][0]["id"]
|
||||
== "websearch_123"
|
||||
)
|
||||
assert (
|
||||
result.generations[0].message.additional_kwargs["tool_outputs"][0]["status"]
|
||||
== "completed"
|
||||
)
|
||||
|
||||
|
||||
def test__construct_lc_result_from_responses_api_file_search_response() -> None:
|
||||
"""Test a response with file search output."""
|
||||
response = Response(
|
||||
id="resp_123",
|
||||
created_at=1234567890,
|
||||
model="gpt-4o",
|
||||
object="response",
|
||||
parallel_tool_calls=True,
|
||||
tools=[],
|
||||
tool_choice="auto",
|
||||
output=[
|
||||
ResponseFileSearchToolCall(
|
||||
id="filesearch_123",
|
||||
type="file_search_call",
|
||||
status="completed",
|
||||
queries=["python code", "langchain"],
|
||||
results=[
|
||||
Result(
|
||||
file_id="file_123",
|
||||
filename="example.py",
|
||||
score=0.95,
|
||||
text="def hello_world() -> None:\n print('Hello, world!')",
|
||||
attributes={"language": "python", "size": 42},
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
result = _construct_lc_result_from_responses_api(response)
|
||||
|
||||
assert "tool_outputs" in result.generations[0].message.additional_kwargs
|
||||
assert len(result.generations[0].message.additional_kwargs["tool_outputs"]) == 1
|
||||
assert (
|
||||
result.generations[0].message.additional_kwargs["tool_outputs"][0]["type"]
|
||||
== "file_search_call"
|
||||
)
|
||||
assert (
|
||||
result.generations[0].message.additional_kwargs["tool_outputs"][0]["id"]
|
||||
== "filesearch_123"
|
||||
)
|
||||
assert (
|
||||
result.generations[0].message.additional_kwargs["tool_outputs"][0]["status"]
|
||||
== "completed"
|
||||
)
|
||||
assert result.generations[0].message.additional_kwargs["tool_outputs"][0][
|
||||
"queries"
|
||||
] == ["python code", "langchain"]
|
||||
assert (
|
||||
len(
|
||||
result.generations[0].message.additional_kwargs["tool_outputs"][0][
|
||||
"results"
|
||||
]
|
||||
)
|
||||
== 1
|
||||
)
|
||||
assert (
|
||||
result.generations[0].message.additional_kwargs["tool_outputs"][0]["results"][
|
||||
0
|
||||
]["file_id"]
|
||||
== "file_123"
|
||||
)
|
||||
assert (
|
||||
result.generations[0].message.additional_kwargs["tool_outputs"][0]["results"][
|
||||
0
|
||||
]["score"]
|
||||
== 0.95
|
||||
)
|
||||
|
||||
|
||||
def test__construct_lc_result_from_responses_api_mixed_search_responses() -> None:
|
||||
"""Test a response with both web search and file search outputs."""
|
||||
|
||||
response = Response(
|
||||
id="resp_123",
|
||||
created_at=1234567890,
|
||||
model="gpt-4o",
|
||||
object="response",
|
||||
parallel_tool_calls=True,
|
||||
tools=[],
|
||||
tool_choice="auto",
|
||||
output=[
|
||||
ResponseOutputMessage(
|
||||
type="message",
|
||||
id="msg_123",
|
||||
content=[
|
||||
ResponseOutputText(
|
||||
type="output_text", text="Here's what I found:", annotations=[]
|
||||
)
|
||||
],
|
||||
role="assistant",
|
||||
status="completed",
|
||||
),
|
||||
ResponseFunctionWebSearch(
|
||||
id="websearch_123", type="web_search_call", status="completed"
|
||||
),
|
||||
ResponseFileSearchToolCall(
|
||||
id="filesearch_123",
|
||||
type="file_search_call",
|
||||
status="completed",
|
||||
queries=["python code"],
|
||||
results=[
|
||||
Result(
|
||||
file_id="file_123",
|
||||
filename="example.py",
|
||||
score=0.95,
|
||||
text="def hello_world() -> None:\n print('Hello, world!')",
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
result = _construct_lc_result_from_responses_api(response)
|
||||
|
||||
# Check message content
|
||||
assert result.generations[0].message.content == [
|
||||
{"type": "text", "text": "Here's what I found:", "annotations": []}
|
||||
]
|
||||
|
||||
# Check tool outputs
|
||||
assert "tool_outputs" in result.generations[0].message.additional_kwargs
|
||||
assert len(result.generations[0].message.additional_kwargs["tool_outputs"]) == 2
|
||||
|
||||
# Check web search output
|
||||
web_search = next(
|
||||
output
|
||||
for output in result.generations[0].message.additional_kwargs["tool_outputs"]
|
||||
if output["type"] == "web_search_call"
|
||||
)
|
||||
assert web_search["id"] == "websearch_123"
|
||||
assert web_search["status"] == "completed"
|
||||
|
||||
# Check file search output
|
||||
file_search = next(
|
||||
output
|
||||
for output in result.generations[0].message.additional_kwargs["tool_outputs"]
|
||||
if output["type"] == "file_search_call"
|
||||
)
|
||||
assert file_search["id"] == "filesearch_123"
|
||||
assert file_search["queries"] == ["python code"]
|
||||
assert file_search["results"][0]["filename"] == "example.py"
|
||||
|
||||
|
||||
def test__construct_responses_api_input_human_message_with_text_blocks_conversion() -> (
|
||||
None
|
||||
):
|
||||
"""Test that human messages with text blocks are properly converted."""
|
||||
messages: list = [
|
||||
HumanMessage(content=[{"type": "text", "text": "What's in this image?"}])
|
||||
]
|
||||
result = _construct_responses_api_input(messages)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0]["role"] == "user"
|
||||
assert isinstance(result[0]["content"], list)
|
||||
assert len(result[0]["content"]) == 1
|
||||
assert result[0]["content"][0]["type"] == "input_text"
|
||||
assert result[0]["content"][0]["text"] == "What's in this image?"
|
||||
|
||||
|
||||
def test__construct_responses_api_input_human_message_with_image_url_conversion() -> (
|
||||
None
|
||||
):
|
||||
"""Test that human messages with image_url blocks are properly converted."""
|
||||
messages: list = [
|
||||
HumanMessage(
|
||||
content=[
|
||||
{"type": "text", "text": "What's in this image?"},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": "https://example.com/image.jpg",
|
||||
"detail": "high",
|
||||
},
|
||||
},
|
||||
]
|
||||
)
|
||||
]
|
||||
result = _construct_responses_api_input(messages)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0]["role"] == "user"
|
||||
assert isinstance(result[0]["content"], list)
|
||||
assert len(result[0]["content"]) == 2
|
||||
|
||||
# Check text block conversion
|
||||
assert result[0]["content"][0]["type"] == "input_text"
|
||||
assert result[0]["content"][0]["text"] == "What's in this image?"
|
||||
|
||||
# Check image block conversion
|
||||
assert result[0]["content"][1]["type"] == "input_image"
|
||||
assert result[0]["content"][1]["image_url"] == "https://example.com/image.jpg"
|
||||
assert result[0]["content"][1]["detail"] == "high"
|
||||
|
||||
|
||||
def test__construct_responses_api_input_ai_message_with_tool_calls() -> None:
|
||||
"""Test that AI messages with tool calls are properly converted."""
|
||||
tool_calls = [
|
||||
{
|
||||
"id": "call_123",
|
||||
"name": "get_weather",
|
||||
"args": {"location": "San Francisco"},
|
||||
"type": "tool_call",
|
||||
}
|
||||
]
|
||||
|
||||
# Create a mapping from tool call IDs to function call IDs
|
||||
function_call_ids = {"call_123": "func_456"}
|
||||
|
||||
ai_message = AIMessage(
|
||||
content="",
|
||||
tool_calls=tool_calls,
|
||||
additional_kwargs={_FUNCTION_CALL_IDS_MAP_KEY: function_call_ids},
|
||||
)
|
||||
|
||||
result = _construct_responses_api_input([ai_message])
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0]["type"] == "function_call"
|
||||
assert result[0]["name"] == "get_weather"
|
||||
assert result[0]["arguments"] == '{"location": "San Francisco"}'
|
||||
assert result[0]["call_id"] == "call_123"
|
||||
assert result[0]["id"] == "func_456"
|
||||
|
||||
|
||||
def test__construct_responses_api_input_ai_message_with_tool_calls_and_content() -> (
|
||||
None
|
||||
):
|
||||
"""Test that AI messages with both tool calls and content are properly converted."""
|
||||
tool_calls = [
|
||||
{
|
||||
"id": "call_123",
|
||||
"name": "get_weather",
|
||||
"args": {"location": "San Francisco"},
|
||||
"type": "tool_call",
|
||||
}
|
||||
]
|
||||
|
||||
# Create a mapping from tool call IDs to function call IDs
|
||||
function_call_ids = {"call_123": "func_456"}
|
||||
|
||||
ai_message = AIMessage(
|
||||
content="I'll check the weather for you.",
|
||||
tool_calls=tool_calls,
|
||||
additional_kwargs={_FUNCTION_CALL_IDS_MAP_KEY: function_call_ids},
|
||||
)
|
||||
|
||||
result = _construct_responses_api_input([ai_message])
|
||||
|
||||
assert len(result) == 2
|
||||
|
||||
# Check content
|
||||
assert result[0]["role"] == "assistant"
|
||||
assert result[0]["content"] == "I'll check the weather for you."
|
||||
|
||||
# Check function call
|
||||
assert result[1]["type"] == "function_call"
|
||||
assert result[1]["name"] == "get_weather"
|
||||
assert result[1]["arguments"] == '{"location": "San Francisco"}'
|
||||
assert result[1]["call_id"] == "call_123"
|
||||
assert result[1]["id"] == "func_456"
|
||||
|
||||
|
||||
def test__construct_responses_api_input_missing_function_call_ids() -> None:
|
||||
"""Test AI messages with tool calls but missing function call IDs raise an error."""
|
||||
tool_calls = [
|
||||
{
|
||||
"id": "call_123",
|
||||
"name": "get_weather",
|
||||
"args": {"location": "San Francisco"},
|
||||
"type": "tool_call",
|
||||
}
|
||||
]
|
||||
|
||||
ai_message = AIMessage(content="", tool_calls=tool_calls)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
_construct_responses_api_input([ai_message])
|
||||
|
||||
|
||||
def test__construct_responses_api_input_tool_message_conversion() -> None:
|
||||
"""Test that tool messages are properly converted to function_call_output."""
|
||||
messages = [
|
||||
ToolMessage(
|
||||
content='{"temperature": 72, "conditions": "sunny"}',
|
||||
tool_call_id="call_123",
|
||||
)
|
||||
]
|
||||
|
||||
result = _construct_responses_api_input(messages)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0]["type"] == "function_call_output"
|
||||
assert result[0]["output"] == '{"temperature": 72, "conditions": "sunny"}'
|
||||
assert result[0]["call_id"] == "call_123"
|
||||
|
||||
|
||||
def test__construct_responses_api_input_multiple_message_types() -> None:
|
||||
"""Test conversion of a conversation with multiple message types."""
|
||||
messages = [
|
||||
SystemMessage(content="You are a helpful assistant."),
|
||||
HumanMessage(content="What's the weather in San Francisco?"),
|
||||
HumanMessage(
|
||||
content=[{"type": "text", "text": "What's the weather in San Francisco?"}]
|
||||
),
|
||||
AIMessage(
|
||||
content="",
|
||||
tool_calls=[
|
||||
{
|
||||
"type": "tool_call",
|
||||
"id": "call_123",
|
||||
"name": "get_weather",
|
||||
"args": {"location": "San Francisco"},
|
||||
}
|
||||
],
|
||||
additional_kwargs={_FUNCTION_CALL_IDS_MAP_KEY: {"call_123": "func_456"}},
|
||||
),
|
||||
ToolMessage(
|
||||
content='{"temperature": 72, "conditions": "sunny"}',
|
||||
tool_call_id="call_123",
|
||||
),
|
||||
AIMessage(content="The weather in San Francisco is 72°F and sunny."),
|
||||
AIMessage(
|
||||
content=[
|
||||
{
|
||||
"type": "text",
|
||||
"text": "The weather in San Francisco is 72°F and sunny.",
|
||||
}
|
||||
]
|
||||
),
|
||||
]
|
||||
messages_copy = [m.copy(deep=True) for m in messages]
|
||||
|
||||
result = _construct_responses_api_input(messages)
|
||||
|
||||
assert len(result) == len(messages)
|
||||
|
||||
# Check system message
|
||||
assert result[0]["role"] == "system"
|
||||
assert result[0]["content"] == "You are a helpful assistant."
|
||||
|
||||
# Check human message
|
||||
assert result[1]["role"] == "user"
|
||||
assert result[1]["content"] == "What's the weather in San Francisco?"
|
||||
assert result[2]["role"] == "user"
|
||||
assert result[2]["content"] == [
|
||||
{"type": "input_text", "text": "What's the weather in San Francisco?"}
|
||||
]
|
||||
|
||||
# Check function call
|
||||
assert result[3]["type"] == "function_call"
|
||||
assert result[3]["name"] == "get_weather"
|
||||
assert result[3]["arguments"] == '{"location": "San Francisco"}'
|
||||
assert result[3]["call_id"] == "call_123"
|
||||
assert result[3]["id"] == "func_456"
|
||||
|
||||
# Check function call output
|
||||
assert result[4]["type"] == "function_call_output"
|
||||
assert result[4]["output"] == '{"temperature": 72, "conditions": "sunny"}'
|
||||
assert result[4]["call_id"] == "call_123"
|
||||
|
||||
assert result[5]["role"] == "assistant"
|
||||
assert result[5]["content"] == "The weather in San Francisco is 72°F and sunny."
|
||||
|
||||
assert result[6]["role"] == "assistant"
|
||||
assert result[6]["content"] == [
|
||||
{
|
||||
"type": "output_text",
|
||||
"text": "The weather in San Francisco is 72°F and sunny.",
|
||||
"annotations": [],
|
||||
}
|
||||
]
|
||||
|
||||
# assert no mutation has occurred
|
||||
assert messages_copy == messages
|
||||
|
@ -462,7 +462,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-core"
|
||||
version = "0.3.42"
|
||||
version = "0.3.45rc1"
|
||||
source = { editable = "../../core" }
|
||||
dependencies = [
|
||||
{ name = "jsonpatch" },
|
||||
@ -520,7 +520,7 @@ typing = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-openai"
|
||||
version = "0.3.8"
|
||||
version = "0.3.9rc1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@ -566,7 +566,7 @@ typing = [
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "langchain-core", editable = "../../core" },
|
||||
{ name = "openai", specifier = ">=1.58.1,<2.0.0" },
|
||||
{ name = "openai", specifier = ">=1.66.0,<2.0.0" },
|
||||
{ name = "tiktoken", specifier = ">=0.7,<1" },
|
||||
]
|
||||
|
||||
@ -603,7 +603,7 @@ typing = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-tests"
|
||||
version = "0.3.13"
|
||||
version = "0.3.14"
|
||||
source = { editable = "../../standard-tests" }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
@ -751,7 +751,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "openai"
|
||||
version = "1.61.1"
|
||||
version = "1.66.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
@ -763,9 +763,9 @@ dependencies = [
|
||||
{ name = "tqdm" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d9/cf/61e71ce64cf0a38f029da0f9a5f10c9fa0e69a7a977b537126dac50adfea/openai-1.61.1.tar.gz", hash = "sha256:ce1851507218209961f89f3520e06726c0aa7d0512386f0f977e3ac3e4f2472e", size = 350784 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/84/c5/3c422ca3ccc81c063955e7c20739d7f8f37fea0af865c4a60c81e6225e14/openai-1.66.0.tar.gz", hash = "sha256:8a9e672bc6eadec60a962f0b40d7d1c09050010179c919ed65322e433e2d1025", size = 396819 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/b6/2e2a011b2dc27a6711376808b4cd8c922c476ea0f1420b39892117fa8563/openai-1.61.1-py3-none-any.whl", hash = "sha256:72b0826240ce26026ac2cd17951691f046e5be82ad122d20a8e1b30ca18bd11e", size = 463126 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/f1/d52960dac9519c9de64593460826a0fe2e19159389ec97ecf3e931d2e6a3/openai-1.66.0-py3-none-any.whl", hash = "sha256:43e4a3c0c066cc5809be4e6aac456a3ebc4ec1848226ef9d1340859ac130d45a", size = 566389 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -7,7 +7,7 @@ authors = [{ name = "Erick Friis", email = "erick@langchain.dev" }]
|
||||
license = { text = "MIT" }
|
||||
requires-python = "<4.0,>=3.9"
|
||||
dependencies = [
|
||||
"langchain-core<1.0.0,>=0.3.42",
|
||||
"langchain-core<1.0.0,>=0.3.43",
|
||||
"pytest<9,>=7",
|
||||
"pytest-asyncio<1,>=0.20",
|
||||
"httpx<1,>=0.25.0",
|
||||
|
@ -288,7 +288,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-core"
|
||||
version = "0.3.42"
|
||||
version = "0.3.43"
|
||||
source = { editable = "../core" }
|
||||
dependencies = [
|
||||
{ name = "jsonpatch" },
|
||||
|
25
uv.lock
25
uv.lock
@ -1,4 +1,5 @@
|
||||
version = 1
|
||||
revision = 1
|
||||
requires-python = ">=3.9, <4.0"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.13' and platform_python_implementation == 'PyPy'",
|
||||
@ -2152,7 +2153,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain"
|
||||
version = "0.3.19"
|
||||
version = "0.3.20"
|
||||
source = { editable = "libs/langchain" }
|
||||
dependencies = [
|
||||
{ name = "async-timeout", marker = "python_full_version < '3.11'" },
|
||||
@ -2191,6 +2192,7 @@ requires-dist = [
|
||||
{ name = "requests", specifier = ">=2,<3" },
|
||||
{ name = "sqlalchemy", specifier = ">=1.4,<3" },
|
||||
]
|
||||
provides-extras = ["community", "anthropic", "openai", "cohere", "google-vertexai", "google-genai", "fireworks", "ollama", "together", "mistralai", "huggingface", "groq", "aws", "deepseek", "xai"]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
codespell = [{ name = "codespell", specifier = ">=2.2.0,<3.0.0" }]
|
||||
@ -2259,7 +2261,7 @@ typing = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-anthropic"
|
||||
version = "0.3.8"
|
||||
version = "0.3.9"
|
||||
source = { editable = "libs/partners/anthropic" }
|
||||
dependencies = [
|
||||
{ name = "anthropic" },
|
||||
@ -2360,7 +2362,7 @@ typing = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-community"
|
||||
version = "0.3.18"
|
||||
version = "0.3.19"
|
||||
source = { editable = "libs/community" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
@ -2385,8 +2387,7 @@ requires-dist = [
|
||||
{ name = "langchain", editable = "libs/langchain" },
|
||||
{ name = "langchain-core", editable = "libs/core" },
|
||||
{ name = "langsmith", specifier = ">=0.1.125,<0.4" },
|
||||
{ name = "numpy", marker = "python_full_version < '3.12'", specifier = ">=1.26.4,<2" },
|
||||
{ name = "numpy", marker = "python_full_version >= '3.12'", specifier = ">=1.26.2,<3" },
|
||||
{ name = "numpy", specifier = ">=1.26.2,<3" },
|
||||
{ name = "pydantic-settings", specifier = ">=2.4.0,<3.0.0" },
|
||||
{ name = "pyyaml", specifier = ">=5.3" },
|
||||
{ name = "requests", specifier = ">=2,<3" },
|
||||
@ -2450,7 +2451,7 @@ typing = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-core"
|
||||
version = "0.3.40"
|
||||
version = "0.3.43"
|
||||
source = { editable = "libs/core" }
|
||||
dependencies = [
|
||||
{ name = "jsonpatch" },
|
||||
@ -2573,7 +2574,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-groq"
|
||||
version = "0.2.4"
|
||||
version = "0.2.5"
|
||||
source = { editable = "libs/partners/groq" }
|
||||
dependencies = [
|
||||
{ name = "groq" },
|
||||
@ -2732,7 +2733,7 @@ typing = []
|
||||
|
||||
[[package]]
|
||||
name = "langchain-openai"
|
||||
version = "0.3.7"
|
||||
version = "0.3.8"
|
||||
source = { editable = "libs/partners/openai" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@ -2743,7 +2744,7 @@ dependencies = [
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "langchain-core", editable = "libs/core" },
|
||||
{ name = "openai", specifier = ">=1.58.1,<2.0.0" },
|
||||
{ name = "openai", specifier = ">=1.66.0,<2.0.0" },
|
||||
{ name = "tiktoken", specifier = ">=0.7,<1" },
|
||||
]
|
||||
|
||||
@ -3630,7 +3631,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "openai"
|
||||
version = "1.61.1"
|
||||
version = "1.66.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
@ -3642,9 +3643,9 @@ dependencies = [
|
||||
{ name = "tqdm" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d9/cf/61e71ce64cf0a38f029da0f9a5f10c9fa0e69a7a977b537126dac50adfea/openai-1.61.1.tar.gz", hash = "sha256:ce1851507218209961f89f3520e06726c0aa7d0512386f0f977e3ac3e4f2472e", size = 350784 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/e1/b3e1fda1aa32d4f40d4de744e91de4de65c854c3e53c63342e4b5f9c5995/openai-1.66.2.tar.gz", hash = "sha256:9b3a843c25f81ee09b6469d483d9fba779d5c6ea41861180772f043481b0598d", size = 397041 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/b6/2e2a011b2dc27a6711376808b4cd8c922c476ea0f1420b39892117fa8563/openai-1.61.1-py3-none-any.whl", hash = "sha256:72b0826240ce26026ac2cd17951691f046e5be82ad122d20a8e1b30ca18bd11e", size = 463126 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/6f/3315b3583ffe3e31c55b446cb22d2a7c235e65ca191674fffae62deb3c11/openai-1.66.2-py3-none-any.whl", hash = "sha256:75194057ee6bb8b732526387b6041327a05656d976fc21c064e21c8ac6b07999", size = 567268 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
Loading…
Reference in New Issue
Block a user