mirror of
https://github.com/hwchase17/langchain.git
synced 2026-02-13 06:16:26 +00:00
Compare commits
145 Commits
langchain-
...
sr/test-co
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d34d3157fc | ||
|
|
06d4206ffb | ||
|
|
3108b14164 | ||
|
|
1922adc092 | ||
|
|
4a242a8a4f | ||
|
|
064b37f90e | ||
|
|
062678fa18 | ||
|
|
5d3e3d3f31 | ||
|
|
5a7cf87626 | ||
|
|
c63f23d233 | ||
|
|
b7091d391d | ||
|
|
7a2952210e | ||
|
|
7549845d82 | ||
|
|
878f033ed7 | ||
|
|
4065106c2e | ||
|
|
12df938ace | ||
|
|
65ee43cc10 | ||
|
|
fe7c000fc1 | ||
|
|
dad50e5624 | ||
|
|
0a6d01e61d | ||
|
|
c6f8b0875a | ||
|
|
4c3800d743 | ||
|
|
7fe1c4b78f | ||
|
|
c375732396 | ||
|
|
9c21f83e82 | ||
|
|
880652b713 | ||
|
|
4ab94579ad | ||
|
|
eb0545a173 | ||
|
|
a2e389de9f | ||
|
|
01573c1375 | ||
|
|
2ba3ce81a6 | ||
|
|
4e4e5d7337 | ||
|
|
2a863727f9 | ||
|
|
30e2260e26 | ||
|
|
cbaea351b2 | ||
|
|
f070217c3b | ||
|
|
0915682c12 | ||
|
|
68ab9a1e56 | ||
|
|
47b79c30c0 | ||
|
|
5899f980aa | ||
|
|
b0bf4afe81 | ||
|
|
33e5d01f7c | ||
|
|
ee3373afc2 | ||
|
|
b296f103a9 | ||
|
|
525d5c0169 | ||
|
|
c4b6ba254e | ||
|
|
b7d1831f9d | ||
|
|
328ba36601 | ||
|
|
6f677ef5c1 | ||
|
|
d47d41cbd3 | ||
|
|
32bbe99efc | ||
|
|
990e346c46 | ||
|
|
9b7792631d | ||
|
|
558a8fe25b | ||
|
|
52b1516d44 | ||
|
|
8a3bb73c05 | ||
|
|
099c042395 | ||
|
|
2d4f00a451 | ||
|
|
9bd401a6d4 | ||
|
|
6aa3794b74 | ||
|
|
189dcf7295 | ||
|
|
1bc88028e6 | ||
|
|
d2942351ce | ||
|
|
83c078f363 | ||
|
|
26d39ffc4a | ||
|
|
421e2ceeee | ||
|
|
275dcbf69f | ||
|
|
9f87b27a5b | ||
|
|
b2e1196e29 | ||
|
|
2dc1396380 | ||
|
|
77941ab3ce | ||
|
|
ee19a30dde | ||
|
|
5d799b3174 | ||
|
|
8f33a985a2 | ||
|
|
78eeccef0e | ||
|
|
3d415441e8 | ||
|
|
74385e0ebd | ||
|
|
2bfbc29ccc | ||
|
|
ef79c26f18 | ||
|
|
fbe32c8e89 | ||
|
|
2511c28f92 | ||
|
|
637bb1cbbc | ||
|
|
3dfea96ec1 | ||
|
|
68643153e5 | ||
|
|
462762f75b | ||
|
|
4f3729c004 | ||
|
|
ba428cdf54 | ||
|
|
69c7d1b01b | ||
|
|
733299ec13 | ||
|
|
e1adf781c6 | ||
|
|
31b5e4810c | ||
|
|
c6801fe159 | ||
|
|
1b563067f8 | ||
|
|
1996d81d72 | ||
|
|
ab0677c6f1 | ||
|
|
bdb53c93cc | ||
|
|
94d5271cb5 | ||
|
|
e499db4266 | ||
|
|
cc3af82b47 | ||
|
|
9383b78be1 | ||
|
|
3c492571ab | ||
|
|
f2410f7ea7 | ||
|
|
91560b6a7a | ||
|
|
b1dd448233 | ||
|
|
904daf6f40 | ||
|
|
8e31a5d7bd | ||
|
|
ee630b4539 | ||
|
|
46971447df | ||
|
|
d8b94007c1 | ||
|
|
cf595dcc38 | ||
|
|
d27211cfa7 | ||
|
|
ca1a3fbe88 | ||
|
|
c955b53aed | ||
|
|
2a626d9608 | ||
|
|
0861cba04b | ||
|
|
88246f45b3 | ||
|
|
1d04514354 | ||
|
|
c2324b8f3e | ||
|
|
957ea65d12 | ||
|
|
00fa38a295 | ||
|
|
9d98c1b669 | ||
|
|
00cc9d421f | ||
|
|
65716cf590 | ||
|
|
1b77a191f4 | ||
|
|
ebfde9173c | ||
|
|
2fe0369049 | ||
|
|
e023201d42 | ||
|
|
d40e340479 | ||
|
|
9a09ed0659 | ||
|
|
5f27b546dd | ||
|
|
022fdd52c3 | ||
|
|
7946a8f64e | ||
|
|
7af79039fc | ||
|
|
1755750ca1 | ||
|
|
ddb53672e2 | ||
|
|
eeae34972f | ||
|
|
47d89b1e47 | ||
|
|
ee0bdaeb79 | ||
|
|
915c446c48 | ||
|
|
d1e2099408 | ||
|
|
6ea15b9efa | ||
|
|
69f33aaff5 | ||
|
|
3f66f102d2 | ||
|
|
c6547f58b7 | ||
|
|
dfb05a7fa0 |
77
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
77
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -8,16 +8,15 @@ body:
|
||||
value: |
|
||||
Thank you for taking the time to file a bug report.
|
||||
|
||||
Use this to report BUGS in LangChain. For usage questions, feature requests and general design questions, please use the [LangChain Forum](https://forum.langchain.com/).
|
||||
For usage questions, feature requests and general design questions, please use the [LangChain Forum](https://forum.langchain.com/).
|
||||
|
||||
Relevant links to check before filing a bug report to see if your issue has already been reported, fixed or
|
||||
if there's another way to solve your problem:
|
||||
Check these before submitting to see if your issue has already been reported, fixed or if there's another way to solve your problem:
|
||||
|
||||
* [LangChain Forum](https://forum.langchain.com/),
|
||||
* [LangChain documentation with the integrated search](https://docs.langchain.com/oss/python/langchain/overview),
|
||||
* [API Reference](https://reference.langchain.com/python/),
|
||||
* [Documentation](https://docs.langchain.com/oss/python/langchain/overview),
|
||||
* [API Reference Documentation](https://reference.langchain.com/python/),
|
||||
* [LangChain ChatBot](https://chat.langchain.com/)
|
||||
* [GitHub search](https://github.com/langchain-ai/langchain),
|
||||
* [LangChain Forum](https://forum.langchain.com/),
|
||||
- type: checkboxes
|
||||
id: checks
|
||||
attributes:
|
||||
@@ -36,16 +35,48 @@ body:
|
||||
required: true
|
||||
- label: This is not related to the langchain-community package.
|
||||
required: true
|
||||
- label: I read what a minimal reproducible example is (https://stackoverflow.com/help/minimal-reproducible-example).
|
||||
required: true
|
||||
- label: I posted a self-contained, minimal, reproducible example. A maintainer can copy it and run it AS IS.
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: package
|
||||
attributes:
|
||||
label: Package (Required)
|
||||
description: |
|
||||
Which `langchain` package(s) is this bug related to? Select at least one.
|
||||
|
||||
Note that if the package you are reporting for is not listed here, it is not in this repository (e.g. `langchain-google-genai` is in [`langchain-ai/langchain-google`](https://github.com/langchain-ai/langchain-google/)).
|
||||
|
||||
Please report issues for other packages to their respective repositories.
|
||||
options:
|
||||
- label: langchain
|
||||
- label: langchain-openai
|
||||
- label: langchain-anthropic
|
||||
- label: langchain-classic
|
||||
- label: langchain-core
|
||||
- label: langchain-cli
|
||||
- label: langchain-model-profiles
|
||||
- label: langchain-tests
|
||||
- label: langchain-text-splitters
|
||||
- label: langchain-chroma
|
||||
- label: langchain-deepseek
|
||||
- label: langchain-exa
|
||||
- label: langchain-fireworks
|
||||
- label: langchain-groq
|
||||
- label: langchain-huggingface
|
||||
- label: langchain-mistralai
|
||||
- label: langchain-nomic
|
||||
- label: langchain-ollama
|
||||
- label: langchain-perplexity
|
||||
- label: langchain-prompty
|
||||
- label: langchain-qdrant
|
||||
- label: langchain-xai
|
||||
- label: Other / not sure / general
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Example Code
|
||||
label: Example Code (Python)
|
||||
description: |
|
||||
Please add a self-contained, [minimal, reproducible, example](https://stackoverflow.com/help/minimal-reproducible-example) with your use case.
|
||||
|
||||
@@ -53,15 +84,12 @@ body:
|
||||
|
||||
**Important!**
|
||||
|
||||
* Avoid screenshots when possible, as they are hard to read and (more importantly) don't allow others to copy-and-paste your code.
|
||||
* Reduce your code to the minimum required to reproduce the issue if possible. This makes it much easier for others to help you.
|
||||
* Use code tags (e.g., ```python ... ```) to correctly [format your code](https://help.github.com/en/github/writing-on-github/creating-and-highlighting-code-blocks#syntax-highlighting).
|
||||
* INCLUDE the language label (e.g. `python`) after the first three backticks to enable syntax highlighting. (e.g., ```python rather than ```).
|
||||
* Avoid screenshots, as they are hard to read and (more importantly) don't allow others to copy-and-paste your code.
|
||||
* Reduce your code to the minimum required to reproduce the issue if possible.
|
||||
|
||||
(This will be automatically formatted into code, so no need for backticks.)
|
||||
render: python
|
||||
placeholder: |
|
||||
The following code:
|
||||
|
||||
```python
|
||||
from langchain_core.runnables import RunnableLambda
|
||||
|
||||
def bad_code(inputs) -> int:
|
||||
@@ -69,17 +97,14 @@ body:
|
||||
|
||||
chain = RunnableLambda(bad_code)
|
||||
chain.invoke('Hello!')
|
||||
```
|
||||
- type: textarea
|
||||
id: error
|
||||
validations:
|
||||
required: false
|
||||
attributes:
|
||||
label: Error Message and Stack Trace (if applicable)
|
||||
description: |
|
||||
If you are reporting an error, please include the full error message and stack trace.
|
||||
placeholder: |
|
||||
Exception + full stack trace
|
||||
If you are reporting an error, please copy and paste the full error message and
|
||||
stack trace.
|
||||
(This will be automatically formatted into code, so no need for backticks.)
|
||||
render: shell
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
@@ -99,9 +124,7 @@ body:
|
||||
attributes:
|
||||
label: System Info
|
||||
description: |
|
||||
Please share your system info with us. Do NOT skip this step and please don't trim
|
||||
the output. Most users don't include enough information here and it makes it harder
|
||||
for us to help you.
|
||||
Please share your system info with us.
|
||||
|
||||
Run the following command in your terminal and paste the output here:
|
||||
|
||||
@@ -113,8 +136,6 @@ body:
|
||||
from langchain_core import sys_info
|
||||
sys_info.print_sys_info()
|
||||
```
|
||||
|
||||
alternatively, put the entire output of `pip freeze` here.
|
||||
placeholder: |
|
||||
python -m langchain_core.sys_info
|
||||
validations:
|
||||
|
||||
13
.github/ISSUE_TEMPLATE/config.yml
vendored
13
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,9 +1,18 @@
|
||||
blank_issues_enabled: false
|
||||
version: 2.1
|
||||
contact_links:
|
||||
- name: 📚 Documentation
|
||||
url: https://github.com/langchain-ai/docs/issues/new?template=langchain.yml
|
||||
- name: 📚 Documentation issue
|
||||
url: https://github.com/langchain-ai/docs/issues/new?template=01-langchain.yml
|
||||
about: Report an issue related to the LangChain documentation
|
||||
- name: 💬 LangChain Forum
|
||||
url: https://forum.langchain.com/
|
||||
about: General community discussions and support
|
||||
- name: 📚 LangChain Documentation
|
||||
url: https://docs.langchain.com/oss/python/langchain/overview
|
||||
about: View the official LangChain documentation
|
||||
- name: 📚 API Reference Documentation
|
||||
url: https://reference.langchain.com/python/
|
||||
about: View the official LangChain API reference documentation
|
||||
- name: 💬 LangChain Forum
|
||||
url: https://forum.langchain.com/
|
||||
about: Ask questions and get help from the community
|
||||
|
||||
40
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
40
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
@@ -13,11 +13,11 @@ body:
|
||||
Relevant links to check before filing a feature request to see if your request has already been made or
|
||||
if there's another way to achieve what you want:
|
||||
|
||||
* [LangChain Forum](https://forum.langchain.com/),
|
||||
* [LangChain documentation with the integrated search](https://docs.langchain.com/oss/python/langchain/overview),
|
||||
* [API Reference](https://reference.langchain.com/python/),
|
||||
* [Documentation](https://docs.langchain.com/oss/python/langchain/overview),
|
||||
* [API Reference Documentation](https://reference.langchain.com/python/),
|
||||
* [LangChain ChatBot](https://chat.langchain.com/)
|
||||
* [GitHub search](https://github.com/langchain-ai/langchain),
|
||||
* [LangChain Forum](https://forum.langchain.com/),
|
||||
- type: checkboxes
|
||||
id: checks
|
||||
attributes:
|
||||
@@ -34,6 +34,40 @@ body:
|
||||
required: true
|
||||
- label: This is not related to the langchain-community package.
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: package
|
||||
attributes:
|
||||
label: Package (Required)
|
||||
description: |
|
||||
Which `langchain` package(s) is this request related to? Select at least one.
|
||||
|
||||
Note that if the package you are requesting for is not listed here, it is not in this repository (e.g. `langchain-google-genai` is in `langchain-ai/langchain`).
|
||||
|
||||
Please submit feature requests for other packages to their respective repositories.
|
||||
options:
|
||||
- label: langchain
|
||||
- label: langchain-openai
|
||||
- label: langchain-anthropic
|
||||
- label: langchain-classic
|
||||
- label: langchain-core
|
||||
- label: langchain-cli
|
||||
- label: langchain-model-profiles
|
||||
- label: langchain-tests
|
||||
- label: langchain-text-splitters
|
||||
- label: langchain-chroma
|
||||
- label: langchain-deepseek
|
||||
- label: langchain-exa
|
||||
- label: langchain-fireworks
|
||||
- label: langchain-groq
|
||||
- label: langchain-huggingface
|
||||
- label: langchain-mistralai
|
||||
- label: langchain-nomic
|
||||
- label: langchain-ollama
|
||||
- label: langchain-perplexity
|
||||
- label: langchain-prompty
|
||||
- label: langchain-qdrant
|
||||
- label: langchain-xai
|
||||
- label: Other / not sure / general
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
validations:
|
||||
|
||||
30
.github/ISSUE_TEMPLATE/privileged.yml
vendored
30
.github/ISSUE_TEMPLATE/privileged.yml
vendored
@@ -18,3 +18,33 @@ body:
|
||||
attributes:
|
||||
label: Issue Content
|
||||
description: Add the content of the issue here.
|
||||
- type: checkboxes
|
||||
id: package
|
||||
attributes:
|
||||
label: Package (Required)
|
||||
description: |
|
||||
Please select package(s) that this issue is related to.
|
||||
options:
|
||||
- label: langchain
|
||||
- label: langchain-openai
|
||||
- label: langchain-anthropic
|
||||
- label: langchain-classic
|
||||
- label: langchain-core
|
||||
- label: langchain-cli
|
||||
- label: langchain-model-profiles
|
||||
- label: langchain-tests
|
||||
- label: langchain-text-splitters
|
||||
- label: langchain-chroma
|
||||
- label: langchain-deepseek
|
||||
- label: langchain-exa
|
||||
- label: langchain-fireworks
|
||||
- label: langchain-groq
|
||||
- label: langchain-huggingface
|
||||
- label: langchain-mistralai
|
||||
- label: langchain-nomic
|
||||
- label: langchain-ollama
|
||||
- label: langchain-perplexity
|
||||
- label: langchain-prompty
|
||||
- label: langchain-qdrant
|
||||
- label: langchain-xai
|
||||
- label: Other / not sure / general
|
||||
|
||||
48
.github/ISSUE_TEMPLATE/task.yml
vendored
48
.github/ISSUE_TEMPLATE/task.yml
vendored
@@ -25,13 +25,13 @@ body:
|
||||
label: Task Description
|
||||
description: |
|
||||
Provide a clear and detailed description of the task.
|
||||
|
||||
|
||||
What needs to be done? Be specific about the scope and requirements.
|
||||
placeholder: |
|
||||
This task involves...
|
||||
|
||||
|
||||
The goal is to...
|
||||
|
||||
|
||||
Specific requirements:
|
||||
- ...
|
||||
- ...
|
||||
@@ -43,7 +43,7 @@ body:
|
||||
label: Acceptance Criteria
|
||||
description: |
|
||||
Define the criteria that must be met for this task to be considered complete.
|
||||
|
||||
|
||||
What are the specific deliverables or outcomes expected?
|
||||
placeholder: |
|
||||
This task will be complete when:
|
||||
@@ -58,15 +58,15 @@ body:
|
||||
label: Context and Background
|
||||
description: |
|
||||
Provide any relevant context, background information, or links to related issues/PRs.
|
||||
|
||||
|
||||
Why is this task needed? What problem does it solve?
|
||||
placeholder: |
|
||||
Background:
|
||||
- ...
|
||||
|
||||
|
||||
Related issues/PRs:
|
||||
- #...
|
||||
|
||||
|
||||
Additional context:
|
||||
- ...
|
||||
validations:
|
||||
@@ -77,15 +77,45 @@ body:
|
||||
label: Dependencies
|
||||
description: |
|
||||
List any dependencies or blockers for this task.
|
||||
|
||||
|
||||
Are there other tasks, issues, or external factors that need to be completed first?
|
||||
placeholder: |
|
||||
This task depends on:
|
||||
- [ ] Issue #...
|
||||
- [ ] PR #...
|
||||
- [ ] External dependency: ...
|
||||
|
||||
|
||||
Blocked by:
|
||||
- ...
|
||||
validations:
|
||||
required: false
|
||||
- type: checkboxes
|
||||
id: package
|
||||
attributes:
|
||||
label: Package (Required)
|
||||
description: |
|
||||
Please select package(s) that this task is related to.
|
||||
options:
|
||||
- label: langchain
|
||||
- label: langchain-openai
|
||||
- label: langchain-anthropic
|
||||
- label: langchain-classic
|
||||
- label: langchain-core
|
||||
- label: langchain-cli
|
||||
- label: langchain-model-profiles
|
||||
- label: langchain-tests
|
||||
- label: langchain-text-splitters
|
||||
- label: langchain-chroma
|
||||
- label: langchain-deepseek
|
||||
- label: langchain-exa
|
||||
- label: langchain-fireworks
|
||||
- label: langchain-groq
|
||||
- label: langchain-huggingface
|
||||
- label: langchain-mistralai
|
||||
- label: langchain-nomic
|
||||
- label: langchain-ollama
|
||||
- label: langchain-perplexity
|
||||
- label: langchain-prompty
|
||||
- label: langchain-qdrant
|
||||
- label: langchain-xai
|
||||
- label: Other / not sure / general
|
||||
|
||||
38
.github/PULL_REQUEST_TEMPLATE.md
vendored
38
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,28 +1,30 @@
|
||||
(Replace this entire block of text)
|
||||
|
||||
Thank you for contributing to LangChain! Follow these steps to mark your pull request as ready for review. **If any of these steps are not completed, your PR will not be considered for review.**
|
||||
Read the full contributing guidelines: https://docs.langchain.com/oss/python/contributing/overview
|
||||
|
||||
Thank you for contributing to LangChain! Follow these steps to have your pull request considered as ready for review.
|
||||
|
||||
1. PR title: Should follow the format: TYPE(SCOPE): DESCRIPTION
|
||||
|
||||
- [ ] **PR title**: Follows the format: {TYPE}({SCOPE}): {DESCRIPTION}
|
||||
- Examples:
|
||||
- fix(anthropic): resolve flag parsing error
|
||||
- feat(core): add multi-tenant support
|
||||
- fix(cli): resolve flag parsing error
|
||||
- docs(openai): update API usage examples
|
||||
- Allowed `{TYPE}` values:
|
||||
- feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert, release
|
||||
- Allowed `{SCOPE}` values (optional):
|
||||
- core, cli, langchain, standard-tests, text-splitters, docs, anthropic, chroma, deepseek, exa, fireworks, groq, huggingface, mistralai, nomic, ollama, openai, perplexity, prompty, qdrant, xai, infra
|
||||
- Once you've written the title, please delete this checklist item; do not include it in the PR.
|
||||
- test(openai): update API usage tests
|
||||
- Allowed TYPE and SCOPE values: https://github.com/langchain-ai/langchain/blob/master/.github/workflows/pr_lint.yml#L15-L33
|
||||
|
||||
- [ ] **PR message**: ***Delete this entire checklist*** and replace with
|
||||
- **Description:** a description of the change. Include a [closing keyword](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) if applicable to a relevant issue.
|
||||
- **Issue:** the issue # it fixes, if applicable (e.g. Fixes #123)
|
||||
- **Dependencies:** any dependencies required for this change
|
||||
2. PR description:
|
||||
|
||||
- [ ] **Lint and test**: Run `make format`, `make lint` and `make test` from the root of the package(s) you've modified. **We will not consider a PR unless these three are passing in CI.** See [contribution guidelines](https://docs.langchain.com/oss/python/contributing) for more.
|
||||
- Write 1-2 sentences summarizing the change.
|
||||
- If this PR addresses a specific issue, please include "Fixes #ISSUE_NUMBER" in the description to automatically close the issue when the PR is merged.
|
||||
- If there are any breaking changes, please clearly describe them.
|
||||
- If this PR depends on another PR being merged first, please include "Depends on #PR_NUMBER" inthe description.
|
||||
|
||||
3. Run `make format`, `make lint` and `make test` from the root of the package(s) you've modified.
|
||||
|
||||
- We will not consider a PR unless these three are passing in CI.
|
||||
|
||||
Additional guidelines:
|
||||
|
||||
- Most PRs should not touch more than one package.
|
||||
- Please do not add dependencies to `pyproject.toml` files (even optional ones) unless they are **required** for unit tests. Likewise, please do not update the `uv.lock` files unless you are adding a required dependency.
|
||||
- Changes should be backwards compatible.
|
||||
- Make sure optional dependencies are imported within a function.
|
||||
- We ask that if you use generative AI for your contribution, you include a disclaimer.
|
||||
- PRs should not touch more than one package unless absolutely necessary.
|
||||
- Do not update the `uv.lock` files unless or add dependencies to `pyproject.toml` files (even optional ones) unless you have explicit permission to do so by a maintainer.
|
||||
|
||||
93
.github/actions/poetry_setup/action.yml
vendored
93
.github/actions/poetry_setup/action.yml
vendored
@@ -1,93 +0,0 @@
|
||||
# An action for setting up poetry install with caching.
|
||||
# Using a custom action since the default action does not
|
||||
# take poetry install groups into account.
|
||||
# Action code from:
|
||||
# https://github.com/actions/setup-python/issues/505#issuecomment-1273013236
|
||||
name: poetry-install-with-caching
|
||||
description: Poetry install with support for caching of dependency groups.
|
||||
|
||||
inputs:
|
||||
python-version:
|
||||
description: Python version, supporting MAJOR.MINOR only
|
||||
required: true
|
||||
|
||||
poetry-version:
|
||||
description: Poetry version
|
||||
required: true
|
||||
|
||||
cache-key:
|
||||
description: Cache key to use for manual handling of caching
|
||||
required: true
|
||||
|
||||
working-directory:
|
||||
description: Directory whose poetry.lock file should be cached
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: actions/setup-python@v5
|
||||
name: Setup python ${{ inputs.python-version }}
|
||||
id: setup-python
|
||||
with:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
|
||||
- uses: actions/cache@v4
|
||||
id: cache-bin-poetry
|
||||
name: Cache Poetry binary - Python ${{ inputs.python-version }}
|
||||
env:
|
||||
SEGMENT_DOWNLOAD_TIMEOUT_MIN: "1"
|
||||
with:
|
||||
path: |
|
||||
/opt/pipx/venvs/poetry
|
||||
# This step caches the poetry installation, so make sure it's keyed on the poetry version as well.
|
||||
key: bin-poetry-${{ runner.os }}-${{ runner.arch }}-py-${{ inputs.python-version }}-${{ inputs.poetry-version }}
|
||||
|
||||
- name: Refresh shell hashtable and fixup softlinks
|
||||
if: steps.cache-bin-poetry.outputs.cache-hit == 'true'
|
||||
shell: bash
|
||||
env:
|
||||
POETRY_VERSION: ${{ inputs.poetry-version }}
|
||||
PYTHON_VERSION: ${{ inputs.python-version }}
|
||||
run: |
|
||||
set -eux
|
||||
|
||||
# Refresh the shell hashtable, to ensure correct `which` output.
|
||||
hash -r
|
||||
|
||||
# `actions/cache@v3` doesn't always seem able to correctly unpack softlinks.
|
||||
# Delete and recreate the softlinks pipx expects to have.
|
||||
rm /opt/pipx/venvs/poetry/bin/python
|
||||
cd /opt/pipx/venvs/poetry/bin
|
||||
ln -s "$(which "python$PYTHON_VERSION")" python
|
||||
chmod +x python
|
||||
cd /opt/pipx_bin/
|
||||
ln -s /opt/pipx/venvs/poetry/bin/poetry poetry
|
||||
chmod +x poetry
|
||||
|
||||
# Ensure everything got set up correctly.
|
||||
/opt/pipx/venvs/poetry/bin/python --version
|
||||
/opt/pipx_bin/poetry --version
|
||||
|
||||
- name: Install poetry
|
||||
if: steps.cache-bin-poetry.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
env:
|
||||
POETRY_VERSION: ${{ inputs.poetry-version }}
|
||||
PYTHON_VERSION: ${{ inputs.python-version }}
|
||||
# Install poetry using the python version installed by setup-python step.
|
||||
run: pipx install "poetry==$POETRY_VERSION" --python '${{ steps.setup-python.outputs.python-path }}' --verbose
|
||||
|
||||
- name: Restore pip and poetry cached dependencies
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
SEGMENT_DOWNLOAD_TIMEOUT_MIN: "4"
|
||||
WORKDIR: ${{ inputs.working-directory == '' && '.' || inputs.working-directory }}
|
||||
with:
|
||||
path: |
|
||||
~/.cache/pip
|
||||
~/.cache/pypoetry/virtualenvs
|
||||
~/.cache/pypoetry/cache
|
||||
~/.cache/pypoetry/artifacts
|
||||
${{ env.WORKDIR }}/.venv
|
||||
key: py-deps-${{ runner.os }}-${{ runner.arch }}-py-${{ inputs.python-version }}-poetry-${{ inputs.poetry-version }}-${{ inputs.cache-key }}-${{ hashFiles(format('{0}/**/poetry.lock', env.WORKDIR)) }}
|
||||
85
.github/pr-file-labeler.yml
vendored
85
.github/pr-file-labeler.yml
vendored
@@ -7,13 +7,12 @@ core:
|
||||
- any-glob-to-any-file:
|
||||
- "libs/core/**/*"
|
||||
|
||||
langchain:
|
||||
langchain-classic:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "libs/langchain/**/*"
|
||||
- "libs/langchain_v1/**/*"
|
||||
|
||||
v1:
|
||||
langchain:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "libs/langchain_v1/**/*"
|
||||
@@ -28,6 +27,11 @@ standard-tests:
|
||||
- any-glob-to-any-file:
|
||||
- "libs/standard-tests/**/*"
|
||||
|
||||
model-profiles:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "libs/model-profiles/**/*"
|
||||
|
||||
text-splitters:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
@@ -39,6 +43,81 @@ integration:
|
||||
- any-glob-to-any-file:
|
||||
- "libs/partners/**/*"
|
||||
|
||||
anthropic:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "libs/partners/anthropic/**/*"
|
||||
|
||||
chroma:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "libs/partners/chroma/**/*"
|
||||
|
||||
deepseek:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "libs/partners/deepseek/**/*"
|
||||
|
||||
exa:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "libs/partners/exa/**/*"
|
||||
|
||||
fireworks:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "libs/partners/fireworks/**/*"
|
||||
|
||||
groq:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "libs/partners/groq/**/*"
|
||||
|
||||
huggingface:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "libs/partners/huggingface/**/*"
|
||||
|
||||
mistralai:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "libs/partners/mistralai/**/*"
|
||||
|
||||
nomic:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "libs/partners/nomic/**/*"
|
||||
|
||||
ollama:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "libs/partners/ollama/**/*"
|
||||
|
||||
openai:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "libs/partners/openai/**/*"
|
||||
|
||||
perplexity:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "libs/partners/perplexity/**/*"
|
||||
|
||||
prompty:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "libs/partners/prompty/**/*"
|
||||
|
||||
qdrant:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "libs/partners/qdrant/**/*"
|
||||
|
||||
xai:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "libs/partners/xai/**/*"
|
||||
|
||||
# Infrastructure and DevOps
|
||||
infra:
|
||||
- changed-files:
|
||||
|
||||
41
.github/pr-title-labeler.yml
vendored
41
.github/pr-title-labeler.yml
vendored
@@ -1,41 +0,0 @@
|
||||
# PR title labeler config
|
||||
#
|
||||
# Labels PRs based on conventional commit patterns in titles
|
||||
#
|
||||
# Format: type(scope): description or type!: description (breaking)
|
||||
|
||||
add-missing-labels: true
|
||||
clear-prexisting: false
|
||||
include-commits: false
|
||||
include-title: true
|
||||
label-for-breaking-changes: breaking
|
||||
|
||||
label-mapping:
|
||||
documentation: ["docs"]
|
||||
feature: ["feat"]
|
||||
fix: ["fix"]
|
||||
infra: ["build", "ci", "chore"]
|
||||
integration:
|
||||
[
|
||||
"anthropic",
|
||||
"chroma",
|
||||
"deepseek",
|
||||
"exa",
|
||||
"fireworks",
|
||||
"groq",
|
||||
"huggingface",
|
||||
"mistralai",
|
||||
"nomic",
|
||||
"ollama",
|
||||
"openai",
|
||||
"perplexity",
|
||||
"prompty",
|
||||
"qdrant",
|
||||
"xai",
|
||||
]
|
||||
linting: ["style"]
|
||||
performance: ["perf"]
|
||||
refactor: ["refactor"]
|
||||
release: ["release"]
|
||||
revert: ["revert"]
|
||||
tests: ["test"]
|
||||
72
.github/scripts/check_diff.py
vendored
72
.github/scripts/check_diff.py
vendored
@@ -25,19 +25,18 @@ import tomllib
|
||||
from get_min_versions import get_min_version_from_toml
|
||||
from packaging.requirements import Requirement
|
||||
|
||||
LANGCHAIN_DIRS = [
|
||||
"libs/core",
|
||||
"libs/text-splitters",
|
||||
"libs/langchain",
|
||||
"libs/langchain_v1",
|
||||
"libs/model-profiles",
|
||||
]
|
||||
# Define explicit dependency relationships for main LangChain packages.
|
||||
# Key = package directory, Value = set of directories that should trigger tests for this package
|
||||
PACKAGE_DEPENDENCIES: Dict[str, Set[str]] = {
|
||||
"libs/core": set(), # core has no upstream dependencies
|
||||
"libs/text-splitters": {"libs/core"},
|
||||
"libs/langchain": {"libs/core", "libs/text-splitters"},
|
||||
"libs/langchain_v1": {"libs/core"},
|
||||
"libs/model-profiles": set(), # model-profiles is independent
|
||||
}
|
||||
|
||||
# When set to True, we are ignoring core dependents
|
||||
# in order to be able to get CI to pass for each individual
|
||||
# package that depends on core
|
||||
# e.g. if you touch core, we don't then add textsplitters/etc to CI
|
||||
IGNORE_CORE_DEPENDENTS = False
|
||||
# All main LangChain directories (order doesn't matter with explicit deps)
|
||||
LANGCHAIN_DIRS = list(PACKAGE_DEPENDENCIES.keys())
|
||||
|
||||
# ignored partners are removed from dependents
|
||||
# but still run if directly edited
|
||||
@@ -61,7 +60,7 @@ def all_package_dirs() -> Set[str]:
|
||||
|
||||
|
||||
def dependents_graph() -> dict:
|
||||
"""Construct a mapping of package -> dependents
|
||||
"""Construct a mapping of package -> dependents.
|
||||
|
||||
Done such that we can run tests on all dependents of a package when a change is made.
|
||||
"""
|
||||
@@ -126,6 +125,25 @@ def add_dependents(dirs_to_eval: Set[str], dependents: dict) -> List[str]:
|
||||
return list(updated)
|
||||
|
||||
|
||||
def get_affected_packages(changed_dir: str) -> Set[str]:
|
||||
"""Get all packages that should be tested when a directory changes.
|
||||
|
||||
Args:
|
||||
changed_dir: The directory that was changed (e.g., "libs/core")
|
||||
|
||||
Returns:
|
||||
Set of package directories that depend on the changed directory
|
||||
"""
|
||||
affected = {changed_dir}
|
||||
|
||||
# Check each package to see if it depends on the changed directory
|
||||
for pkg_dir, dependencies in PACKAGE_DEPENDENCIES.items():
|
||||
if changed_dir in dependencies:
|
||||
affected.add(pkg_dir)
|
||||
|
||||
return affected
|
||||
|
||||
|
||||
def _get_configs_for_single_dir(job: str, dir_: str) -> List[Dict[str, str]]:
|
||||
if job == "test-pydantic":
|
||||
return _get_pydantic_test_configs(dir_)
|
||||
@@ -254,27 +272,23 @@ if __name__ == "__main__":
|
||||
# infrastructure don't inadvertently break package testing, even if the change
|
||||
# appears unrelated (e.g., documentation build workflows). This is intentionally
|
||||
# conservative to catch unexpected side effects from workflow modifications.
|
||||
#
|
||||
# Example: A PR modifying .github/workflows/api_doc_build.yml will trigger
|
||||
# lint/test jobs for libs/core, libs/text-splitters, libs/langchain, and
|
||||
# libs/langchain_v1, even though the workflow may only affect documentation.
|
||||
dirs_to_run["extended-test"].update(LANGCHAIN_DIRS)
|
||||
|
||||
if file.startswith("libs/core"):
|
||||
dirs_to_run["codspeed"].add("libs/core")
|
||||
if any(file.startswith(dir_) for dir_ in LANGCHAIN_DIRS):
|
||||
# add that dir and all dirs after in LANGCHAIN_DIRS
|
||||
# for extended testing
|
||||
|
||||
found = False
|
||||
for dir_ in LANGCHAIN_DIRS:
|
||||
if dir_ == "libs/core" and IGNORE_CORE_DEPENDENTS:
|
||||
dirs_to_run["extended-test"].add(dir_)
|
||||
continue
|
||||
if file.startswith(dir_):
|
||||
found = True
|
||||
if found:
|
||||
dirs_to_run["extended-test"].add(dir_)
|
||||
# Check if file is in one of the main LangChain directories
|
||||
matched_langchain_dir = None
|
||||
for dir_ in LANGCHAIN_DIRS:
|
||||
if file.startswith(dir_):
|
||||
matched_langchain_dir = dir_
|
||||
break
|
||||
|
||||
if matched_langchain_dir:
|
||||
# Add the changed directory and all packages that depend on it
|
||||
affected_packages = get_affected_packages(matched_langchain_dir)
|
||||
dirs_to_run["extended-test"].update(affected_packages)
|
||||
|
||||
elif file.startswith("libs/standard-tests"):
|
||||
# TODO: update to include all packages that rely on standard-tests (all partner packages)
|
||||
# Note: won't run on external repo partners
|
||||
|
||||
2
.github/scripts/get_min_versions.py
vendored
2
.github/scripts/get_min_versions.py
vendored
@@ -98,7 +98,7 @@ def _check_python_version_from_requirement(
|
||||
return True
|
||||
else:
|
||||
marker_str = str(requirement.marker)
|
||||
if "python_version" or "python_full_version" in marker_str:
|
||||
if "python_version" in marker_str or "python_full_version" in marker_str:
|
||||
python_version_str = "".join(
|
||||
char
|
||||
for char in marker_str
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
name: "Python ${{ inputs.python-version }}"
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: "🐍 Set up Python ${{ inputs.python-version }} + UV"
|
||||
uses: "./.github/actions/uv_setup"
|
||||
|
||||
2
.github/workflows/_lint.yml
vendored
2
.github/workflows/_lint.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: "📋 Checkout Code"
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: "🐍 Set up Python ${{ inputs.python-version }} + UV"
|
||||
uses: "./.github/actions/uv_setup"
|
||||
|
||||
16
.github/workflows/_release.yml
vendored
16
.github/workflows/_release.yml
vendored
@@ -54,7 +54,7 @@ jobs:
|
||||
version: ${{ steps.check-version.outputs.version }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python + uv
|
||||
uses: "./.github/actions/uv_setup"
|
||||
@@ -105,7 +105,7 @@ jobs:
|
||||
outputs:
|
||||
release-body: ${{ steps.generate-release-body.outputs.release-body }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
repository: langchain-ai/langchain
|
||||
path: langchain
|
||||
@@ -206,7 +206,7 @@ jobs:
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/download-artifact@v6
|
||||
with:
|
||||
@@ -237,7 +237,7 @@ jobs:
|
||||
contents: read
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
# We explicitly *don't* set up caching here. This ensures our tests are
|
||||
# maximally sensitive to catching breakage.
|
||||
@@ -396,7 +396,7 @@ jobs:
|
||||
contents: read
|
||||
strategy:
|
||||
matrix:
|
||||
partner: [openai]
|
||||
partner: [anthropic]
|
||||
fail-fast: false # Continue testing other partners if one fails
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
@@ -412,7 +412,7 @@ jobs:
|
||||
AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT_NAME: ${{ secrets.AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT_NAME }}
|
||||
LANGCHAIN_TESTS_USER_AGENT: ${{ secrets.LANGCHAIN_TESTS_USER_AGENT }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
# We implement this conditional as Github Actions does not have good support
|
||||
# for conditionally needing steps. https://github.com/actions/runner/issues/491
|
||||
@@ -492,7 +492,7 @@ jobs:
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python + uv
|
||||
uses: "./.github/actions/uv_setup"
|
||||
@@ -532,7 +532,7 @@ jobs:
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python + uv
|
||||
uses: "./.github/actions/uv_setup"
|
||||
|
||||
2
.github/workflows/_test.yml
vendored
2
.github/workflows/_test.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
name: "Python ${{ inputs.python-version }}"
|
||||
steps:
|
||||
- name: "📋 Checkout Code"
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: "🐍 Set up Python ${{ inputs.python-version }} + UV"
|
||||
uses: "./.github/actions/uv_setup"
|
||||
|
||||
2
.github/workflows/_test_pydantic.yml
vendored
2
.github/workflows/_test_pydantic.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
name: "Pydantic ~=${{ inputs.pydantic-version }}"
|
||||
steps:
|
||||
- name: "📋 Checkout Code"
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: "🐍 Set up Python ${{ inputs.python-version }} + UV"
|
||||
uses: "./.github/actions/uv_setup"
|
||||
|
||||
107
.github/workflows/auto-label-by-package.yml
vendored
Normal file
107
.github/workflows/auto-label-by-package.yml
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
name: Auto Label Issues by Package
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited]
|
||||
|
||||
jobs:
|
||||
label-by-package:
|
||||
permissions:
|
||||
issues: write
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Sync package labels
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const body = context.payload.issue.body || "";
|
||||
|
||||
// Extract text under "### Package"
|
||||
const match = body.match(/### Package\s+([\s\S]*?)\n###/i);
|
||||
if (!match) return;
|
||||
|
||||
const packageSection = match[1].trim();
|
||||
|
||||
// Mapping table for package names to labels
|
||||
const mapping = {
|
||||
"langchain": "langchain",
|
||||
"langchain-openai": "openai",
|
||||
"langchain-anthropic": "anthropic",
|
||||
"langchain-classic": "langchain-classic",
|
||||
"langchain-core": "core",
|
||||
"langchain-cli": "cli",
|
||||
"langchain-model-profiles": "model-profiles",
|
||||
"langchain-tests": "standard-tests",
|
||||
"langchain-text-splitters": "text-splitters",
|
||||
"langchain-chroma": "chroma",
|
||||
"langchain-deepseek": "deepseek",
|
||||
"langchain-exa": "exa",
|
||||
"langchain-fireworks": "fireworks",
|
||||
"langchain-groq": "groq",
|
||||
"langchain-huggingface": "huggingface",
|
||||
"langchain-mistralai": "mistralai",
|
||||
"langchain-nomic": "nomic",
|
||||
"langchain-ollama": "ollama",
|
||||
"langchain-perplexity": "perplexity",
|
||||
"langchain-prompty": "prompty",
|
||||
"langchain-qdrant": "qdrant",
|
||||
"langchain-xai": "xai",
|
||||
};
|
||||
|
||||
// All possible package labels we manage
|
||||
const allPackageLabels = Object.values(mapping);
|
||||
const selectedLabels = [];
|
||||
|
||||
// Check if this is checkbox format (multiple selection)
|
||||
const checkboxMatches = packageSection.match(/- \[x\]\s+([^\n\r]+)/gi);
|
||||
if (checkboxMatches) {
|
||||
// Handle checkbox format
|
||||
for (const match of checkboxMatches) {
|
||||
const packageName = match.replace(/- \[x\]\s+/i, '').trim();
|
||||
const label = mapping[packageName];
|
||||
if (label && !selectedLabels.includes(label)) {
|
||||
selectedLabels.push(label);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Handle dropdown format (single selection)
|
||||
const label = mapping[packageSection];
|
||||
if (label) {
|
||||
selectedLabels.push(label);
|
||||
}
|
||||
}
|
||||
|
||||
// Get current issue labels
|
||||
const issue = await github.rest.issues.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number
|
||||
});
|
||||
|
||||
const currentLabels = issue.data.labels.map(label => label.name);
|
||||
const currentPackageLabels = currentLabels.filter(label => allPackageLabels.includes(label));
|
||||
|
||||
// Determine labels to add and remove
|
||||
const labelsToAdd = selectedLabels.filter(label => !currentPackageLabels.includes(label));
|
||||
const labelsToRemove = currentPackageLabels.filter(label => !selectedLabels.includes(label));
|
||||
|
||||
// Add new labels
|
||||
if (labelsToAdd.length > 0) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
labels: labelsToAdd
|
||||
});
|
||||
}
|
||||
|
||||
// Remove old labels
|
||||
for (const label of labelsToRemove) {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
name: label
|
||||
});
|
||||
}
|
||||
2
.github/workflows/check_core_versions.yml
vendored
2
.github/workflows/check_core_versions.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: "✅ Verify pyproject.toml & version.py Match"
|
||||
run: |
|
||||
|
||||
6
.github/workflows/check_diffs.yml
vendored
6
.github/workflows/check_diffs.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'ci-ignore') }}
|
||||
steps:
|
||||
- name: "📋 Checkout Code"
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
- name: "🐍 Setup Python 3.11"
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
@@ -141,7 +141,7 @@ jobs:
|
||||
run:
|
||||
working-directory: ${{ matrix.job-configs.working-directory }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: "🐍 Set up Python ${{ matrix.job-configs.python-version }} + UV"
|
||||
uses: "./.github/actions/uv_setup"
|
||||
@@ -182,7 +182,7 @@ jobs:
|
||||
job-configs: ${{ fromJson(needs.build.outputs.codspeed) }}
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: "📦 Install UV Package Manager"
|
||||
uses: astral-sh/setup-uv@v7
|
||||
|
||||
6
.github/workflows/integration_tests.yml
vendored
6
.github/workflows/integration_tests.yml
vendored
@@ -71,14 +71,14 @@ jobs:
|
||||
working-directory: ${{ fromJSON(needs.compute-matrix.outputs.matrix).working-directory }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
path: langchain
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
repository: langchain-ai/langchain-google
|
||||
path: langchain-google
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
repository: langchain-ai/langchain-aws
|
||||
path: langchain-aws
|
||||
|
||||
14
.github/workflows/pr_lint.yml
vendored
14
.github/workflows/pr_lint.yml
vendored
@@ -26,11 +26,13 @@
|
||||
# * revert — reverts a previous commit
|
||||
# * release — prepare a new release
|
||||
#
|
||||
# Allowed Scopes (optional):
|
||||
# core, cli, langchain, langchain_v1, langchain-classic, standard-tests,
|
||||
# text-splitters, docs, anthropic, chroma, deepseek, exa, fireworks, groq,
|
||||
# huggingface, mistralai, nomic, ollama, openai, perplexity, prompty, qdrant,
|
||||
# xai, infra, deps
|
||||
# Allowed Scope(s) (optional):
|
||||
# core, cli, langchain, langchain_v1, langchain-classic, model-profiles,
|
||||
# standard-tests, text-splitters, docs, anthropic, chroma, deepseek, exa,
|
||||
# fireworks, groq, huggingface, mistralai, nomic, ollama, openai,
|
||||
# perplexity, prompty, qdrant, xai, infra, deps
|
||||
#
|
||||
# Multiple scopes can be used by separating them with a comma.
|
||||
#
|
||||
# Rules:
|
||||
# 1. The 'Type' must start with a lowercase letter.
|
||||
@@ -79,7 +81,6 @@ jobs:
|
||||
core
|
||||
cli
|
||||
langchain
|
||||
langchain_v1
|
||||
langchain-classic
|
||||
model-profiles
|
||||
standard-tests
|
||||
@@ -101,6 +102,7 @@ jobs:
|
||||
qdrant
|
||||
xai
|
||||
infra
|
||||
deps
|
||||
requireScope: false
|
||||
disallowScopes: |
|
||||
release
|
||||
|
||||
4
.github/workflows/v03_api_doc_build.yml
vendored
4
.github/workflows/v03_api_doc_build.yml
vendored
@@ -23,12 +23,12 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: v0.3
|
||||
path: langchain
|
||||
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
repository: langchain-ai/langchain-api-docs-html
|
||||
path: langchain-api-docs-html
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -163,3 +163,6 @@ node_modules
|
||||
|
||||
prof
|
||||
virtualenv/
|
||||
scratch/
|
||||
|
||||
.langgraph_api/
|
||||
|
||||
405
AGENTS.md
405
AGENTS.md
@@ -1,255 +1,58 @@
|
||||
# Global Development Guidelines for LangChain Projects
|
||||
# Global development guidelines for the LangChain monorepo
|
||||
|
||||
## Core Development Principles
|
||||
This document provides context to understand the LangChain Python project and assist with development.
|
||||
|
||||
### 1. Maintain Stable Public Interfaces ⚠️ CRITICAL
|
||||
## Project architecture and context
|
||||
|
||||
**Always attempt to preserve function signatures, argument positions, and names for exported/public methods.**
|
||||
### Monorepo structure
|
||||
|
||||
❌ **Bad - Breaking Change:**
|
||||
This is a Python monorepo with multiple independently versioned packages that use `uv`.
|
||||
|
||||
```python
|
||||
def get_user(id, verbose=False): # Changed from `user_id`
|
||||
pass
|
||||
```txt
|
||||
langchain/
|
||||
├── libs/
|
||||
│ ├── core/ # `langchain-core` primitives and base abstractions
|
||||
│ ├── langchain/ # `langchain-classic` (legacy, no new features)
|
||||
│ ├── langchain_v1/ # Actively maintained `langchain` package
|
||||
│ ├── partners/ # Third-party integrations
|
||||
│ │ ├── openai/ # OpenAI models and embeddings
|
||||
│ │ ├── anthropic/ # Anthropic (Claude) integration
|
||||
│ │ ├── ollama/ # Local model support
|
||||
│ │ └── ... (other integrations maintained by the LangChain team)
|
||||
│ ├── text-splitters/ # Document chunking utilities
|
||||
│ ├── standard-tests/ # Shared test suite for integrations
|
||||
│ ├── model-profiles/ # Model configuration profiles
|
||||
│ └── cli/ # Command-line interface tools
|
||||
├── .github/ # CI/CD workflows and templates
|
||||
├── .vscode/ # VSCode IDE standard settings and recommended extensions
|
||||
└── README.md # Information about LangChain
|
||||
```
|
||||
|
||||
✅ **Good - Stable Interface:**
|
||||
- **Core layer** (`langchain-core`): Base abstractions, interfaces, and protocols. Users should not need to know about this layer directly.
|
||||
- **Implementation layer** (`langchain`): Concrete implementations and high-level public utilities
|
||||
- **Integration layer** (`partners/`): Third-party service integrations. Note that this monorepo is not exhaustive of all LangChain integrations; some are maintained in separate repos, such as `langchain-ai/langchain-google` and `langchain-ai/langchain-aws`. Usually these repos are cloned at the same level as this monorepo, so if needed, you can refer to their code directly by navigating to `../langchain-google/` from this monorepo.
|
||||
- **Testing layer** (`standard-tests/`): Standardized integration tests for partner integrations
|
||||
|
||||
```python
|
||||
def get_user(user_id: str, verbose: bool = False) -> User:
|
||||
"""Retrieve user by ID with optional verbose output."""
|
||||
pass
|
||||
```
|
||||
### Development tools & commands**
|
||||
|
||||
**Before making ANY changes to public APIs:**
|
||||
- `uv` – Fast Python package installer and resolver (replaces pip/poetry)
|
||||
- `make` – Task runner for common development commands. Feel free to look at the `Makefile` for available commands and usage patterns.
|
||||
- `ruff` – Fast Python linter and formatter
|
||||
- `mypy` – Static type checking
|
||||
- `pytest` – Testing framework
|
||||
|
||||
- Check if the function/class is exported in `__init__.py`
|
||||
- Look for existing usage patterns in tests and examples
|
||||
- Use keyword-only arguments for new parameters: `*, new_param: str = "default"`
|
||||
- Mark experimental features clearly with docstring warnings (using MkDocs Material admonitions, like `!!! warning`)
|
||||
This monorepo uses `uv` for dependency management. Local development uses editable installs: `[tool.uv.sources]`
|
||||
|
||||
🧠 *Ask yourself:* "Would this change break someone's code if they used it last week?"
|
||||
|
||||
### 2. Code Quality Standards
|
||||
|
||||
**All Python code MUST include type hints and return types.**
|
||||
|
||||
❌ **Bad:**
|
||||
|
||||
```python
|
||||
def p(u, d):
|
||||
return [x for x in u if x not in d]
|
||||
```
|
||||
|
||||
✅ **Good:**
|
||||
|
||||
```python
|
||||
def filter_unknown_users(users: list[str], known_users: set[str]) -> list[str]:
|
||||
"""Filter out users that are not in the known users set.
|
||||
|
||||
Args:
|
||||
users: List of user identifiers to filter.
|
||||
known_users: Set of known/valid user identifiers.
|
||||
|
||||
Returns:
|
||||
List of users that are not in the known_users set.
|
||||
"""
|
||||
return [user for user in users if user not in known_users]
|
||||
```
|
||||
|
||||
**Style Requirements:**
|
||||
|
||||
- Use descriptive, **self-explanatory variable names**. Avoid overly short or cryptic identifiers.
|
||||
- Attempt to break up complex functions (>20 lines) into smaller, focused functions where it makes sense
|
||||
- Avoid unnecessary abstraction or premature optimization
|
||||
- Follow existing patterns in the codebase you're modifying
|
||||
|
||||
### 3. Testing Requirements
|
||||
|
||||
**Every new feature or bugfix MUST be covered by unit tests.**
|
||||
|
||||
**Test Organization:**
|
||||
|
||||
- Unit tests: `tests/unit_tests/` (no network calls allowed)
|
||||
- Integration tests: `tests/integration_tests/` (network calls permitted)
|
||||
- Use `pytest` as the testing framework
|
||||
|
||||
**Test Quality Checklist:**
|
||||
|
||||
- [ ] Tests fail when your new logic is broken
|
||||
- [ ] Happy path is covered
|
||||
- [ ] Edge cases and error conditions are tested
|
||||
- [ ] Use fixtures/mocks for external dependencies
|
||||
- [ ] Tests are deterministic (no flaky tests)
|
||||
|
||||
Checklist questions:
|
||||
|
||||
- [ ] Does the test suite fail if your new logic is broken?
|
||||
- [ ] Are all expected behaviors exercised (happy path, invalid input, etc)?
|
||||
- [ ] Do tests use fixtures or mocks where needed?
|
||||
|
||||
```python
|
||||
def test_filter_unknown_users():
|
||||
"""Test filtering unknown users from a list."""
|
||||
users = ["alice", "bob", "charlie"]
|
||||
known_users = {"alice", "bob"}
|
||||
|
||||
result = filter_unknown_users(users, known_users)
|
||||
|
||||
assert result == ["charlie"]
|
||||
assert len(result) == 1
|
||||
```
|
||||
|
||||
### 4. Security and Risk Assessment
|
||||
|
||||
**Security Checklist:**
|
||||
|
||||
- No `eval()`, `exec()`, or `pickle` on user-controlled input
|
||||
- Proper exception handling (no bare `except:`) and use a `msg` variable for error messages
|
||||
- Remove unreachable/commented code before committing
|
||||
- Race conditions or resource leaks (file handles, sockets, threads).
|
||||
- Ensure proper resource cleanup (file handles, connections)
|
||||
|
||||
❌ **Bad:**
|
||||
|
||||
```python
|
||||
def load_config(path):
|
||||
with open(path) as f:
|
||||
return eval(f.read()) # ⚠️ Never eval config
|
||||
```
|
||||
|
||||
✅ **Good:**
|
||||
|
||||
```python
|
||||
import json
|
||||
|
||||
def load_config(path: str) -> dict:
|
||||
with open(path) as f:
|
||||
return json.load(f)
|
||||
```
|
||||
|
||||
### 5. Documentation Standards
|
||||
|
||||
**Use Google-style docstrings with Args section for all public functions.**
|
||||
|
||||
❌ **Insufficient Documentation:**
|
||||
|
||||
```python
|
||||
def send_email(to, msg):
|
||||
"""Send an email to a recipient."""
|
||||
```
|
||||
|
||||
✅ **Complete Documentation:**
|
||||
|
||||
```python
|
||||
def send_email(to: str, msg: str, *, priority: str = "normal") -> bool:
|
||||
"""
|
||||
Send an email to a recipient with specified priority.
|
||||
|
||||
Args:
|
||||
to: The email address of the recipient.
|
||||
msg: The message body to send.
|
||||
priority: Email priority level (`'low'`, `'normal'`, `'high'`).
|
||||
|
||||
Returns:
|
||||
`True` if email was sent successfully, `False` otherwise.
|
||||
|
||||
Raises:
|
||||
`InvalidEmailError`: If the email address format is invalid.
|
||||
`SMTPConnectionError`: If unable to connect to email server.
|
||||
"""
|
||||
```
|
||||
|
||||
**Documentation Guidelines:**
|
||||
|
||||
- Types go in function signatures, NOT in docstrings
|
||||
- If a default is present, DO NOT repeat it in the docstring unless there is post-processing or it is set conditionally.
|
||||
- Focus on "why" rather than "what" in descriptions
|
||||
- Document all parameters, return values, and exceptions
|
||||
- Keep descriptions concise but clear
|
||||
- Ensure American English spelling (e.g., "behavior", not "behaviour")
|
||||
|
||||
📌 *Tip:* Keep descriptions concise but clear. Only document return values if non-obvious.
|
||||
|
||||
### 6. Architectural Improvements
|
||||
|
||||
**When you encounter code that could be improved, suggest better designs:**
|
||||
|
||||
❌ **Poor Design:**
|
||||
|
||||
```python
|
||||
def process_data(data, db_conn, email_client, logger):
|
||||
# Function doing too many things
|
||||
validated = validate_data(data)
|
||||
result = db_conn.save(validated)
|
||||
email_client.send_notification(result)
|
||||
logger.log(f"Processed {len(data)} items")
|
||||
return result
|
||||
```
|
||||
|
||||
✅ **Better Design:**
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class ProcessingResult:
|
||||
"""Result of data processing operation."""
|
||||
items_processed: int
|
||||
success: bool
|
||||
errors: List[str] = field(default_factory=list)
|
||||
|
||||
class DataProcessor:
|
||||
"""Handles data validation, storage, and notification."""
|
||||
|
||||
def __init__(self, db_conn: Database, email_client: EmailClient):
|
||||
self.db = db_conn
|
||||
self.email = email_client
|
||||
|
||||
def process(self, data: List[dict]) -> ProcessingResult:
|
||||
"""Process and store data with notifications."""
|
||||
validated = self._validate_data(data)
|
||||
result = self.db.save(validated)
|
||||
self._notify_completion(result)
|
||||
return result
|
||||
```
|
||||
|
||||
**Design Improvement Areas:**
|
||||
|
||||
If there's a **cleaner**, **more scalable**, or **simpler** design, highlight it and suggest improvements that would:
|
||||
|
||||
- Reduce code duplication through shared utilities
|
||||
- Make unit testing easier
|
||||
- Improve separation of concerns (single responsibility)
|
||||
- Make unit testing easier through dependency injection
|
||||
- Add clarity without adding complexity
|
||||
- Prefer dataclasses for structured data
|
||||
|
||||
## Development Tools & Commands
|
||||
|
||||
### Package Management
|
||||
|
||||
```bash
|
||||
# Add package
|
||||
uv add package-name
|
||||
|
||||
# Sync project dependencies
|
||||
uv sync
|
||||
uv lock
|
||||
```
|
||||
|
||||
### Testing
|
||||
Each package in `libs/` has its own `pyproject.toml` and `uv.lock`.
|
||||
|
||||
```bash
|
||||
# Run unit tests (no network)
|
||||
make test
|
||||
|
||||
# Don't run integration tests, as API keys must be set
|
||||
|
||||
# Run specific test file
|
||||
uv run --group test pytest tests/unit_tests/test_specific.py
|
||||
```
|
||||
|
||||
### Code Quality
|
||||
|
||||
```bash
|
||||
# Lint code
|
||||
make lint
|
||||
@@ -261,66 +64,118 @@ make format
|
||||
uv run --group lint mypy .
|
||||
```
|
||||
|
||||
### Dependency Management Patterns
|
||||
#### Key config files
|
||||
|
||||
**Local Development Dependencies:**
|
||||
- pyproject.toml: Main workspace configuration with dependency groups
|
||||
- uv.lock: Locked dependencies for reproducible builds
|
||||
- Makefile: Development tasks
|
||||
|
||||
```toml
|
||||
[tool.uv.sources]
|
||||
langchain-core = { path = "../core", editable = true }
|
||||
langchain-tests = { path = "../standard-tests", editable = true }
|
||||
```
|
||||
#### Commit standards
|
||||
|
||||
**For tools, use the `@tool` decorator from `langchain_core.tools`:**
|
||||
Suggest PR titles that follow Conventional Commits format. Refer to .github/workflows/pr_lint for allowed types and scopes.
|
||||
|
||||
```python
|
||||
from langchain_core.tools import tool
|
||||
#### Pull request guidelines
|
||||
|
||||
@tool
|
||||
def search_database(query: str) -> str:
|
||||
"""Search the database for relevant information.
|
||||
- Always add a disclaimer to the PR description mentioning how AI agents are involved with the contribution.
|
||||
- Describe the "why" of the changes, why the proposed solution is the right one. Limit prose.
|
||||
- Highlight areas of the proposed changes that require careful review.
|
||||
|
||||
## Core development principles
|
||||
|
||||
### Maintain stable public interfaces
|
||||
|
||||
CRITICAL: Always attempt to preserve function signatures, argument positions, and names for exported/public methods. Do not make breaking changes.
|
||||
|
||||
**Before making ANY changes to public APIs:**
|
||||
|
||||
- Check if the function/class is exported in `__init__.py`
|
||||
- Look for existing usage patterns in tests and examples
|
||||
- Use keyword-only arguments for new parameters: `*, new_param: str = "default"`
|
||||
- Mark experimental features clearly with docstring warnings (using MkDocs Material admonitions, like `!!! warning`)
|
||||
|
||||
Ask: "Would this change break someone's code if they used it last week?"
|
||||
|
||||
### Code quality standards
|
||||
|
||||
All Python code MUST include type hints and return types.
|
||||
|
||||
```python title="Example"
|
||||
def filter_unknown_users(users: list[str], known_users: set[str]) -> list[str]:
|
||||
"""Single line description of the function.
|
||||
|
||||
Any additional context about the function can go here.
|
||||
|
||||
Args:
|
||||
query: The search query string.
|
||||
users: List of user identifiers to filter.
|
||||
known_users: Set of known/valid user identifiers.
|
||||
|
||||
Returns:
|
||||
List of users that are not in the known_users set.
|
||||
"""
|
||||
# Implementation here
|
||||
return results
|
||||
```
|
||||
|
||||
## Commit Standards
|
||||
- Use descriptive, self-explanatory variable names.
|
||||
- Follow existing patterns in the codebase you're modifying
|
||||
- Attempt to break up complex functions (>20 lines) into smaller, focused functions where it makes sense
|
||||
|
||||
**Use Conventional Commits format for PR titles:**
|
||||
### Testing requirements
|
||||
|
||||
- `feat(core): add multi-tenant support`
|
||||
- `fix(cli): resolve flag parsing error`
|
||||
- `docs: update API usage examples`
|
||||
- `docs(openai): update API usage examples`
|
||||
Every new feature or bugfix MUST be covered by unit tests.
|
||||
|
||||
## Framework-Specific Guidelines
|
||||
- Unit tests: `tests/unit_tests/` (no network calls allowed)
|
||||
- Integration tests: `tests/integration_tests/` (network calls permitted)
|
||||
- We use `pytest` as the testing framework; if in doubt, check other existing tests for examples.
|
||||
- The testing file structure should mirror the source code structure.
|
||||
|
||||
- Follow the existing patterns in `langchain-core` for base abstractions
|
||||
- Use `langchain_core.callbacks` for execution tracking
|
||||
- Implement proper streaming support where applicable
|
||||
- Avoid deprecated components like legacy `LLMChain`
|
||||
**Checklist:**
|
||||
|
||||
### Partner Integrations
|
||||
- [ ] Tests fail when your new logic is broken
|
||||
- [ ] Happy path is covered
|
||||
- [ ] Edge cases and error conditions are tested
|
||||
- [ ] Use fixtures/mocks for external dependencies
|
||||
- [ ] Tests are deterministic (no flaky tests)
|
||||
- [ ] Does the test suite fail if your new logic is broken?
|
||||
|
||||
- Follow the established patterns in existing partner libraries
|
||||
- Implement standard interfaces (`BaseChatModel`, `BaseEmbeddings`, etc.)
|
||||
- Include comprehensive integration tests
|
||||
- Document API key requirements and authentication
|
||||
### Security and risk assessment
|
||||
|
||||
---
|
||||
- No `eval()`, `exec()`, or `pickle` on user-controlled input
|
||||
- Proper exception handling (no bare `except:`) and use a `msg` variable for error messages
|
||||
- Remove unreachable/commented code before committing
|
||||
- Race conditions or resource leaks (file handles, sockets, threads).
|
||||
- Ensure proper resource cleanup (file handles, connections)
|
||||
|
||||
## Quick Reference Checklist
|
||||
### Documentation standards
|
||||
|
||||
Before submitting code changes:
|
||||
Use Google-style docstrings with Args section for all public functions.
|
||||
|
||||
- [ ] **Breaking Changes**: Verified no public API changes
|
||||
- [ ] **Type Hints**: All functions have complete type annotations
|
||||
- [ ] **Tests**: New functionality is fully tested
|
||||
- [ ] **Security**: No dangerous patterns (eval, silent failures, etc.)
|
||||
- [ ] **Documentation**: Google-style docstrings for public functions
|
||||
- [ ] **Code Quality**: `make lint` and `make format` pass
|
||||
- [ ] **Architecture**: Suggested improvements where applicable
|
||||
- [ ] **Commit Message**: Follows Conventional Commits format
|
||||
```python title="Example"
|
||||
def send_email(to: str, msg: str, *, priority: str = "normal") -> bool:
|
||||
"""Send an email to a recipient with specified priority.
|
||||
|
||||
Any additional context about the function can go here.
|
||||
|
||||
Args:
|
||||
to: The email address of the recipient.
|
||||
msg: The message body to send.
|
||||
priority: Email priority level.
|
||||
|
||||
Returns:
|
||||
`True` if email was sent successfully, `False` otherwise.
|
||||
|
||||
Raises:
|
||||
InvalidEmailError: If the email address format is invalid.
|
||||
SMTPConnectionError: If unable to connect to email server.
|
||||
"""
|
||||
```
|
||||
|
||||
- Types go in function signatures, NOT in docstrings
|
||||
- If a default is present, DO NOT repeat it in the docstring unless there is post-processing or it is set conditionally.
|
||||
- Focus on "why" rather than "what" in descriptions
|
||||
- Document all parameters, return values, and exceptions
|
||||
- Keep descriptions concise but clear
|
||||
- Ensure American English spelling (e.g., "behavior", not "behaviour")
|
||||
|
||||
## Additional resources
|
||||
|
||||
- **Documentation:** https://docs.langchain.com/oss/python/langchain/overview and source at https://github.com/langchain-ai/docs or `../docs/`. Prefer the local install and use file search tools for best results. If needed, use the docs MCP server as defined in `.mcp.json` for programmatic access.
|
||||
- **Contributing Guide:** [`.github/CONTRIBUTING.md`](https://docs.langchain.com/oss/python/contributing/overview)
|
||||
|
||||
405
CLAUDE.md
405
CLAUDE.md
@@ -1,255 +1,58 @@
|
||||
# Global Development Guidelines for LangChain Projects
|
||||
# Global development guidelines for the LangChain monorepo
|
||||
|
||||
## Core Development Principles
|
||||
This document provides context to understand the LangChain Python project and assist with development.
|
||||
|
||||
### 1. Maintain Stable Public Interfaces ⚠️ CRITICAL
|
||||
## Project architecture and context
|
||||
|
||||
**Always attempt to preserve function signatures, argument positions, and names for exported/public methods.**
|
||||
### Monorepo structure
|
||||
|
||||
❌ **Bad - Breaking Change:**
|
||||
This is a Python monorepo with multiple independently versioned packages that use `uv`.
|
||||
|
||||
```python
|
||||
def get_user(id, verbose=False): # Changed from `user_id`
|
||||
pass
|
||||
```txt
|
||||
langchain/
|
||||
├── libs/
|
||||
│ ├── core/ # `langchain-core` primitives and base abstractions
|
||||
│ ├── langchain/ # `langchain-classic` (legacy, no new features)
|
||||
│ ├── langchain_v1/ # Actively maintained `langchain` package
|
||||
│ ├── partners/ # Third-party integrations
|
||||
│ │ ├── openai/ # OpenAI models and embeddings
|
||||
│ │ ├── anthropic/ # Anthropic (Claude) integration
|
||||
│ │ ├── ollama/ # Local model support
|
||||
│ │ └── ... (other integrations maintained by the LangChain team)
|
||||
│ ├── text-splitters/ # Document chunking utilities
|
||||
│ ├── standard-tests/ # Shared test suite for integrations
|
||||
│ ├── model-profiles/ # Model configuration profiles
|
||||
│ └── cli/ # Command-line interface tools
|
||||
├── .github/ # CI/CD workflows and templates
|
||||
├── .vscode/ # VSCode IDE standard settings and recommended extensions
|
||||
└── README.md # Information about LangChain
|
||||
```
|
||||
|
||||
✅ **Good - Stable Interface:**
|
||||
- **Core layer** (`langchain-core`): Base abstractions, interfaces, and protocols. Users should not need to know about this layer directly.
|
||||
- **Implementation layer** (`langchain`): Concrete implementations and high-level public utilities
|
||||
- **Integration layer** (`partners/`): Third-party service integrations. Note that this monorepo is not exhaustive of all LangChain integrations; some are maintained in separate repos, such as `langchain-ai/langchain-google` and `langchain-ai/langchain-aws`. Usually these repos are cloned at the same level as this monorepo, so if needed, you can refer to their code directly by navigating to `../langchain-google/` from this monorepo.
|
||||
- **Testing layer** (`standard-tests/`): Standardized integration tests for partner integrations
|
||||
|
||||
```python
|
||||
def get_user(user_id: str, verbose: bool = False) -> User:
|
||||
"""Retrieve user by ID with optional verbose output."""
|
||||
pass
|
||||
```
|
||||
### Development tools & commands**
|
||||
|
||||
**Before making ANY changes to public APIs:**
|
||||
- `uv` – Fast Python package installer and resolver (replaces pip/poetry)
|
||||
- `make` – Task runner for common development commands. Feel free to look at the `Makefile` for available commands and usage patterns.
|
||||
- `ruff` – Fast Python linter and formatter
|
||||
- `mypy` – Static type checking
|
||||
- `pytest` – Testing framework
|
||||
|
||||
- Check if the function/class is exported in `__init__.py`
|
||||
- Look for existing usage patterns in tests and examples
|
||||
- Use keyword-only arguments for new parameters: `*, new_param: str = "default"`
|
||||
- Mark experimental features clearly with docstring warnings (using MkDocs Material admonitions, like `!!! warning`)
|
||||
This monorepo uses `uv` for dependency management. Local development uses editable installs: `[tool.uv.sources]`
|
||||
|
||||
🧠 *Ask yourself:* "Would this change break someone's code if they used it last week?"
|
||||
|
||||
### 2. Code Quality Standards
|
||||
|
||||
**All Python code MUST include type hints and return types.**
|
||||
|
||||
❌ **Bad:**
|
||||
|
||||
```python
|
||||
def p(u, d):
|
||||
return [x for x in u if x not in d]
|
||||
```
|
||||
|
||||
✅ **Good:**
|
||||
|
||||
```python
|
||||
def filter_unknown_users(users: list[str], known_users: set[str]) -> list[str]:
|
||||
"""Filter out users that are not in the known users set.
|
||||
|
||||
Args:
|
||||
users: List of user identifiers to filter.
|
||||
known_users: Set of known/valid user identifiers.
|
||||
|
||||
Returns:
|
||||
List of users that are not in the known_users set.
|
||||
"""
|
||||
return [user for user in users if user not in known_users]
|
||||
```
|
||||
|
||||
**Style Requirements:**
|
||||
|
||||
- Use descriptive, **self-explanatory variable names**. Avoid overly short or cryptic identifiers.
|
||||
- Attempt to break up complex functions (>20 lines) into smaller, focused functions where it makes sense
|
||||
- Avoid unnecessary abstraction or premature optimization
|
||||
- Follow existing patterns in the codebase you're modifying
|
||||
|
||||
### 3. Testing Requirements
|
||||
|
||||
**Every new feature or bugfix MUST be covered by unit tests.**
|
||||
|
||||
**Test Organization:**
|
||||
|
||||
- Unit tests: `tests/unit_tests/` (no network calls allowed)
|
||||
- Integration tests: `tests/integration_tests/` (network calls permitted)
|
||||
- Use `pytest` as the testing framework
|
||||
|
||||
**Test Quality Checklist:**
|
||||
|
||||
- [ ] Tests fail when your new logic is broken
|
||||
- [ ] Happy path is covered
|
||||
- [ ] Edge cases and error conditions are tested
|
||||
- [ ] Use fixtures/mocks for external dependencies
|
||||
- [ ] Tests are deterministic (no flaky tests)
|
||||
|
||||
Checklist questions:
|
||||
|
||||
- [ ] Does the test suite fail if your new logic is broken?
|
||||
- [ ] Are all expected behaviors exercised (happy path, invalid input, etc)?
|
||||
- [ ] Do tests use fixtures or mocks where needed?
|
||||
|
||||
```python
|
||||
def test_filter_unknown_users():
|
||||
"""Test filtering unknown users from a list."""
|
||||
users = ["alice", "bob", "charlie"]
|
||||
known_users = {"alice", "bob"}
|
||||
|
||||
result = filter_unknown_users(users, known_users)
|
||||
|
||||
assert result == ["charlie"]
|
||||
assert len(result) == 1
|
||||
```
|
||||
|
||||
### 4. Security and Risk Assessment
|
||||
|
||||
**Security Checklist:**
|
||||
|
||||
- No `eval()`, `exec()`, or `pickle` on user-controlled input
|
||||
- Proper exception handling (no bare `except:`) and use a `msg` variable for error messages
|
||||
- Remove unreachable/commented code before committing
|
||||
- Race conditions or resource leaks (file handles, sockets, threads).
|
||||
- Ensure proper resource cleanup (file handles, connections)
|
||||
|
||||
❌ **Bad:**
|
||||
|
||||
```python
|
||||
def load_config(path):
|
||||
with open(path) as f:
|
||||
return eval(f.read()) # ⚠️ Never eval config
|
||||
```
|
||||
|
||||
✅ **Good:**
|
||||
|
||||
```python
|
||||
import json
|
||||
|
||||
def load_config(path: str) -> dict:
|
||||
with open(path) as f:
|
||||
return json.load(f)
|
||||
```
|
||||
|
||||
### 5. Documentation Standards
|
||||
|
||||
**Use Google-style docstrings with Args section for all public functions.**
|
||||
|
||||
❌ **Insufficient Documentation:**
|
||||
|
||||
```python
|
||||
def send_email(to, msg):
|
||||
"""Send an email to a recipient."""
|
||||
```
|
||||
|
||||
✅ **Complete Documentation:**
|
||||
|
||||
```python
|
||||
def send_email(to: str, msg: str, *, priority: str = "normal") -> bool:
|
||||
"""
|
||||
Send an email to a recipient with specified priority.
|
||||
|
||||
Args:
|
||||
to: The email address of the recipient.
|
||||
msg: The message body to send.
|
||||
priority: Email priority level (`'low'`, `'normal'`, `'high'`).
|
||||
|
||||
Returns:
|
||||
`True` if email was sent successfully, `False` otherwise.
|
||||
|
||||
Raises:
|
||||
`InvalidEmailError`: If the email address format is invalid.
|
||||
`SMTPConnectionError`: If unable to connect to email server.
|
||||
"""
|
||||
```
|
||||
|
||||
**Documentation Guidelines:**
|
||||
|
||||
- Types go in function signatures, NOT in docstrings
|
||||
- If a default is present, DO NOT repeat it in the docstring unless there is post-processing or it is set conditionally.
|
||||
- Focus on "why" rather than "what" in descriptions
|
||||
- Document all parameters, return values, and exceptions
|
||||
- Keep descriptions concise but clear
|
||||
- Ensure American English spelling (e.g., "behavior", not "behaviour")
|
||||
|
||||
📌 *Tip:* Keep descriptions concise but clear. Only document return values if non-obvious.
|
||||
|
||||
### 6. Architectural Improvements
|
||||
|
||||
**When you encounter code that could be improved, suggest better designs:**
|
||||
|
||||
❌ **Poor Design:**
|
||||
|
||||
```python
|
||||
def process_data(data, db_conn, email_client, logger):
|
||||
# Function doing too many things
|
||||
validated = validate_data(data)
|
||||
result = db_conn.save(validated)
|
||||
email_client.send_notification(result)
|
||||
logger.log(f"Processed {len(data)} items")
|
||||
return result
|
||||
```
|
||||
|
||||
✅ **Better Design:**
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class ProcessingResult:
|
||||
"""Result of data processing operation."""
|
||||
items_processed: int
|
||||
success: bool
|
||||
errors: List[str] = field(default_factory=list)
|
||||
|
||||
class DataProcessor:
|
||||
"""Handles data validation, storage, and notification."""
|
||||
|
||||
def __init__(self, db_conn: Database, email_client: EmailClient):
|
||||
self.db = db_conn
|
||||
self.email = email_client
|
||||
|
||||
def process(self, data: List[dict]) -> ProcessingResult:
|
||||
"""Process and store data with notifications."""
|
||||
validated = self._validate_data(data)
|
||||
result = self.db.save(validated)
|
||||
self._notify_completion(result)
|
||||
return result
|
||||
```
|
||||
|
||||
**Design Improvement Areas:**
|
||||
|
||||
If there's a **cleaner**, **more scalable**, or **simpler** design, highlight it and suggest improvements that would:
|
||||
|
||||
- Reduce code duplication through shared utilities
|
||||
- Make unit testing easier
|
||||
- Improve separation of concerns (single responsibility)
|
||||
- Make unit testing easier through dependency injection
|
||||
- Add clarity without adding complexity
|
||||
- Prefer dataclasses for structured data
|
||||
|
||||
## Development Tools & Commands
|
||||
|
||||
### Package Management
|
||||
|
||||
```bash
|
||||
# Add package
|
||||
uv add package-name
|
||||
|
||||
# Sync project dependencies
|
||||
uv sync
|
||||
uv lock
|
||||
```
|
||||
|
||||
### Testing
|
||||
Each package in `libs/` has its own `pyproject.toml` and `uv.lock`.
|
||||
|
||||
```bash
|
||||
# Run unit tests (no network)
|
||||
make test
|
||||
|
||||
# Don't run integration tests, as API keys must be set
|
||||
|
||||
# Run specific test file
|
||||
uv run --group test pytest tests/unit_tests/test_specific.py
|
||||
```
|
||||
|
||||
### Code Quality
|
||||
|
||||
```bash
|
||||
# Lint code
|
||||
make lint
|
||||
@@ -261,66 +64,118 @@ make format
|
||||
uv run --group lint mypy .
|
||||
```
|
||||
|
||||
### Dependency Management Patterns
|
||||
#### Key config files
|
||||
|
||||
**Local Development Dependencies:**
|
||||
- pyproject.toml: Main workspace configuration with dependency groups
|
||||
- uv.lock: Locked dependencies for reproducible builds
|
||||
- Makefile: Development tasks
|
||||
|
||||
```toml
|
||||
[tool.uv.sources]
|
||||
langchain-core = { path = "../core", editable = true }
|
||||
langchain-tests = { path = "../standard-tests", editable = true }
|
||||
```
|
||||
#### Commit standards
|
||||
|
||||
**For tools, use the `@tool` decorator from `langchain_core.tools`:**
|
||||
Suggest PR titles that follow Conventional Commits format. Refer to .github/workflows/pr_lint for allowed types and scopes.
|
||||
|
||||
```python
|
||||
from langchain_core.tools import tool
|
||||
#### Pull request guidelines
|
||||
|
||||
@tool
|
||||
def search_database(query: str) -> str:
|
||||
"""Search the database for relevant information.
|
||||
- Always add a disclaimer to the PR description mentioning how AI agents are involved with the contribution.
|
||||
- Describe the "why" of the changes, why the proposed solution is the right one. Limit prose.
|
||||
- Highlight areas of the proposed changes that require careful review.
|
||||
|
||||
## Core development principles
|
||||
|
||||
### Maintain stable public interfaces
|
||||
|
||||
CRITICAL: Always attempt to preserve function signatures, argument positions, and names for exported/public methods. Do not make breaking changes.
|
||||
|
||||
**Before making ANY changes to public APIs:**
|
||||
|
||||
- Check if the function/class is exported in `__init__.py`
|
||||
- Look for existing usage patterns in tests and examples
|
||||
- Use keyword-only arguments for new parameters: `*, new_param: str = "default"`
|
||||
- Mark experimental features clearly with docstring warnings (using MkDocs Material admonitions, like `!!! warning`)
|
||||
|
||||
Ask: "Would this change break someone's code if they used it last week?"
|
||||
|
||||
### Code quality standards
|
||||
|
||||
All Python code MUST include type hints and return types.
|
||||
|
||||
```python title="Example"
|
||||
def filter_unknown_users(users: list[str], known_users: set[str]) -> list[str]:
|
||||
"""Single line description of the function.
|
||||
|
||||
Any additional context about the function can go here.
|
||||
|
||||
Args:
|
||||
query: The search query string.
|
||||
users: List of user identifiers to filter.
|
||||
known_users: Set of known/valid user identifiers.
|
||||
|
||||
Returns:
|
||||
List of users that are not in the known_users set.
|
||||
"""
|
||||
# Implementation here
|
||||
return results
|
||||
```
|
||||
|
||||
## Commit Standards
|
||||
- Use descriptive, self-explanatory variable names.
|
||||
- Follow existing patterns in the codebase you're modifying
|
||||
- Attempt to break up complex functions (>20 lines) into smaller, focused functions where it makes sense
|
||||
|
||||
**Use Conventional Commits format for PR titles:**
|
||||
### Testing requirements
|
||||
|
||||
- `feat(core): add multi-tenant support`
|
||||
- `fix(cli): resolve flag parsing error`
|
||||
- `docs: update API usage examples`
|
||||
- `docs(openai): update API usage examples`
|
||||
Every new feature or bugfix MUST be covered by unit tests.
|
||||
|
||||
## Framework-Specific Guidelines
|
||||
- Unit tests: `tests/unit_tests/` (no network calls allowed)
|
||||
- Integration tests: `tests/integration_tests/` (network calls permitted)
|
||||
- We use `pytest` as the testing framework; if in doubt, check other existing tests for examples.
|
||||
- The testing file structure should mirror the source code structure.
|
||||
|
||||
- Follow the existing patterns in `langchain-core` for base abstractions
|
||||
- Use `langchain_core.callbacks` for execution tracking
|
||||
- Implement proper streaming support where applicable
|
||||
- Avoid deprecated components like legacy `LLMChain`
|
||||
**Checklist:**
|
||||
|
||||
### Partner Integrations
|
||||
- [ ] Tests fail when your new logic is broken
|
||||
- [ ] Happy path is covered
|
||||
- [ ] Edge cases and error conditions are tested
|
||||
- [ ] Use fixtures/mocks for external dependencies
|
||||
- [ ] Tests are deterministic (no flaky tests)
|
||||
- [ ] Does the test suite fail if your new logic is broken?
|
||||
|
||||
- Follow the established patterns in existing partner libraries
|
||||
- Implement standard interfaces (`BaseChatModel`, `BaseEmbeddings`, etc.)
|
||||
- Include comprehensive integration tests
|
||||
- Document API key requirements and authentication
|
||||
### Security and risk assessment
|
||||
|
||||
---
|
||||
- No `eval()`, `exec()`, or `pickle` on user-controlled input
|
||||
- Proper exception handling (no bare `except:`) and use a `msg` variable for error messages
|
||||
- Remove unreachable/commented code before committing
|
||||
- Race conditions or resource leaks (file handles, sockets, threads).
|
||||
- Ensure proper resource cleanup (file handles, connections)
|
||||
|
||||
## Quick Reference Checklist
|
||||
### Documentation standards
|
||||
|
||||
Before submitting code changes:
|
||||
Use Google-style docstrings with Args section for all public functions.
|
||||
|
||||
- [ ] **Breaking Changes**: Verified no public API changes
|
||||
- [ ] **Type Hints**: All functions have complete type annotations
|
||||
- [ ] **Tests**: New functionality is fully tested
|
||||
- [ ] **Security**: No dangerous patterns (eval, silent failures, etc.)
|
||||
- [ ] **Documentation**: Google-style docstrings for public functions
|
||||
- [ ] **Code Quality**: `make lint` and `make format` pass
|
||||
- [ ] **Architecture**: Suggested improvements where applicable
|
||||
- [ ] **Commit Message**: Follows Conventional Commits format
|
||||
```python title="Example"
|
||||
def send_email(to: str, msg: str, *, priority: str = "normal") -> bool:
|
||||
"""Send an email to a recipient with specified priority.
|
||||
|
||||
Any additional context about the function can go here.
|
||||
|
||||
Args:
|
||||
to: The email address of the recipient.
|
||||
msg: The message body to send.
|
||||
priority: Email priority level.
|
||||
|
||||
Returns:
|
||||
`True` if email was sent successfully, `False` otherwise.
|
||||
|
||||
Raises:
|
||||
InvalidEmailError: If the email address format is invalid.
|
||||
SMTPConnectionError: If unable to connect to email server.
|
||||
"""
|
||||
```
|
||||
|
||||
- Types go in function signatures, NOT in docstrings
|
||||
- If a default is present, DO NOT repeat it in the docstring unless there is post-processing or it is set conditionally.
|
||||
- Focus on "why" rather than "what" in descriptions
|
||||
- Document all parameters, return values, and exceptions
|
||||
- Keep descriptions concise but clear
|
||||
- Ensure American English spelling (e.g., "behavior", not "behaviour")
|
||||
|
||||
## Additional resources
|
||||
|
||||
- **Documentation:** https://docs.langchain.com/oss/python/langchain/overview and source at https://github.com/langchain-ai/docs or `../docs/`. Prefer the local install and use file search tools for best results. If needed, use the docs MCP server as defined in `.mcp.json` for programmatic access.
|
||||
- **Contributing Guide:** [`.github/CONTRIBUTING.md`](https://docs.langchain.com/oss/python/contributing/overview)
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
# Migrating
|
||||
|
||||
Please see the following guides for migrating LangChain code:
|
||||
|
||||
* Migrate to [LangChain v1.0](https://docs.langchain.com/oss/python/migrate/langchain-v1)
|
||||
* Migrate to [LangChain v0.3](https://python.langchain.com/docs/versions/v0_3/)
|
||||
* Migrate to [LangChain v0.2](https://python.langchain.com/docs/versions/v0_2/)
|
||||
* Migrating from [LangChain 0.0.x Chains](https://python.langchain.com/docs/versions/migrating_chains/)
|
||||
* Upgrade to [LangGraph Memory](https://python.langchain.com/docs/versions/migrating_memory/)
|
||||
84
README.md
84
README.md
@@ -1,40 +1,28 @@
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: light)" srcset=".github/images/logo-dark.svg">
|
||||
<source media="(prefers-color-scheme: dark)" srcset=".github/images/logo-light.svg">
|
||||
<img alt="LangChain Logo" src=".github/images/logo-dark.svg" width="80%">
|
||||
</picture>
|
||||
</p>
|
||||
<div align="center">
|
||||
<a href="https://www.langchain.com/">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: light)" srcset=".github/images/logo-dark.svg">
|
||||
<source media="(prefers-color-scheme: dark)" srcset=".github/images/logo-light.svg">
|
||||
<img alt="LangChain Logo" src=".github/images/logo-dark.svg" width="80%">
|
||||
</picture>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
The platform for reliable agents.
|
||||
</p>
|
||||
<div align="center">
|
||||
<h3>The platform for reliable agents.</h3>
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://opensource.org/licenses/MIT" target="_blank">
|
||||
<img src="https://img.shields.io/pypi/l/langchain" alt="PyPI - License">
|
||||
</a>
|
||||
<a href="https://pypistats.org/packages/langchain" target="_blank">
|
||||
<img src="https://img.shields.io/pepy/dt/langchain" alt="PyPI - Downloads">
|
||||
</a>
|
||||
<a href="https://pypi.org/project/langchain/#history" target="_blank">
|
||||
<img src="https://img.shields.io/pypi/v/langchain?label=%20" alt="Version">
|
||||
</a>
|
||||
<a href="https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/langchain-ai/langchain" target="_blank">
|
||||
<img src="https://img.shields.io/static/v1?label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode" alt="Open in Dev Containers">
|
||||
</a>
|
||||
<a href="https://codespaces.new/langchain-ai/langchain" target="_blank">
|
||||
<img src="https://github.com/codespaces/badge.svg" alt="Open in Github Codespace" title="Open in Github Codespace" width="150" height="20">
|
||||
</a>
|
||||
<a href="https://codspeed.io/langchain-ai/langchain" target="_blank">
|
||||
<img src="https://img.shields.io/endpoint?url=https://codspeed.io/badge.json" alt="CodSpeed Badge">
|
||||
</a>
|
||||
<a href="https://twitter.com/langchainai" target="_blank">
|
||||
<img src="https://img.shields.io/twitter/url/https/twitter.com/langchainai.svg?style=social&label=Follow%20%40LangChainAI" alt="Twitter / X">
|
||||
</a>
|
||||
</p>
|
||||
<div align="center">
|
||||
<a href="https://opensource.org/licenses/MIT" target="_blank"><img src="https://img.shields.io/pypi/l/langchain" alt="PyPI - License"></a>
|
||||
<a href="https://pypistats.org/packages/langchain" target="_blank"><img src="https://img.shields.io/pepy/dt/langchain" alt="PyPI - Downloads"></a>
|
||||
<a href="https://pypi.org/project/langchain/#history" target="_blank"><img src="https://img.shields.io/pypi/v/langchain?label=%20" alt="Version"></a>
|
||||
<a href="https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/langchain-ai/langchain" target="_blank"><img src="https://img.shields.io/static/v1?label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode" alt="Open in Dev Containers"></a>
|
||||
<a href="https://codespaces.new/langchain-ai/langchain" target="_blank"><img src="https://github.com/codespaces/badge.svg" alt="Open in Github Codespace" title="Open in Github Codespace" width="150" height="20"></a>
|
||||
<a href="https://codspeed.io/langchain-ai/langchain" target="_blank"><img src="https://img.shields.io/endpoint?url=https://codspeed.io/badge.json" alt="CodSpeed Badge"></a>
|
||||
<a href="https://twitter.com/langchainai" target="_blank"><img src="https://img.shields.io/twitter/url/https/twitter.com/langchainai.svg?style=social&label=Follow%20%40LangChainAI" alt="Twitter / X"></a>
|
||||
</div>
|
||||
|
||||
LangChain is a framework for building agents and 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.
|
||||
LangChain is a framework for building agents and 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
|
||||
@@ -44,7 +32,10 @@ If you're looking for more advanced customization or agent orchestration, check
|
||||
|
||||
---
|
||||
|
||||
**Documentation**: To learn more about LangChain, check out [the docs](https://docs.langchain.com/oss/python/langchain/overview).
|
||||
**Documentation**:
|
||||
|
||||
- [docs.langchain.com](https://docs.langchain.com/oss/python/langchain/overview) – Comprehensive documentation, including conceptual overviews and guides
|
||||
- [reference.langchain.com/python](https://reference.langchain.com/python) – API reference docs for LangChain packages
|
||||
|
||||
**Discussions**: Visit the [LangChain Forum](https://forum.langchain.com) to connect with the community and share all of your technical questions, ideas, and feedback.
|
||||
|
||||
@@ -57,8 +48,12 @@ LangChain helps developers build applications powered by LLMs through a standard
|
||||
|
||||
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.
|
||||
- **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.
|
||||
- **Rapid prototyping**. Quickly build and iterate on LLM applications with LangChain's modular, component-based architecture. Test different approaches and workflows without rebuilding from scratch, accelerating your development cycle.
|
||||
- **Production-ready features**. Deploy reliable applications with built-in support for monitoring, evaluation, and debugging through integrations like LangSmith. Scale with confidence using battle-tested patterns and best practices.
|
||||
- **Vibrant community and ecosystem**. Leverage a rich ecosystem of integrations, templates, and community-contributed components. Benefit from continuous improvements and stay up-to-date with the latest AI developments through an active open-source community.
|
||||
- **Flexible abstraction layers**. Work at the level of abstraction that suits your needs - from high-level chains for quick starts to low-level components for fine-grained control. LangChain grows with your application's complexity.
|
||||
|
||||
## LangChain ecosystem
|
||||
|
||||
@@ -66,13 +61,14 @@ While the LangChain framework can be used standalone, it also integrates seamles
|
||||
|
||||
To improve your LLM application development, pair LangChain with:
|
||||
|
||||
- [LangGraph](https://docs.langchain.com/oss/python/langgraph/overview) - 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.
|
||||
- [LangSmith](https://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.
|
||||
- [LangSmith Deployment](https://docs.langchain.com/langsmith/deployments) - 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 [LangSmith Studio](https://docs.langchain.com/langsmith/studio).
|
||||
- [LangGraph](https://docs.langchain.com/oss/python/langgraph/overview) – 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.
|
||||
- [Integrations](https://docs.langchain.com/oss/python/integrations/providers/overview) – List of LangChain integrations, including chat & embedding models, tools & toolkits, and more
|
||||
- [LangSmith](https://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.
|
||||
- [LangSmith Deployment](https://docs.langchain.com/langsmith/deployments) – 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 [LangSmith Studio](https://docs.langchain.com/langsmith/studio).
|
||||
- [Deep Agents](https://github.com/langchain-ai/deepagents) *(new!)* – Build agents that can plan, use subagents, and leverage file systems for complex tasks
|
||||
|
||||
## Additional resources
|
||||
|
||||
- [API Reference](https://reference.langchain.com/python): Detailed reference on navigating base packages and integrations for LangChain.
|
||||
- [Integrations](https://docs.langchain.com/oss/python/integrations/providers/overview): List of LangChain integrations, including chat & embedding models, tools & toolkits, and more
|
||||
- [Contributing Guide](https://docs.langchain.com/oss/python/contributing/overview): Learn how to contribute to LangChain and find good first issues.
|
||||
- [Code of Conduct](https://github.com/langchain-ai/langchain/blob/master/.github/CODE_OF_CONDUCT.md): Our community guidelines and standards for participation.
|
||||
- [API Reference](https://reference.langchain.com/python) – Detailed reference on navigating base packages and integrations for LangChain.
|
||||
- [Contributing Guide](https://docs.langchain.com/oss/python/contributing/overview) – Learn how to contribute to LangChain projects and find good first issues.
|
||||
- [Code of Conduct](https://github.com/langchain-ai/langchain/blob/master/.github/CODE_OF_CONDUCT.md) – Our community guidelines and standards for participation.
|
||||
|
||||
@@ -55,10 +55,10 @@ All out of scope targets defined by huntr as well as:
|
||||
* **langchain-experimental**: This repository is for experimental code and is not
|
||||
eligible for bug bounties (see [package warning](https://pypi.org/project/langchain-experimental/)), bug reports to it will be marked as interesting or waste of
|
||||
time and published with no bounty attached.
|
||||
* **tools**: Tools in either langchain or langchain-community are not eligible for bug
|
||||
* **tools**: Tools in either `langchain` or `langchain-community` are not eligible for bug
|
||||
bounties. This includes the following directories
|
||||
* libs/langchain/langchain/tools
|
||||
* libs/community/langchain_community/tools
|
||||
* `libs/langchain/langchain/tools`
|
||||
* `libs/community/langchain_community/tools`
|
||||
* Please review the [Best Practices](#best-practices)
|
||||
for more details, but generally tools interact with the real world. Developers are
|
||||
expected to understand the security implications of their code and are responsible
|
||||
|
||||
@@ -295,7 +295,7 @@
|
||||
"source": [
|
||||
"## TODO: Any functionality specific to this vector store\n",
|
||||
"\n",
|
||||
"E.g. creating a persisten database to save to your disk, etc."
|
||||
"E.g. creating a persistent database to save to your disk, etc."
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -6,9 +6,8 @@ import hashlib
|
||||
import logging
|
||||
import re
|
||||
import shutil
|
||||
from collections.abc import Sequence
|
||||
from pathlib import Path
|
||||
from typing import Any, TypedDict
|
||||
from typing import TYPE_CHECKING, Any, TypedDict
|
||||
|
||||
from git import Repo
|
||||
|
||||
@@ -18,6 +17,9 @@ from langchain_cli.constants import (
|
||||
DEFAULT_GIT_SUBDIRECTORY,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Sequence
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .file import File
|
||||
from .folder import Folder
|
||||
if TYPE_CHECKING:
|
||||
from .file import File
|
||||
from .folder import Folder
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .file import File
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class Folder:
|
||||
def __init__(self, name: str, *files: Folder | File) -> None:
|
||||
|
||||
@@ -34,7 +34,7 @@ The LangChain ecosystem is built on top of `langchain-core`. Some of the benefit
|
||||
|
||||
## 📖 Documentation
|
||||
|
||||
For full documentation, see the [API reference](https://reference.langchain.com/python/langchain_core/).
|
||||
For full documentation, see the [API reference](https://reference.langchain.com/python/langchain_core/). For conceptual guides, tutorials, and examples on using LangChain, see the [LangChain Docs](https://docs.langchain.com/oss/python/langchain/overview).
|
||||
|
||||
## 📕 Releases & Versioning
|
||||
|
||||
|
||||
@@ -52,31 +52,33 @@ class AgentAction(Serializable):
|
||||
"""The input to pass in to the Tool."""
|
||||
log: str
|
||||
"""Additional information to log about the action.
|
||||
This log can be used in a few ways. First, it can be used to audit
|
||||
what exactly the LLM predicted to lead to this (tool, tool_input).
|
||||
Second, it can be used in future iterations to show the LLMs prior
|
||||
thoughts. This is useful when (tool, tool_input) does not contain
|
||||
full information about the LLM prediction (for example, any `thought`
|
||||
before the tool/tool_input)."""
|
||||
|
||||
This log can be used in a few ways. First, it can be used to audit what exactly the
|
||||
LLM predicted to lead to this `(tool, tool_input)`.
|
||||
|
||||
Second, it can be used in future iterations to show the LLMs prior thoughts. This is
|
||||
useful when `(tool, tool_input)` does not contain full information about the LLM
|
||||
prediction (for example, any `thought` before the tool/tool_input).
|
||||
"""
|
||||
type: Literal["AgentAction"] = "AgentAction"
|
||||
|
||||
# Override init to support instantiation by position for backward compat.
|
||||
def __init__(self, tool: str, tool_input: str | dict, log: str, **kwargs: Any):
|
||||
"""Create an AgentAction.
|
||||
"""Create an `AgentAction`.
|
||||
|
||||
Args:
|
||||
tool: The name of the tool to execute.
|
||||
tool_input: The input to pass in to the Tool.
|
||||
tool_input: The input to pass in to the `Tool`.
|
||||
log: Additional information to log about the action.
|
||||
"""
|
||||
super().__init__(tool=tool, tool_input=tool_input, log=log, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def is_lc_serializable(cls) -> bool:
|
||||
"""AgentAction is serializable.
|
||||
"""`AgentAction` is serializable.
|
||||
|
||||
Returns:
|
||||
True
|
||||
`True`
|
||||
"""
|
||||
return True
|
||||
|
||||
@@ -98,19 +100,23 @@ class AgentAction(Serializable):
|
||||
class AgentActionMessageLog(AgentAction):
|
||||
"""Representation of an action to be executed by an agent.
|
||||
|
||||
This is similar to AgentAction, but includes a message log consisting of
|
||||
chat messages. This is useful when working with ChatModels, and is used
|
||||
to reconstruct conversation history from the agent's perspective.
|
||||
This is similar to `AgentAction`, but includes a message log consisting of
|
||||
chat messages.
|
||||
|
||||
This is useful when working with `ChatModels`, and is used to reconstruct
|
||||
conversation history from the agent's perspective.
|
||||
"""
|
||||
|
||||
message_log: Sequence[BaseMessage]
|
||||
"""Similar to log, this can be used to pass along extra
|
||||
information about what exact messages were predicted by the LLM
|
||||
before parsing out the (tool, tool_input). This is again useful
|
||||
if (tool, tool_input) cannot be used to fully recreate the LLM
|
||||
prediction, and you need that LLM prediction (for future agent iteration).
|
||||
"""Similar to log, this can be used to pass along extra information about what exact
|
||||
messages were predicted by the LLM before parsing out the `(tool, tool_input)`.
|
||||
|
||||
This is again useful if `(tool, tool_input)` cannot be used to fully recreate the
|
||||
LLM prediction, and you need that LLM prediction (for future agent iteration).
|
||||
|
||||
Compared to `log`, this is useful when the underlying LLM is a
|
||||
chat model (and therefore returns messages rather than a string)."""
|
||||
chat model (and therefore returns messages rather than a string).
|
||||
"""
|
||||
# Ignoring type because we're overriding the type from AgentAction.
|
||||
# And this is the correct thing to do in this case.
|
||||
# The type literal is used for serialization purposes.
|
||||
@@ -132,19 +138,22 @@ class AgentStep(Serializable):
|
||||
|
||||
|
||||
class AgentFinish(Serializable):
|
||||
"""Final return value of an ActionAgent.
|
||||
"""Final return value of an `ActionAgent`.
|
||||
|
||||
Agents return an AgentFinish when they have reached a stopping condition.
|
||||
Agents return an `AgentFinish` when they have reached a stopping condition.
|
||||
"""
|
||||
|
||||
return_values: dict
|
||||
"""Dictionary of return values."""
|
||||
log: str
|
||||
"""Additional information to log about the return value.
|
||||
|
||||
This is used to pass along the full LLM prediction, not just the parsed out
|
||||
return value. For example, if the full LLM prediction was
|
||||
`Final Answer: 2` you may want to just return `2` as a return value, but pass
|
||||
along the full string as a `log` (for debugging or observability purposes).
|
||||
return value.
|
||||
|
||||
For example, if the full LLM prediction was `Final Answer: 2` you may want to just
|
||||
return `2` as a return value, but pass along the full string as a `log` (for
|
||||
debugging or observability purposes).
|
||||
"""
|
||||
type: Literal["AgentFinish"] = "AgentFinish"
|
||||
|
||||
@@ -154,7 +163,7 @@ class AgentFinish(Serializable):
|
||||
|
||||
@classmethod
|
||||
def is_lc_serializable(cls) -> bool:
|
||||
"""Return True as this class is serializable."""
|
||||
"""Return `True` as this class is serializable."""
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
@@ -202,7 +211,7 @@ def _convert_agent_observation_to_messages(
|
||||
observation: Observation to convert to a message.
|
||||
|
||||
Returns:
|
||||
AIMessage that corresponds to the original tool invocation.
|
||||
`AIMessage` that corresponds to the original tool invocation.
|
||||
"""
|
||||
if isinstance(agent_action, AgentActionMessageLog):
|
||||
return [_create_function_message(agent_action, observation)]
|
||||
@@ -225,7 +234,7 @@ def _create_function_message(
|
||||
observation: the result of the tool invocation.
|
||||
|
||||
Returns:
|
||||
FunctionMessage that corresponds to the original tool invocation.
|
||||
`FunctionMessage` that corresponds to the original tool invocation.
|
||||
"""
|
||||
if not isinstance(observation, str):
|
||||
try:
|
||||
|
||||
@@ -5,13 +5,12 @@ from __future__ import annotations
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Sequence
|
||||
from uuid import UUID
|
||||
|
||||
from tenacity import RetryCallState
|
||||
from typing_extensions import Self
|
||||
|
||||
from langchain_core.agents import AgentAction, AgentFinish
|
||||
from langchain_core.documents import Document
|
||||
|
||||
@@ -39,7 +39,6 @@ from langchain_core.tracers.context import (
|
||||
tracing_v2_callback_var,
|
||||
)
|
||||
from langchain_core.tracers.langchain import LangChainTracer
|
||||
from langchain_core.tracers.schemas import Run
|
||||
from langchain_core.tracers.stdout import ConsoleCallbackHandler
|
||||
from langchain_core.utils.env import env_var_is_set
|
||||
|
||||
@@ -52,6 +51,7 @@ if TYPE_CHECKING:
|
||||
from langchain_core.documents import Document
|
||||
from langchain_core.outputs import ChatGenerationChunk, GenerationChunk, LLMResult
|
||||
from langchain_core.runnables.config import RunnableConfig
|
||||
from langchain_core.tracers.schemas import Run
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -229,7 +229,24 @@ def shielded(func: Func) -> Func:
|
||||
|
||||
@functools.wraps(func)
|
||||
async def wrapped(*args: Any, **kwargs: Any) -> Any:
|
||||
return await asyncio.shield(func(*args, **kwargs))
|
||||
# Capture the current context to preserve context variables
|
||||
ctx = copy_context()
|
||||
|
||||
# Create the coroutine
|
||||
coro = func(*args, **kwargs)
|
||||
|
||||
# For Python 3.11+, create task with explicit context
|
||||
# For older versions, fallback to original behavior
|
||||
try:
|
||||
# Create a task with the captured context to preserve context variables
|
||||
task = asyncio.create_task(coro, context=ctx) # type: ignore[call-arg, unused-ignore]
|
||||
# `call-arg` used to not fail 3.9 or 3.10 tests
|
||||
return await asyncio.shield(task)
|
||||
except TypeError:
|
||||
# Python < 3.11 fallback - create task normally then shield
|
||||
# This won't preserve context perfectly but is better than nothing
|
||||
task = asyncio.create_task(coro)
|
||||
return await asyncio.shield(task)
|
||||
|
||||
return cast("Func", wrapped)
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ class UsageMetadataCallbackHandler(BaseCallbackHandler):
|
||||
'input_token_details': {'cache_read': 0, 'cache_creation': 0}}}
|
||||
```
|
||||
|
||||
!!! version-added "Added in version 0.3.49"
|
||||
!!! version-added "Added in `langchain-core` 0.3.49"
|
||||
|
||||
"""
|
||||
|
||||
@@ -134,7 +134,7 @@ def get_usage_metadata_callback(
|
||||
}
|
||||
```
|
||||
|
||||
!!! version-added "Added in version 0.3.49"
|
||||
!!! version-added "Added in `langchain-core` 0.3.49"
|
||||
|
||||
"""
|
||||
usage_metadata_callback_var: ContextVar[UsageMetadataCallbackHandler | None] = (
|
||||
|
||||
@@ -114,11 +114,11 @@ class Blob(BaseMedia):
|
||||
data: bytes | str | None = None
|
||||
"""Raw data associated with the `Blob`."""
|
||||
mimetype: str | None = None
|
||||
"""MimeType not to be confused with a file extension."""
|
||||
"""MIME type, not to be confused with a file extension."""
|
||||
encoding: str = "utf-8"
|
||||
"""Encoding to use if decoding the bytes into a string.
|
||||
|
||||
Use `utf-8` as default encoding, if decoding to string.
|
||||
Uses `utf-8` as default encoding if decoding to string.
|
||||
"""
|
||||
path: PathLike | None = None
|
||||
"""Location where the original content was found."""
|
||||
@@ -134,7 +134,7 @@ class Blob(BaseMedia):
|
||||
|
||||
If a path is associated with the `Blob`, it will default to the path location.
|
||||
|
||||
Unless explicitly set via a metadata field called `"source"`, in which
|
||||
Unless explicitly set via a metadata field called `'source'`, in which
|
||||
case that value will be used instead.
|
||||
"""
|
||||
if self.metadata and "source" in self.metadata:
|
||||
@@ -309,7 +309,7 @@ class Document(BaseMedia):
|
||||
|
||||
@classmethod
|
||||
def is_lc_serializable(cls) -> bool:
|
||||
"""Return True as this class is serializable."""
|
||||
"""Return `True` as this class is serializable."""
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
@@ -322,10 +322,10 @@ class Document(BaseMedia):
|
||||
return ["langchain", "schema", "document"]
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Override __str__ to restrict it to page_content and metadata.
|
||||
"""Override `__str__` to restrict it to page_content and metadata.
|
||||
|
||||
Returns:
|
||||
A string representation of the Document.
|
||||
A string representation of the `Document`.
|
||||
"""
|
||||
# The format matches pydantic format for __str__.
|
||||
#
|
||||
|
||||
@@ -29,7 +29,7 @@ class LengthBasedExampleSelector(BaseExampleSelector, BaseModel):
|
||||
max_length: int = 2048
|
||||
"""Max length for the prompt, beyond which examples are cut."""
|
||||
|
||||
example_text_lengths: list[int] = Field(default_factory=list) # :meta private:
|
||||
example_text_lengths: list[int] = Field(default_factory=list)
|
||||
"""Length of each example."""
|
||||
|
||||
def add_example(self, example: dict[str, str]) -> None:
|
||||
|
||||
@@ -6,16 +6,9 @@ import hashlib
|
||||
import json
|
||||
import uuid
|
||||
import warnings
|
||||
from collections.abc import (
|
||||
AsyncIterable,
|
||||
AsyncIterator,
|
||||
Callable,
|
||||
Iterable,
|
||||
Iterator,
|
||||
Sequence,
|
||||
)
|
||||
from itertools import islice
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Literal,
|
||||
TypedDict,
|
||||
@@ -29,6 +22,16 @@ from langchain_core.exceptions import LangChainException
|
||||
from langchain_core.indexing.base import DocumentIndex, RecordManager
|
||||
from langchain_core.vectorstores import VectorStore
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import (
|
||||
AsyncIterable,
|
||||
AsyncIterator,
|
||||
Callable,
|
||||
Iterable,
|
||||
Iterator,
|
||||
Sequence,
|
||||
)
|
||||
|
||||
# Magic UUID to use as a namespace for hashing.
|
||||
# Used to try and generate a unique UUID for each document
|
||||
# from hashing the document content and metadata.
|
||||
@@ -298,7 +301,8 @@ def index(
|
||||
For the time being, documents are indexed using their hashes, and users
|
||||
are not able to specify the uid of the document.
|
||||
|
||||
!!! warning "Behavior changed in 0.3.25"
|
||||
!!! warning "Behavior changed in `langchain-core` 0.3.25"
|
||||
|
||||
Added `scoped_full` cleanup mode.
|
||||
|
||||
!!! warning
|
||||
@@ -349,7 +353,7 @@ def index(
|
||||
key_encoder: Hashing algorithm to use for hashing the document content and
|
||||
metadata. Options include "blake2b", "sha256", and "sha512".
|
||||
|
||||
!!! version-added "Added in version 0.3.66"
|
||||
!!! version-added "Added in `langchain-core` 0.3.66"
|
||||
|
||||
key_encoder: Hashing algorithm to use for hashing the document.
|
||||
If not provided, a default encoder using SHA-1 will be used.
|
||||
@@ -366,7 +370,7 @@ def index(
|
||||
method of the `VectorStore` or the upsert method of the DocumentIndex.
|
||||
For example, you can use this to specify a custom vector_field:
|
||||
upsert_kwargs={"vector_field": "embedding"}
|
||||
!!! version-added "Added in version 0.3.10"
|
||||
!!! version-added "Added in `langchain-core` 0.3.10"
|
||||
|
||||
Returns:
|
||||
Indexing result which contains information about how many documents
|
||||
@@ -636,7 +640,8 @@ async def aindex(
|
||||
For the time being, documents are indexed using their hashes, and users
|
||||
are not able to specify the uid of the document.
|
||||
|
||||
!!! warning "Behavior changed in 0.3.25"
|
||||
!!! warning "Behavior changed in `langchain-core` 0.3.25"
|
||||
|
||||
Added `scoped_full` cleanup mode.
|
||||
|
||||
!!! warning
|
||||
@@ -687,7 +692,7 @@ async def aindex(
|
||||
key_encoder: Hashing algorithm to use for hashing the document content and
|
||||
metadata. Options include "blake2b", "sha256", and "sha512".
|
||||
|
||||
!!! version-added "Added in version 0.3.66"
|
||||
!!! version-added "Added in `langchain-core` 0.3.66"
|
||||
|
||||
key_encoder: Hashing algorithm to use for hashing the document.
|
||||
If not provided, a default encoder using SHA-1 will be used.
|
||||
@@ -704,7 +709,7 @@ async def aindex(
|
||||
method of the `VectorStore` or the upsert method of the DocumentIndex.
|
||||
For example, you can use this to specify a custom vector_field:
|
||||
upsert_kwargs={"vector_field": "embedding"}
|
||||
!!! version-added "Added in version 0.3.10"
|
||||
!!! version-added "Added in `langchain-core` 0.3.10"
|
||||
|
||||
Returns:
|
||||
Indexing result which contains information about how many documents
|
||||
|
||||
@@ -53,6 +53,10 @@ if TYPE_CHECKING:
|
||||
ParrotFakeChatModel,
|
||||
)
|
||||
from langchain_core.language_models.llms import LLM, BaseLLM
|
||||
from langchain_core.language_models.model_profile import (
|
||||
ModelProfile,
|
||||
ModelProfileRegistry,
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
"LLM",
|
||||
@@ -68,6 +72,8 @@ __all__ = (
|
||||
"LanguageModelInput",
|
||||
"LanguageModelLike",
|
||||
"LanguageModelOutput",
|
||||
"ModelProfile",
|
||||
"ModelProfileRegistry",
|
||||
"ParrotFakeChatModel",
|
||||
"SimpleChatModel",
|
||||
"get_tokenizer",
|
||||
@@ -90,6 +96,8 @@ _dynamic_imports = {
|
||||
"GenericFakeChatModel": "fake_chat_models",
|
||||
"ParrotFakeChatModel": "fake_chat_models",
|
||||
"LLM": "llms",
|
||||
"ModelProfile": "model_profile",
|
||||
"ModelProfileRegistry": "model_profile",
|
||||
"BaseLLM": "llms",
|
||||
"is_openai_data_block": "_utils",
|
||||
}
|
||||
|
||||
@@ -139,7 +139,8 @@ def _normalize_messages(
|
||||
directly; this may change in the future
|
||||
- LangChain v0 standard content blocks for backward compatibility
|
||||
|
||||
!!! warning "Behavior changed in 1.0.0"
|
||||
!!! warning "Behavior changed in `langchain-core` 1.0.0"
|
||||
|
||||
In previous versions, this function returned messages in LangChain v0 format.
|
||||
Now, it returns messages in LangChain v1 format, which upgraded chat models now
|
||||
expect to receive when passing back in message history. For backward
|
||||
|
||||
@@ -131,14 +131,19 @@ class BaseLanguageModel(
|
||||
|
||||
Caching is not currently supported for streaming methods of models.
|
||||
"""
|
||||
|
||||
verbose: bool = Field(default_factory=_get_verbosity, exclude=True, repr=False)
|
||||
"""Whether to print out response text."""
|
||||
|
||||
callbacks: Callbacks = Field(default=None, exclude=True)
|
||||
"""Callbacks to add to the run trace."""
|
||||
|
||||
tags: list[str] | None = Field(default=None, exclude=True)
|
||||
"""Tags to add to the run trace."""
|
||||
|
||||
metadata: dict[str, Any] | None = Field(default=None, exclude=True)
|
||||
"""Metadata to add to the run trace."""
|
||||
|
||||
custom_get_token_ids: Callable[[str], list[int]] | None = Field(
|
||||
default=None, exclude=True
|
||||
)
|
||||
@@ -195,15 +200,22 @@ class BaseLanguageModel(
|
||||
type (e.g., pure text completion models vs chat models).
|
||||
|
||||
Args:
|
||||
prompts: List of `PromptValue` objects. A `PromptValue` is an object that
|
||||
can be converted to match the format of any language model (string for
|
||||
pure text generation models and `BaseMessage` objects for chat models).
|
||||
stop: Stop words to use when generating. Model output is cut off at the
|
||||
first occurrence of any of these substrings.
|
||||
callbacks: `Callbacks` to pass through. Used for executing additional
|
||||
functionality, such as logging or streaming, throughout generation.
|
||||
**kwargs: Arbitrary additional keyword arguments. These are usually passed
|
||||
to the model provider API call.
|
||||
prompts: List of `PromptValue` objects.
|
||||
|
||||
A `PromptValue` is an object that can be converted to match the format
|
||||
of any language model (string for pure text generation models and
|
||||
`BaseMessage` objects for chat models).
|
||||
stop: Stop words to use when generating.
|
||||
|
||||
Model output is cut off at the first occurrence of any of these
|
||||
substrings.
|
||||
callbacks: `Callbacks` to pass through.
|
||||
|
||||
Used for executing additional functionality, such as logging or
|
||||
streaming, throughout generation.
|
||||
**kwargs: Arbitrary additional keyword arguments.
|
||||
|
||||
These are usually passed to the model provider API call.
|
||||
|
||||
Returns:
|
||||
An `LLMResult`, which contains a list of candidate `Generation` objects for
|
||||
@@ -232,15 +244,22 @@ class BaseLanguageModel(
|
||||
type (e.g., pure text completion models vs chat models).
|
||||
|
||||
Args:
|
||||
prompts: List of `PromptValue` objects. A `PromptValue` is an object that
|
||||
can be converted to match the format of any language model (string for
|
||||
pure text generation models and `BaseMessage` objects for chat models).
|
||||
stop: Stop words to use when generating. Model output is cut off at the
|
||||
first occurrence of any of these substrings.
|
||||
callbacks: `Callbacks` to pass through. Used for executing additional
|
||||
functionality, such as logging or streaming, throughout generation.
|
||||
**kwargs: Arbitrary additional keyword arguments. These are usually passed
|
||||
to the model provider API call.
|
||||
prompts: List of `PromptValue` objects.
|
||||
|
||||
A `PromptValue` is an object that can be converted to match the format
|
||||
of any language model (string for pure text generation models and
|
||||
`BaseMessage` objects for chat models).
|
||||
stop: Stop words to use when generating.
|
||||
|
||||
Model output is cut off at the first occurrence of any of these
|
||||
substrings.
|
||||
callbacks: `Callbacks` to pass through.
|
||||
|
||||
Used for executing additional functionality, such as logging or
|
||||
streaming, throughout generation.
|
||||
**kwargs: Arbitrary additional keyword arguments.
|
||||
|
||||
These are usually passed to the model provider API call.
|
||||
|
||||
Returns:
|
||||
An `LLMResult`, which contains a list of candidate `Generation` objects for
|
||||
@@ -280,6 +299,9 @@ class BaseLanguageModel(
|
||||
|
||||
Useful for checking if an input fits in a model's context window.
|
||||
|
||||
This should be overridden by model-specific implementations to provide accurate
|
||||
token counts via model-specific tokenizers.
|
||||
|
||||
Args:
|
||||
text: The string input to tokenize.
|
||||
|
||||
@@ -298,9 +320,17 @@ class BaseLanguageModel(
|
||||
|
||||
Useful for checking if an input fits in a model's context window.
|
||||
|
||||
This should be overridden by model-specific implementations to provide accurate
|
||||
token counts via model-specific tokenizers.
|
||||
|
||||
!!! note
|
||||
The base implementation of `get_num_tokens_from_messages` ignores tool
|
||||
schemas.
|
||||
|
||||
* The base implementation of `get_num_tokens_from_messages` ignores tool
|
||||
schemas.
|
||||
* The base implementation of `get_num_tokens_from_messages` adds additional
|
||||
prefixes to messages in represent user roles, which will add to the
|
||||
overall token count. Model-specific implementations may choose to
|
||||
handle this differently.
|
||||
|
||||
Args:
|
||||
messages: The message inputs to tokenize.
|
||||
|
||||
@@ -15,7 +15,6 @@ from typing import TYPE_CHECKING, Any, Literal, cast
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from typing_extensions import override
|
||||
|
||||
from langchain_core._api.beta_decorator import beta
|
||||
from langchain_core.caches import BaseCache
|
||||
from langchain_core.callbacks import (
|
||||
AsyncCallbackManager,
|
||||
@@ -34,6 +33,7 @@ from langchain_core.language_models.base import (
|
||||
LangSmithParams,
|
||||
LanguageModelInput,
|
||||
)
|
||||
from langchain_core.language_models.model_profile import ModelProfile
|
||||
from langchain_core.load import dumpd, dumps
|
||||
from langchain_core.messages import (
|
||||
AIMessage,
|
||||
@@ -76,8 +76,6 @@ from langchain_core.utils.utils import LC_ID_PREFIX, from_env
|
||||
if TYPE_CHECKING:
|
||||
import uuid
|
||||
|
||||
from langchain_model_profiles import ModelProfile # type: ignore[import-untyped]
|
||||
|
||||
from langchain_core.output_parsers.base import OutputParserLike
|
||||
from langchain_core.runnables import Runnable, RunnableConfig
|
||||
from langchain_core.tools import BaseTool
|
||||
@@ -91,7 +89,10 @@ def _generate_response_from_error(error: BaseException) -> list[ChatGeneration]:
|
||||
try:
|
||||
metadata["body"] = response.json()
|
||||
except Exception:
|
||||
metadata["body"] = getattr(response, "text", None)
|
||||
try:
|
||||
metadata["body"] = getattr(response, "text", None)
|
||||
except Exception:
|
||||
metadata["body"] = None
|
||||
if hasattr(response, "headers"):
|
||||
try:
|
||||
metadata["headers"] = dict(response.headers)
|
||||
@@ -332,10 +333,25 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
|
||||
[`langchain-openai`](https://pypi.org/project/langchain-openai)) can also use this
|
||||
field to roll out new content formats in a backward-compatible way.
|
||||
|
||||
!!! version-added "Added in version 1.0"
|
||||
!!! version-added "Added in `langchain-core` 1.0.0"
|
||||
|
||||
"""
|
||||
|
||||
profile: ModelProfile | None = Field(default=None, exclude=True)
|
||||
"""Profile detailing model capabilities.
|
||||
|
||||
!!! warning "Beta feature"
|
||||
This is a beta feature. The format of model profiles is subject to change.
|
||||
|
||||
If not specified, automatically loaded from the provider package on initialization
|
||||
if data is available.
|
||||
|
||||
Example profile data includes context window sizes, supported modalities, or support
|
||||
for tool calling, structured output, and other features.
|
||||
|
||||
!!! version-added "Added in `langchain-core` 1.1.0"
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(
|
||||
arbitrary_types_allowed=True,
|
||||
)
|
||||
@@ -845,16 +861,21 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
|
||||
|
||||
Args:
|
||||
messages: List of list of messages.
|
||||
stop: Stop words to use when generating. Model output is cut off at the
|
||||
first occurrence of any of these substrings.
|
||||
callbacks: `Callbacks` to pass through. Used for executing additional
|
||||
functionality, such as logging or streaming, throughout generation.
|
||||
stop: Stop words to use when generating.
|
||||
|
||||
Model output is cut off at the first occurrence of any of these
|
||||
substrings.
|
||||
callbacks: `Callbacks` to pass through.
|
||||
|
||||
Used for executing additional functionality, such as logging or
|
||||
streaming, throughout generation.
|
||||
tags: The tags to apply.
|
||||
metadata: The metadata to apply.
|
||||
run_name: The name of the run.
|
||||
run_id: The ID of the run.
|
||||
**kwargs: Arbitrary additional keyword arguments. These are usually passed
|
||||
to the model provider API call.
|
||||
**kwargs: Arbitrary additional keyword arguments.
|
||||
|
||||
These are usually passed to the model provider API call.
|
||||
|
||||
Returns:
|
||||
An `LLMResult`, which contains a list of candidate `Generations` for each
|
||||
@@ -963,16 +984,21 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
|
||||
|
||||
Args:
|
||||
messages: List of list of messages.
|
||||
stop: Stop words to use when generating. Model output is cut off at the
|
||||
first occurrence of any of these substrings.
|
||||
callbacks: `Callbacks` to pass through. Used for executing additional
|
||||
functionality, such as logging or streaming, throughout generation.
|
||||
stop: Stop words to use when generating.
|
||||
|
||||
Model output is cut off at the first occurrence of any of these
|
||||
substrings.
|
||||
callbacks: `Callbacks` to pass through.
|
||||
|
||||
Used for executing additional functionality, such as logging or
|
||||
streaming, throughout generation.
|
||||
tags: The tags to apply.
|
||||
metadata: The metadata to apply.
|
||||
run_name: The name of the run.
|
||||
run_id: The ID of the run.
|
||||
**kwargs: Arbitrary additional keyword arguments. These are usually passed
|
||||
to the model provider API call.
|
||||
**kwargs: Arbitrary additional keyword arguments.
|
||||
|
||||
These are usually passed to the model provider API call.
|
||||
|
||||
Returns:
|
||||
An `LLMResult`, which contains a list of candidate `Generations` for each
|
||||
@@ -1505,10 +1531,10 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
|
||||
Args:
|
||||
schema: The output schema. Can be passed in as:
|
||||
|
||||
- an OpenAI function/tool schema,
|
||||
- a JSON Schema,
|
||||
- a `TypedDict` class,
|
||||
- or a Pydantic class.
|
||||
- An OpenAI function/tool schema,
|
||||
- A JSON Schema,
|
||||
- A `TypedDict` class,
|
||||
- Or a Pydantic class.
|
||||
|
||||
If `schema` is a Pydantic class then the model output will be a
|
||||
Pydantic instance of that class, and the model-generated fields will be
|
||||
@@ -1520,11 +1546,15 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
|
||||
when specifying a Pydantic or `TypedDict` class.
|
||||
|
||||
include_raw:
|
||||
If `False` then only the parsed structured output is returned. If
|
||||
an error occurs during model output parsing it will be raised. If `True`
|
||||
then both the raw model response (a `BaseMessage`) and the parsed model
|
||||
response will be returned. If an error occurs during output parsing it
|
||||
will be caught and returned as well.
|
||||
If `False` then only the parsed structured output is returned.
|
||||
|
||||
If an error occurs during model output parsing it will be raised.
|
||||
|
||||
If `True` then both the raw model response (a `BaseMessage`) and the
|
||||
parsed model response will be returned.
|
||||
|
||||
If an error occurs during output parsing it will be caught and returned
|
||||
as well.
|
||||
|
||||
The final output is always a `dict` with keys `'raw'`, `'parsed'`, and
|
||||
`'parsing_error'`.
|
||||
@@ -1602,7 +1632,7 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
|
||||
# }
|
||||
```
|
||||
|
||||
Example: `dict` schema (`include_raw=False`):
|
||||
Example: Dictionary schema (`include_raw=False`):
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
@@ -1629,8 +1659,9 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
|
||||
# }
|
||||
```
|
||||
|
||||
!!! warning "Behavior changed in 0.2.26"
|
||||
Added support for TypedDict class.
|
||||
!!! warning "Behavior changed in `langchain-core` 0.2.26"
|
||||
|
||||
Added support for `TypedDict` class.
|
||||
|
||||
""" # noqa: E501
|
||||
_ = kwargs.pop("method", None)
|
||||
@@ -1671,41 +1702,6 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
|
||||
return RunnableMap(raw=llm) | parser_with_fallback
|
||||
return llm | output_parser
|
||||
|
||||
@property
|
||||
@beta()
|
||||
def profile(self) -> ModelProfile:
|
||||
"""Return profiling information for the model.
|
||||
|
||||
This property will relies on the `langchain-model-profiles` package to
|
||||
retrieve chat model capabilities, such as context window sizes and supported
|
||||
features.
|
||||
|
||||
Raises:
|
||||
ImportError: If `langchain-model-profiles` is not installed.
|
||||
|
||||
Returns:
|
||||
A `ModelProfile` object containing profiling information for the model.
|
||||
"""
|
||||
try:
|
||||
from langchain_model_profiles import get_model_profile # noqa: PLC0415
|
||||
except ImportError as err:
|
||||
informative_error_message = (
|
||||
"To access model profiling information, please install the "
|
||||
"`langchain-model-profiles` package: "
|
||||
"`pip install langchain-model-profiles`."
|
||||
)
|
||||
raise ImportError(informative_error_message) from err
|
||||
|
||||
provider_id = self._llm_type
|
||||
model_name = (
|
||||
# Model name is not standardized across integrations. New integrations
|
||||
# should prefer `model`.
|
||||
getattr(self, "model", None)
|
||||
or getattr(self, "model_name", None)
|
||||
or getattr(self, "model_id", "")
|
||||
)
|
||||
return get_model_profile(provider_id, model_name) or {}
|
||||
|
||||
|
||||
class SimpleChatModel(BaseChatModel):
|
||||
"""Simplified implementation for a chat model to inherit from.
|
||||
@@ -1764,9 +1760,12 @@ def _gen_info_and_msg_metadata(
|
||||
}
|
||||
|
||||
|
||||
_MAX_CLEANUP_DEPTH = 100
|
||||
|
||||
|
||||
def _cleanup_llm_representation(serialized: Any, depth: int) -> None:
|
||||
"""Remove non-serializable objects from a serialized object."""
|
||||
if depth > 100: # Don't cooperate for pathological cases
|
||||
if depth > _MAX_CLEANUP_DEPTH: # Don't cooperate for pathological cases
|
||||
return
|
||||
|
||||
if not isinstance(serialized, dict):
|
||||
|
||||
@@ -651,9 +651,12 @@ class BaseLLM(BaseLanguageModel[str], ABC):
|
||||
|
||||
Args:
|
||||
prompts: The prompts to generate from.
|
||||
stop: Stop words to use when generating. Model output is cut off at the
|
||||
first occurrence of any of the stop substrings.
|
||||
If stop tokens are not supported consider raising NotImplementedError.
|
||||
stop: Stop words to use when generating.
|
||||
|
||||
Model output is cut off at the first occurrence of any of these
|
||||
substrings.
|
||||
|
||||
If stop tokens are not supported consider raising `NotImplementedError`.
|
||||
run_manager: Callback manager for the run.
|
||||
|
||||
Returns:
|
||||
@@ -671,9 +674,12 @@ class BaseLLM(BaseLanguageModel[str], ABC):
|
||||
|
||||
Args:
|
||||
prompts: The prompts to generate from.
|
||||
stop: Stop words to use when generating. Model output is cut off at the
|
||||
first occurrence of any of the stop substrings.
|
||||
If stop tokens are not supported consider raising NotImplementedError.
|
||||
stop: Stop words to use when generating.
|
||||
|
||||
Model output is cut off at the first occurrence of any of these
|
||||
substrings.
|
||||
|
||||
If stop tokens are not supported consider raising `NotImplementedError`.
|
||||
run_manager: Callback manager for the run.
|
||||
|
||||
Returns:
|
||||
@@ -705,11 +711,14 @@ class BaseLLM(BaseLanguageModel[str], ABC):
|
||||
|
||||
Args:
|
||||
prompt: The prompt to generate from.
|
||||
stop: Stop words to use when generating. Model output is cut off at the
|
||||
first occurrence of any of these substrings.
|
||||
stop: Stop words to use when generating.
|
||||
|
||||
Model output is cut off at the first occurrence of any of these
|
||||
substrings.
|
||||
run_manager: Callback manager for the run.
|
||||
**kwargs: Arbitrary additional keyword arguments. These are usually passed
|
||||
to the model provider API call.
|
||||
**kwargs: Arbitrary additional keyword arguments.
|
||||
|
||||
These are usually passed to the model provider API call.
|
||||
|
||||
Yields:
|
||||
Generation chunks.
|
||||
@@ -731,11 +740,14 @@ class BaseLLM(BaseLanguageModel[str], ABC):
|
||||
|
||||
Args:
|
||||
prompt: The prompt to generate from.
|
||||
stop: Stop words to use when generating. Model output is cut off at the
|
||||
first occurrence of any of these substrings.
|
||||
stop: Stop words to use when generating.
|
||||
|
||||
Model output is cut off at the first occurrence of any of these
|
||||
substrings.
|
||||
run_manager: Callback manager for the run.
|
||||
**kwargs: Arbitrary additional keyword arguments. These are usually passed
|
||||
to the model provider API call.
|
||||
**kwargs: Arbitrary additional keyword arguments.
|
||||
|
||||
These are usually passed to the model provider API call.
|
||||
|
||||
Yields:
|
||||
Generation chunks.
|
||||
@@ -846,10 +858,14 @@ class BaseLLM(BaseLanguageModel[str], ABC):
|
||||
|
||||
Args:
|
||||
prompts: List of string prompts.
|
||||
stop: Stop words to use when generating. Model output is cut off at the
|
||||
first occurrence of any of these substrings.
|
||||
callbacks: `Callbacks` to pass through. Used for executing additional
|
||||
functionality, such as logging or streaming, throughout generation.
|
||||
stop: Stop words to use when generating.
|
||||
|
||||
Model output is cut off at the first occurrence of any of these
|
||||
substrings.
|
||||
callbacks: `Callbacks` to pass through.
|
||||
|
||||
Used for executing additional functionality, such as logging or
|
||||
streaming, throughout generation.
|
||||
tags: List of tags to associate with each prompt. If provided, the length
|
||||
of the list must match the length of the prompts list.
|
||||
metadata: List of metadata dictionaries to associate with each prompt. If
|
||||
@@ -859,8 +875,9 @@ class BaseLLM(BaseLanguageModel[str], ABC):
|
||||
length of the list must match the length of the prompts list.
|
||||
run_id: List of run IDs to associate with each prompt. If provided, the
|
||||
length of the list must match the length of the prompts list.
|
||||
**kwargs: Arbitrary additional keyword arguments. These are usually passed
|
||||
to the model provider API call.
|
||||
**kwargs: Arbitrary additional keyword arguments.
|
||||
|
||||
These are usually passed to the model provider API call.
|
||||
|
||||
Raises:
|
||||
ValueError: If prompts is not a list.
|
||||
@@ -1116,10 +1133,14 @@ class BaseLLM(BaseLanguageModel[str], ABC):
|
||||
|
||||
Args:
|
||||
prompts: List of string prompts.
|
||||
stop: Stop words to use when generating. Model output is cut off at the
|
||||
first occurrence of any of these substrings.
|
||||
callbacks: `Callbacks` to pass through. Used for executing additional
|
||||
functionality, such as logging or streaming, throughout generation.
|
||||
stop: Stop words to use when generating.
|
||||
|
||||
Model output is cut off at the first occurrence of any of these
|
||||
substrings.
|
||||
callbacks: `Callbacks` to pass through.
|
||||
|
||||
Used for executing additional functionality, such as logging or
|
||||
streaming, throughout generation.
|
||||
tags: List of tags to associate with each prompt. If provided, the length
|
||||
of the list must match the length of the prompts list.
|
||||
metadata: List of metadata dictionaries to associate with each prompt. If
|
||||
@@ -1129,8 +1150,9 @@ class BaseLLM(BaseLanguageModel[str], ABC):
|
||||
length of the list must match the length of the prompts list.
|
||||
run_id: List of run IDs to associate with each prompt. If provided, the
|
||||
length of the list must match the length of the prompts list.
|
||||
**kwargs: Arbitrary additional keyword arguments. These are usually passed
|
||||
to the model provider API call.
|
||||
**kwargs: Arbitrary additional keyword arguments.
|
||||
|
||||
These are usually passed to the model provider API call.
|
||||
|
||||
Raises:
|
||||
ValueError: If the length of `callbacks`, `tags`, `metadata`, or
|
||||
@@ -1410,12 +1432,16 @@ class LLM(BaseLLM):
|
||||
|
||||
Args:
|
||||
prompt: The prompt to generate from.
|
||||
stop: Stop words to use when generating. Model output is cut off at the
|
||||
first occurrence of any of the stop substrings.
|
||||
If stop tokens are not supported consider raising NotImplementedError.
|
||||
stop: Stop words to use when generating.
|
||||
|
||||
Model output is cut off at the first occurrence of any of these
|
||||
substrings.
|
||||
|
||||
If stop tokens are not supported consider raising `NotImplementedError`.
|
||||
run_manager: Callback manager for the run.
|
||||
**kwargs: Arbitrary additional keyword arguments. These are usually passed
|
||||
to the model provider API call.
|
||||
**kwargs: Arbitrary additional keyword arguments.
|
||||
|
||||
These are usually passed to the model provider API call.
|
||||
|
||||
Returns:
|
||||
The model output as a string. SHOULD NOT include the prompt.
|
||||
@@ -1436,12 +1462,16 @@ class LLM(BaseLLM):
|
||||
|
||||
Args:
|
||||
prompt: The prompt to generate from.
|
||||
stop: Stop words to use when generating. Model output is cut off at the
|
||||
first occurrence of any of the stop substrings.
|
||||
If stop tokens are not supported consider raising NotImplementedError.
|
||||
stop: Stop words to use when generating.
|
||||
|
||||
Model output is cut off at the first occurrence of any of these
|
||||
substrings.
|
||||
|
||||
If stop tokens are not supported consider raising `NotImplementedError`.
|
||||
run_manager: Callback manager for the run.
|
||||
**kwargs: Arbitrary additional keyword arguments. These are usually passed
|
||||
to the model provider API call.
|
||||
**kwargs: Arbitrary additional keyword arguments.
|
||||
|
||||
These are usually passed to the model provider API call.
|
||||
|
||||
Returns:
|
||||
The model output as a string. SHOULD NOT include the prompt.
|
||||
|
||||
84
libs/core/langchain_core/language_models/model_profile.py
Normal file
84
libs/core/langchain_core/language_models/model_profile.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""Model profile types and utilities."""
|
||||
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
|
||||
class ModelProfile(TypedDict, total=False):
|
||||
"""Model profile.
|
||||
|
||||
!!! warning "Beta feature"
|
||||
This is a beta feature. The format of model profiles is subject to change.
|
||||
|
||||
Provides information about chat model capabilities, such as context window sizes
|
||||
and supported features.
|
||||
"""
|
||||
|
||||
# --- Input constraints ---
|
||||
|
||||
max_input_tokens: int
|
||||
"""Maximum context window (tokens)"""
|
||||
|
||||
image_inputs: bool
|
||||
"""Whether image inputs are supported."""
|
||||
# TODO: add more detail about formats?
|
||||
|
||||
image_url_inputs: bool
|
||||
"""Whether [image URL inputs](https://docs.langchain.com/oss/python/langchain/models#multimodal)
|
||||
are supported."""
|
||||
|
||||
pdf_inputs: bool
|
||||
"""Whether [PDF inputs](https://docs.langchain.com/oss/python/langchain/models#multimodal)
|
||||
are supported."""
|
||||
# TODO: add more detail about formats? e.g. bytes or base64
|
||||
|
||||
audio_inputs: bool
|
||||
"""Whether [audio inputs](https://docs.langchain.com/oss/python/langchain/models#multimodal)
|
||||
are supported."""
|
||||
# TODO: add more detail about formats? e.g. bytes or base64
|
||||
|
||||
video_inputs: bool
|
||||
"""Whether [video inputs](https://docs.langchain.com/oss/python/langchain/models#multimodal)
|
||||
are supported."""
|
||||
# TODO: add more detail about formats? e.g. bytes or base64
|
||||
|
||||
image_tool_message: bool
|
||||
"""Whether images can be included in tool messages."""
|
||||
|
||||
pdf_tool_message: bool
|
||||
"""Whether PDFs can be included in tool messages."""
|
||||
|
||||
# --- Output constraints ---
|
||||
|
||||
max_output_tokens: int
|
||||
"""Maximum output tokens"""
|
||||
|
||||
reasoning_output: bool
|
||||
"""Whether the model supports [reasoning / chain-of-thought](https://docs.langchain.com/oss/python/langchain/models#reasoning)"""
|
||||
|
||||
image_outputs: bool
|
||||
"""Whether [image outputs](https://docs.langchain.com/oss/python/langchain/models#multimodal)
|
||||
are supported."""
|
||||
|
||||
audio_outputs: bool
|
||||
"""Whether [audio outputs](https://docs.langchain.com/oss/python/langchain/models#multimodal)
|
||||
are supported."""
|
||||
|
||||
video_outputs: bool
|
||||
"""Whether [video outputs](https://docs.langchain.com/oss/python/langchain/models#multimodal)
|
||||
are supported."""
|
||||
|
||||
# --- Tool calling ---
|
||||
tool_calling: bool
|
||||
"""Whether the model supports [tool calling](https://docs.langchain.com/oss/python/langchain/models#tool-calling)"""
|
||||
|
||||
tool_choice: bool
|
||||
"""Whether the model supports [tool choice](https://docs.langchain.com/oss/python/langchain/models#forcing-tool-calls)"""
|
||||
|
||||
# --- Structured output ---
|
||||
structured_output: bool
|
||||
"""Whether the model supports a native [structured output](https://docs.langchain.com/oss/python/langchain/models#structured-outputs)
|
||||
feature"""
|
||||
|
||||
|
||||
ModelProfileRegistry = dict[str, ModelProfile]
|
||||
"""Registry mapping model identifiers or names to their ModelProfile."""
|
||||
@@ -61,13 +61,15 @@ class Reviver:
|
||||
"""Initialize the reviver.
|
||||
|
||||
Args:
|
||||
secrets_map: A map of secrets to load. If a secret is not found in
|
||||
the map, it will be loaded from the environment if `secrets_from_env`
|
||||
is True.
|
||||
secrets_map: A map of secrets to load.
|
||||
|
||||
If a secret is not found in the map, it will be loaded from the
|
||||
environment if `secrets_from_env` is `True`.
|
||||
valid_namespaces: A list of additional namespaces (modules)
|
||||
to allow to be deserialized.
|
||||
secrets_from_env: Whether to load secrets from the environment.
|
||||
additional_import_mappings: A dictionary of additional namespace mappings
|
||||
|
||||
You can use this to override default mappings or add new mappings.
|
||||
ignore_unserializable_fields: Whether to ignore unserializable fields.
|
||||
"""
|
||||
@@ -195,13 +197,15 @@ def loads(
|
||||
|
||||
Args:
|
||||
text: The string to load.
|
||||
secrets_map: A map of secrets to load. If a secret is not found in
|
||||
the map, it will be loaded from the environment if `secrets_from_env`
|
||||
is True.
|
||||
secrets_map: A map of secrets to load.
|
||||
|
||||
If a secret is not found in the map, it will be loaded from the environment
|
||||
if `secrets_from_env` is `True`.
|
||||
valid_namespaces: A list of additional namespaces (modules)
|
||||
to allow to be deserialized.
|
||||
secrets_from_env: Whether to load secrets from the environment.
|
||||
additional_import_mappings: A dictionary of additional namespace mappings
|
||||
|
||||
You can use this to override default mappings or add new mappings.
|
||||
ignore_unserializable_fields: Whether to ignore unserializable fields.
|
||||
|
||||
@@ -237,13 +241,15 @@ def load(
|
||||
|
||||
Args:
|
||||
obj: The object to load.
|
||||
secrets_map: A map of secrets to load. If a secret is not found in
|
||||
the map, it will be loaded from the environment if `secrets_from_env`
|
||||
is True.
|
||||
secrets_map: A map of secrets to load.
|
||||
|
||||
If a secret is not found in the map, it will be loaded from the environment
|
||||
if `secrets_from_env` is `True`.
|
||||
valid_namespaces: A list of additional namespaces (modules)
|
||||
to allow to be deserialized.
|
||||
secrets_from_env: Whether to load secrets from the environment.
|
||||
additional_import_mappings: A dictionary of additional namespace mappings
|
||||
|
||||
You can use this to override default mappings or add new mappings.
|
||||
ignore_unserializable_fields: Whether to ignore unserializable fields.
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ class InputTokenDetails(TypedDict, total=False):
|
||||
|
||||
May also hold extra provider-specific keys.
|
||||
|
||||
!!! version-added "Added in version 0.3.9"
|
||||
!!! version-added "Added in `langchain-core` 0.3.9"
|
||||
|
||||
"""
|
||||
|
||||
@@ -85,7 +85,7 @@ class OutputTokenDetails(TypedDict, total=False):
|
||||
|
||||
May also hold extra provider-specific keys.
|
||||
|
||||
!!! version-added "Added in version 0.3.9"
|
||||
!!! version-added "Added in `langchain-core` 0.3.9"
|
||||
|
||||
"""
|
||||
|
||||
@@ -123,10 +123,12 @@ class UsageMetadata(TypedDict):
|
||||
}
|
||||
```
|
||||
|
||||
!!! warning "Behavior changed in 0.3.9"
|
||||
!!! warning "Behavior changed in `langchain-core` 0.3.9"
|
||||
|
||||
Added `input_token_details` and `output_token_details`.
|
||||
|
||||
!!! note "LangSmith SDK"
|
||||
|
||||
The LangSmith SDK also has a `UsageMetadata` class. While the two share fields,
|
||||
LangSmith's `UsageMetadata` has additional fields to capture cost information
|
||||
used by the LangSmith platform.
|
||||
|
||||
@@ -5,11 +5,9 @@ from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Any, cast, overload
|
||||
|
||||
from pydantic import ConfigDict, Field
|
||||
from typing_extensions import Self
|
||||
|
||||
from langchain_core._api.deprecation import warn_deprecated
|
||||
from langchain_core.load.serializable import Serializable
|
||||
from langchain_core.messages import content as types
|
||||
from langchain_core.utils import get_bolded_text
|
||||
from langchain_core.utils._merge import merge_dicts, merge_lists
|
||||
from langchain_core.utils.interactive_env import is_interactive_env
|
||||
@@ -17,6 +15,9 @@ from langchain_core.utils.interactive_env import is_interactive_env
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Sequence
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from langchain_core.messages import content as types
|
||||
from langchain_core.prompts.chat import ChatPromptTemplate
|
||||
|
||||
|
||||
@@ -199,7 +200,7 @@ class BaseMessage(Serializable):
|
||||
def content_blocks(self) -> list[types.ContentBlock]:
|
||||
r"""Load content blocks from the message content.
|
||||
|
||||
!!! version-added "Added in version 1.0.0"
|
||||
!!! version-added "Added in `langchain-core` 1.0.0"
|
||||
|
||||
"""
|
||||
# Needed here to avoid circular import, as these classes import BaseMessages
|
||||
|
||||
@@ -12,10 +12,11 @@ the implementation in `BaseMessage`.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
from langchain_core.messages import AIMessage, AIMessageChunk
|
||||
from langchain_core.messages import content as types
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import warnings
|
||||
from collections.abc import Iterable
|
||||
from typing import TYPE_CHECKING, Any, Literal, cast
|
||||
|
||||
from langchain_core.language_models._utils import (
|
||||
@@ -14,6 +13,8 @@ from langchain_core.language_models._utils import (
|
||||
from langchain_core.messages import content as types
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterable
|
||||
|
||||
from langchain_core.messages import AIMessage, AIMessageChunk
|
||||
|
||||
|
||||
|
||||
@@ -654,7 +654,7 @@ class PlainTextContentBlock(TypedDict):
|
||||
|
||||
!!! note
|
||||
Title and context are optional fields that may be passed to the model. See
|
||||
Anthropic [example](https://docs.claude.com/en/docs/build-with-claude/citations#citable-vs-non-citable-content).
|
||||
Anthropic [example](https://platform.claude.com/docs/en/build-with-claude/citations#citable-vs-non-citable-content).
|
||||
|
||||
!!! note "Factory function"
|
||||
`create_plaintext_block` may also be used as a factory to create a
|
||||
@@ -867,7 +867,7 @@ def _get_data_content_block_types() -> tuple[str, ...]:
|
||||
Example: ("image", "video", "audio", "text-plain", "file")
|
||||
|
||||
Note that old style multimodal blocks type literals with new style blocks.
|
||||
Speficially, "image", "audio", and "file".
|
||||
Specifically, "image", "audio", and "file".
|
||||
|
||||
See the docstring of `_normalize_messages` in `language_models._utils` for details.
|
||||
"""
|
||||
@@ -906,7 +906,7 @@ def is_data_content_block(block: dict) -> bool:
|
||||
|
||||
# 'text' is checked to support v0 PlainTextContentBlock types
|
||||
# We must guard against new style TextContentBlock which also has 'text' `type`
|
||||
# by ensuring the presense of `source_type`
|
||||
# by ensuring the presence of `source_type`
|
||||
if block["type"] == "text" and "source_type" not in block: # noqa: SIM103 # This is more readable
|
||||
return False
|
||||
|
||||
|
||||
@@ -328,12 +328,16 @@ def _convert_to_message(message: MessageLikeRepresentation) -> BaseMessage:
|
||||
"""
|
||||
if isinstance(message, BaseMessage):
|
||||
message_ = message
|
||||
elif isinstance(message, str):
|
||||
message_ = _create_message_from_message_type("human", message)
|
||||
elif isinstance(message, Sequence) and len(message) == 2:
|
||||
# mypy doesn't realise this can't be a string given the previous branch
|
||||
message_type_str, template = message # type: ignore[misc]
|
||||
message_ = _create_message_from_message_type(message_type_str, template)
|
||||
elif isinstance(message, Sequence):
|
||||
if isinstance(message, str):
|
||||
message_ = _create_message_from_message_type("human", message)
|
||||
else:
|
||||
try:
|
||||
message_type_str, template = message
|
||||
except ValueError as e:
|
||||
msg = "Message as a sequence must be (role string, template)"
|
||||
raise NotImplementedError(msg) from e
|
||||
message_ = _create_message_from_message_type(message_type_str, template)
|
||||
elif isinstance(message, dict):
|
||||
msg_kwargs = message.copy()
|
||||
try:
|
||||
@@ -734,8 +738,10 @@ def trim_messages(
|
||||
Set to `len` to count the number of **messages** in the chat history.
|
||||
|
||||
!!! note
|
||||
|
||||
Use `count_tokens_approximately` to get fast, approximate token
|
||||
counts.
|
||||
|
||||
This is recommended for using `trim_messages` on the hot path, where
|
||||
exact token counting is not necessary.
|
||||
|
||||
@@ -1097,7 +1103,7 @@ def convert_to_openai_messages(
|
||||
# ]
|
||||
```
|
||||
|
||||
!!! version-added "Added in version 0.3.11"
|
||||
!!! version-added "Added in `langchain-core` 0.3.11"
|
||||
|
||||
""" # noqa: E501
|
||||
if text_format not in {"string", "block"}:
|
||||
@@ -1697,7 +1703,7 @@ def count_tokens_approximately(
|
||||
Warning:
|
||||
This function does not currently support counting image tokens.
|
||||
|
||||
!!! version-added "Added in version 0.3.46"
|
||||
!!! version-added "Added in `langchain-core` 0.3.46"
|
||||
|
||||
"""
|
||||
token_count = 0.0
|
||||
|
||||
@@ -15,7 +15,11 @@ from langchain_core.messages.tool import tool_call as create_tool_call
|
||||
from langchain_core.output_parsers.transform import BaseCumulativeTransformOutputParser
|
||||
from langchain_core.outputs import ChatGeneration, Generation
|
||||
from langchain_core.utils.json import parse_partial_json
|
||||
from langchain_core.utils.pydantic import TypeBaseModel
|
||||
from langchain_core.utils.pydantic import (
|
||||
TypeBaseModel,
|
||||
is_pydantic_v1_subclass,
|
||||
is_pydantic_v2_subclass,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -323,7 +327,15 @@ class PydanticToolsParser(JsonOutputToolsParser):
|
||||
return None if self.first_tool_only else []
|
||||
|
||||
json_results = [json_results] if self.first_tool_only else json_results
|
||||
name_dict = {tool.__name__: tool for tool in self.tools}
|
||||
name_dict_v2: dict[str, TypeBaseModel] = {
|
||||
tool.model_config.get("title") or tool.__name__: tool
|
||||
for tool in self.tools
|
||||
if is_pydantic_v2_subclass(tool)
|
||||
}
|
||||
name_dict_v1: dict[str, TypeBaseModel] = {
|
||||
tool.__name__: tool for tool in self.tools if is_pydantic_v1_subclass(tool)
|
||||
}
|
||||
name_dict: dict[str, TypeBaseModel] = {**name_dict_v2, **name_dict_v1}
|
||||
pydantic_objects = []
|
||||
for res in json_results:
|
||||
if not isinstance(res["args"], dict):
|
||||
|
||||
@@ -37,7 +37,7 @@ class PydanticOutputParser(JsonOutputParser, Generic[TBaseModel]):
|
||||
def _parser_exception(
|
||||
self, e: Exception, json_object: dict
|
||||
) -> OutputParserException:
|
||||
json_string = json.dumps(json_object)
|
||||
json_string = json.dumps(json_object, ensure_ascii=False)
|
||||
name = self.pydantic_object.__name__
|
||||
msg = f"Failed to parse {name} from completion {json_string}. Got: {e}"
|
||||
return OutputParserException(msg, llm_output=json_string)
|
||||
|
||||
@@ -2,15 +2,17 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
|
||||
from pydantic import model_validator
|
||||
from typing_extensions import Self
|
||||
|
||||
from langchain_core.messages import BaseMessage, BaseMessageChunk
|
||||
from langchain_core.outputs.generation import Generation
|
||||
from langchain_core.utils._merge import merge_dicts
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
|
||||
class ChatGeneration(Generation):
|
||||
"""A single chat generation output.
|
||||
|
||||
@@ -20,8 +20,7 @@ class Generation(Serializable):
|
||||
|
||||
LangChain users working with chat models will usually access information via
|
||||
`AIMessage` (returned from runnable interfaces) or `LLMResult` (available
|
||||
via callbacks). Please refer the `AIMessage` and `LLMResult` schema documentation
|
||||
for more information.
|
||||
via callbacks). Please refer to `AIMessage` and `LLMResult` for more information.
|
||||
"""
|
||||
|
||||
text: str
|
||||
@@ -34,11 +33,13 @@ class Generation(Serializable):
|
||||
"""
|
||||
type: Literal["Generation"] = "Generation"
|
||||
"""Type is used exclusively for serialization purposes.
|
||||
Set to "Generation" for this class."""
|
||||
|
||||
Set to "Generation" for this class.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def is_lc_serializable(cls) -> bool:
|
||||
"""Return True as this class is serializable."""
|
||||
"""Return `True` as this class is serializable."""
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
@@ -52,7 +53,7 @@ class Generation(Serializable):
|
||||
|
||||
|
||||
class GenerationChunk(Generation):
|
||||
"""Generation chunk, which can be concatenated with other Generation chunks."""
|
||||
"""`GenerationChunk`, which can be concatenated with other Generation chunks."""
|
||||
|
||||
def __add__(self, other: GenerationChunk) -> GenerationChunk:
|
||||
"""Concatenate two `GenerationChunk`s.
|
||||
|
||||
@@ -30,7 +30,7 @@ class PromptValue(Serializable, ABC):
|
||||
|
||||
@classmethod
|
||||
def is_lc_serializable(cls) -> bool:
|
||||
"""Return True as this class is serializable."""
|
||||
"""Return `True` as this class is serializable."""
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
@@ -48,7 +48,7 @@ class PromptValue(Serializable, ABC):
|
||||
|
||||
@abstractmethod
|
||||
def to_messages(self) -> list[BaseMessage]:
|
||||
"""Return prompt as a list of Messages."""
|
||||
"""Return prompt as a list of messages."""
|
||||
|
||||
|
||||
class StringPromptValue(PromptValue):
|
||||
|
||||
@@ -6,7 +6,7 @@ import contextlib
|
||||
import json
|
||||
import typing
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Callable, Mapping
|
||||
from collections.abc import Mapping
|
||||
from functools import cached_property
|
||||
from pathlib import Path
|
||||
from typing import (
|
||||
@@ -33,6 +33,8 @@ from langchain_core.runnables.config import ensure_config
|
||||
from langchain_core.utils.pydantic import create_model_v2
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
from langchain_core.documents import Document
|
||||
|
||||
|
||||
@@ -46,23 +48,27 @@ class BasePromptTemplate(
|
||||
|
||||
input_variables: list[str]
|
||||
"""A list of the names of the variables whose values are required as inputs to the
|
||||
prompt."""
|
||||
prompt.
|
||||
"""
|
||||
optional_variables: list[str] = Field(default=[])
|
||||
"""A list of the names of the variables for placeholder or `MessagePlaceholder` that
|
||||
are optional.
|
||||
|
||||
These variables are auto inferred from the prompt and user need not provide them."""
|
||||
These variables are auto inferred from the prompt and user need not provide them.
|
||||
"""
|
||||
input_types: typing.Dict[str, Any] = Field(default_factory=dict, exclude=True) # noqa: UP006
|
||||
"""A dictionary of the types of the variables the prompt template expects.
|
||||
|
||||
If not provided, all variables are assumed to be strings."""
|
||||
If not provided, all variables are assumed to be strings.
|
||||
"""
|
||||
output_parser: BaseOutputParser | None = None
|
||||
"""How to parse the output of calling an LLM on this formatted prompt."""
|
||||
partial_variables: Mapping[str, Any] = Field(default_factory=dict)
|
||||
"""A dictionary of the partial variables the prompt template carries.
|
||||
|
||||
Partial variables populate the template so that you don't need to
|
||||
pass them in every time you call the prompt."""
|
||||
Partial variables populate the template so that you don't need to pass them in every
|
||||
time you call the prompt.
|
||||
"""
|
||||
metadata: typing.Dict[str, Any] | None = None # noqa: UP006
|
||||
"""Metadata to be used for tracing."""
|
||||
tags: list[str] | None = None
|
||||
@@ -107,7 +113,7 @@ class BasePromptTemplate(
|
||||
|
||||
@classmethod
|
||||
def is_lc_serializable(cls) -> bool:
|
||||
"""Return True as this class is serializable."""
|
||||
"""Return `True` as this class is serializable."""
|
||||
return True
|
||||
|
||||
model_config = ConfigDict(
|
||||
@@ -129,7 +135,7 @@ class BasePromptTemplate(
|
||||
"""Get the input schema for the prompt.
|
||||
|
||||
Args:
|
||||
config: configuration for the prompt.
|
||||
config: Configuration for the prompt.
|
||||
|
||||
Returns:
|
||||
The input schema for the prompt.
|
||||
@@ -197,8 +203,8 @@ class BasePromptTemplate(
|
||||
"""Invoke the prompt.
|
||||
|
||||
Args:
|
||||
input: Dict, input to the prompt.
|
||||
config: RunnableConfig, configuration for the prompt.
|
||||
input: Input to the prompt.
|
||||
config: Configuration for the prompt.
|
||||
|
||||
Returns:
|
||||
The output of the prompt.
|
||||
@@ -223,8 +229,8 @@ class BasePromptTemplate(
|
||||
"""Async invoke the prompt.
|
||||
|
||||
Args:
|
||||
input: Dict, input to the prompt.
|
||||
config: RunnableConfig, configuration for the prompt.
|
||||
input: Input to the prompt.
|
||||
config: Configuration for the prompt.
|
||||
|
||||
Returns:
|
||||
The output of the prompt.
|
||||
@@ -244,7 +250,7 @@ class BasePromptTemplate(
|
||||
|
||||
@abstractmethod
|
||||
def format_prompt(self, **kwargs: Any) -> PromptValue:
|
||||
"""Create Prompt Value.
|
||||
"""Create `PromptValue`.
|
||||
|
||||
Args:
|
||||
**kwargs: Any arguments to be passed to the prompt template.
|
||||
@@ -254,7 +260,7 @@ class BasePromptTemplate(
|
||||
"""
|
||||
|
||||
async def aformat_prompt(self, **kwargs: Any) -> PromptValue:
|
||||
"""Async create Prompt Value.
|
||||
"""Async create `PromptValue`.
|
||||
|
||||
Args:
|
||||
**kwargs: Any arguments to be passed to the prompt template.
|
||||
@@ -268,7 +274,7 @@ class BasePromptTemplate(
|
||||
"""Return a partial of the prompt template.
|
||||
|
||||
Args:
|
||||
**kwargs: partial variables to set.
|
||||
**kwargs: Partial variables to set.
|
||||
|
||||
Returns:
|
||||
A partial of the prompt template.
|
||||
@@ -298,9 +304,9 @@ class BasePromptTemplate(
|
||||
A formatted string.
|
||||
|
||||
Example:
|
||||
```python
|
||||
prompt.format(variable1="foo")
|
||||
```
|
||||
```python
|
||||
prompt.format(variable1="foo")
|
||||
```
|
||||
"""
|
||||
|
||||
async def aformat(self, **kwargs: Any) -> FormatOutputType:
|
||||
@@ -313,9 +319,9 @@ class BasePromptTemplate(
|
||||
A formatted string.
|
||||
|
||||
Example:
|
||||
```python
|
||||
await prompt.aformat(variable1="foo")
|
||||
```
|
||||
```python
|
||||
await prompt.aformat(variable1="foo")
|
||||
```
|
||||
"""
|
||||
return self.format(**kwargs)
|
||||
|
||||
@@ -350,9 +356,9 @@ class BasePromptTemplate(
|
||||
NotImplementedError: If the prompt type is not implemented.
|
||||
|
||||
Example:
|
||||
```python
|
||||
prompt.save(file_path="path/prompt.yaml")
|
||||
```
|
||||
```python
|
||||
prompt.save(file_path="path/prompt.yaml")
|
||||
```
|
||||
"""
|
||||
if self.partial_variables:
|
||||
msg = "Cannot save prompt with partial variables."
|
||||
@@ -404,23 +410,23 @@ def format_document(doc: Document, prompt: BasePromptTemplate[str]) -> str:
|
||||
|
||||
First, this pulls information from the document from two sources:
|
||||
|
||||
1. page_content:
|
||||
This takes the information from the `document.page_content`
|
||||
and assigns it to a variable named `page_content`.
|
||||
2. metadata:
|
||||
This takes information from `document.metadata` and assigns
|
||||
it to variables of the same name.
|
||||
1. `page_content`:
|
||||
This takes the information from the `document.page_content` and assigns it to a
|
||||
variable named `page_content`.
|
||||
2. `metadata`:
|
||||
This takes information from `document.metadata` and assigns it to variables of
|
||||
the same name.
|
||||
|
||||
Those variables are then passed into the `prompt` to produce a formatted string.
|
||||
|
||||
Args:
|
||||
doc: Document, the page_content and metadata will be used to create
|
||||
doc: `Document`, the `page_content` and `metadata` will be used to create
|
||||
the final string.
|
||||
prompt: BasePromptTemplate, will be used to format the page_content
|
||||
and metadata into the final string.
|
||||
prompt: `BasePromptTemplate`, will be used to format the `page_content`
|
||||
and `metadata` into the final string.
|
||||
|
||||
Returns:
|
||||
string of the document formatted.
|
||||
String of the document formatted.
|
||||
|
||||
Example:
|
||||
```python
|
||||
@@ -431,7 +437,6 @@ def format_document(doc: Document, prompt: BasePromptTemplate[str]) -> str:
|
||||
prompt = PromptTemplate.from_template("Page {page}: {page_content}")
|
||||
format_document(doc, prompt)
|
||||
>>> "Page 1: This is a joke"
|
||||
|
||||
```
|
||||
"""
|
||||
return prompt.format(**_get_document_info(doc, prompt))
|
||||
@@ -442,22 +447,22 @@ async def aformat_document(doc: Document, prompt: BasePromptTemplate[str]) -> st
|
||||
|
||||
First, this pulls information from the document from two sources:
|
||||
|
||||
1. page_content:
|
||||
This takes the information from the `document.page_content`
|
||||
and assigns it to a variable named `page_content`.
|
||||
2. metadata:
|
||||
This takes information from `document.metadata` and assigns
|
||||
it to variables of the same name.
|
||||
1. `page_content`:
|
||||
This takes the information from the `document.page_content` and assigns it to a
|
||||
variable named `page_content`.
|
||||
2. `metadata`:
|
||||
This takes information from `document.metadata` and assigns it to variables of
|
||||
the same name.
|
||||
|
||||
Those variables are then passed into the `prompt` to produce a formatted string.
|
||||
|
||||
Args:
|
||||
doc: Document, the page_content and metadata will be used to create
|
||||
doc: `Document`, the `page_content` and `metadata` will be used to create
|
||||
the final string.
|
||||
prompt: BasePromptTemplate, will be used to format the page_content
|
||||
and metadata into the final string.
|
||||
prompt: `BasePromptTemplate`, will be used to format the `page_content`
|
||||
and `metadata` into the final string.
|
||||
|
||||
Returns:
|
||||
string of the document formatted.
|
||||
String of the document formatted.
|
||||
"""
|
||||
return await prompt.aformat(**_get_document_info(doc, prompt))
|
||||
|
||||
@@ -587,14 +587,15 @@ class _StringImageMessagePromptTemplate(BaseMessagePromptTemplate):
|
||||
for prompt in self.prompt:
|
||||
inputs = {var: kwargs[var] for var in prompt.input_variables}
|
||||
if isinstance(prompt, StringPromptTemplate):
|
||||
formatted: str | ImageURL | dict[str, Any] = prompt.format(**inputs)
|
||||
content.append({"type": "text", "text": formatted})
|
||||
formatted_text: str = prompt.format(**inputs)
|
||||
if formatted_text != "":
|
||||
content.append({"type": "text", "text": formatted_text})
|
||||
elif isinstance(prompt, ImagePromptTemplate):
|
||||
formatted = prompt.format(**inputs)
|
||||
content.append({"type": "image_url", "image_url": formatted})
|
||||
formatted_image: ImageURL = prompt.format(**inputs)
|
||||
content.append({"type": "image_url", "image_url": formatted_image})
|
||||
elif isinstance(prompt, DictPromptTemplate):
|
||||
formatted = prompt.format(**inputs)
|
||||
content.append(formatted)
|
||||
formatted_dict: dict[str, Any] = prompt.format(**inputs)
|
||||
content.append(formatted_dict)
|
||||
return self._msg_class(
|
||||
content=content, additional_kwargs=self.additional_kwargs
|
||||
)
|
||||
@@ -617,16 +618,15 @@ class _StringImageMessagePromptTemplate(BaseMessagePromptTemplate):
|
||||
for prompt in self.prompt:
|
||||
inputs = {var: kwargs[var] for var in prompt.input_variables}
|
||||
if isinstance(prompt, StringPromptTemplate):
|
||||
formatted: str | ImageURL | dict[str, Any] = await prompt.aformat(
|
||||
**inputs
|
||||
)
|
||||
content.append({"type": "text", "text": formatted})
|
||||
formatted_text: str = await prompt.aformat(**inputs)
|
||||
if formatted_text != "":
|
||||
content.append({"type": "text", "text": formatted_text})
|
||||
elif isinstance(prompt, ImagePromptTemplate):
|
||||
formatted = await prompt.aformat(**inputs)
|
||||
content.append({"type": "image_url", "image_url": formatted})
|
||||
formatted_image: ImageURL = await prompt.aformat(**inputs)
|
||||
content.append({"type": "image_url", "image_url": formatted_image})
|
||||
elif isinstance(prompt, DictPromptTemplate):
|
||||
formatted = prompt.format(**inputs)
|
||||
content.append(formatted)
|
||||
formatted_dict: dict[str, Any] = prompt.format(**inputs)
|
||||
content.append(formatted_dict)
|
||||
return self._msg_class(
|
||||
content=content, additional_kwargs=self.additional_kwargs
|
||||
)
|
||||
@@ -903,23 +903,28 @@ class ChatPromptTemplate(BaseChatPromptTemplate):
|
||||
5. A string which is shorthand for `("human", template)`; e.g.,
|
||||
`"{user_input}"`
|
||||
template_format: Format of the template.
|
||||
input_variables: A list of the names of the variables whose values are
|
||||
required as inputs to the prompt.
|
||||
optional_variables: A list of the names of the variables for placeholder
|
||||
or MessagePlaceholder that are optional.
|
||||
**kwargs: Additional keyword arguments passed to `BasePromptTemplate`,
|
||||
including (but not limited to):
|
||||
|
||||
These variables are auto inferred from the prompt and user need not
|
||||
provide them.
|
||||
partial_variables: A dictionary of the partial variables the prompt
|
||||
template carries.
|
||||
- `input_variables`: A list of the names of the variables whose values
|
||||
are required as inputs to the prompt.
|
||||
- `optional_variables`: A list of the names of the variables for
|
||||
placeholder or `MessagePlaceholder` that are optional.
|
||||
|
||||
Partial variables populate the template so that you don't need to pass
|
||||
them in every time you call the prompt.
|
||||
validate_template: Whether to validate the template.
|
||||
input_types: A dictionary of the types of the variables the prompt template
|
||||
expects.
|
||||
These variables are auto inferred from the prompt and user need not
|
||||
provide them.
|
||||
|
||||
If not provided, all variables are assumed to be strings.
|
||||
- `partial_variables`: A dictionary of the partial variables the prompt
|
||||
template carries.
|
||||
|
||||
Partial variables populate the template so that you don't need to
|
||||
pass them in every time you call the prompt.
|
||||
|
||||
- `validate_template`: Whether to validate the template.
|
||||
- `input_types`: A dictionary of the types of the variables the prompt
|
||||
template expects.
|
||||
|
||||
If not provided, all variables are assumed to be strings.
|
||||
|
||||
Examples:
|
||||
Instantiation from a list of message templates:
|
||||
@@ -1343,11 +1348,25 @@ def _create_template_from_message_type(
|
||||
raise ValueError(msg)
|
||||
var_name = template[1:-1]
|
||||
message = MessagesPlaceholder(variable_name=var_name, optional=True)
|
||||
elif len(template) == 2 and isinstance(template[1], bool):
|
||||
var_name_wrapped, is_optional = template
|
||||
else:
|
||||
try:
|
||||
var_name_wrapped, is_optional = template
|
||||
except ValueError as e:
|
||||
msg = (
|
||||
"Unexpected arguments for placeholder message type."
|
||||
" Expected either a single string variable name"
|
||||
" or a list of [variable_name: str, is_optional: bool]."
|
||||
f" Got: {template}"
|
||||
)
|
||||
raise ValueError(msg) from e
|
||||
|
||||
if not isinstance(is_optional, bool):
|
||||
msg = f"Expected is_optional to be a boolean. Got: {is_optional}"
|
||||
raise ValueError(msg) # noqa: TRY004
|
||||
|
||||
if not isinstance(var_name_wrapped, str):
|
||||
msg = f"Expected variable name to be a string. Got: {var_name_wrapped}"
|
||||
raise ValueError(msg) # noqa:TRY004
|
||||
raise ValueError(msg) # noqa: TRY004
|
||||
if var_name_wrapped[0] != "{" or var_name_wrapped[-1] != "}":
|
||||
msg = (
|
||||
f"Invalid placeholder template: {var_name_wrapped}."
|
||||
@@ -1357,14 +1376,6 @@ def _create_template_from_message_type(
|
||||
var_name = var_name_wrapped[1:-1]
|
||||
|
||||
message = MessagesPlaceholder(variable_name=var_name, optional=is_optional)
|
||||
else:
|
||||
msg = (
|
||||
"Unexpected arguments for placeholder message type."
|
||||
" Expected either a single string variable name"
|
||||
" or a list of [variable_name: str, is_optional: bool]."
|
||||
f" Got: {template}"
|
||||
)
|
||||
raise ValueError(msg)
|
||||
else:
|
||||
msg = (
|
||||
f"Unexpected message type: {message_type}. Use one of 'human',"
|
||||
@@ -1418,10 +1429,11 @@ def _convert_to_message_template(
|
||||
)
|
||||
raise ValueError(msg)
|
||||
message = (message["role"], message["content"])
|
||||
if len(message) != 2:
|
||||
try:
|
||||
message_type_str, template = message
|
||||
except ValueError as e:
|
||||
msg = f"Expected 2-tuple of (role, template), got {message}"
|
||||
raise ValueError(msg)
|
||||
message_type_str, template = message
|
||||
raise ValueError(msg) from e
|
||||
if isinstance(message_type_str, str):
|
||||
message_ = _create_template_from_message_type(
|
||||
message_type_str, template, template_format=template_format
|
||||
|
||||
@@ -69,7 +69,7 @@ class DictPromptTemplate(RunnableSerializable[dict, dict]):
|
||||
|
||||
@classmethod
|
||||
def is_lc_serializable(cls) -> bool:
|
||||
"""Return True as this class is serializable."""
|
||||
"""Return `True` as this class is serializable."""
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -6,10 +6,10 @@ from abc import ABC, abstractmethod
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from langchain_core.load import Serializable
|
||||
from langchain_core.messages import BaseMessage
|
||||
from langchain_core.utils.interactive_env import is_interactive_env
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langchain_core.messages import BaseMessage
|
||||
from langchain_core.prompts.chat import ChatPromptTemplate
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ class BaseMessagePromptTemplate(Serializable, ABC):
|
||||
|
||||
@classmethod
|
||||
def is_lc_serializable(cls) -> bool:
|
||||
"""Return True as this class is serializable."""
|
||||
"""Return `True` as this class is serializable."""
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
@@ -32,13 +32,13 @@ class BaseMessagePromptTemplate(Serializable, ABC):
|
||||
|
||||
@abstractmethod
|
||||
def format_messages(self, **kwargs: Any) -> list[BaseMessage]:
|
||||
"""Format messages from kwargs. Should return a list of BaseMessages.
|
||||
"""Format messages from kwargs. Should return a list of `BaseMessage` objects.
|
||||
|
||||
Args:
|
||||
**kwargs: Keyword arguments to use for formatting.
|
||||
|
||||
Returns:
|
||||
List of BaseMessages.
|
||||
List of `BaseMessage` objects.
|
||||
"""
|
||||
|
||||
async def aformat_messages(self, **kwargs: Any) -> list[BaseMessage]:
|
||||
@@ -48,7 +48,7 @@ class BaseMessagePromptTemplate(Serializable, ABC):
|
||||
**kwargs: Keyword arguments to use for formatting.
|
||||
|
||||
Returns:
|
||||
List of BaseMessages.
|
||||
List of `BaseMessage` objects.
|
||||
"""
|
||||
return self.format_messages(**kwargs)
|
||||
|
||||
|
||||
@@ -4,9 +4,8 @@ from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
from abc import ABC
|
||||
from collections.abc import Callable, Sequence
|
||||
from string import Formatter
|
||||
from typing import Any, Literal
|
||||
from typing import TYPE_CHECKING, Any, Literal
|
||||
|
||||
from pydantic import BaseModel, create_model
|
||||
|
||||
@@ -16,10 +15,70 @@ from langchain_core.utils import get_colored_text, mustache
|
||||
from langchain_core.utils.formatting import formatter
|
||||
from langchain_core.utils.interactive_env import is_interactive_env
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable, Sequence
|
||||
|
||||
try:
|
||||
from jinja2 import Environment, meta
|
||||
from jinja2 import meta
|
||||
from jinja2.exceptions import SecurityError
|
||||
from jinja2.sandbox import SandboxedEnvironment
|
||||
|
||||
class _RestrictedSandboxedEnvironment(SandboxedEnvironment):
|
||||
"""A more restrictive Jinja2 sandbox that blocks all attribute/method access.
|
||||
|
||||
This sandbox only allows simple variable lookups, no attribute or method access.
|
||||
This prevents template injection attacks via methods like parse_raw().
|
||||
"""
|
||||
|
||||
def is_safe_attribute(self, _obj: Any, _attr: str, _value: Any) -> bool:
|
||||
"""Block ALL attribute access for security.
|
||||
|
||||
Only allow accessing variables directly from the context dict,
|
||||
no attribute access on those objects.
|
||||
|
||||
Args:
|
||||
_obj: The object being accessed (unused, always blocked).
|
||||
_attr: The attribute name (unused, always blocked).
|
||||
_value: The attribute value (unused, always blocked).
|
||||
|
||||
Returns:
|
||||
False - all attribute access is blocked.
|
||||
"""
|
||||
# Block all attribute access
|
||||
return False
|
||||
|
||||
def is_safe_callable(self, _obj: Any) -> bool:
|
||||
"""Block all method calls for security.
|
||||
|
||||
Args:
|
||||
_obj: The object being checked (unused, always blocked).
|
||||
|
||||
Returns:
|
||||
False - all callables are blocked.
|
||||
"""
|
||||
return False
|
||||
|
||||
def getattr(self, obj: Any, attribute: str) -> Any:
|
||||
"""Override getattr to block all attribute access.
|
||||
|
||||
Args:
|
||||
obj: The object.
|
||||
attribute: The attribute name.
|
||||
|
||||
Returns:
|
||||
Never returns.
|
||||
|
||||
Raises:
|
||||
SecurityError: Always, to block attribute access.
|
||||
"""
|
||||
msg = (
|
||||
f"Access to attributes is not allowed in templates. "
|
||||
f"Attempted to access '{attribute}' on {type(obj).__name__}. "
|
||||
f"Use only simple variable names like {{{{variable}}}} "
|
||||
f"without dots or methods."
|
||||
)
|
||||
raise SecurityError(msg)
|
||||
|
||||
_HAS_JINJA2 = True
|
||||
except ImportError:
|
||||
_HAS_JINJA2 = False
|
||||
@@ -59,14 +118,10 @@ def jinja2_formatter(template: str, /, **kwargs: Any) -> str:
|
||||
)
|
||||
raise ImportError(msg)
|
||||
|
||||
# This uses a sandboxed environment to prevent arbitrary code execution.
|
||||
# Jinja2 uses an opt-out rather than opt-in approach for sand-boxing.
|
||||
# Please treat this sand-boxing as a best-effort approach rather than
|
||||
# a guarantee of security.
|
||||
# We recommend to never use jinja2 templates with untrusted inputs.
|
||||
# https://jinja.palletsprojects.com/en/3.1.x/sandbox/
|
||||
# approach not a guarantee of security.
|
||||
return SandboxedEnvironment().from_string(template).render(**kwargs)
|
||||
# Use a restricted sandbox that blocks ALL attribute/method access
|
||||
# Only simple variable lookups like {{variable}} are allowed
|
||||
# Attribute access like {{variable.attr}} or {{variable.method()}} is blocked
|
||||
return _RestrictedSandboxedEnvironment().from_string(template).render(**kwargs)
|
||||
|
||||
|
||||
def validate_jinja2(template: str, input_variables: list[str]) -> None:
|
||||
@@ -101,7 +156,7 @@ def _get_jinja2_variables_from_template(template: str) -> set[str]:
|
||||
"Please install it with `pip install jinja2`."
|
||||
)
|
||||
raise ImportError(msg)
|
||||
env = Environment() # noqa: S701
|
||||
env = _RestrictedSandboxedEnvironment()
|
||||
ast = env.parse(template)
|
||||
return meta.find_undeclared_variables(ast)
|
||||
|
||||
@@ -271,6 +326,30 @@ def get_template_variables(template: str, template_format: str) -> list[str]:
|
||||
msg = f"Unsupported template format: {template_format}"
|
||||
raise ValueError(msg)
|
||||
|
||||
# For f-strings, block attribute access and indexing syntax
|
||||
# This prevents template injection attacks via accessing dangerous attributes
|
||||
if template_format == "f-string":
|
||||
for var in input_variables:
|
||||
# Formatter().parse() returns field names with dots/brackets if present
|
||||
# e.g., "obj.attr" or "obj[0]" - we need to block these
|
||||
if "." in var or "[" in var or "]" in var:
|
||||
msg = (
|
||||
f"Invalid variable name {var!r} in f-string template. "
|
||||
f"Variable names cannot contain attribute "
|
||||
f"access (.) or indexing ([])."
|
||||
)
|
||||
raise ValueError(msg)
|
||||
|
||||
# Block variable names that are all digits (e.g., "0", "100")
|
||||
# These are interpreted as positional arguments, not keyword arguments
|
||||
if var.isdigit():
|
||||
msg = (
|
||||
f"Invalid variable name {var!r} in f-string template. "
|
||||
f"Variable names cannot be all digits as they are interpreted "
|
||||
f"as positional arguments."
|
||||
)
|
||||
raise ValueError(msg)
|
||||
|
||||
return sorted(input_variables)
|
||||
|
||||
|
||||
|
||||
@@ -49,7 +49,13 @@ class StructuredPrompt(ChatPromptTemplate):
|
||||
structured_output_kwargs: additional kwargs for structured output.
|
||||
template_format: template format for the prompt.
|
||||
"""
|
||||
schema_ = schema_ or kwargs.pop("schema")
|
||||
schema_ = schema_ or kwargs.pop("schema", None)
|
||||
if not schema_:
|
||||
err_msg = (
|
||||
"Must pass in a non-empty structured output schema. Received: "
|
||||
f"{schema_}"
|
||||
)
|
||||
raise ValueError(err_msg)
|
||||
structured_output_kwargs = structured_output_kwargs or {}
|
||||
for k in set(kwargs).difference(get_pydantic_field_names(self.__class__)):
|
||||
structured_output_kwargs[k] = kwargs.pop(k)
|
||||
|
||||
@@ -118,6 +118,8 @@ if TYPE_CHECKING:
|
||||
|
||||
Other = TypeVar("Other")
|
||||
|
||||
_RUNNABLE_GENERIC_NUM_ARGS = 2 # Input and Output
|
||||
|
||||
|
||||
class Runnable(ABC, Generic[Input, Output]):
|
||||
"""A unit of work that can be invoked, batched, streamed, transformed and composed.
|
||||
@@ -309,7 +311,10 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
for base in self.__class__.mro():
|
||||
if hasattr(base, "__pydantic_generic_metadata__"):
|
||||
metadata = base.__pydantic_generic_metadata__
|
||||
if "args" in metadata and len(metadata["args"]) == 2:
|
||||
if (
|
||||
"args" in metadata
|
||||
and len(metadata["args"]) == _RUNNABLE_GENERIC_NUM_ARGS
|
||||
):
|
||||
return metadata["args"][0]
|
||||
|
||||
# If we didn't find a Pydantic model in the parent classes,
|
||||
@@ -317,7 +322,7 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
# Runnables that are not pydantic models.
|
||||
for cls in self.__class__.__orig_bases__: # type: ignore[attr-defined]
|
||||
type_args = get_args(cls)
|
||||
if type_args and len(type_args) == 2:
|
||||
if type_args and len(type_args) == _RUNNABLE_GENERIC_NUM_ARGS:
|
||||
return type_args[0]
|
||||
|
||||
msg = (
|
||||
@@ -340,12 +345,15 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
for base in self.__class__.mro():
|
||||
if hasattr(base, "__pydantic_generic_metadata__"):
|
||||
metadata = base.__pydantic_generic_metadata__
|
||||
if "args" in metadata and len(metadata["args"]) == 2:
|
||||
if (
|
||||
"args" in metadata
|
||||
and len(metadata["args"]) == _RUNNABLE_GENERIC_NUM_ARGS
|
||||
):
|
||||
return metadata["args"][1]
|
||||
|
||||
for cls in self.__class__.__orig_bases__: # type: ignore[attr-defined]
|
||||
type_args = get_args(cls)
|
||||
if type_args and len(type_args) == 2:
|
||||
if type_args and len(type_args) == _RUNNABLE_GENERIC_NUM_ARGS:
|
||||
return type_args[1]
|
||||
|
||||
msg = (
|
||||
@@ -424,7 +432,7 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
print(runnable.get_input_jsonschema())
|
||||
```
|
||||
|
||||
!!! version-added "Added in version 0.3.0"
|
||||
!!! version-added "Added in `langchain-core` 0.3.0"
|
||||
|
||||
"""
|
||||
return self.get_input_schema(config).model_json_schema()
|
||||
@@ -502,7 +510,7 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
print(runnable.get_output_jsonschema())
|
||||
```
|
||||
|
||||
!!! version-added "Added in version 0.3.0"
|
||||
!!! version-added "Added in `langchain-core` 0.3.0"
|
||||
|
||||
"""
|
||||
return self.get_output_schema(config).model_json_schema()
|
||||
@@ -566,7 +574,7 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
Returns:
|
||||
A JSON schema that represents the config of the `Runnable`.
|
||||
|
||||
!!! version-added "Added in version 0.3.0"
|
||||
!!! version-added "Added in `langchain-core` 0.3.0"
|
||||
|
||||
"""
|
||||
return self.config_schema(include=include).model_json_schema()
|
||||
@@ -699,51 +707,53 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
def pick(self, keys: str | list[str]) -> RunnableSerializable[Any, Any]:
|
||||
"""Pick keys from the output `dict` of this `Runnable`.
|
||||
|
||||
Pick a single key:
|
||||
!!! example "Pick a single key"
|
||||
|
||||
```python
|
||||
import json
|
||||
```python
|
||||
import json
|
||||
|
||||
from langchain_core.runnables import RunnableLambda, RunnableMap
|
||||
from langchain_core.runnables import RunnableLambda, RunnableMap
|
||||
|
||||
as_str = RunnableLambda(str)
|
||||
as_json = RunnableLambda(json.loads)
|
||||
chain = RunnableMap(str=as_str, json=as_json)
|
||||
as_str = RunnableLambda(str)
|
||||
as_json = RunnableLambda(json.loads)
|
||||
chain = RunnableMap(str=as_str, json=as_json)
|
||||
|
||||
chain.invoke("[1, 2, 3]")
|
||||
# -> {"str": "[1, 2, 3]", "json": [1, 2, 3]}
|
||||
chain.invoke("[1, 2, 3]")
|
||||
# -> {"str": "[1, 2, 3]", "json": [1, 2, 3]}
|
||||
|
||||
json_only_chain = chain.pick("json")
|
||||
json_only_chain.invoke("[1, 2, 3]")
|
||||
# -> [1, 2, 3]
|
||||
```
|
||||
json_only_chain = chain.pick("json")
|
||||
json_only_chain.invoke("[1, 2, 3]")
|
||||
# -> [1, 2, 3]
|
||||
```
|
||||
|
||||
Pick a list of keys:
|
||||
!!! example "Pick a list of keys"
|
||||
|
||||
```python
|
||||
from typing import Any
|
||||
```python
|
||||
from typing import Any
|
||||
|
||||
import json
|
||||
import json
|
||||
|
||||
from langchain_core.runnables import RunnableLambda, RunnableMap
|
||||
from langchain_core.runnables import RunnableLambda, RunnableMap
|
||||
|
||||
as_str = RunnableLambda(str)
|
||||
as_json = RunnableLambda(json.loads)
|
||||
as_str = RunnableLambda(str)
|
||||
as_json = RunnableLambda(json.loads)
|
||||
|
||||
|
||||
def as_bytes(x: Any) -> bytes:
|
||||
return bytes(x, "utf-8")
|
||||
def as_bytes(x: Any) -> bytes:
|
||||
return bytes(x, "utf-8")
|
||||
|
||||
|
||||
chain = RunnableMap(str=as_str, json=as_json, bytes=RunnableLambda(as_bytes))
|
||||
chain = RunnableMap(
|
||||
str=as_str, json=as_json, bytes=RunnableLambda(as_bytes)
|
||||
)
|
||||
|
||||
chain.invoke("[1, 2, 3]")
|
||||
# -> {"str": "[1, 2, 3]", "json": [1, 2, 3], "bytes": b"[1, 2, 3]"}
|
||||
chain.invoke("[1, 2, 3]")
|
||||
# -> {"str": "[1, 2, 3]", "json": [1, 2, 3], "bytes": b"[1, 2, 3]"}
|
||||
|
||||
json_and_bytes_chain = chain.pick(["json", "bytes"])
|
||||
json_and_bytes_chain.invoke("[1, 2, 3]")
|
||||
# -> {"json": [1, 2, 3], "bytes": b"[1, 2, 3]"}
|
||||
```
|
||||
json_and_bytes_chain = chain.pick(["json", "bytes"])
|
||||
json_and_bytes_chain.invoke("[1, 2, 3]")
|
||||
# -> {"json": [1, 2, 3], "bytes": b"[1, 2, 3]"}
|
||||
```
|
||||
|
||||
Args:
|
||||
keys: A key or list of keys to pick from the output dict.
|
||||
@@ -766,7 +776,7 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
"""Assigns new fields to the `dict` output of this `Runnable`.
|
||||
|
||||
```python
|
||||
from langchain_community.llms.fake import FakeStreamingListLLM
|
||||
from langchain_core.language_models.fake import FakeStreamingListLLM
|
||||
from langchain_core.output_parsers import StrOutputParser
|
||||
from langchain_core.prompts import SystemMessagePromptTemplate
|
||||
from langchain_core.runnables import Runnable
|
||||
@@ -818,10 +828,12 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
Args:
|
||||
input: The input to the `Runnable`.
|
||||
config: A config to use when invoking the `Runnable`.
|
||||
|
||||
The config supports standard keys like `'tags'`, `'metadata'` for
|
||||
tracing purposes, `'max_concurrency'` for controlling how much work to
|
||||
do in parallel, and other keys. Please refer to the `RunnableConfig`
|
||||
for more details.
|
||||
do in parallel, and other keys.
|
||||
|
||||
Please refer to `RunnableConfig` for more details.
|
||||
|
||||
Returns:
|
||||
The output of the `Runnable`.
|
||||
@@ -838,10 +850,12 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
Args:
|
||||
input: The input to the `Runnable`.
|
||||
config: A config to use when invoking the `Runnable`.
|
||||
|
||||
The config supports standard keys like `'tags'`, `'metadata'` for
|
||||
tracing purposes, `'max_concurrency'` for controlling how much work to
|
||||
do in parallel, and other keys. Please refer to the `RunnableConfig`
|
||||
for more details.
|
||||
do in parallel, and other keys.
|
||||
|
||||
Please refer to `RunnableConfig` for more details.
|
||||
|
||||
Returns:
|
||||
The output of the `Runnable`.
|
||||
@@ -868,8 +882,9 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
config: A config to use when invoking the `Runnable`. The config supports
|
||||
standard keys like `'tags'`, `'metadata'` for
|
||||
tracing purposes, `'max_concurrency'` for controlling how much work
|
||||
to do in parallel, and other keys. Please refer to the
|
||||
`RunnableConfig` for more details.
|
||||
to do in parallel, and other keys.
|
||||
|
||||
Please refer to `RunnableConfig` for more details.
|
||||
return_exceptions: Whether to return exceptions instead of raising them.
|
||||
**kwargs: Additional keyword arguments to pass to the `Runnable`.
|
||||
|
||||
@@ -932,10 +947,12 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
Args:
|
||||
inputs: A list of inputs to the `Runnable`.
|
||||
config: A config to use when invoking the `Runnable`.
|
||||
|
||||
The config supports standard keys like `'tags'`, `'metadata'` for
|
||||
tracing purposes, `'max_concurrency'` for controlling how much work to
|
||||
do in parallel, and other keys. Please refer to the `RunnableConfig`
|
||||
for more details.
|
||||
do in parallel, and other keys.
|
||||
|
||||
Please refer to `RunnableConfig` for more details.
|
||||
return_exceptions: Whether to return exceptions instead of raising them.
|
||||
**kwargs: Additional keyword arguments to pass to the `Runnable`.
|
||||
|
||||
@@ -998,10 +1015,12 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
Args:
|
||||
inputs: A list of inputs to the `Runnable`.
|
||||
config: A config to use when invoking the `Runnable`.
|
||||
|
||||
The config supports standard keys like `'tags'`, `'metadata'` for
|
||||
tracing purposes, `'max_concurrency'` for controlling how much work to
|
||||
do in parallel, and other keys. Please refer to the `RunnableConfig`
|
||||
for more details.
|
||||
do in parallel, and other keys.
|
||||
|
||||
Please refer to `RunnableConfig` for more details.
|
||||
return_exceptions: Whether to return exceptions instead of raising them.
|
||||
**kwargs: Additional keyword arguments to pass to the `Runnable`.
|
||||
|
||||
@@ -1061,10 +1080,12 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
Args:
|
||||
inputs: A list of inputs to the `Runnable`.
|
||||
config: A config to use when invoking the `Runnable`.
|
||||
|
||||
The config supports standard keys like `'tags'`, `'metadata'` for
|
||||
tracing purposes, `'max_concurrency'` for controlling how much work to
|
||||
do in parallel, and other keys. Please refer to the `RunnableConfig`
|
||||
for more details.
|
||||
do in parallel, and other keys.
|
||||
|
||||
Please refer to `RunnableConfig` for more details.
|
||||
return_exceptions: Whether to return exceptions instead of raising them.
|
||||
**kwargs: Additional keyword arguments to pass to the `Runnable`.
|
||||
|
||||
@@ -1353,48 +1374,50 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
).with_config({"run_name": "my_template", "tags": ["my_template"]})
|
||||
```
|
||||
|
||||
For instance:
|
||||
!!! example
|
||||
|
||||
```python
|
||||
from langchain_core.runnables import RunnableLambda
|
||||
```python
|
||||
from langchain_core.runnables import RunnableLambda
|
||||
|
||||
|
||||
async def reverse(s: str) -> str:
|
||||
return s[::-1]
|
||||
async def reverse(s: str) -> str:
|
||||
return s[::-1]
|
||||
|
||||
|
||||
chain = RunnableLambda(func=reverse)
|
||||
chain = RunnableLambda(func=reverse)
|
||||
|
||||
events = [event async for event in chain.astream_events("hello", version="v2")]
|
||||
events = [
|
||||
event async for event in chain.astream_events("hello", version="v2")
|
||||
]
|
||||
|
||||
# Will produce the following events
|
||||
# (run_id, and parent_ids has been omitted for brevity):
|
||||
[
|
||||
{
|
||||
"data": {"input": "hello"},
|
||||
"event": "on_chain_start",
|
||||
"metadata": {},
|
||||
"name": "reverse",
|
||||
"tags": [],
|
||||
},
|
||||
{
|
||||
"data": {"chunk": "olleh"},
|
||||
"event": "on_chain_stream",
|
||||
"metadata": {},
|
||||
"name": "reverse",
|
||||
"tags": [],
|
||||
},
|
||||
{
|
||||
"data": {"output": "olleh"},
|
||||
"event": "on_chain_end",
|
||||
"metadata": {},
|
||||
"name": "reverse",
|
||||
"tags": [],
|
||||
},
|
||||
]
|
||||
```
|
||||
# Will produce the following events
|
||||
# (run_id, and parent_ids has been omitted for brevity):
|
||||
[
|
||||
{
|
||||
"data": {"input": "hello"},
|
||||
"event": "on_chain_start",
|
||||
"metadata": {},
|
||||
"name": "reverse",
|
||||
"tags": [],
|
||||
},
|
||||
{
|
||||
"data": {"chunk": "olleh"},
|
||||
"event": "on_chain_stream",
|
||||
"metadata": {},
|
||||
"name": "reverse",
|
||||
"tags": [],
|
||||
},
|
||||
{
|
||||
"data": {"output": "olleh"},
|
||||
"event": "on_chain_end",
|
||||
"metadata": {},
|
||||
"name": "reverse",
|
||||
"tags": [],
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
```python title="Example: Dispatch Custom Event"
|
||||
```python title="Dispatch custom event"
|
||||
from langchain_core.callbacks.manager import (
|
||||
adispatch_custom_event,
|
||||
)
|
||||
@@ -1428,10 +1451,13 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
Args:
|
||||
input: The input to the `Runnable`.
|
||||
config: The config to use for the `Runnable`.
|
||||
version: The version of the schema to use either `'v2'` or `'v1'`.
|
||||
version: The version of the schema to use, either `'v2'` or `'v1'`.
|
||||
|
||||
Users should use `'v2'`.
|
||||
|
||||
`'v1'` is for backwards compatibility and will be deprecated
|
||||
in `0.4.0`.
|
||||
|
||||
No default will be assigned until the API is stabilized.
|
||||
custom events will only be surfaced in `'v2'`.
|
||||
include_names: Only include events from `Runnable` objects with matching names.
|
||||
@@ -1441,6 +1467,7 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
exclude_types: Exclude events from `Runnable` objects with matching types.
|
||||
exclude_tags: Exclude events from `Runnable` objects with matching tags.
|
||||
**kwargs: Additional keyword arguments to pass to the `Runnable`.
|
||||
|
||||
These will be passed to `astream_log` as this implementation
|
||||
of `astream_events` is built on top of `astream_log`.
|
||||
|
||||
@@ -1742,46 +1769,52 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
import time
|
||||
import asyncio
|
||||
|
||||
|
||||
def format_t(timestamp: float) -> str:
|
||||
return datetime.fromtimestamp(timestamp, tz=timezone.utc).isoformat()
|
||||
|
||||
|
||||
async def test_runnable(time_to_sleep: int):
|
||||
print(f"Runnable[{time_to_sleep}s]: starts at {format_t(time.time())}")
|
||||
await asyncio.sleep(time_to_sleep)
|
||||
print(f"Runnable[{time_to_sleep}s]: ends at {format_t(time.time())}")
|
||||
|
||||
|
||||
async def fn_start(run_obj: Runnable):
|
||||
print(f"on start callback starts at {format_t(time.time())}")
|
||||
await asyncio.sleep(3)
|
||||
print(f"on start callback ends at {format_t(time.time())}")
|
||||
|
||||
|
||||
async def fn_end(run_obj: Runnable):
|
||||
print(f"on end callback starts at {format_t(time.time())}")
|
||||
await asyncio.sleep(2)
|
||||
print(f"on end callback ends at {format_t(time.time())}")
|
||||
|
||||
|
||||
runnable = RunnableLambda(test_runnable).with_alisteners(
|
||||
on_start=fn_start,
|
||||
on_end=fn_end
|
||||
on_start=fn_start, on_end=fn_end
|
||||
)
|
||||
|
||||
|
||||
async def concurrent_runs():
|
||||
await asyncio.gather(runnable.ainvoke(2), runnable.ainvoke(3))
|
||||
|
||||
asyncio.run(concurrent_runs())
|
||||
Result:
|
||||
on start callback starts at 2025-03-01T07:05:22.875378+00:00
|
||||
on start callback starts at 2025-03-01T07:05:22.875495+00:00
|
||||
on start callback ends at 2025-03-01T07:05:25.878862+00:00
|
||||
on start callback ends at 2025-03-01T07:05:25.878947+00:00
|
||||
Runnable[2s]: starts at 2025-03-01T07:05:25.879392+00:00
|
||||
Runnable[3s]: starts at 2025-03-01T07:05:25.879804+00:00
|
||||
Runnable[2s]: ends at 2025-03-01T07:05:27.881998+00:00
|
||||
on end callback starts at 2025-03-01T07:05:27.882360+00:00
|
||||
Runnable[3s]: ends at 2025-03-01T07:05:28.881737+00:00
|
||||
on end callback starts at 2025-03-01T07:05:28.882428+00:00
|
||||
on end callback ends at 2025-03-01T07:05:29.883893+00:00
|
||||
on end callback ends at 2025-03-01T07:05:30.884831+00:00
|
||||
|
||||
asyncio.run(concurrent_runs())
|
||||
# Result:
|
||||
# on start callback starts at 2025-03-01T07:05:22.875378+00:00
|
||||
# on start callback starts at 2025-03-01T07:05:22.875495+00:00
|
||||
# on start callback ends at 2025-03-01T07:05:25.878862+00:00
|
||||
# on start callback ends at 2025-03-01T07:05:25.878947+00:00
|
||||
# Runnable[2s]: starts at 2025-03-01T07:05:25.879392+00:00
|
||||
# Runnable[3s]: starts at 2025-03-01T07:05:25.879804+00:00
|
||||
# Runnable[2s]: ends at 2025-03-01T07:05:27.881998+00:00
|
||||
# on end callback starts at 2025-03-01T07:05:27.882360+00:00
|
||||
# Runnable[3s]: ends at 2025-03-01T07:05:28.881737+00:00
|
||||
# on end callback starts at 2025-03-01T07:05:28.882428+00:00
|
||||
# on end callback ends at 2025-03-01T07:05:29.883893+00:00
|
||||
# on end callback ends at 2025-03-01T07:05:30.884831+00:00
|
||||
```
|
||||
"""
|
||||
return RunnableBinding(
|
||||
@@ -1843,7 +1876,7 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
`exp_base`, and `jitter` (all `float` values).
|
||||
|
||||
Returns:
|
||||
A new Runnable that retries the original Runnable on exceptions.
|
||||
A new `Runnable` that retries the original `Runnable` on exceptions.
|
||||
|
||||
Example:
|
||||
```python
|
||||
@@ -1927,7 +1960,9 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
exceptions_to_handle: A tuple of exception types to handle.
|
||||
exception_key: If `string` is specified then handled exceptions will be
|
||||
passed to fallbacks as part of the input under the specified key.
|
||||
|
||||
If `None`, exceptions will not be passed to fallbacks.
|
||||
|
||||
If used, the base `Runnable` and its fallbacks must accept a
|
||||
dictionary as input.
|
||||
|
||||
@@ -1963,7 +1998,9 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
exceptions_to_handle: A tuple of exception types to handle.
|
||||
exception_key: If `string` is specified then handled exceptions will be
|
||||
passed to fallbacks as part of the input under the specified key.
|
||||
|
||||
If `None`, exceptions will not be passed to fallbacks.
|
||||
|
||||
If used, the base `Runnable` and its fallbacks must accept a
|
||||
dictionary as input.
|
||||
|
||||
@@ -2429,10 +2466,14 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
|
||||
`as_tool` will instantiate a `BaseTool` with a name, description, and
|
||||
`args_schema` from a `Runnable`. Where possible, schemas are inferred
|
||||
from `runnable.get_input_schema`. Alternatively (e.g., if the
|
||||
`Runnable` takes a dict as input and the specific dict keys are not typed),
|
||||
the schema can be specified directly with `args_schema`. You can also
|
||||
pass `arg_types` to just specify the required arguments and their types.
|
||||
from `runnable.get_input_schema`.
|
||||
|
||||
Alternatively (e.g., if the `Runnable` takes a dict as input and the specific
|
||||
`dict` keys are not typed), the schema can be specified directly with
|
||||
`args_schema`.
|
||||
|
||||
You can also pass `arg_types` to just specify the required arguments and their
|
||||
types.
|
||||
|
||||
Args:
|
||||
args_schema: The schema for the tool.
|
||||
@@ -2443,82 +2484,82 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
Returns:
|
||||
A `BaseTool` instance.
|
||||
|
||||
Typed dict input:
|
||||
!!! example "`TypedDict` input"
|
||||
|
||||
```python
|
||||
from typing_extensions import TypedDict
|
||||
from langchain_core.runnables import RunnableLambda
|
||||
```python
|
||||
from typing_extensions import TypedDict
|
||||
from langchain_core.runnables import RunnableLambda
|
||||
|
||||
|
||||
class Args(TypedDict):
|
||||
a: int
|
||||
b: list[int]
|
||||
class Args(TypedDict):
|
||||
a: int
|
||||
b: list[int]
|
||||
|
||||
|
||||
def f(x: Args) -> str:
|
||||
return str(x["a"] * max(x["b"]))
|
||||
def f(x: Args) -> str:
|
||||
return str(x["a"] * max(x["b"]))
|
||||
|
||||
|
||||
runnable = RunnableLambda(f)
|
||||
as_tool = runnable.as_tool()
|
||||
as_tool.invoke({"a": 3, "b": [1, 2]})
|
||||
```
|
||||
runnable = RunnableLambda(f)
|
||||
as_tool = runnable.as_tool()
|
||||
as_tool.invoke({"a": 3, "b": [1, 2]})
|
||||
```
|
||||
|
||||
`dict` input, specifying schema via `args_schema`:
|
||||
!!! example "`dict` input, specifying schema via `args_schema`"
|
||||
|
||||
```python
|
||||
from typing import Any
|
||||
from pydantic import BaseModel, Field
|
||||
from langchain_core.runnables import RunnableLambda
|
||||
```python
|
||||
from typing import Any
|
||||
from pydantic import BaseModel, Field
|
||||
from langchain_core.runnables import RunnableLambda
|
||||
|
||||
def f(x: dict[str, Any]) -> str:
|
||||
return str(x["a"] * max(x["b"]))
|
||||
def f(x: dict[str, Any]) -> str:
|
||||
return str(x["a"] * max(x["b"]))
|
||||
|
||||
class FSchema(BaseModel):
|
||||
\"\"\"Apply a function to an integer and list of integers.\"\"\"
|
||||
class FSchema(BaseModel):
|
||||
\"\"\"Apply a function to an integer and list of integers.\"\"\"
|
||||
|
||||
a: int = Field(..., description="Integer")
|
||||
b: list[int] = Field(..., description="List of ints")
|
||||
a: int = Field(..., description="Integer")
|
||||
b: list[int] = Field(..., description="List of ints")
|
||||
|
||||
runnable = RunnableLambda(f)
|
||||
as_tool = runnable.as_tool(FSchema)
|
||||
as_tool.invoke({"a": 3, "b": [1, 2]})
|
||||
```
|
||||
runnable = RunnableLambda(f)
|
||||
as_tool = runnable.as_tool(FSchema)
|
||||
as_tool.invoke({"a": 3, "b": [1, 2]})
|
||||
```
|
||||
|
||||
`dict` input, specifying schema via `arg_types`:
|
||||
!!! example "`dict` input, specifying schema via `arg_types`"
|
||||
|
||||
```python
|
||||
from typing import Any
|
||||
from langchain_core.runnables import RunnableLambda
|
||||
```python
|
||||
from typing import Any
|
||||
from langchain_core.runnables import RunnableLambda
|
||||
|
||||
|
||||
def f(x: dict[str, Any]) -> str:
|
||||
return str(x["a"] * max(x["b"]))
|
||||
def f(x: dict[str, Any]) -> str:
|
||||
return str(x["a"] * max(x["b"]))
|
||||
|
||||
|
||||
runnable = RunnableLambda(f)
|
||||
as_tool = runnable.as_tool(arg_types={"a": int, "b": list[int]})
|
||||
as_tool.invoke({"a": 3, "b": [1, 2]})
|
||||
```
|
||||
runnable = RunnableLambda(f)
|
||||
as_tool = runnable.as_tool(arg_types={"a": int, "b": list[int]})
|
||||
as_tool.invoke({"a": 3, "b": [1, 2]})
|
||||
```
|
||||
|
||||
String input:
|
||||
!!! example "`str` input"
|
||||
|
||||
```python
|
||||
from langchain_core.runnables import RunnableLambda
|
||||
```python
|
||||
from langchain_core.runnables import RunnableLambda
|
||||
|
||||
|
||||
def f(x: str) -> str:
|
||||
return x + "a"
|
||||
def f(x: str) -> str:
|
||||
return x + "a"
|
||||
|
||||
|
||||
def g(x: str) -> str:
|
||||
return x + "z"
|
||||
def g(x: str) -> str:
|
||||
return x + "z"
|
||||
|
||||
|
||||
runnable = RunnableLambda(f) | g
|
||||
as_tool = runnable.as_tool()
|
||||
as_tool.invoke("b")
|
||||
```
|
||||
runnable = RunnableLambda(f) | g
|
||||
as_tool = runnable.as_tool()
|
||||
as_tool.invoke("b")
|
||||
```
|
||||
"""
|
||||
# Avoid circular import
|
||||
from langchain_core.tools import convert_runnable_to_tool # noqa: PLC0415
|
||||
@@ -2570,29 +2611,33 @@ class RunnableSerializable(Serializable, Runnable[Input, Output]):
|
||||
Returns:
|
||||
A new `Runnable` with the fields configured.
|
||||
|
||||
```python
|
||||
from langchain_core.runnables import ConfigurableField
|
||||
from langchain_openai import ChatOpenAI
|
||||
!!! example
|
||||
|
||||
model = ChatOpenAI(max_tokens=20).configurable_fields(
|
||||
max_tokens=ConfigurableField(
|
||||
id="output_token_number",
|
||||
name="Max tokens in the output",
|
||||
description="The maximum number of tokens in the output",
|
||||
```python
|
||||
from langchain_core.runnables import ConfigurableField
|
||||
from langchain_openai import ChatOpenAI
|
||||
|
||||
model = ChatOpenAI(max_tokens=20).configurable_fields(
|
||||
max_tokens=ConfigurableField(
|
||||
id="output_token_number",
|
||||
name="Max tokens in the output",
|
||||
description="The maximum number of tokens in the output",
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# max_tokens = 20
|
||||
print("max_tokens_20: ", model.invoke("tell me something about chess").content)
|
||||
# max_tokens = 20
|
||||
print(
|
||||
"max_tokens_20: ", model.invoke("tell me something about chess").content
|
||||
)
|
||||
|
||||
# max_tokens = 200
|
||||
print(
|
||||
"max_tokens_200: ",
|
||||
model.with_config(configurable={"output_token_number": 200})
|
||||
.invoke("tell me something about chess")
|
||||
.content,
|
||||
)
|
||||
```
|
||||
# max_tokens = 200
|
||||
print(
|
||||
"max_tokens_200: ",
|
||||
model.with_config(configurable={"output_token_number": 200})
|
||||
.invoke("tell me something about chess")
|
||||
.content,
|
||||
)
|
||||
```
|
||||
"""
|
||||
# Import locally to prevent circular import
|
||||
from langchain_core.runnables.configurable import ( # noqa: PLC0415
|
||||
@@ -2631,29 +2676,31 @@ class RunnableSerializable(Serializable, Runnable[Input, Output]):
|
||||
Returns:
|
||||
A new `Runnable` with the alternatives configured.
|
||||
|
||||
```python
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
from langchain_core.runnables.utils import ConfigurableField
|
||||
from langchain_openai import ChatOpenAI
|
||||
!!! example
|
||||
|
||||
model = ChatAnthropic(
|
||||
model_name="claude-sonnet-4-5-20250929"
|
||||
).configurable_alternatives(
|
||||
ConfigurableField(id="llm"),
|
||||
default_key="anthropic",
|
||||
openai=ChatOpenAI(),
|
||||
)
|
||||
```python
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
from langchain_core.runnables.utils import ConfigurableField
|
||||
from langchain_openai import ChatOpenAI
|
||||
|
||||
# uses the default model ChatAnthropic
|
||||
print(model.invoke("which organization created you?").content)
|
||||
model = ChatAnthropic(
|
||||
model_name="claude-sonnet-4-5-20250929"
|
||||
).configurable_alternatives(
|
||||
ConfigurableField(id="llm"),
|
||||
default_key="anthropic",
|
||||
openai=ChatOpenAI(),
|
||||
)
|
||||
|
||||
# uses ChatOpenAI
|
||||
print(
|
||||
model.with_config(configurable={"llm": "openai"})
|
||||
.invoke("which organization created you?")
|
||||
.content
|
||||
)
|
||||
```
|
||||
# uses the default model ChatAnthropic
|
||||
print(model.invoke("which organization created you?").content)
|
||||
|
||||
# uses ChatOpenAI
|
||||
print(
|
||||
model.with_config(configurable={"llm": "openai"})
|
||||
.invoke("which organization created you?")
|
||||
.content
|
||||
)
|
||||
```
|
||||
"""
|
||||
# Import locally to prevent circular import
|
||||
from langchain_core.runnables.configurable import ( # noqa: PLC0415
|
||||
@@ -2750,6 +2797,9 @@ def _seq_output_schema(
|
||||
return last.get_output_schema(config)
|
||||
|
||||
|
||||
_RUNNABLE_SEQUENCE_MIN_STEPS = 2
|
||||
|
||||
|
||||
class RunnableSequence(RunnableSerializable[Input, Output]):
|
||||
"""Sequence of `Runnable` objects, where the output of one is the input of the next.
|
||||
|
||||
@@ -2859,7 +2909,7 @@ class RunnableSequence(RunnableSerializable[Input, Output]):
|
||||
name: The name of the `Runnable`.
|
||||
first: The first `Runnable` in the sequence.
|
||||
middle: The middle `Runnable` objects in the sequence.
|
||||
last: The last Runnable in the sequence.
|
||||
last: The last `Runnable` in the sequence.
|
||||
|
||||
Raises:
|
||||
ValueError: If the sequence has less than 2 steps.
|
||||
@@ -2872,8 +2922,11 @@ class RunnableSequence(RunnableSerializable[Input, Output]):
|
||||
steps_flat.extend(step.steps)
|
||||
else:
|
||||
steps_flat.append(coerce_to_runnable(step))
|
||||
if len(steps_flat) < 2:
|
||||
msg = f"RunnableSequence must have at least 2 steps, got {len(steps_flat)}"
|
||||
if len(steps_flat) < _RUNNABLE_SEQUENCE_MIN_STEPS:
|
||||
msg = (
|
||||
f"RunnableSequence must have at least {_RUNNABLE_SEQUENCE_MIN_STEPS} "
|
||||
f"steps, got {len(steps_flat)}"
|
||||
)
|
||||
raise ValueError(msg)
|
||||
super().__init__(
|
||||
first=steps_flat[0],
|
||||
@@ -2904,7 +2957,7 @@ class RunnableSequence(RunnableSerializable[Input, Output]):
|
||||
@classmethod
|
||||
@override
|
||||
def is_lc_serializable(cls) -> bool:
|
||||
"""Return True as this class is serializable."""
|
||||
"""Return `True` as this class is serializable."""
|
||||
return True
|
||||
|
||||
model_config = ConfigDict(
|
||||
@@ -3610,7 +3663,7 @@ class RunnableParallel(RunnableSerializable[Input, dict[str, Any]]):
|
||||
@classmethod
|
||||
@override
|
||||
def is_lc_serializable(cls) -> bool:
|
||||
"""Return True as this class is serializable."""
|
||||
"""Return `True` as this class is serializable."""
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
@@ -3668,6 +3721,12 @@ class RunnableParallel(RunnableSerializable[Input, dict[str, Any]]):
|
||||
== "object"
|
||||
for s in self.steps__.values()
|
||||
):
|
||||
for step in self.steps__.values():
|
||||
fields = step.get_input_schema(config).model_fields
|
||||
root_field = fields.get("root")
|
||||
if root_field is not None and root_field.annotation != Any:
|
||||
return super().get_input_schema(config)
|
||||
|
||||
# This is correct, but pydantic typings/mypy don't think so.
|
||||
return create_model_v2(
|
||||
self.get_name("Input"),
|
||||
@@ -4477,7 +4536,7 @@ class RunnableLambda(Runnable[Input, Output]):
|
||||
# on itemgetter objects, so we have to parse the repr
|
||||
items = str(func).replace("operator.itemgetter(", "")[:-1].split(", ")
|
||||
if all(
|
||||
item[0] == "'" and item[-1] == "'" and len(item) > 2 for item in items
|
||||
item[0] == "'" and item[-1] == "'" and item != "''" for item in items
|
||||
):
|
||||
fields = {item[1:-1]: (Any, ...) for item in items}
|
||||
# It's a dict, lol
|
||||
@@ -5139,7 +5198,7 @@ class RunnableEachBase(RunnableSerializable[list[Input], list[Output]]):
|
||||
@classmethod
|
||||
@override
|
||||
def is_lc_serializable(cls) -> bool:
|
||||
"""Return True as this class is serializable."""
|
||||
"""Return `True` as this class is serializable."""
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
@@ -5322,7 +5381,7 @@ class RunnableEach(RunnableEachBase[Input, Output]):
|
||||
|
||||
|
||||
class RunnableBindingBase(RunnableSerializable[Input, Output]): # type: ignore[no-redef]
|
||||
"""`Runnable` that delegates calls to another `Runnable` with a set of kwargs.
|
||||
"""`Runnable` that delegates calls to another `Runnable` with a set of `**kwargs`.
|
||||
|
||||
Use only if creating a new `RunnableBinding` subclass with different `__init__`
|
||||
args.
|
||||
@@ -5462,7 +5521,7 @@ class RunnableBindingBase(RunnableSerializable[Input, Output]): # type: ignore[
|
||||
@classmethod
|
||||
@override
|
||||
def is_lc_serializable(cls) -> bool:
|
||||
"""Return True as this class is serializable."""
|
||||
"""Return `True` as this class is serializable."""
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
@@ -5752,7 +5811,7 @@ class RunnableBinding(RunnableBindingBase[Input, Output]): # type: ignore[no-re
|
||||
```python
|
||||
# Create a Runnable binding that invokes the chat model with the
|
||||
# additional kwarg `stop=['-']` when running it.
|
||||
from langchain_community.chat_models import ChatOpenAI
|
||||
from langchain_openai import ChatOpenAI
|
||||
|
||||
model = ChatOpenAI()
|
||||
model.invoke('Say "Parrot-MAGIC"', stop=["-"]) # Should return `Parrot`
|
||||
|
||||
@@ -36,11 +36,13 @@ from langchain_core.runnables.utils import (
|
||||
get_unique_config_specs,
|
||||
)
|
||||
|
||||
_MIN_BRANCHES = 2
|
||||
|
||||
|
||||
class RunnableBranch(RunnableSerializable[Input, Output]):
|
||||
"""Runnable that selects which branch to run based on a condition.
|
||||
"""`Runnable` that selects which branch to run based on a condition.
|
||||
|
||||
The Runnable is initialized with a list of `(condition, Runnable)` pairs and
|
||||
The `Runnable` is initialized with a list of `(condition, Runnable)` pairs and
|
||||
a default branch.
|
||||
|
||||
When operating on an input, the first condition that evaluates to True is
|
||||
@@ -86,12 +88,12 @@ class RunnableBranch(RunnableSerializable[Input, Output]):
|
||||
Defaults a `Runnable` to run if no condition is met.
|
||||
|
||||
Raises:
|
||||
ValueError: If the number of branches is less than 2.
|
||||
ValueError: If the number of branches is less than `2`.
|
||||
TypeError: If the default branch is not `Runnable`, `Callable` or `Mapping`.
|
||||
TypeError: If a branch is not a tuple or list.
|
||||
ValueError: If a branch is not of length 2.
|
||||
TypeError: If a branch is not a `tuple` or `list`.
|
||||
ValueError: If a branch is not of length `2`.
|
||||
"""
|
||||
if len(branches) < 2:
|
||||
if len(branches) < _MIN_BRANCHES:
|
||||
msg = "RunnableBranch requires at least two branches"
|
||||
raise ValueError(msg)
|
||||
|
||||
@@ -118,7 +120,7 @@ class RunnableBranch(RunnableSerializable[Input, Output]):
|
||||
)
|
||||
raise TypeError(msg)
|
||||
|
||||
if len(branch) != 2:
|
||||
if len(branch) != _MIN_BRANCHES:
|
||||
msg = (
|
||||
f"RunnableBranch branches must be "
|
||||
f"tuples or lists of length 2, not {len(branch)}"
|
||||
@@ -140,7 +142,7 @@ class RunnableBranch(RunnableSerializable[Input, Output]):
|
||||
|
||||
@classmethod
|
||||
def is_lc_serializable(cls) -> bool:
|
||||
"""Return True as this class is serializable."""
|
||||
"""Return `True` as this class is serializable."""
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
@@ -187,12 +189,12 @@ class RunnableBranch(RunnableSerializable[Input, Output]):
|
||||
def invoke(
|
||||
self, input: Input, config: RunnableConfig | None = None, **kwargs: Any
|
||||
) -> Output:
|
||||
"""First evaluates the condition, then delegate to true or false branch.
|
||||
"""First evaluates the condition, then delegate to `True` or `False` branch.
|
||||
|
||||
Args:
|
||||
input: The input to the Runnable.
|
||||
config: The configuration for the Runnable.
|
||||
**kwargs: Additional keyword arguments to pass to the Runnable.
|
||||
input: The input to the `Runnable`.
|
||||
config: The configuration for the `Runnable`.
|
||||
**kwargs: Additional keyword arguments to pass to the `Runnable`.
|
||||
|
||||
Returns:
|
||||
The output of the branch that was run.
|
||||
@@ -297,12 +299,12 @@ class RunnableBranch(RunnableSerializable[Input, Output]):
|
||||
config: RunnableConfig | None = None,
|
||||
**kwargs: Any | None,
|
||||
) -> Iterator[Output]:
|
||||
"""First evaluates the condition, then delegate to true or false branch.
|
||||
"""First evaluates the condition, then delegate to `True` or `False` branch.
|
||||
|
||||
Args:
|
||||
input: The input to the Runnable.
|
||||
config: The configuration for the Runnable.
|
||||
**kwargs: Additional keyword arguments to pass to the Runnable.
|
||||
input: The input to the `Runnable`.
|
||||
config: The configuration for the `Runnable`.
|
||||
**kwargs: Additional keyword arguments to pass to the `Runnable`.
|
||||
|
||||
Yields:
|
||||
The output of the branch that was run.
|
||||
@@ -381,12 +383,12 @@ class RunnableBranch(RunnableSerializable[Input, Output]):
|
||||
config: RunnableConfig | None = None,
|
||||
**kwargs: Any | None,
|
||||
) -> AsyncIterator[Output]:
|
||||
"""First evaluates the condition, then delegate to true or false branch.
|
||||
"""First evaluates the condition, then delegate to `True` or `False` branch.
|
||||
|
||||
Args:
|
||||
input: The input to the Runnable.
|
||||
config: The configuration for the Runnable.
|
||||
**kwargs: Additional keyword arguments to pass to the Runnable.
|
||||
input: The input to the `Runnable`.
|
||||
config: The configuration for the `Runnable`.
|
||||
**kwargs: Additional keyword arguments to pass to the `Runnable`.
|
||||
|
||||
Yields:
|
||||
The output of the branch that was run.
|
||||
|
||||
@@ -47,54 +47,59 @@ class EmptyDict(TypedDict, total=False):
|
||||
|
||||
|
||||
class RunnableConfig(TypedDict, total=False):
|
||||
"""Configuration for a Runnable."""
|
||||
"""Configuration for a `Runnable`.
|
||||
|
||||
See the [reference docs](https://reference.langchain.com/python/langchain_core/runnables/#langchain_core.runnables.RunnableConfig)
|
||||
for more details.
|
||||
"""
|
||||
|
||||
tags: list[str]
|
||||
"""
|
||||
Tags for this call and any sub-calls (eg. a Chain calling an LLM).
|
||||
"""Tags for this call and any sub-calls (e.g. a Chain calling an LLM).
|
||||
|
||||
You can use these to filter calls.
|
||||
"""
|
||||
|
||||
metadata: dict[str, Any]
|
||||
"""
|
||||
Metadata for this call and any sub-calls (eg. a Chain calling an LLM).
|
||||
"""Metadata for this call and any sub-calls (e.g. a Chain calling an LLM).
|
||||
|
||||
Keys should be strings, values should be JSON-serializable.
|
||||
"""
|
||||
|
||||
callbacks: Callbacks
|
||||
"""
|
||||
Callbacks for this call and any sub-calls (eg. a Chain calling an LLM).
|
||||
"""Callbacks for this call and any sub-calls (e.g. a Chain calling an LLM).
|
||||
|
||||
Tags are passed to all callbacks, metadata is passed to handle*Start callbacks.
|
||||
"""
|
||||
|
||||
run_name: str
|
||||
"""
|
||||
Name for the tracer run for this call. Defaults to the name of the class.
|
||||
"""
|
||||
"""Name for the tracer run for this call.
|
||||
|
||||
Defaults to the name of the class."""
|
||||
|
||||
max_concurrency: int | None
|
||||
"""
|
||||
Maximum number of parallel calls to make. If not provided, defaults to
|
||||
`ThreadPoolExecutor`'s default.
|
||||
"""Maximum number of parallel calls to make.
|
||||
|
||||
If not provided, defaults to `ThreadPoolExecutor`'s default.
|
||||
"""
|
||||
|
||||
recursion_limit: int
|
||||
"""
|
||||
Maximum number of times a call can recurse. If not provided, defaults to `25`.
|
||||
"""Maximum number of times a call can recurse.
|
||||
|
||||
If not provided, defaults to `25`.
|
||||
"""
|
||||
|
||||
configurable: dict[str, Any]
|
||||
"""
|
||||
Runtime values for attributes previously made configurable on this `Runnable`,
|
||||
"""Runtime values for attributes previously made configurable on this `Runnable`,
|
||||
or sub-Runnables, through `configurable_fields` or `configurable_alternatives`.
|
||||
|
||||
Check `output_schema` for a description of the attributes that have been made
|
||||
configurable.
|
||||
"""
|
||||
|
||||
run_id: uuid.UUID | None
|
||||
"""
|
||||
Unique identifier for the tracer run for this call. If not provided, a new UUID
|
||||
will be generated.
|
||||
"""Unique identifier for the tracer run for this call.
|
||||
|
||||
If not provided, a new UUID will be generated.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Runnables that can be dynamically configured."""
|
||||
"""`Runnable` objects that can be dynamically configured."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -47,14 +47,14 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
class DynamicRunnable(RunnableSerializable[Input, Output]):
|
||||
"""Serializable Runnable that can be dynamically configured.
|
||||
"""Serializable `Runnable` that can be dynamically configured.
|
||||
|
||||
A DynamicRunnable should be initiated using the `configurable_fields` or
|
||||
`configurable_alternatives` method of a Runnable.
|
||||
A `DynamicRunnable` should be initiated using the `configurable_fields` or
|
||||
`configurable_alternatives` method of a `Runnable`.
|
||||
"""
|
||||
|
||||
default: RunnableSerializable[Input, Output]
|
||||
"""The default Runnable to use."""
|
||||
"""The default `Runnable` to use."""
|
||||
|
||||
config: RunnableConfig | None = None
|
||||
"""The configuration to use."""
|
||||
@@ -66,7 +66,7 @@ class DynamicRunnable(RunnableSerializable[Input, Output]):
|
||||
@classmethod
|
||||
@override
|
||||
def is_lc_serializable(cls) -> bool:
|
||||
"""Return True as this class is serializable."""
|
||||
"""Return `True` as this class is serializable."""
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
@@ -120,13 +120,13 @@ class DynamicRunnable(RunnableSerializable[Input, Output]):
|
||||
def prepare(
|
||||
self, config: RunnableConfig | None = None
|
||||
) -> tuple[Runnable[Input, Output], RunnableConfig]:
|
||||
"""Prepare the Runnable for invocation.
|
||||
"""Prepare the `Runnable` for invocation.
|
||||
|
||||
Args:
|
||||
config: The configuration to use.
|
||||
|
||||
Returns:
|
||||
The prepared Runnable and configuration.
|
||||
The prepared `Runnable` and configuration.
|
||||
"""
|
||||
runnable: Runnable[Input, Output] = self
|
||||
while isinstance(runnable, DynamicRunnable):
|
||||
@@ -316,12 +316,12 @@ class DynamicRunnable(RunnableSerializable[Input, Output]):
|
||||
|
||||
|
||||
class RunnableConfigurableFields(DynamicRunnable[Input, Output]):
|
||||
"""Runnable that can be dynamically configured.
|
||||
"""`Runnable` that can be dynamically configured.
|
||||
|
||||
A RunnableConfigurableFields should be initiated using the
|
||||
`configurable_fields` method of a Runnable.
|
||||
A `RunnableConfigurableFields` should be initiated using the
|
||||
`configurable_fields` method of a `Runnable`.
|
||||
|
||||
Here is an example of using a RunnableConfigurableFields with LLMs:
|
||||
Here is an example of using a `RunnableConfigurableFields` with LLMs:
|
||||
|
||||
```python
|
||||
from langchain_core.prompts import PromptTemplate
|
||||
@@ -348,7 +348,7 @@ class RunnableConfigurableFields(DynamicRunnable[Input, Output]):
|
||||
chain.invoke({"x": 0}, config={"configurable": {"temperature": 0.9}})
|
||||
```
|
||||
|
||||
Here is an example of using a RunnableConfigurableFields with HubRunnables:
|
||||
Here is an example of using a `RunnableConfigurableFields` with `HubRunnables`:
|
||||
|
||||
```python
|
||||
from langchain_core.prompts import PromptTemplate
|
||||
@@ -380,7 +380,7 @@ class RunnableConfigurableFields(DynamicRunnable[Input, Output]):
|
||||
|
||||
@property
|
||||
def config_specs(self) -> list[ConfigurableFieldSpec]:
|
||||
"""Get the configuration specs for the RunnableConfigurableFields.
|
||||
"""Get the configuration specs for the `RunnableConfigurableFields`.
|
||||
|
||||
Returns:
|
||||
The configuration specs.
|
||||
@@ -473,10 +473,10 @@ _enums_for_spec_lock = threading.Lock()
|
||||
|
||||
|
||||
class RunnableConfigurableAlternatives(DynamicRunnable[Input, Output]):
|
||||
"""Runnable that can be dynamically configured.
|
||||
"""`Runnable` that can be dynamically configured.
|
||||
|
||||
A `RunnableConfigurableAlternatives` should be initiated using the
|
||||
`configurable_alternatives` method of a Runnable or can be
|
||||
`configurable_alternatives` method of a `Runnable` or can be
|
||||
initiated directly as well.
|
||||
|
||||
Here is an example of using a `RunnableConfigurableAlternatives` that uses
|
||||
@@ -531,7 +531,7 @@ class RunnableConfigurableAlternatives(DynamicRunnable[Input, Output]):
|
||||
"""
|
||||
|
||||
which: ConfigurableField
|
||||
"""The ConfigurableField to use to choose between alternatives."""
|
||||
"""The `ConfigurableField` to use to choose between alternatives."""
|
||||
|
||||
alternatives: dict[
|
||||
str,
|
||||
@@ -544,8 +544,9 @@ class RunnableConfigurableAlternatives(DynamicRunnable[Input, Output]):
|
||||
|
||||
prefix_keys: bool
|
||||
"""Whether to prefix configurable fields of each alternative with a namespace
|
||||
of the form <which.id>==<alternative_key>, eg. a key named "temperature" used by
|
||||
the alternative named "gpt3" becomes "model==gpt3/temperature"."""
|
||||
of the form <which.id>==<alternative_key>, e.g. a key named "temperature" used by
|
||||
the alternative named "gpt3" becomes "model==gpt3/temperature".
|
||||
"""
|
||||
|
||||
@property
|
||||
@override
|
||||
@@ -638,24 +639,24 @@ class RunnableConfigurableAlternatives(DynamicRunnable[Input, Output]):
|
||||
|
||||
|
||||
def _strremoveprefix(s: str, prefix: str) -> str:
|
||||
"""str.removeprefix() is only available in Python 3.9+."""
|
||||
"""`str.removeprefix()` is only available in Python 3.9+."""
|
||||
return s.replace(prefix, "", 1) if s.startswith(prefix) else s
|
||||
|
||||
|
||||
def prefix_config_spec(
|
||||
spec: ConfigurableFieldSpec, prefix: str
|
||||
) -> ConfigurableFieldSpec:
|
||||
"""Prefix the id of a ConfigurableFieldSpec.
|
||||
"""Prefix the id of a `ConfigurableFieldSpec`.
|
||||
|
||||
This is useful when a RunnableConfigurableAlternatives is used as a
|
||||
ConfigurableField of another RunnableConfigurableAlternatives.
|
||||
This is useful when a `RunnableConfigurableAlternatives` is used as a
|
||||
`ConfigurableField` of another `RunnableConfigurableAlternatives`.
|
||||
|
||||
Args:
|
||||
spec: The ConfigurableFieldSpec to prefix.
|
||||
spec: The `ConfigurableFieldSpec` to prefix.
|
||||
prefix: The prefix to add.
|
||||
|
||||
Returns:
|
||||
The prefixed ConfigurableFieldSpec.
|
||||
The prefixed `ConfigurableFieldSpec`.
|
||||
"""
|
||||
return (
|
||||
ConfigurableFieldSpec(
|
||||
@@ -677,15 +678,15 @@ def make_options_spec(
|
||||
) -> ConfigurableFieldSpec:
|
||||
"""Make options spec.
|
||||
|
||||
Make a ConfigurableFieldSpec for a ConfigurableFieldSingleOption or
|
||||
ConfigurableFieldMultiOption.
|
||||
Make a `ConfigurableFieldSpec` for a `ConfigurableFieldSingleOption` or
|
||||
`ConfigurableFieldMultiOption`.
|
||||
|
||||
Args:
|
||||
spec: The ConfigurableFieldSingleOption or ConfigurableFieldMultiOption.
|
||||
spec: The `ConfigurableFieldSingleOption` or `ConfigurableFieldMultiOption`.
|
||||
description: The description to use if the spec does not have one.
|
||||
|
||||
Returns:
|
||||
The ConfigurableFieldSpec.
|
||||
The `ConfigurableFieldSpec`.
|
||||
"""
|
||||
with _enums_for_spec_lock:
|
||||
if enum := _enums_for_spec.get(spec):
|
||||
|
||||
@@ -35,20 +35,20 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
class RunnableWithFallbacks(RunnableSerializable[Input, Output]):
|
||||
"""Runnable that can fallback to other Runnables if it fails.
|
||||
"""`Runnable` that can fallback to other `Runnable`s if it fails.
|
||||
|
||||
External APIs (e.g., APIs for a language model) may at times experience
|
||||
degraded performance or even downtime.
|
||||
|
||||
In these cases, it can be useful to have a fallback Runnable that can be
|
||||
used in place of the original Runnable (e.g., fallback to another LLM provider).
|
||||
In these cases, it can be useful to have a fallback `Runnable` that can be
|
||||
used in place of the original `Runnable` (e.g., fallback to another LLM provider).
|
||||
|
||||
Fallbacks can be defined at the level of a single Runnable, or at the level
|
||||
of a chain of Runnables. Fallbacks are tried in order until one succeeds or
|
||||
Fallbacks can be defined at the level of a single `Runnable`, or at the level
|
||||
of a chain of `Runnable`s. Fallbacks are tried in order until one succeeds or
|
||||
all fail.
|
||||
|
||||
While you can instantiate a `RunnableWithFallbacks` directly, it is usually
|
||||
more convenient to use the `with_fallbacks` method on a Runnable.
|
||||
more convenient to use the `with_fallbacks` method on a `Runnable`.
|
||||
|
||||
Example:
|
||||
```python
|
||||
@@ -87,7 +87,7 @@ class RunnableWithFallbacks(RunnableSerializable[Input, Output]):
|
||||
"""
|
||||
|
||||
runnable: Runnable[Input, Output]
|
||||
"""The Runnable to run first."""
|
||||
"""The `Runnable` to run first."""
|
||||
fallbacks: Sequence[Runnable[Input, Output]]
|
||||
"""A sequence of fallbacks to try."""
|
||||
exceptions_to_handle: tuple[type[BaseException], ...] = (Exception,)
|
||||
@@ -97,9 +97,12 @@ class RunnableWithFallbacks(RunnableSerializable[Input, Output]):
|
||||
"""
|
||||
exception_key: str | None = None
|
||||
"""If `string` is specified then handled exceptions will be passed to fallbacks as
|
||||
part of the input under the specified key. If `None`, exceptions
|
||||
will not be passed to fallbacks. If used, the base Runnable and its fallbacks
|
||||
must accept a dictionary as input."""
|
||||
part of the input under the specified key.
|
||||
|
||||
If `None`, exceptions will not be passed to fallbacks.
|
||||
|
||||
If used, the base `Runnable` and its fallbacks must accept a dictionary as input.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(
|
||||
arbitrary_types_allowed=True,
|
||||
@@ -137,7 +140,7 @@ class RunnableWithFallbacks(RunnableSerializable[Input, Output]):
|
||||
@classmethod
|
||||
@override
|
||||
def is_lc_serializable(cls) -> bool:
|
||||
"""Return True as this class is serializable."""
|
||||
"""Return `True` as this class is serializable."""
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
@@ -152,10 +155,10 @@ class RunnableWithFallbacks(RunnableSerializable[Input, Output]):
|
||||
|
||||
@property
|
||||
def runnables(self) -> Iterator[Runnable[Input, Output]]:
|
||||
"""Iterator over the Runnable and its fallbacks.
|
||||
"""Iterator over the `Runnable` and its fallbacks.
|
||||
|
||||
Yields:
|
||||
The Runnable then its fallbacks.
|
||||
The `Runnable` then its fallbacks.
|
||||
"""
|
||||
yield self.runnable
|
||||
yield from self.fallbacks
|
||||
@@ -589,14 +592,14 @@ class RunnableWithFallbacks(RunnableSerializable[Input, Output]):
|
||||
await run_manager.on_chain_end(output)
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
"""Get an attribute from the wrapped Runnable and its fallbacks.
|
||||
"""Get an attribute from the wrapped `Runnable` and its fallbacks.
|
||||
|
||||
Returns:
|
||||
If the attribute is anything other than a method that outputs a Runnable,
|
||||
returns getattr(self.runnable, name). If the attribute is a method that
|
||||
does return a new Runnable (e.g. model.bind_tools([...]) outputs a new
|
||||
RunnableBinding) then self.runnable and each of the runnables in
|
||||
self.fallbacks is replaced with getattr(x, name).
|
||||
If the attribute is anything other than a method that outputs a `Runnable`,
|
||||
returns `getattr(self.runnable, name)`. If the attribute is a method that
|
||||
does return a new `Runnable` (e.g. `model.bind_tools([...])` outputs a new
|
||||
`RunnableBinding`) then `self.runnable` and each of the runnables in
|
||||
`self.fallbacks` is replaced with `getattr(x, name)`.
|
||||
|
||||
Example:
|
||||
```python
|
||||
@@ -618,7 +621,6 @@ class RunnableWithFallbacks(RunnableSerializable[Input, Output]):
|
||||
runnable=RunnableBinding(bound=ChatOpenAI(...), kwargs={"tools": [...]}),
|
||||
fallbacks=[RunnableBinding(bound=ChatAnthropic(...), kwargs={"tools": [...]})],
|
||||
)
|
||||
|
||||
```
|
||||
""" # noqa: E501
|
||||
attr = getattr(self.runnable, name)
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import (
|
||||
@@ -22,7 +21,7 @@ from langchain_core.runnables.base import Runnable, RunnableSerializable
|
||||
from langchain_core.utils.pydantic import _IgnoreUnserializable, is_basemodel_subclass
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Sequence
|
||||
from collections.abc import Callable, Sequence
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
@@ -642,6 +641,7 @@ class Graph:
|
||||
retry_delay: float = 1.0,
|
||||
frontmatter_config: dict[str, Any] | None = None,
|
||||
base_url: str | None = None,
|
||||
proxies: dict[str, str] | None = None,
|
||||
) -> bytes:
|
||||
"""Draw the graph as a PNG image using Mermaid.
|
||||
|
||||
@@ -674,11 +674,10 @@ class Graph:
|
||||
}
|
||||
```
|
||||
base_url: The base URL of the Mermaid server for rendering via API.
|
||||
|
||||
proxies: HTTP/HTTPS proxies for requests (e.g. `{"http": "http://127.0.0.1:7890"}`).
|
||||
|
||||
Returns:
|
||||
The PNG image as bytes.
|
||||
|
||||
"""
|
||||
# Import locally to prevent circular import
|
||||
from langchain_core.runnables.graph_mermaid import ( # noqa: PLC0415
|
||||
@@ -699,6 +698,7 @@ class Graph:
|
||||
padding=padding,
|
||||
max_retries=max_retries,
|
||||
retry_delay=retry_delay,
|
||||
proxies=proxies,
|
||||
base_url=base_url,
|
||||
)
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ from __future__ import annotations
|
||||
|
||||
import math
|
||||
import os
|
||||
from collections.abc import Mapping, Sequence
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
try:
|
||||
@@ -20,6 +19,8 @@ except ImportError:
|
||||
_HAS_GRANDALF = False
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Mapping, Sequence
|
||||
|
||||
from langchain_core.runnables.graph import Edge as LangEdge
|
||||
|
||||
|
||||
|
||||
@@ -281,6 +281,7 @@ def draw_mermaid_png(
|
||||
max_retries: int = 1,
|
||||
retry_delay: float = 1.0,
|
||||
base_url: str | None = None,
|
||||
proxies: dict[str, str] | None = None,
|
||||
) -> bytes:
|
||||
"""Draws a Mermaid graph as PNG using provided syntax.
|
||||
|
||||
@@ -293,6 +294,7 @@ def draw_mermaid_png(
|
||||
max_retries: Maximum number of retries (MermaidDrawMethod.API).
|
||||
retry_delay: Delay between retries (MermaidDrawMethod.API).
|
||||
base_url: Base URL for the Mermaid.ink API.
|
||||
proxies: HTTP/HTTPS proxies for requests (e.g. `{"http": "http://127.0.0.1:7890"}`).
|
||||
|
||||
Returns:
|
||||
PNG image bytes.
|
||||
@@ -314,6 +316,7 @@ def draw_mermaid_png(
|
||||
max_retries=max_retries,
|
||||
retry_delay=retry_delay,
|
||||
base_url=base_url,
|
||||
proxies=proxies,
|
||||
)
|
||||
else:
|
||||
supported_methods = ", ".join([m.value for m in MermaidDrawMethod])
|
||||
@@ -405,6 +408,7 @@ def _render_mermaid_using_api(
|
||||
file_type: Literal["jpeg", "png", "webp"] | None = "png",
|
||||
max_retries: int = 1,
|
||||
retry_delay: float = 1.0,
|
||||
proxies: dict[str, str] | None = None,
|
||||
base_url: str | None = None,
|
||||
) -> bytes:
|
||||
"""Renders Mermaid graph using the Mermaid.INK API."""
|
||||
@@ -445,7 +449,7 @@ def _render_mermaid_using_api(
|
||||
|
||||
for attempt in range(max_retries + 1):
|
||||
try:
|
||||
response = requests.get(image_url, timeout=10)
|
||||
response = requests.get(image_url, timeout=10, proxies=proxies)
|
||||
if response.status_code == requests.codes.ok:
|
||||
img_bytes = response.content
|
||||
if output_file_path is not None:
|
||||
@@ -454,7 +458,10 @@ def _render_mermaid_using_api(
|
||||
return img_bytes
|
||||
|
||||
# If we get a server error (5xx), retry
|
||||
if 500 <= response.status_code < 600 and attempt < max_retries:
|
||||
if (
|
||||
requests.codes.internal_server_error <= response.status_code
|
||||
and attempt < max_retries
|
||||
):
|
||||
# Exponential backoff with jitter
|
||||
sleep_time = retry_delay * (2**attempt) * (0.5 + 0.5 * random.random()) # noqa: S311 not used for crypto
|
||||
time.sleep(sleep_time)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Helper class to draw a state graph into a PNG file."""
|
||||
|
||||
from itertools import groupby
|
||||
from typing import Any
|
||||
|
||||
from langchain_core.runnables.graph import Graph, LabelsDict
|
||||
@@ -141,6 +142,7 @@ class PngDrawer:
|
||||
# Add nodes, conditional edges, and edges to the graph
|
||||
self.add_nodes(viz, graph)
|
||||
self.add_edges(viz, graph)
|
||||
self.add_subgraph(viz, [node.split(":") for node in graph.nodes])
|
||||
|
||||
# Update entrypoint and END styles
|
||||
self.update_styles(viz, graph)
|
||||
@@ -161,6 +163,32 @@ class PngDrawer:
|
||||
for node in graph.nodes:
|
||||
self.add_node(viz, node)
|
||||
|
||||
def add_subgraph(
|
||||
self,
|
||||
viz: Any,
|
||||
nodes: list[list[str]],
|
||||
parent_prefix: list[str] | None = None,
|
||||
) -> None:
|
||||
"""Add subgraphs to the graph.
|
||||
|
||||
Args:
|
||||
viz: The graphviz object.
|
||||
nodes: The nodes to add.
|
||||
parent_prefix: The prefix of the parent subgraph.
|
||||
"""
|
||||
for prefix, grouped in groupby(
|
||||
[node[:] for node in sorted(nodes)],
|
||||
key=lambda x: x.pop(0),
|
||||
):
|
||||
current_prefix = (parent_prefix or []) + [prefix]
|
||||
grouped_nodes = list(grouped)
|
||||
if len(grouped_nodes) > 1:
|
||||
subgraph = viz.add_subgraph(
|
||||
[":".join(current_prefix + node) for node in grouped_nodes],
|
||||
name="cluster_" + ":".join(current_prefix),
|
||||
)
|
||||
self.add_subgraph(subgraph, grouped_nodes, current_prefix)
|
||||
|
||||
def add_edges(self, viz: Any, graph: Graph) -> None:
|
||||
"""Add edges to the graph.
|
||||
|
||||
|
||||
@@ -36,23 +36,23 @@ GetSessionHistoryCallable = Callable[..., BaseChatMessageHistory]
|
||||
|
||||
|
||||
class RunnableWithMessageHistory(RunnableBindingBase): # type: ignore[no-redef]
|
||||
"""Runnable that manages chat message history for another Runnable.
|
||||
"""`Runnable` that manages chat message history for another `Runnable`.
|
||||
|
||||
A chat message history is a sequence of messages that represent a conversation.
|
||||
|
||||
RunnableWithMessageHistory wraps another Runnable and manages the chat message
|
||||
`RunnableWithMessageHistory` wraps another `Runnable` and manages the chat message
|
||||
history for it; it is responsible for reading and updating the chat message
|
||||
history.
|
||||
|
||||
The formats supported for the inputs and outputs of the wrapped Runnable
|
||||
The formats supported for the inputs and outputs of the wrapped `Runnable`
|
||||
are described below.
|
||||
|
||||
RunnableWithMessageHistory must always be called with a config that contains
|
||||
`RunnableWithMessageHistory` must always be called with a config that contains
|
||||
the appropriate parameters for the chat message history factory.
|
||||
|
||||
By default, the Runnable is expected to take a single configuration parameter
|
||||
By default, the `Runnable` is expected to take a single configuration parameter
|
||||
called `session_id` which is a string. This parameter is used to create a new
|
||||
or look up an existing chat message history that matches the given session_id.
|
||||
or look up an existing chat message history that matches the given `session_id`.
|
||||
|
||||
In this case, the invocation would look like this:
|
||||
|
||||
@@ -117,12 +117,12 @@ class RunnableWithMessageHistory(RunnableBindingBase): # type: ignore[no-redef]
|
||||
|
||||
```
|
||||
|
||||
Example where the wrapped Runnable takes a dictionary input:
|
||||
Example where the wrapped `Runnable` takes a dictionary input:
|
||||
|
||||
```python
|
||||
from typing import Optional
|
||||
|
||||
from langchain_community.chat_models import ChatAnthropic
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||
from langchain_core.runnables.history import RunnableWithMessageHistory
|
||||
|
||||
@@ -166,7 +166,7 @@ class RunnableWithMessageHistory(RunnableBindingBase): # type: ignore[no-redef]
|
||||
print(store) # noqa: T201
|
||||
```
|
||||
|
||||
Example where the session factory takes two keys, user_id and conversation id):
|
||||
Example where the session factory takes two keys (`user_id` and `conversation_id`):
|
||||
|
||||
```python
|
||||
store = {}
|
||||
@@ -223,21 +223,28 @@ class RunnableWithMessageHistory(RunnableBindingBase): # type: ignore[no-redef]
|
||||
"""
|
||||
|
||||
get_session_history: GetSessionHistoryCallable
|
||||
"""Function that returns a new BaseChatMessageHistory.
|
||||
"""Function that returns a new `BaseChatMessageHistory`.
|
||||
|
||||
This function should either take a single positional argument `session_id` of type
|
||||
string and return a corresponding chat message history instance"""
|
||||
string and return a corresponding chat message history instance
|
||||
"""
|
||||
input_messages_key: str | None = None
|
||||
"""Must be specified if the base runnable accepts a dict as input.
|
||||
The key in the input dict that contains the messages."""
|
||||
"""Must be specified if the base `Runnable` accepts a `dict` as input.
|
||||
The key in the input `dict` that contains the messages.
|
||||
"""
|
||||
output_messages_key: str | None = None
|
||||
"""Must be specified if the base Runnable returns a dict as output.
|
||||
The key in the output dict that contains the messages."""
|
||||
"""Must be specified if the base `Runnable` returns a `dict` as output.
|
||||
The key in the output `dict` that contains the messages.
|
||||
"""
|
||||
history_messages_key: str | None = None
|
||||
"""Must be specified if the base runnable accepts a dict as input and expects a
|
||||
separate key for historical messages."""
|
||||
"""Must be specified if the base `Runnable` accepts a `dict` as input and expects a
|
||||
separate key for historical messages.
|
||||
"""
|
||||
history_factory_config: Sequence[ConfigurableFieldSpec]
|
||||
"""Configure fields that should be passed to the chat history factory.
|
||||
See `ConfigurableFieldSpec` for more details."""
|
||||
|
||||
See `ConfigurableFieldSpec` for more details.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -254,15 +261,16 @@ class RunnableWithMessageHistory(RunnableBindingBase): # type: ignore[no-redef]
|
||||
history_factory_config: Sequence[ConfigurableFieldSpec] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize RunnableWithMessageHistory.
|
||||
"""Initialize `RunnableWithMessageHistory`.
|
||||
|
||||
Args:
|
||||
runnable: The base Runnable to be wrapped.
|
||||
runnable: The base `Runnable` to be wrapped.
|
||||
|
||||
Must take as input one of:
|
||||
|
||||
1. A list of `BaseMessage`
|
||||
2. A dict with one key for all messages
|
||||
3. A dict with one key for the current input string/message(s) and
|
||||
2. A `dict` with one key for all messages
|
||||
3. A `dict` with one key for the current input string/message(s) and
|
||||
a separate key for historical messages. If the input key points
|
||||
to a string, it will be treated as a `HumanMessage` in history.
|
||||
|
||||
@@ -270,13 +278,15 @@ class RunnableWithMessageHistory(RunnableBindingBase): # type: ignore[no-redef]
|
||||
|
||||
1. A string which can be treated as an `AIMessage`
|
||||
2. A `BaseMessage` or sequence of `BaseMessage`
|
||||
3. A dict with a key for a `BaseMessage` or sequence of
|
||||
3. A `dict` with a key for a `BaseMessage` or sequence of
|
||||
`BaseMessage`
|
||||
|
||||
get_session_history: Function that returns a new BaseChatMessageHistory.
|
||||
get_session_history: Function that returns a new `BaseChatMessageHistory`.
|
||||
|
||||
This function should either take a single positional argument
|
||||
`session_id` of type string and return a corresponding
|
||||
chat message history instance.
|
||||
|
||||
```python
|
||||
def get_session_history(
|
||||
session_id: str, *, user_id: str | None = None
|
||||
@@ -295,16 +305,17 @@ class RunnableWithMessageHistory(RunnableBindingBase): # type: ignore[no-redef]
|
||||
) -> BaseChatMessageHistory: ...
|
||||
```
|
||||
|
||||
input_messages_key: Must be specified if the base runnable accepts a dict
|
||||
input_messages_key: Must be specified if the base runnable accepts a `dict`
|
||||
as input.
|
||||
output_messages_key: Must be specified if the base runnable returns a dict
|
||||
output_messages_key: Must be specified if the base runnable returns a `dict`
|
||||
as output.
|
||||
history_messages_key: Must be specified if the base runnable accepts a dict
|
||||
as input and expects a separate key for historical messages.
|
||||
history_messages_key: Must be specified if the base runnable accepts a
|
||||
`dict` as input and expects a separate key for historical messages.
|
||||
history_factory_config: Configure fields that should be passed to the
|
||||
chat history factory. See `ConfigurableFieldSpec` for more details.
|
||||
Specifying these allows you to pass multiple config keys
|
||||
into the get_session_history factory.
|
||||
|
||||
Specifying these allows you to pass multiple config keys into the
|
||||
`get_session_history` factory.
|
||||
**kwargs: Arbitrary additional kwargs to pass to parent class
|
||||
`RunnableBindingBase` init.
|
||||
|
||||
@@ -364,7 +375,7 @@ class RunnableWithMessageHistory(RunnableBindingBase): # type: ignore[no-redef]
|
||||
@property
|
||||
@override
|
||||
def config_specs(self) -> list[ConfigurableFieldSpec]:
|
||||
"""Get the configuration specs for the RunnableWithMessageHistory."""
|
||||
"""Get the configuration specs for the `RunnableWithMessageHistory`."""
|
||||
return get_unique_config_specs(
|
||||
super().config_specs + list(self.history_factory_config)
|
||||
)
|
||||
@@ -606,6 +617,6 @@ class RunnableWithMessageHistory(RunnableBindingBase): # type: ignore[no-redef]
|
||||
|
||||
|
||||
def _get_parameter_names(callable_: GetSessionHistoryCallable) -> list[str]:
|
||||
"""Get the parameter names of the callable."""
|
||||
"""Get the parameter names of the `Callable`."""
|
||||
sig = inspect.signature(callable_)
|
||||
return list(sig.parameters.keys())
|
||||
|
||||
@@ -51,10 +51,10 @@ def identity(x: Other) -> Other:
|
||||
"""Identity function.
|
||||
|
||||
Args:
|
||||
x: input.
|
||||
x: Input.
|
||||
|
||||
Returns:
|
||||
output.
|
||||
Output.
|
||||
"""
|
||||
return x
|
||||
|
||||
@@ -63,10 +63,10 @@ async def aidentity(x: Other) -> Other:
|
||||
"""Async identity function.
|
||||
|
||||
Args:
|
||||
x: input.
|
||||
x: Input.
|
||||
|
||||
Returns:
|
||||
output.
|
||||
Output.
|
||||
"""
|
||||
return x
|
||||
|
||||
@@ -74,11 +74,11 @@ async def aidentity(x: Other) -> Other:
|
||||
class RunnablePassthrough(RunnableSerializable[Other, Other]):
|
||||
"""Runnable to passthrough inputs unchanged or with additional keys.
|
||||
|
||||
This Runnable behaves almost like the identity function, except that it
|
||||
This `Runnable` behaves almost like the identity function, except that it
|
||||
can be configured to add additional keys to the output, if the input is a
|
||||
dict.
|
||||
|
||||
The examples below demonstrate this Runnable works using a few simple
|
||||
The examples below demonstrate this `Runnable` works using a few simple
|
||||
chains. The chains rely on simple lambdas to make the examples easy to execute
|
||||
and experiment with.
|
||||
|
||||
@@ -164,7 +164,7 @@ class RunnablePassthrough(RunnableSerializable[Other, Other]):
|
||||
input_type: type[Other] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Create e RunnablePassthrough.
|
||||
"""Create a `RunnablePassthrough`.
|
||||
|
||||
Args:
|
||||
func: Function to be called with the input.
|
||||
@@ -180,7 +180,7 @@ class RunnablePassthrough(RunnableSerializable[Other, Other]):
|
||||
@classmethod
|
||||
@override
|
||||
def is_lc_serializable(cls) -> bool:
|
||||
"""Return True as this class is serializable."""
|
||||
"""Return `True` as this class is serializable."""
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
@@ -213,11 +213,11 @@ class RunnablePassthrough(RunnableSerializable[Other, Other]):
|
||||
"""Merge the Dict input with the output produced by the mapping argument.
|
||||
|
||||
Args:
|
||||
**kwargs: Runnable, Callable or a Mapping from keys to Runnables
|
||||
or Callables.
|
||||
**kwargs: `Runnable`, `Callable` or a `Mapping` from keys to `Runnable`
|
||||
objects or `Callable`s.
|
||||
|
||||
Returns:
|
||||
A Runnable that merges the Dict input with the output produced by the
|
||||
A `Runnable` that merges the `dict` input with the output produced by the
|
||||
mapping argument.
|
||||
"""
|
||||
return RunnableAssign(RunnableParallel[dict[str, Any]](kwargs))
|
||||
@@ -350,7 +350,7 @@ _graph_passthrough: RunnablePassthrough = RunnablePassthrough()
|
||||
|
||||
|
||||
class RunnableAssign(RunnableSerializable[dict[str, Any], dict[str, Any]]):
|
||||
"""Runnable that assigns key-value pairs to dict[str, Any] inputs.
|
||||
"""Runnable that assigns key-value pairs to `dict[str, Any]` inputs.
|
||||
|
||||
The `RunnableAssign` class takes input dictionaries and, through a
|
||||
`RunnableParallel` instance, applies transformations, then combines
|
||||
@@ -392,7 +392,7 @@ class RunnableAssign(RunnableSerializable[dict[str, Any], dict[str, Any]]):
|
||||
mapper: RunnableParallel
|
||||
|
||||
def __init__(self, mapper: RunnableParallel[dict[str, Any]], **kwargs: Any) -> None:
|
||||
"""Create a RunnableAssign.
|
||||
"""Create a `RunnableAssign`.
|
||||
|
||||
Args:
|
||||
mapper: A `RunnableParallel` instance that will be used to transform the
|
||||
@@ -403,7 +403,7 @@ class RunnableAssign(RunnableSerializable[dict[str, Any], dict[str, Any]]):
|
||||
@classmethod
|
||||
@override
|
||||
def is_lc_serializable(cls) -> bool:
|
||||
"""Return True as this class is serializable."""
|
||||
"""Return `True` as this class is serializable."""
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
@@ -668,13 +668,19 @@ class RunnableAssign(RunnableSerializable[dict[str, Any], dict[str, Any]]):
|
||||
yield chunk
|
||||
|
||||
|
||||
class RunnablePick(RunnableSerializable[dict[str, Any], dict[str, Any]]):
|
||||
"""Runnable that picks keys from dict[str, Any] inputs.
|
||||
class RunnablePick(RunnableSerializable[dict[str, Any], Any]):
|
||||
"""`Runnable` that picks keys from `dict[str, Any]` inputs.
|
||||
|
||||
RunnablePick class represents a Runnable that selectively picks keys from a
|
||||
`RunnablePick` class represents a `Runnable` that selectively picks keys from a
|
||||
dictionary input. It allows you to specify one or more keys to extract
|
||||
from the input dictionary. It returns a new dictionary containing only
|
||||
the selected keys.
|
||||
from the input dictionary.
|
||||
|
||||
!!! note "Return Type Behavior"
|
||||
The return type depends on the `keys` parameter:
|
||||
|
||||
- When `keys` is a `str`: Returns the single value associated with that key
|
||||
- When `keys` is a `list`: Returns a dictionary containing only the selected
|
||||
keys
|
||||
|
||||
Example:
|
||||
```python
|
||||
@@ -687,18 +693,22 @@ class RunnablePick(RunnableSerializable[dict[str, Any], dict[str, Any]]):
|
||||
"country": "USA",
|
||||
}
|
||||
|
||||
runnable = RunnablePick(keys=["name", "age"])
|
||||
# Single key - returns the value directly
|
||||
runnable_single = RunnablePick(keys="name")
|
||||
result_single = runnable_single.invoke(input_data)
|
||||
print(result_single) # Output: "John"
|
||||
|
||||
output_data = runnable.invoke(input_data)
|
||||
|
||||
print(output_data) # Output: {'name': 'John', 'age': 30}
|
||||
# Multiple keys - returns a dictionary
|
||||
runnable_multiple = RunnablePick(keys=["name", "age"])
|
||||
result_multiple = runnable_multiple.invoke(input_data)
|
||||
print(result_multiple) # Output: {'name': 'John', 'age': 30}
|
||||
```
|
||||
"""
|
||||
|
||||
keys: str | list[str]
|
||||
|
||||
def __init__(self, keys: str | list[str], **kwargs: Any) -> None:
|
||||
"""Create a RunnablePick.
|
||||
"""Create a `RunnablePick`.
|
||||
|
||||
Args:
|
||||
keys: A single key or a list of keys to pick from the input dictionary.
|
||||
@@ -708,7 +718,7 @@ class RunnablePick(RunnableSerializable[dict[str, Any], dict[str, Any]]):
|
||||
@classmethod
|
||||
@override
|
||||
def is_lc_serializable(cls) -> bool:
|
||||
"""Return True as this class is serializable."""
|
||||
"""Return `True` as this class is serializable."""
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -40,11 +40,11 @@ class RouterInput(TypedDict):
|
||||
key: str
|
||||
"""The key to route on."""
|
||||
input: Any
|
||||
"""The input to pass to the selected Runnable."""
|
||||
"""The input to pass to the selected `Runnable`."""
|
||||
|
||||
|
||||
class RouterRunnable(RunnableSerializable[RouterInput, Output]):
|
||||
"""Runnable that routes to a set of Runnables based on Input['key'].
|
||||
"""`Runnable` that routes to a set of `Runnable` based on `Input['key']`.
|
||||
|
||||
Returns the output of the selected Runnable.
|
||||
|
||||
@@ -74,10 +74,10 @@ class RouterRunnable(RunnableSerializable[RouterInput, Output]):
|
||||
self,
|
||||
runnables: Mapping[str, Runnable[Any, Output] | Callable[[Any], Output]],
|
||||
) -> None:
|
||||
"""Create a RouterRunnable.
|
||||
"""Create a `RouterRunnable`.
|
||||
|
||||
Args:
|
||||
runnables: A mapping of keys to Runnables.
|
||||
runnables: A mapping of keys to `Runnable` objects.
|
||||
"""
|
||||
super().__init__(
|
||||
runnables={key: coerce_to_runnable(r) for key, r in runnables.items()}
|
||||
@@ -90,7 +90,7 @@ class RouterRunnable(RunnableSerializable[RouterInput, Output]):
|
||||
@classmethod
|
||||
@override
|
||||
def is_lc_serializable(cls) -> bool:
|
||||
"""Return True as this class is serializable."""
|
||||
"""Return `True` as this class is serializable."""
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -28,7 +28,7 @@ class EventData(TypedDict, total=False):
|
||||
|
||||
This field is only available if the `Runnable` raised an exception.
|
||||
|
||||
!!! version-added "Added in version 1.0.0"
|
||||
!!! version-added "Added in `langchain-core` 1.0.0"
|
||||
"""
|
||||
output: Any
|
||||
"""The output of the `Runnable` that generated the event.
|
||||
|
||||
@@ -7,8 +7,7 @@ import asyncio
|
||||
import inspect
|
||||
import sys
|
||||
import textwrap
|
||||
from collections.abc import Callable, Mapping, Sequence
|
||||
from contextvars import Context
|
||||
from collections.abc import Mapping, Sequence
|
||||
from functools import lru_cache
|
||||
from inspect import signature
|
||||
from itertools import groupby
|
||||
@@ -31,9 +30,11 @@ if TYPE_CHECKING:
|
||||
AsyncIterable,
|
||||
AsyncIterator,
|
||||
Awaitable,
|
||||
Callable,
|
||||
Coroutine,
|
||||
Iterable,
|
||||
)
|
||||
from contextvars import Context
|
||||
|
||||
from langchain_core.runnables.schema import StreamEvent
|
||||
|
||||
|
||||
@@ -125,9 +125,11 @@ def print_sys_info(*, additional_pkgs: Sequence[str] = ()) -> None:
|
||||
for dep in sub_dependencies:
|
||||
try:
|
||||
dep_version = metadata.version(dep)
|
||||
print(f"> {dep}: {dep_version}")
|
||||
except Exception:
|
||||
print(f"> {dep}: Installed. No version info available.")
|
||||
dep_version = None
|
||||
|
||||
if dep_version is not None:
|
||||
print(f"> {dep}: {dep_version}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -386,6 +386,8 @@ class ToolException(Exception): # noqa: N818
|
||||
|
||||
ArgsSchema = TypeBaseModel | dict[str, Any]
|
||||
|
||||
_EMPTY_SET: frozenset[str] = frozenset()
|
||||
|
||||
|
||||
class BaseTool(RunnableSerializable[str | dict | ToolCall, Any]):
|
||||
"""Base class for all LangChain tools.
|
||||
@@ -569,6 +571,11 @@ class ChildTool(BaseTool):
|
||||
self.name, full_schema, fields, fn_description=self.description
|
||||
)
|
||||
|
||||
@functools.cached_property
|
||||
def _injected_args_keys(self) -> frozenset[str]:
|
||||
# base implementation doesn't manage injected args
|
||||
return _EMPTY_SET
|
||||
|
||||
# --- Runnable ---
|
||||
|
||||
@override
|
||||
@@ -649,6 +656,7 @@ class ChildTool(BaseTool):
|
||||
if isinstance(input_args, dict):
|
||||
return tool_input
|
||||
if issubclass(input_args, BaseModel):
|
||||
# Check args_schema for InjectedToolCallId
|
||||
for k, v in get_all_basemodel_annotations(input_args).items():
|
||||
if _is_injected_arg_type(v, injected_type=InjectedToolCallId):
|
||||
if tool_call_id is None:
|
||||
@@ -664,6 +672,7 @@ class ChildTool(BaseTool):
|
||||
result = input_args.model_validate(tool_input)
|
||||
result_dict = result.model_dump()
|
||||
elif issubclass(input_args, BaseModelV1):
|
||||
# Check args_schema for InjectedToolCallId
|
||||
for k, v in get_all_basemodel_annotations(input_args).items():
|
||||
if _is_injected_arg_type(v, injected_type=InjectedToolCallId):
|
||||
if tool_call_id is None:
|
||||
@@ -683,9 +692,25 @@ class ChildTool(BaseTool):
|
||||
f"args_schema must be a Pydantic BaseModel, got {self.args_schema}"
|
||||
)
|
||||
raise NotImplementedError(msg)
|
||||
return {
|
||||
k: getattr(result, k) for k, v in result_dict.items() if k in tool_input
|
||||
validated_input = {
|
||||
k: getattr(result, k) for k in result_dict if k in tool_input
|
||||
}
|
||||
for k in self._injected_args_keys:
|
||||
if k == "tool_call_id":
|
||||
if tool_call_id is None:
|
||||
msg = (
|
||||
"When tool includes an InjectedToolCallId "
|
||||
"argument, tool must always be invoked with a full "
|
||||
"model ToolCall of the form: {'args': {...}, "
|
||||
"'name': '...', 'type': 'tool_call', "
|
||||
"'tool_call_id': '...'}"
|
||||
)
|
||||
raise ValueError(msg)
|
||||
validated_input[k] = tool_call_id
|
||||
if k in tool_input:
|
||||
injected_val = tool_input[k]
|
||||
validated_input[k] = injected_val
|
||||
return validated_input
|
||||
return tool_input
|
||||
|
||||
@abstractmethod
|
||||
@@ -872,16 +897,19 @@ class ChildTool(BaseTool):
|
||||
tool_kwargs |= {config_param: config}
|
||||
response = context.run(self._run, *tool_args, **tool_kwargs)
|
||||
if self.response_format == "content_and_artifact":
|
||||
if not isinstance(response, tuple) or len(response) != 2:
|
||||
msg = (
|
||||
"Since response_format='content_and_artifact' "
|
||||
"a two-tuple of the message content and raw tool output is "
|
||||
f"expected. Instead generated response of type: "
|
||||
f"{type(response)}."
|
||||
)
|
||||
msg = (
|
||||
"Since response_format='content_and_artifact' "
|
||||
"a two-tuple of the message content and raw tool output is "
|
||||
f"expected. Instead, generated response is of type: "
|
||||
f"{type(response)}."
|
||||
)
|
||||
if not isinstance(response, tuple):
|
||||
error_to_raise = ValueError(msg)
|
||||
else:
|
||||
content, artifact = response
|
||||
try:
|
||||
content, artifact = response
|
||||
except ValueError:
|
||||
error_to_raise = ValueError(msg)
|
||||
else:
|
||||
content = response
|
||||
except (ValidationError, ValidationErrorV1) as e:
|
||||
@@ -998,16 +1026,19 @@ class ChildTool(BaseTool):
|
||||
coro = self._arun(*tool_args, **tool_kwargs)
|
||||
response = await coro_with_context(coro, context)
|
||||
if self.response_format == "content_and_artifact":
|
||||
if not isinstance(response, tuple) or len(response) != 2:
|
||||
msg = (
|
||||
"Since response_format='content_and_artifact' "
|
||||
"a two-tuple of the message content and raw tool output is "
|
||||
f"expected. Instead generated response of type: "
|
||||
f"{type(response)}."
|
||||
)
|
||||
msg = (
|
||||
"Since response_format='content_and_artifact' "
|
||||
"a two-tuple of the message content and raw tool output is "
|
||||
f"expected. Instead, generated response is of type: "
|
||||
f"{type(response)}."
|
||||
)
|
||||
if not isinstance(response, tuple):
|
||||
error_to_raise = ValueError(msg)
|
||||
else:
|
||||
content, artifact = response
|
||||
try:
|
||||
content, artifact = response
|
||||
except ValueError:
|
||||
error_to_raise = ValueError(msg)
|
||||
else:
|
||||
content = response
|
||||
except ValidationError as e:
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import textwrap
|
||||
from collections.abc import Awaitable, Callable
|
||||
from inspect import signature
|
||||
@@ -21,10 +22,12 @@ from langchain_core.callbacks import (
|
||||
)
|
||||
from langchain_core.runnables import RunnableConfig, run_in_executor
|
||||
from langchain_core.tools.base import (
|
||||
_EMPTY_SET,
|
||||
FILTERED_ARGS,
|
||||
ArgsSchema,
|
||||
BaseTool,
|
||||
_get_runnable_config_param,
|
||||
_is_injected_arg_type,
|
||||
create_schema_from_function,
|
||||
)
|
||||
from langchain_core.utils.pydantic import is_basemodel_subclass
|
||||
@@ -241,6 +244,17 @@ class StructuredTool(BaseTool):
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@functools.cached_property
|
||||
def _injected_args_keys(self) -> frozenset[str]:
|
||||
fn = self.func or self.coroutine
|
||||
if fn is None:
|
||||
return _EMPTY_SET
|
||||
return frozenset(
|
||||
k
|
||||
for k, v in signature(fn).parameters.items()
|
||||
if _is_injected_arg_type(v.annotation)
|
||||
)
|
||||
|
||||
|
||||
def _filter_schema_args(func: Callable) -> list[str]:
|
||||
filter_args = list(FILTERED_ARGS)
|
||||
|
||||
@@ -15,12 +15,6 @@ from typing import (
|
||||
|
||||
from langchain_core.exceptions import TracerException
|
||||
from langchain_core.load import dumpd
|
||||
from langchain_core.outputs import (
|
||||
ChatGeneration,
|
||||
ChatGenerationChunk,
|
||||
GenerationChunk,
|
||||
LLMResult,
|
||||
)
|
||||
from langchain_core.tracers.schemas import Run
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -31,6 +25,12 @@ if TYPE_CHECKING:
|
||||
|
||||
from langchain_core.documents import Document
|
||||
from langchain_core.messages import BaseMessage
|
||||
from langchain_core.outputs import (
|
||||
ChatGeneration,
|
||||
ChatGenerationChunk,
|
||||
GenerationChunk,
|
||||
LLMResult,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import logging
|
||||
import types
|
||||
import typing
|
||||
import uuid
|
||||
from collections.abc import Callable
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Annotated,
|
||||
@@ -33,6 +32,8 @@ from langchain_core.utils.json_schema import dereference_refs
|
||||
from langchain_core.utils.pydantic import is_basemodel_subclass
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
from langchain_core.tools import BaseTool
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -351,7 +352,8 @@ def convert_to_openai_function(
|
||||
Raises:
|
||||
ValueError: If function is not in a supported format.
|
||||
|
||||
!!! warning "Behavior changed in 0.3.16"
|
||||
!!! warning "Behavior changed in `langchain-core` 0.3.16"
|
||||
|
||||
`description` and `parameters` keys are now optional. Only `name` is
|
||||
required and guaranteed to be part of the output.
|
||||
"""
|
||||
@@ -412,7 +414,7 @@ def convert_to_openai_function(
|
||||
if strict is not None:
|
||||
if "strict" in oai_function and oai_function["strict"] != strict:
|
||||
msg = (
|
||||
f"Tool/function already has a 'strict' key wth value "
|
||||
f"Tool/function already has a 'strict' key with value "
|
||||
f"{oai_function['strict']} which is different from the explicit "
|
||||
f"`strict` arg received {strict=}."
|
||||
)
|
||||
@@ -475,16 +477,19 @@ def convert_to_openai_tool(
|
||||
A dict version of the passed in tool which is compatible with the
|
||||
OpenAI tool-calling API.
|
||||
|
||||
!!! warning "Behavior changed in 0.3.16"
|
||||
!!! warning "Behavior changed in `langchain-core` 0.3.16"
|
||||
|
||||
`description` and `parameters` keys are now optional. Only `name` is
|
||||
required and guaranteed to be part of the output.
|
||||
|
||||
!!! warning "Behavior changed in 0.3.44"
|
||||
!!! warning "Behavior changed in `langchain-core` 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"`.
|
||||
|
||||
!!! warning "Behavior changed in 0.3.63"
|
||||
!!! warning "Behavior changed in `langchain-core` 0.3.63"
|
||||
|
||||
Added support for OpenAI's image generation built-in tool.
|
||||
"""
|
||||
# Import locally to prevent circular import
|
||||
@@ -653,6 +658,9 @@ def tool_example_to_messages(
|
||||
return messages
|
||||
|
||||
|
||||
_MIN_DOCSTRING_BLOCKS = 2
|
||||
|
||||
|
||||
def _parse_google_docstring(
|
||||
docstring: str | None,
|
||||
args: list[str],
|
||||
@@ -671,7 +679,7 @@ def _parse_google_docstring(
|
||||
arg for arg in args if arg not in {"run_manager", "callbacks", "return"}
|
||||
}
|
||||
if filtered_annotations and (
|
||||
len(docstring_blocks) < 2
|
||||
len(docstring_blocks) < _MIN_DOCSTRING_BLOCKS
|
||||
or not any(block.startswith("Args:") for block in docstring_blocks[1:])
|
||||
):
|
||||
msg = "Found invalid Google-Style docstring."
|
||||
|
||||
@@ -4,11 +4,13 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from langchain_core.exceptions import OutputParserException
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
|
||||
def _replace_new_line(match: re.Match[str]) -> str:
|
||||
value = match.group(2)
|
||||
|
||||
@@ -170,28 +170,33 @@ def dereference_refs(
|
||||
full_schema: dict | None = None,
|
||||
skip_keys: Sequence[str] | None = None,
|
||||
) -> dict:
|
||||
"""Resolve and inline JSON Schema $ref references in a schema object.
|
||||
"""Resolve and inline JSON Schema `$ref` references in a schema object.
|
||||
|
||||
This function processes a JSON Schema and resolves all $ref references by replacing
|
||||
them with the actual referenced content. It handles both simple references and
|
||||
complex cases like circular references and mixed $ref objects that contain
|
||||
additional properties alongside the $ref.
|
||||
This function processes a JSON Schema and resolves all `$ref` references by
|
||||
replacing them with the actual referenced content.
|
||||
|
||||
Handles both simple references and complex cases like circular references and mixed
|
||||
`$ref` objects that contain additional properties alongside the `$ref`.
|
||||
|
||||
Args:
|
||||
schema_obj: The JSON Schema object or fragment to process. This can be a
|
||||
complete schema or just a portion of one.
|
||||
full_schema: The complete schema containing all definitions that $refs might
|
||||
point to. If not provided, defaults to schema_obj (useful when the
|
||||
schema is self-contained).
|
||||
skip_keys: Controls recursion behavior and reference resolution depth:
|
||||
- If `None` (Default): Only recurse under '$defs' and use shallow reference
|
||||
resolution (break cycles but don't deep-inline nested refs)
|
||||
- If provided (even as []): Recurse under all keys and use deep reference
|
||||
resolution (fully inline all nested references)
|
||||
schema_obj: The JSON Schema object or fragment to process.
|
||||
|
||||
This can be a complete schema or just a portion of one.
|
||||
full_schema: The complete schema containing all definitions that `$refs` might
|
||||
point to.
|
||||
|
||||
If not provided, defaults to `schema_obj` (useful when the schema is
|
||||
self-contained).
|
||||
skip_keys: Controls recursion behavior and reference resolution depth.
|
||||
|
||||
- If `None` (Default): Only recurse under `'$defs'` and use shallow
|
||||
reference resolution (break cycles but don't deep-inline nested refs)
|
||||
- If provided (even as `[]`): Recurse under all keys and use deep reference
|
||||
resolution (fully inline all nested references)
|
||||
|
||||
Returns:
|
||||
A new dictionary with all $ref references resolved and inlined. The original
|
||||
schema_obj is not modified.
|
||||
A new dictionary with all $ref references resolved and inlined.
|
||||
The original `schema_obj` is not modified.
|
||||
|
||||
Examples:
|
||||
Basic reference resolution:
|
||||
@@ -203,7 +208,8 @@ def dereference_refs(
|
||||
>>> result = dereference_refs(schema)
|
||||
>>> result["properties"]["name"] # {"type": "string"}
|
||||
|
||||
Mixed $ref with additional properties:
|
||||
Mixed `$ref` with additional properties:
|
||||
|
||||
>>> schema = {
|
||||
... "properties": {
|
||||
... "name": {"$ref": "#/$defs/base", "description": "User name"}
|
||||
@@ -215,6 +221,7 @@ def dereference_refs(
|
||||
# {"type": "string", "minLength": 1, "description": "User name"}
|
||||
|
||||
Handling circular references:
|
||||
|
||||
>>> schema = {
|
||||
... "properties": {"user": {"$ref": "#/$defs/User"}},
|
||||
... "$defs": {
|
||||
@@ -227,10 +234,11 @@ def dereference_refs(
|
||||
>>> result = dereference_refs(schema) # Won't cause infinite recursion
|
||||
|
||||
!!! note
|
||||
|
||||
- Circular references are handled gracefully by breaking cycles
|
||||
- Mixed $ref objects (with both $ref and other properties) are supported
|
||||
- Additional properties in mixed $refs override resolved properties
|
||||
- The $defs section is preserved in the output by default
|
||||
- Mixed `$ref` objects (with both `$ref` and other properties) are supported
|
||||
- Additional properties in mixed `$refs` override resolved properties
|
||||
- The `$defs` section is preserved in the output by default
|
||||
"""
|
||||
full = full_schema or schema_obj
|
||||
keys_to_skip = list(skip_keys) if skip_keys is not None else ["$defs"]
|
||||
|
||||
@@ -374,15 +374,29 @@ def _get_key(
|
||||
if resolved_scope in (0, False):
|
||||
return resolved_scope
|
||||
# Move into the scope
|
||||
try:
|
||||
# Try subscripting (Normal dictionaries)
|
||||
resolved_scope = cast("dict[str, Any]", resolved_scope)[child]
|
||||
except (TypeError, AttributeError):
|
||||
if isinstance(resolved_scope, dict):
|
||||
try:
|
||||
resolved_scope = getattr(resolved_scope, child)
|
||||
except (TypeError, AttributeError):
|
||||
# Try as a list
|
||||
resolved_scope = resolved_scope[int(child)] # type: ignore[index]
|
||||
resolved_scope = resolved_scope[child]
|
||||
except (KeyError, TypeError):
|
||||
# Key not found - will be caught by outer try-except
|
||||
msg = f"Key {child!r} not found in dict"
|
||||
raise KeyError(msg) from None
|
||||
elif isinstance(resolved_scope, (list, tuple)):
|
||||
try:
|
||||
resolved_scope = resolved_scope[int(child)]
|
||||
except (ValueError, IndexError, TypeError):
|
||||
# Invalid index - will be caught by outer try-except
|
||||
msg = f"Invalid index {child!r} for list/tuple"
|
||||
raise IndexError(msg) from None
|
||||
else:
|
||||
# Reject everything else for security
|
||||
# This prevents traversing into arbitrary Python objects
|
||||
msg = (
|
||||
f"Cannot traverse into {type(resolved_scope).__name__}. "
|
||||
"Mustache templates only support dict, list, and tuple. "
|
||||
f"Got: {type(resolved_scope)}"
|
||||
)
|
||||
raise TypeError(msg) # noqa: TRY301
|
||||
|
||||
try:
|
||||
# This allows for custom falsy data types
|
||||
@@ -393,8 +407,9 @@ def _get_key(
|
||||
if resolved_scope in (0, False):
|
||||
return resolved_scope
|
||||
return resolved_scope or ""
|
||||
except (AttributeError, KeyError, IndexError, ValueError):
|
||||
except (AttributeError, KeyError, IndexError, ValueError, TypeError):
|
||||
# We couldn't find the key in the current scope
|
||||
# TypeError: Attempted to traverse into non-dict/list type
|
||||
# We'll try again on the next pass
|
||||
pass
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ from __future__ import annotations
|
||||
import inspect
|
||||
import textwrap
|
||||
import warnings
|
||||
from collections.abc import Callable
|
||||
from contextlib import nullcontext
|
||||
from functools import lru_cache, wraps
|
||||
from types import GenericAlias
|
||||
@@ -41,10 +40,12 @@ from pydantic.json_schema import (
|
||||
)
|
||||
from pydantic.v1 import BaseModel as BaseModelV1
|
||||
from pydantic.v1 import create_model as create_model_v1
|
||||
from pydantic.v1.fields import ModelField
|
||||
from typing_extensions import deprecated, override
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
from pydantic.v1.fields import ModelField
|
||||
from pydantic_core import core_schema
|
||||
|
||||
PYDANTIC_VERSION = version.parse(pydantic.__version__)
|
||||
@@ -65,8 +66,8 @@ def get_pydantic_major_version() -> int:
|
||||
PYDANTIC_MAJOR_VERSION = PYDANTIC_VERSION.major
|
||||
PYDANTIC_MINOR_VERSION = PYDANTIC_VERSION.minor
|
||||
|
||||
IS_PYDANTIC_V1 = PYDANTIC_VERSION.major == 1
|
||||
IS_PYDANTIC_V2 = PYDANTIC_VERSION.major == 2
|
||||
IS_PYDANTIC_V1 = False
|
||||
IS_PYDANTIC_V2 = True
|
||||
|
||||
PydanticBaseModel = BaseModel
|
||||
TypeBaseModel = type[BaseModel]
|
||||
|
||||
@@ -11,7 +11,6 @@ import logging
|
||||
import math
|
||||
import warnings
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Callable
|
||||
from itertools import cycle
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
@@ -29,7 +28,7 @@ from langchain_core.retrievers import BaseRetriever, LangSmithRetrieverParams
|
||||
from langchain_core.runnables.config import run_in_executor
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Collection, Iterable, Iterator, Sequence
|
||||
from collections.abc import Callable, Collection, Iterable, Iterator, Sequence
|
||||
|
||||
from langchain_core.callbacks.manager import (
|
||||
AsyncCallbackManagerForRetrieverRun,
|
||||
@@ -295,8 +294,9 @@ class VectorStore(ABC):
|
||||
|
||||
Args:
|
||||
query: Input text.
|
||||
search_type: Type of search to perform. Can be `'similarity'`, `'mmr'`, or
|
||||
`'similarity_score_threshold'`.
|
||||
search_type: Type of search to perform.
|
||||
|
||||
Can be `'similarity'`, `'mmr'`, or `'similarity_score_threshold'`.
|
||||
**kwargs: Arguments to pass to the search method.
|
||||
|
||||
Returns:
|
||||
@@ -329,8 +329,9 @@ class VectorStore(ABC):
|
||||
|
||||
Args:
|
||||
query: Input text.
|
||||
search_type: Type of search to perform. Can be `'similarity'`, `'mmr'`, or
|
||||
`'similarity_score_threshold'`.
|
||||
search_type: Type of search to perform.
|
||||
|
||||
Can be `'similarity'`, `'mmr'`, or `'similarity_score_threshold'`.
|
||||
**kwargs: Arguments to pass to the search method.
|
||||
|
||||
Returns:
|
||||
@@ -461,9 +462,10 @@ class VectorStore(ABC):
|
||||
Args:
|
||||
query: Input text.
|
||||
k: Number of `Document` objects to return.
|
||||
**kwargs: kwargs to be passed to similarity search. Should include
|
||||
`score_threshold`, An optional floating point value between `0` to `1`
|
||||
to filter the resulting set of retrieved docs
|
||||
**kwargs: Kwargs to be passed to similarity search.
|
||||
|
||||
Should include `score_threshold`, an optional floating point value
|
||||
between `0` to `1` to filter the resulting set of retrieved docs.
|
||||
|
||||
Returns:
|
||||
List of tuples of `(doc, similarity_score)`
|
||||
@@ -488,9 +490,10 @@ class VectorStore(ABC):
|
||||
Args:
|
||||
query: Input text.
|
||||
k: Number of `Document` objects to return.
|
||||
**kwargs: kwargs to be passed to similarity search. Should include
|
||||
`score_threshold`, An optional floating point value between `0` to `1`
|
||||
to filter the resulting set of retrieved docs
|
||||
**kwargs: Kwargs to be passed to similarity search.
|
||||
|
||||
Should include `score_threshold`, an optional floating point value
|
||||
between `0` to `1` to filter the resulting set of retrieved docs.
|
||||
|
||||
Returns:
|
||||
List of tuples of `(doc, similarity_score)`
|
||||
@@ -512,9 +515,10 @@ class VectorStore(ABC):
|
||||
Args:
|
||||
query: Input text.
|
||||
k: Number of `Document` objects to return.
|
||||
**kwargs: kwargs to be passed to similarity search. Should include
|
||||
`score_threshold`, An optional floating point value between `0` to `1`
|
||||
to filter the resulting set of retrieved docs
|
||||
**kwargs: Kwargs to be passed to similarity search.
|
||||
|
||||
Should include `score_threshold`, an optional floating point value
|
||||
between `0` to `1` to filter the resulting set of retrieved docs.
|
||||
|
||||
Returns:
|
||||
List of tuples of `(doc, similarity_score)`.
|
||||
@@ -561,9 +565,10 @@ class VectorStore(ABC):
|
||||
Args:
|
||||
query: Input text.
|
||||
k: Number of `Document` objects to return.
|
||||
**kwargs: kwargs to be passed to similarity search. Should include
|
||||
`score_threshold`, An optional floating point value between `0` to `1`
|
||||
to filter the resulting set of retrieved docs
|
||||
**kwargs: Kwargs to be passed to similarity search.
|
||||
|
||||
Should include `score_threshold`, an optional floating point value
|
||||
between `0` to `1` to filter the resulting set of retrieved docs.
|
||||
|
||||
Returns:
|
||||
List of tuples of `(doc, similarity_score)`
|
||||
@@ -901,13 +906,15 @@ class VectorStore(ABC):
|
||||
|
||||
Args:
|
||||
**kwargs: Keyword arguments to pass to the search function.
|
||||
|
||||
Can include:
|
||||
|
||||
* `search_type`: Defines the type of search that the Retriever should
|
||||
perform. Can be `'similarity'` (default), `'mmr'`, or
|
||||
`'similarity_score_threshold'`.
|
||||
* `search_kwargs`: Keyword arguments to pass to the search function. Can
|
||||
include things like:
|
||||
* `search_kwargs`: Keyword arguments to pass to the search function.
|
||||
|
||||
Can include things like:
|
||||
|
||||
* `k`: Amount of documents to return (Default: `4`)
|
||||
* `score_threshold`: Minimum relevance threshold
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import uuid
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
@@ -20,7 +19,7 @@ from langchain_core.vectorstores.utils import _cosine_similarity as cosine_simil
|
||||
from langchain_core.vectorstores.utils import maximal_marginal_relevance
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterator, Sequence
|
||||
from collections.abc import Callable, Iterator, Sequence
|
||||
|
||||
from langchain_core.embeddings import Embeddings
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""langchain-core version information and utilities."""
|
||||
|
||||
VERSION = "1.0.3"
|
||||
VERSION = "1.1.0"
|
||||
|
||||
@@ -3,8 +3,13 @@ requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
authors = []
|
||||
name = "langchain-core"
|
||||
description = "Building applications with LLMs through composability"
|
||||
license = {text = "MIT"}
|
||||
readme = "README.md"
|
||||
authors = []
|
||||
|
||||
version = "1.1.0"
|
||||
requires-python = ">=3.10.0,<4.0.0"
|
||||
dependencies = [
|
||||
"langsmith>=0.3.45,<1.0.0",
|
||||
@@ -15,10 +20,6 @@ dependencies = [
|
||||
"packaging>=23.2.0,<26.0.0",
|
||||
"pydantic>=2.7.4,<3.0.0",
|
||||
]
|
||||
name = "langchain-core"
|
||||
version = "1.0.3"
|
||||
description = "Building applications with LLMs through composability"
|
||||
readme = "README.md"
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://docs.langchain.com/"
|
||||
@@ -35,7 +36,6 @@ typing = [
|
||||
"mypy>=1.18.1,<1.19.0",
|
||||
"types-pyyaml>=6.0.12.2,<7.0.0.0",
|
||||
"types-requests>=2.28.11.5,<3.0.0.0",
|
||||
"langchain-model-profiles",
|
||||
"langchain-text-splitters",
|
||||
]
|
||||
dev = [
|
||||
@@ -57,7 +57,6 @@ test = [
|
||||
"blockbuster>=1.5.18,<1.6.0",
|
||||
"numpy>=1.26.4; python_version<'3.13'",
|
||||
"numpy>=2.1.0; python_version>='3.13'",
|
||||
"langchain-model-profiles",
|
||||
"langchain-tests",
|
||||
"pytest-benchmark",
|
||||
"pytest-codspeed",
|
||||
@@ -65,7 +64,6 @@ test = [
|
||||
test_integration = []
|
||||
|
||||
[tool.uv.sources]
|
||||
langchain-model-profiles = { path = "../model-profiles" }
|
||||
langchain-tests = { path = "../standard-tests" }
|
||||
langchain-text-splitters = { path = "../text-splitters" }
|
||||
|
||||
@@ -104,7 +102,6 @@ ignore = [
|
||||
"ANN401", # No Any types
|
||||
"BLE", # Blind exceptions
|
||||
"ERA", # No commented-out code
|
||||
"PLR2004", # Comparison to magic number
|
||||
]
|
||||
unfixable = [
|
||||
"B028", # People should intentionally tune the stacklevel
|
||||
@@ -125,7 +122,7 @@ ignore-var-parameters = true # ignore missing documentation for *args and **kwa
|
||||
"langchain_core/utils/mustache.py" = [ "PLW0603",]
|
||||
"langchain_core/sys_info.py" = [ "T201",]
|
||||
"tests/unit_tests/test_tools.py" = [ "ARG",]
|
||||
"tests/**" = [ "D1", "S", "SLF",]
|
||||
"tests/**" = [ "D1", "PLR2004", "S", "SLF",]
|
||||
"scripts/**" = [ "INP", "S",]
|
||||
|
||||
[tool.coverage.run]
|
||||
@@ -133,7 +130,10 @@ omit = [ "tests/*",]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
addopts = "--snapshot-warn-unused --strict-markers --strict-config --durations=5"
|
||||
markers = [ "requires: mark tests as requiring a specific library", "compile: mark placeholder test used to compile integration tests without running them", ]
|
||||
markers = [
|
||||
"requires: mark tests as requiring a specific library",
|
||||
"compile: mark placeholder test used to compile integration tests without running them",
|
||||
]
|
||||
asyncio_mode = "auto"
|
||||
filterwarnings = [ "ignore::langchain_core._api.beta_decorator.LangChainBetaWarning",]
|
||||
asyncio_default_fixture_loop_scope = "function"
|
||||
filterwarnings = [ "ignore::langchain_core._api.beta_decorator.LangChainBetaWarning",]
|
||||
|
||||
@@ -148,4 +148,65 @@ async def test_inline_handlers_share_parent_context_multiple() -> None:
|
||||
2,
|
||||
3,
|
||||
3,
|
||||
], f"Expected order of states was broken due to context loss. Got {states}"
|
||||
]
|
||||
|
||||
|
||||
async def test_shielded_callback_context_preservation() -> None:
|
||||
"""Verify that shielded callbacks preserve context variables.
|
||||
|
||||
This test specifically addresses the issue where async callbacks decorated
|
||||
with @shielded do not properly preserve context variables, breaking
|
||||
instrumentation and other context-dependent functionality.
|
||||
|
||||
The issue manifests in callbacks that use the @shielded decorator:
|
||||
* on_llm_end
|
||||
* on_llm_error
|
||||
* on_chain_end
|
||||
* on_chain_error
|
||||
* And other shielded callback methods
|
||||
"""
|
||||
context_var: contextvars.ContextVar[str] = contextvars.ContextVar("test_context")
|
||||
|
||||
class ContextTestHandler(AsyncCallbackHandler):
|
||||
"""Handler that reads context variables in shielded callbacks."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.run_inline = False
|
||||
self.context_values: list[str] = []
|
||||
|
||||
@override
|
||||
async def on_llm_end(self, response: Any, **kwargs: Any) -> None:
|
||||
"""This method is decorated with @shielded in the run manager."""
|
||||
# This should preserve the context variable value
|
||||
self.context_values.append(context_var.get("not_found"))
|
||||
|
||||
@override
|
||||
async def on_chain_end(self, outputs: Any, **kwargs: Any) -> None:
|
||||
"""This method is decorated with @shielded in the run manager."""
|
||||
# This should preserve the context variable value
|
||||
self.context_values.append(context_var.get("not_found"))
|
||||
|
||||
# Set up the test context
|
||||
context_var.set("test_value")
|
||||
handler = ContextTestHandler()
|
||||
manager = AsyncCallbackManager(handlers=[handler])
|
||||
|
||||
# Create run managers that have the shielded methods
|
||||
llm_managers = await manager.on_llm_start({}, ["test prompt"])
|
||||
llm_run_manager = llm_managers[0]
|
||||
|
||||
chain_run_manager = await manager.on_chain_start({}, {"test": "input"})
|
||||
|
||||
# Test LLM end callback (which is shielded)
|
||||
await llm_run_manager.on_llm_end({"response": "test"}) # type: ignore[arg-type]
|
||||
|
||||
# Test Chain end callback (which is shielded)
|
||||
await chain_run_manager.on_chain_end({"output": "test"})
|
||||
|
||||
# The context should be preserved in shielded callbacks
|
||||
# This was the main issue - shielded decorators were not preserving context
|
||||
assert handler.context_values == ["test_value", "test_value"], (
|
||||
f"Expected context values ['test_value', 'test_value'], "
|
||||
f"but got {handler.context_values}. "
|
||||
f"This indicates the shielded decorator is not preserving context variables."
|
||||
)
|
||||
|
||||
@@ -33,7 +33,7 @@ def test_hashing() -> None:
|
||||
# hash should be deterministic
|
||||
assert hashed_document.id == "fd1dc827-051b-537d-a1fe-1fa043e8b276"
|
||||
|
||||
# Verify that hashing with sha1 is determinstic
|
||||
# Verify that hashing with sha1 is deterministic
|
||||
another_hashed_document = _get_document_with_hash(document, key_encoder="sha1")
|
||||
assert another_hashed_document.id == hashed_document.id
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ from langchain_core.language_models import (
|
||||
ParrotFakeChatModel,
|
||||
)
|
||||
from langchain_core.language_models._utils import _normalize_messages
|
||||
from langchain_core.language_models.chat_models import _generate_response_from_error
|
||||
from langchain_core.language_models.fake_chat_models import (
|
||||
FakeListChatModelError,
|
||||
GenericFakeChatModel,
|
||||
@@ -1221,16 +1222,99 @@ def test_get_ls_params() -> None:
|
||||
|
||||
def test_model_profiles() -> None:
|
||||
model = GenericFakeChatModel(messages=iter([]))
|
||||
profile = model.profile
|
||||
assert profile == {}
|
||||
assert model.profile is None
|
||||
|
||||
class MyModel(GenericFakeChatModel):
|
||||
model: str = "gpt-5"
|
||||
model_with_profile = GenericFakeChatModel(
|
||||
messages=iter([]), profile={"max_input_tokens": 100}
|
||||
)
|
||||
assert model_with_profile.profile == {"max_input_tokens": 100}
|
||||
|
||||
@property
|
||||
def _llm_type(self) -> str:
|
||||
return "openai-chat"
|
||||
|
||||
model = MyModel(messages=iter([]))
|
||||
profile = model.profile
|
||||
assert profile
|
||||
class MockResponse:
|
||||
"""Mock response for testing _generate_response_from_error."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
status_code: int = 400,
|
||||
headers: dict[str, str] | None = None,
|
||||
json_data: dict[str, Any] | None = None,
|
||||
json_raises: type[Exception] | None = None,
|
||||
text_raises: type[Exception] | None = None,
|
||||
):
|
||||
self.status_code = status_code
|
||||
self.headers = headers or {}
|
||||
self._json_data = json_data
|
||||
self._json_raises = json_raises
|
||||
self._text_raises = text_raises
|
||||
|
||||
def json(self) -> dict[str, Any]:
|
||||
if self._json_raises:
|
||||
msg = "JSON parsing failed"
|
||||
raise self._json_raises(msg)
|
||||
return self._json_data or {}
|
||||
|
||||
@property
|
||||
def text(self) -> str:
|
||||
if self._text_raises:
|
||||
msg = "Text access failed"
|
||||
raise self._text_raises(msg)
|
||||
return ""
|
||||
|
||||
|
||||
class MockAPIError(Exception):
|
||||
"""Mock API error with response attribute."""
|
||||
|
||||
def __init__(self, message: str, response: MockResponse | None = None):
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
if response is not None:
|
||||
self.response = response
|
||||
|
||||
|
||||
def test_generate_response_from_error_with_valid_json() -> None:
|
||||
"""Test `_generate_response_from_error` with valid JSON response."""
|
||||
response = MockResponse(
|
||||
status_code=400,
|
||||
headers={"content-type": "application/json"},
|
||||
json_data={"error": {"message": "Bad request", "type": "invalid_request"}},
|
||||
)
|
||||
error = MockAPIError("API Error", response=response)
|
||||
|
||||
generations = _generate_response_from_error(error)
|
||||
|
||||
assert len(generations) == 1
|
||||
generation = generations[0]
|
||||
assert isinstance(generation, ChatGeneration)
|
||||
assert isinstance(generation.message, AIMessage)
|
||||
assert generation.message.content == ""
|
||||
|
||||
metadata = generation.message.response_metadata
|
||||
assert metadata["body"] == {
|
||||
"error": {"message": "Bad request", "type": "invalid_request"}
|
||||
}
|
||||
assert metadata["headers"] == {"content-type": "application/json"}
|
||||
assert metadata["status_code"] == 400
|
||||
|
||||
|
||||
def test_generate_response_from_error_handles_streaming_response_failure() -> None:
|
||||
# Simulates scenario where accessing response.json() or response.text
|
||||
# raises ResponseNotRead on streaming responses
|
||||
response = MockResponse(
|
||||
status_code=400,
|
||||
headers={"content-type": "application/json"},
|
||||
json_raises=Exception, # Simulates ResponseNotRead or similar
|
||||
text_raises=Exception,
|
||||
)
|
||||
error = MockAPIError("API Error", response=response)
|
||||
|
||||
# This should NOT raise an exception, but should handle it gracefully
|
||||
generations = _generate_response_from_error(error)
|
||||
|
||||
assert len(generations) == 1
|
||||
generation = generations[0]
|
||||
metadata = generation.message.response_metadata
|
||||
|
||||
# When both fail, body should be None instead of raising an exception
|
||||
assert metadata["body"] is None
|
||||
assert metadata["headers"] == {"content-type": "application/json"}
|
||||
assert metadata["status_code"] == 400
|
||||
|
||||
@@ -18,6 +18,8 @@ EXPECTED_ALL = [
|
||||
"FakeStreamingListLLM",
|
||||
"FakeListLLM",
|
||||
"ParrotFakeChatModel",
|
||||
"ModelProfile",
|
||||
"ModelProfileRegistry",
|
||||
"is_openai_data_block",
|
||||
]
|
||||
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
"""Test groq block translator."""
|
||||
|
||||
from typing import cast
|
||||
|
||||
import pytest
|
||||
|
||||
from langchain_core.messages import AIMessage
|
||||
from langchain_core.messages import content as types
|
||||
from langchain_core.messages.base import _extract_reasoning_from_additional_kwargs
|
||||
from langchain_core.messages.block_translators import PROVIDER_TRANSLATORS
|
||||
from langchain_core.messages.block_translators.groq import (
|
||||
_parse_code_json,
|
||||
translate_content,
|
||||
)
|
||||
|
||||
|
||||
def test_groq_translator_registered() -> None:
|
||||
"""Test that groq translator is properly registered."""
|
||||
assert "groq" in PROVIDER_TRANSLATORS
|
||||
assert "translate_content" in PROVIDER_TRANSLATORS["groq"]
|
||||
assert "translate_content_chunk" in PROVIDER_TRANSLATORS["groq"]
|
||||
|
||||
|
||||
def test_extract_reasoning_from_additional_kwargs_exists() -> None:
|
||||
"""Test that _extract_reasoning_from_additional_kwargs can be imported."""
|
||||
# Verify it's callable
|
||||
assert callable(_extract_reasoning_from_additional_kwargs)
|
||||
|
||||
|
||||
def test_groq_translate_content_basic() -> None:
|
||||
"""Test basic groq content translation."""
|
||||
# Test with simple text message
|
||||
message = AIMessage(content="Hello world")
|
||||
blocks = translate_content(message)
|
||||
|
||||
assert isinstance(blocks, list)
|
||||
assert len(blocks) == 1
|
||||
assert blocks[0]["type"] == "text"
|
||||
assert blocks[0]["text"] == "Hello world"
|
||||
|
||||
|
||||
def test_groq_translate_content_with_reasoning() -> None:
|
||||
"""Test groq content translation with reasoning content."""
|
||||
# Test with reasoning content in additional_kwargs
|
||||
message = AIMessage(
|
||||
content="Final answer",
|
||||
additional_kwargs={"reasoning_content": "Let me think about this..."},
|
||||
)
|
||||
blocks = translate_content(message)
|
||||
|
||||
assert isinstance(blocks, list)
|
||||
assert len(blocks) == 2
|
||||
|
||||
# First block should be reasoning
|
||||
assert blocks[0]["type"] == "reasoning"
|
||||
assert blocks[0]["reasoning"] == "Let me think about this..."
|
||||
|
||||
# Second block should be text
|
||||
assert blocks[1]["type"] == "text"
|
||||
assert blocks[1]["text"] == "Final answer"
|
||||
|
||||
|
||||
def test_groq_translate_content_with_tool_calls() -> None:
|
||||
"""Test groq content translation with tool calls."""
|
||||
# Test with tool calls
|
||||
message = AIMessage(
|
||||
content="",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "search",
|
||||
"args": {"query": "test"},
|
||||
"id": "call_123",
|
||||
}
|
||||
],
|
||||
)
|
||||
blocks = translate_content(message)
|
||||
|
||||
assert isinstance(blocks, list)
|
||||
assert len(blocks) == 1
|
||||
assert blocks[0]["type"] == "tool_call"
|
||||
assert blocks[0]["name"] == "search"
|
||||
assert blocks[0]["args"] == {"query": "test"}
|
||||
assert blocks[0]["id"] == "call_123"
|
||||
|
||||
|
||||
def test_groq_translate_content_with_executed_tools() -> None:
|
||||
"""Test groq content translation with executed tools (built-in tools)."""
|
||||
# Test with executed_tools in additional_kwargs (Groq built-in tools)
|
||||
message = AIMessage(
|
||||
content="",
|
||||
additional_kwargs={
|
||||
"executed_tools": [
|
||||
{
|
||||
"type": "python",
|
||||
"arguments": '{"code": "print(\\"hello\\")"}',
|
||||
"output": "hello\\n",
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
blocks = translate_content(message)
|
||||
|
||||
assert isinstance(blocks, list)
|
||||
# Should have server_tool_call and server_tool_result
|
||||
assert len(blocks) >= 2
|
||||
|
||||
# Check for server_tool_call
|
||||
tool_call_blocks = [
|
||||
cast("types.ServerToolCall", b)
|
||||
for b in blocks
|
||||
if b.get("type") == "server_tool_call"
|
||||
]
|
||||
assert len(tool_call_blocks) == 1
|
||||
assert tool_call_blocks[0]["name"] == "code_interpreter"
|
||||
assert "code" in tool_call_blocks[0]["args"]
|
||||
|
||||
# Check for server_tool_result
|
||||
tool_result_blocks = [
|
||||
cast("types.ServerToolResult", b)
|
||||
for b in blocks
|
||||
if b.get("type") == "server_tool_result"
|
||||
]
|
||||
assert len(tool_result_blocks) == 1
|
||||
assert tool_result_blocks[0]["output"] == "hello\\n"
|
||||
assert tool_result_blocks[0]["status"] == "success"
|
||||
|
||||
|
||||
def test_parse_code_json() -> None:
|
||||
"""Test the _parse_code_json helper function."""
|
||||
# Test valid code JSON
|
||||
result = _parse_code_json('{"code": "print(\'hello\')"}')
|
||||
assert result == {"code": "print('hello')"}
|
||||
|
||||
# Test code with unescaped quotes (Groq format)
|
||||
result = _parse_code_json('{"code": "print("hello")"}')
|
||||
assert result == {"code": 'print("hello")'}
|
||||
|
||||
# Test invalid format raises ValueError
|
||||
with pytest.raises(ValueError, match="Could not extract Python code"):
|
||||
_parse_code_json('{"invalid": "format"}')
|
||||
@@ -1,3 +1,4 @@
|
||||
import sys
|
||||
from collections.abc import AsyncIterator, Iterator
|
||||
from typing import Any
|
||||
|
||||
@@ -886,3 +887,461 @@ def test_max_tokens_error(caplog: Any) -> None:
|
||||
"`max_tokens` stop reason" in msg and record.levelname == "ERROR"
|
||||
for record, msg in zip(caplog.records, caplog.messages, strict=False)
|
||||
)
|
||||
|
||||
|
||||
def test_pydantic_tools_parser_with_mixed_pydantic_versions() -> None:
|
||||
"""Test PydanticToolsParser with both Pydantic v1 and v2 models."""
|
||||
# For Python 3.14+ compatibility, use create_model for Pydantic v1
|
||||
if sys.version_info >= (3, 14):
|
||||
WeatherV1 = pydantic.v1.create_model( # noqa: N806
|
||||
"WeatherV1",
|
||||
__doc__="Weather information using Pydantic v1.",
|
||||
temperature=(int, ...),
|
||||
conditions=(str, ...),
|
||||
)
|
||||
else:
|
||||
|
||||
class WeatherV1(pydantic.v1.BaseModel):
|
||||
"""Weather information using Pydantic v1."""
|
||||
|
||||
temperature: int
|
||||
conditions: str
|
||||
|
||||
class LocationV2(BaseModel):
|
||||
"""Location information using Pydantic v2."""
|
||||
|
||||
city: str
|
||||
country: str
|
||||
|
||||
# Test with Pydantic v1 model
|
||||
parser_v1 = PydanticToolsParser(tools=[WeatherV1])
|
||||
message_v1 = AIMessage(
|
||||
content="",
|
||||
tool_calls=[
|
||||
{
|
||||
"id": "call_weather",
|
||||
"name": "WeatherV1",
|
||||
"args": {"temperature": 25, "conditions": "sunny"},
|
||||
}
|
||||
],
|
||||
)
|
||||
generation_v1 = ChatGeneration(message=message_v1)
|
||||
result_v1 = parser_v1.parse_result([generation_v1])
|
||||
|
||||
assert len(result_v1) == 1
|
||||
assert isinstance(result_v1[0], WeatherV1)
|
||||
assert result_v1[0].temperature == 25 # type: ignore[attr-defined,unused-ignore]
|
||||
assert result_v1[0].conditions == "sunny" # type: ignore[attr-defined,unused-ignore]
|
||||
|
||||
# Test with Pydantic v2 model
|
||||
parser_v2 = PydanticToolsParser(tools=[LocationV2])
|
||||
message_v2 = AIMessage(
|
||||
content="",
|
||||
tool_calls=[
|
||||
{
|
||||
"id": "call_location",
|
||||
"name": "LocationV2",
|
||||
"args": {"city": "Paris", "country": "France"},
|
||||
}
|
||||
],
|
||||
)
|
||||
generation_v2 = ChatGeneration(message=message_v2)
|
||||
result_v2 = parser_v2.parse_result([generation_v2])
|
||||
|
||||
assert len(result_v2) == 1
|
||||
assert isinstance(result_v2[0], LocationV2)
|
||||
assert result_v2[0].city == "Paris"
|
||||
assert result_v2[0].country == "France"
|
||||
|
||||
# Test with both v1 and v2 models
|
||||
parser_mixed = PydanticToolsParser(tools=[WeatherV1, LocationV2])
|
||||
message_mixed = AIMessage(
|
||||
content="",
|
||||
tool_calls=[
|
||||
{
|
||||
"id": "call_weather",
|
||||
"name": "WeatherV1",
|
||||
"args": {"temperature": 20, "conditions": "cloudy"},
|
||||
},
|
||||
{
|
||||
"id": "call_location",
|
||||
"name": "LocationV2",
|
||||
"args": {"city": "London", "country": "UK"},
|
||||
},
|
||||
],
|
||||
)
|
||||
generation_mixed = ChatGeneration(message=message_mixed)
|
||||
result_mixed = parser_mixed.parse_result([generation_mixed])
|
||||
|
||||
assert len(result_mixed) == 2
|
||||
assert isinstance(result_mixed[0], WeatherV1)
|
||||
assert result_mixed[0].temperature == 20 # type: ignore[attr-defined,unused-ignore]
|
||||
assert isinstance(result_mixed[1], LocationV2)
|
||||
assert result_mixed[1].city == "London"
|
||||
|
||||
|
||||
def test_pydantic_tools_parser_with_custom_title() -> None:
|
||||
"""Test PydanticToolsParser with Pydantic v2 model using custom title."""
|
||||
|
||||
class CustomTitleTool(BaseModel):
|
||||
"""Tool with custom title in model config."""
|
||||
|
||||
model_config = {"title": "MyCustomToolName"}
|
||||
|
||||
value: int
|
||||
description: str
|
||||
|
||||
# Test with custom title - tool should be callable by custom name
|
||||
parser = PydanticToolsParser(tools=[CustomTitleTool])
|
||||
message = AIMessage(
|
||||
content="",
|
||||
tool_calls=[
|
||||
{
|
||||
"id": "call_custom",
|
||||
"name": "MyCustomToolName",
|
||||
"args": {"value": 42, "description": "test"},
|
||||
}
|
||||
],
|
||||
)
|
||||
generation = ChatGeneration(message=message)
|
||||
result = parser.parse_result([generation])
|
||||
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], CustomTitleTool)
|
||||
assert result[0].value == 42
|
||||
assert result[0].description == "test"
|
||||
|
||||
|
||||
def test_pydantic_tools_parser_name_dict_fallback() -> None:
|
||||
"""Test that name_dict properly falls back to __name__ when title is None."""
|
||||
|
||||
class ToolWithoutTitle(BaseModel):
|
||||
"""Tool without explicit title."""
|
||||
|
||||
data: str
|
||||
|
||||
# Ensure model_config doesn't have a title or it's None
|
||||
# (This is the default behavior)
|
||||
parser = PydanticToolsParser(tools=[ToolWithoutTitle])
|
||||
message = AIMessage(
|
||||
content="",
|
||||
tool_calls=[
|
||||
{
|
||||
"id": "call_no_title",
|
||||
"name": "ToolWithoutTitle",
|
||||
"args": {"data": "test_data"},
|
||||
}
|
||||
],
|
||||
)
|
||||
generation = ChatGeneration(message=message)
|
||||
result = parser.parse_result([generation])
|
||||
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], ToolWithoutTitle)
|
||||
assert result[0].data == "test_data"
|
||||
|
||||
|
||||
def test_pydantic_tools_parser_with_nested_models() -> None:
|
||||
"""Test PydanticToolsParser with nested Pydantic v1 and v2 models."""
|
||||
# Nested v1 models
|
||||
if sys.version_info >= (3, 14):
|
||||
AddressV1 = pydantic.v1.create_model( # noqa: N806
|
||||
"AddressV1",
|
||||
__doc__="Address using Pydantic v1.",
|
||||
street=(str, ...),
|
||||
city=(str, ...),
|
||||
zip_code=(str, ...),
|
||||
)
|
||||
PersonV1 = pydantic.v1.create_model( # noqa: N806
|
||||
"PersonV1",
|
||||
__doc__="Person with nested address using Pydantic v1.",
|
||||
name=(str, ...),
|
||||
age=(int, ...),
|
||||
address=(AddressV1, ...),
|
||||
)
|
||||
else:
|
||||
|
||||
class AddressV1(pydantic.v1.BaseModel):
|
||||
"""Address using Pydantic v1."""
|
||||
|
||||
street: str
|
||||
city: str
|
||||
zip_code: str
|
||||
|
||||
class PersonV1(pydantic.v1.BaseModel):
|
||||
"""Person with nested address using Pydantic v1."""
|
||||
|
||||
name: str
|
||||
age: int
|
||||
address: AddressV1
|
||||
|
||||
# Nested v2 models
|
||||
class CoordinatesV2(BaseModel):
|
||||
"""Coordinates using Pydantic v2."""
|
||||
|
||||
latitude: float
|
||||
longitude: float
|
||||
|
||||
class LocationV2(BaseModel):
|
||||
"""Location with nested coordinates using Pydantic v2."""
|
||||
|
||||
name: str
|
||||
coordinates: CoordinatesV2
|
||||
|
||||
# Test with nested Pydantic v1 model
|
||||
parser_v1 = PydanticToolsParser(tools=[PersonV1])
|
||||
message_v1 = AIMessage(
|
||||
content="",
|
||||
tool_calls=[
|
||||
{
|
||||
"id": "call_person",
|
||||
"name": "PersonV1",
|
||||
"args": {
|
||||
"name": "Alice",
|
||||
"age": 30,
|
||||
"address": {
|
||||
"street": "123 Main St",
|
||||
"city": "Springfield",
|
||||
"zip_code": "12345",
|
||||
},
|
||||
},
|
||||
}
|
||||
],
|
||||
)
|
||||
generation_v1 = ChatGeneration(message=message_v1)
|
||||
result_v1 = parser_v1.parse_result([generation_v1])
|
||||
|
||||
assert len(result_v1) == 1
|
||||
assert isinstance(result_v1[0], PersonV1)
|
||||
assert result_v1[0].name == "Alice" # type: ignore[attr-defined,unused-ignore]
|
||||
assert result_v1[0].age == 30 # type: ignore[attr-defined,unused-ignore]
|
||||
assert isinstance(result_v1[0].address, AddressV1) # type: ignore[attr-defined,unused-ignore]
|
||||
assert result_v1[0].address.street == "123 Main St" # type: ignore[attr-defined,unused-ignore]
|
||||
assert result_v1[0].address.city == "Springfield" # type: ignore[attr-defined,unused-ignore]
|
||||
|
||||
# Test with nested Pydantic v2 model
|
||||
parser_v2 = PydanticToolsParser(tools=[LocationV2])
|
||||
message_v2 = AIMessage(
|
||||
content="",
|
||||
tool_calls=[
|
||||
{
|
||||
"id": "call_location",
|
||||
"name": "LocationV2",
|
||||
"args": {
|
||||
"name": "Eiffel Tower",
|
||||
"coordinates": {"latitude": 48.8584, "longitude": 2.2945},
|
||||
},
|
||||
}
|
||||
],
|
||||
)
|
||||
generation_v2 = ChatGeneration(message=message_v2)
|
||||
result_v2 = parser_v2.parse_result([generation_v2])
|
||||
|
||||
assert len(result_v2) == 1
|
||||
assert isinstance(result_v2[0], LocationV2)
|
||||
assert result_v2[0].name == "Eiffel Tower"
|
||||
assert isinstance(result_v2[0].coordinates, CoordinatesV2)
|
||||
assert result_v2[0].coordinates.latitude == 48.8584
|
||||
assert result_v2[0].coordinates.longitude == 2.2945
|
||||
|
||||
# Test with both nested models in one message
|
||||
parser_mixed = PydanticToolsParser(tools=[PersonV1, LocationV2])
|
||||
message_mixed = AIMessage(
|
||||
content="",
|
||||
tool_calls=[
|
||||
{
|
||||
"id": "call_person",
|
||||
"name": "PersonV1",
|
||||
"args": {
|
||||
"name": "Bob",
|
||||
"age": 25,
|
||||
"address": {
|
||||
"street": "456 Oak Ave",
|
||||
"city": "Portland",
|
||||
"zip_code": "97201",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "call_location",
|
||||
"name": "LocationV2",
|
||||
"args": {
|
||||
"name": "Golden Gate Bridge",
|
||||
"coordinates": {"latitude": 37.8199, "longitude": -122.4783},
|
||||
},
|
||||
},
|
||||
],
|
||||
)
|
||||
generation_mixed = ChatGeneration(message=message_mixed)
|
||||
result_mixed = parser_mixed.parse_result([generation_mixed])
|
||||
|
||||
assert len(result_mixed) == 2
|
||||
assert isinstance(result_mixed[0], PersonV1)
|
||||
assert result_mixed[0].name == "Bob" # type: ignore[attr-defined,unused-ignore]
|
||||
assert result_mixed[0].address.city == "Portland" # type: ignore[attr-defined,unused-ignore]
|
||||
assert isinstance(result_mixed[1], LocationV2)
|
||||
assert result_mixed[1].name == "Golden Gate Bridge"
|
||||
assert result_mixed[1].coordinates.latitude == 37.8199
|
||||
|
||||
|
||||
def test_pydantic_tools_parser_with_optional_fields() -> None:
|
||||
"""Test PydanticToolsParser with optional fields in v1 and v2 models."""
|
||||
if sys.version_info >= (3, 14):
|
||||
ProductV1 = pydantic.v1.create_model( # noqa: N806
|
||||
"ProductV1",
|
||||
__doc__="Product with optional fields using Pydantic v1.",
|
||||
name=(str, ...),
|
||||
price=(float, ...),
|
||||
description=(str | None, None),
|
||||
stock=(int, 0),
|
||||
)
|
||||
else:
|
||||
|
||||
class ProductV1(pydantic.v1.BaseModel):
|
||||
"""Product with optional fields using Pydantic v1."""
|
||||
|
||||
name: str
|
||||
price: float
|
||||
description: str | None = None
|
||||
stock: int = 0
|
||||
|
||||
# v2 model with optional fields
|
||||
class UserV2(BaseModel):
|
||||
"""User with optional fields using Pydantic v2."""
|
||||
|
||||
username: str
|
||||
email: str
|
||||
bio: str | None = None
|
||||
age: int | None = None
|
||||
|
||||
# Test v1 with all fields provided
|
||||
parser_v1_full = PydanticToolsParser(tools=[ProductV1])
|
||||
message_v1_full = AIMessage(
|
||||
content="",
|
||||
tool_calls=[
|
||||
{
|
||||
"id": "call_product_full",
|
||||
"name": "ProductV1",
|
||||
"args": {
|
||||
"name": "Laptop",
|
||||
"price": 999.99,
|
||||
"description": "High-end laptop",
|
||||
"stock": 50,
|
||||
},
|
||||
}
|
||||
],
|
||||
)
|
||||
generation_v1_full = ChatGeneration(message=message_v1_full)
|
||||
result_v1_full = parser_v1_full.parse_result([generation_v1_full])
|
||||
|
||||
assert len(result_v1_full) == 1
|
||||
assert isinstance(result_v1_full[0], ProductV1)
|
||||
assert result_v1_full[0].name == "Laptop" # type: ignore[attr-defined,unused-ignore]
|
||||
assert result_v1_full[0].price == 999.99 # type: ignore[attr-defined,unused-ignore]
|
||||
assert result_v1_full[0].description == "High-end laptop" # type: ignore[attr-defined,unused-ignore]
|
||||
assert result_v1_full[0].stock == 50 # type: ignore[attr-defined,unused-ignore]
|
||||
|
||||
# Test v1 with only required fields
|
||||
parser_v1_minimal = PydanticToolsParser(tools=[ProductV1])
|
||||
message_v1_minimal = AIMessage(
|
||||
content="",
|
||||
tool_calls=[
|
||||
{
|
||||
"id": "call_product_minimal",
|
||||
"name": "ProductV1",
|
||||
"args": {"name": "Mouse", "price": 29.99},
|
||||
}
|
||||
],
|
||||
)
|
||||
generation_v1_minimal = ChatGeneration(message=message_v1_minimal)
|
||||
result_v1_minimal = parser_v1_minimal.parse_result([generation_v1_minimal])
|
||||
|
||||
assert len(result_v1_minimal) == 1
|
||||
assert isinstance(result_v1_minimal[0], ProductV1)
|
||||
assert result_v1_minimal[0].name == "Mouse" # type: ignore[attr-defined,unused-ignore]
|
||||
assert result_v1_minimal[0].price == 29.99 # type: ignore[attr-defined,unused-ignore]
|
||||
assert result_v1_minimal[0].description is None # type: ignore[attr-defined,unused-ignore]
|
||||
assert result_v1_minimal[0].stock == 0 # type: ignore[attr-defined,unused-ignore]
|
||||
|
||||
# Test v2 with all fields provided
|
||||
parser_v2_full = PydanticToolsParser(tools=[UserV2])
|
||||
message_v2_full = AIMessage(
|
||||
content="",
|
||||
tool_calls=[
|
||||
{
|
||||
"id": "call_user_full",
|
||||
"name": "UserV2",
|
||||
"args": {
|
||||
"username": "john_doe",
|
||||
"email": "john@example.com",
|
||||
"bio": "Software developer",
|
||||
"age": 28,
|
||||
},
|
||||
}
|
||||
],
|
||||
)
|
||||
generation_v2_full = ChatGeneration(message=message_v2_full)
|
||||
result_v2_full = parser_v2_full.parse_result([generation_v2_full])
|
||||
|
||||
assert len(result_v2_full) == 1
|
||||
assert isinstance(result_v2_full[0], UserV2)
|
||||
assert result_v2_full[0].username == "john_doe"
|
||||
assert result_v2_full[0].email == "john@example.com"
|
||||
assert result_v2_full[0].bio == "Software developer"
|
||||
assert result_v2_full[0].age == 28
|
||||
|
||||
# Test v2 with only required fields
|
||||
parser_v2_minimal = PydanticToolsParser(tools=[UserV2])
|
||||
message_v2_minimal = AIMessage(
|
||||
content="",
|
||||
tool_calls=[
|
||||
{
|
||||
"id": "call_user_minimal",
|
||||
"name": "UserV2",
|
||||
"args": {"username": "jane_smith", "email": "jane@example.com"},
|
||||
}
|
||||
],
|
||||
)
|
||||
generation_v2_minimal = ChatGeneration(message=message_v2_minimal)
|
||||
result_v2_minimal = parser_v2_minimal.parse_result([generation_v2_minimal])
|
||||
|
||||
assert len(result_v2_minimal) == 1
|
||||
assert isinstance(result_v2_minimal[0], UserV2)
|
||||
assert result_v2_minimal[0].username == "jane_smith"
|
||||
assert result_v2_minimal[0].email == "jane@example.com"
|
||||
assert result_v2_minimal[0].bio is None
|
||||
assert result_v2_minimal[0].age is None
|
||||
|
||||
# Test mixed v1 and v2 with partial optional fields
|
||||
parser_mixed = PydanticToolsParser(tools=[ProductV1, UserV2])
|
||||
message_mixed = AIMessage(
|
||||
content="",
|
||||
tool_calls=[
|
||||
{
|
||||
"id": "call_product",
|
||||
"name": "ProductV1",
|
||||
"args": {"name": "Keyboard", "price": 79.99, "stock": 100},
|
||||
},
|
||||
{
|
||||
"id": "call_user",
|
||||
"name": "UserV2",
|
||||
"args": {
|
||||
"username": "alice",
|
||||
"email": "alice@example.com",
|
||||
"age": 35,
|
||||
},
|
||||
},
|
||||
],
|
||||
)
|
||||
generation_mixed = ChatGeneration(message=message_mixed)
|
||||
result_mixed = parser_mixed.parse_result([generation_mixed])
|
||||
|
||||
assert len(result_mixed) == 2
|
||||
assert isinstance(result_mixed[0], ProductV1)
|
||||
assert result_mixed[0].name == "Keyboard" # type: ignore[attr-defined,unused-ignore]
|
||||
assert result_mixed[0].description is None # type: ignore[attr-defined,unused-ignore]
|
||||
assert result_mixed[0].stock == 100 # type: ignore[attr-defined,unused-ignore]
|
||||
assert isinstance(result_mixed[1], UserV2)
|
||||
assert result_mixed[1].username == "alice"
|
||||
assert result_mixed[1].bio is None
|
||||
assert result_mixed[1].age == 35
|
||||
|
||||
@@ -682,7 +682,7 @@
|
||||
|
||||
May also hold extra provider-specific keys.
|
||||
|
||||
!!! version-added "Added in version 0.3.9"
|
||||
!!! version-added "Added in `langchain-core` 0.3.9"
|
||||
''',
|
||||
'properties': dict({
|
||||
'audio': dict({
|
||||
@@ -800,7 +800,7 @@
|
||||
|
||||
May also hold extra provider-specific keys.
|
||||
|
||||
!!! version-added "Added in version 0.3.9"
|
||||
!!! version-added "Added in `langchain-core` 0.3.9"
|
||||
''',
|
||||
'properties': dict({
|
||||
'audio': dict({
|
||||
@@ -1319,10 +1319,12 @@
|
||||
}
|
||||
```
|
||||
|
||||
!!! warning "Behavior changed in 0.3.9"
|
||||
!!! warning "Behavior changed in `langchain-core` 0.3.9"
|
||||
|
||||
Added `input_token_details` and `output_token_details`.
|
||||
|
||||
!!! note "LangSmith SDK"
|
||||
|
||||
The LangSmith SDK also has a `UsageMetadata` class. While the two share fields,
|
||||
LangSmith's `UsageMetadata` has additional fields to capture cost information
|
||||
used by the LangSmith platform.
|
||||
@@ -2096,7 +2098,7 @@
|
||||
|
||||
May also hold extra provider-specific keys.
|
||||
|
||||
!!! version-added "Added in version 0.3.9"
|
||||
!!! version-added "Added in `langchain-core` 0.3.9"
|
||||
''',
|
||||
'properties': dict({
|
||||
'audio': dict({
|
||||
@@ -2214,7 +2216,7 @@
|
||||
|
||||
May also hold extra provider-specific keys.
|
||||
|
||||
!!! version-added "Added in version 0.3.9"
|
||||
!!! version-added "Added in `langchain-core` 0.3.9"
|
||||
''',
|
||||
'properties': dict({
|
||||
'audio': dict({
|
||||
@@ -2733,10 +2735,12 @@
|
||||
}
|
||||
```
|
||||
|
||||
!!! warning "Behavior changed in 0.3.9"
|
||||
!!! warning "Behavior changed in `langchain-core` 0.3.9"
|
||||
|
||||
Added `input_token_details` and `output_token_details`.
|
||||
|
||||
!!! note "LangSmith SDK"
|
||||
|
||||
The LangSmith SDK also has a `UsageMetadata` class. While the two share fields,
|
||||
LangSmith's `UsageMetadata` has additional fields to capture cost information
|
||||
used by the LangSmith platform.
|
||||
|
||||
@@ -1193,3 +1193,511 @@ def test_dict_message_prompt_template_errors_on_jinja2() -> None:
|
||||
_ = ChatPromptTemplate.from_messages(
|
||||
[("human", [prompt])], template_format="jinja2"
|
||||
)
|
||||
|
||||
|
||||
def test_rendering_prompt_with_conditionals_no_empty_text_blocks() -> None:
|
||||
manifest = {
|
||||
"lc": 1,
|
||||
"type": "constructor",
|
||||
"id": ["langchain_core", "prompts", "chat", "ChatPromptTemplate"],
|
||||
"kwargs": {
|
||||
"messages": [
|
||||
{
|
||||
"lc": 1,
|
||||
"type": "constructor",
|
||||
"id": [
|
||||
"langchain_core",
|
||||
"prompts",
|
||||
"chat",
|
||||
"SystemMessagePromptTemplate",
|
||||
],
|
||||
"kwargs": {
|
||||
"prompt": {
|
||||
"lc": 1,
|
||||
"type": "constructor",
|
||||
"id": [
|
||||
"langchain_core",
|
||||
"prompts",
|
||||
"prompt",
|
||||
"PromptTemplate",
|
||||
],
|
||||
"kwargs": {
|
||||
"input_variables": [],
|
||||
"template_format": "mustache",
|
||||
"template": "Always echo back whatever I send you.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"lc": 1,
|
||||
"type": "constructor",
|
||||
"id": [
|
||||
"langchain_core",
|
||||
"prompts",
|
||||
"chat",
|
||||
"HumanMessagePromptTemplate",
|
||||
],
|
||||
"kwargs": {
|
||||
"prompt": [
|
||||
{
|
||||
"lc": 1,
|
||||
"type": "constructor",
|
||||
"id": [
|
||||
"langchain_core",
|
||||
"prompts",
|
||||
"prompt",
|
||||
"PromptTemplate",
|
||||
],
|
||||
"kwargs": {
|
||||
"input_variables": [],
|
||||
"template_format": "mustache",
|
||||
"template": "Here is the teacher's prompt:",
|
||||
"additional_content_fields": {
|
||||
"text": "Here is the teacher's prompt:",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"lc": 1,
|
||||
"type": "constructor",
|
||||
"id": [
|
||||
"langchain_core",
|
||||
"prompts",
|
||||
"prompt",
|
||||
"PromptTemplate",
|
||||
],
|
||||
"kwargs": {
|
||||
"input_variables": ["promptDescription"],
|
||||
"template_format": "mustache",
|
||||
"template": '"{{promptDescription}}"\n',
|
||||
"additional_content_fields": {
|
||||
"text": '"{{promptDescription}}"\n',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"lc": 1,
|
||||
"type": "constructor",
|
||||
"id": [
|
||||
"langchain_core",
|
||||
"prompts",
|
||||
"prompt",
|
||||
"PromptTemplate",
|
||||
],
|
||||
"kwargs": {
|
||||
"input_variables": [],
|
||||
"template_format": "mustache",
|
||||
"template": "Here is the expected answer or success criteria given by the teacher:", # noqa: E501
|
||||
"additional_content_fields": {
|
||||
"text": "Here is the expected answer or success criteria given by the teacher:", # noqa: E501
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"lc": 1,
|
||||
"type": "constructor",
|
||||
"id": [
|
||||
"langchain_core",
|
||||
"prompts",
|
||||
"prompt",
|
||||
"PromptTemplate",
|
||||
],
|
||||
"kwargs": {
|
||||
"input_variables": ["expectedResponse"],
|
||||
"template_format": "mustache",
|
||||
"template": '"{{expectedResponse}}"\n',
|
||||
"additional_content_fields": {
|
||||
"text": '"{{expectedResponse}}"\n',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"lc": 1,
|
||||
"type": "constructor",
|
||||
"id": [
|
||||
"langchain_core",
|
||||
"prompts",
|
||||
"prompt",
|
||||
"PromptTemplate",
|
||||
],
|
||||
"kwargs": {
|
||||
"input_variables": [],
|
||||
"template_format": "mustache",
|
||||
"template": "Note: This may be just one example of many possible correct ways for the student to respond.\n", # noqa: E501
|
||||
"additional_content_fields": {
|
||||
"text": "Note: This may be just one example of many possible correct ways for the student to respond.\n", # noqa: E501
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"lc": 1,
|
||||
"type": "constructor",
|
||||
"id": [
|
||||
"langchain_core",
|
||||
"prompts",
|
||||
"prompt",
|
||||
"PromptTemplate",
|
||||
],
|
||||
"kwargs": {
|
||||
"input_variables": [],
|
||||
"template_format": "mustache",
|
||||
"template": "For your evaluation of the student's response:\n", # noqa: E501
|
||||
"additional_content_fields": {
|
||||
"text": "For your evaluation of the student's response:\n", # noqa: E501
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"lc": 1,
|
||||
"type": "constructor",
|
||||
"id": [
|
||||
"langchain_core",
|
||||
"prompts",
|
||||
"prompt",
|
||||
"PromptTemplate",
|
||||
],
|
||||
"kwargs": {
|
||||
"input_variables": [],
|
||||
"template_format": "mustache",
|
||||
"template": "Here is a transcript of the student's explanation:", # noqa: E501
|
||||
"additional_content_fields": {
|
||||
"text": "Here is a transcript of the student's explanation:", # noqa: E501
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"lc": 1,
|
||||
"type": "constructor",
|
||||
"id": [
|
||||
"langchain_core",
|
||||
"prompts",
|
||||
"prompt",
|
||||
"PromptTemplate",
|
||||
],
|
||||
"kwargs": {
|
||||
"input_variables": ["responseTranscript"],
|
||||
"template_format": "mustache",
|
||||
"template": '"{{responseTranscript}}"\n',
|
||||
"additional_content_fields": {
|
||||
"text": '"{{responseTranscript}}"\n',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"lc": 1,
|
||||
"type": "constructor",
|
||||
"id": [
|
||||
"langchain_core",
|
||||
"prompts",
|
||||
"prompt",
|
||||
"PromptTemplate",
|
||||
],
|
||||
"kwargs": {
|
||||
"input_variables": ["readingFluencyAnalysis"],
|
||||
"template_format": "mustache",
|
||||
"template": "{{#readingFluencyAnalysis}} For this task, the student's reading pronunciation and fluency were important. Here is analysis of the student's oral response: \"{{readingFluencyAnalysis}}\" {{/readingFluencyAnalysis}}", # noqa: E501
|
||||
"additional_content_fields": {
|
||||
"text": "{{#readingFluencyAnalysis}} For this task, the student's reading pronunciation and fluency were important. Here is analysis of the student's oral response: \"{{readingFluencyAnalysis}}\" {{/readingFluencyAnalysis}}", # noqa: E501
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"lc": 1,
|
||||
"type": "constructor",
|
||||
"id": [
|
||||
"langchain_core",
|
||||
"prompts",
|
||||
"prompt",
|
||||
"PromptTemplate",
|
||||
],
|
||||
"kwargs": {
|
||||
"input_variables": ["readingFluencyAnalysis"],
|
||||
"template_format": "mustache",
|
||||
"template": "{{#readingFluencyAnalysis}}Root analysis of the student's response (step 3) in this oral analysis rather than inconsistencies in the transcript.{{/readingFluencyAnalysis}}", # noqa: E501
|
||||
"additional_content_fields": {
|
||||
"text": "{{#readingFluencyAnalysis}}Root analysis of the student's response (step 3) in this oral analysis rather than inconsistencies in the transcript.{{/readingFluencyAnalysis}}", # noqa: E501
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"lc": 1,
|
||||
"type": "constructor",
|
||||
"id": [
|
||||
"langchain_core",
|
||||
"prompts",
|
||||
"prompt",
|
||||
"PromptTemplate",
|
||||
],
|
||||
"kwargs": {
|
||||
"input_variables": ["readingFluencyAnalysis"],
|
||||
"template_format": "mustache",
|
||||
"template": "{{#readingFluencyAnalysis}}Remember this is a student, so we care about general fluency - not voice acting. {{/readingFluencyAnalysis}}\n", # noqa: E501
|
||||
"additional_content_fields": {
|
||||
"text": "{{#readingFluencyAnalysis}}Remember this is a student, so we care about general fluency - not voice acting. {{/readingFluencyAnalysis}}\n", # noqa: E501
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"lc": 1,
|
||||
"type": "constructor",
|
||||
"id": [
|
||||
"langchain_core",
|
||||
"prompts",
|
||||
"prompt",
|
||||
"PromptTemplate",
|
||||
],
|
||||
"kwargs": {
|
||||
"input_variables": ["multipleChoiceAnalysis"],
|
||||
"template_format": "mustache",
|
||||
"template": "{{#multipleChoiceAnalysis}}Here is an analysis of the student's multiple choice response: {{multipleChoiceAnalysis}}{{/multipleChoiceAnalysis}}\n", # noqa: E501
|
||||
"additional_content_fields": {
|
||||
"text": "{{#multipleChoiceAnalysis}}Here is an analysis of the student's multiple choice response: {{multipleChoiceAnalysis}}{{/multipleChoiceAnalysis}}\n", # noqa: E501
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"lc": 1,
|
||||
"type": "constructor",
|
||||
"id": [
|
||||
"langchain_core",
|
||||
"prompts",
|
||||
"prompt",
|
||||
"PromptTemplate",
|
||||
],
|
||||
"kwargs": {
|
||||
"input_variables": [],
|
||||
"template_format": "mustache",
|
||||
"template": "Here is the student's whiteboard:\n",
|
||||
"additional_content_fields": {
|
||||
"text": "Here is the student's whiteboard:\n",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"lc": 1,
|
||||
"type": "constructor",
|
||||
"id": [
|
||||
"langchain_core",
|
||||
"prompts",
|
||||
"image",
|
||||
"ImagePromptTemplate",
|
||||
],
|
||||
"kwargs": {
|
||||
"template": {
|
||||
"url": "{{whiteboard}}",
|
||||
},
|
||||
"input_variables": ["whiteboard"],
|
||||
"template_format": "mustache",
|
||||
"additional_content_fields": {
|
||||
"image_url": {
|
||||
"url": "{{whiteboard}}",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
"additional_options": {},
|
||||
},
|
||||
},
|
||||
],
|
||||
"input_variables": [
|
||||
"promptDescription",
|
||||
"expectedResponse",
|
||||
"responseTranscript",
|
||||
"readingFluencyAnalysis",
|
||||
"readingFluencyAnalysis",
|
||||
"readingFluencyAnalysis",
|
||||
"multipleChoiceAnalysis",
|
||||
"whiteboard",
|
||||
],
|
||||
"template_format": "mustache",
|
||||
"metadata": {
|
||||
"lc_hub_owner": "jacob",
|
||||
"lc_hub_repo": "mustache-conditionals",
|
||||
"lc_hub_commit_hash": "836ad82d512409ea6024fb760b76a27ba58fc68b1179656c0ba2789778686d46", # noqa: E501
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# Load the ChatPromptTemplate from the manifest
|
||||
template = load(manifest)
|
||||
|
||||
# Format with conditional data - rules is empty, so mustache conditionals
|
||||
# should not render
|
||||
result = template.invoke(
|
||||
{
|
||||
"promptDescription": "What is the capital of the USA?",
|
||||
"expectedResponse": "Washington, D.C.",
|
||||
"responseTranscript": "Washington, D.C.",
|
||||
"readingFluencyAnalysis": None,
|
||||
"multipleChoiceAnalysis": "testing2",
|
||||
"whiteboard": "https://foo.com/bar.png",
|
||||
}
|
||||
)
|
||||
content = result.messages[1].content
|
||||
assert isinstance(content, list)
|
||||
assert not [
|
||||
block for block in content if block["type"] == "text" and block["text"] == ""
|
||||
]
|
||||
|
||||
|
||||
def test_fstring_rejects_invalid_identifier_variable_names() -> None:
|
||||
"""Test that f-string templates block attribute access, indexing.
|
||||
|
||||
This validation prevents template injection attacks by blocking:
|
||||
- Attribute access like {msg.__class__}
|
||||
- Indexing like {msg[0]}
|
||||
- All-digit variable names like {0} or {100} (interpreted as positional args)
|
||||
|
||||
While allowing any other field names that Python's Formatter accepts.
|
||||
"""
|
||||
# Test that attribute access and indexing are blocked (security issue)
|
||||
invalid_templates = [
|
||||
"{msg.__class__}", # Attribute access with dunder
|
||||
"{msg.__class__.__name__}", # Multiple dunders
|
||||
"{msg.content}", # Attribute access
|
||||
"{msg[0]}", # Item access
|
||||
"{0}", # All-digit variable name (positional argument)
|
||||
"{100}", # All-digit variable name (positional argument)
|
||||
"{42}", # All-digit variable name (positional argument)
|
||||
]
|
||||
|
||||
for template_str in invalid_templates:
|
||||
with pytest.raises(ValueError, match="Invalid variable name") as exc_info:
|
||||
ChatPromptTemplate.from_messages(
|
||||
[("human", template_str)],
|
||||
template_format="f-string",
|
||||
)
|
||||
|
||||
error_msg = str(exc_info.value)
|
||||
assert "Invalid variable name" in error_msg
|
||||
# Check for any of the expected error message parts
|
||||
assert (
|
||||
"attribute access" in error_msg
|
||||
or "indexing" in error_msg
|
||||
or "positional arguments" in error_msg
|
||||
)
|
||||
|
||||
# Valid templates - Python's Formatter accepts non-identifier field names
|
||||
valid_templates = [
|
||||
(
|
||||
"Hello {name} and {user_id}",
|
||||
{"name": "Alice", "user_id": "123"},
|
||||
"Hello Alice and 123",
|
||||
),
|
||||
("User: {user-name}", {"user-name": "Bob"}, "User: Bob"), # Hyphen allowed
|
||||
(
|
||||
"Value: {2fast}",
|
||||
{"2fast": "Charlie"},
|
||||
"Value: Charlie",
|
||||
), # Starts with digit allowed
|
||||
("Data: {my var}", {"my var": "Dave"}, "Data: Dave"), # Space allowed
|
||||
]
|
||||
|
||||
for template_str, kwargs, expected in valid_templates:
|
||||
template = ChatPromptTemplate.from_messages(
|
||||
[("human", template_str)],
|
||||
template_format="f-string",
|
||||
)
|
||||
result = template.invoke(kwargs)
|
||||
assert result.messages[0].content == expected # type: ignore[attr-defined]
|
||||
|
||||
|
||||
def test_mustache_template_attribute_access_vulnerability() -> None:
|
||||
"""Test that Mustache template injection is blocked.
|
||||
|
||||
Verify the fix for security vulnerability GHSA-6qv9-48xg-fc7f
|
||||
|
||||
Previously, Mustache used getattr() as a fallback, allowing access to
|
||||
dangerous attributes like __class__, __globals__, etc.
|
||||
|
||||
The fix adds isinstance checks that reject non-dict/list types.
|
||||
When templates try to traverse Python objects, they get empty string
|
||||
per Mustache spec (better than the previous behavior of exposing internals).
|
||||
"""
|
||||
msg = HumanMessage("howdy")
|
||||
|
||||
# Template tries to access attributes on a Python object
|
||||
prompt = ChatPromptTemplate.from_messages(
|
||||
[("human", "{{question.__class__.__name__}}")],
|
||||
template_format="mustache",
|
||||
)
|
||||
|
||||
# After the fix: returns empty string (attack blocked!)
|
||||
# Previously would return "HumanMessage" via getattr()
|
||||
result = prompt.invoke({"question": msg})
|
||||
assert result.messages[0].content == "" # type: ignore[attr-defined]
|
||||
|
||||
# Mustache still works correctly with actual dicts
|
||||
prompt_dict = ChatPromptTemplate.from_messages(
|
||||
[("human", "{{person.name}}")],
|
||||
template_format="mustache",
|
||||
)
|
||||
result_dict = prompt_dict.invoke({"person": {"name": "Alice"}})
|
||||
assert result_dict.messages[0].content == "Alice" # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@pytest.mark.requires("jinja2")
|
||||
def test_jinja2_template_attribute_access_is_blocked() -> None:
|
||||
"""Test that Jinja2 SandboxedEnvironment blocks dangerous attribute access.
|
||||
|
||||
This test verifies that Jinja2's sandbox successfully blocks access to
|
||||
dangerous dunder attributes like __class__, unlike Mustache.
|
||||
|
||||
GOOD: Jinja2 SandboxedEnvironment raises SecurityError when attempting
|
||||
to access __class__, __globals__, etc. This is expected behavior.
|
||||
"""
|
||||
msg = HumanMessage("howdy")
|
||||
|
||||
# Create a Jinja2 template that attempts to access __class__.__name__
|
||||
prompt = ChatPromptTemplate.from_messages(
|
||||
[("human", "{{question.__class__.__name__}}")],
|
||||
template_format="jinja2",
|
||||
)
|
||||
|
||||
# Jinja2 sandbox should block this with SecurityError
|
||||
with pytest.raises(Exception, match="attribute") as exc_info:
|
||||
prompt.invoke(
|
||||
{"question": msg, "question.__class__.__name__": "safe_placeholder"}
|
||||
)
|
||||
|
||||
# Verify it's a SecurityError from Jinja2 blocking __class__ access
|
||||
error_msg = str(exc_info.value)
|
||||
assert (
|
||||
"SecurityError" in str(type(exc_info.value))
|
||||
or "access to attribute '__class__'" in error_msg
|
||||
), f"Expected SecurityError blocking __class__, got: {error_msg}"
|
||||
|
||||
|
||||
@pytest.mark.requires("jinja2")
|
||||
def test_jinja2_blocks_all_attribute_access() -> None:
|
||||
"""Test that Jinja2 now blocks ALL attribute/method access for security.
|
||||
|
||||
After the fix, Jinja2 uses _RestrictedSandboxedEnvironment which blocks
|
||||
ALL attribute access, not just dunder attributes. This prevents the
|
||||
parse_raw() vulnerability.
|
||||
"""
|
||||
msg = HumanMessage("test content")
|
||||
|
||||
# Test 1: Simple variable access should still work
|
||||
prompt_simple = ChatPromptTemplate.from_messages(
|
||||
[("human", "Message: {{message}}")],
|
||||
template_format="jinja2",
|
||||
)
|
||||
result = prompt_simple.invoke({"message": "hello world"})
|
||||
assert "hello world" in result.messages[0].content # type: ignore[attr-defined]
|
||||
|
||||
# Test 2: Attribute access should now be blocked (including safe attributes)
|
||||
prompt_attr = ChatPromptTemplate.from_messages(
|
||||
[("human", "Content: {{msg.content}}")],
|
||||
template_format="jinja2",
|
||||
)
|
||||
with pytest.raises(Exception, match="attribute") as exc_info:
|
||||
prompt_attr.invoke({"msg": msg})
|
||||
|
||||
error_msg = str(exc_info.value)
|
||||
assert (
|
||||
"SecurityError" in str(type(exc_info.value))
|
||||
or "Access to attributes is not allowed" in error_msg
|
||||
), f"Expected SecurityError blocking attribute access, got: {error_msg}"
|
||||
|
||||
@@ -125,7 +125,9 @@ def test_structured_prompt_kwargs() -> None:
|
||||
|
||||
def test_structured_prompt_template_format() -> None:
|
||||
prompt = StructuredPrompt(
|
||||
[("human", "hi {{person.name}}")], schema={}, template_format="mustache"
|
||||
[("human", "hi {{person.name}}")],
|
||||
schema={"type": "object", "properties": {}, "title": "foo"},
|
||||
template_format="mustache",
|
||||
)
|
||||
assert prompt.messages[0].prompt.template_format == "mustache" # type: ignore[union-attr, union-attr]
|
||||
assert prompt.input_variables == ["person"]
|
||||
@@ -136,4 +138,8 @@ def test_structured_prompt_template_format() -> None:
|
||||
|
||||
def test_structured_prompt_template_empty_vars() -> None:
|
||||
with pytest.raises(ChevronError, match="empty tag"):
|
||||
StructuredPrompt([("human", "hi {{}}")], schema={}, template_format="mustache")
|
||||
StructuredPrompt(
|
||||
[("human", "hi {{}}")],
|
||||
schema={"type": "object", "properties": {}, "title": "foo"},
|
||||
template_format="mustache",
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user