Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/unittest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ jobs:
- name: Update setuptools
run: |
pip install setuptools==78.1.1 wheel==0.45.1
- name: Install Full Dependencies
- name: Install Dev Dependencies
run: |
pip install -q -e .[full]
pip install -q -e .[dev]
pip install coverage pytest
- name: Run tests with coverage
run: |
Expand Down
62 changes: 61 additions & 1 deletion src/agentscope/_utils/_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@
import types
import typing
from datetime import datetime
from typing import Union, Any, Callable
from typing import Union, Any, Callable, Type, Dict

import requests
from json_repair import repair_json
from pydantic import BaseModel

from .._logging import logger

Expand Down Expand Up @@ -208,3 +209,62 @@ def _remove_title_field(schema: dict) -> None:
_remove_title_field(
schema["additionalProperties"],
)


def _create_tool_from_base_model(
structured_model: Type[BaseModel],
tool_name: str = "generate_structured_output",
) -> Dict[str, Any]:
"""Create a function tool definition from a Pydantic BaseModel.
This function converts a Pydantic BaseModel class into a tool definition
that can be used with function calling API. The resulting tool
definition includes the model's JSON schema as parameters, enabling
structured output generation by forcing the model to call this function
with properly formatted data.

Args:
structured_model (`Type[BaseModel]`):
A Pydantic BaseModel class that defines the expected structure
for the tool's output.
tool_name (`str`, default `"generate_structured_output"`):
The tool name that used to force the LLM to generate structured
output by calling this function.

Returns:
`Dict[str, Any]`: A tool definition dictionary compatible with
function calling API, containing type ("function") and
function dictionary with name, description, and parameters
(JSON schema).

.. code-block:: python
:caption: Example usage

from pydantic import BaseModel

class PersonInfo(BaseModel):
name: str
age: int
email: str

tool = _create_tool_from_base_model(PersonInfo, "extract_person")
print(tool["function"]["name"]) # extract_person
print(tool["type"]) # function

.. note:: The function automatically removes the 'title' field from
the JSON schema to ensure compatibility with function calling
format. This is handled by the internal ``_remove_title_field()``
function.
"""
schema = structured_model.model_json_schema()

_remove_title_field(schema)
tool_definition = {
"type": "function",
"function": {
"name": tool_name,
"description": "Generate the required structured output with "
"this function",
"parameters": schema,
},
}
return tool_definition
98 changes: 93 additions & 5 deletions src/agentscope/model/_anthropic_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,20 @@
TYPE_CHECKING,
List,
Literal,
Type,
)
from collections import OrderedDict

from pydantic import BaseModel

from ._model_base import ChatModelBase
from ._model_response import ChatResponse
from ._model_usage import ChatUsage
from .._utils._common import _json_loads_with_repair
from .._logging import logger
from .._utils._common import (
_json_loads_with_repair,
_create_tool_from_base_model,
)
from ..message import TextBlock, ToolUseBlock, ThinkingBlock
from ..tracing import trace_llm
from ..types._json import JSONSerializableObject
Expand Down Expand Up @@ -97,6 +104,7 @@ async def __call__(
tool_choice: Literal["auto", "none", "any", "required"]
| str
| None = None,
structured_model: Type[BaseModel] | None = None,
**generate_kwargs: Any,
) -> ChatResponse | AsyncGenerator[ChatResponse, None]:
"""Get the response from Anthropic chat completions API by the given
Expand Down Expand Up @@ -132,12 +140,26 @@ async def __call__(
},
# More schemas here
]

tool_choice (`Literal["auto", "none", "any", "required"] | str \
| None`, default `None`):
Controls which (if any) tool is called by the model.
Can be "auto", "none", "any", "required", or specific tool
name. For more details, please refer to
https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/implement-tool-use
structured_model (`Type[BaseModel] | None`, default `None`):
A Pydantic BaseModel class that defines the expected structure
for the model's output. When provided, the model will be forced
to return data that conforms to this schema by automatically
converting the BaseModel to a tool function and setting
`tool_choice` to enforce its usage. This enables structured
output generation.

.. note:: When `structured_model` is specified,
both `tools` and `tool_choice` parameters are ignored,
and the model will only perform structured output
generation without calling any other tools.

**generate_kwargs (`Any`):
The keyword arguments for Anthropic chat completions API,
e.g. `temperature`, `top_p`, etc. Please
Expand All @@ -164,6 +186,22 @@ async def __call__(
self._validate_tool_choice(tool_choice, tools)
kwargs["tool_choice"] = self._format_tool_choice(tool_choice)

if structured_model:
if tools or tool_choice:
logger.warning(
"structured_model is provided. Both 'tools' and "
"'tool_choice' parameters will be overridden and "
"ignored. The model will only perform structured output "
"generation without calling any other tools.",
)
format_tool = _create_tool_from_base_model(structured_model)
kwargs["tools"] = self._format_tools_json_schemas(
[format_tool],
)
kwargs["tool_choice"] = self._format_tool_choice(
format_tool["function"]["name"],
)

# Extract the system message
if messages[0]["role"] == "system":
kwargs["system"] = messages[0]["content"]
Expand All @@ -179,12 +217,14 @@ async def __call__(
return self._parse_anthropic_stream_completion_response(
start_datetime,
response,
structured_model,
)

# Non-streaming response
parsed_response = await self._parse_anthropic_completion_response(
start_datetime,
response,
structured_model,
)

return parsed_response
Expand All @@ -193,10 +233,30 @@ async def _parse_anthropic_completion_response(
self,
start_datetime: datetime,
response: Message,
structured_model: Type[BaseModel] | None = None,
) -> ChatResponse:
"""Parse the Anthropic chat completion response into a `ChatResponse`
object."""
"""Given an Anthropic Message object, extract the content blocks and
usages from it.

Args:
start_datetime (`datetime`):
The start datetime of the response generation.
response (`Message`):
Anthropic Message object to parse.
structured_model (`Type[BaseModel] | None`, default `None`):
A Pydantic BaseModel class that defines the expected structure
for the model's output.

Returns:
ChatResponse (`ChatResponse`):
A ChatResponse object containing the content blocks and usage.

.. note::
If `structured_model` is not `None`, the expected structured output
will be stored in the metadata of the `ChatResponse`.
"""
content_blocks: List[ThinkingBlock | TextBlock | ToolUseBlock] = []
metadata = None

if hasattr(response, "content") and response.content:
for content_block in response.content:
Expand Down Expand Up @@ -231,6 +291,8 @@ async def _parse_anthropic_completion_response(
input=content_block.input,
),
)
if structured_model:
metadata = content_block.input

usage = None
if response.usage:
Expand All @@ -243,6 +305,7 @@ async def _parse_anthropic_completion_response(
parsed_response = ChatResponse(
content=content_blocks,
usage=usage,
metadata=metadata,
)

return parsed_response
Expand All @@ -251,9 +314,30 @@ async def _parse_anthropic_stream_completion_response(
self,
start_datetime: datetime,
response: AsyncStream,
structured_model: Type[BaseModel] | None = None,
) -> AsyncGenerator[ChatResponse, None]:
"""Parse the Anthropic chat completion response stream into an async
generator of `ChatResponse` objects."""
"""Given an Anthropic streaming response, extract the content blocks
and usages from it and yield ChatResponse objects.

Args:
start_datetime (`datetime`):
The start datetime of the response generation.
response (`AsyncStream`):
Anthropic AsyncStream object to parse.
structured_model (`Type[BaseModel] | None`, default `None`):
A Pydantic BaseModel class that defines the expected structure
for the model's output.

Returns:
`AsyncGenerator[ChatResponse, None]`:
An async generator that yields ChatResponse objects containing
the content blocks and usage information for each chunk in
the streaming response.

.. note::
If `structured_model` is not `None`, the expected structured output
will be stored in the metadata of the `ChatResponse`.
"""

usage = None
text_buffer = ""
Expand All @@ -262,6 +346,7 @@ async def _parse_anthropic_stream_completion_response(
tool_calls = OrderedDict()
tool_call_buffers = {}
res = None
metadata = None

async for event in response:
content_changed = False
Expand Down Expand Up @@ -352,10 +437,13 @@ async def _parse_anthropic_stream_completion_response(
input=input_obj,
),
)
if structured_model:
metadata = input_obj
if contents:
res = ChatResponse(
content=contents,
usage=usage,
metadata=metadata,
)
yield res

Expand Down
Loading
Loading