Source code for agentscope.formatters._openai_formatter
# -*- coding: utf-8 -*-
"""The formatter for OpenAI models."""
import json
from typing import Union
from loguru import logger
from ._formatter_base import FormatterBase
from ..message import Msg
from ..utils.common import _to_openai_image_url
[docs]
class OpenAIFormatter(FormatterBase):
"""The formatter for OpenAI models, which is responsible for formatting
messages, JSON schemas description of the tool functions."""
supported_model_regexes: list[str] = [
"gpt-.*",
"o1",
"o1-mini",
"o3-mini",
]
[docs]
@classmethod
def format_chat(
cls,
*msgs: Union[Msg, list[Msg], None],
) -> list[dict]:
"""Format the messages in chat scenario, where only one user and one
assistant are involved.
For OpenAI models, the `name` field can be used to distinguish
different agents (even with the same role as `"assistant"`). So we
simply reuse the `format_multi_agent` here.
"""
return cls.format_multi_agent(*msgs)
[docs]
@classmethod
def format_multi_agent(
cls,
*msgs: Union[Msg, list[Msg], None],
) -> list[dict]:
"""Format the messages in multi-agent scenario, where multiple agents
are involved.
For OpenAI models, the `name` field can be used to distinguish
different agents (even with the same role as `"assistant"`).
"""
msgs = cls.check_and_flat_messages(*msgs)
messages = []
for msg in msgs:
content_blocks = []
tool_calls = []
for block in msg.get_content_blocks():
typ = block.get("type")
if typ == "text":
content_blocks.append({**block})
elif typ == "tool_use":
tool_calls.append(
{
"id": block.get("id"),
"type": "function",
"function": {
"name": block.get("name"),
"arguments": json.dumps(
block.get("input", {}),
ensure_ascii=False,
),
},
},
)
elif typ == "tool_result":
messages.append(
{
"role": "tool",
"tool_call_id": block.get("id"),
"content": str(block.get("output")),
"name": block.get("name"),
},
)
elif typ == "image":
content_blocks.append(
{
"type": "image_url",
"image_url": {
"url": _to_openai_image_url(
str(block.get("url")),
),
},
},
)
else:
logger.warning(
f"Unsupported block type {typ} in the message, "
f"skipped.",
)
msg_openai = {
"role": msg.role,
"name": msg.name,
"content": content_blocks or None,
}
if tool_calls:
msg_openai["tool_calls"] = tool_calls
# When both content and tool_calls are None, skipped
if msg_openai["content"] or msg_openai.get("tool_calls"):
messages.append(msg_openai)
return messages
[docs]
@classmethod
def format_tools_json_schemas(cls, schemas: dict[str, dict]) -> list[dict]:
"""Format the JSON schemas of the tool functions to the format that
OpenAI API expects. This function will take the parsed JSON schema
from `agentscope.service.ServiceToolkit` as input and return
the formatted JSON schema.
Note:
An example of the input tool JSON schema
..code-block:: json
{
"bing_search": {
"type": "function",
"function": {
"name": "bing_search",
"description": "Search the web using Bing.",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query.",
}
},
"required": ["query"],
}
}
}
}
Args:
schemas (`dict[str, dict]`):
The JSON schema of the tool functions.
Returns:
`list[dict]`:
The formatted JSON schema.
"""
# The schemas from ServiceToolkit is exactly the same with the format
# that OpenAI API expects, so we just return the input schemas.
assert isinstance(
schemas,
dict,
), f"Expect dict of schemas, got {type(schemas)}."
for schema in schemas.values():
assert isinstance(
schema,
dict,
), f"Expect dict schema, got {type(schema)}."
assert (
"type" in schema and "function" in schema
), f"Invalid schema: {schema}, expect keys 'type' and 'function'."
assert (
schema["type"] == "function"
), f"Invalid schema type: {schema['type']}, expect 'function'."
assert "name" in schema["function"], (
f"Invalid schema: {schema}, "
f"expect key 'name' in 'function' field."
)
return list(schemas.values())