mirror of
https://github.com/hwchase17/langchain.git
synced 2025-08-08 12:31:49 +00:00
start on openai
This commit is contained in:
parent
041b196145
commit
4899857042
@ -307,7 +307,7 @@ def _convert_from_v1_message(message: MessageV1) -> BaseMessage:
|
|||||||
id=message.id,
|
id=message.id,
|
||||||
name=message.name,
|
name=message.name,
|
||||||
tool_calls=message.tool_calls,
|
tool_calls=message.tool_calls,
|
||||||
response_metadata=cast(dict, message.response_metadata),
|
response_metadata=cast("dict", message.response_metadata),
|
||||||
)
|
)
|
||||||
if isinstance(message, AIMessageChunkV1):
|
if isinstance(message, AIMessageChunkV1):
|
||||||
return AIMessageChunk(
|
return AIMessageChunk(
|
||||||
@ -315,7 +315,7 @@ def _convert_from_v1_message(message: MessageV1) -> BaseMessage:
|
|||||||
id=message.id,
|
id=message.id,
|
||||||
name=message.name,
|
name=message.name,
|
||||||
tool_call_chunks=message.tool_call_chunks,
|
tool_call_chunks=message.tool_call_chunks,
|
||||||
response_metadata=cast(dict, message.response_metadata),
|
response_metadata=cast("dict", message.response_metadata),
|
||||||
)
|
)
|
||||||
if isinstance(message, HumanMessageV1):
|
if isinstance(message, HumanMessageV1):
|
||||||
return HumanMessage(
|
return HumanMessage(
|
||||||
|
@ -5,6 +5,8 @@ import uuid
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Any, Literal, Optional, TypedDict, Union, cast, get_args
|
from typing import Any, Literal, Optional, TypedDict, Union, cast, get_args
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
import langchain_core.messages.content_blocks as types
|
import langchain_core.messages.content_blocks as types
|
||||||
from langchain_core.messages.ai import _LC_ID_PREFIX, UsageMetadata, add_usage
|
from langchain_core.messages.ai import _LC_ID_PREFIX, UsageMetadata, add_usage
|
||||||
from langchain_core.messages.base import merge_content
|
from langchain_core.messages.base import merge_content
|
||||||
@ -32,20 +34,20 @@ def _ensure_id(id_val: Optional[str]) -> str:
|
|||||||
return id_val or str(uuid.uuid4())
|
return id_val or str(uuid.uuid4())
|
||||||
|
|
||||||
|
|
||||||
class Provider(TypedDict):
|
class ResponseMetadata(TypedDict, total=False):
|
||||||
"""Information about the provider that generated the message.
|
"""Metadata about the response from the AI provider.
|
||||||
|
|
||||||
Contains metadata about the AI provider and model used to generate content.
|
Contains additional information returned by the provider, such as
|
||||||
|
response headers, service tiers, log probabilities, system fingerprints, etc.
|
||||||
|
|
||||||
Attributes:
|
Extra keys are permitted from what is typed here.
|
||||||
name: Name and version of the provider that created the content block.
|
|
||||||
model_name: Name of the model that generated the content block.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name: str
|
model_provider: str
|
||||||
"""Name and version of the provider that created the content block."""
|
"""Name and version of the provider that created the message (e.g., openai)."""
|
||||||
|
|
||||||
model_name: str
|
model_name: str
|
||||||
"""Name of the model that generated the content block."""
|
"""Name of the model that generated the message."""
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -91,21 +93,29 @@ class AIMessage:
|
|||||||
usage_metadata: Optional[UsageMetadata] = None
|
usage_metadata: Optional[UsageMetadata] = None
|
||||||
"""If provided, usage metadata for a message, such as token counts."""
|
"""If provided, usage metadata for a message, such as token counts."""
|
||||||
|
|
||||||
response_metadata: dict = field(default_factory=dict)
|
response_metadata: ResponseMetadata = field(
|
||||||
|
default_factory=lambda: ResponseMetadata()
|
||||||
|
)
|
||||||
"""Metadata about the response.
|
"""Metadata about the response.
|
||||||
|
|
||||||
This field should include non-standard data returned by the provider, such as
|
This field should include non-standard data returned by the provider, such as
|
||||||
response headers, service tiers, or log probabilities.
|
response headers, service tiers, or log probabilities.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
parsed: Optional[Union[dict[str, Any], BaseModel]] = None
|
||||||
|
"""Auto-parsed message contents, if applicable."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
content: Union[str, list[types.ContentBlock]],
|
content: Union[str, list[types.ContentBlock]],
|
||||||
id: Optional[str] = None,
|
id: Optional[str] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
lc_version: str = "v1",
|
lc_version: str = "v1",
|
||||||
response_metadata: Optional[dict] = None,
|
response_metadata: Optional[ResponseMetadata] = None,
|
||||||
usage_metadata: Optional[UsageMetadata] = None,
|
usage_metadata: Optional[UsageMetadata] = None,
|
||||||
|
tool_calls: Optional[list[types.ToolCall]] = None,
|
||||||
|
invalid_tool_calls: Optional[list[types.InvalidToolCall]] = None,
|
||||||
|
parsed: Optional[Union[dict[str, Any], BaseModel]] = None,
|
||||||
):
|
):
|
||||||
"""Initialize an AI message.
|
"""Initialize an AI message.
|
||||||
|
|
||||||
@ -116,6 +126,11 @@ class AIMessage:
|
|||||||
lc_version: Encoding version for the message.
|
lc_version: Encoding version for the message.
|
||||||
response_metadata: Optional metadata about the response.
|
response_metadata: Optional metadata about the response.
|
||||||
usage_metadata: Optional metadata about token usage.
|
usage_metadata: Optional metadata about token usage.
|
||||||
|
tool_calls: Optional list of tool calls made by the AI. Tool calls should
|
||||||
|
generally be included in message content. If passed on init, they will
|
||||||
|
be added to the content list.
|
||||||
|
invalid_tool_calls: Optional list of tool calls that failed validation.
|
||||||
|
parsed: Optional auto-parsed message contents, if applicable.
|
||||||
"""
|
"""
|
||||||
if isinstance(content, str):
|
if isinstance(content, str):
|
||||||
self.content = [{"type": "text", "text": content}]
|
self.content = [{"type": "text", "text": content}]
|
||||||
@ -126,13 +141,27 @@ class AIMessage:
|
|||||||
self.name = name
|
self.name = name
|
||||||
self.lc_version = lc_version
|
self.lc_version = lc_version
|
||||||
self.usage_metadata = usage_metadata
|
self.usage_metadata = usage_metadata
|
||||||
|
self.parsed = parsed
|
||||||
if response_metadata is None:
|
if response_metadata is None:
|
||||||
self.response_metadata = {}
|
self.response_metadata = {}
|
||||||
else:
|
else:
|
||||||
self.response_metadata = response_metadata
|
self.response_metadata = response_metadata
|
||||||
|
|
||||||
self._tool_calls: list[types.ToolCall] = []
|
# Add tool calls to content if provided on init
|
||||||
self._invalid_tool_calls: list[types.InvalidToolCall] = []
|
if tool_calls:
|
||||||
|
content_tool_calls = {
|
||||||
|
block["id"]
|
||||||
|
for block in self.content
|
||||||
|
if block["type"] == "tool_call" and "id" in block
|
||||||
|
}
|
||||||
|
for tool_call in tool_calls:
|
||||||
|
if "id" in tool_call and tool_call["id"] in content_tool_calls:
|
||||||
|
continue
|
||||||
|
self.content.append(tool_call)
|
||||||
|
self._tool_calls = [
|
||||||
|
block for block in self.content if block["type"] == "tool_call"
|
||||||
|
]
|
||||||
|
self.invalid_tool_calls = invalid_tool_calls or []
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def text(self) -> Optional[str]:
|
def text(self) -> Optional[str]:
|
||||||
@ -150,7 +179,7 @@ class AIMessage:
|
|||||||
tool_calls = [block for block in self.content if block["type"] == "tool_call"]
|
tool_calls = [block for block in self.content if block["type"] == "tool_call"]
|
||||||
if tool_calls:
|
if tool_calls:
|
||||||
self._tool_calls = tool_calls
|
self._tool_calls = tool_calls
|
||||||
return self._tool_calls
|
return [block for block in self.content if block["type"] == "tool_call"]
|
||||||
|
|
||||||
@tool_calls.setter
|
@tool_calls.setter
|
||||||
def tool_calls(self, value: list[types.ToolCall]) -> None:
|
def tool_calls(self, value: list[types.ToolCall]) -> None:
|
||||||
@ -202,13 +231,16 @@ class AIMessageChunk:
|
|||||||
These data represent incremental usage statistics, as opposed to a running total.
|
These data represent incremental usage statistics, as opposed to a running total.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
response_metadata: dict = field(init=False)
|
response_metadata: ResponseMetadata = field(init=False)
|
||||||
"""Metadata about the response chunk.
|
"""Metadata about the response chunk.
|
||||||
|
|
||||||
This field should include non-standard data returned by the provider, such as
|
This field should include non-standard data returned by the provider, such as
|
||||||
response headers, service tiers, or log probabilities.
|
response headers, service tiers, or log probabilities.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
parsed: Optional[Union[dict[str, Any], BaseModel]] = None
|
||||||
|
"""Auto-parsed message contents, if applicable."""
|
||||||
|
|
||||||
tool_call_chunks: list[types.ToolCallChunk] = field(init=False)
|
tool_call_chunks: list[types.ToolCallChunk] = field(init=False)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -217,9 +249,10 @@ class AIMessageChunk:
|
|||||||
id: Optional[str] = None,
|
id: Optional[str] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
lc_version: str = "v1",
|
lc_version: str = "v1",
|
||||||
response_metadata: Optional[dict] = None,
|
response_metadata: Optional[ResponseMetadata] = None,
|
||||||
usage_metadata: Optional[UsageMetadata] = None,
|
usage_metadata: Optional[UsageMetadata] = None,
|
||||||
tool_call_chunks: Optional[list[types.ToolCallChunk]] = None,
|
tool_call_chunks: Optional[list[types.ToolCallChunk]] = None,
|
||||||
|
parsed: Optional[Union[dict[str, Any], BaseModel]] = None,
|
||||||
):
|
):
|
||||||
"""Initialize an AI message.
|
"""Initialize an AI message.
|
||||||
|
|
||||||
@ -231,6 +264,7 @@ class AIMessageChunk:
|
|||||||
response_metadata: Optional metadata about the response.
|
response_metadata: Optional metadata about the response.
|
||||||
usage_metadata: Optional metadata about token usage.
|
usage_metadata: Optional metadata about token usage.
|
||||||
tool_call_chunks: Optional list of partial tool call data.
|
tool_call_chunks: Optional list of partial tool call data.
|
||||||
|
parsed: Optional auto-parsed message contents, if applicable.
|
||||||
"""
|
"""
|
||||||
if isinstance(content, str):
|
if isinstance(content, str):
|
||||||
self.content = [{"type": "text", "text": content, "index": 0}]
|
self.content = [{"type": "text", "text": content, "index": 0}]
|
||||||
@ -241,6 +275,7 @@ class AIMessageChunk:
|
|||||||
self.name = name
|
self.name = name
|
||||||
self.lc_version = lc_version
|
self.lc_version = lc_version
|
||||||
self.usage_metadata = usage_metadata
|
self.usage_metadata = usage_metadata
|
||||||
|
self.parsed = parsed
|
||||||
if response_metadata is None:
|
if response_metadata is None:
|
||||||
self.response_metadata = {}
|
self.response_metadata = {}
|
||||||
else:
|
else:
|
||||||
@ -251,7 +286,7 @@ class AIMessageChunk:
|
|||||||
self.tool_call_chunks = tool_call_chunks
|
self.tool_call_chunks = tool_call_chunks
|
||||||
|
|
||||||
self._tool_calls: list[types.ToolCall] = []
|
self._tool_calls: list[types.ToolCall] = []
|
||||||
self._invalid_tool_calls: list[types.InvalidToolCall] = []
|
self.invalid_tool_calls: list[types.InvalidToolCall] = []
|
||||||
self._init_tool_calls()
|
self._init_tool_calls()
|
||||||
|
|
||||||
def _init_tool_calls(self) -> None:
|
def _init_tool_calls(self) -> None:
|
||||||
@ -264,7 +299,7 @@ class AIMessageChunk:
|
|||||||
ValueError: If the tool call chunks are malformed.
|
ValueError: If the tool call chunks are malformed.
|
||||||
"""
|
"""
|
||||||
self._tool_calls = []
|
self._tool_calls = []
|
||||||
self._invalid_tool_calls = []
|
self.invalid_tool_calls = []
|
||||||
if not self.tool_call_chunks:
|
if not self.tool_call_chunks:
|
||||||
if self._tool_calls:
|
if self._tool_calls:
|
||||||
self.tool_call_chunks = [
|
self.tool_call_chunks = [
|
||||||
@ -276,14 +311,14 @@ class AIMessageChunk:
|
|||||||
)
|
)
|
||||||
for tc in self._tool_calls
|
for tc in self._tool_calls
|
||||||
]
|
]
|
||||||
if self._invalid_tool_calls:
|
if self.invalid_tool_calls:
|
||||||
tool_call_chunks = self.tool_call_chunks
|
tool_call_chunks = self.tool_call_chunks
|
||||||
tool_call_chunks.extend(
|
tool_call_chunks.extend(
|
||||||
[
|
[
|
||||||
create_tool_call_chunk(
|
create_tool_call_chunk(
|
||||||
name=tc["name"], args=tc["args"], id=tc["id"], index=None
|
name=tc["name"], args=tc["args"], id=tc["id"], index=None
|
||||||
)
|
)
|
||||||
for tc in self._invalid_tool_calls
|
for tc in self.invalid_tool_calls
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
self.tool_call_chunks = tool_call_chunks
|
self.tool_call_chunks = tool_call_chunks
|
||||||
@ -317,7 +352,7 @@ class AIMessageChunk:
|
|||||||
except Exception:
|
except Exception:
|
||||||
add_chunk_to_invalid_tool_calls(chunk)
|
add_chunk_to_invalid_tool_calls(chunk)
|
||||||
self._tool_calls = tool_calls
|
self._tool_calls = tool_calls
|
||||||
self._invalid_tool_calls = invalid_tool_calls
|
self.invalid_tool_calls = invalid_tool_calls
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def text(self) -> Optional[str]:
|
def text(self) -> Optional[str]:
|
||||||
@ -361,6 +396,20 @@ class AIMessageChunk:
|
|||||||
error_msg = "Can only add AIMessageChunk or sequence of AIMessageChunk."
|
error_msg = "Can only add AIMessageChunk or sequence of AIMessageChunk."
|
||||||
raise NotImplementedError(error_msg)
|
raise NotImplementedError(error_msg)
|
||||||
|
|
||||||
|
def to_message(self) -> "AIMessage":
|
||||||
|
"""Convert this AIMessageChunk to an AIMessage."""
|
||||||
|
return AIMessage(
|
||||||
|
content=self.content,
|
||||||
|
id=self.id,
|
||||||
|
name=self.name,
|
||||||
|
lc_version=self.lc_version,
|
||||||
|
response_metadata=self.response_metadata,
|
||||||
|
usage_metadata=self.usage_metadata,
|
||||||
|
tool_calls=self.tool_calls,
|
||||||
|
invalid_tool_calls=self.invalid_tool_calls,
|
||||||
|
parsed=self.parsed,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def add_ai_message_chunks(
|
def add_ai_message_chunks(
|
||||||
left: AIMessageChunk, *others: AIMessageChunk
|
left: AIMessageChunk, *others: AIMessageChunk
|
||||||
@ -371,7 +420,8 @@ def add_ai_message_chunks(
|
|||||||
*(cast("list[str | dict[Any, Any]]", o.content) for o in others),
|
*(cast("list[str | dict[Any, Any]]", o.content) for o in others),
|
||||||
)
|
)
|
||||||
response_metadata = merge_dicts(
|
response_metadata = merge_dicts(
|
||||||
left.response_metadata, *(o.response_metadata for o in others)
|
cast("dict", left.response_metadata),
|
||||||
|
*(cast("dict", o.response_metadata) for o in others),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Merge tool call chunks
|
# Merge tool call chunks
|
||||||
@ -398,6 +448,15 @@ def add_ai_message_chunks(
|
|||||||
else:
|
else:
|
||||||
usage_metadata = None
|
usage_metadata = None
|
||||||
|
|
||||||
|
# Parsed
|
||||||
|
# '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
|
||||||
|
|
||||||
chunk_id = None
|
chunk_id = None
|
||||||
candidates = [left.id] + [o.id for o in others]
|
candidates = [left.id] + [o.id for o in others]
|
||||||
# first pass: pick the first non-run-* id
|
# first pass: pick the first non-run-* id
|
||||||
@ -415,8 +474,9 @@ def add_ai_message_chunks(
|
|||||||
return left.__class__(
|
return left.__class__(
|
||||||
content=cast("list[types.ContentBlock]", content),
|
content=cast("list[types.ContentBlock]", content),
|
||||||
tool_call_chunks=tool_call_chunks,
|
tool_call_chunks=tool_call_chunks,
|
||||||
response_metadata=response_metadata,
|
response_metadata=cast("ResponseMetadata", response_metadata),
|
||||||
usage_metadata=usage_metadata,
|
usage_metadata=usage_metadata,
|
||||||
|
parsed=parsed,
|
||||||
id=chunk_id,
|
id=chunk_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -453,19 +513,25 @@ class HumanMessage:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, content: Union[str, list[types.ContentBlock]], id: Optional[str] = None
|
self,
|
||||||
|
content: Union[str, list[types.ContentBlock]],
|
||||||
|
*,
|
||||||
|
id: Optional[str] = None,
|
||||||
|
name: Optional[str] = None,
|
||||||
):
|
):
|
||||||
"""Initialize a human message.
|
"""Initialize a human message.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
content: Message content as string or list of content blocks.
|
content: Message content as string or list of content blocks.
|
||||||
id: Optional unique identifier for the message.
|
id: Optional unique identifier for the message.
|
||||||
|
name: Optional human-readable name for the message.
|
||||||
"""
|
"""
|
||||||
self.id = _ensure_id(id)
|
self.id = _ensure_id(id)
|
||||||
if isinstance(content, str):
|
if isinstance(content, str):
|
||||||
self.content = [{"type": "text", "text": content}]
|
self.content = [{"type": "text", "text": content}]
|
||||||
else:
|
else:
|
||||||
self.content = content
|
self.content = content
|
||||||
|
self.name = name
|
||||||
|
|
||||||
def text(self) -> str:
|
def text(self) -> str:
|
||||||
"""Extract all text content from the message.
|
"""Extract all text content from the message.
|
||||||
@ -495,20 +561,47 @@ class SystemMessage:
|
|||||||
content: list[types.ContentBlock]
|
content: list[types.ContentBlock]
|
||||||
type: Literal["system"] = "system"
|
type: Literal["system"] = "system"
|
||||||
|
|
||||||
|
name: Optional[str] = None
|
||||||
|
"""An optional name for the message.
|
||||||
|
|
||||||
|
This can be used to provide a human-readable name for the message.
|
||||||
|
|
||||||
|
Usage of this field is optional, and whether it's used or not is up to the
|
||||||
|
model implementation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
custom_role: Optional[str] = None
|
||||||
|
"""If provided, a custom role for the system message.
|
||||||
|
|
||||||
|
Example: ``"developer"``.
|
||||||
|
|
||||||
|
Integration packages may use this field to assign the system message role if it
|
||||||
|
contains a recognized value.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, content: Union[str, list[types.ContentBlock]], *, id: Optional[str] = None
|
self,
|
||||||
|
content: Union[str, list[types.ContentBlock]],
|
||||||
|
*,
|
||||||
|
id: Optional[str] = None,
|
||||||
|
custom_role: Optional[str] = None,
|
||||||
|
name: Optional[str] = None,
|
||||||
):
|
):
|
||||||
"""Initialize a system message.
|
"""Initialize a human message.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
content: System instructions as string or list of content blocks.
|
content: Message content as string or list of content blocks.
|
||||||
id: Optional unique identifier for the message.
|
id: Optional unique identifier for the message.
|
||||||
|
custom_role: If provided, a custom role for the system message.
|
||||||
|
name: Optional human-readable name for the message.
|
||||||
"""
|
"""
|
||||||
self.id = _ensure_id(id)
|
self.id = _ensure_id(id)
|
||||||
if isinstance(content, str):
|
if isinstance(content, str):
|
||||||
self.content = [{"type": "text", "text": content}]
|
self.content = [{"type": "text", "text": content}]
|
||||||
else:
|
else:
|
||||||
self.content = content
|
self.content = content
|
||||||
|
self.custom_role = custom_role
|
||||||
|
self.name = name
|
||||||
|
|
||||||
def text(self) -> str:
|
def text(self) -> str:
|
||||||
"""Extract all text content from the system message."""
|
"""Extract all text content from the system message."""
|
||||||
@ -535,11 +628,51 @@ class ToolMessage:
|
|||||||
|
|
||||||
id: str
|
id: str
|
||||||
tool_call_id: str
|
tool_call_id: str
|
||||||
content: list[dict[str, Any]]
|
content: list[types.ContentBlock]
|
||||||
artifact: Optional[Any] = None # App-side payload not for the model
|
artifact: Optional[Any] = None # App-side payload not for the model
|
||||||
|
|
||||||
|
name: Optional[str] = None
|
||||||
|
"""An optional name for the message.
|
||||||
|
|
||||||
|
This can be used to provide a human-readable name for the message.
|
||||||
|
|
||||||
|
Usage of this field is optional, and whether it's used or not is up to the
|
||||||
|
model implementation.
|
||||||
|
"""
|
||||||
|
|
||||||
status: Literal["success", "error"] = "success"
|
status: Literal["success", "error"] = "success"
|
||||||
type: Literal["tool"] = "tool"
|
type: Literal["tool"] = "tool"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
content: Union[str, list[types.ContentBlock]],
|
||||||
|
tool_call_id: str,
|
||||||
|
*,
|
||||||
|
id: Optional[str] = None,
|
||||||
|
name: Optional[str] = None,
|
||||||
|
artifact: Optional[Any] = None,
|
||||||
|
status: Literal["success", "error"] = "success",
|
||||||
|
):
|
||||||
|
"""Initialize a human message.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: Message content as string or list of content blocks.
|
||||||
|
tool_call_id: ID of the tool call this message responds to.
|
||||||
|
id: Optional unique identifier for the message.
|
||||||
|
name: Optional human-readable name for the message.
|
||||||
|
artifact: Optional app-side payload not intended for the model.
|
||||||
|
status: Execution status ("success" or "error").
|
||||||
|
"""
|
||||||
|
self.id = _ensure_id(id)
|
||||||
|
self.tool_call_id = tool_call_id
|
||||||
|
if isinstance(content, str):
|
||||||
|
self.content = [{"type": "text", "text": content}]
|
||||||
|
else:
|
||||||
|
self.content = content
|
||||||
|
self.name = name
|
||||||
|
self.artifact = artifact
|
||||||
|
self.status = status
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def text(self) -> str:
|
def text(self) -> str:
|
||||||
"""Extract all text content from the tool message."""
|
"""Extract all text content from the tool message."""
|
||||||
|
@ -14,16 +14,18 @@ from typing_extensions import TypedDict, overload
|
|||||||
|
|
||||||
from langchain_core.load.serializable import Serializable
|
from langchain_core.load.serializable import Serializable
|
||||||
from langchain_core.messages import (
|
from langchain_core.messages import (
|
||||||
|
AIMessage,
|
||||||
AnyMessage,
|
AnyMessage,
|
||||||
BaseMessage,
|
BaseMessage,
|
||||||
HumanMessage,
|
HumanMessage,
|
||||||
|
SystemMessage,
|
||||||
|
ToolMessage,
|
||||||
get_buffer_string,
|
get_buffer_string,
|
||||||
)
|
)
|
||||||
from langchain_core.messages import content_blocks as types
|
from langchain_core.messages import content_blocks as types
|
||||||
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage, ToolMessage
|
|
||||||
from langchain_core.messages.v1 import AIMessage as AIMessageV1
|
from langchain_core.messages.v1 import AIMessage as AIMessageV1
|
||||||
from langchain_core.messages.v1 import HumanMessage as HumanMessageV1
|
from langchain_core.messages.v1 import HumanMessage as HumanMessageV1
|
||||||
from langchain_core.messages.v1 import MessageV1
|
from langchain_core.messages.v1 import MessageV1, ResponseMetadata
|
||||||
from langchain_core.messages.v1 import SystemMessage as SystemMessageV1
|
from langchain_core.messages.v1 import SystemMessage as SystemMessageV1
|
||||||
from langchain_core.messages.v1 import ToolMessage as ToolMessageV1
|
from langchain_core.messages.v1 import ToolMessage as ToolMessageV1
|
||||||
|
|
||||||
@ -40,7 +42,7 @@ def _convert_to_v1(message: BaseMessage) -> MessageV1:
|
|||||||
if isinstance(block, str):
|
if isinstance(block, str):
|
||||||
content.append({"type": "text", "text": block})
|
content.append({"type": "text", "text": block})
|
||||||
elif isinstance(block, dict):
|
elif isinstance(block, dict):
|
||||||
content.append(block)
|
content.append(cast("types.ContentBlock", block))
|
||||||
else:
|
else:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -52,7 +54,7 @@ def _convert_to_v1(message: BaseMessage) -> MessageV1:
|
|||||||
return AIMessageV1(
|
return AIMessageV1(
|
||||||
content=content,
|
content=content,
|
||||||
usage_metadata=message.usage_metadata,
|
usage_metadata=message.usage_metadata,
|
||||||
response_metadata=message.response_metadata,
|
response_metadata=cast("ResponseMetadata", message.response_metadata),
|
||||||
tool_calls=message.tool_calls,
|
tool_calls=message.tool_calls,
|
||||||
)
|
)
|
||||||
if isinstance(message, SystemMessage):
|
if isinstance(message, SystemMessage):
|
||||||
@ -92,8 +94,18 @@ class PromptValue(Serializable, ABC):
|
|||||||
def to_string(self) -> str:
|
def to_string(self) -> str:
|
||||||
"""Return prompt value as string."""
|
"""Return prompt value as string."""
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def to_messages(
|
||||||
|
self, output_version: Literal["v0"] = "v0"
|
||||||
|
) -> list[BaseMessage]: ...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def to_messages(self, output_version: Literal["v1"]) -> list[MessageV1]: ...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def to_messages(self) -> list[BaseMessage]:
|
def to_messages(
|
||||||
|
self, output_version: Literal["v0", "v1"] = "v0"
|
||||||
|
) -> Union[Sequence[BaseMessage], Sequence[MessageV1]]:
|
||||||
"""Return prompt as a list of Messages."""
|
"""Return prompt as a list of Messages."""
|
||||||
|
|
||||||
|
|
||||||
@ -117,10 +129,6 @@ class StringPromptValue(PromptValue):
|
|||||||
"""Return prompt as string."""
|
"""Return prompt as string."""
|
||||||
return self.text
|
return self.text
|
||||||
|
|
||||||
def to_messages(self) -> list[BaseMessage]:
|
|
||||||
"""Return prompt as messages."""
|
|
||||||
return [HumanMessage(content=self.text)]
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def to_messages(
|
def to_messages(
|
||||||
self, output_version: Literal["v0"] = "v0"
|
self, output_version: Literal["v0"] = "v0"
|
||||||
@ -131,12 +139,8 @@ class StringPromptValue(PromptValue):
|
|||||||
|
|
||||||
def to_messages(
|
def to_messages(
|
||||||
self, output_version: Literal["v0", "v1"] = "v0"
|
self, output_version: Literal["v0", "v1"] = "v0"
|
||||||
) -> Union[list[BaseMessage], list[MessageV1]]:
|
) -> Union[Sequence[BaseMessage], Sequence[MessageV1]]:
|
||||||
"""Return prompt as a list of messages.
|
"""Return prompt as messages."""
|
||||||
|
|
||||||
Args:
|
|
||||||
output_version: The output version, either "v0" (default) or "v1".
|
|
||||||
"""
|
|
||||||
if output_version == "v1":
|
if output_version == "v1":
|
||||||
return [HumanMessageV1(content=self.text)]
|
return [HumanMessageV1(content=self.text)]
|
||||||
return [HumanMessage(content=self.text)]
|
return [HumanMessage(content=self.text)]
|
||||||
@ -165,7 +169,7 @@ class ChatPromptValue(PromptValue):
|
|||||||
|
|
||||||
def to_messages(
|
def to_messages(
|
||||||
self, output_version: Literal["v0", "v1"] = "v0"
|
self, output_version: Literal["v0", "v1"] = "v0"
|
||||||
) -> Union[list[BaseMessage], list[MessageV1]]:
|
) -> Union[Sequence[BaseMessage], Sequence[MessageV1]]:
|
||||||
"""Return prompt as a list of messages.
|
"""Return prompt as a list of messages.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -207,8 +211,26 @@ class ImagePromptValue(PromptValue):
|
|||||||
"""Return prompt (image URL) as string."""
|
"""Return prompt (image URL) as string."""
|
||||||
return self.image_url["url"]
|
return self.image_url["url"]
|
||||||
|
|
||||||
def to_messages(self) -> list[BaseMessage]:
|
@overload
|
||||||
|
def to_messages(
|
||||||
|
self, output_version: Literal["v0"] = "v0"
|
||||||
|
) -> list[BaseMessage]: ...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def to_messages(self, output_version: Literal["v1"]) -> list[MessageV1]: ...
|
||||||
|
|
||||||
|
def to_messages(
|
||||||
|
self, output_version: Literal["v0", "v1"] = "v0"
|
||||||
|
) -> Union[Sequence[BaseMessage], Sequence[MessageV1]]:
|
||||||
"""Return prompt (image URL) as messages."""
|
"""Return prompt (image URL) as messages."""
|
||||||
|
if output_version == "v1":
|
||||||
|
block: types.ImageContentBlock = {
|
||||||
|
"type": "image",
|
||||||
|
"url": self.image_url["url"],
|
||||||
|
}
|
||||||
|
if "detail" in self.image_url:
|
||||||
|
block["detail"] = self.image_url["detail"]
|
||||||
|
return [HumanMessageV1(content=[block])]
|
||||||
return [HumanMessage(content=[cast("dict", self.image_url)])]
|
return [HumanMessage(content=[cast("dict", self.image_url)])]
|
||||||
|
|
||||||
|
|
||||||
|
@ -67,6 +67,7 @@ langchain-text-splitters = { path = "../text-splitters" }
|
|||||||
strict = "True"
|
strict = "True"
|
||||||
strict_bytes = "True"
|
strict_bytes = "True"
|
||||||
enable_error_code = "deprecated"
|
enable_error_code = "deprecated"
|
||||||
|
disable_error_code = ["typeddict-unknown-key"]
|
||||||
|
|
||||||
# TODO: activate for 'strict' checking
|
# TODO: activate for 'strict' checking
|
||||||
disallow_any_generics = "False"
|
disallow_any_generics = "False"
|
||||||
|
@ -66,11 +66,14 @@ For backwards compatibility, this module provides functions to convert between t
|
|||||||
formats. The functions are used internally by ChatOpenAI.
|
formats. The functions are used internally by ChatOpenAI.
|
||||||
""" # noqa: E501
|
""" # noqa: E501
|
||||||
|
|
||||||
|
import copy
|
||||||
import json
|
import json
|
||||||
from collections.abc import Iterable, Iterator
|
from collections.abc import Iterable, Iterator
|
||||||
from typing import Any, Literal, Union, cast
|
from typing import Any, Literal, Union, cast
|
||||||
|
|
||||||
from langchain_core.messages import AIMessage, AIMessageChunk, is_data_content_block
|
from langchain_core.messages import AIMessage, AIMessageChunk, is_data_content_block
|
||||||
|
from langchain_core.messages import content_blocks as types
|
||||||
|
from langchain_core.messages.v1 import AIMessage as AIMessageV1
|
||||||
|
|
||||||
_FUNCTION_CALL_IDS_MAP_KEY = "__openai_function_call_ids__"
|
_FUNCTION_CALL_IDS_MAP_KEY = "__openai_function_call_ids__"
|
||||||
|
|
||||||
@ -289,25 +292,21 @@ def _convert_to_v1_from_chat_completions_chunk(chunk: AIMessageChunk) -> AIMessa
|
|||||||
return cast(AIMessageChunk, result)
|
return cast(AIMessageChunk, result)
|
||||||
|
|
||||||
|
|
||||||
def _convert_from_v1_to_chat_completions(message: AIMessage) -> AIMessage:
|
def _convert_from_v1_to_chat_completions(message: AIMessageV1) -> AIMessageV1:
|
||||||
"""Convert a v1 message to the Chat Completions format."""
|
"""Convert a v1 message to the Chat Completions format."""
|
||||||
if isinstance(message.content, list):
|
new_content: list[types.ContentBlock] = []
|
||||||
new_content: list = []
|
|
||||||
for block in message.content:
|
for block in message.content:
|
||||||
if isinstance(block, dict):
|
if block["type"] == "text":
|
||||||
block_type = block.get("type")
|
|
||||||
if block_type == "text":
|
|
||||||
# Strip annotations
|
# Strip annotations
|
||||||
new_content.append({"type": "text", "text": block["text"]})
|
new_content.append({"type": "text", "text": block["text"]})
|
||||||
elif block_type in ("reasoning", "tool_call"):
|
elif block["type"] in ("reasoning", "tool_call"):
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
new_content.append(block)
|
new_content.append(block)
|
||||||
else:
|
new_message = copy.copy(message)
|
||||||
new_content.append(block)
|
new_message.content = new_content
|
||||||
return message.model_copy(update={"content": new_content})
|
|
||||||
|
|
||||||
return message
|
return new_message
|
||||||
|
|
||||||
|
|
||||||
# v1 / Responses
|
# v1 / Responses
|
||||||
@ -367,13 +366,13 @@ def _explode_reasoning(block: dict[str, Any]) -> Iterable[dict[str, Any]]:
|
|||||||
yield new_block
|
yield new_block
|
||||||
|
|
||||||
|
|
||||||
def _convert_to_v1_from_responses(message: AIMessage) -> AIMessage:
|
def _convert_to_v1_from_responses(
|
||||||
|
content: list[dict[str, Any]],
|
||||||
|
) -> list[types.ContentBlock]:
|
||||||
"""Mutate a Responses message to v1 format."""
|
"""Mutate a Responses message to v1 format."""
|
||||||
if not isinstance(message.content, list):
|
|
||||||
return message
|
|
||||||
|
|
||||||
def _iter_blocks() -> Iterable[dict[str, Any]]:
|
def _iter_blocks() -> Iterable[dict[str, Any]]:
|
||||||
for block in message.content:
|
for block in content:
|
||||||
if not isinstance(block, dict):
|
if not isinstance(block, dict):
|
||||||
continue
|
continue
|
||||||
block_type = block.get("type")
|
block_type = block.get("type")
|
||||||
@ -485,16 +484,14 @@ def _convert_to_v1_from_responses(message: AIMessage) -> AIMessage:
|
|||||||
new_block["index"] = new_block["value"].pop("index")
|
new_block["index"] = new_block["value"].pop("index")
|
||||||
yield new_block
|
yield new_block
|
||||||
|
|
||||||
# Replace the list with the fully converted one
|
return list(_iter_blocks())
|
||||||
message.content = list(_iter_blocks())
|
|
||||||
|
|
||||||
return message
|
|
||||||
|
|
||||||
|
|
||||||
def _convert_annotation_from_v1(annotation: dict[str, Any]) -> dict[str, Any]:
|
def _convert_annotation_from_v1(annotation: types.Annotation) -> dict[str, Any]:
|
||||||
annotation_type = annotation.get("type")
|
if annotation["type"] == "citation":
|
||||||
|
if "url" in annotation:
|
||||||
|
return dict(annotation)
|
||||||
|
|
||||||
if annotation_type == "document_citation":
|
|
||||||
new_ann: dict[str, Any] = {"type": "file_citation"}
|
new_ann: dict[str, Any] = {"type": "file_citation"}
|
||||||
|
|
||||||
if "title" in annotation:
|
if "title" in annotation:
|
||||||
@ -502,11 +499,11 @@ def _convert_annotation_from_v1(annotation: dict[str, Any]) -> dict[str, Any]:
|
|||||||
|
|
||||||
for fld in ("file_id", "index"):
|
for fld in ("file_id", "index"):
|
||||||
if fld in annotation:
|
if fld in annotation:
|
||||||
new_ann[fld] = annotation[fld]
|
new_ann[fld] = annotation[fld] # type: ignore[typeddict-item]
|
||||||
|
|
||||||
return new_ann
|
return new_ann
|
||||||
|
|
||||||
elif annotation_type == "non_standard_annotation":
|
elif annotation["type"] == "non_standard_annotation":
|
||||||
return annotation["value"]
|
return annotation["value"]
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@ -621,53 +618,50 @@ def _consolidate_calls(
|
|||||||
yield nxt
|
yield nxt
|
||||||
|
|
||||||
|
|
||||||
def _convert_from_v1_to_responses(message: AIMessage) -> AIMessage:
|
def _convert_from_v1_to_responses(
|
||||||
if not isinstance(message.content, list):
|
content: list[types.ContentBlock], tool_calls: list[types.ToolCall]
|
||||||
return message
|
) -> list[dict[str, Any]]:
|
||||||
|
|
||||||
new_content: list = []
|
new_content: list = []
|
||||||
for block in message.content:
|
for block in content:
|
||||||
if isinstance(block, dict):
|
if block["type"] == "text" and "annotations" in block:
|
||||||
block_type = block.get("type")
|
|
||||||
if block_type == "text" and "annotations" in block:
|
|
||||||
# Need a copy because we’re changing the annotations list
|
# Need a copy because we’re changing the annotations list
|
||||||
new_block = dict(block)
|
new_block = dict(block)
|
||||||
new_block["annotations"] = [
|
new_block["annotations"] = [
|
||||||
_convert_annotation_from_v1(a) for a in block["annotations"]
|
_convert_annotation_from_v1(a) for a in block["annotations"]
|
||||||
]
|
]
|
||||||
new_content.append(new_block)
|
new_content.append(new_block)
|
||||||
elif block_type == "tool_call":
|
elif block["type"] == "tool_call":
|
||||||
new_block = {"type": "function_call", "call_id": block["id"]}
|
new_block = {"type": "function_call", "call_id": block["id"]}
|
||||||
if "item_id" in block:
|
if "item_id" in block:
|
||||||
new_block["id"] = block["item_id"]
|
new_block["id"] = block["item_id"] # type: ignore[typeddict-item]
|
||||||
if "name" in block and "arguments" in block:
|
if "name" in block and "arguments" in block:
|
||||||
new_block["name"] = block["name"]
|
new_block["name"] = block["name"]
|
||||||
new_block["arguments"] = block["arguments"]
|
new_block["arguments"] = block["arguments"] # type: ignore[typeddict-item]
|
||||||
else:
|
else:
|
||||||
tool_call = next(
|
matching_tool_calls = [
|
||||||
call for call in message.tool_calls if call["id"] == block["id"]
|
call for call in tool_calls if call["id"] == block["id"]
|
||||||
)
|
]
|
||||||
|
if matching_tool_calls:
|
||||||
|
tool_call = matching_tool_calls[0]
|
||||||
if "name" not in block:
|
if "name" not in block:
|
||||||
new_block["name"] = tool_call["name"]
|
new_block["name"] = tool_call["name"]
|
||||||
if "arguments" not in block:
|
if "arguments" not in block:
|
||||||
new_block["arguments"] = json.dumps(tool_call["args"])
|
new_block["arguments"] = json.dumps(tool_call["args"])
|
||||||
new_content.append(new_block)
|
new_content.append(new_block)
|
||||||
elif (
|
elif (
|
||||||
is_data_content_block(block)
|
is_data_content_block(cast(dict, block))
|
||||||
and block["type"] == "image"
|
and block["type"] == "image"
|
||||||
and "base64" in block
|
and "base64" in block
|
||||||
):
|
):
|
||||||
new_block = {"type": "image_generation_call", "result": block["base64"]}
|
new_block = {"type": "image_generation_call", "result": block["base64"]}
|
||||||
for extra_key in ("id", "status"):
|
for extra_key in ("id", "status"):
|
||||||
if extra_key in block:
|
if extra_key in block:
|
||||||
new_block[extra_key] = block[extra_key]
|
new_block[extra_key] = block[extra_key] # type: ignore[typeddict-item]
|
||||||
new_content.append(new_block)
|
new_content.append(new_block)
|
||||||
elif block_type == "non_standard" and "value" in block:
|
elif block["type"] == "non_standard" and "value" in block:
|
||||||
new_content.append(block["value"])
|
new_content.append(block["value"])
|
||||||
else:
|
else:
|
||||||
new_content.append(block)
|
new_content.append(block)
|
||||||
else:
|
|
||||||
new_content.append(block)
|
|
||||||
|
|
||||||
new_content = list(_implode_reasoning_blocks(new_content))
|
new_content = list(_implode_reasoning_blocks(new_content))
|
||||||
new_content = list(
|
new_content = list(
|
||||||
@ -679,4 +673,4 @@ def _convert_from_v1_to_responses(message: AIMessage) -> AIMessage:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return message.model_copy(update={"content": new_content})
|
return new_content
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -56,6 +56,8 @@ langchain-tests = { path = "../../standard-tests", editable = true }
|
|||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
disallow_untyped_defs = "True"
|
disallow_untyped_defs = "True"
|
||||||
|
disable_error_code = ["typeddict-unknown-key"]
|
||||||
|
|
||||||
[[tool.mypy.overrides]]
|
[[tool.mypy.overrides]]
|
||||||
module = "transformers"
|
module = "transformers"
|
||||||
ignore_missing_imports = true
|
ignore_missing_imports = true
|
||||||
|
Loading…
Reference in New Issue
Block a user