From 22863b8ac32d28e027ba8b7c50ca3315db7abfee Mon Sep 17 00:00:00 2001 From: Bagatur Date: Mon, 6 Jan 2025 16:11:20 -0500 Subject: [PATCH] rfc: AIMessage.parsed --- libs/core/langchain_core/messages/ai.py | 13 +++- .../prompts/__snapshots__/test_chat.ambr | 72 +++++++++++++++++++ .../runnables/__snapshots__/test_graph.ambr | 36 ++++++++++ 3 files changed, 120 insertions(+), 1 deletion(-) diff --git a/libs/core/langchain_core/messages/ai.py b/libs/core/langchain_core/messages/ai.py index 727a0045ffb..5661ae23eb5 100644 --- a/libs/core/langchain_core/messages/ai.py +++ b/libs/core/langchain_core/messages/ai.py @@ -2,7 +2,7 @@ import json import operator from typing import Any, Literal, Optional, Union, cast -from pydantic import model_validator +from pydantic import BaseModel, model_validator from typing_extensions import NotRequired, Self, TypedDict from langchain_core.messages.base import ( @@ -163,6 +163,8 @@ class AIMessage(BaseMessage): This is a standard representation of token usage that is consistent across models. """ + parsed: Optional[Union[dict, BaseModel]] = None + """The auto-parsed message contents.""" type: Literal["ai"] = "ai" """The type of the message (used for deserialization). Defaults to "ai".""" @@ -440,11 +442,20 @@ def add_ai_message_chunks( else: usage_metadata = None + # 'parsed' always represents an aggregation not an incremental value, so the last + # non-null value is kept. + parsed = None + for m in reversed([left, *others]): + if m.parsed is not None: + parsed = m.parsed + break + return left.__class__( example=left.example, content=content, additional_kwargs=additional_kwargs, tool_call_chunks=tool_call_chunks, + parsed=parsed, response_metadata=response_metadata, usage_metadata=usage_metadata, id=left.id, diff --git a/libs/core/tests/unit_tests/prompts/__snapshots__/test_chat.ambr b/libs/core/tests/unit_tests/prompts/__snapshots__/test_chat.ambr index 7a24cf9dc65..7ac608880f3 100644 --- a/libs/core/tests/unit_tests/prompts/__snapshots__/test_chat.ambr +++ b/libs/core/tests/unit_tests/prompts/__snapshots__/test_chat.ambr @@ -77,6 +77,21 @@ 'default': None, 'title': 'Name', }), + 'parsed': dict({ + 'anyOf': list([ + dict({ + 'type': 'object', + }), + dict({ + '$ref': '#/$defs/BaseModel', + }), + dict({ + 'type': 'null', + }), + ]), + 'default': None, + 'title': 'Parsed', + }), 'response_metadata': dict({ 'title': 'Response Metadata', 'type': 'object', @@ -181,6 +196,21 @@ 'default': None, 'title': 'Name', }), + 'parsed': dict({ + 'anyOf': list([ + dict({ + 'type': 'object', + }), + dict({ + '$ref': '#/$defs/BaseModel', + }), + dict({ + 'type': 'null', + }), + ]), + 'default': None, + 'title': 'Parsed', + }), 'response_metadata': dict({ 'title': 'Response Metadata', 'type': 'object', @@ -227,6 +257,12 @@ 'title': 'AIMessageChunk', 'type': 'object', }), + 'BaseModel': dict({ + 'properties': dict({ + }), + 'title': 'BaseModel', + 'type': 'object', + }), 'ChatMessage': dict({ 'additionalProperties': True, 'description': 'Message that can be assigned an arbitrary speaker (i.e. role).', @@ -1507,6 +1543,21 @@ 'default': None, 'title': 'Name', }), + 'parsed': dict({ + 'anyOf': list([ + dict({ + 'type': 'object', + }), + dict({ + '$ref': '#/$defs/BaseModel', + }), + dict({ + 'type': 'null', + }), + ]), + 'default': None, + 'title': 'Parsed', + }), 'response_metadata': dict({ 'title': 'Response Metadata', 'type': 'object', @@ -1611,6 +1662,21 @@ 'default': None, 'title': 'Name', }), + 'parsed': dict({ + 'anyOf': list([ + dict({ + 'type': 'object', + }), + dict({ + '$ref': '#/$defs/BaseModel', + }), + dict({ + 'type': 'null', + }), + ]), + 'default': None, + 'title': 'Parsed', + }), 'response_metadata': dict({ 'title': 'Response Metadata', 'type': 'object', @@ -1657,6 +1723,12 @@ 'title': 'AIMessageChunk', 'type': 'object', }), + 'BaseModel': dict({ + 'properties': dict({ + }), + 'title': 'BaseModel', + 'type': 'object', + }), 'ChatMessage': dict({ 'additionalProperties': True, 'description': 'Message that can be assigned an arbitrary speaker (i.e. role).', diff --git a/libs/core/tests/unit_tests/runnables/__snapshots__/test_graph.ambr b/libs/core/tests/unit_tests/runnables/__snapshots__/test_graph.ambr index 5a3b7126d99..65c3f83f192 100644 --- a/libs/core/tests/unit_tests/runnables/__snapshots__/test_graph.ambr +++ b/libs/core/tests/unit_tests/runnables/__snapshots__/test_graph.ambr @@ -451,6 +451,21 @@ 'default': None, 'title': 'Name', }), + 'parsed': dict({ + 'anyOf': list([ + dict({ + 'type': 'object', + }), + dict({ + '$ref': '#/$defs/BaseModel', + }), + dict({ + 'type': 'null', + }), + ]), + 'default': None, + 'title': 'Parsed', + }), 'response_metadata': dict({ 'title': 'Response Metadata', 'type': 'object', @@ -555,6 +570,21 @@ 'default': None, 'title': 'Name', }), + 'parsed': dict({ + 'anyOf': list([ + dict({ + 'type': 'object', + }), + dict({ + '$ref': '#/$defs/BaseModel', + }), + dict({ + 'type': 'null', + }), + ]), + 'default': None, + 'title': 'Parsed', + }), 'response_metadata': dict({ 'title': 'Response Metadata', 'type': 'object', @@ -601,6 +631,12 @@ 'title': 'AIMessageChunk', 'type': 'object', }), + 'BaseModel': dict({ + 'properties': dict({ + }), + 'title': 'BaseModel', + 'type': 'object', + }), 'ChatMessage': dict({ 'additionalProperties': True, 'description': 'Message that can be assigned an arbitrary speaker (i.e. role).',