Skip to content

Commit 5b165ef

Browse files
core(fix): revert set_text optimization (#31555)
Revert serialization regression introduced in #31238 Fixes #31486
1 parent e455fab commit 5b165ef

File tree

3 files changed

+35
-43
lines changed

3 files changed

+35
-43
lines changed

libs/core/langchain_core/outputs/chat_generation.py

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
from typing import Literal, Union
66

7-
from pydantic import computed_field
7+
from pydantic import model_validator
8+
from typing_extensions import Self
89

910
from langchain_core.messages import BaseMessage, BaseMessageChunk
1011
from langchain_core.outputs.generation import Generation
@@ -25,30 +26,44 @@ class ChatGeneration(Generation):
2526
via callbacks).
2627
"""
2728

29+
text: str = ""
30+
"""*SHOULD NOT BE SET DIRECTLY* The text contents of the output message."""
2831
message: BaseMessage
2932
"""The message output by the chat model."""
30-
33+
# Override type to be ChatGeneration, ignore mypy error as this is intentional
3134
type: Literal["ChatGeneration"] = "ChatGeneration" # type: ignore[assignment]
3235
"""Type is used exclusively for serialization purposes."""
3336

34-
@computed_field # type: ignore[prop-decorator]
35-
@property
36-
def text(self) -> str:
37-
"""Set the text attribute to be the contents of the message."""
38-
text_ = ""
37+
@model_validator(mode="after")
38+
def set_text(self) -> Self:
39+
"""Set the text attribute to be the contents of the message.
40+
41+
Args:
42+
values: The values of the object.
43+
44+
Returns:
45+
The values of the object with the text attribute set.
46+
47+
Raises:
48+
ValueError: If the message is not a string or a list.
49+
"""
50+
text = ""
3951
if isinstance(self.message.content, str):
40-
text_ = self.message.content
52+
text = self.message.content
4153
# Assumes text in content blocks in OpenAI format.
4254
# Uses first text block.
4355
elif isinstance(self.message.content, list):
4456
for block in self.message.content:
4557
if isinstance(block, str):
46-
text_ = block
58+
text = block
4759
break
4860
if isinstance(block, dict) and "text" in block:
49-
text_ = block["text"]
61+
text = block["text"]
5062
break
51-
return text_
63+
else:
64+
pass
65+
self.text = text
66+
return self
5267

5368

5469
class ChatGenerationChunk(ChatGeneration):
@@ -59,7 +74,7 @@ class ChatGenerationChunk(ChatGeneration):
5974

6075
message: BaseMessageChunk
6176
"""The message chunk output by the chat model."""
62-
77+
# Override type to be ChatGeneration, ignore mypy error as this is intentional
6378
type: Literal["ChatGenerationChunk"] = "ChatGenerationChunk" # type: ignore[assignment]
6479
"""Type is used exclusively for serialization purposes."""
6580

libs/core/langchain_core/outputs/generation.py

Lines changed: 2 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44

55
from typing import Any, Literal, Optional
66

7-
from pydantic import computed_field
8-
97
from langchain_core.load import Serializable
108
from langchain_core.utils._merge import merge_dicts
119

@@ -26,30 +24,14 @@ class Generation(Serializable):
2624
for more information.
2725
"""
2826

29-
def __init__(
30-
self,
31-
text: str = "",
32-
generation_info: Optional[dict[str, Any]] = None,
33-
**kwargs: Any,
34-
):
35-
"""Initialize a Generation."""
36-
super().__init__(generation_info=generation_info, **kwargs)
37-
self._text = text
38-
39-
# workaround for ChatGeneration so that we can use a computed field to populate
40-
# the text field from the message content (parent class needs to have a property)
41-
@computed_field # type: ignore[prop-decorator]
42-
@property
43-
def text(self) -> str:
44-
"""The text contents of the output."""
45-
return self._text
27+
text: str
28+
"""Generated text output."""
4629

4730
generation_info: Optional[dict[str, Any]] = None
4831
"""Raw response from the provider.
4932
5033
May include things like the reason for finishing or token log probabilities.
5134
"""
52-
5335
type: Literal["Generation"] = "Generation"
5436
"""Type is used exclusively for serialization purposes.
5537
Set to "Generation" for this class."""
@@ -71,16 +53,6 @@ def get_lc_namespace(cls) -> list[str]:
7153
class GenerationChunk(Generation):
7254
"""Generation chunk, which can be concatenated with other Generation chunks."""
7355

74-
def __init__(
75-
self,
76-
text: str = "",
77-
generation_info: Optional[dict[str, Any]] = None,
78-
**kwargs: Any,
79-
):
80-
"""Initialize a GenerationChunk."""
81-
super().__init__(text=text, generation_info=generation_info, **kwargs)
82-
self._text = text
83-
8456
def __add__(self, other: GenerationChunk) -> GenerationChunk:
8557
"""Concatenate two GenerationChunks."""
8658
if isinstance(other, GenerationChunk):

libs/core/tests/unit_tests/load/test_serializable.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from langchain_core.load import Serializable, dumpd, load
44
from langchain_core.load.serializable import _is_field_useful
55
from langchain_core.messages import AIMessage
6-
from langchain_core.outputs import ChatGeneration
6+
from langchain_core.outputs import ChatGeneration, Generation
77

88

99
class NonBoolObj:
@@ -223,3 +223,8 @@ class MyModel(BaseModel):
223223
assert isinstance(deser, ChatGeneration)
224224
assert deser.message.content
225225
assert deser.message.additional_kwargs["parsed"] == my_model.model_dump()
226+
227+
228+
def test_serialization_with_generation() -> None:
229+
generation = Generation(text="hello-world")
230+
assert dumpd(generation)["kwargs"] == {"text": "hello-world", "type": "Generation"}

0 commit comments

Comments
 (0)