Skip to content

Commit e02eed5

Browse files
ccurmemdrxyeyurtsevnfcampos
authored
feat: standard outputs (#32287)
Co-authored-by: Mason Daugherty <[email protected]> Co-authored-by: Eugene Yurtsev <[email protected]> Co-authored-by: Mason Daugherty <[email protected]> Co-authored-by: Nuno Campos <[email protected]>
1 parent 5414527 commit e02eed5

File tree

91 files changed

+16390
-1788
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

91 files changed

+16390
-1788
lines changed

.github/workflows/codspeed.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ jobs:
2020
codspeed:
2121
name: 'Benchmark'
2222
runs-on: ubuntu-latest
23+
if: ${{ !contains(github.event.pull_request.labels.*.name, 'codspeed-ignore') }}
2324
strategy:
2425
matrix:
2526
include:

docs/api_reference/create_api_rst.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,11 @@ def _load_package_modules(
217217
# Get the full namespace of the module
218218
namespace = str(relative_module_name).replace(".py", "").replace("/", ".")
219219
# Keep only the top level namespace
220-
top_namespace = namespace.split(".")[0]
220+
# (but make special exception for content_blocks and messages.v1)
221+
if namespace == "messages.content_blocks" or namespace == "messages.v1":
222+
top_namespace = namespace # Keep full namespace for content_blocks
223+
else:
224+
top_namespace = namespace.split(".")[0]
221225

222226
try:
223227
# If submodule is present, we need to construct the paths in a slightly
@@ -283,7 +287,7 @@ def _construct_doc(
283287
.. toctree::
284288
:hidden:
285289
:maxdepth: 2
286-
290+
287291
"""
288292
index_autosummary = """
289293
"""
@@ -365,9 +369,9 @@ def _construct_doc(
365369

366370
module_doc += f"""\
367371
:template: {template}
368-
372+
369373
{class_["qualified_name"]}
370-
374+
371375
"""
372376
index_autosummary += f"""
373377
{class_["qualified_name"]}
@@ -550,8 +554,8 @@ def _build_index(dirs: List[str]) -> None:
550554
integrations = sorted(dir_ for dir_ in dirs if dir_ not in main_)
551555
doc = """# LangChain Python API Reference
552556
553-
Welcome to the LangChain Python API reference. This is a reference for all
554-
`langchain-x` packages.
557+
Welcome to the LangChain Python API reference. This is a reference for all
558+
`langchain-x` packages.
555559
556560
For user guides see [https://python.langchain.com](https://python.langchain.com).
557561

docs/docs/contributing/how_to/testing.mdx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,47 @@ start "" htmlcov/index.html || open htmlcov/index.html
124124

125125
```
126126

127+
## Snapshot Testing
128+
129+
Some tests use [syrupy](https://github.com/tophat/syrupy) for snapshot testing, which captures the output of functions and compares them to stored snapshots. This is particularly useful for testing JSON schema generation and other structured outputs.
130+
131+
### Updating Snapshots
132+
133+
To update snapshots when the expected output has legitimately changed:
134+
135+
```bash
136+
uv run --group test pytest path/to/test.py --snapshot-update
137+
```
138+
139+
### Pydantic Version Compatibility Issues
140+
141+
Pydantic generates different JSON schemas across versions, which can cause snapshot test failures in CI when tests run with different Pydantic versions than what was used to generate the snapshots.
142+
143+
**Symptoms:**
144+
- CI fails with snapshot mismatches showing differences like missing or extra fields.
145+
- Tests pass locally but fail in CI with different Pydantic versions
146+
147+
**Solution:**
148+
Locally update snapshots using the same Pydantic version that CI uses:
149+
150+
1. **Identify the failing Pydantic version** from CI logs (e.g., `2.7.0`, `2.8.0`, `2.9.0`)
151+
152+
2. **Update snapshots with that version:**
153+
```bash
154+
uv run --with "pydantic==2.9.0" --group test pytest tests/unit_tests/path/to/test.py::test_name --snapshot-update
155+
```
156+
157+
3. **Verify compatibility across supported versions:**
158+
```bash
159+
# Test with the version you used to update
160+
uv run --with "pydantic==2.9.0" --group test pytest tests/unit_tests/path/to/test.py::test_name
161+
162+
# Test with other supported versions
163+
uv run --with "pydantic==2.8.0" --group test pytest tests/unit_tests/path/to/test.py::test_name
164+
```
165+
166+
**Note:** Some tests use `@pytest.mark.skipif` decorators to only run with specific Pydantic version ranges (e.g., `PYDANTIC_VERSION_AT_LEAST_210`). Make sure to understand these constraints when updating snapshots.
167+
127168
## Coverage
128169

129170
Code coverage (i.e. the amount of code that is covered by unit tests) helps identify areas of the code that are potentially more or less brittle.

libs/core/langchain_core/callbacks/base.py

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
from typing_extensions import Self
99

10+
from langchain_core.v1.messages import AIMessage, AIMessageChunk, MessageV1
11+
1012
if TYPE_CHECKING:
1113
from collections.abc import Sequence
1214
from uuid import UUID
@@ -66,7 +68,9 @@ def on_llm_new_token(
6668
self,
6769
token: str,
6870
*,
69-
chunk: Optional[Union[GenerationChunk, ChatGenerationChunk]] = None,
71+
chunk: Optional[
72+
Union[GenerationChunk, ChatGenerationChunk, AIMessageChunk]
73+
] = None,
7074
run_id: UUID,
7175
parent_run_id: Optional[UUID] = None,
7276
**kwargs: Any,
@@ -75,16 +79,16 @@ def on_llm_new_token(
7579
7680
Args:
7781
token (str): The new token.
78-
chunk (GenerationChunk | ChatGenerationChunk): The new generated chunk,
79-
containing content and other information.
82+
chunk (GenerationChunk | ChatGenerationChunk | AIMessageChunk): The new
83+
generated chunk, containing content and other information.
8084
run_id (UUID): The run ID. This is the ID of the current run.
8185
parent_run_id (UUID): The parent run ID. This is the ID of the parent run.
8286
kwargs (Any): Additional keyword arguments.
8387
"""
8488

8589
def on_llm_end(
8690
self,
87-
response: LLMResult,
91+
response: Union[LLMResult, AIMessage],
8892
*,
8993
run_id: UUID,
9094
parent_run_id: Optional[UUID] = None,
@@ -93,7 +97,7 @@ def on_llm_end(
9397
"""Run when LLM ends running.
9498
9599
Args:
96-
response (LLMResult): The response which was generated.
100+
response (LLMResult | AIMessage): The response which was generated.
97101
run_id (UUID): The run ID. This is the ID of the current run.
98102
parent_run_id (UUID): The parent run ID. This is the ID of the parent run.
99103
kwargs (Any): Additional keyword arguments.
@@ -261,7 +265,7 @@ def on_llm_start(
261265
def on_chat_model_start(
262266
self,
263267
serialized: dict[str, Any],
264-
messages: list[list[BaseMessage]],
268+
messages: Union[list[list[BaseMessage]], list[MessageV1]],
265269
*,
266270
run_id: UUID,
267271
parent_run_id: Optional[UUID] = None,
@@ -439,6 +443,9 @@ class BaseCallbackHandler(
439443
run_inline: bool = False
440444
"""Whether to run the callback inline."""
441445

446+
accepts_new_messages: bool = False
447+
"""Whether the callback accepts new message format."""
448+
442449
@property
443450
def ignore_llm(self) -> bool:
444451
"""Whether to ignore LLM callbacks."""
@@ -509,7 +516,7 @@ async def on_llm_start(
509516
async def on_chat_model_start(
510517
self,
511518
serialized: dict[str, Any],
512-
messages: list[list[BaseMessage]],
519+
messages: Union[list[list[BaseMessage]], list[MessageV1]],
513520
*,
514521
run_id: UUID,
515522
parent_run_id: Optional[UUID] = None,
@@ -540,7 +547,9 @@ async def on_llm_new_token(
540547
self,
541548
token: str,
542549
*,
543-
chunk: Optional[Union[GenerationChunk, ChatGenerationChunk]] = None,
550+
chunk: Optional[
551+
Union[GenerationChunk, ChatGenerationChunk, AIMessageChunk]
552+
] = None,
544553
run_id: UUID,
545554
parent_run_id: Optional[UUID] = None,
546555
tags: Optional[list[str]] = None,
@@ -550,8 +559,8 @@ async def on_llm_new_token(
550559
551560
Args:
552561
token (str): The new token.
553-
chunk (GenerationChunk | ChatGenerationChunk): The new generated chunk,
554-
containing content and other information.
562+
chunk (GenerationChunk | ChatGenerationChunk | AIMessageChunk): The new
563+
generated chunk, containing content and other information.
555564
run_id (UUID): The run ID. This is the ID of the current run.
556565
parent_run_id (UUID): The parent run ID. This is the ID of the parent run.
557566
tags (Optional[list[str]]): The tags.
@@ -560,7 +569,7 @@ async def on_llm_new_token(
560569

561570
async def on_llm_end(
562571
self,
563-
response: LLMResult,
572+
response: Union[LLMResult, AIMessage],
564573
*,
565574
run_id: UUID,
566575
parent_run_id: Optional[UUID] = None,
@@ -570,7 +579,7 @@ async def on_llm_end(
570579
"""Run when LLM ends running.
571580
572581
Args:
573-
response (LLMResult): The response which was generated.
582+
response (LLMResult | AIMessage): The response which was generated.
574583
run_id (UUID): The run ID. This is the ID of the current run.
575584
parent_run_id (UUID): The parent run ID. This is the ID of the parent run.
576585
tags (Optional[list[str]]): The tags.
@@ -594,8 +603,8 @@ async def on_llm_error(
594603
parent_run_id: The parent run ID. This is the ID of the parent run.
595604
tags: The tags.
596605
kwargs (Any): Additional keyword arguments.
597-
- response (LLMResult): The response which was generated before
598-
the error occurred.
606+
- response (LLMResult | AIMessage): The response which was generated
607+
before the error occurred.
599608
"""
600609

601610
async def on_chain_start(

0 commit comments

Comments
 (0)