Source code for agentscope.formatters._formatter_base

# -*- coding: utf-8 -*-
"""The base class for formatters."""
import collections
import re
from abc import abstractmethod, ABC
from typing import Union, Any

from ..message import Msg


[docs] class FormatterBase(ABC): """The base class for formatters.""" supported_model_regexes: list[str] """The supported model regexes"""
[docs] @classmethod def is_supported_model(cls, model_name: str) -> bool: """Check if the provided model_name is supported by the formatter.""" for regex in cls.supported_model_regexes: if re.match(regex, model_name): return True return False
[docs] @classmethod def format_auto( cls, msgs: Union[Msg, list[Msg]], ) -> list[dict]: """This function will decide which format function to use between `format_chat` and `format_multi_agent` based on the roles and names in the input messages. - If only "user" and "assistant" (or "system") roles are present and only two names are present, then `format_chat` will be used. - If more than two names are involved, then `format_multi_agent` will be used. """ names_mapping = collections.defaultdict(list) for msg in msgs: names_mapping[msg.role].append(msg.name) if len(names_mapping["user"]) + len(names_mapping["assistant"]) <= 2: return cls.format_chat(msgs) else: return cls.format_multi_agent(msgs)
[docs] @classmethod @abstractmethod def format_chat(cls, *args: Any, **kwargs: Any) -> list[dict]: """Format the messages in chat scenario, where only one user and one assistant are involved."""
[docs] @classmethod @abstractmethod def format_multi_agent(cls, *args: Any, **kwargs: Any) -> list[dict]: """Format the messages in multi-agent scenario, where multiple agents are involved."""
@classmethod def _format_chat_for_common_models( cls, *msgs: Union[Msg, list[Msg], None], require_alternative: bool = False, require_user_last: bool = False, ) -> list[dict]: """Format the messages for common LLMs in chat scenario, where only user and assistant are involved. When `require_alternative` or `require_user_last` is set to `True`, but the input messages do not meet the requirement, we will use the strategy in `format_multi_agent_for_common_models` instead, which gather all messages into one system message(if provided) and one user message. Args: msgs (`Union[Msg, list[Msg], None]`): The message(s) to be formatted. The `None` input will be ignored. require_alternative (`bool`, optional): If the model API requires the roles to be "user" and "model" alternatively. require_user_last (`bool`, optional): Whether the user message should be placed at the end. Defaults to `False`. """ msgs = cls.check_and_flat_messages(*msgs) if require_alternative: meet_alternative = True for i in range(len(msgs) - 1): if msgs[i].role == msgs[i + 1].role: meet_alternative = False break if not meet_alternative: # If not meet the requirement, we use the multi-agent format # instead, which will combine all messages into one. return cls.format_multi_agent(msgs) if require_user_last: if msgs[-1].role != "user": return cls.format_multi_agent(msgs) formatted_msgs = [] for msg in msgs: formatted_msgs.append( { "role": msg.role, "content": msg.get_text_content(), }, ) return formatted_msgs @classmethod def _format_multi_agent_for_common_models( cls, *msgs: Union[Msg, list[Msg], None], ) -> list[dict]: """A common format strategy for chat models, which will format the input messages into a system message (if provided) and a user message. Note this strategy maybe not suitable for all scenarios, and developers are encouraged to implement their own prompt engineering strategies. The following is an example: .. code-block:: python prompt1 = model.format( Msg("system", "You're a helpful assistant", role="system"), Msg("Bob", "Hi, how can I help you?", role="assistant"), Msg("user", "What's the date today?", role="user") ) prompt2 = model.format( Msg("Bob", "Hi, how can I help you?", role="assistant"), Msg("user", "What's the date today?", role="user") ) The prompt will be as follows: .. code-block:: python # prompt1 [ { "role": "system", "content": "You're a helpful assistant" }, { "role": "user", "content": ( "## Conversation History\\n" "Bob: Hi, how can I help you?\\n" "user: What's the date today?" ) } ] # prompt2 [ { "role": "user", "content": ( "## Conversation History\\n" "Bob: Hi, how can I help you?\\n" "user: What's the date today?" ) } ] Args: msgs (`Union[Msg, list[Msg], None]`): The input arguments to be formatted, where each argument should be a `Msg` object, or a list of `Msg` objects. The `None` input will be ignored. Returns: `List[dict]`: The formatted messages. """ if len(msgs) == 0: raise ValueError( "At least one message should be provided. An empty message " "list is not allowed.", ) # Parse all information into a list of messages input_msgs = cls.check_and_flat_messages(*msgs) # record dialog history as a list of strings dialogue = [] sys_prompt = None for i, msg in enumerate(input_msgs): if i == 0 and msg.role == "system": # if system prompt is available, place it at the beginning sys_prompt = msg.get_text_content() else: # Merge all messages into a conversation history prompt text_content = msg.get_text_content() if text_content is not None: dialogue.append( f"{msg.name}: {msg.get_text_content()}", ) content_components = [] # The conversation history is added to the user message if not empty if len(dialogue) > 0: content_components.extend(["## Conversation History"] + dialogue) messages = [ { "role": "user", "content": "\n".join(content_components), }, ] # Add system prompt at the beginning if provided if sys_prompt is not None: messages = [{"role": "system", "content": sys_prompt}] + messages return messages
[docs] @staticmethod def check_and_flat_messages( *msgs: Union[Msg, list[Msg], None], ) -> list[Msg]: """Check the input messages.""" input_msgs = [] for _ in msgs: if _ is None: continue if isinstance(_, Msg): input_msgs.append(_) elif isinstance(_, list) and all(isinstance(__, Msg) for __ in _): input_msgs.extend(_) else: raise TypeError( f"The input should be a Msg object or a list " f"of Msg objects, got {type(_)}.", ) return input_msgs
[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 API provider expects.""" raise NotImplementedError( f"The method `format_tools_json_schemas` is not implemented yet " f"in {cls.__name__}.", )