mirror of
https://github.com/hwchase17/langchain.git
synced 2025-07-13 16:36:06 +00:00
anthropic[patch]: ruff fixes and rules (#31899)
* bump ruff deps * add more thorough ruff rules * fix said rules
This commit is contained in:
parent
e7eac27241
commit
2a7645300c
@ -180,13 +180,13 @@ select = [
|
|||||||
"YTT", # flake8-2020
|
"YTT", # flake8-2020
|
||||||
]
|
]
|
||||||
ignore = [
|
ignore = [
|
||||||
"D100",
|
"D100", # Missing docstring in public module
|
||||||
"D101",
|
"D101", # Missing docstring in public class
|
||||||
"D102",
|
"D102", # Missing docstring in public method
|
||||||
"D103",
|
"D103", # Missing docstring in public function
|
||||||
"D104",
|
"D104", # Missing docstring in public package
|
||||||
"D105",
|
"D105", # Missing docstring in magic method
|
||||||
"D107",
|
"D107", # Missing docstring in __init__
|
||||||
"COM812", # Messes with the formatter
|
"COM812", # Messes with the formatter
|
||||||
"ISC001", # Messes with the formatter
|
"ISC001", # Messes with the formatter
|
||||||
"PERF203", # Rarely useful
|
"PERF203", # Rarely useful
|
||||||
|
@ -6,9 +6,9 @@ from langchain_anthropic.chat_models import (
|
|||||||
from langchain_anthropic.llms import Anthropic, AnthropicLLM
|
from langchain_anthropic.llms import Anthropic, AnthropicLLM
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"ChatAnthropicMessages",
|
|
||||||
"ChatAnthropic",
|
|
||||||
"convert_to_anthropic_tool",
|
|
||||||
"Anthropic",
|
"Anthropic",
|
||||||
"AnthropicLLM",
|
"AnthropicLLM",
|
||||||
|
"ChatAnthropic",
|
||||||
|
"ChatAnthropicMessages",
|
||||||
|
"convert_to_anthropic_tool",
|
||||||
]
|
]
|
||||||
|
@ -6,6 +6,8 @@ for each instance of ChatAnthropic.
|
|||||||
Logic is largely replicated from anthropic._base_client.
|
Logic is largely replicated from anthropic._base_client.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
@ -17,7 +19,7 @@ _NOT_GIVEN: Any = object()
|
|||||||
|
|
||||||
|
|
||||||
class _SyncHttpxClientWrapper(anthropic.DefaultHttpxClient):
|
class _SyncHttpxClientWrapper(anthropic.DefaultHttpxClient):
|
||||||
"""Borrowed from anthropic._base_client"""
|
"""Borrowed from anthropic._base_client."""
|
||||||
|
|
||||||
def __del__(self) -> None:
|
def __del__(self) -> None:
|
||||||
if self.is_closed:
|
if self.is_closed:
|
||||||
@ -30,7 +32,7 @@ class _SyncHttpxClientWrapper(anthropic.DefaultHttpxClient):
|
|||||||
|
|
||||||
|
|
||||||
class _AsyncHttpxClientWrapper(anthropic.DefaultAsyncHttpxClient):
|
class _AsyncHttpxClientWrapper(anthropic.DefaultAsyncHttpxClient):
|
||||||
"""Borrowed from anthropic._base_client"""
|
"""Borrowed from anthropic._base_client."""
|
||||||
|
|
||||||
def __del__(self) -> None:
|
def __del__(self) -> None:
|
||||||
if self.is_closed:
|
if self.is_closed:
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
@ -111,18 +113,19 @@ def _is_builtin_tool(tool: Any) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def _format_image(url: str) -> dict:
|
def _format_image(url: str) -> dict:
|
||||||
"""
|
"""Convert part["image_url"]["url"] strings (OpenAI format) to Anthropic format.
|
||||||
Converts part["image_url"]["url"] strings (OpenAI format)
|
|
||||||
to the correct Anthropic format:
|
|
||||||
{
|
{
|
||||||
"type": "base64",
|
"type": "base64",
|
||||||
"media_type": "image/jpeg",
|
"media_type": "image/jpeg",
|
||||||
"data": "/9j/4AAQSkZJRg...",
|
"data": "/9j/4AAQSkZJRg...",
|
||||||
}
|
}
|
||||||
|
|
||||||
Or
|
Or
|
||||||
|
|
||||||
{
|
{
|
||||||
"type": "url",
|
"type": "url",
|
||||||
"url": "https://example.com/image.jpg",
|
"url": "https://example.com/image.jpg",
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
# Base64 encoded image
|
# Base64 encoded image
|
||||||
@ -146,11 +149,14 @@ def _format_image(url: str) -> dict:
|
|||||||
"url": url,
|
"url": url,
|
||||||
}
|
}
|
||||||
|
|
||||||
raise ValueError(
|
msg = (
|
||||||
"Malformed url parameter."
|
"Malformed url parameter."
|
||||||
" Must be either an image URL (https://example.com/image.jpg)"
|
" Must be either an image URL (https://example.com/image.jpg)"
|
||||||
" or base64 encoded string (data:image/png;base64,'/9j/4AAQSk'...)"
|
" or base64 encoded string (data:image/png;base64,'/9j/4AAQSk'...)"
|
||||||
)
|
)
|
||||||
|
raise ValueError(
|
||||||
|
msg,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _merge_messages(
|
def _merge_messages(
|
||||||
@ -177,8 +183,8 @@ def _merge_messages(
|
|||||||
"content": curr.content,
|
"content": curr.content,
|
||||||
"tool_use_id": curr.tool_call_id,
|
"tool_use_id": curr.tool_call_id,
|
||||||
"is_error": curr.status == "error",
|
"is_error": curr.status == "error",
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
)
|
)
|
||||||
last = merged[-1] if merged else None
|
last = merged[-1] if merged else None
|
||||||
if any(
|
if any(
|
||||||
@ -187,7 +193,7 @@ def _merge_messages(
|
|||||||
):
|
):
|
||||||
if isinstance(cast(BaseMessage, last).content, str):
|
if isinstance(cast(BaseMessage, last).content, str):
|
||||||
new_content: list = [
|
new_content: list = [
|
||||||
{"type": "text", "text": cast(BaseMessage, last).content}
|
{"type": "text", "text": cast(BaseMessage, last).content},
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
new_content = copy.copy(cast(list, cast(BaseMessage, last).content))
|
new_content = copy.copy(cast(list, cast(BaseMessage, last).content))
|
||||||
@ -234,10 +240,13 @@ def _format_data_content_block(block: dict) -> dict:
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
raise ValueError(
|
msg = (
|
||||||
"Anthropic only supports 'url' and 'base64' source_type for image "
|
"Anthropic only supports 'url' and 'base64' source_type for image "
|
||||||
"content blocks."
|
"content blocks."
|
||||||
)
|
)
|
||||||
|
raise ValueError(
|
||||||
|
msg,
|
||||||
|
)
|
||||||
|
|
||||||
elif block["type"] == "file":
|
elif block["type"] == "file":
|
||||||
if block["source_type"] == "url":
|
if block["source_type"] == "url":
|
||||||
@ -276,7 +285,8 @@ def _format_data_content_block(block: dict) -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Block of type {block['type']} is not supported.")
|
msg = f"Block of type {block['type']} is not supported."
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
if formatted_block:
|
if formatted_block:
|
||||||
for key in ["cache_control", "citations", "title", "context"]:
|
for key in ["cache_control", "citations", "title", "context"]:
|
||||||
@ -292,7 +302,6 @@ def _format_messages(
|
|||||||
messages: Sequence[BaseMessage],
|
messages: Sequence[BaseMessage],
|
||||||
) -> tuple[Union[str, list[dict], None], list[dict]]:
|
) -> tuple[Union[str, list[dict], None], list[dict]]:
|
||||||
"""Format messages for anthropic."""
|
"""Format messages for anthropic."""
|
||||||
|
|
||||||
"""
|
"""
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@ -308,8 +317,9 @@ def _format_messages(
|
|||||||
for i, message in enumerate(merged_messages):
|
for i, message in enumerate(merged_messages):
|
||||||
if message.type == "system":
|
if message.type == "system":
|
||||||
if system is not None:
|
if system is not None:
|
||||||
raise ValueError("Received multiple non-consecutive system messages.")
|
msg = "Received multiple non-consecutive system messages."
|
||||||
elif isinstance(message.content, list):
|
raise ValueError(msg)
|
||||||
|
if isinstance(message.content, list):
|
||||||
system = [
|
system = [
|
||||||
(
|
(
|
||||||
block
|
block
|
||||||
@ -328,8 +338,9 @@ def _format_messages(
|
|||||||
if not isinstance(message.content, str):
|
if not isinstance(message.content, str):
|
||||||
# parse as dict
|
# parse as dict
|
||||||
if not isinstance(message.content, list):
|
if not isinstance(message.content, list):
|
||||||
|
msg = "Anthropic message content must be str or list of dicts"
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Anthropic message content must be str or list of dicts"
|
msg,
|
||||||
)
|
)
|
||||||
|
|
||||||
# populate content
|
# populate content
|
||||||
@ -339,8 +350,9 @@ def _format_messages(
|
|||||||
content.append({"type": "text", "text": block})
|
content.append({"type": "text", "text": block})
|
||||||
elif isinstance(block, dict):
|
elif isinstance(block, dict):
|
||||||
if "type" not in block:
|
if "type" not in block:
|
||||||
raise ValueError("Dict content block must have a type key")
|
msg = "Dict content block must have a type key"
|
||||||
elif block["type"] == "image_url":
|
raise ValueError(msg)
|
||||||
|
if block["type"] == "image_url":
|
||||||
# convert format
|
# convert format
|
||||||
source = _format_image(block["image_url"]["url"])
|
source = _format_image(block["image_url"]["url"])
|
||||||
content.append({"type": "image", "source": source})
|
content.append({"type": "image", "source": source})
|
||||||
@ -358,7 +370,9 @@ def _format_messages(
|
|||||||
if tc["id"] == block["id"]
|
if tc["id"] == block["id"]
|
||||||
]
|
]
|
||||||
content.extend(
|
content.extend(
|
||||||
_lc_tool_calls_to_anthropic_tool_use_blocks(overlapping)
|
_lc_tool_calls_to_anthropic_tool_use_blocks(
|
||||||
|
overlapping,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
block.pop("text", None)
|
block.pop("text", None)
|
||||||
@ -398,7 +412,7 @@ def _format_messages(
|
|||||||
for k, v in block.items()
|
for k, v in block.items()
|
||||||
if k
|
if k
|
||||||
in ("type", "text", "cache_control", "citations")
|
in ("type", "text", "cache_control", "citations")
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
elif block["type"] == "thinking":
|
elif block["type"] == "thinking":
|
||||||
content.append(
|
content.append(
|
||||||
@ -407,7 +421,7 @@ def _format_messages(
|
|||||||
for k, v in block.items()
|
for k, v in block.items()
|
||||||
if k
|
if k
|
||||||
in ("type", "thinking", "cache_control", "signature")
|
in ("type", "thinking", "cache_control", "signature")
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
elif block["type"] == "redacted_thinking":
|
elif block["type"] == "redacted_thinking":
|
||||||
content.append(
|
content.append(
|
||||||
@ -415,13 +429,13 @@ def _format_messages(
|
|||||||
k: v
|
k: v
|
||||||
for k, v in block.items()
|
for k, v in block.items()
|
||||||
if k in ("type", "cache_control", "data")
|
if k in ("type", "cache_control", "data")
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
elif block["type"] == "tool_result":
|
elif block["type"] == "tool_result":
|
||||||
tool_content = _format_messages(
|
tool_content = _format_messages(
|
||||||
[HumanMessage(block["content"])]
|
[HumanMessage(block["content"])],
|
||||||
)[1][0]["content"]
|
)[1][0]["content"]
|
||||||
content.append({**block, **{"content": tool_content}})
|
content.append({**block, "content": tool_content})
|
||||||
elif block["type"] in (
|
elif block["type"] in (
|
||||||
"code_execution_tool_result",
|
"code_execution_tool_result",
|
||||||
"mcp_tool_result",
|
"mcp_tool_result",
|
||||||
@ -439,15 +453,18 @@ def _format_messages(
|
|||||||
"is_error", # for mcp_tool_result
|
"is_error", # for mcp_tool_result
|
||||||
"cache_control",
|
"cache_control",
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
content.append(block)
|
content.append(block)
|
||||||
else:
|
else:
|
||||||
raise ValueError(
|
msg = (
|
||||||
f"Content blocks must be str or dict, instead was: "
|
f"Content blocks must be str or dict, instead was: "
|
||||||
f"{type(block)}"
|
f"{type(block)}"
|
||||||
)
|
)
|
||||||
|
raise ValueError(
|
||||||
|
msg,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
content = message.content
|
content = message.content
|
||||||
|
|
||||||
@ -468,7 +485,7 @@ def _format_messages(
|
|||||||
tc for tc in message.tool_calls if tc["id"] not in tool_use_ids
|
tc for tc in message.tool_calls if tc["id"] not in tool_use_ids
|
||||||
]
|
]
|
||||||
cast(list, content).extend(
|
cast(list, content).extend(
|
||||||
_lc_tool_calls_to_anthropic_tool_use_blocks(missing_tool_calls)
|
_lc_tool_calls_to_anthropic_tool_use_blocks(missing_tool_calls),
|
||||||
)
|
)
|
||||||
|
|
||||||
formatted_messages.append({"role": role, "content": content})
|
formatted_messages.append({"role": role, "content": content})
|
||||||
@ -481,8 +498,7 @@ def _handle_anthropic_bad_request(e: anthropic.BadRequestError) -> None:
|
|||||||
message = "Received only system message(s). "
|
message = "Received only system message(s). "
|
||||||
warnings.warn(message)
|
warnings.warn(message)
|
||||||
raise e
|
raise e
|
||||||
else:
|
raise
|
||||||
raise
|
|
||||||
|
|
||||||
|
|
||||||
class ChatAnthropic(BaseChatModel):
|
class ChatAnthropic(BaseChatModel):
|
||||||
@ -635,17 +651,17 @@ class ChatAnthropic(BaseChatModel):
|
|||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
[{'name': 'GetWeather',
|
[{'name': 'GetWeather',
|
||||||
'args': {'location': 'Los Angeles, CA'},
|
'args': {'location': 'Los Angeles, CA'},
|
||||||
'id': 'toolu_01KzpPEAgzura7hpBqwHbWdo'},
|
'id': 'toolu_01KzpPEAgzura7hpBqwHbWdo'},
|
||||||
{'name': 'GetWeather',
|
{'name': 'GetWeather',
|
||||||
'args': {'location': 'New York, NY'},
|
'args': {'location': 'New York, NY'},
|
||||||
'id': 'toolu_01JtgbVGVJbiSwtZk3Uycezx'},
|
'id': 'toolu_01JtgbVGVJbiSwtZk3Uycezx'},
|
||||||
{'name': 'GetPopulation',
|
{'name': 'GetPopulation',
|
||||||
'args': {'location': 'Los Angeles, CA'},
|
'args': {'location': 'Los Angeles, CA'},
|
||||||
'id': 'toolu_01429aygngesudV9nTbCKGuw'},
|
'id': 'toolu_01429aygngesudV9nTbCKGuw'},
|
||||||
{'name': 'GetPopulation',
|
{'name': 'GetPopulation',
|
||||||
'args': {'location': 'New York, NY'},
|
'args': {'location': 'New York, NY'},
|
||||||
'id': 'toolu_01JPktyd44tVMeBcPPnFSEJG'}]
|
'id': 'toolu_01JPktyd44tVMeBcPPnFSEJG'}]
|
||||||
|
|
||||||
See ``ChatAnthropic.bind_tools()`` method for more.
|
See ``ChatAnthropic.bind_tools()`` method for more.
|
||||||
|
|
||||||
@ -673,7 +689,7 @@ class ChatAnthropic(BaseChatModel):
|
|||||||
See ``ChatAnthropic.with_structured_output()`` for more.
|
See ``ChatAnthropic.with_structured_output()`` for more.
|
||||||
|
|
||||||
Image input:
|
Image input:
|
||||||
See `multimodal guides <https://python.langchain.com/docs/how_to/multimodal_inputs/>`_
|
See `multimodal guides <https://python.langchain.com/docs/how_to/multimodal_inputs/>`__
|
||||||
for more detail.
|
for more detail.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
@ -717,7 +733,7 @@ class ChatAnthropic(BaseChatModel):
|
|||||||
.. dropdown:: Files API
|
.. dropdown:: Files API
|
||||||
|
|
||||||
You can also pass in files that are managed through Anthropic's
|
You can also pass in files that are managed through Anthropic's
|
||||||
`Files API <https://docs.anthropic.com/en/docs/build-with-claude/files>`_:
|
`Files API <https://docs.anthropic.com/en/docs/build-with-claude/files>`__:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
@ -744,7 +760,7 @@ class ChatAnthropic(BaseChatModel):
|
|||||||
llm.invoke([input_message])
|
llm.invoke([input_message])
|
||||||
|
|
||||||
PDF input:
|
PDF input:
|
||||||
See `multimodal guides <https://python.langchain.com/docs/how_to/multimodal_inputs/>`_
|
See `multimodal guides <https://python.langchain.com/docs/how_to/multimodal_inputs/>`__
|
||||||
for more detail.
|
for more detail.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
@ -782,7 +798,7 @@ class ChatAnthropic(BaseChatModel):
|
|||||||
.. dropdown:: Files API
|
.. dropdown:: Files API
|
||||||
|
|
||||||
You can also pass in files that are managed through Anthropic's
|
You can also pass in files that are managed through Anthropic's
|
||||||
`Files API <https://docs.anthropic.com/en/docs/build-with-claude/files>`_:
|
`Files API <https://docs.anthropic.com/en/docs/build-with-claude/files>`__:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
@ -810,7 +826,7 @@ class ChatAnthropic(BaseChatModel):
|
|||||||
|
|
||||||
Extended thinking:
|
Extended thinking:
|
||||||
Claude 3.7 Sonnet supports an
|
Claude 3.7 Sonnet supports an
|
||||||
`extended thinking <https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking>`_
|
`extended thinking <https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking>`__
|
||||||
feature, which will output the step-by-step reasoning process that led to its
|
feature, which will output the step-by-step reasoning process that led to its
|
||||||
final answer.
|
final answer.
|
||||||
|
|
||||||
@ -838,10 +854,10 @@ class ChatAnthropic(BaseChatModel):
|
|||||||
|
|
||||||
Citations:
|
Citations:
|
||||||
Anthropic supports a
|
Anthropic supports a
|
||||||
`citations <https://docs.anthropic.com/en/docs/build-with-claude/citations>`_
|
`citations <https://docs.anthropic.com/en/docs/build-with-claude/citations>`__
|
||||||
feature that lets Claude attach context to its answers based on source
|
feature that lets Claude attach context to its answers based on source
|
||||||
documents supplied by the user. When
|
documents supplied by the user. When
|
||||||
`document content blocks <https://docs.anthropic.com/en/docs/build-with-claude/citations#document-types>`_
|
`document content blocks <https://docs.anthropic.com/en/docs/build-with-claude/citations#document-types>`__
|
||||||
with ``"citations": {"enabled": True}`` are included in a query, Claude may
|
with ``"citations": {"enabled": True}`` are included in a query, Claude may
|
||||||
generate citations in its response.
|
generate citations in its response.
|
||||||
|
|
||||||
@ -924,7 +940,7 @@ class ChatAnthropic(BaseChatModel):
|
|||||||
or by setting ``stream_usage=False`` when initializing ChatAnthropic.
|
or by setting ``stream_usage=False`` when initializing ChatAnthropic.
|
||||||
|
|
||||||
Prompt caching:
|
Prompt caching:
|
||||||
See LangChain `docs <https://python.langchain.com/docs/integrations/chat/anthropic/#built-in-tools>`_
|
See LangChain `docs <https://python.langchain.com/docs/integrations/chat/anthropic/#built-in-tools>`__
|
||||||
for more detail.
|
for more detail.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
@ -1000,11 +1016,11 @@ class ChatAnthropic(BaseChatModel):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
See `Claude documentation <https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching#1-hour-cache-duration-beta>`_
|
See `Claude documentation <https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching#1-hour-cache-duration-beta>`__
|
||||||
for detail.
|
for detail.
|
||||||
|
|
||||||
Token-efficient tool use (beta):
|
Token-efficient tool use (beta):
|
||||||
See LangChain `docs <https://python.langchain.com/docs/integrations/chat/anthropic/>`_
|
See LangChain `docs <https://python.langchain.com/docs/integrations/chat/anthropic/>`__
|
||||||
for more detail.
|
for more detail.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
@ -1041,7 +1057,7 @@ class ChatAnthropic(BaseChatModel):
|
|||||||
Total tokens: 408
|
Total tokens: 408
|
||||||
|
|
||||||
Built-in tools:
|
Built-in tools:
|
||||||
See LangChain `docs <https://python.langchain.com/docs/integrations/chat/anthropic/>`_
|
See LangChain `docs <https://python.langchain.com/docs/integrations/chat/anthropic/>`__
|
||||||
for more detail.
|
for more detail.
|
||||||
|
|
||||||
.. dropdown:: Web search
|
.. dropdown:: Web search
|
||||||
@ -1266,7 +1282,9 @@ class ChatAnthropic(BaseChatModel):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _get_ls_params(
|
def _get_ls_params(
|
||||||
self, stop: Optional[list[str]] = None, **kwargs: Any
|
self,
|
||||||
|
stop: Optional[list[str]] = None,
|
||||||
|
**kwargs: Any,
|
||||||
) -> LangSmithParams:
|
) -> LangSmithParams:
|
||||||
"""Get standard params for tracing."""
|
"""Get standard params for tracing."""
|
||||||
params = self._get_invocation_params(stop=stop, **kwargs)
|
params = self._get_invocation_params(stop=stop, **kwargs)
|
||||||
@ -1286,8 +1304,7 @@ class ChatAnthropic(BaseChatModel):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def build_extra(cls, values: dict) -> Any:
|
def build_extra(cls, values: dict) -> Any:
|
||||||
all_required_field_names = get_pydantic_field_names(cls)
|
all_required_field_names = get_pydantic_field_names(cls)
|
||||||
values = _build_model_kwargs(values, all_required_field_names)
|
return _build_model_kwargs(values, all_required_field_names)
|
||||||
return values
|
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def _client_params(self) -> dict[str, Any]:
|
def _client_params(self) -> dict[str, Any]:
|
||||||
@ -1361,14 +1378,12 @@ class ChatAnthropic(BaseChatModel):
|
|||||||
def _create(self, payload: dict) -> Any:
|
def _create(self, payload: dict) -> Any:
|
||||||
if "betas" in payload:
|
if "betas" in payload:
|
||||||
return self._client.beta.messages.create(**payload)
|
return self._client.beta.messages.create(**payload)
|
||||||
else:
|
return self._client.messages.create(**payload)
|
||||||
return self._client.messages.create(**payload)
|
|
||||||
|
|
||||||
async def _acreate(self, payload: dict) -> Any:
|
async def _acreate(self, payload: dict) -> Any:
|
||||||
if "betas" in payload:
|
if "betas" in payload:
|
||||||
return await self._async_client.beta.messages.create(**payload)
|
return await self._async_client.beta.messages.create(**payload)
|
||||||
else:
|
return await self._async_client.messages.create(**payload)
|
||||||
return await self._async_client.messages.create(**payload)
|
|
||||||
|
|
||||||
def _stream(
|
def _stream(
|
||||||
self,
|
self,
|
||||||
@ -1496,7 +1511,10 @@ class ChatAnthropic(BaseChatModel):
|
|||||||
) -> ChatResult:
|
) -> ChatResult:
|
||||||
if self.streaming:
|
if self.streaming:
|
||||||
stream_iter = self._stream(
|
stream_iter = self._stream(
|
||||||
messages, stop=stop, run_manager=run_manager, **kwargs
|
messages,
|
||||||
|
stop=stop,
|
||||||
|
run_manager=run_manager,
|
||||||
|
**kwargs,
|
||||||
)
|
)
|
||||||
return generate_from_stream(stream_iter)
|
return generate_from_stream(stream_iter)
|
||||||
payload = self._get_request_payload(messages, stop=stop, **kwargs)
|
payload = self._get_request_payload(messages, stop=stop, **kwargs)
|
||||||
@ -1515,7 +1533,10 @@ class ChatAnthropic(BaseChatModel):
|
|||||||
) -> ChatResult:
|
) -> ChatResult:
|
||||||
if self.streaming:
|
if self.streaming:
|
||||||
stream_iter = self._astream(
|
stream_iter = self._astream(
|
||||||
messages, stop=stop, run_manager=run_manager, **kwargs
|
messages,
|
||||||
|
stop=stop,
|
||||||
|
run_manager=run_manager,
|
||||||
|
**kwargs,
|
||||||
)
|
)
|
||||||
return await agenerate_from_stream(stream_iter)
|
return await agenerate_from_stream(stream_iter)
|
||||||
payload = self._get_request_payload(messages, stop=stop, **kwargs)
|
payload = self._get_request_payload(messages, stop=stop, **kwargs)
|
||||||
@ -1558,7 +1579,7 @@ class ChatAnthropic(BaseChatModel):
|
|||||||
tools: Sequence[Union[dict[str, Any], type, Callable, BaseTool]],
|
tools: Sequence[Union[dict[str, Any], type, Callable, BaseTool]],
|
||||||
*,
|
*,
|
||||||
tool_choice: Optional[
|
tool_choice: Optional[
|
||||||
Union[dict[str, str], Literal["any", "auto"], str]
|
Union[dict[str, str], Literal["any", "auto"], str] # noqa: PYI051
|
||||||
] = None,
|
] = None,
|
||||||
parallel_tool_calls: Optional[bool] = None,
|
parallel_tool_calls: Optional[bool] = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
@ -1716,10 +1737,13 @@ class ChatAnthropic(BaseChatModel):
|
|||||||
elif isinstance(tool_choice, str):
|
elif isinstance(tool_choice, str):
|
||||||
kwargs["tool_choice"] = {"type": "tool", "name": tool_choice}
|
kwargs["tool_choice"] = {"type": "tool", "name": tool_choice}
|
||||||
else:
|
else:
|
||||||
raise ValueError(
|
msg = (
|
||||||
f"Unrecognized 'tool_choice' type {tool_choice=}. Expected dict, "
|
f"Unrecognized 'tool_choice' type {tool_choice=}. Expected dict, "
|
||||||
f"str, or None."
|
f"str, or None."
|
||||||
)
|
)
|
||||||
|
raise ValueError(
|
||||||
|
msg,
|
||||||
|
)
|
||||||
|
|
||||||
if parallel_tool_calls is not None:
|
if parallel_tool_calls is not None:
|
||||||
disable_parallel_tool_use = not parallel_tool_calls
|
disable_parallel_tool_use = not parallel_tool_calls
|
||||||
@ -1861,7 +1885,8 @@ class ChatAnthropic(BaseChatModel):
|
|||||||
tool_name = formatted_tool["name"]
|
tool_name = formatted_tool["name"]
|
||||||
if self.thinking is not None and self.thinking.get("type") == "enabled":
|
if self.thinking is not None and self.thinking.get("type") == "enabled":
|
||||||
llm = self._get_llm_for_structured_output_when_thinking_is_enabled(
|
llm = self._get_llm_for_structured_output_when_thinking_is_enabled(
|
||||||
schema, formatted_tool
|
schema,
|
||||||
|
formatted_tool,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
llm = self.bind_tools(
|
llm = self.bind_tools(
|
||||||
@ -1875,24 +1900,27 @@ class ChatAnthropic(BaseChatModel):
|
|||||||
|
|
||||||
if isinstance(schema, type) and is_basemodel_subclass(schema):
|
if isinstance(schema, type) and is_basemodel_subclass(schema):
|
||||||
output_parser: OutputParserLike = PydanticToolsParser(
|
output_parser: OutputParserLike = PydanticToolsParser(
|
||||||
tools=[schema], first_tool_only=True
|
tools=[schema],
|
||||||
|
first_tool_only=True,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
output_parser = JsonOutputKeyToolsParser(
|
output_parser = JsonOutputKeyToolsParser(
|
||||||
key_name=tool_name, first_tool_only=True
|
key_name=tool_name,
|
||||||
|
first_tool_only=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
if include_raw:
|
if include_raw:
|
||||||
parser_assign = RunnablePassthrough.assign(
|
parser_assign = RunnablePassthrough.assign(
|
||||||
parsed=itemgetter("raw") | output_parser, parsing_error=lambda _: None
|
parsed=itemgetter("raw") | output_parser,
|
||||||
|
parsing_error=lambda _: None,
|
||||||
)
|
)
|
||||||
parser_none = RunnablePassthrough.assign(parsed=lambda _: None)
|
parser_none = RunnablePassthrough.assign(parsed=lambda _: None)
|
||||||
parser_with_fallback = parser_assign.with_fallbacks(
|
parser_with_fallback = parser_assign.with_fallbacks(
|
||||||
[parser_none], exception_key="parsing_error"
|
[parser_none],
|
||||||
|
exception_key="parsing_error",
|
||||||
)
|
)
|
||||||
return RunnableMap(raw=llm) | parser_with_fallback
|
return RunnableMap(raw=llm) | parser_with_fallback
|
||||||
else:
|
return llm | output_parser
|
||||||
return llm | output_parser
|
|
||||||
|
|
||||||
@beta()
|
@beta()
|
||||||
def get_num_tokens_from_messages(
|
def get_num_tokens_from_messages(
|
||||||
@ -1909,6 +1937,8 @@ class ChatAnthropic(BaseChatModel):
|
|||||||
messages: The message inputs to tokenize.
|
messages: The message inputs to tokenize.
|
||||||
tools: If provided, sequence of dict, BaseModel, function, or BaseTools
|
tools: If provided, sequence of dict, BaseModel, function, or BaseTools
|
||||||
to be converted to tool schemas.
|
to be converted to tool schemas.
|
||||||
|
kwargs: Additional keyword arguments are passed to the
|
||||||
|
:meth:`~langchain_anthropic.chat_models.ChatAnthropic.bind` method.
|
||||||
|
|
||||||
Basic usage:
|
Basic usage:
|
||||||
|
|
||||||
@ -1985,7 +2015,7 @@ def convert_to_anthropic_tool(
|
|||||||
if isinstance(tool, dict) and all(
|
if isinstance(tool, dict) and all(
|
||||||
k in tool for k in ("name", "description", "input_schema")
|
k in tool for k in ("name", "description", "input_schema")
|
||||||
):
|
):
|
||||||
anthropic_formatted = AnthropicTool(tool) # type: ignore
|
anthropic_formatted = AnthropicTool(tool) # type: ignore[misc]
|
||||||
else:
|
else:
|
||||||
oai_formatted = convert_to_openai_tool(tool)["function"]
|
oai_formatted = convert_to_openai_tool(tool)["function"]
|
||||||
anthropic_formatted = AnthropicTool(
|
anthropic_formatted = AnthropicTool(
|
||||||
@ -2032,17 +2062,15 @@ class _AnthropicToolUse(TypedDict):
|
|||||||
def _lc_tool_calls_to_anthropic_tool_use_blocks(
|
def _lc_tool_calls_to_anthropic_tool_use_blocks(
|
||||||
tool_calls: list[ToolCall],
|
tool_calls: list[ToolCall],
|
||||||
) -> list[_AnthropicToolUse]:
|
) -> list[_AnthropicToolUse]:
|
||||||
blocks = []
|
return [
|
||||||
for tool_call in tool_calls:
|
_AnthropicToolUse(
|
||||||
blocks.append(
|
type="tool_use",
|
||||||
_AnthropicToolUse(
|
name=tool_call["name"],
|
||||||
type="tool_use",
|
input=tool_call["args"],
|
||||||
name=tool_call["name"],
|
id=cast(str, tool_call["id"]),
|
||||||
input=tool_call["args"],
|
|
||||||
id=cast(str, tool_call["id"]),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
return blocks
|
for tool_call in tool_calls
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def _make_message_chunk_from_anthropic_event(
|
def _make_message_chunk_from_anthropic_event(
|
||||||
@ -2107,7 +2135,7 @@ def _make_message_chunk_from_anthropic_event(
|
|||||||
tool_call_chunks = []
|
tool_call_chunks = []
|
||||||
message_chunk = AIMessageChunk(
|
message_chunk = AIMessageChunk(
|
||||||
content=[content_block],
|
content=[content_block],
|
||||||
tool_call_chunks=tool_call_chunks, # type: ignore
|
tool_call_chunks=tool_call_chunks,
|
||||||
)
|
)
|
||||||
block_start_event = event
|
block_start_event = event
|
||||||
elif event.type == "content_block_delta":
|
elif event.type == "content_block_delta":
|
||||||
@ -2122,14 +2150,10 @@ def _make_message_chunk_from_anthropic_event(
|
|||||||
if "citation" in content_block:
|
if "citation" in content_block:
|
||||||
content_block["citations"] = [content_block.pop("citation")]
|
content_block["citations"] = [content_block.pop("citation")]
|
||||||
message_chunk = AIMessageChunk(content=[content_block])
|
message_chunk = AIMessageChunk(content=[content_block])
|
||||||
elif event.delta.type == "thinking_delta":
|
elif (
|
||||||
content_block = event.delta.model_dump()
|
event.delta.type == "thinking_delta"
|
||||||
if "text" in content_block and content_block["text"] is None:
|
or event.delta.type == "signature_delta"
|
||||||
content_block.pop("text")
|
):
|
||||||
content_block["index"] = event.index
|
|
||||||
content_block["type"] = "thinking"
|
|
||||||
message_chunk = AIMessageChunk(content=[content_block])
|
|
||||||
elif event.delta.type == "signature_delta":
|
|
||||||
content_block = event.delta.model_dump()
|
content_block = event.delta.model_dump()
|
||||||
if "text" in content_block and content_block["text"] is None:
|
if "text" in content_block and content_block["text"] is None:
|
||||||
content_block.pop("text")
|
content_block.pop("text")
|
||||||
@ -2155,7 +2179,7 @@ def _make_message_chunk_from_anthropic_event(
|
|||||||
tool_call_chunks = []
|
tool_call_chunks = []
|
||||||
message_chunk = AIMessageChunk(
|
message_chunk = AIMessageChunk(
|
||||||
content=[content_block],
|
content=[content_block],
|
||||||
tool_call_chunks=tool_call_chunks, # type: ignore
|
tool_call_chunks=tool_call_chunks,
|
||||||
)
|
)
|
||||||
elif event.type == "message_delta" and stream_usage:
|
elif event.type == "message_delta" and stream_usage:
|
||||||
usage_metadata = UsageMetadata(
|
usage_metadata = UsageMetadata(
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
Any,
|
||||||
@ -66,7 +68,7 @@ def get_system_message(tools: list[dict]) -> str:
|
|||||||
parameter_description=parameter.get("description"),
|
parameter_description=parameter.get("description"),
|
||||||
)
|
)
|
||||||
for name, parameter in tool["parameters"]["properties"].items()
|
for name, parameter in tool["parameters"]["properties"].items()
|
||||||
]
|
],
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
for tool in tools
|
for tool in tools
|
||||||
@ -79,7 +81,7 @@ def get_system_message(tools: list[dict]) -> str:
|
|||||||
formatted_parameters=tool["formatted_parameters"],
|
formatted_parameters=tool["formatted_parameters"],
|
||||||
)
|
)
|
||||||
for tool in tools_data
|
for tool in tools_data
|
||||||
]
|
],
|
||||||
)
|
)
|
||||||
return SYSTEM_PROMPT_FORMAT.format(formatted_tools=tools_formatted)
|
return SYSTEM_PROMPT_FORMAT.format(formatted_tools=tools_formatted)
|
||||||
|
|
||||||
@ -111,18 +113,20 @@ def _xml_to_function_call(invoke: Any, tools: list[dict]) -> dict[str, Any]:
|
|||||||
if len(filtered_tools) > 0 and not isinstance(arguments, str):
|
if len(filtered_tools) > 0 and not isinstance(arguments, str):
|
||||||
tool = filtered_tools[0]
|
tool = filtered_tools[0]
|
||||||
for key, value in arguments.items():
|
for key, value in arguments.items():
|
||||||
if key in tool["parameters"]["properties"]:
|
if (
|
||||||
if "type" in tool["parameters"]["properties"][key]:
|
key in tool["parameters"]["properties"]
|
||||||
if tool["parameters"]["properties"][key][
|
and "type" in tool["parameters"]["properties"][key]
|
||||||
"type"
|
):
|
||||||
] == "array" and not isinstance(value, list):
|
if tool["parameters"]["properties"][key][
|
||||||
arguments[key] = [value]
|
"type"
|
||||||
if (
|
] == "array" and not isinstance(value, list):
|
||||||
tool["parameters"]["properties"][key]["type"] != "object"
|
arguments[key] = [value]
|
||||||
and isinstance(value, dict)
|
if (
|
||||||
and len(value.keys()) == 1
|
tool["parameters"]["properties"][key]["type"] != "object"
|
||||||
):
|
and isinstance(value, dict)
|
||||||
arguments[key] = list(value.values())[0]
|
and len(value.keys()) == 1
|
||||||
|
):
|
||||||
|
arguments[key] = next(iter(value.values()))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"function": {
|
"function": {
|
||||||
@ -134,9 +138,7 @@ def _xml_to_function_call(invoke: Any, tools: list[dict]) -> dict[str, Any]:
|
|||||||
|
|
||||||
|
|
||||||
def _xml_to_tool_calls(elem: Any, tools: list[dict]) -> list[dict[str, Any]]:
|
def _xml_to_tool_calls(elem: Any, tools: list[dict]) -> list[dict[str, Any]]:
|
||||||
"""
|
"""Convert an XML element and its children into a dictionary of dictionaries."""
|
||||||
Convert an XML element and its children into a dictionary of dictionaries.
|
|
||||||
"""
|
|
||||||
invokes = elem.findall("invoke")
|
invokes = elem.findall("invoke")
|
||||||
|
|
||||||
return [_xml_to_function_call(invoke, tools) for invoke in invokes]
|
return [_xml_to_function_call(invoke, tools) for invoke in invokes]
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import warnings
|
import warnings
|
||||||
from collections.abc import AsyncIterator, Iterator, Mapping
|
from collections.abc import AsyncIterator, Iterator, Mapping
|
||||||
@ -85,8 +87,7 @@ class _AnthropicCommon(BaseLanguageModel):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def build_extra(cls, values: dict) -> Any:
|
def build_extra(cls, values: dict) -> Any:
|
||||||
all_required_field_names = get_pydantic_field_names(cls)
|
all_required_field_names = get_pydantic_field_names(cls)
|
||||||
values = _build_model_kwargs(values, all_required_field_names)
|
return _build_model_kwargs(values, all_required_field_names)
|
||||||
return values
|
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def validate_environment(self) -> Self:
|
def validate_environment(self) -> Self:
|
||||||
@ -125,11 +126,12 @@ class _AnthropicCommon(BaseLanguageModel):
|
|||||||
@property
|
@property
|
||||||
def _identifying_params(self) -> Mapping[str, Any]:
|
def _identifying_params(self) -> Mapping[str, Any]:
|
||||||
"""Get the identifying parameters."""
|
"""Get the identifying parameters."""
|
||||||
return {**{}, **self._default_params}
|
return {**self._default_params}
|
||||||
|
|
||||||
def _get_anthropic_stop(self, stop: Optional[list[str]] = None) -> list[str]:
|
def _get_anthropic_stop(self, stop: Optional[list[str]] = None) -> list[str]:
|
||||||
if not self.HUMAN_PROMPT or not self.AI_PROMPT:
|
if not self.HUMAN_PROMPT or not self.AI_PROMPT:
|
||||||
raise NameError("Please ensure the anthropic package is loaded")
|
msg = "Please ensure the anthropic package is loaded"
|
||||||
|
raise NameError(msg)
|
||||||
|
|
||||||
if stop is None:
|
if stop is None:
|
||||||
stop = []
|
stop = []
|
||||||
@ -152,6 +154,7 @@ class AnthropicLLM(LLM, _AnthropicCommon):
|
|||||||
from langchain_anthropic import AnthropicLLM
|
from langchain_anthropic import AnthropicLLM
|
||||||
|
|
||||||
model = AnthropicLLM()
|
model = AnthropicLLM()
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_config = ConfigDict(
|
model_config = ConfigDict(
|
||||||
@ -166,7 +169,7 @@ class AnthropicLLM(LLM, _AnthropicCommon):
|
|||||||
warnings.warn(
|
warnings.warn(
|
||||||
"This Anthropic LLM is deprecated. "
|
"This Anthropic LLM is deprecated. "
|
||||||
"Please use `from langchain_anthropic import ChatAnthropic` "
|
"Please use `from langchain_anthropic import ChatAnthropic` "
|
||||||
"instead"
|
"instead",
|
||||||
)
|
)
|
||||||
return values
|
return values
|
||||||
|
|
||||||
@ -199,7 +202,9 @@ class AnthropicLLM(LLM, _AnthropicCommon):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _get_ls_params(
|
def _get_ls_params(
|
||||||
self, stop: Optional[list[str]] = None, **kwargs: Any
|
self,
|
||||||
|
stop: Optional[list[str]] = None,
|
||||||
|
**kwargs: Any,
|
||||||
) -> LangSmithParams:
|
) -> LangSmithParams:
|
||||||
"""Get standard params for tracing."""
|
"""Get standard params for tracing."""
|
||||||
params = super()._get_ls_params(stop=stop, **kwargs)
|
params = super()._get_ls_params(stop=stop, **kwargs)
|
||||||
@ -213,7 +218,8 @@ class AnthropicLLM(LLM, _AnthropicCommon):
|
|||||||
|
|
||||||
def _wrap_prompt(self, prompt: str) -> str:
|
def _wrap_prompt(self, prompt: str) -> str:
|
||||||
if not self.HUMAN_PROMPT or not self.AI_PROMPT:
|
if not self.HUMAN_PROMPT or not self.AI_PROMPT:
|
||||||
raise NameError("Please ensure the anthropic package is loaded")
|
msg = "Please ensure the anthropic package is loaded"
|
||||||
|
raise NameError(msg)
|
||||||
|
|
||||||
if prompt.startswith(self.HUMAN_PROMPT):
|
if prompt.startswith(self.HUMAN_PROMPT):
|
||||||
return prompt # Already wrapped.
|
return prompt # Already wrapped.
|
||||||
@ -238,6 +244,8 @@ class AnthropicLLM(LLM, _AnthropicCommon):
|
|||||||
Args:
|
Args:
|
||||||
prompt: The prompt to pass into the model.
|
prompt: The prompt to pass into the model.
|
||||||
stop: Optional list of stop words to use when generating.
|
stop: Optional list of stop words to use when generating.
|
||||||
|
run_manager: Optional callback manager for LLM run.
|
||||||
|
kwargs: Additional keyword arguments to pass to the model.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The string generated by the model.
|
The string generated by the model.
|
||||||
@ -253,7 +261,10 @@ class AnthropicLLM(LLM, _AnthropicCommon):
|
|||||||
if self.streaming:
|
if self.streaming:
|
||||||
completion = ""
|
completion = ""
|
||||||
for chunk in self._stream(
|
for chunk in self._stream(
|
||||||
prompt=prompt, stop=stop, run_manager=run_manager, **kwargs
|
prompt=prompt,
|
||||||
|
stop=stop,
|
||||||
|
run_manager=run_manager,
|
||||||
|
**kwargs,
|
||||||
):
|
):
|
||||||
completion += chunk.text
|
completion += chunk.text
|
||||||
return completion
|
return completion
|
||||||
@ -281,7 +292,10 @@ class AnthropicLLM(LLM, _AnthropicCommon):
|
|||||||
if self.streaming:
|
if self.streaming:
|
||||||
completion = ""
|
completion = ""
|
||||||
async for chunk in self._astream(
|
async for chunk in self._astream(
|
||||||
prompt=prompt, stop=stop, run_manager=run_manager, **kwargs
|
prompt=prompt,
|
||||||
|
stop=stop,
|
||||||
|
run_manager=run_manager,
|
||||||
|
**kwargs,
|
||||||
):
|
):
|
||||||
completion += chunk.text
|
completion += chunk.text
|
||||||
return completion
|
return completion
|
||||||
@ -308,8 +322,12 @@ class AnthropicLLM(LLM, _AnthropicCommon):
|
|||||||
Args:
|
Args:
|
||||||
prompt: The prompt to pass into the model.
|
prompt: The prompt to pass into the model.
|
||||||
stop: Optional list of stop words to use when generating.
|
stop: Optional list of stop words to use when generating.
|
||||||
|
run_manager: Optional callback manager for LLM run.
|
||||||
|
kwargs: Additional keyword arguments to pass to the model.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A generator representing the stream of tokens from Anthropic.
|
A generator representing the stream of tokens from Anthropic.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
@ -319,12 +337,16 @@ class AnthropicLLM(LLM, _AnthropicCommon):
|
|||||||
generator = anthropic.stream(prompt)
|
generator = anthropic.stream(prompt)
|
||||||
for token in generator:
|
for token in generator:
|
||||||
yield token
|
yield token
|
||||||
|
|
||||||
"""
|
"""
|
||||||
stop = self._get_anthropic_stop(stop)
|
stop = self._get_anthropic_stop(stop)
|
||||||
params = {**self._default_params, **kwargs}
|
params = {**self._default_params, **kwargs}
|
||||||
|
|
||||||
for token in self.client.completions.create(
|
for token in self.client.completions.create(
|
||||||
prompt=self._wrap_prompt(prompt), stop_sequences=stop, stream=True, **params
|
prompt=self._wrap_prompt(prompt),
|
||||||
|
stop_sequences=stop,
|
||||||
|
stream=True,
|
||||||
|
**params,
|
||||||
):
|
):
|
||||||
chunk = GenerationChunk(text=token.completion)
|
chunk = GenerationChunk(text=token.completion)
|
||||||
|
|
||||||
@ -344,8 +366,12 @@ class AnthropicLLM(LLM, _AnthropicCommon):
|
|||||||
Args:
|
Args:
|
||||||
prompt: The prompt to pass into the model.
|
prompt: The prompt to pass into the model.
|
||||||
stop: Optional list of stop words to use when generating.
|
stop: Optional list of stop words to use when generating.
|
||||||
|
run_manager: Optional callback manager for LLM run.
|
||||||
|
kwargs: Additional keyword arguments to pass to the model.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A generator representing the stream of tokens from Anthropic.
|
A generator representing the stream of tokens from Anthropic.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
@ -354,6 +380,7 @@ class AnthropicLLM(LLM, _AnthropicCommon):
|
|||||||
generator = anthropic.stream(prompt)
|
generator = anthropic.stream(prompt)
|
||||||
for token in generator:
|
for token in generator:
|
||||||
yield token
|
yield token
|
||||||
|
|
||||||
"""
|
"""
|
||||||
stop = self._get_anthropic_stop(stop)
|
stop = self._get_anthropic_stop(stop)
|
||||||
params = {**self._default_params, **kwargs}
|
params = {**self._default_params, **kwargs}
|
||||||
@ -372,15 +399,16 @@ class AnthropicLLM(LLM, _AnthropicCommon):
|
|||||||
|
|
||||||
def get_num_tokens(self, text: str) -> int:
|
def get_num_tokens(self, text: str) -> int:
|
||||||
"""Calculate number of tokens."""
|
"""Calculate number of tokens."""
|
||||||
raise NotImplementedError(
|
msg = (
|
||||||
"Anthropic's legacy count_tokens method was removed in anthropic 0.39.0 "
|
"Anthropic's legacy count_tokens method was removed in anthropic 0.39.0 "
|
||||||
"and langchain-anthropic 0.3.0. Please use "
|
"and langchain-anthropic 0.3.0. Please use "
|
||||||
"ChatAnthropic.get_num_tokens_from_messages instead."
|
"ChatAnthropic.get_num_tokens_from_messages instead."
|
||||||
)
|
)
|
||||||
|
raise NotImplementedError(
|
||||||
|
msg,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@deprecated(since="0.1.0", removal="1.0.0", alternative="AnthropicLLM")
|
@deprecated(since="0.1.0", removal="1.0.0", alternative="AnthropicLLM")
|
||||||
class Anthropic(AnthropicLLM):
|
class Anthropic(AnthropicLLM):
|
||||||
"""Anthropic large language model."""
|
"""Anthropic large language model."""
|
||||||
|
|
||||||
pass
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Optional, Union, cast
|
from typing import Any, Optional, Union, cast
|
||||||
|
|
||||||
from langchain_core.messages import AIMessage, ToolCall
|
from langchain_core.messages import AIMessage, ToolCall
|
||||||
@ -27,8 +29,12 @@ class ToolsOutputParser(BaseGenerationOutputParser):
|
|||||||
Args:
|
Args:
|
||||||
result: A list of Generations to be parsed. The Generations are assumed
|
result: A list of Generations to be parsed. The Generations are assumed
|
||||||
to be different candidate outputs for a single model input.
|
to be different candidate outputs for a single model input.
|
||||||
|
partial: (Not used) Whether the result is a partial result. If True, the
|
||||||
|
parser may return a partial result, which may not be complete or valid.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Structured output.
|
Structured output.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if not result or not isinstance(result[0], ChatGeneration):
|
if not result or not isinstance(result[0], ChatGeneration):
|
||||||
return None if self.first_tool_only else []
|
return None if self.first_tool_only else []
|
||||||
@ -53,8 +59,7 @@ class ToolsOutputParser(BaseGenerationOutputParser):
|
|||||||
|
|
||||||
if self.first_tool_only:
|
if self.first_tool_only:
|
||||||
return tool_calls[0] if tool_calls else None
|
return tool_calls[0] if tool_calls else None
|
||||||
else:
|
return list(tool_calls)
|
||||||
return [tool_call for tool_call in tool_calls]
|
|
||||||
|
|
||||||
def _pydantic_parse(self, tool_call: dict) -> BaseModel:
|
def _pydantic_parse(self, tool_call: dict) -> BaseModel:
|
||||||
cls_ = {schema.__name__: schema for schema in self.pydantic_schemas or []}[
|
cls_ = {schema.__name__: schema for schema in self.pydantic_schemas or []}[
|
||||||
@ -80,8 +85,7 @@ def extract_tool_calls(content: Union[str, list[Union[str, dict]]]) -> list[Tool
|
|||||||
if block["type"] != "tool_use":
|
if block["type"] != "tool_use":
|
||||||
continue
|
continue
|
||||||
tool_calls.append(
|
tool_calls.append(
|
||||||
tool_call(name=block["name"], args=block["input"], id=block["id"])
|
tool_call(name=block["name"], args=block["input"], id=block["id"]),
|
||||||
)
|
)
|
||||||
return tool_calls
|
return tool_calls
|
||||||
else:
|
return []
|
||||||
return []
|
|
||||||
|
@ -60,8 +60,58 @@ plugins = ['pydantic.mypy']
|
|||||||
target-version = "py39"
|
target-version = "py39"
|
||||||
|
|
||||||
[tool.ruff.lint]
|
[tool.ruff.lint]
|
||||||
select = ["E", "F", "I", "T201", "UP", "S"]
|
select = [
|
||||||
ignore = [ "UP007", ]
|
"A", # flake8-builtins
|
||||||
|
"ASYNC", # flake8-async
|
||||||
|
"C4", # flake8-comprehensions
|
||||||
|
"COM", # flake8-commas
|
||||||
|
"D", # pydocstyle
|
||||||
|
"DOC", # pydoclint
|
||||||
|
"E", # pycodestyle error
|
||||||
|
"EM", # flake8-errmsg
|
||||||
|
"F", # pyflakes
|
||||||
|
"FA", # flake8-future-annotations
|
||||||
|
"FBT", # flake8-boolean-trap
|
||||||
|
"FLY", # flake8-flynt
|
||||||
|
"I", # isort
|
||||||
|
"ICN", # flake8-import-conventions
|
||||||
|
"INT", # flake8-gettext
|
||||||
|
"ISC", # isort-comprehensions
|
||||||
|
"PGH", # pygrep-hooks
|
||||||
|
"PIE", # flake8-pie
|
||||||
|
"PERF", # flake8-perf
|
||||||
|
"PYI", # flake8-pyi
|
||||||
|
"Q", # flake8-quotes
|
||||||
|
"RET", # flake8-return
|
||||||
|
"RSE", # flake8-rst-docstrings
|
||||||
|
"RUF", # ruff
|
||||||
|
"S", # flake8-bandit
|
||||||
|
"SLF", # flake8-self
|
||||||
|
"SLOT", # flake8-slots
|
||||||
|
"SIM", # flake8-simplify
|
||||||
|
"T10", # flake8-debugger
|
||||||
|
"T20", # flake8-print
|
||||||
|
"TID", # flake8-tidy-imports
|
||||||
|
"UP", # pyupgrade
|
||||||
|
"W", # pycodestyle warning
|
||||||
|
"YTT", # flake8-2020
|
||||||
|
]
|
||||||
|
ignore = [
|
||||||
|
"D100", # Missing docstring in public module
|
||||||
|
"D101", # Missing docstring in public class
|
||||||
|
"D102", # Missing docstring in public method
|
||||||
|
"D103", # Missing docstring in public function
|
||||||
|
"D104", # Missing docstring in public package
|
||||||
|
"D105", # Missing docstring in magic method
|
||||||
|
"D107", # Missing docstring in __init__
|
||||||
|
"D214", # Section over-indented, doesn't play well with reStructuredText
|
||||||
|
"COM812", # Messes with the formatter
|
||||||
|
"ISC001", # Messes with the formatter
|
||||||
|
"PERF203", # Rarely useful
|
||||||
|
"UP007", # non-pep604-annotation-union
|
||||||
|
"UP045", # non-pep604-annotation-optional
|
||||||
|
"SIM105", # Rarely useful
|
||||||
|
]
|
||||||
|
|
||||||
[tool.coverage.run]
|
[tool.coverage.run]
|
||||||
omit = ["tests/*"]
|
omit = ["tests/*"]
|
||||||
@ -78,4 +128,5 @@ asyncio_mode = "auto"
|
|||||||
"tests/**/*.py" = [
|
"tests/**/*.py" = [
|
||||||
"S101", # Tests need assertions
|
"S101", # Tests need assertions
|
||||||
"S311", # Standard pseudo-random generators are not suitable for cryptographic purposes
|
"S311", # Standard pseudo-random generators are not suitable for cryptographic purposes
|
||||||
|
"SLF001", # Private member access in tests
|
||||||
]
|
]
|
||||||
|
@ -20,9 +20,7 @@ def remove_response_headers(response: dict) -> dict:
|
|||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def vcr_config(_base_vcr_config: dict) -> dict: # noqa: F811
|
def vcr_config(_base_vcr_config: dict) -> dict: # noqa: F811
|
||||||
"""
|
"""Extend the default configuration coming from langchain_tests."""
|
||||||
Extend the default configuration coming from langchain_tests.
|
|
||||||
"""
|
|
||||||
config = _base_vcr_config.copy()
|
config = _base_vcr_config.copy()
|
||||||
config["before_record_request"] = remove_request_headers
|
config["before_record_request"] = remove_request_headers
|
||||||
config["before_record_response"] = remove_response_headers
|
config["before_record_response"] = remove_response_headers
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
"""Test ChatAnthropic chat model."""
|
"""Test ChatAnthropic chat model."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
@ -43,10 +45,7 @@ def test_stream() -> None:
|
|||||||
chunks_with_model_name = 0
|
chunks_with_model_name = 0
|
||||||
for token in llm.stream("I'm Pickle Rick"):
|
for token in llm.stream("I'm Pickle Rick"):
|
||||||
assert isinstance(token.content, str)
|
assert isinstance(token.content, str)
|
||||||
if full is None:
|
full = cast(BaseMessageChunk, token) if full is None else full + token
|
||||||
full = cast(BaseMessageChunk, token)
|
|
||||||
else:
|
|
||||||
full = full + token
|
|
||||||
assert isinstance(token, AIMessageChunk)
|
assert isinstance(token, AIMessageChunk)
|
||||||
if token.usage_metadata is not None:
|
if token.usage_metadata is not None:
|
||||||
if token.usage_metadata.get("input_tokens"):
|
if token.usage_metadata.get("input_tokens"):
|
||||||
@ -55,11 +54,14 @@ def test_stream() -> None:
|
|||||||
chunks_with_output_token_counts += 1
|
chunks_with_output_token_counts += 1
|
||||||
chunks_with_model_name += int("model_name" in token.response_metadata)
|
chunks_with_model_name += int("model_name" in token.response_metadata)
|
||||||
if chunks_with_input_token_counts != 1 or chunks_with_output_token_counts != 1:
|
if chunks_with_input_token_counts != 1 or chunks_with_output_token_counts != 1:
|
||||||
raise AssertionError(
|
msg = (
|
||||||
"Expected exactly one chunk with input or output token counts. "
|
"Expected exactly one chunk with input or output token counts. "
|
||||||
"AIMessageChunk aggregation adds counts. Check that "
|
"AIMessageChunk aggregation adds counts. Check that "
|
||||||
"this is behaving properly."
|
"this is behaving properly."
|
||||||
)
|
)
|
||||||
|
raise AssertionError(
|
||||||
|
msg,
|
||||||
|
)
|
||||||
assert chunks_with_model_name == 1
|
assert chunks_with_model_name == 1
|
||||||
# check token usage is populated
|
# check token usage is populated
|
||||||
assert isinstance(full, AIMessageChunk)
|
assert isinstance(full, AIMessageChunk)
|
||||||
@ -85,10 +87,7 @@ async def test_astream() -> None:
|
|||||||
chunks_with_output_token_counts = 0
|
chunks_with_output_token_counts = 0
|
||||||
async for token in llm.astream("I'm Pickle Rick"):
|
async for token in llm.astream("I'm Pickle Rick"):
|
||||||
assert isinstance(token.content, str)
|
assert isinstance(token.content, str)
|
||||||
if full is None:
|
full = cast(BaseMessageChunk, token) if full is None else full + token
|
||||||
full = cast(BaseMessageChunk, token)
|
|
||||||
else:
|
|
||||||
full = full + token
|
|
||||||
assert isinstance(token, AIMessageChunk)
|
assert isinstance(token, AIMessageChunk)
|
||||||
if token.usage_metadata is not None:
|
if token.usage_metadata is not None:
|
||||||
if token.usage_metadata.get("input_tokens"):
|
if token.usage_metadata.get("input_tokens"):
|
||||||
@ -96,11 +95,14 @@ async def test_astream() -> None:
|
|||||||
if token.usage_metadata.get("output_tokens"):
|
if token.usage_metadata.get("output_tokens"):
|
||||||
chunks_with_output_token_counts += 1
|
chunks_with_output_token_counts += 1
|
||||||
if chunks_with_input_token_counts != 1 or chunks_with_output_token_counts != 1:
|
if chunks_with_input_token_counts != 1 or chunks_with_output_token_counts != 1:
|
||||||
raise AssertionError(
|
msg = (
|
||||||
"Expected exactly one chunk with input or output token counts. "
|
"Expected exactly one chunk with input or output token counts. "
|
||||||
"AIMessageChunk aggregation adds counts. Check that "
|
"AIMessageChunk aggregation adds counts. Check that "
|
||||||
"this is behaving properly."
|
"this is behaving properly."
|
||||||
)
|
)
|
||||||
|
raise AssertionError(
|
||||||
|
msg,
|
||||||
|
)
|
||||||
# check token usage is populated
|
# check token usage is populated
|
||||||
assert isinstance(full, AIMessageChunk)
|
assert isinstance(full, AIMessageChunk)
|
||||||
assert full.usage_metadata is not None
|
assert full.usage_metadata is not None
|
||||||
@ -167,7 +169,8 @@ async def test_abatch_tags() -> None:
|
|||||||
llm = ChatAnthropicMessages(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]
|
llm = ChatAnthropicMessages(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]
|
||||||
|
|
||||||
result = await llm.abatch(
|
result = await llm.abatch(
|
||||||
["I'm Pickle Rick", "I'm not Pickle Rick"], config={"tags": ["foo"]}
|
["I'm Pickle Rick", "I'm not Pickle Rick"],
|
||||||
|
config={"tags": ["foo"]},
|
||||||
)
|
)
|
||||||
for token in result:
|
for token in result:
|
||||||
assert isinstance(token.content, str)
|
assert isinstance(token.content, str)
|
||||||
@ -187,8 +190,8 @@ async def test_async_tool_use() -> None:
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {"location": {"type": "string"}},
|
"properties": {"location": {"type": "string"}},
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
)
|
)
|
||||||
response = await llm_with_tools.ainvoke("what's the weather in san francisco, ca")
|
response = await llm_with_tools.ainvoke("what's the weather in san francisco, ca")
|
||||||
assert isinstance(response, AIMessage)
|
assert isinstance(response, AIMessage)
|
||||||
@ -202,16 +205,16 @@ async def test_async_tool_use() -> None:
|
|||||||
|
|
||||||
# Test streaming
|
# Test streaming
|
||||||
first = True
|
first = True
|
||||||
chunks = [] # type: ignore
|
chunks: list[BaseMessage | BaseMessageChunk] = []
|
||||||
async for chunk in llm_with_tools.astream(
|
async for chunk in llm_with_tools.astream(
|
||||||
"what's the weather in san francisco, ca"
|
"what's the weather in san francisco, ca",
|
||||||
):
|
):
|
||||||
chunks = chunks + [chunk]
|
chunks = [*chunks, chunk]
|
||||||
if first:
|
if first:
|
||||||
gathered = chunk
|
gathered = chunk
|
||||||
first = False
|
first = False
|
||||||
else:
|
else:
|
||||||
gathered = gathered + chunk # type: ignore
|
gathered = gathered + chunk # type: ignore[assignment]
|
||||||
assert len(chunks) > 1
|
assert len(chunks) > 1
|
||||||
assert isinstance(gathered, AIMessageChunk)
|
assert isinstance(gathered, AIMessageChunk)
|
||||||
assert isinstance(gathered.tool_call_chunks, list)
|
assert isinstance(gathered.tool_call_chunks, list)
|
||||||
@ -244,12 +247,12 @@ def test_invoke() -> None:
|
|||||||
"""Test invoke tokens from ChatAnthropicMessages."""
|
"""Test invoke tokens from ChatAnthropicMessages."""
|
||||||
llm = ChatAnthropicMessages(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]
|
llm = ChatAnthropicMessages(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]
|
||||||
|
|
||||||
result = llm.invoke("I'm Pickle Rick", config=dict(tags=["foo"]))
|
result = llm.invoke("I'm Pickle Rick", config={"tags": ["foo"]})
|
||||||
assert isinstance(result.content, str)
|
assert isinstance(result.content, str)
|
||||||
|
|
||||||
|
|
||||||
def test_system_invoke() -> None:
|
def test_system_invoke() -> None:
|
||||||
"""Test invoke tokens with a system message"""
|
"""Test invoke tokens with a system message."""
|
||||||
llm = ChatAnthropicMessages(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]
|
llm = ChatAnthropicMessages(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]
|
||||||
|
|
||||||
prompt = ChatPromptTemplate.from_messages(
|
prompt = ChatPromptTemplate.from_messages(
|
||||||
@ -260,7 +263,7 @@ def test_system_invoke() -> None:
|
|||||||
"STAY IN CHARACTER",
|
"STAY IN CHARACTER",
|
||||||
),
|
),
|
||||||
("human", "Are you a mathematician?"),
|
("human", "Are you a mathematician?"),
|
||||||
]
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
chain = prompt | llm
|
chain = prompt | llm
|
||||||
@ -282,7 +285,7 @@ def test_anthropic_generate() -> None:
|
|||||||
"""Test generate method of anthropic."""
|
"""Test generate method of anthropic."""
|
||||||
chat = ChatAnthropic(model=MODEL_NAME)
|
chat = ChatAnthropic(model=MODEL_NAME)
|
||||||
chat_messages: list[list[BaseMessage]] = [
|
chat_messages: list[list[BaseMessage]] = [
|
||||||
[HumanMessage(content="How many toes do dogs have?")]
|
[HumanMessage(content="How many toes do dogs have?")],
|
||||||
]
|
]
|
||||||
messages_copy = [messages.copy() for messages in chat_messages]
|
messages_copy = [messages.copy() for messages in chat_messages]
|
||||||
result: LLMResult = chat.generate(chat_messages)
|
result: LLMResult = chat.generate(chat_messages)
|
||||||
@ -330,7 +333,7 @@ async def test_anthropic_async_streaming_callback() -> None:
|
|||||||
verbose=True,
|
verbose=True,
|
||||||
)
|
)
|
||||||
chat_messages: list[BaseMessage] = [
|
chat_messages: list[BaseMessage] = [
|
||||||
HumanMessage(content="How many toes do dogs have?")
|
HumanMessage(content="How many toes do dogs have?"),
|
||||||
]
|
]
|
||||||
async for token in chat.astream(chat_messages):
|
async for token in chat.astream(chat_messages):
|
||||||
assert isinstance(token, AIMessageChunk)
|
assert isinstance(token, AIMessageChunk)
|
||||||
@ -352,8 +355,8 @@ def test_anthropic_multimodal() -> None:
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{"type": "text", "text": "What is this a logo for?"},
|
{"type": "text", "text": "What is this a logo for?"},
|
||||||
]
|
],
|
||||||
)
|
),
|
||||||
]
|
]
|
||||||
response = chat.invoke(messages)
|
response = chat.invoke(messages)
|
||||||
assert isinstance(response, AIMessage)
|
assert isinstance(response, AIMessage)
|
||||||
@ -368,7 +371,9 @@ def test_streaming() -> None:
|
|||||||
callback_manager = CallbackManager([callback_handler])
|
callback_manager = CallbackManager([callback_handler])
|
||||||
|
|
||||||
llm = ChatAnthropicMessages( # type: ignore[call-arg, call-arg]
|
llm = ChatAnthropicMessages( # type: ignore[call-arg, call-arg]
|
||||||
model_name=MODEL_NAME, streaming=True, callback_manager=callback_manager
|
model_name=MODEL_NAME,
|
||||||
|
streaming=True,
|
||||||
|
callback_manager=callback_manager,
|
||||||
)
|
)
|
||||||
|
|
||||||
response = llm.generate([[HumanMessage(content="I'm Pickle Rick")]])
|
response = llm.generate([[HumanMessage(content="I'm Pickle Rick")]])
|
||||||
@ -382,7 +387,9 @@ async def test_astreaming() -> None:
|
|||||||
callback_manager = CallbackManager([callback_handler])
|
callback_manager = CallbackManager([callback_handler])
|
||||||
|
|
||||||
llm = ChatAnthropicMessages( # type: ignore[call-arg, call-arg]
|
llm = ChatAnthropicMessages( # type: ignore[call-arg, call-arg]
|
||||||
model_name=MODEL_NAME, streaming=True, callback_manager=callback_manager
|
model_name=MODEL_NAME,
|
||||||
|
streaming=True,
|
||||||
|
callback_manager=callback_manager,
|
||||||
)
|
)
|
||||||
|
|
||||||
response = await llm.agenerate([[HumanMessage(content="I'm Pickle Rick")]])
|
response = await llm.agenerate([[HumanMessage(content="I'm Pickle Rick")]])
|
||||||
@ -421,19 +428,19 @@ def test_tool_use() -> None:
|
|||||||
temperature=0,
|
temperature=0,
|
||||||
# Add extra headers to also test token-efficient tools
|
# Add extra headers to also test token-efficient tools
|
||||||
model_kwargs={
|
model_kwargs={
|
||||||
"extra_headers": {"anthropic-beta": "token-efficient-tools-2025-02-19"}
|
"extra_headers": {"anthropic-beta": "token-efficient-tools-2025-02-19"},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
llm_with_tools = llm.bind_tools([tool_definition])
|
llm_with_tools = llm.bind_tools([tool_definition])
|
||||||
first = True
|
first = True
|
||||||
chunks = [] # type: ignore
|
chunks: list[BaseMessage | BaseMessageChunk] = []
|
||||||
for chunk in llm_with_tools.stream(query):
|
for chunk in llm_with_tools.stream(query):
|
||||||
chunks = chunks + [chunk]
|
chunks = [*chunks, chunk]
|
||||||
if first:
|
if first:
|
||||||
gathered = chunk
|
gathered = chunk
|
||||||
first = False
|
first = False
|
||||||
else:
|
else:
|
||||||
gathered = gathered + chunk # type: ignore
|
gathered = gathered + chunk # type: ignore[assignment]
|
||||||
assert len(chunks) > 1
|
assert len(chunks) > 1
|
||||||
assert isinstance(gathered.content, list)
|
assert isinstance(gathered.content, list)
|
||||||
assert len(gathered.content) == 2
|
assert len(gathered.content) == 2
|
||||||
@ -470,17 +477,17 @@ def test_tool_use() -> None:
|
|||||||
query,
|
query,
|
||||||
gathered,
|
gathered,
|
||||||
ToolMessage(content="sunny and warm", tool_call_id=tool_call["id"]),
|
ToolMessage(content="sunny and warm", tool_call_id=tool_call["id"]),
|
||||||
]
|
],
|
||||||
)
|
)
|
||||||
chunks = [] # type: ignore
|
chunks = []
|
||||||
first = True
|
first = True
|
||||||
for chunk in stream:
|
for chunk in stream:
|
||||||
chunks = chunks + [chunk]
|
chunks = [*chunks, chunk]
|
||||||
if first:
|
if first:
|
||||||
gathered = chunk
|
gathered = chunk
|
||||||
first = False
|
first = False
|
||||||
else:
|
else:
|
||||||
gathered = gathered + chunk # type: ignore
|
gathered = gathered + chunk # type: ignore[assignment]
|
||||||
assert len(chunks) > 1
|
assert len(chunks) > 1
|
||||||
|
|
||||||
|
|
||||||
@ -489,14 +496,14 @@ def test_builtin_tools() -> None:
|
|||||||
tool = {"type": "text_editor_20250124", "name": "str_replace_editor"}
|
tool = {"type": "text_editor_20250124", "name": "str_replace_editor"}
|
||||||
llm_with_tools = llm.bind_tools([tool])
|
llm_with_tools = llm.bind_tools([tool])
|
||||||
response = llm_with_tools.invoke(
|
response = llm_with_tools.invoke(
|
||||||
"There's a syntax error in my primes.py file. Can you help me fix it?"
|
"There's a syntax error in my primes.py file. Can you help me fix it?",
|
||||||
)
|
)
|
||||||
assert isinstance(response, AIMessage)
|
assert isinstance(response, AIMessage)
|
||||||
assert response.tool_calls
|
assert response.tool_calls
|
||||||
|
|
||||||
|
|
||||||
class GenerateUsername(BaseModel):
|
class GenerateUsername(BaseModel):
|
||||||
"Get a username based on someone's name and hair color."
|
"""Get a username based on someone's name and hair color."""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
hair_color: str
|
hair_color: str
|
||||||
@ -508,7 +515,7 @@ def test_disable_parallel_tool_calling() -> None:
|
|||||||
result = llm_with_tools.invoke(
|
result = llm_with_tools.invoke(
|
||||||
"Use the GenerateUsername tool to generate user names for:\n\n"
|
"Use the GenerateUsername tool to generate user names for:\n\n"
|
||||||
"Sally with green hair\n"
|
"Sally with green hair\n"
|
||||||
"Bob with blue hair"
|
"Bob with blue hair",
|
||||||
)
|
)
|
||||||
assert isinstance(result, AIMessage)
|
assert isinstance(result, AIMessage)
|
||||||
assert len(result.tool_calls) == 1
|
assert len(result.tool_calls) == 1
|
||||||
@ -523,7 +530,7 @@ def test_anthropic_with_empty_text_block() -> None:
|
|||||||
return "OK"
|
return "OK"
|
||||||
|
|
||||||
model = ChatAnthropic(model="claude-3-opus-20240229", temperature=0).bind_tools(
|
model = ChatAnthropic(model="claude-3-opus-20240229", temperature=0).bind_tools(
|
||||||
[type_letter]
|
[type_letter],
|
||||||
)
|
)
|
||||||
|
|
||||||
messages = [
|
messages = [
|
||||||
@ -531,7 +538,7 @@ def test_anthropic_with_empty_text_block() -> None:
|
|||||||
content="Repeat the given string using the provided tools. Do not write "
|
content="Repeat the given string using the provided tools. Do not write "
|
||||||
"anything else or provide any explanations. For example, "
|
"anything else or provide any explanations. For example, "
|
||||||
"if the string is 'abc', you must print the "
|
"if the string is 'abc', you must print the "
|
||||||
"letters 'a', 'b', and 'c' one at a time and in that order. "
|
"letters 'a', 'b', and 'c' one at a time and in that order. ",
|
||||||
),
|
),
|
||||||
HumanMessage(content="dog"),
|
HumanMessage(content="dog"),
|
||||||
AIMessage(
|
AIMessage(
|
||||||
@ -572,7 +579,7 @@ def test_with_structured_output() -> None:
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {"location": {"type": "string"}},
|
"properties": {"location": {"type": "string"}},
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
response = structured_llm.invoke("what's the weather in san francisco, ca")
|
response = structured_llm.invoke("what's the weather in san francisco, ca")
|
||||||
assert isinstance(response, dict)
|
assert isinstance(response, dict)
|
||||||
@ -593,10 +600,11 @@ def test_get_num_tokens_from_messages() -> None:
|
|||||||
# Test tool use
|
# Test tool use
|
||||||
@tool(parse_docstring=True)
|
@tool(parse_docstring=True)
|
||||||
def get_weather(location: str) -> str:
|
def get_weather(location: str) -> str:
|
||||||
"""Get the current weather in a given location
|
"""Get the current weather in a given location.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
location: The city and state, e.g. San Francisco, CA
|
location: The city and state, e.g. San Francisco, CA
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return "Sunny"
|
return "Sunny"
|
||||||
|
|
||||||
@ -634,7 +642,7 @@ def test_get_num_tokens_from_messages() -> None:
|
|||||||
|
|
||||||
|
|
||||||
class GetWeather(BaseModel):
|
class GetWeather(BaseModel):
|
||||||
"""Get the current weather in a given location"""
|
"""Get the current weather in a given location."""
|
||||||
|
|
||||||
location: str = Field(..., description="The city and state, e.g. San Francisco, CA")
|
location: str = Field(..., description="The city and state, e.g. San Francisco, CA")
|
||||||
|
|
||||||
@ -666,9 +674,9 @@ def test_pdf_document_input() -> None:
|
|||||||
"media_type": "application/pdf",
|
"media_type": "application/pdf",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
],
|
||||||
)
|
),
|
||||||
]
|
],
|
||||||
)
|
)
|
||||||
assert isinstance(result, AIMessage)
|
assert isinstance(result, AIMessage)
|
||||||
assert isinstance(result.content, str)
|
assert isinstance(result.content, str)
|
||||||
@ -694,7 +702,7 @@ def test_citations() -> None:
|
|||||||
},
|
},
|
||||||
{"type": "text", "text": "What color is the grass and sky?"},
|
{"type": "text", "text": "What color is the grass and sky?"},
|
||||||
],
|
],
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
response = llm.invoke(messages)
|
response = llm.invoke(messages)
|
||||||
assert isinstance(response, AIMessage)
|
assert isinstance(response, AIMessage)
|
||||||
@ -704,10 +712,7 @@ def test_citations() -> None:
|
|||||||
# Test streaming
|
# Test streaming
|
||||||
full: Optional[BaseMessageChunk] = None
|
full: Optional[BaseMessageChunk] = None
|
||||||
for chunk in llm.stream(messages):
|
for chunk in llm.stream(messages):
|
||||||
if full is None:
|
full = cast(BaseMessageChunk, chunk) if full is None else full + chunk
|
||||||
full = cast(BaseMessageChunk, chunk)
|
|
||||||
else:
|
|
||||||
full = full + chunk
|
|
||||||
assert isinstance(full, AIMessageChunk)
|
assert isinstance(full, AIMessageChunk)
|
||||||
assert isinstance(full.content, list)
|
assert isinstance(full.content, list)
|
||||||
assert any("citations" in block for block in full.content)
|
assert any("citations" in block for block in full.content)
|
||||||
@ -718,7 +723,7 @@ def test_citations() -> None:
|
|||||||
"role": "user",
|
"role": "user",
|
||||||
"content": "Can you comment on the citations you just made?",
|
"content": "Can you comment on the citations you just made?",
|
||||||
}
|
}
|
||||||
_ = llm.invoke(messages + [full, next_message])
|
_ = llm.invoke([*messages, full, next_message])
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.vcr
|
@pytest.mark.vcr
|
||||||
@ -742,10 +747,7 @@ def test_thinking() -> None:
|
|||||||
# Test streaming
|
# Test streaming
|
||||||
full: Optional[BaseMessageChunk] = None
|
full: Optional[BaseMessageChunk] = None
|
||||||
for chunk in llm.stream([input_message]):
|
for chunk in llm.stream([input_message]):
|
||||||
if full is None:
|
full = cast(BaseMessageChunk, chunk) if full is None else full + chunk
|
||||||
full = cast(BaseMessageChunk, chunk)
|
|
||||||
else:
|
|
||||||
full = full + chunk
|
|
||||||
assert isinstance(full, AIMessageChunk)
|
assert isinstance(full, AIMessageChunk)
|
||||||
assert isinstance(full.content, list)
|
assert isinstance(full.content, list)
|
||||||
assert any("thinking" in block for block in full.content)
|
assert any("thinking" in block for block in full.content)
|
||||||
@ -784,10 +786,7 @@ def test_redacted_thinking() -> None:
|
|||||||
# Test streaming
|
# Test streaming
|
||||||
full: Optional[BaseMessageChunk] = None
|
full: Optional[BaseMessageChunk] = None
|
||||||
for chunk in llm.stream([input_message]):
|
for chunk in llm.stream([input_message]):
|
||||||
if full is None:
|
full = cast(BaseMessageChunk, chunk) if full is None else full + chunk
|
||||||
full = cast(BaseMessageChunk, chunk)
|
|
||||||
else:
|
|
||||||
full = full + chunk
|
|
||||||
assert isinstance(full, AIMessageChunk)
|
assert isinstance(full, AIMessageChunk)
|
||||||
assert isinstance(full.content, list)
|
assert isinstance(full.content, list)
|
||||||
stream_has_reasoning = False
|
stream_has_reasoning = False
|
||||||
@ -864,7 +863,7 @@ def test_image_tool_calling() -> None:
|
|||||||
"media_type": "image/jpeg",
|
"media_type": "image/jpeg",
|
||||||
"data": image_data,
|
"data": image_data,
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
messages = [
|
messages = [
|
||||||
SystemMessage("you're a good assistant"),
|
SystemMessage("you're a good assistant"),
|
||||||
@ -878,7 +877,7 @@ def test_image_tool_calling() -> None:
|
|||||||
"id": "foo",
|
"id": "foo",
|
||||||
"name": "color_picker",
|
"name": "color_picker",
|
||||||
},
|
},
|
||||||
]
|
],
|
||||||
),
|
),
|
||||||
HumanMessage(
|
HumanMessage(
|
||||||
[
|
[
|
||||||
@ -889,12 +888,12 @@ def test_image_tool_calling() -> None:
|
|||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"text": "green is a great pick! that's my sister's favorite color", # noqa: E501
|
"text": "green is a great pick! that's my sister's favorite color", # noqa: E501
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
"is_error": False,
|
"is_error": False,
|
||||||
},
|
},
|
||||||
{"type": "text", "text": "what's my sister's favorite color"},
|
{"type": "text", "text": "what's my sister's favorite color"},
|
||||||
]
|
],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
llm = ChatAnthropic(model="claude-3-5-sonnet-latest")
|
llm = ChatAnthropic(model="claude-3-5-sonnet-latest")
|
||||||
@ -914,7 +913,7 @@ def test_web_search() -> None:
|
|||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"text": "How do I update a web app to TypeScript 5.5?",
|
"text": "How do I update a web app to TypeScript 5.5?",
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
response = llm_with_tools.invoke([input_message])
|
response = llm_with_tools.invoke([input_message])
|
||||||
@ -962,7 +961,7 @@ def test_code_execution() -> None:
|
|||||||
"Calculate the mean and standard deviation of "
|
"Calculate the mean and standard deviation of "
|
||||||
"[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]"
|
"[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]"
|
||||||
),
|
),
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
response = llm_with_tools.invoke([input_message])
|
response = llm_with_tools.invoke([input_message])
|
||||||
@ -999,7 +998,7 @@ def test_remote_mcp() -> None:
|
|||||||
"name": "deepwiki",
|
"name": "deepwiki",
|
||||||
"tool_configuration": {"enabled": True, "allowed_tools": ["ask_question"]},
|
"tool_configuration": {"enabled": True, "allowed_tools": ["ask_question"]},
|
||||||
"authorization_token": "PLACEHOLDER",
|
"authorization_token": "PLACEHOLDER",
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
llm = ChatAnthropic(
|
llm = ChatAnthropic(
|
||||||
@ -1018,7 +1017,7 @@ def test_remote_mcp() -> None:
|
|||||||
"What transport protocols does the 2025-03-26 version of the MCP "
|
"What transport protocols does the 2025-03-26 version of the MCP "
|
||||||
"spec (modelcontextprotocol/modelcontextprotocol) support?"
|
"spec (modelcontextprotocol/modelcontextprotocol) support?"
|
||||||
),
|
),
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
response = llm.invoke([input_message])
|
response = llm.invoke([input_message])
|
||||||
@ -1132,9 +1131,9 @@ def test_search_result_tool_message() -> None:
|
|||||||
"To request vacation days, submit a leave request form "
|
"To request vacation days, submit a leave request form "
|
||||||
"through the HR portal. Approval will be sent by email."
|
"through the HR portal. Approval will be sent by email."
|
||||||
),
|
),
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
tool_call = {
|
tool_call = {
|
||||||
@ -1182,7 +1181,7 @@ def test_search_result_top_level() -> None:
|
|||||||
"To request vacation days, submit a leave request form "
|
"To request vacation days, submit a leave request form "
|
||||||
"through the HR portal. Approval will be sent by email."
|
"through the HR portal. Approval will be sent by email."
|
||||||
),
|
),
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -1194,14 +1193,14 @@ def test_search_result_top_level() -> None:
|
|||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"text": "Managers have 3 days to approve a request.",
|
"text": "Managers have 3 days to approve a request.",
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"text": "How do I request vacation days?",
|
"text": "How do I request vacation days?",
|
||||||
},
|
},
|
||||||
]
|
],
|
||||||
)
|
)
|
||||||
result = llm.invoke([input_message])
|
result = llm.invoke([input_message])
|
||||||
assert isinstance(result, AIMessage)
|
assert isinstance(result, AIMessage)
|
||||||
|
@ -4,4 +4,3 @@ import pytest
|
|||||||
@pytest.mark.compile
|
@pytest.mark.compile
|
||||||
def test_placeholder() -> None:
|
def test_placeholder() -> None:
|
||||||
"""Used for compiling integration tests without running any real tests."""
|
"""Used for compiling integration tests without running any real tests."""
|
||||||
pass
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
"""Test ChatAnthropic chat model."""
|
"""Test ChatAnthropic chat model."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
@ -46,7 +48,8 @@ async def test_abatch_tags() -> None:
|
|||||||
llm = ChatAnthropicTools(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]
|
llm = ChatAnthropicTools(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]
|
||||||
|
|
||||||
result = await llm.abatch(
|
result = await llm.abatch(
|
||||||
["I'm Pickle Rick", "I'm not Pickle Rick"], config={"tags": ["foo"]}
|
["I'm Pickle Rick", "I'm not Pickle Rick"],
|
||||||
|
config={"tags": ["foo"]},
|
||||||
)
|
)
|
||||||
for token in result:
|
for token in result:
|
||||||
assert isinstance(token.content, str)
|
assert isinstance(token.content, str)
|
||||||
@ -73,12 +76,12 @@ def test_invoke() -> None:
|
|||||||
"""Test invoke tokens from ChatAnthropicTools."""
|
"""Test invoke tokens from ChatAnthropicTools."""
|
||||||
llm = ChatAnthropicTools(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]
|
llm = ChatAnthropicTools(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]
|
||||||
|
|
||||||
result = llm.invoke("I'm Pickle Rick", config=dict(tags=["foo"]))
|
result = llm.invoke("I'm Pickle Rick", config={"tags": ["foo"]})
|
||||||
assert isinstance(result.content, str)
|
assert isinstance(result.content, str)
|
||||||
|
|
||||||
|
|
||||||
def test_system_invoke() -> None:
|
def test_system_invoke() -> None:
|
||||||
"""Test invoke tokens with a system message"""
|
"""Test invoke tokens with a system message."""
|
||||||
llm = ChatAnthropicTools(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]
|
llm = ChatAnthropicTools(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]
|
||||||
|
|
||||||
prompt = ChatPromptTemplate.from_messages(
|
prompt = ChatPromptTemplate.from_messages(
|
||||||
@ -89,7 +92,7 @@ def test_system_invoke() -> None:
|
|||||||
"STAY IN CHARACTER",
|
"STAY IN CHARACTER",
|
||||||
),
|
),
|
||||||
("human", "Are you a mathematician?"),
|
("human", "Are you a mathematician?"),
|
||||||
]
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
chain = prompt | llm
|
chain = prompt | llm
|
||||||
@ -128,19 +131,24 @@ def test_anthropic_complex_structured_output() -> None:
|
|||||||
"""Relevant information about an email."""
|
"""Relevant information about an email."""
|
||||||
|
|
||||||
sender: Optional[str] = Field(
|
sender: Optional[str] = Field(
|
||||||
None, description="The sender's name, if available"
|
None,
|
||||||
|
description="The sender's name, if available",
|
||||||
)
|
)
|
||||||
sender_phone_number: Optional[str] = Field(
|
sender_phone_number: Optional[str] = Field(
|
||||||
None, description="The sender's phone number, if available"
|
None,
|
||||||
|
description="The sender's phone number, if available",
|
||||||
)
|
)
|
||||||
sender_address: Optional[str] = Field(
|
sender_address: Optional[str] = Field(
|
||||||
None, description="The sender's address, if available"
|
None,
|
||||||
|
description="The sender's address, if available",
|
||||||
)
|
)
|
||||||
action_items: list[str] = Field(
|
action_items: list[str] = Field(
|
||||||
..., description="A list of action items requested by the email"
|
...,
|
||||||
|
description="A list of action items requested by the email",
|
||||||
)
|
)
|
||||||
topic: str = Field(
|
topic: str = Field(
|
||||||
..., description="High level description of what the email is about"
|
...,
|
||||||
|
description="High level description of what the email is about",
|
||||||
)
|
)
|
||||||
tone: ToneEnum = Field(..., description="The tone of the email.")
|
tone: ToneEnum = Field(..., description="The tone of the email.")
|
||||||
|
|
||||||
@ -150,7 +158,7 @@ def test_anthropic_complex_structured_output() -> None:
|
|||||||
"human",
|
"human",
|
||||||
"What can you tell me about the following email? Make sure to answer in the correct format: {email}", # noqa: E501
|
"What can you tell me about the following email? Make sure to answer in the correct format: {email}", # noqa: E501
|
||||||
),
|
),
|
||||||
]
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
llm = ChatAnthropicTools( # type: ignore[call-arg, call-arg]
|
llm = ChatAnthropicTools( # type: ignore[call-arg, call-arg]
|
||||||
@ -163,7 +171,7 @@ def test_anthropic_complex_structured_output() -> None:
|
|||||||
|
|
||||||
response = extraction_chain.invoke(
|
response = extraction_chain.invoke(
|
||||||
{
|
{
|
||||||
"email": "From: Erick. The email is about the new project. The tone is positive. The action items are to send the report and to schedule a meeting." # noqa: E501
|
"email": "From: Erick. The email is about the new project. The tone is positive. The action items are to send the report and to schedule a meeting.", # noqa: E501
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
assert isinstance(response, Email)
|
assert isinstance(response, Email)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
"""Standard LangChain interface tests"""
|
"""Standard LangChain interface tests."""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Literal, cast
|
from typing import Literal, cast
|
||||||
@ -87,9 +87,9 @@ class TestAnthropicStandard(ChatModelIntegrationTests):
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"text": input_,
|
"text": input_,
|
||||||
"cache_control": {"type": "ephemeral"},
|
"cache_control": {"type": "ephemeral"},
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
stream,
|
stream,
|
||||||
)
|
)
|
||||||
@ -118,9 +118,9 @@ class TestAnthropicStandard(ChatModelIntegrationTests):
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"text": input_,
|
"text": input_,
|
||||||
"cache_control": {"type": "ephemeral"},
|
"cache_control": {"type": "ephemeral"},
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
stream,
|
stream,
|
||||||
)
|
)
|
||||||
@ -134,22 +134,18 @@ class TestAnthropicStandard(ChatModelIntegrationTests):
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"text": input_,
|
"text": input_,
|
||||||
"cache_control": {"type": "ephemeral"},
|
"cache_control": {"type": "ephemeral"},
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
stream,
|
stream,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _invoke(llm: ChatAnthropic, input_: list, stream: bool) -> AIMessage:
|
def _invoke(llm: ChatAnthropic, input_: list, stream: bool) -> AIMessage: # noqa: FBT001
|
||||||
if stream:
|
if stream:
|
||||||
full = None
|
full = None
|
||||||
for chunk in llm.stream(input_):
|
for chunk in llm.stream(input_):
|
||||||
if full is None:
|
full = cast(BaseMessageChunk, chunk) if full is None else full + chunk
|
||||||
full = cast(BaseMessageChunk, chunk)
|
|
||||||
else:
|
|
||||||
full = full + chunk
|
|
||||||
return cast(AIMessage, full)
|
return cast(AIMessage, full)
|
||||||
else:
|
return cast(AIMessage, llm.invoke(input_))
|
||||||
return cast(AIMessage, llm.invoke(input_))
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
"""A fake callback handler for testing purposes."""
|
"""A fake callback handler for testing purposes."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Union
|
from typing import Any, Union
|
||||||
|
|
||||||
from langchain_core.callbacks import BaseCallbackHandler
|
from langchain_core.callbacks import BaseCallbackHandler
|
||||||
@ -252,5 +254,5 @@ class FakeCallbackHandler(BaseCallbackHandler, BaseFakeCallbackHandlerMixin):
|
|||||||
self.on_retriever_error_common()
|
self.on_retriever_error_common()
|
||||||
|
|
||||||
# Overriding since BaseModel has __deepcopy__ method as well
|
# Overriding since BaseModel has __deepcopy__ method as well
|
||||||
def __deepcopy__(self, memo: dict) -> "FakeCallbackHandler": # type: ignore
|
def __deepcopy__(self, memo: dict) -> FakeCallbackHandler: # type: ignore[override]
|
||||||
return self
|
return self
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
"""Test chat model integration."""
|
"""Test chat model integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from typing import Any, Callable, Literal, Optional, cast
|
from typing import Any, Callable, Literal, Optional, cast
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
@ -187,7 +189,7 @@ def test__merge_messages() -> None:
|
|||||||
"text": None,
|
"text": None,
|
||||||
"name": "blah",
|
"name": "blah",
|
||||||
},
|
},
|
||||||
]
|
],
|
||||||
),
|
),
|
||||||
ToolMessage("buz output", tool_call_id="1", status="error"), # type: ignore[misc]
|
ToolMessage("buz output", tool_call_id="1", status="error"), # type: ignore[misc]
|
||||||
ToolMessage(
|
ToolMessage(
|
||||||
@ -234,7 +236,7 @@ def test__merge_messages() -> None:
|
|||||||
"text": None,
|
"text": None,
|
||||||
"name": "blah",
|
"name": "blah",
|
||||||
},
|
},
|
||||||
]
|
],
|
||||||
),
|
),
|
||||||
HumanMessage( # type: ignore[misc]
|
HumanMessage( # type: ignore[misc]
|
||||||
[
|
[
|
||||||
@ -266,7 +268,7 @@ def test__merge_messages() -> None:
|
|||||||
"is_error": False,
|
"is_error": False,
|
||||||
},
|
},
|
||||||
{"type": "text", "text": "next thing"},
|
{"type": "text", "text": "next thing"},
|
||||||
]
|
],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
actual = _merge_messages(messages)
|
actual = _merge_messages(messages)
|
||||||
@ -277,7 +279,7 @@ def test__merge_messages() -> None:
|
|||||||
ToolMessage("buz output", tool_call_id="1"), # type: ignore[misc]
|
ToolMessage("buz output", tool_call_id="1"), # type: ignore[misc]
|
||||||
ToolMessage( # type: ignore[misc]
|
ToolMessage( # type: ignore[misc]
|
||||||
content=[
|
content=[
|
||||||
{"type": "tool_result", "content": "blah output", "tool_use_id": "2"}
|
{"type": "tool_result", "content": "blah output", "tool_use_id": "2"},
|
||||||
],
|
],
|
||||||
tool_call_id="2",
|
tool_call_id="2",
|
||||||
),
|
),
|
||||||
@ -292,8 +294,8 @@ def test__merge_messages() -> None:
|
|||||||
"is_error": False,
|
"is_error": False,
|
||||||
},
|
},
|
||||||
{"type": "tool_result", "content": "blah output", "tool_use_id": "2"},
|
{"type": "tool_result", "content": "blah output", "tool_use_id": "2"},
|
||||||
]
|
],
|
||||||
)
|
),
|
||||||
]
|
]
|
||||||
actual = _merge_messages(messages)
|
actual = _merge_messages(messages)
|
||||||
assert expected == actual
|
assert expected == actual
|
||||||
@ -310,7 +312,7 @@ def test__merge_messages_mutation() -> None:
|
|||||||
]
|
]
|
||||||
expected = [
|
expected = [
|
||||||
HumanMessage( # type: ignore[misc]
|
HumanMessage( # type: ignore[misc]
|
||||||
[{"type": "text", "text": "bar"}, {"type": "text", "text": "next thing"}]
|
[{"type": "text", "text": "bar"}, {"type": "text", "text": "next thing"}],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
actual = _merge_messages(messages)
|
actual = _merge_messages(messages)
|
||||||
@ -327,7 +329,7 @@ def test__format_image() -> None:
|
|||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def pydantic() -> type[BaseModel]:
|
def pydantic() -> type[BaseModel]:
|
||||||
class dummy_function(BaseModel):
|
class dummy_function(BaseModel):
|
||||||
"""dummy function"""
|
"""Dummy function."""
|
||||||
|
|
||||||
arg1: int = Field(..., description="foo")
|
arg1: int = Field(..., description="foo")
|
||||||
arg2: Literal["bar", "baz"] = Field(..., description="one of 'bar', 'baz'")
|
arg2: Literal["bar", "baz"] = Field(..., description="one of 'bar', 'baz'")
|
||||||
@ -338,13 +340,14 @@ def pydantic() -> type[BaseModel]:
|
|||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def function() -> Callable:
|
def function() -> Callable:
|
||||||
def dummy_function(arg1: int, arg2: Literal["bar", "baz"]) -> None:
|
def dummy_function(arg1: int, arg2: Literal["bar", "baz"]) -> None:
|
||||||
"""dummy function
|
"""Dummy function.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
----
|
||||||
arg1: foo
|
arg1: foo
|
||||||
arg2: one of 'bar', 'baz'
|
arg2: one of 'bar', 'baz'
|
||||||
"""
|
|
||||||
pass
|
""" # noqa: D401
|
||||||
|
|
||||||
return dummy_function
|
return dummy_function
|
||||||
|
|
||||||
@ -358,7 +361,7 @@ def dummy_tool() -> BaseTool:
|
|||||||
class DummyFunction(BaseTool): # type: ignore[override]
|
class DummyFunction(BaseTool): # type: ignore[override]
|
||||||
args_schema: type[BaseModel] = Schema
|
args_schema: type[BaseModel] = Schema
|
||||||
name: str = "dummy_function"
|
name: str = "dummy_function"
|
||||||
description: str = "dummy function"
|
description: str = "Dummy function."
|
||||||
|
|
||||||
def _run(self, *args: Any, **kwargs: Any) -> Any:
|
def _run(self, *args: Any, **kwargs: Any) -> Any:
|
||||||
pass
|
pass
|
||||||
@ -370,7 +373,7 @@ def dummy_tool() -> BaseTool:
|
|||||||
def json_schema() -> dict:
|
def json_schema() -> dict:
|
||||||
return {
|
return {
|
||||||
"title": "dummy_function",
|
"title": "dummy_function",
|
||||||
"description": "dummy function",
|
"description": "Dummy function.",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"arg1": {"description": "foo", "type": "integer"},
|
"arg1": {"description": "foo", "type": "integer"},
|
||||||
@ -388,7 +391,7 @@ def json_schema() -> dict:
|
|||||||
def openai_function() -> dict:
|
def openai_function() -> dict:
|
||||||
return {
|
return {
|
||||||
"name": "dummy_function",
|
"name": "dummy_function",
|
||||||
"description": "dummy function",
|
"description": "Dummy function.",
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -413,7 +416,7 @@ def test_convert_to_anthropic_tool(
|
|||||||
) -> None:
|
) -> None:
|
||||||
expected = {
|
expected = {
|
||||||
"name": "dummy_function",
|
"name": "dummy_function",
|
||||||
"description": "dummy function",
|
"description": "Dummy function.",
|
||||||
"input_schema": {
|
"input_schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -429,7 +432,7 @@ def test_convert_to_anthropic_tool(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for fn in (pydantic, function, dummy_tool, json_schema, expected, openai_function):
|
for fn in (pydantic, function, dummy_tool, json_schema, expected, openai_function):
|
||||||
actual = convert_to_anthropic_tool(fn) # type: ignore
|
actual = convert_to_anthropic_tool(fn)
|
||||||
assert actual == expected
|
assert actual == expected
|
||||||
|
|
||||||
|
|
||||||
@ -461,7 +464,7 @@ def test__format_messages_with_tool_calls() -> None:
|
|||||||
"type": "base64",
|
"type": "base64",
|
||||||
"media_type": "image/jpeg",
|
"media_type": "image/jpeg",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
tool_call_id="3",
|
tool_call_id="3",
|
||||||
)
|
)
|
||||||
@ -478,7 +481,7 @@ def test__format_messages_with_tool_calls() -> None:
|
|||||||
"name": "bar",
|
"name": "bar",
|
||||||
"id": "1",
|
"id": "1",
|
||||||
"input": {"baz": "buzz"},
|
"input": {"baz": "buzz"},
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -489,7 +492,7 @@ def test__format_messages_with_tool_calls() -> None:
|
|||||||
"content": "blurb",
|
"content": "blurb",
|
||||||
"tool_use_id": "1",
|
"tool_use_id": "1",
|
||||||
"is_error": False,
|
"is_error": False,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -500,7 +503,7 @@ def test__format_messages_with_tool_calls() -> None:
|
|||||||
"name": "bar",
|
"name": "bar",
|
||||||
"id": "2",
|
"id": "2",
|
||||||
"input": {"baz": "buzz"},
|
"input": {"baz": "buzz"},
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -516,7 +519,7 @@ def test__format_messages_with_tool_calls() -> None:
|
|||||||
"type": "base64",
|
"type": "base64",
|
||||||
"media_type": "image/jpeg",
|
"media_type": "image/jpeg",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
"tool_use_id": "2",
|
"tool_use_id": "2",
|
||||||
"is_error": False,
|
"is_error": False,
|
||||||
@ -531,7 +534,7 @@ def test__format_messages_with_tool_calls() -> None:
|
|||||||
"type": "base64",
|
"type": "base64",
|
||||||
"media_type": "image/jpeg",
|
"media_type": "image/jpeg",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
"tool_use_id": "3",
|
"tool_use_id": "3",
|
||||||
"is_error": False,
|
"is_error": False,
|
||||||
@ -579,7 +582,7 @@ def test__format_messages_with_str_content_and_tool_calls() -> None:
|
|||||||
"content": "blurb",
|
"content": "blurb",
|
||||||
"tool_use_id": "1",
|
"tool_use_id": "1",
|
||||||
"is_error": False,
|
"is_error": False,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -624,7 +627,7 @@ def test__format_messages_with_list_content_and_tool_calls() -> None:
|
|||||||
"content": "blurb",
|
"content": "blurb",
|
||||||
"tool_use_id": "1",
|
"tool_use_id": "1",
|
||||||
"is_error": False,
|
"is_error": False,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -676,7 +679,7 @@ def test__format_messages_with_tool_use_blocks_and_tool_calls() -> None:
|
|||||||
"content": "blurb",
|
"content": "blurb",
|
||||||
"tool_use_id": "1",
|
"tool_use_id": "1",
|
||||||
"is_error": False,
|
"is_error": False,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -690,7 +693,7 @@ def test__format_messages_with_cache_control() -> None:
|
|||||||
SystemMessage(
|
SystemMessage(
|
||||||
[
|
[
|
||||||
{"type": "text", "text": "foo", "cache_control": {"type": "ephemeral"}},
|
{"type": "text", "text": "foo", "cache_control": {"type": "ephemeral"}},
|
||||||
]
|
],
|
||||||
),
|
),
|
||||||
HumanMessage(
|
HumanMessage(
|
||||||
[
|
[
|
||||||
@ -699,11 +702,11 @@ def test__format_messages_with_cache_control() -> None:
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"text": "foo",
|
"text": "foo",
|
||||||
},
|
},
|
||||||
]
|
],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
expected_system = [
|
expected_system = [
|
||||||
{"type": "text", "text": "foo", "cache_control": {"type": "ephemeral"}}
|
{"type": "text", "text": "foo", "cache_control": {"type": "ephemeral"}},
|
||||||
]
|
]
|
||||||
expected_messages = [
|
expected_messages = [
|
||||||
{
|
{
|
||||||
@ -712,7 +715,7 @@ def test__format_messages_with_cache_control() -> None:
|
|||||||
{"type": "text", "text": "foo", "cache_control": {"type": "ephemeral"}},
|
{"type": "text", "text": "foo", "cache_control": {"type": "ephemeral"}},
|
||||||
{"type": "text", "text": "foo"},
|
{"type": "text", "text": "foo"},
|
||||||
],
|
],
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
actual_system, actual_messages = _format_messages(messages)
|
actual_system, actual_messages = _format_messages(messages)
|
||||||
assert expected_system == actual_system
|
assert expected_system == actual_system
|
||||||
@ -733,8 +736,8 @@ def test__format_messages_with_cache_control() -> None:
|
|||||||
"data": "<base64 data>",
|
"data": "<base64 data>",
|
||||||
"cache_control": {"type": "ephemeral"},
|
"cache_control": {"type": "ephemeral"},
|
||||||
},
|
},
|
||||||
]
|
],
|
||||||
)
|
),
|
||||||
]
|
]
|
||||||
actual_system, actual_messages = _format_messages(messages)
|
actual_system, actual_messages = _format_messages(messages)
|
||||||
assert actual_system is None
|
assert actual_system is None
|
||||||
@ -756,7 +759,7 @@ def test__format_messages_with_cache_control() -> None:
|
|||||||
"cache_control": {"type": "ephemeral"},
|
"cache_control": {"type": "ephemeral"},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
assert actual_messages == expected_messages
|
assert actual_messages == expected_messages
|
||||||
|
|
||||||
@ -773,8 +776,8 @@ def test__format_messages_with_citations() -> None:
|
|||||||
"citations": {"enabled": True},
|
"citations": {"enabled": True},
|
||||||
},
|
},
|
||||||
{"type": "text", "text": "What color is the grass and sky?"},
|
{"type": "text", "text": "What color is the grass and sky?"},
|
||||||
]
|
],
|
||||||
)
|
),
|
||||||
]
|
]
|
||||||
expected_messages = [
|
expected_messages = [
|
||||||
{
|
{
|
||||||
@ -791,7 +794,7 @@ def test__format_messages_with_citations() -> None:
|
|||||||
},
|
},
|
||||||
{"type": "text", "text": "What color is the grass and sky?"},
|
{"type": "text", "text": "What color is the grass and sky?"},
|
||||||
],
|
],
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
actual_system, actual_messages = _format_messages(input_messages)
|
actual_system, actual_messages = _format_messages(input_messages)
|
||||||
assert actual_system is None
|
assert actual_system is None
|
||||||
@ -843,7 +846,7 @@ def test__format_messages_openai_image_format() -> None:
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
assert actual_messages == expected_messages
|
assert actual_messages == expected_messages
|
||||||
|
|
||||||
@ -856,7 +859,7 @@ def test__format_messages_with_multiple_system() -> None:
|
|||||||
SystemMessage(
|
SystemMessage(
|
||||||
[
|
[
|
||||||
{"type": "text", "text": "foo", "cache_control": {"type": "ephemeral"}},
|
{"type": "text", "text": "foo", "cache_control": {"type": "ephemeral"}},
|
||||||
]
|
],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
expected_system = [
|
expected_system = [
|
||||||
@ -880,7 +883,8 @@ def test_anthropic_api_key_is_secret_string() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_anthropic_api_key_masked_when_passed_from_env(
|
def test_anthropic_api_key_masked_when_passed_from_env(
|
||||||
monkeypatch: MonkeyPatch, capsys: CaptureFixture
|
monkeypatch: MonkeyPatch,
|
||||||
|
capsys: CaptureFixture,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test that the API key is masked when passed from an environment variable."""
|
"""Test that the API key is masked when passed from an environment variable."""
|
||||||
monkeypatch.setenv("ANTHROPIC_API_KEY ", "secret-api-key")
|
monkeypatch.setenv("ANTHROPIC_API_KEY ", "secret-api-key")
|
||||||
@ -920,7 +924,7 @@ def test_anthropic_uses_actual_secret_value_from_secretstr() -> None:
|
|||||||
|
|
||||||
|
|
||||||
class GetWeather(BaseModel):
|
class GetWeather(BaseModel):
|
||||||
"""Get the current weather in a given location"""
|
"""Get the current weather in a given location."""
|
||||||
|
|
||||||
location: str = Field(..., description="The city and state, e.g. San Francisco, CA")
|
location: str = Field(..., description="The city and state, e.g. San Francisco, CA")
|
||||||
|
|
||||||
@ -931,14 +935,16 @@ def test_anthropic_bind_tools_tool_choice() -> None:
|
|||||||
anthropic_api_key="secret-api-key",
|
anthropic_api_key="secret-api-key",
|
||||||
)
|
)
|
||||||
chat_model_with_tools = chat_model.bind_tools(
|
chat_model_with_tools = chat_model.bind_tools(
|
||||||
[GetWeather], tool_choice={"type": "tool", "name": "GetWeather"}
|
[GetWeather],
|
||||||
|
tool_choice={"type": "tool", "name": "GetWeather"},
|
||||||
)
|
)
|
||||||
assert cast(RunnableBinding, chat_model_with_tools).kwargs["tool_choice"] == {
|
assert cast(RunnableBinding, chat_model_with_tools).kwargs["tool_choice"] == {
|
||||||
"type": "tool",
|
"type": "tool",
|
||||||
"name": "GetWeather",
|
"name": "GetWeather",
|
||||||
}
|
}
|
||||||
chat_model_with_tools = chat_model.bind_tools(
|
chat_model_with_tools = chat_model.bind_tools(
|
||||||
[GetWeather], tool_choice="GetWeather"
|
[GetWeather],
|
||||||
|
tool_choice="GetWeather",
|
||||||
)
|
)
|
||||||
assert cast(RunnableBinding, chat_model_with_tools).kwargs["tool_choice"] == {
|
assert cast(RunnableBinding, chat_model_with_tools).kwargs["tool_choice"] == {
|
||||||
"type": "tool",
|
"type": "tool",
|
||||||
@ -946,11 +952,11 @@ def test_anthropic_bind_tools_tool_choice() -> None:
|
|||||||
}
|
}
|
||||||
chat_model_with_tools = chat_model.bind_tools([GetWeather], tool_choice="auto")
|
chat_model_with_tools = chat_model.bind_tools([GetWeather], tool_choice="auto")
|
||||||
assert cast(RunnableBinding, chat_model_with_tools).kwargs["tool_choice"] == {
|
assert cast(RunnableBinding, chat_model_with_tools).kwargs["tool_choice"] == {
|
||||||
"type": "auto"
|
"type": "auto",
|
||||||
}
|
}
|
||||||
chat_model_with_tools = chat_model.bind_tools([GetWeather], tool_choice="any")
|
chat_model_with_tools = chat_model.bind_tools([GetWeather], tool_choice="any")
|
||||||
assert cast(RunnableBinding, chat_model_with_tools).kwargs["tool_choice"] == {
|
assert cast(RunnableBinding, chat_model_with_tools).kwargs["tool_choice"] == {
|
||||||
"type": "any"
|
"type": "any",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -1021,7 +1027,6 @@ class FakeTracer(BaseTracer):
|
|||||||
|
|
||||||
def _persist_run(self, run: Run) -> None:
|
def _persist_run(self, run: Run) -> None:
|
||||||
"""Persist a run."""
|
"""Persist a run."""
|
||||||
pass
|
|
||||||
|
|
||||||
def on_chat_model_start(self, *args: Any, **kwargs: Any) -> Run:
|
def on_chat_model_start(self, *args: Any, **kwargs: Any) -> Run:
|
||||||
self.chat_model_start_inputs.append({"args": args, "kwargs": kwargs})
|
self.chat_model_start_inputs.append({"args": args, "kwargs": kwargs})
|
||||||
@ -1036,7 +1041,7 @@ def test_mcp_tracing() -> None:
|
|||||||
"url": "https://mcp.deepwiki.com/mcp",
|
"url": "https://mcp.deepwiki.com/mcp",
|
||||||
"name": "deepwiki",
|
"name": "deepwiki",
|
||||||
"authorization_token": "PLACEHOLDER",
|
"authorization_token": "PLACEHOLDER",
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
llm = ChatAnthropic(
|
llm = ChatAnthropic(
|
||||||
|
@ -95,7 +95,8 @@ def test_tools_output_parser_empty_content() -> None:
|
|||||||
chart_type: Literal["pie", "line", "bar"]
|
chart_type: Literal["pie", "line", "bar"]
|
||||||
|
|
||||||
output_parser = ToolsOutputParser(
|
output_parser = ToolsOutputParser(
|
||||||
first_tool_only=True, pydantic_schemas=[ChartType]
|
first_tool_only=True,
|
||||||
|
pydantic_schemas=[ChartType],
|
||||||
)
|
)
|
||||||
message = AIMessage(
|
message = AIMessage(
|
||||||
"",
|
"",
|
||||||
@ -105,7 +106,7 @@ def test_tools_output_parser_empty_content() -> None:
|
|||||||
"args": {"chart_type": "pie"},
|
"args": {"chart_type": "pie"},
|
||||||
"id": "foo",
|
"id": "foo",
|
||||||
"type": "tool_call",
|
"type": "tool_call",
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
actual = output_parser.invoke(message)
|
actual = output_parser.invoke(message)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
"""Standard LangChain interface tests"""
|
"""Standard LangChain interface tests."""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from langchain_core.language_models import BaseChatModel
|
from langchain_core.language_models import BaseChatModel
|
||||||
|
@ -477,7 +477,7 @@ requires-dist = [
|
|||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
codespell = [{ name = "codespell", specifier = ">=2.2.0,<3.0.0" }]
|
codespell = [{ name = "codespell", specifier = ">=2.2.0,<3.0.0" }]
|
||||||
dev = [{ name = "langchain-core", editable = "../../core" }]
|
dev = [{ name = "langchain-core", editable = "../../core" }]
|
||||||
lint = [{ name = "ruff", specifier = ">=0.5,<1.0" }]
|
lint = [{ name = "ruff", specifier = ">=0.12.2,<0.13" }]
|
||||||
test = [
|
test = [
|
||||||
{ name = "defusedxml", specifier = ">=0.7.1,<1.0.0" },
|
{ name = "defusedxml", specifier = ">=0.7.1,<1.0.0" },
|
||||||
{ name = "freezegun", specifier = ">=1.2.2,<2.0.0" },
|
{ name = "freezegun", specifier = ">=1.2.2,<2.0.0" },
|
||||||
@ -534,7 +534,7 @@ dev = [
|
|||||||
{ name = "jupyter", specifier = ">=1.0.0,<2.0.0" },
|
{ name = "jupyter", specifier = ">=1.0.0,<2.0.0" },
|
||||||
{ name = "setuptools", specifier = ">=67.6.1,<68.0.0" },
|
{ name = "setuptools", specifier = ">=67.6.1,<68.0.0" },
|
||||||
]
|
]
|
||||||
lint = [{ name = "ruff", specifier = ">=0.11.2,<0.12.0" }]
|
lint = [{ name = "ruff", specifier = ">=0.12.2,<0.13" }]
|
||||||
test = [
|
test = [
|
||||||
{ name = "blockbuster", specifier = "~=1.5.18" },
|
{ name = "blockbuster", specifier = "~=1.5.18" },
|
||||||
{ name = "freezegun", specifier = ">=1.2.2,<2.0.0" },
|
{ name = "freezegun", specifier = ">=1.2.2,<2.0.0" },
|
||||||
@ -598,7 +598,7 @@ requires-dist = [
|
|||||||
|
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
codespell = [{ name = "codespell", specifier = ">=2.2.0,<3.0.0" }]
|
codespell = [{ name = "codespell", specifier = ">=2.2.0,<3.0.0" }]
|
||||||
lint = [{ name = "ruff", specifier = ">=0.9.2,<1.0.0" }]
|
lint = [{ name = "ruff", specifier = ">=0.12.2,<0.13" }]
|
||||||
test = [{ name = "langchain-core", editable = "../../core" }]
|
test = [{ name = "langchain-core", editable = "../../core" }]
|
||||||
test-integration = []
|
test-integration = []
|
||||||
typing = [
|
typing = [
|
||||||
@ -1513,27 +1513,27 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.5.7"
|
version = "0.12.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/bf/2b/69e5e412f9d390adbdbcbf4f64d6914fa61b44b08839a6584655014fc524/ruff-0.5.7.tar.gz", hash = "sha256:8dfc0a458797f5d9fb622dd0efc52d796f23f0a1493a9527f4e49a550ae9a7e5", size = 2449817, upload-time = "2024-08-08T15:43:07.467Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/6c/3d/d9a195676f25d00dbfcf3cf95fdd4c685c497fcfa7e862a44ac5e4e96480/ruff-0.12.2.tar.gz", hash = "sha256:d7b4f55cd6f325cb7621244f19c873c565a08aff5a4ba9c69aa7355f3f7afd3e", size = 4432239, upload-time = "2025-07-03T16:40:19.566Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/6b/eb/06e06aaf96af30a68e83b357b037008c54a2ddcbad4f989535007c700394/ruff-0.5.7-py3-none-linux_armv6l.whl", hash = "sha256:548992d342fc404ee2e15a242cdbea4f8e39a52f2e7752d0e4cbe88d2d2f416a", size = 9570571, upload-time = "2024-08-08T15:41:56.537Z" },
|
{ url = "https://files.pythonhosted.org/packages/74/b6/2098d0126d2d3318fd5bec3ad40d06c25d377d95749f7a0c5af17129b3b1/ruff-0.12.2-py3-none-linux_armv6l.whl", hash = "sha256:093ea2b221df1d2b8e7ad92fc6ffdca40a2cb10d8564477a987b44fd4008a7be", size = 10369761, upload-time = "2025-07-03T16:39:38.847Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a4/10/1be32aeaab8728f78f673e7a47dd813222364479b2d6573dbcf0085e83ea/ruff-0.5.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00cc8872331055ee017c4f1071a8a31ca0809ccc0657da1d154a1d2abac5c0be", size = 8685138, upload-time = "2024-08-08T15:42:02.833Z" },
|
{ url = "https://files.pythonhosted.org/packages/b1/4b/5da0142033dbe155dc598cfb99262d8ee2449d76920ea92c4eeb9547c208/ruff-0.12.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:09e4cf27cc10f96b1708100fa851e0daf21767e9709e1649175355280e0d950e", size = 11155659, upload-time = "2025-07-03T16:39:42.294Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/3d/1d/c218ce83beb4394ba04d05e9aa2ae6ce9fba8405688fe878b0fdb40ce855/ruff-0.5.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf3d86a1fdac1aec8a3417a63587d93f906c678bb9ed0b796da7b59c1114a1e", size = 8266785, upload-time = "2024-08-08T15:42:08.321Z" },
|
{ url = "https://files.pythonhosted.org/packages/3e/21/967b82550a503d7c5c5c127d11c935344b35e8c521f52915fc858fb3e473/ruff-0.12.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8ae64755b22f4ff85e9c52d1f82644abd0b6b6b6deedceb74bd71f35c24044cc", size = 10537769, upload-time = "2025-07-03T16:39:44.75Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/26/79/7f49509bd844476235b40425756def366b227a9714191c91f02fb2178635/ruff-0.5.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a01c34400097b06cf8a6e61b35d6d456d5bd1ae6961542de18ec81eaf33b4cb8", size = 9983964, upload-time = "2024-08-08T15:42:12.419Z" },
|
{ url = "https://files.pythonhosted.org/packages/33/91/00cff7102e2ec71a4890fb7ba1803f2cdb122d82787c7d7cf8041fe8cbc1/ruff-0.12.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eb3a6b2db4d6e2c77e682f0b988d4d61aff06860158fdb413118ca133d57922", size = 10717602, upload-time = "2025-07-03T16:39:47.652Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/bf/b1/939836b70bf9fcd5e5cd3ea67fdb8abb9eac7631351d32f26544034a35e4/ruff-0.5.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcc8054f1a717e2213500edaddcf1dbb0abad40d98e1bd9d0ad364f75c763eea", size = 9359490, upload-time = "2024-08-08T15:42:16.713Z" },
|
{ url = "https://files.pythonhosted.org/packages/9b/eb/928814daec4e1ba9115858adcda44a637fb9010618721937491e4e2283b8/ruff-0.12.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:73448de992d05517170fc37169cbca857dfeaeaa8c2b9be494d7bcb0d36c8f4b", size = 10198772, upload-time = "2025-07-03T16:39:49.641Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/32/7d/b3db19207de105daad0c8b704b2c6f2a011f9c07017bd58d8d6e7b8eba19/ruff-0.5.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f70284e73f36558ef51602254451e50dd6cc479f8b6f8413a95fcb5db4a55fc", size = 10170833, upload-time = "2024-08-08T15:42:20.54Z" },
|
{ url = "https://files.pythonhosted.org/packages/50/fa/f15089bc20c40f4f72334f9145dde55ab2b680e51afb3b55422effbf2fb6/ruff-0.12.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b8b94317cbc2ae4a2771af641739f933934b03555e51515e6e021c64441532d", size = 11845173, upload-time = "2025-07-03T16:39:52.069Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a2/45/eae9da55f3357a1ac04220230b8b07800bf516e6dd7e1ad20a2ff3b03b1b/ruff-0.5.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a78ad870ae3c460394fc95437d43deb5c04b5c29297815a2a1de028903f19692", size = 10896360, upload-time = "2024-08-08T15:42:25.2Z" },
|
{ url = "https://files.pythonhosted.org/packages/43/9f/1f6f98f39f2b9302acc161a4a2187b1e3a97634fe918a8e731e591841cf4/ruff-0.12.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:45fc42c3bf1d30d2008023a0a9a0cfb06bf9835b147f11fe0679f21ae86d34b1", size = 12553002, upload-time = "2025-07-03T16:39:54.551Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/99/67/4388b36d145675f4c51ebec561fcd4298a0e2550c81e629116f83ce45a39/ruff-0.5.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ccd078c66a8e419475174bfe60a69adb36ce04f8d4e91b006f1329d5cd44bcf", size = 10477094, upload-time = "2024-08-08T15:42:29.553Z" },
|
{ url = "https://files.pythonhosted.org/packages/d8/70/08991ac46e38ddd231c8f4fd05ef189b1b94be8883e8c0c146a025c20a19/ruff-0.12.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce48f675c394c37e958bf229fb5c1e843e20945a6d962cf3ea20b7a107dcd9f4", size = 12171330, upload-time = "2025-07-03T16:39:57.55Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e1/9c/f5e6ed1751dc187a4ecf19a4970dd30a521c0ee66b7941c16e292a4043fb/ruff-0.5.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e31c9bad4ebf8fdb77b59cae75814440731060a09a0e0077d559a556453acbb", size = 11480896, upload-time = "2024-08-08T15:42:33.772Z" },
|
{ url = "https://files.pythonhosted.org/packages/88/a9/5a55266fec474acfd0a1c73285f19dd22461d95a538f29bba02edd07a5d9/ruff-0.12.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:793d8859445ea47591272021a81391350205a4af65a9392401f418a95dfb75c9", size = 11774717, upload-time = "2025-07-03T16:39:59.78Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c8/3b/2b683be597bbd02046678fc3fc1c199c641512b20212073b58f173822bb3/ruff-0.5.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d796327eed8e168164346b769dd9a27a70e0298d667b4ecee6877ce8095ec8e", size = 10179702, upload-time = "2024-08-08T15:42:38.038Z" },
|
{ url = "https://files.pythonhosted.org/packages/87/e5/0c270e458fc73c46c0d0f7cf970bb14786e5fdb88c87b5e423a4bd65232b/ruff-0.12.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6932323db80484dda89153da3d8e58164d01d6da86857c79f1961934354992da", size = 11646659, upload-time = "2025-07-03T16:40:01.934Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f1/38/c2d94054dc4b3d1ea4c2ba3439b2a7095f08d1c8184bc41e6abe2a688be7/ruff-0.5.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a09ea2c3f7778cc635e7f6edf57d566a8ee8f485f3c4454db7771efb692c499", size = 9982855, upload-time = "2024-08-08T15:42:42.031Z" },
|
{ url = "https://files.pythonhosted.org/packages/b7/b6/45ab96070c9752af37f0be364d849ed70e9ccede07675b0ec4e3ef76b63b/ruff-0.12.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6aa7e623a3a11538108f61e859ebf016c4f14a7e6e4eba1980190cacb57714ce", size = 10604012, upload-time = "2025-07-03T16:40:04.363Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7d/e7/1433db2da505ffa8912dcf5b28a8743012ee780cbc20ad0bf114787385d9/ruff-0.5.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a36d8dcf55b3a3bc353270d544fb170d75d2dff41eba5df57b4e0b67a95bb64e", size = 9433156, upload-time = "2024-08-08T15:42:45.339Z" },
|
{ url = "https://files.pythonhosted.org/packages/86/91/26a6e6a424eb147cc7627eebae095cfa0b4b337a7c1c413c447c9ebb72fd/ruff-0.12.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2a4a20aeed74671b2def096bdf2eac610c7d8ffcbf4fb0e627c06947a1d7078d", size = 10176799, upload-time = "2025-07-03T16:40:06.514Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e0/36/4fa43250e67741edeea3d366f59a1dc993d4d89ad493a36cbaa9889895f2/ruff-0.5.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9369c218f789eefbd1b8d82a8cf25017b523ac47d96b2f531eba73770971c9e5", size = 9782971, upload-time = "2024-08-08T15:42:49.354Z" },
|
{ url = "https://files.pythonhosted.org/packages/f5/0c/9f344583465a61c8918a7cda604226e77b2c548daf8ef7c2bfccf2b37200/ruff-0.12.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:71a4c550195612f486c9d1f2b045a600aeba851b298c667807ae933478fcef04", size = 11241507, upload-time = "2025-07-03T16:40:08.708Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/80/0e/8c276103d518e5cf9202f70630aaa494abf6fc71c04d87c08b6d3cd07a4b/ruff-0.5.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b88ca3db7eb377eb24fb7c82840546fb7acef75af4a74bd36e9ceb37a890257e", size = 10247775, upload-time = "2024-08-08T15:42:53.294Z" },
|
{ url = "https://files.pythonhosted.org/packages/1c/b7/99c34ded8fb5f86c0280278fa89a0066c3760edc326e935ce0b1550d315d/ruff-0.12.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:4987b8f4ceadf597c927beee65a5eaf994c6e2b631df963f86d8ad1bdea99342", size = 11717609, upload-time = "2025-07-03T16:40:10.836Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/cb/b9/673096d61276f39291b729dddde23c831a5833d98048349835782688a0ec/ruff-0.5.7-py3-none-win32.whl", hash = "sha256:33d61fc0e902198a3e55719f4be6b375b28f860b09c281e4bdbf783c0566576a", size = 7841772, upload-time = "2024-08-08T15:42:57.488Z" },
|
{ url = "https://files.pythonhosted.org/packages/51/de/8589fa724590faa057e5a6d171e7f2f6cffe3287406ef40e49c682c07d89/ruff-0.12.2-py3-none-win32.whl", hash = "sha256:369ffb69b70cd55b6c3fc453b9492d98aed98062db9fec828cdfd069555f5f1a", size = 10523823, upload-time = "2025-07-03T16:40:13.203Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/67/1c/4520c98bfc06b9c73cd1457686d4d3935d40046b1ddea08403e5a6deff51/ruff-0.5.7-py3-none-win_amd64.whl", hash = "sha256:083bbcbe6fadb93cd86709037acc510f86eed5a314203079df174c40bbbca6b3", size = 8699779, upload-time = "2024-08-08T15:43:00.429Z" },
|
{ url = "https://files.pythonhosted.org/packages/94/47/8abf129102ae4c90cba0c2199a1a9b0fa896f6f806238d6f8c14448cc748/ruff-0.12.2-py3-none-win_amd64.whl", hash = "sha256:dca8a3b6d6dc9810ed8f328d406516bf4d660c00caeaef36eb831cf4871b0639", size = 11629831, upload-time = "2025-07-03T16:40:15.478Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/38/23/b3763a237d2523d40a31fe2d1a301191fe392dd48d3014977d079cf8c0bd/ruff-0.5.7-py3-none-win_arm64.whl", hash = "sha256:2dca26154ff9571995107221d0aeaad0e75a77b5a682d6236cf89a58c70b76f4", size = 8091891, upload-time = "2024-08-08T15:43:04.162Z" },
|
{ url = "https://files.pythonhosted.org/packages/e2/1f/72d2946e3cc7456bb837e88000eb3437e55f80db339c840c04015a11115d/ruff-0.12.2-py3-none-win_arm64.whl", hash = "sha256:48d6c6bfb4761df68bc05ae630e24f506755e702d4fb08f08460be778c7ccb12", size = 10735334, upload-time = "2025-07-03T16:40:17.677Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
Loading…
Reference in New Issue
Block a user