init
This commit is contained in:
189
AlgoriAgent/src/agentscope/models/__init__.py
Normal file
189
AlgoriAgent/src/agentscope/models/__init__.py
Normal file
@@ -0,0 +1,189 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
""" Import modules in models package."""
|
||||
import json
|
||||
from typing import Union, Type
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from .config import _ModelConfig
|
||||
from .model import ModelWrapperBase
|
||||
from .response import ModelResponse
|
||||
from .post_model import (
|
||||
PostAPIModelWrapperBase,
|
||||
PostAPIChatWrapper,
|
||||
)
|
||||
from .openai_model import (
|
||||
OpenAIWrapperBase,
|
||||
OpenAIChatWrapper,
|
||||
OpenAIDALLEWrapper,
|
||||
OpenAIEmbeddingWrapper,
|
||||
)
|
||||
from .dashscope_model import (
|
||||
DashScopeChatWrapper,
|
||||
DashScopeImageSynthesisWrapper,
|
||||
DashScopeTextEmbeddingWrapper,
|
||||
DashScopeMultiModalWrapper,
|
||||
)
|
||||
from .ollama_model import (
|
||||
OllamaChatWrapper,
|
||||
OllamaEmbeddingWrapper,
|
||||
OllamaGenerationWrapper,
|
||||
)
|
||||
from .gemini_model import (
|
||||
GeminiChatWrapper,
|
||||
GeminiEmbeddingWrapper,
|
||||
)
|
||||
from .zhipu_model import (
|
||||
ZhipuAIChatWrapper,
|
||||
ZhipuAIEmbeddingWrapper,
|
||||
)
|
||||
from .litellm_model import (
|
||||
LiteLLMChatWrapper,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ModelWrapperBase",
|
||||
"ModelResponse",
|
||||
"PostAPIModelWrapperBase",
|
||||
"PostAPIChatWrapper",
|
||||
"OpenAIWrapperBase",
|
||||
"OpenAIChatWrapper",
|
||||
"OpenAIDALLEWrapper",
|
||||
"OpenAIEmbeddingWrapper",
|
||||
"DashScopeChatWrapper",
|
||||
"DashScopeImageSynthesisWrapper",
|
||||
"DashScopeTextEmbeddingWrapper",
|
||||
"DashScopeMultiModalWrapper",
|
||||
"OllamaChatWrapper",
|
||||
"OllamaEmbeddingWrapper",
|
||||
"OllamaGenerationWrapper",
|
||||
"GeminiChatWrapper",
|
||||
"GeminiEmbeddingWrapper",
|
||||
"ZhipuAIChatWrapper",
|
||||
"ZhipuAIEmbeddingWrapper",
|
||||
"LiteLLMChatWrapper",
|
||||
"load_model_by_config_name",
|
||||
"load_config_by_name",
|
||||
"read_model_configs",
|
||||
"clear_model_configs",
|
||||
]
|
||||
|
||||
_MODEL_CONFIGS: dict[str, dict] = {}
|
||||
|
||||
|
||||
def _get_model_wrapper(model_type: str) -> Type[ModelWrapperBase]:
|
||||
"""Get the specific type of model wrapper
|
||||
|
||||
Args:
|
||||
model_type (`str`): The model type name.
|
||||
|
||||
Returns:
|
||||
`Type[ModelWrapperBase]`: The corresponding model wrapper class.
|
||||
"""
|
||||
wrapper = ModelWrapperBase.get_wrapper(model_type=model_type)
|
||||
if wrapper is None:
|
||||
logger.warning(
|
||||
f"Unsupported model_type [{model_type}],"
|
||||
"use PostApiModelWrapper instead.",
|
||||
)
|
||||
return PostAPIModelWrapperBase
|
||||
return wrapper
|
||||
|
||||
|
||||
def load_config_by_name(config_name: str) -> Union[dict, None]:
|
||||
"""Load the model config by name, and return the config dict."""
|
||||
return _MODEL_CONFIGS.get(config_name, None)
|
||||
|
||||
|
||||
def load_model_by_config_name(config_name: str) -> ModelWrapperBase:
|
||||
"""Load the model by config name, and return the model wrapper."""
|
||||
if len(_MODEL_CONFIGS) == 0:
|
||||
raise ValueError(
|
||||
"No model configs loaded, please call "
|
||||
"`read_model_configs` first.",
|
||||
)
|
||||
|
||||
# Find model config by name
|
||||
if config_name not in _MODEL_CONFIGS:
|
||||
raise ValueError(
|
||||
f"Cannot find [{config_name}] in loaded configurations.",
|
||||
)
|
||||
config = _MODEL_CONFIGS.get(config_name, None)
|
||||
|
||||
if config is None:
|
||||
raise ValueError(
|
||||
f"Cannot find [{config_name}] in loaded configurations.",
|
||||
)
|
||||
|
||||
model_type = config.model_type
|
||||
|
||||
kwargs = {k: v for k, v in config.items() if k != "model_type"}
|
||||
|
||||
return _get_model_wrapper(model_type=model_type)(**kwargs)
|
||||
|
||||
|
||||
def clear_model_configs() -> None:
|
||||
"""Clear the loaded model configs."""
|
||||
_MODEL_CONFIGS.clear()
|
||||
|
||||
|
||||
def read_model_configs(
|
||||
configs: Union[dict, str, list],
|
||||
clear_existing: bool = False,
|
||||
) -> None:
|
||||
"""read model configs from a path or a list of dicts.
|
||||
|
||||
Args:
|
||||
configs (`Union[str, list, dict]`):
|
||||
The path of the model configs | a config dict | a list of model
|
||||
configs.
|
||||
clear_existing (`bool`, defaults to `False`):
|
||||
Whether to clear the loaded model configs before reading.
|
||||
|
||||
Returns:
|
||||
`dict`:
|
||||
The model configs.
|
||||
"""
|
||||
if clear_existing:
|
||||
clear_model_configs()
|
||||
|
||||
cfgs = None
|
||||
|
||||
if isinstance(configs, str):
|
||||
with open(configs, "r", encoding="utf-8") as f:
|
||||
cfgs = json.load(f)
|
||||
|
||||
if isinstance(configs, dict):
|
||||
cfgs = [configs]
|
||||
|
||||
if isinstance(configs, list):
|
||||
if not all(isinstance(_, dict) for _ in configs):
|
||||
raise ValueError(
|
||||
"The model config unit should be a dict.",
|
||||
)
|
||||
cfgs = configs
|
||||
|
||||
if cfgs is None:
|
||||
raise TypeError(
|
||||
f"Invalid type of model_configs, it could be a dict, a list of "
|
||||
f"dicts, or a path to a json file (containing a dict or a list "
|
||||
f"of dicts), but got {type(configs)}",
|
||||
)
|
||||
|
||||
format_configs = _ModelConfig.format_configs(configs=cfgs)
|
||||
|
||||
# check if name is unique
|
||||
for cfg in format_configs:
|
||||
if cfg.config_name in _MODEL_CONFIGS:
|
||||
logger.warning(
|
||||
f"config_name [{cfg.config_name}] already exists.",
|
||||
)
|
||||
continue
|
||||
_MODEL_CONFIGS[cfg.config_name] = cfg
|
||||
|
||||
# print the loaded model configs
|
||||
logger.info(
|
||||
"Load configs for model wrapper: {}",
|
||||
", ".join(_MODEL_CONFIGS.keys()),
|
||||
)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
59
AlgoriAgent/src/agentscope/models/config.py
Normal file
59
AlgoriAgent/src/agentscope/models/config.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""The model config."""
|
||||
from typing import Union, Sequence, Any
|
||||
|
||||
from loguru import logger
|
||||
|
||||
|
||||
class _ModelConfig(dict):
|
||||
"""Base class for model config."""
|
||||
|
||||
__getattr__ = dict.__getitem__
|
||||
__setattr__ = dict.__setitem__
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config_name: str,
|
||||
model_type: str = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""Initialize the config with the given arguments, and checking the
|
||||
type of the arguments.
|
||||
|
||||
Args:
|
||||
config_name (`str`): A unique name of the model config.
|
||||
model_type (`str`, optional): The class name (or its model type) of
|
||||
the generated model wrapper. Defaults to None.
|
||||
|
||||
Raises:
|
||||
`ValueError`: If `config_name` is not provided.
|
||||
"""
|
||||
if config_name is None:
|
||||
raise ValueError("The `config_name` field is required for Cfg")
|
||||
if model_type is None:
|
||||
logger.warning(
|
||||
f"`model_type` is not provided in config [{config_name}],"
|
||||
" use `PostAPIModelWrapperBase` by default.",
|
||||
)
|
||||
super().__init__(
|
||||
config_name=config_name,
|
||||
model_type=model_type,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def format_configs(
|
||||
cls,
|
||||
configs: Union[Sequence[dict], dict],
|
||||
) -> Sequence:
|
||||
"""Covert config dicts into a list of _ModelConfig.
|
||||
|
||||
Args:
|
||||
configs (Union[Sequence[dict], dict]): configs in dict format.
|
||||
|
||||
Returns:
|
||||
Sequence[_ModelConfig]: converted ModelConfig list.
|
||||
"""
|
||||
if isinstance(configs, dict):
|
||||
return [_ModelConfig(**configs)]
|
||||
return [_ModelConfig(**cfg) for cfg in configs]
|
||||
847
AlgoriAgent/src/agentscope/models/dashscope_model.py
Normal file
847
AlgoriAgent/src/agentscope/models/dashscope_model.py
Normal file
@@ -0,0 +1,847 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Model wrapper for DashScope models"""
|
||||
import os
|
||||
from abc import ABC
|
||||
from http import HTTPStatus
|
||||
from typing import Any, Union, List, Sequence
|
||||
from loguru import logger
|
||||
|
||||
from ..message import Msg
|
||||
from ..utils.tools import _convert_to_str, _guess_type_by_extension
|
||||
|
||||
try:
|
||||
import dashscope
|
||||
except ImportError:
|
||||
dashscope = None
|
||||
|
||||
from .model import ModelWrapperBase, ModelResponse
|
||||
|
||||
from ..file_manager import file_manager
|
||||
|
||||
|
||||
class DashScopeWrapperBase(ModelWrapperBase, ABC):
|
||||
"""The model wrapper for DashScope API."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config_name: str,
|
||||
model_name: str = None,
|
||||
api_key: str = None,
|
||||
generate_args: dict = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize the DashScope wrapper.
|
||||
|
||||
Args:
|
||||
config_name (`str`):
|
||||
The name of the model config.
|
||||
model_name (`str`, default `None`):
|
||||
The name of the model to use in DashScope API.
|
||||
api_key (`str`, default `None`):
|
||||
The API key for DashScope API.
|
||||
generate_args (`dict`, default `None`):
|
||||
The extra keyword arguments used in DashScope api generation,
|
||||
e.g. `temperature`, `seed`.
|
||||
"""
|
||||
if model_name is None:
|
||||
model_name = config_name
|
||||
logger.warning("model_name is not set, use config_name instead.")
|
||||
|
||||
super().__init__(config_name=config_name)
|
||||
|
||||
if dashscope is None:
|
||||
raise ImportError(
|
||||
"Cannot find dashscope package in current python environment.",
|
||||
)
|
||||
|
||||
self.model_name = model_name
|
||||
self.generate_args = generate_args or {}
|
||||
|
||||
self.api_key = api_key
|
||||
dashscope.api_key = self.api_key
|
||||
self.max_length = None
|
||||
|
||||
# Set monitor accordingly
|
||||
self._register_default_metrics()
|
||||
|
||||
def format(
|
||||
self,
|
||||
*args: Union[Msg, Sequence[Msg]],
|
||||
) -> Union[List[dict], str]:
|
||||
raise RuntimeError(
|
||||
f"Model Wrapper [{type(self).__name__}] doesn't "
|
||||
f"need to format the input. Please try to use the "
|
||||
f"model wrapper directly.",
|
||||
)
|
||||
|
||||
|
||||
class DashScopeChatWrapper(DashScopeWrapperBase):
|
||||
"""The model wrapper for DashScope's chat API, refer to
|
||||
https://help.aliyun.com/zh/dashscope/developer-reference/api-details
|
||||
"""
|
||||
|
||||
model_type: str = "dashscope_chat"
|
||||
|
||||
deprecated_model_type: str = "tongyi_chat"
|
||||
|
||||
def _register_default_metrics(self) -> None:
|
||||
# Set monitor accordingly
|
||||
# TODO: set quota to the following metrics
|
||||
self.monitor.register(
|
||||
self._metric("call_counter"),
|
||||
metric_unit="times",
|
||||
)
|
||||
self.monitor.register(
|
||||
self._metric("prompt_tokens"),
|
||||
metric_unit="token",
|
||||
)
|
||||
self.monitor.register(
|
||||
self._metric("completion_tokens"),
|
||||
metric_unit="token",
|
||||
)
|
||||
self.monitor.register(
|
||||
self._metric("total_tokens"),
|
||||
metric_unit="token",
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
messages: list,
|
||||
**kwargs: Any,
|
||||
) -> ModelResponse:
|
||||
"""Processes a list of messages to construct a payload for the
|
||||
DashScope API call. It then makes a request to the DashScope API
|
||||
and returns the response. This method also updates monitoring
|
||||
metrics based on the API response.
|
||||
|
||||
Each message in the 'messages' list can contain text content and
|
||||
optionally an 'image_urls' key. If 'image_urls' is provided,
|
||||
it is expected to be a list of strings representing URLs to images.
|
||||
These URLs will be transformed to a suitable format for the DashScope
|
||||
API, which might involve converting local file paths to data URIs.
|
||||
|
||||
Args:
|
||||
messages (`list`):
|
||||
A list of messages to process.
|
||||
**kwargs (`Any`):
|
||||
The keyword arguments to DashScope chat completions API,
|
||||
e.g. `temperature`, `max_tokens`, `top_p`, etc. Please
|
||||
refer to
|
||||
https://help.aliyun.com/zh/dashscope/developer-reference/api-details
|
||||
for more detailed arguments.
|
||||
|
||||
Returns:
|
||||
`ModelResponse`:
|
||||
The response text in text field, and the raw response in
|
||||
raw field.
|
||||
|
||||
Note:
|
||||
`parse_func`, `fault_handler` and `max_retries` are reserved for
|
||||
`_response_parse_decorator` to parse and check the response
|
||||
generated by model wrapper. Their usages are listed as follows:
|
||||
- `parse_func` is a callable function used to parse and check
|
||||
the response generated by the model, which takes the response
|
||||
as input.
|
||||
- `max_retries` is the maximum number of retries when the
|
||||
`parse_func` raise an exception.
|
||||
- `fault_handler` is a callable function which is called
|
||||
when the response generated by the model is invalid after
|
||||
`max_retries` retries.
|
||||
The rule of roles in messages for DashScope is very rigid,
|
||||
for more details, please refer to
|
||||
https://help.aliyun.com/zh/dashscope/developer-reference/api-details
|
||||
"""
|
||||
|
||||
# step1: prepare keyword arguments
|
||||
kwargs = {**self.generate_args, **kwargs}
|
||||
|
||||
# step2: checking messages
|
||||
if not isinstance(messages, list):
|
||||
raise ValueError(
|
||||
"Dashscope `messages` field expected type `list`, "
|
||||
f"got `{type(messages)}` instead.",
|
||||
)
|
||||
if not all("role" in msg and "content" in msg for msg in messages):
|
||||
raise ValueError(
|
||||
"Each message in the 'messages' list must contain a 'role' "
|
||||
"and 'content' key for DashScope API.",
|
||||
)
|
||||
|
||||
# step3: forward to generate response
|
||||
response = dashscope.Generation.call(
|
||||
model=self.model_name,
|
||||
messages=messages,
|
||||
result_format="message", # set the result to be "message" format.
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
if response.status_code != HTTPStatus.OK:
|
||||
error_msg = (
|
||||
f" Request id: {response.request_id},"
|
||||
f" Status code: {response.status_code},"
|
||||
f" error code: {response.code},"
|
||||
f" error message: {response.message}."
|
||||
)
|
||||
|
||||
raise RuntimeError(error_msg)
|
||||
|
||||
# step4: record the api invocation if needed
|
||||
self._save_model_invocation(
|
||||
arguments={
|
||||
"model": self.model_name,
|
||||
"messages": messages,
|
||||
**kwargs,
|
||||
},
|
||||
response=response,
|
||||
)
|
||||
|
||||
# step5: update monitor accordingly
|
||||
# The metric names are unified for comparison
|
||||
self.update_monitor(
|
||||
call_counter=1,
|
||||
prompt_tokens=response.usage.get("input_tokens", 0),
|
||||
completion_tokens=response.usage.get("output_tokens", 0),
|
||||
total_tokens=response.usage.get("input_tokens", 0)
|
||||
+ response.usage.get("output_tokens", 0),
|
||||
)
|
||||
|
||||
# step6: return response
|
||||
return ModelResponse(
|
||||
text=response.output["choices"][0]["message"]["content"],
|
||||
raw=response,
|
||||
)
|
||||
|
||||
def format(
|
||||
self,
|
||||
*args: Union[Msg, Sequence[Msg]],
|
||||
) -> List:
|
||||
"""Format the messages for DashScope Chat API.
|
||||
|
||||
In this format function, the input messages are formatted into a
|
||||
single system messages with format "{name}: {content}" for each
|
||||
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
|
||||
|
||||
prompt = 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")
|
||||
)
|
||||
|
||||
The prompt will be as follows:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
[
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You're a helpful assistant",
|
||||
}
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
"## Dialogue History\\n"
|
||||
"Bob: Hi, how can I help you?\\n"
|
||||
"user: What's the date today?"
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
Args:
|
||||
args (`Union[Msg, Sequence[Msg]]`):
|
||||
The input arguments to be formatted, where each argument
|
||||
should be a `Msg` object, or a list of `Msg` objects.
|
||||
In distribution, placeholder is also allowed.
|
||||
|
||||
Returns:
|
||||
`List[dict]`:
|
||||
The formatted messages.
|
||||
"""
|
||||
|
||||
# Parse all information into a list of messages
|
||||
input_msgs = []
|
||||
for _ in args:
|
||||
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(_)}.",
|
||||
)
|
||||
|
||||
messages = []
|
||||
|
||||
# record dialog history as a list of strings
|
||||
dialogue = []
|
||||
for i, unit in enumerate(input_msgs):
|
||||
if i == 0 and unit.role == "system":
|
||||
# system prompt
|
||||
messages.append(
|
||||
{
|
||||
"role": unit.role,
|
||||
"content": _convert_to_str(unit.content),
|
||||
},
|
||||
)
|
||||
else:
|
||||
# Merge all messages into a dialogue history prompt
|
||||
dialogue.append(
|
||||
f"{unit.name}: {_convert_to_str(unit.content)}",
|
||||
)
|
||||
|
||||
dialogue_history = "\n".join(dialogue)
|
||||
|
||||
user_content_template = "## Dialogue History\n{dialogue_history}"
|
||||
|
||||
messages.append(
|
||||
{
|
||||
"role": "user",
|
||||
"content": user_content_template.format(
|
||||
dialogue_history=dialogue_history,
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
return messages
|
||||
|
||||
|
||||
class DashScopeImageSynthesisWrapper(DashScopeWrapperBase):
|
||||
"""The model wrapper for DashScope Image Synthesis API, refer to
|
||||
https://help.aliyun.com/zh/dashscope/developer-reference/quick-start-1
|
||||
"""
|
||||
|
||||
model_type: str = "dashscope_image_synthesis"
|
||||
|
||||
def _register_default_metrics(self) -> None:
|
||||
# Set monitor accordingly
|
||||
# TODO: set quota to the following metrics
|
||||
self.monitor.register(
|
||||
self._metric("call_counter"),
|
||||
metric_unit="times",
|
||||
)
|
||||
self.monitor.register(
|
||||
self._metric("image_count"),
|
||||
metric_unit="image",
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
prompt: str,
|
||||
save_local: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> ModelResponse:
|
||||
"""
|
||||
Args:
|
||||
prompt (`str`):
|
||||
The prompt string to generate images from.
|
||||
save_local: (`bool`, default `False`):
|
||||
Whether to save the generated images locally, and replace
|
||||
the returned image url with the local path.
|
||||
**kwargs (`Any`):
|
||||
The keyword arguments to DashScope Image Synthesis API,
|
||||
e.g. `n`, `size`, etc. Please refer to
|
||||
https://help.aliyun.com/zh/dashscope/developer-reference/api-details-9
|
||||
for more detailed arguments.
|
||||
|
||||
Returns:
|
||||
`ModelResponse`:
|
||||
A list of image urls in image_urls field and the
|
||||
raw response in raw field.
|
||||
|
||||
Note:
|
||||
`parse_func`, `fault_handler` and `max_retries` are reserved
|
||||
for `_response_parse_decorator` to parse and check the
|
||||
response generated by model wrapper. Their usages are listed
|
||||
as follows:
|
||||
- `parse_func` is a callable function used to parse and
|
||||
check the response generated by the model, which takes
|
||||
the response as input.
|
||||
- `max_retries` is the maximum number of retries when the
|
||||
`parse_func` raise an exception.
|
||||
- `fault_handler` is a callable function which is called
|
||||
when the response generated by the model is invalid after
|
||||
`max_retries` retries.
|
||||
"""
|
||||
# step1: prepare keyword arguments
|
||||
kwargs = {**self.generate_args, **kwargs}
|
||||
|
||||
# step2: forward to generate response
|
||||
response = dashscope.ImageSynthesis.call(
|
||||
model=self.model_name,
|
||||
prompt=prompt,
|
||||
**kwargs,
|
||||
)
|
||||
if response.status_code != HTTPStatus.OK:
|
||||
error_msg = (
|
||||
f" Request id: {response.request_id},"
|
||||
f" Status code: {response.status_code},"
|
||||
f" error code: {response.code},"
|
||||
f" error message: {response.message}."
|
||||
)
|
||||
raise RuntimeError(error_msg)
|
||||
|
||||
# step3: record the model api invocation if needed
|
||||
self._save_model_invocation(
|
||||
arguments={
|
||||
"model": self.model_name,
|
||||
"prompt": prompt,
|
||||
**kwargs,
|
||||
},
|
||||
response=response,
|
||||
)
|
||||
|
||||
# step4: update monitor accordingly
|
||||
self.update_monitor(
|
||||
call_counter=1,
|
||||
**response.usage,
|
||||
)
|
||||
|
||||
# step5: return response
|
||||
images = response.output["results"]
|
||||
# Get image urls as a list
|
||||
urls = [_["url"] for _ in images]
|
||||
|
||||
if save_local:
|
||||
# Return local url if save_local is True
|
||||
urls = [file_manager.save_image(_) for _ in urls]
|
||||
return ModelResponse(image_urls=urls, raw=response)
|
||||
|
||||
|
||||
class DashScopeTextEmbeddingWrapper(DashScopeWrapperBase):
|
||||
"""The model wrapper for DashScope Text Embedding API."""
|
||||
|
||||
model_type: str = "dashscope_text_embedding"
|
||||
|
||||
def _register_default_metrics(self) -> None:
|
||||
# Set monitor accordingly
|
||||
# TODO: set quota to the following metrics
|
||||
self.monitor.register(
|
||||
self._metric("call_counter"),
|
||||
metric_unit="times",
|
||||
)
|
||||
self.monitor.register(
|
||||
self._metric("total_tokens"),
|
||||
metric_unit="token",
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
texts: Union[list[str], str],
|
||||
**kwargs: Any,
|
||||
) -> ModelResponse:
|
||||
"""Embed the messages with DashScope Text Embedding API.
|
||||
|
||||
Args:
|
||||
texts (`list[str]` or `str`):
|
||||
The messages used to embed.
|
||||
**kwargs (`Any`):
|
||||
The keyword arguments to DashScope Text Embedding API,
|
||||
e.g. `text_type`. Please refer to
|
||||
https://help.aliyun.com/zh/dashscope/developer-reference/api-details-15
|
||||
for more detailed arguments.
|
||||
|
||||
Returns:
|
||||
`ModelResponse`:
|
||||
A list of embeddings in embedding field and the raw
|
||||
response in raw field.
|
||||
|
||||
Note:
|
||||
`parse_func`, `fault_handler` and `max_retries` are reserved
|
||||
for `_response_parse_decorator` to parse and check the response
|
||||
generated by model wrapper. Their usages are listed as follows:
|
||||
- `parse_func` is a callable function used to parse and
|
||||
check the response generated by the model, which takes the
|
||||
response as input.
|
||||
- `max_retries` is the maximum number of retries when the
|
||||
`parse_func` raise an exception.
|
||||
- `fault_handler` is a callable function which is called
|
||||
when the response generated by the model is invalid after
|
||||
`max_retries` retries.
|
||||
"""
|
||||
# step1: prepare keyword arguments
|
||||
kwargs = {**self.generate_args, **kwargs}
|
||||
|
||||
# step2: forward to generate response
|
||||
response = dashscope.TextEmbedding.call(
|
||||
input=texts,
|
||||
model=self.model_name,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
if response.status_code != HTTPStatus.OK:
|
||||
error_msg = (
|
||||
f" Request id: {response.request_id},"
|
||||
f" Status code: {response.status_code},"
|
||||
f" error code: {response.code},"
|
||||
f" error message: {response.message}."
|
||||
)
|
||||
raise RuntimeError(error_msg)
|
||||
|
||||
# step3: record the model api invocation if needed
|
||||
self._save_model_invocation(
|
||||
arguments={
|
||||
"model": self.model_name,
|
||||
"input": texts,
|
||||
**kwargs,
|
||||
},
|
||||
response=response,
|
||||
)
|
||||
|
||||
# step4: update monitor accordingly
|
||||
self.update_monitor(
|
||||
call_counter=1,
|
||||
**response.usage,
|
||||
)
|
||||
|
||||
# step5: return response
|
||||
return ModelResponse(
|
||||
embedding=[_["embedding"] for _ in response.output["embeddings"]],
|
||||
raw=response,
|
||||
)
|
||||
|
||||
|
||||
class DashScopeMultiModalWrapper(DashScopeWrapperBase):
|
||||
"""The model wrapper for DashScope Multimodal API, refer to
|
||||
https://help.aliyun.com/zh/dashscope/developer-reference/tongyi-qianwen-vl-api
|
||||
"""
|
||||
|
||||
model_type: str = "dashscope_multimodal"
|
||||
|
||||
def _register_default_metrics(self) -> None:
|
||||
# Set monitor accordingly
|
||||
# TODO: set quota to the following metrics
|
||||
self.monitor.register(
|
||||
self._metric("call_counter"),
|
||||
metric_unit="times",
|
||||
)
|
||||
self.monitor.register(
|
||||
self._metric("prompt_tokens"),
|
||||
metric_unit="token",
|
||||
)
|
||||
self.monitor.register(
|
||||
self._metric("completion_tokens"),
|
||||
metric_unit="token",
|
||||
)
|
||||
self.monitor.register(
|
||||
self._metric("total_tokens"),
|
||||
metric_unit="token",
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
messages: list,
|
||||
**kwargs: Any,
|
||||
) -> ModelResponse:
|
||||
"""Model call for DashScope MultiModal API.
|
||||
|
||||
Args:
|
||||
messages (`list`):
|
||||
A list of messages to process.
|
||||
**kwargs (`Any`):
|
||||
The keyword arguments to DashScope MultiModal API,
|
||||
e.g. `stream`. Please refer to
|
||||
https://help.aliyun.com/zh/dashscope/developer-reference/tongyi-qianwen-vl-plus-api
|
||||
for more detailed arguments.
|
||||
|
||||
Returns:
|
||||
`ModelResponse`:
|
||||
The response text in text field, and the raw response in
|
||||
raw field.
|
||||
|
||||
Note:
|
||||
If involving image links, then the messages should be of the
|
||||
following form:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": [
|
||||
{"text": "You are a helpful assistant."},
|
||||
],
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"text": "What does this picture depict?"},
|
||||
{"image": "http://example.com/image.jpg"},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
Therefore, you should input a list matching the content value
|
||||
above.
|
||||
If only involving words, just input them.
|
||||
|
||||
`parse_func`, `fault_handler` and `max_retries` are reserved
|
||||
for `_response_parse_decorator` to parse and check the response
|
||||
generated by model wrapper. Their usages are listed as follows:
|
||||
- `parse_func` is a callable function used to parse and
|
||||
check the response generated by the model, which takes the
|
||||
response as input.
|
||||
- `max_retries` is the maximum number of retries when the
|
||||
`parse_func` raise an exception.
|
||||
- `fault_handler` is a callable function which is called
|
||||
when the response generated by the model is invalid after
|
||||
`max_retries` retries.
|
||||
"""
|
||||
# step1: prepare keyword arguments
|
||||
kwargs = {**self.generate_args, **kwargs}
|
||||
|
||||
# step2: forward to generate response
|
||||
response = dashscope.MultiModalConversation.call(
|
||||
model=self.model_name,
|
||||
messages=messages,
|
||||
**kwargs,
|
||||
)
|
||||
# Unhandle code path here
|
||||
# response could be a generator , if stream is yes
|
||||
# suggest add a check here
|
||||
if response.status_code != HTTPStatus.OK:
|
||||
error_msg = (
|
||||
f" Request id: {response.request_id},"
|
||||
f" Status code: {response.status_code},"
|
||||
f" error code: {response.code},"
|
||||
f" error message: {response.message}."
|
||||
)
|
||||
raise RuntimeError(error_msg)
|
||||
|
||||
# step3: record the model api invocation if needed
|
||||
self._save_model_invocation(
|
||||
arguments={
|
||||
"model": self.model_name,
|
||||
"messages": messages,
|
||||
**kwargs,
|
||||
},
|
||||
response=response,
|
||||
)
|
||||
|
||||
# step4: update monitor accordingly
|
||||
input_tokens = response.usage.get("input_tokens", 0)
|
||||
image_tokens = response.usage.get("image_tokens", 0)
|
||||
audio_tokens = response.usage.get("audio_tokens", 0)
|
||||
output_tokens = response.usage.get("output_tokens", 0)
|
||||
self.update_monitor(
|
||||
call_counter=1,
|
||||
prompt_tokens=input_tokens,
|
||||
completion_tokens=output_tokens,
|
||||
total_tokens=input_tokens
|
||||
+ output_tokens
|
||||
+ image_tokens
|
||||
+ audio_tokens,
|
||||
)
|
||||
|
||||
# step5: return response
|
||||
content = response.output["choices"][0]["message"]["content"]
|
||||
if isinstance(content, list):
|
||||
content = content[0]["text"]
|
||||
|
||||
return ModelResponse(
|
||||
text=content,
|
||||
raw=response,
|
||||
)
|
||||
|
||||
def format(
|
||||
self,
|
||||
*args: Union[Msg, Sequence[Msg]],
|
||||
) -> List:
|
||||
"""Format the messages for DashScope Multimodal API.
|
||||
|
||||
The multimodal API has the following requirements:
|
||||
|
||||
- The roles of messages must alternate between "user" and
|
||||
"assistant".
|
||||
- The message with the role "system" should be the first message
|
||||
in the list.
|
||||
- If the system message exists, then the second message must
|
||||
have the role "user".
|
||||
- The last message in the list should have the role "user".
|
||||
- In each message, more than one figure is allowed.
|
||||
|
||||
With the above requirements, we format the messages as follows:
|
||||
|
||||
- If the first message is a system message, then we will keep it as
|
||||
system prompt.
|
||||
- We merge all messages into a dialogue history prompt in a single
|
||||
message with the role "user".
|
||||
- When there are multiple figures in the given messages, we will
|
||||
attach it to the user message by order. Note if there are
|
||||
multiple figures, this strategy may cause misunderstanding for
|
||||
the model. For advanced solutions, developers are encouraged to
|
||||
implement their own prompt engineering strategies.
|
||||
|
||||
The following is an example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
prompt = model.format(
|
||||
Msg(
|
||||
"system",
|
||||
"You're a helpful assistant",
|
||||
role="system", url="figure1"
|
||||
),
|
||||
Msg(
|
||||
"Bob",
|
||||
"How about this picture?",
|
||||
role="assistant", url="figure2"
|
||||
),
|
||||
Msg(
|
||||
"user",
|
||||
"It's wonderful! How about mine?",
|
||||
role="user", image="figure3"
|
||||
)
|
||||
)
|
||||
|
||||
The prompt will be as follows:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
[
|
||||
{
|
||||
"role": "system",
|
||||
"content": [
|
||||
{"text": "You are a helpful assistant"},
|
||||
{"image": "figure1"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"image": "figure2"},
|
||||
{"image": "figure3"},
|
||||
{
|
||||
"text": (
|
||||
"## Dialogue History\\n"
|
||||
"Bob: How about this picture?\\n"
|
||||
"user: It's wonderful! How about mine?"
|
||||
)
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
Note:
|
||||
In multimodal API, the url of local files should be prefixed with
|
||||
"file://", which will be attached in this format function.
|
||||
|
||||
Args:
|
||||
args (`Union[Msg, Sequence[Msg]]`):
|
||||
The input arguments to be formatted, where each argument
|
||||
should be a `Msg` object, or a list of `Msg` objects.
|
||||
In distribution, placeholder is also allowed.
|
||||
|
||||
Returns:
|
||||
`List[dict]`:
|
||||
The formatted messages.
|
||||
"""
|
||||
|
||||
# Parse all information into a list of messages
|
||||
input_msgs = []
|
||||
for _ in args:
|
||||
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(_)}.",
|
||||
)
|
||||
|
||||
messages = []
|
||||
|
||||
# record dialog history as a list of strings
|
||||
dialogue = []
|
||||
image_or_audio_dicts = []
|
||||
for i, unit in enumerate(input_msgs):
|
||||
if i == 0 and unit.role == "system":
|
||||
# system prompt
|
||||
content = self.convert_url(unit.url)
|
||||
content.append({"text": _convert_to_str(unit.content)})
|
||||
|
||||
messages.append(
|
||||
{
|
||||
"role": unit.role,
|
||||
"content": content,
|
||||
},
|
||||
)
|
||||
else:
|
||||
# text message
|
||||
dialogue.append(
|
||||
f"{unit.name}: {_convert_to_str(unit.content)}",
|
||||
)
|
||||
# image and audio
|
||||
image_or_audio_dicts.extend(self.convert_url(unit.url))
|
||||
|
||||
dialogue_history = "\n".join(dialogue)
|
||||
|
||||
user_content_template = "## Dialogue History\n{dialogue_history}"
|
||||
|
||||
messages.append(
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
# Place the image or audio before the dialogue history
|
||||
*image_or_audio_dicts,
|
||||
{
|
||||
"text": user_content_template.format(
|
||||
dialogue_history=dialogue_history,
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
return messages
|
||||
|
||||
def convert_url(self, url: Union[str, Sequence[str], None]) -> List[dict]:
|
||||
"""Convert the url to the format of DashScope API. Note for local
|
||||
files, a prefix "file://" will be added.
|
||||
|
||||
Args:
|
||||
url (`Union[str, Sequence[str], None]`):
|
||||
A string of url of a list of urls to be converted.
|
||||
|
||||
Returns:
|
||||
`List[dict]`:
|
||||
A list of dictionaries with key as the type of the url
|
||||
and value as the url. Only "image" and "audio" are supported.
|
||||
"""
|
||||
if url is None:
|
||||
return []
|
||||
|
||||
if isinstance(url, str):
|
||||
url_type = _guess_type_by_extension(url)
|
||||
if url_type in ["audio", "image"]:
|
||||
# Add prefix for local files
|
||||
if os.path.exists(url):
|
||||
url = "file://" + url
|
||||
return [{url_type: url}]
|
||||
else:
|
||||
# skip unsupported url
|
||||
logger.warning(
|
||||
f"Skip unsupported url ({url_type}), "
|
||||
f"expect image or audio.",
|
||||
)
|
||||
return []
|
||||
elif isinstance(url, list):
|
||||
dicts = []
|
||||
for _ in url:
|
||||
dicts.extend(self.convert_url(_))
|
||||
return dicts
|
||||
else:
|
||||
raise TypeError(
|
||||
f"Unsupported url type {type(url)}, " f"str or list expected.",
|
||||
)
|
||||
417
AlgoriAgent/src/agentscope/models/gemini_model.py
Normal file
417
AlgoriAgent/src/agentscope/models/gemini_model.py
Normal file
@@ -0,0 +1,417 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Google Gemini model wrapper."""
|
||||
import os
|
||||
from abc import ABC
|
||||
from collections.abc import Iterable
|
||||
from typing import Sequence, Union, Any, List
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from agentscope.message import Msg
|
||||
from agentscope.models import ModelWrapperBase, ModelResponse
|
||||
from agentscope.utils.tools import _convert_to_str
|
||||
|
||||
try:
|
||||
import google.generativeai as genai
|
||||
|
||||
# This package will be installed when the google-generativeai is installed
|
||||
import google.ai.generativelanguage as glm
|
||||
except ImportError:
|
||||
genai = None
|
||||
glm = None
|
||||
|
||||
|
||||
class GeminiWrapperBase(ModelWrapperBase, ABC):
|
||||
"""The base class for Google Gemini model wrapper."""
|
||||
|
||||
_generation_method = None
|
||||
"""The generation method used in `__call__` function, which is used to
|
||||
filter models in `list_models` function."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config_name: str,
|
||||
model_name: str,
|
||||
api_key: str = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize the wrapper for Google Gemini model.
|
||||
|
||||
Args:
|
||||
model_name (`str`):
|
||||
The name of the model.
|
||||
api_key (`str`, defaults to `None`):
|
||||
The api_key for the model. If it is not provided, it will be
|
||||
loaded from environment variable.
|
||||
"""
|
||||
super().__init__(config_name=config_name)
|
||||
|
||||
# Test if the required package is installed
|
||||
if genai is None:
|
||||
raise ImportError(
|
||||
"The google-generativeai package is not installed, "
|
||||
"please install it first.",
|
||||
)
|
||||
|
||||
# Load the api_key from argument or environment variable
|
||||
api_key = api_key or os.environ.get("GOOGLE_API_KEY")
|
||||
|
||||
if api_key is None:
|
||||
raise ValueError(
|
||||
"Google api_key must be provided or set as an "
|
||||
"environment variable.",
|
||||
)
|
||||
|
||||
genai.configure(api_key=api_key, **kwargs)
|
||||
|
||||
self.model_name = model_name
|
||||
|
||||
self._register_default_metrics()
|
||||
|
||||
def _register_default_metrics(self) -> None:
|
||||
"""Register the default metrics for the model."""
|
||||
raise NotImplementedError(
|
||||
"The method `_register_default_metrics` must be implemented.",
|
||||
)
|
||||
|
||||
def list_models(self) -> Sequence:
|
||||
"""List all available models for this API calling."""
|
||||
support_models = list(genai.list_models())
|
||||
|
||||
if self.generation_method is None:
|
||||
return support_models
|
||||
else:
|
||||
return [
|
||||
_
|
||||
for _ in support_models
|
||||
if self._generation_method in _.supported_generation_methods
|
||||
]
|
||||
|
||||
def __call__(self, *args: Any, **kwargs: Any) -> ModelResponse:
|
||||
"""Processing input with the model."""
|
||||
raise NotImplementedError(
|
||||
f"Model Wrapper [{type(self).__name__}]"
|
||||
f" is missing the the required `__call__`"
|
||||
f" method.",
|
||||
)
|
||||
|
||||
|
||||
class GeminiChatWrapper(GeminiWrapperBase):
|
||||
"""The wrapper for Google Gemini chat model, e.g. gemini-pro"""
|
||||
|
||||
model_type: str = "gemini_chat"
|
||||
"""The type of the model, which is used in model configuration."""
|
||||
|
||||
generation_method = "generateContent"
|
||||
"""The generation method used in `__call__` function."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config_name: str,
|
||||
model_name: str,
|
||||
api_key: str = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
config_name=config_name,
|
||||
model_name=model_name,
|
||||
api_key=api_key,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# Create the generative model
|
||||
self.model = genai.GenerativeModel(model_name, **kwargs)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
contents: Union[Sequence, str],
|
||||
stream: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> ModelResponse:
|
||||
"""Generate response for the given contents.
|
||||
|
||||
Args:
|
||||
contents (`Union[Sequence, str]`):
|
||||
The content to generate response.
|
||||
stream (`bool`, defaults to `False`):
|
||||
Whether to use stream mode.
|
||||
**kwargs:
|
||||
The additional arguments for generating response.
|
||||
|
||||
Returns:
|
||||
`ModelResponse`:
|
||||
The response text in text field, and the raw response in raw
|
||||
field.
|
||||
"""
|
||||
# step1: checking messages
|
||||
if isinstance(contents, Iterable):
|
||||
pass
|
||||
elif not isinstance(contents, str):
|
||||
logger.warning(
|
||||
"The input content is not a string or a list of "
|
||||
"messages, which may cause unexpected behavior.",
|
||||
)
|
||||
|
||||
# step2: forward to generate response
|
||||
# TODO: support response in stream mode
|
||||
response = self.model.generate_content(
|
||||
contents,
|
||||
stream=stream,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# step3: Check for candidates and handle accordingly
|
||||
if (
|
||||
not response.candidates[0].content
|
||||
or not response.candidates[0].content.parts
|
||||
or not response.candidates[0].content.parts[0].text
|
||||
):
|
||||
# If we cannot get the response text from the model
|
||||
finish_reason = response.candidates[0].finish_reason
|
||||
reasons = glm.Candidate.FinishReason
|
||||
|
||||
if finish_reason == reasons.STOP:
|
||||
error_info = (
|
||||
"Natural stop point of the model or provided stop "
|
||||
"sequence."
|
||||
)
|
||||
elif finish_reason == reasons.MAX_TOKENS:
|
||||
error_info = (
|
||||
"The maximum number of tokens as specified in the request "
|
||||
"was reached."
|
||||
)
|
||||
elif finish_reason == reasons.SAFETY:
|
||||
error_info = (
|
||||
"The candidate content was flagged for safety reasons."
|
||||
)
|
||||
elif finish_reason == reasons.RECITATION:
|
||||
error_info = (
|
||||
"The candidate content was flagged for recitation reasons."
|
||||
)
|
||||
elif finish_reason in [
|
||||
reasons.FINISH_REASON_UNSPECIFIED,
|
||||
reasons.OTHER,
|
||||
]:
|
||||
error_info = "Unknown error."
|
||||
else:
|
||||
error_info = "No information provided from Gemini API."
|
||||
|
||||
raise ValueError(
|
||||
"The Google Gemini API failed to generate text response with "
|
||||
f"the following finish reason: {error_info}\n"
|
||||
f"YOUR INPUT: {contents}\n"
|
||||
f"RAW RESPONSE FROM GEMINI API: {response}\n",
|
||||
)
|
||||
|
||||
# step4: record the api invocation if needed
|
||||
self._save_model_invocation(
|
||||
arguments={
|
||||
"contents": contents,
|
||||
"stream": stream,
|
||||
**kwargs,
|
||||
},
|
||||
response=str(response),
|
||||
)
|
||||
|
||||
# step5: update monitor accordingly
|
||||
token_prompt = self.model.count_tokens(contents).total_tokens
|
||||
token_response = self.model.count_tokens(response.text).total_tokens
|
||||
self.update_monitor(
|
||||
call_counter=1,
|
||||
completion_tokens=token_response,
|
||||
prompt_tokens=token_prompt,
|
||||
total_tokens=token_prompt + token_response,
|
||||
)
|
||||
|
||||
# step6: return response
|
||||
return ModelResponse(
|
||||
text=response.text,
|
||||
raw=response,
|
||||
)
|
||||
|
||||
def _register_default_metrics(self) -> None:
|
||||
"""Register the default metrics for the model."""
|
||||
self.monitor.register(
|
||||
self._metric("call_counter"),
|
||||
metric_unit="times",
|
||||
)
|
||||
self.monitor.register(
|
||||
self._metric("prompt_tokens"),
|
||||
metric_unit="token",
|
||||
)
|
||||
self.monitor.register(
|
||||
self._metric("completion_tokens"),
|
||||
metric_unit="token",
|
||||
)
|
||||
self.monitor.register(
|
||||
self._metric("total_tokens"),
|
||||
metric_unit="token",
|
||||
)
|
||||
|
||||
def format(
|
||||
self,
|
||||
*args: Union[Msg, Sequence[Msg]],
|
||||
) -> List[dict]:
|
||||
"""This function provide a basic prompting strategy for Gemini Chat
|
||||
API in multi-party conversation, which combines all input into a
|
||||
single string, and wrap it into a user message.
|
||||
|
||||
We make the above decision based on the following constraints of the
|
||||
Gemini generate API:
|
||||
|
||||
1. In Gemini `generate_content` API, the `role` field must be either
|
||||
`user` or `model`.
|
||||
|
||||
2. If we pass a list of messages to the `generate_content` API,
|
||||
the `user` role must speak in the beginning and end of the
|
||||
messages, and `user` and `model` must alternative. This prevents
|
||||
us to build a multi-party conversations, where `model` may keep
|
||||
speaking in different names.
|
||||
|
||||
The above information is updated to 2024/03/21. More information
|
||||
about the Gemini `generate_content` API can be found in
|
||||
https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/gemini
|
||||
|
||||
Based on the above considerations, we decide to combine all messages
|
||||
into a single user message. This is a simple and straightforward
|
||||
strategy, if you have any better ideas, pull request and
|
||||
discussion are welcome in our GitHub repository
|
||||
https://github.com/agentscope/agentscope!
|
||||
|
||||
Args:
|
||||
args (`Union[Msg, Sequence[Msg]]`):
|
||||
The input arguments to be formatted, where each argument
|
||||
should be a `Msg` object, or a list of `Msg` objects.
|
||||
In distribution, placeholder is also allowed.
|
||||
|
||||
Returns:
|
||||
`List[dict]`:
|
||||
A list with one user message.
|
||||
"""
|
||||
input_msgs = []
|
||||
for _ in args:
|
||||
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(_)}.",
|
||||
)
|
||||
|
||||
# record dialog history as a list of strings
|
||||
sys_prompt = None
|
||||
dialogue = []
|
||||
for i, unit in enumerate(input_msgs):
|
||||
if i == 0 and unit.role == "system":
|
||||
# system prompt
|
||||
sys_prompt = _convert_to_str(unit.content)
|
||||
else:
|
||||
# Merge all messages into a dialogue history prompt
|
||||
dialogue.append(
|
||||
f"{unit.name}: {_convert_to_str(unit.content)}",
|
||||
)
|
||||
|
||||
dialogue_history = "\n".join(dialogue)
|
||||
|
||||
if sys_prompt is None:
|
||||
user_content_template = "## Dialogue History\n{dialogue_history}"
|
||||
else:
|
||||
user_content_template = (
|
||||
"{sys_prompt}\n"
|
||||
"\n"
|
||||
"## Dialogue History\n"
|
||||
"{dialogue_history}"
|
||||
)
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"parts": [
|
||||
user_content_template.format(
|
||||
sys_prompt=sys_prompt,
|
||||
dialogue_history=dialogue_history,
|
||||
),
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
return messages
|
||||
|
||||
|
||||
class GeminiEmbeddingWrapper(GeminiWrapperBase):
|
||||
"""The wrapper for Google Gemini embedding model,
|
||||
e.g. models/embedding-001"""
|
||||
|
||||
model_type: str = "gemini_embedding"
|
||||
"""The type of the model, which is used in model configuration."""
|
||||
|
||||
_generation_method = "embedContent"
|
||||
"""The generation method used in `__call__` function."""
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
content: Union[Sequence[Msg], str],
|
||||
task_type: str = None,
|
||||
title: str = None,
|
||||
**kwargs: Any,
|
||||
) -> ModelResponse:
|
||||
"""Generate embedding for the given content. More detailed information
|
||||
please refer to
|
||||
https://ai.google.dev/tutorials/python_quickstart#use_embeddings
|
||||
|
||||
Args:
|
||||
content (`Union[Sequence[Msg], str]`):
|
||||
The content to generate embedding.
|
||||
task_type (`str`, defaults to `None`):
|
||||
The type of the task.
|
||||
title (`str`, defaults to `None`):
|
||||
The title of the content.
|
||||
**kwargs:
|
||||
The additional arguments for generating embedding.
|
||||
|
||||
Returns:
|
||||
`ModelResponse`:
|
||||
The response embedding in embedding field, and the raw response
|
||||
in raw field.
|
||||
"""
|
||||
|
||||
# step1: forward to generate response
|
||||
response = genai.embed_content(
|
||||
model=self.model_name,
|
||||
content=content,
|
||||
task_type=task_type,
|
||||
title=title,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# step2: record the api invocation if needed
|
||||
self._save_model_invocation(
|
||||
arguments={
|
||||
"content": content,
|
||||
"task_type": task_type,
|
||||
"title": title,
|
||||
**kwargs,
|
||||
},
|
||||
response=response,
|
||||
)
|
||||
|
||||
# TODO: Up to 2023/03/11, the embedding model doesn't support to
|
||||
# count tokens.
|
||||
# step3: update monitor accordingly
|
||||
self.update_monitor(call_counter=1)
|
||||
|
||||
return ModelResponse(
|
||||
raw=response,
|
||||
embedding=response["embedding"],
|
||||
)
|
||||
|
||||
def _register_default_metrics(self) -> None:
|
||||
"""Register the default metrics for the model."""
|
||||
self.monitor.register(
|
||||
self._metric("call_counter"),
|
||||
metric_unit="times",
|
||||
)
|
||||
252
AlgoriAgent/src/agentscope/models/litellm_model.py
Normal file
252
AlgoriAgent/src/agentscope/models/litellm_model.py
Normal file
@@ -0,0 +1,252 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Model wrapper based on litellm https://docs.litellm.ai/docs/"""
|
||||
from abc import ABC
|
||||
from typing import Union, Any, List, Sequence
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from .model import ModelWrapperBase, ModelResponse
|
||||
from ..message import Msg
|
||||
from ..utils.tools import _convert_to_str
|
||||
|
||||
try:
|
||||
import litellm
|
||||
except ImportError:
|
||||
litellm = None
|
||||
|
||||
|
||||
class LiteLLMWrapperBase(ModelWrapperBase, ABC):
|
||||
"""The model wrapper based on LiteLLM API."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config_name: str,
|
||||
model_name: str = None,
|
||||
generate_args: dict = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""
|
||||
To use the LiteLLM wrapper, environent variables must be set.
|
||||
Different model_name could be using different environment variables.
|
||||
For example:
|
||||
- for model_name: "gpt-3.5-turbo", you need to set "OPENAI_API_KEY"
|
||||
```
|
||||
os.environ["OPENAI_API_KEY"] = "your-api-key"
|
||||
```
|
||||
- for model_name: "claude-2", you need to set "ANTHROPIC_API_KEY"
|
||||
- for Azure OpenAI, you need to set "AZURE_API_KEY",
|
||||
"AZURE_API_BASE", "AZURE_API_VERSION"
|
||||
You should refer to the docs in https://docs.litellm.ai/docs/ .
|
||||
Args:
|
||||
config_name (`str`):
|
||||
The name of the model config.
|
||||
model_name (`str`, default `None`):
|
||||
The name of the model to use in OpenAI API.
|
||||
generate_args (`dict`, default `None`):
|
||||
The extra keyword arguments used in litellm api generation,
|
||||
e.g. `temperature`, `seed`.
|
||||
For generate_args, please refer to
|
||||
https://docs.litellm.ai/docs/completion/input
|
||||
for more detailes.
|
||||
|
||||
"""
|
||||
|
||||
if model_name is None:
|
||||
model_name = config_name
|
||||
logger.warning("model_name is not set, use config_name instead.")
|
||||
|
||||
super().__init__(config_name=config_name)
|
||||
|
||||
if litellm is None:
|
||||
raise ImportError(
|
||||
"Cannot import litellm package in current python environment."
|
||||
"You should try:"
|
||||
"1. Install litellm by `pip install litellm`"
|
||||
"2. If you still have import error, you should try to "
|
||||
"update the openai to higher version, e.g. "
|
||||
"by runing `pip install openai==1.25.1",
|
||||
)
|
||||
|
||||
self.model_name = model_name
|
||||
self.generate_args = generate_args or {}
|
||||
self._register_default_metrics()
|
||||
|
||||
def format(
|
||||
self,
|
||||
*args: Union[Msg, Sequence[Msg]],
|
||||
) -> Union[List[dict], str]:
|
||||
raise RuntimeError(
|
||||
f"Model Wrapper [{type(self).__name__}] doesn't "
|
||||
f"need to format the input. Please try to use the "
|
||||
f"model wrapper directly.",
|
||||
)
|
||||
|
||||
|
||||
class LiteLLMChatWrapper(LiteLLMWrapperBase):
|
||||
"""The model wrapper based on litellm chat API.
|
||||
To use the LiteLLM wrapper, environent variables must be set.
|
||||
Different model_name could be using different environment variables.
|
||||
For example:
|
||||
- for model_name: "gpt-3.5-turbo", you need to set "OPENAI_API_KEY"
|
||||
```
|
||||
os.environ["OPENAI_API_KEY"] = "your-api-key"
|
||||
```
|
||||
- for model_name: "claude-2", you need to set "ANTHROPIC_API_KEY"
|
||||
- for Azure OpenAI, you need to set "AZURE_API_KEY",
|
||||
"AZURE_API_BASE", "AZURE_API_VERSION"
|
||||
You should refer to the docs in https://docs.litellm.ai/docs/ .
|
||||
"""
|
||||
|
||||
model_type: str = "litellm_chat"
|
||||
|
||||
def _register_default_metrics(self) -> None:
|
||||
# Set monitor accordingly
|
||||
# TODO: set quota to the following metrics
|
||||
self.monitor.register(
|
||||
self._metric("call_counter"),
|
||||
metric_unit="times",
|
||||
)
|
||||
self.monitor.register(
|
||||
self._metric("prompt_tokens"),
|
||||
metric_unit="token",
|
||||
)
|
||||
self.monitor.register(
|
||||
self._metric("completion_tokens"),
|
||||
metric_unit="token",
|
||||
)
|
||||
self.monitor.register(
|
||||
self._metric("total_tokens"),
|
||||
metric_unit="token",
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
messages: list,
|
||||
**kwargs: Any,
|
||||
) -> ModelResponse:
|
||||
"""
|
||||
Args:
|
||||
messages (`list`):
|
||||
A list of messages to process.
|
||||
**kwargs (`Any`):
|
||||
The keyword arguments to litellm chat completions API,
|
||||
e.g. `temperature`, `max_tokens`, `top_p`, etc. Please refer to
|
||||
https://docs.litellm.ai/docs/completion/input
|
||||
for more detailed arguments.
|
||||
|
||||
Returns:
|
||||
`ModelResponse`:
|
||||
The response text in text field, and the raw response in
|
||||
raw field.
|
||||
"""
|
||||
|
||||
# step1: prepare keyword arguments
|
||||
kwargs = {**self.generate_args, **kwargs}
|
||||
|
||||
# step2: checking messages
|
||||
if not isinstance(messages, list):
|
||||
raise ValueError(
|
||||
"LiteLLM `messages` field expected type `list`, "
|
||||
f"got `{type(messages)}` instead.",
|
||||
)
|
||||
if not all("role" in msg and "content" in msg for msg in messages):
|
||||
raise ValueError(
|
||||
"Each message in the 'messages' list must contain a 'role' "
|
||||
"and 'content' key for LiteLLM API.",
|
||||
)
|
||||
|
||||
# step3: forward to generate response
|
||||
response = litellm.completion(
|
||||
model=self.model_name,
|
||||
messages=messages,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# step4: record the api invocation if needed
|
||||
self._save_model_invocation(
|
||||
arguments={
|
||||
"model": self.model_name,
|
||||
"messages": messages,
|
||||
**kwargs,
|
||||
},
|
||||
response=response.model_dump(),
|
||||
)
|
||||
|
||||
# step5: update monitor accordingly
|
||||
self.update_monitor(call_counter=1, **response.usage.model_dump())
|
||||
|
||||
# step6: return response
|
||||
return ModelResponse(
|
||||
text=response.choices[0].message.content,
|
||||
raw=response.model_dump(),
|
||||
)
|
||||
|
||||
def format(
|
||||
self,
|
||||
*args: Union[Msg, Sequence[Msg]],
|
||||
) -> List[dict]:
|
||||
"""Format the input string and dictionary into the unified format.
|
||||
Note that the format function might not be the optimal way to construct
|
||||
prompt for every model, but a common way to do so.
|
||||
Developers are encouraged to implement their own prompt
|
||||
engineering strategies if have strong performance concerns.
|
||||
|
||||
Args:
|
||||
args (`Union[Msg, Sequence[Msg]]`):
|
||||
The input arguments to be formatted, where each argument
|
||||
should be a `Msg` object, or a list of `Msg` objects.
|
||||
In distribution, placeholder is also allowed.
|
||||
Returns:
|
||||
`List[dict]`:
|
||||
The formatted messages in the format that anthropic Chat API
|
||||
required.
|
||||
"""
|
||||
|
||||
# Parse all information into a list of messages
|
||||
input_msgs = []
|
||||
for _ in args:
|
||||
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(_)}.",
|
||||
)
|
||||
|
||||
# record dialog history as a list of strings
|
||||
system_content_template = []
|
||||
dialogue = []
|
||||
for i, unit in enumerate(input_msgs):
|
||||
if i == 0 and unit.role == "system":
|
||||
# system prompt
|
||||
system_prompt = _convert_to_str(unit.content)
|
||||
if not system_prompt.endswith("\n"):
|
||||
system_prompt += "\n"
|
||||
system_content_template.append(system_prompt)
|
||||
else:
|
||||
# Merge all messages into a dialogue history prompt
|
||||
dialogue.append(
|
||||
f"{unit.name}: {_convert_to_str(unit.content)}",
|
||||
)
|
||||
|
||||
if len(dialogue) != 0:
|
||||
dialogue_history = "\n".join(dialogue)
|
||||
|
||||
system_content_template.extend(
|
||||
["## Dialogue History", dialogue_history],
|
||||
)
|
||||
|
||||
system_content = "\n".join(system_content_template)
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": system_content,
|
||||
},
|
||||
]
|
||||
|
||||
return messages
|
||||
305
AlgoriAgent/src/agentscope/models/model.py
Normal file
305
AlgoriAgent/src/agentscope/models/model.py
Normal file
@@ -0,0 +1,305 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""The configuration file should contain one or a list of model configs,
|
||||
and each model config should follow the following format.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
{
|
||||
"config_name": "{config_name}",
|
||||
"model_type": "openai_chat" | "post_api" | ...,
|
||||
...
|
||||
}
|
||||
|
||||
After that, you can specify model by {config_name}.
|
||||
|
||||
Note:
|
||||
The parameters for different types of models are different. For OpenAI API,
|
||||
the format is:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
{
|
||||
"config_name": "{id of your model}",
|
||||
"model_type": "openai_chat",
|
||||
"model_name": "{model_name_for_openai, e.g. gpt-3.5-turbo}",
|
||||
"api_key": "{your_api_key}",
|
||||
"organization": "{your_organization, if needed}",
|
||||
"client_args": {
|
||||
# ...
|
||||
},
|
||||
"generate_args": {
|
||||
# ...
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
For Post API, toking huggingface inference API as an example, its format
|
||||
is:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
{
|
||||
"config_name": "{config_name}",
|
||||
"model_type": "post_api",
|
||||
"api_url": "{api_url}",
|
||||
"headers": {"Authorization": "Bearer {API_TOKEN}"},
|
||||
"max_length": {max_length_of_model},
|
||||
"timeout": {timeout},
|
||||
"max_retries": {max_retries},
|
||||
"generate_args": {
|
||||
"temperature": 0.5,
|
||||
# ...
|
||||
}
|
||||
}
|
||||
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import inspect
|
||||
import time
|
||||
from abc import ABCMeta
|
||||
from functools import wraps
|
||||
from typing import Sequence, Any, Callable, Union, List, Type
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from agentscope.utils import QuotaExceededError
|
||||
from .response import ModelResponse
|
||||
from ..exception import ResponseParsingError
|
||||
|
||||
from ..file_manager import file_manager
|
||||
from ..message import Msg
|
||||
from ..utils import MonitorFactory
|
||||
from ..utils.monitor import get_full_name
|
||||
from ..utils.tools import _get_timestamp
|
||||
from ..constants import _DEFAULT_MAX_RETRIES
|
||||
from ..constants import _DEFAULT_RETRY_INTERVAL
|
||||
|
||||
|
||||
def _response_parse_decorator(
|
||||
model_call: Callable,
|
||||
) -> Callable:
|
||||
"""A decorator for parsing the response of model call. It will take
|
||||
`parse_func`, `fault_handler` and `max_retries` as arguments. The
|
||||
detailed process is as follows:
|
||||
|
||||
1. If `parse_func` is provided, then the response will be parsed first.
|
||||
|
||||
2. If the parsing fails (throws an exception), then response generation
|
||||
will be repeated for `max_retries` times and parsed again.
|
||||
|
||||
3. After `max_retries` times, if the parsing still fails, then if
|
||||
`fault_handler` is provided, the response will be processed by
|
||||
`fault_handler`.
|
||||
"""
|
||||
|
||||
# check if the decorated `model_call` function uses the default
|
||||
# arguments of this decorator.
|
||||
parameters = inspect.signature(model_call).parameters
|
||||
|
||||
for name in parameters.keys():
|
||||
if name in ["parse_func", "max_retries"]:
|
||||
logger.warning(
|
||||
f"The argument {name} is used by the decorator, "
|
||||
f"which will not be passed to the model call "
|
||||
f"function.",
|
||||
)
|
||||
|
||||
@wraps(model_call)
|
||||
def checking_wrapper(self: Any, *args: Any, **kwargs: Any) -> dict:
|
||||
# Step1: Extract parse_func and fault_handler
|
||||
parse_func = kwargs.pop("parse_func", None)
|
||||
fault_handler = kwargs.pop("fault_handler", None)
|
||||
max_retries = kwargs.pop("max_retries", None) or _DEFAULT_MAX_RETRIES
|
||||
|
||||
# Step2: Call the model and parse the response
|
||||
# Return the response directly if parse_func is not provided
|
||||
if parse_func is None:
|
||||
return model_call(self, *args, **kwargs)
|
||||
|
||||
# Otherwise, try to parse the response
|
||||
for itr in range(1, max_retries + 1):
|
||||
# Call the model
|
||||
response = model_call(self, *args, **kwargs)
|
||||
|
||||
# Parse the response if needed
|
||||
try:
|
||||
return parse_func(response)
|
||||
except ResponseParsingError as e:
|
||||
if itr < max_retries:
|
||||
logger.warning(
|
||||
f"Fail to parse response ({itr}/{max_retries}):\n"
|
||||
f"{response}.\n"
|
||||
f"{e.__class__.__name__}: {e}",
|
||||
)
|
||||
time.sleep(_DEFAULT_RETRY_INTERVAL * itr)
|
||||
else:
|
||||
if fault_handler is not None and callable(fault_handler):
|
||||
return fault_handler(response)
|
||||
else:
|
||||
raise
|
||||
return {}
|
||||
|
||||
return checking_wrapper
|
||||
|
||||
|
||||
class _ModelWrapperMeta(ABCMeta):
|
||||
"""A meta call to replace the model wrapper's __call__ function with
|
||||
wrapper about error handling."""
|
||||
|
||||
def __new__(mcs, name: Any, bases: Any, attrs: Any) -> Any:
|
||||
if "__call__" in attrs:
|
||||
attrs["__call__"] = _response_parse_decorator(attrs["__call__"])
|
||||
return super().__new__(mcs, name, bases, attrs)
|
||||
|
||||
def __init__(cls, name: Any, bases: Any, attrs: Any) -> None:
|
||||
if not hasattr(cls, "_registry"):
|
||||
cls._registry = {}
|
||||
cls._type_registry = {}
|
||||
cls._deprecated_type_registry = {}
|
||||
else:
|
||||
cls._registry[name] = cls
|
||||
if hasattr(cls, "model_type"):
|
||||
cls._type_registry[cls.model_type] = cls
|
||||
if hasattr(cls, "deprecated_model_type"):
|
||||
cls._deprecated_type_registry[
|
||||
cls.deprecated_model_type
|
||||
] = cls
|
||||
super().__init__(name, bases, attrs)
|
||||
|
||||
|
||||
class ModelWrapperBase(metaclass=_ModelWrapperMeta):
|
||||
"""The base class for model wrapper."""
|
||||
|
||||
model_type: str
|
||||
"""The type of the model wrapper, which is to identify the model wrapper
|
||||
class in model configuration."""
|
||||
|
||||
config_name: str
|
||||
"""The name of the model configuration."""
|
||||
|
||||
model_name: str
|
||||
"""The name of the model, which is used in model api calling."""
|
||||
|
||||
def __init__(
|
||||
self, # pylint: disable=W0613
|
||||
config_name: str,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Base class for model wrapper.
|
||||
|
||||
All model wrappers should inherit this class and implement the
|
||||
`__call__` function.
|
||||
|
||||
Args:
|
||||
config_name (`str`):
|
||||
The id of the model, which is used to extract configuration
|
||||
from the config file.
|
||||
"""
|
||||
self.monitor = MonitorFactory.get_monitor()
|
||||
|
||||
self.config_name = config_name
|
||||
logger.info(f"Initialize model by configuration [{config_name}]")
|
||||
|
||||
@classmethod
|
||||
def get_wrapper(cls, model_type: str) -> Type[ModelWrapperBase]:
|
||||
"""Get the specific model wrapper"""
|
||||
if model_type in cls._type_registry:
|
||||
return cls._type_registry[model_type] # type: ignore[return-value]
|
||||
elif model_type in cls._registry:
|
||||
return cls._registry[model_type] # type: ignore[return-value]
|
||||
elif model_type in cls._deprecated_type_registry:
|
||||
deprecated_cls = cls._deprecated_type_registry[model_type]
|
||||
logger.warning(
|
||||
f"Model type [{model_type}] will be deprecated in future "
|
||||
f"releases, please use [{deprecated_cls.model_type}] instead.",
|
||||
)
|
||||
return deprecated_cls # type: ignore[return-value]
|
||||
else:
|
||||
return None # type: ignore[return-value]
|
||||
|
||||
def __call__(self, *args: Any, **kwargs: Any) -> ModelResponse:
|
||||
"""Processing input with the model."""
|
||||
raise NotImplementedError(
|
||||
f"Model Wrapper [{type(self).__name__}]"
|
||||
f" is missing the required `__call__`"
|
||||
f" method.",
|
||||
)
|
||||
|
||||
def format(
|
||||
self,
|
||||
*args: Union[Msg, Sequence[Msg]],
|
||||
) -> Union[List[dict], str]:
|
||||
"""Format the input string or dict into the format that the model
|
||||
API required."""
|
||||
raise NotImplementedError(
|
||||
f"Model Wrapper [{type(self).__name__}]"
|
||||
f" is missing the required `format` method",
|
||||
)
|
||||
|
||||
def _save_model_invocation(
|
||||
self,
|
||||
arguments: dict,
|
||||
response: Any,
|
||||
) -> None:
|
||||
"""Save model invocation."""
|
||||
model_class = self.__class__.__name__
|
||||
timestamp = _get_timestamp("%Y%m%d-%H%M%S")
|
||||
|
||||
invocation_record = {
|
||||
"model_class": model_class,
|
||||
"timestamp": timestamp,
|
||||
"arguments": arguments,
|
||||
"response": response,
|
||||
}
|
||||
|
||||
file_manager.save_api_invocation(
|
||||
f"model_{model_class}_{timestamp}",
|
||||
invocation_record,
|
||||
)
|
||||
|
||||
def _register_budget(self, model_name: str, budget: float) -> None:
|
||||
"""Register the budget of the model by model_name."""
|
||||
self.monitor.register_budget(
|
||||
model_name=model_name,
|
||||
value=budget,
|
||||
prefix=model_name,
|
||||
)
|
||||
|
||||
def _register_default_metrics(self) -> None:
|
||||
"""Register metrics to the monitor."""
|
||||
|
||||
def _metric(self, metric_name: str) -> str:
|
||||
"""Add the class name and model name as prefix to the metric name.
|
||||
|
||||
Args:
|
||||
metric_name (`str`):
|
||||
The metric name.
|
||||
|
||||
Returns:
|
||||
`str`: Metric name of this wrapper.
|
||||
"""
|
||||
|
||||
if hasattr(self, "model_name"):
|
||||
return get_full_name(name=metric_name, prefix=self.model_name)
|
||||
else:
|
||||
return get_full_name(name=metric_name)
|
||||
|
||||
def update_monitor(self, **kwargs: Any) -> None:
|
||||
"""Update the monitor with the given values.
|
||||
|
||||
Args:
|
||||
kwargs (`dict`):
|
||||
The values to be updated to the monitor.
|
||||
"""
|
||||
if hasattr(self, "model_name"):
|
||||
prefix = self.model_name
|
||||
else:
|
||||
prefix = None
|
||||
|
||||
try:
|
||||
self.monitor.update(
|
||||
kwargs,
|
||||
prefix=prefix,
|
||||
)
|
||||
except QuotaExceededError as e:
|
||||
logger.error(e.message)
|
||||
513
AlgoriAgent/src/agentscope/models/ollama_model.py
Normal file
513
AlgoriAgent/src/agentscope/models/ollama_model.py
Normal file
@@ -0,0 +1,513 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Model wrapper for Ollama models."""
|
||||
from abc import ABC
|
||||
from typing import Sequence, Any, Optional, List, Union
|
||||
|
||||
from agentscope.message import Msg
|
||||
from agentscope.models import ModelWrapperBase, ModelResponse
|
||||
from agentscope.utils.tools import _convert_to_str
|
||||
|
||||
try:
|
||||
import ollama
|
||||
except ImportError:
|
||||
ollama = None
|
||||
|
||||
|
||||
class OllamaWrapperBase(ModelWrapperBase, ABC):
|
||||
"""The base class for Ollama model wrappers.
|
||||
|
||||
To use Ollama API, please
|
||||
1. First install ollama server from https://ollama.com/download and
|
||||
start the server
|
||||
2. Pull the model by `ollama pull {model_name}` in terminal
|
||||
After that, you can use the ollama API.
|
||||
"""
|
||||
|
||||
model_type: str
|
||||
"""The type of the model wrapper, which is to identify the model wrapper
|
||||
class in model configuration."""
|
||||
|
||||
model_name: str
|
||||
"""The model name used in ollama API."""
|
||||
|
||||
options: dict
|
||||
"""A dict contains the options for ollama generation API,
|
||||
e.g. {"temperature": 0, "seed": 123}"""
|
||||
|
||||
keep_alive: str
|
||||
"""Controls how long the model will stay loaded into memory following
|
||||
the request."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config_name: str,
|
||||
model_name: str,
|
||||
options: dict = None,
|
||||
keep_alive: str = "5m",
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize the model wrapper for Ollama API.
|
||||
|
||||
Args:
|
||||
model_name (`str`):
|
||||
The model name used in ollama API.
|
||||
options (`dict`, default `None`):
|
||||
The extra keyword arguments used in Ollama api generation,
|
||||
e.g. `{"temperature": 0., "seed": 123}`.
|
||||
keep_alive (`str`, default `5m`):
|
||||
Controls how long the model will stay loaded into memory
|
||||
following the request.
|
||||
"""
|
||||
|
||||
super().__init__(config_name=config_name)
|
||||
|
||||
self.model_name = model_name
|
||||
self.options = options
|
||||
self.keep_alive = keep_alive
|
||||
|
||||
self._register_default_metrics()
|
||||
|
||||
|
||||
class OllamaChatWrapper(OllamaWrapperBase):
|
||||
"""The model wrapper for Ollama chat API."""
|
||||
|
||||
model_type: str = "ollama_chat"
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
messages: Sequence[dict],
|
||||
options: Optional[dict] = None,
|
||||
keep_alive: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
) -> ModelResponse:
|
||||
"""Generate response from the given messages.
|
||||
|
||||
Args:
|
||||
messages (`Sequence[dict]`):
|
||||
A list of messages, each message is a dict contains the `role`
|
||||
and `content` of the message.
|
||||
options (`dict`, default `None`):
|
||||
The extra arguments used in ollama chat API, which takes
|
||||
effect only on this call, and will be merged with the
|
||||
`options` input in the constructor,
|
||||
e.g. `{"temperature": 0., "seed": 123}`.
|
||||
keep_alive (`str`, default `None`):
|
||||
How long the model will stay loaded into memory following
|
||||
the request, which takes effect only on this call, and will
|
||||
override the `keep_alive` input in the constructor.
|
||||
|
||||
Returns:
|
||||
`ModelResponse`:
|
||||
The response text in `text` field, and the raw response in
|
||||
`raw` field.
|
||||
"""
|
||||
# step1: prepare parameters accordingly
|
||||
if options is None:
|
||||
options = self.options
|
||||
else:
|
||||
options = {**self.options, **options}
|
||||
|
||||
keep_alive = keep_alive or self.keep_alive
|
||||
|
||||
# step2: forward to generate response
|
||||
response = ollama.chat(
|
||||
model=self.model_name,
|
||||
messages=messages,
|
||||
options=options,
|
||||
keep_alive=keep_alive,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# step2: record the api invocation if needed
|
||||
self._save_model_invocation(
|
||||
arguments={
|
||||
"model": self.model_name,
|
||||
"messages": messages,
|
||||
"options": options,
|
||||
"keep_alive": keep_alive,
|
||||
**kwargs,
|
||||
},
|
||||
response=response,
|
||||
)
|
||||
|
||||
# step3: monitor the response
|
||||
self.update_monitor(
|
||||
call_counter=1,
|
||||
prompt_tokens=response.get("prompt_eval_count", 0),
|
||||
completion_tokens=response.get("eval_count", 0),
|
||||
total_tokens=response.get("prompt_eval_count", 0)
|
||||
+ response.get("eval_count", 0),
|
||||
)
|
||||
|
||||
# step4: return response
|
||||
return ModelResponse(
|
||||
text=response["message"]["content"],
|
||||
raw=response,
|
||||
)
|
||||
|
||||
def _register_default_metrics(self) -> None:
|
||||
"""Register metrics to the monitor."""
|
||||
self.monitor.register(
|
||||
self._metric("call_counter"),
|
||||
metric_unit="times",
|
||||
)
|
||||
self.monitor.register(
|
||||
self._metric("prompt_tokens"),
|
||||
metric_unit="tokens",
|
||||
)
|
||||
self.monitor.register(
|
||||
self._metric("completion_tokens"),
|
||||
metric_unit="token",
|
||||
)
|
||||
self.monitor.register(
|
||||
self._metric("total_tokens"),
|
||||
metric_unit="token",
|
||||
)
|
||||
|
||||
def format(
|
||||
self,
|
||||
*args: Union[Msg, Sequence[Msg]],
|
||||
) -> List[dict]:
|
||||
"""Format the messages for ollama Chat API.
|
||||
|
||||
All messages will be formatted into a single system message with
|
||||
system prompt and dialogue history.
|
||||
|
||||
Note:
|
||||
1. This strategy maybe not suitable for all scenarios,
|
||||
and developers are encouraged to implement their own prompt
|
||||
engineering strategies.
|
||||
2. For ollama chat api, the content field shouldn't be empty string.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
prompt = 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")
|
||||
)
|
||||
|
||||
The prompt will be as follows:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
[
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
"You're a helpful assistant\\n\\n"
|
||||
"## Dialogue History\\n"
|
||||
"Bob: Hi, how can I help you?\\n"
|
||||
"user: What's the date today?"
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
Args:
|
||||
args (`Union[Msg, Sequence[Msg]]`):
|
||||
The input arguments to be formatted, where each argument
|
||||
should be a `Msg` object, or a list of `Msg` objects.
|
||||
In distribution, placeholder is also allowed.
|
||||
|
||||
Returns:
|
||||
`List[dict]`:
|
||||
The formatted messages.
|
||||
"""
|
||||
|
||||
# Parse all information into a list of messages
|
||||
input_msgs = []
|
||||
for _ in args:
|
||||
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(_)}.",
|
||||
)
|
||||
|
||||
# record dialog history as a list of strings
|
||||
system_content_template = []
|
||||
dialogue = []
|
||||
# TODO: here we default the url links to images
|
||||
images = []
|
||||
for i, unit in enumerate(input_msgs):
|
||||
if i == 0 and unit.role == "system":
|
||||
# system prompt
|
||||
system_prompt = _convert_to_str(unit.content)
|
||||
if not system_prompt.endswith("\n"):
|
||||
system_prompt += "\n"
|
||||
system_content_template.append(system_prompt)
|
||||
else:
|
||||
# Merge all messages into a dialogue history prompt
|
||||
dialogue.append(
|
||||
f"{unit.name}: {_convert_to_str(unit.content)}",
|
||||
)
|
||||
|
||||
if unit.url is not None:
|
||||
images.append(unit.url)
|
||||
|
||||
if len(dialogue) != 0:
|
||||
dialogue_history = "\n".join(dialogue)
|
||||
|
||||
system_content_template.extend(
|
||||
["## Dialogue History", dialogue_history],
|
||||
)
|
||||
|
||||
system_content = "\n".join(system_content_template)
|
||||
|
||||
system_message = {
|
||||
"role": "system",
|
||||
"content": system_content,
|
||||
}
|
||||
|
||||
if len(images) != 0:
|
||||
system_message["images"] = images
|
||||
|
||||
return [system_message]
|
||||
|
||||
|
||||
class OllamaEmbeddingWrapper(OllamaWrapperBase):
|
||||
"""The model wrapper for Ollama embedding API."""
|
||||
|
||||
model_type: str = "ollama_embedding"
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
prompt: str,
|
||||
options: Optional[dict] = None,
|
||||
keep_alive: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
) -> ModelResponse:
|
||||
"""Generate embedding from the given prompt.
|
||||
|
||||
Args:
|
||||
prompt (`str`):
|
||||
The prompt to generate response.
|
||||
options (`dict`, default `None`):
|
||||
The extra arguments used in ollama embedding API, which takes
|
||||
effect only on this call, and will be merged with the
|
||||
`options` input in the constructor,
|
||||
e.g. `{"temperature": 0., "seed": 123}`.
|
||||
keep_alive (`str`, default `None`):
|
||||
How long the model will stay loaded into memory following
|
||||
the request, which takes effect only on this call, and will
|
||||
override the `keep_alive` input in the constructor.
|
||||
|
||||
Returns:
|
||||
`ModelResponse`:
|
||||
The response embedding in `embedding` field, and the raw
|
||||
response in `raw` field.
|
||||
"""
|
||||
# step1: prepare parameters accordingly
|
||||
if options is None:
|
||||
options = self.options
|
||||
else:
|
||||
options = {**self.options, **options}
|
||||
|
||||
keep_alive = keep_alive or self.keep_alive
|
||||
|
||||
# step2: forward to generate response
|
||||
response = ollama.embeddings(
|
||||
model=self.model_name,
|
||||
prompt=prompt,
|
||||
options=options,
|
||||
keep_alive=keep_alive,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# step3: record the api invocation if needed
|
||||
self._save_model_invocation(
|
||||
arguments={
|
||||
"model": self.model_name,
|
||||
"prompt": prompt,
|
||||
"options": options,
|
||||
"keep_alive": keep_alive,
|
||||
**kwargs,
|
||||
},
|
||||
response=response,
|
||||
)
|
||||
|
||||
# step4: monitor the response
|
||||
self.update_monitor(call_counter=1)
|
||||
|
||||
# step5: return response
|
||||
return ModelResponse(
|
||||
embedding=[response["embedding"]],
|
||||
raw=response,
|
||||
)
|
||||
|
||||
def _register_default_metrics(self) -> None:
|
||||
"""Register metrics to the monitor."""
|
||||
self.monitor.register(
|
||||
self._metric("call_counter"),
|
||||
metric_unit="times",
|
||||
)
|
||||
|
||||
def format(
|
||||
self,
|
||||
*args: Union[Msg, Sequence[Msg]],
|
||||
) -> Union[List[dict], str]:
|
||||
raise RuntimeError(
|
||||
f"Model Wrapper [{type(self).__name__}] doesn't "
|
||||
f"need to format the input. Please try to use the "
|
||||
f"model wrapper directly.",
|
||||
)
|
||||
|
||||
|
||||
class OllamaGenerationWrapper(OllamaWrapperBase):
|
||||
"""The model wrapper for Ollama generation API."""
|
||||
|
||||
model_type: str = "ollama_generate"
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
prompt: str,
|
||||
options: Optional[dict] = None,
|
||||
keep_alive: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
) -> ModelResponse:
|
||||
"""Generate response from the given prompt.
|
||||
|
||||
Args:
|
||||
prompt (`str`):
|
||||
The prompt to generate response.
|
||||
options (`dict`, default `None`):
|
||||
The extra arguments used in ollama generation API, which takes
|
||||
effect only on this call, and will be merged with the
|
||||
`options` input in the constructor,
|
||||
e.g. `{"temperature": 0., "seed": 123}`.
|
||||
keep_alive (`str`, default `None`):
|
||||
How long the model will stay loaded into memory following
|
||||
the request, which takes effect only on this call, and will
|
||||
override the `keep_alive` input in the constructor.
|
||||
|
||||
Returns:
|
||||
`ModelResponse`:
|
||||
The response text in `text` field, and the raw response in
|
||||
`raw` field.
|
||||
|
||||
"""
|
||||
# step1: prepare parameters accordingly
|
||||
if options is None:
|
||||
options = self.options
|
||||
else:
|
||||
options = {**self.options, **options}
|
||||
|
||||
keep_alive = keep_alive or self.keep_alive
|
||||
|
||||
# step2: forward to generate response
|
||||
response = ollama.generate(
|
||||
model=self.model_name,
|
||||
prompt=prompt,
|
||||
options=options,
|
||||
keep_alive=keep_alive,
|
||||
)
|
||||
|
||||
# step3: record the api invocation if needed
|
||||
self._save_model_invocation(
|
||||
arguments={
|
||||
"model": self.model_name,
|
||||
"prompt": prompt,
|
||||
"options": options,
|
||||
"keep_alive": keep_alive,
|
||||
**kwargs,
|
||||
},
|
||||
response=response,
|
||||
)
|
||||
|
||||
# step4: monitor the response
|
||||
self.update_monitor(
|
||||
call_counter=1,
|
||||
prompt_tokens=response.get("prompt_eval_count", 0),
|
||||
completion_tokens=response.get("eval_count", 0),
|
||||
total_tokens=response.get("prompt_eval_count", 0)
|
||||
+ response.get("eval_count", 0),
|
||||
)
|
||||
|
||||
# step5: return response
|
||||
return ModelResponse(
|
||||
text=response["response"],
|
||||
raw=response,
|
||||
)
|
||||
|
||||
def _register_default_metrics(self) -> None:
|
||||
"""Register metrics to the monitor."""
|
||||
self.monitor.register(
|
||||
self._metric("call_counter"),
|
||||
metric_unit="times",
|
||||
)
|
||||
self.monitor.register(
|
||||
self._metric("prompt_tokens"),
|
||||
metric_unit="tokens",
|
||||
)
|
||||
self.monitor.register(
|
||||
self._metric("completion_tokens"),
|
||||
metric_unit="token",
|
||||
)
|
||||
self.monitor.register(
|
||||
self._metric("total_tokens"),
|
||||
metric_unit="token",
|
||||
)
|
||||
|
||||
def format(self, *args: Union[Msg, Sequence[Msg]]) -> str:
|
||||
"""Forward the input to the model.
|
||||
|
||||
Args:
|
||||
args (`Union[Msg, Sequence[Msg]]`):
|
||||
The input arguments to be formatted, where each argument
|
||||
should be a `Msg` object, or a list of `Msg` objects.
|
||||
In distribution, placeholder is also allowed.
|
||||
|
||||
Returns:
|
||||
`str`:
|
||||
The formatted string prompt.
|
||||
"""
|
||||
input_msgs = []
|
||||
for _ in args:
|
||||
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(_)}.",
|
||||
)
|
||||
|
||||
sys_prompt = None
|
||||
dialogue = []
|
||||
for i, unit in enumerate(input_msgs):
|
||||
if i == 0 and unit.role == "system":
|
||||
# system prompt
|
||||
sys_prompt = _convert_to_str(unit.content)
|
||||
else:
|
||||
# Merge all messages into a dialogue history prompt
|
||||
dialogue.append(
|
||||
f"{unit.name}: {_convert_to_str(unit.content)}",
|
||||
)
|
||||
|
||||
dialogue_history = "\n".join(dialogue)
|
||||
|
||||
if sys_prompt is None:
|
||||
prompt_template = "## Dialogue History\n{dialogue_history}"
|
||||
else:
|
||||
prompt_template = (
|
||||
"{system_prompt}\n"
|
||||
"\n"
|
||||
"## Dialogue History\n"
|
||||
"{dialogue_history}"
|
||||
)
|
||||
|
||||
return prompt_template.format(
|
||||
system_prompt=sys_prompt,
|
||||
dialogue_history=dialogue_history,
|
||||
)
|
||||
528
AlgoriAgent/src/agentscope/models/openai_model.py
Normal file
528
AlgoriAgent/src/agentscope/models/openai_model.py
Normal file
@@ -0,0 +1,528 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Model wrapper for OpenAI models"""
|
||||
from abc import ABC
|
||||
from typing import Union, Any, List, Sequence, Dict
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from .model import ModelWrapperBase, ModelResponse
|
||||
from ..file_manager import file_manager
|
||||
from ..message import Msg
|
||||
from ..utils.tools import _convert_to_str, _to_openai_image_url
|
||||
|
||||
try:
|
||||
import openai
|
||||
except ImportError:
|
||||
openai = None
|
||||
|
||||
from ..utils.token_utils import get_openai_max_length
|
||||
from ..constants import _DEFAULT_API_BUDGET
|
||||
|
||||
|
||||
class OpenAIWrapperBase(ModelWrapperBase, ABC):
|
||||
"""The model wrapper for OpenAI API."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config_name: str,
|
||||
model_name: str = None,
|
||||
api_key: str = None,
|
||||
organization: str = None,
|
||||
client_args: dict = None,
|
||||
generate_args: dict = None,
|
||||
budget: float = _DEFAULT_API_BUDGET,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize the openai client.
|
||||
|
||||
Args:
|
||||
config_name (`str`):
|
||||
The name of the model config.
|
||||
model_name (`str`, default `None`):
|
||||
The name of the model to use in OpenAI API.
|
||||
api_key (`str`, default `None`):
|
||||
The API key for OpenAI API. If not specified, it will
|
||||
be read from the environment variable `OPENAI_API_KEY`.
|
||||
organization (`str`, default `None`):
|
||||
The organization ID for OpenAI API. If not specified, it will
|
||||
be read from the environment variable `OPENAI_ORGANIZATION`.
|
||||
client_args (`dict`, default `None`):
|
||||
The extra keyword arguments to initialize the OpenAI client.
|
||||
generate_args (`dict`, default `None`):
|
||||
The extra keyword arguments used in openai api generation,
|
||||
e.g. `temperature`, `seed`.
|
||||
budget (`float`, default `None`):
|
||||
The total budget using this model. Set to `None` means no
|
||||
limit.
|
||||
"""
|
||||
|
||||
if model_name is None:
|
||||
model_name = config_name
|
||||
logger.warning("model_name is not set, use config_name instead.")
|
||||
|
||||
super().__init__(config_name=config_name)
|
||||
|
||||
if openai is None:
|
||||
raise ImportError(
|
||||
"Cannot find openai package in current python environment.",
|
||||
)
|
||||
|
||||
self.model_name = model_name
|
||||
self.generate_args = generate_args or {}
|
||||
|
||||
self.client = openai.OpenAI(
|
||||
api_key=api_key,
|
||||
organization=organization,
|
||||
**(client_args or {}),
|
||||
)
|
||||
|
||||
# Set the max length of OpenAI model
|
||||
try:
|
||||
self.max_length = get_openai_max_length(self.model_name)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"fail to get max_length for {self.model_name}: " f"{e}",
|
||||
)
|
||||
self.max_length = None
|
||||
|
||||
# Set monitor accordingly
|
||||
self._register_budget(model_name, budget)
|
||||
self._register_default_metrics()
|
||||
|
||||
def format(
|
||||
self,
|
||||
*args: Union[Msg, Sequence[Msg]],
|
||||
) -> Union[List[dict], str]:
|
||||
raise RuntimeError(
|
||||
f"Model Wrapper [{type(self).__name__}] doesn't "
|
||||
f"need to format the input. Please try to use the "
|
||||
f"model wrapper directly.",
|
||||
)
|
||||
|
||||
|
||||
class OpenAIChatWrapper(OpenAIWrapperBase):
|
||||
"""The model wrapper for OpenAI's chat API."""
|
||||
|
||||
model_type: str = "openai_chat"
|
||||
|
||||
deprecated_model_type: str = "openai"
|
||||
|
||||
substrings_in_vision_models_names = ["gpt-4-turbo", "vision", "gpt-4o"]
|
||||
"""The substrings in the model names of vision models."""
|
||||
|
||||
def _register_default_metrics(self) -> None:
|
||||
# Set monitor accordingly
|
||||
# TODO: set quota to the following metrics
|
||||
self.monitor.register(
|
||||
self._metric("call_counter"),
|
||||
metric_unit="times",
|
||||
)
|
||||
self.monitor.register(
|
||||
self._metric("prompt_tokens"),
|
||||
metric_unit="token",
|
||||
)
|
||||
self.monitor.register(
|
||||
self._metric("completion_tokens"),
|
||||
metric_unit="token",
|
||||
)
|
||||
self.monitor.register(
|
||||
self._metric("total_tokens"),
|
||||
metric_unit="token",
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
messages: list,
|
||||
**kwargs: Any,
|
||||
) -> ModelResponse:
|
||||
"""Processes a list of messages to construct a payload for the OpenAI
|
||||
API call. It then makes a request to the OpenAI API and returns the
|
||||
response. This method also updates monitoring metrics based on the
|
||||
API response.
|
||||
|
||||
Each message in the 'messages' list can contain text content and
|
||||
optionally an 'image_urls' key. If 'image_urls' is provided,
|
||||
it is expected to be a list of strings representing URLs to images.
|
||||
These URLs will be transformed to a suitable format for the OpenAI
|
||||
API, which might involve converting local file paths to data URIs.
|
||||
|
||||
Args:
|
||||
messages (`list`):
|
||||
A list of messages to process.
|
||||
**kwargs (`Any`):
|
||||
The keyword arguments to OpenAI chat completions API,
|
||||
e.g. `temperature`, `max_tokens`, `top_p`, etc. Please refer to
|
||||
https://platform.openai.com/docs/api-reference/chat/create
|
||||
for more detailed arguments.
|
||||
|
||||
Returns:
|
||||
`ModelResponse`:
|
||||
The response text in text field, and the raw response in
|
||||
raw field.
|
||||
|
||||
Note:
|
||||
`parse_func`, `fault_handler` and `max_retries` are reserved for
|
||||
`_response_parse_decorator` to parse and check the response
|
||||
generated by model wrapper. Their usages are listed as follows:
|
||||
- `parse_func` is a callable function used to parse and check
|
||||
the response generated by the model, which takes the response
|
||||
as input.
|
||||
- `max_retries` is the maximum number of retries when the
|
||||
`parse_func` raise an exception.
|
||||
- `fault_handler` is a callable function which is called
|
||||
when the response generated by the model is invalid after
|
||||
`max_retries` retries.
|
||||
"""
|
||||
|
||||
# step1: prepare keyword arguments
|
||||
kwargs = {**self.generate_args, **kwargs}
|
||||
|
||||
# step2: checking messages
|
||||
if not isinstance(messages, list):
|
||||
raise ValueError(
|
||||
"OpenAI `messages` field expected type `list`, "
|
||||
f"got `{type(messages)}` instead.",
|
||||
)
|
||||
if not all("role" in msg and "content" in msg for msg in messages):
|
||||
raise ValueError(
|
||||
"Each message in the 'messages' list must contain a 'role' "
|
||||
"and 'content' key for OpenAI API.",
|
||||
)
|
||||
|
||||
# step3: forward to generate response
|
||||
response = self.client.chat.completions.create(
|
||||
model=self.model_name,
|
||||
messages=messages,
|
||||
**kwargs,
|
||||
)
|
||||
print(response)
|
||||
# step4: record the api invocation if needed
|
||||
self._save_model_invocation(
|
||||
arguments={
|
||||
"model": self.model_name,
|
||||
"messages": messages,
|
||||
**kwargs,
|
||||
},
|
||||
response=response.model_dump(),
|
||||
)
|
||||
|
||||
# step5: update monitor accordingly
|
||||
#self.update_monitor(call_counter=1, **response.usage.model_dump())
|
||||
|
||||
# step6: return response
|
||||
return ModelResponse(
|
||||
text=response.choices[0].message.content,
|
||||
raw=response.model_dump(),
|
||||
)
|
||||
|
||||
def _format_msg_with_url(
|
||||
self,
|
||||
msg: Msg,
|
||||
) -> Dict:
|
||||
"""Format a message with image urls into openai chat format.
|
||||
This format method is used for gpt-4o, gpt-4-turbo, gpt-4-vision and
|
||||
other vision models.
|
||||
"""
|
||||
# Check if the model is a vision model
|
||||
if not any(
|
||||
_ in self.model_name
|
||||
for _ in self.substrings_in_vision_models_names
|
||||
):
|
||||
logger.warning(
|
||||
f"The model {self.model_name} is not a vision model. "
|
||||
f"Skip the url in the message.",
|
||||
)
|
||||
return {
|
||||
"role": msg.role,
|
||||
"name": msg.name,
|
||||
"content": _convert_to_str(msg.content),
|
||||
}
|
||||
|
||||
# Put all urls into a list
|
||||
urls = [msg.url] if isinstance(msg.url, str) else msg.url
|
||||
|
||||
# Check if the url refers to an image
|
||||
checked_urls = []
|
||||
for url in urls:
|
||||
try:
|
||||
checked_urls.append(_to_openai_image_url(url))
|
||||
except TypeError:
|
||||
logger.warning(
|
||||
f"The url {url} is not a valid image url for "
|
||||
f"OpenAI Chat API, skipped.",
|
||||
)
|
||||
|
||||
if len(checked_urls) == 0:
|
||||
# If no valid image url is provided, return the normal message dict
|
||||
return {
|
||||
"role": msg.role,
|
||||
"name": msg.name,
|
||||
"content": _convert_to_str(msg.content),
|
||||
}
|
||||
else:
|
||||
# otherwise, use the vision format message
|
||||
returned_msg = {
|
||||
"role": msg.role,
|
||||
"name": msg.name,
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": _convert_to_str(msg.content),
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
image_dicts = [
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": _,
|
||||
},
|
||||
}
|
||||
for _ in checked_urls
|
||||
]
|
||||
|
||||
returned_msg["content"].extend(image_dicts)
|
||||
|
||||
return returned_msg
|
||||
|
||||
def format(
|
||||
self,
|
||||
*args: Union[Msg, Sequence[Msg]],
|
||||
) -> List[dict]:
|
||||
"""Format the input string and dictionary into the format that
|
||||
OpenAI Chat API required.
|
||||
|
||||
Args:
|
||||
args (`Union[Msg, Sequence[Msg]]`):
|
||||
The input arguments to be formatted, where each argument
|
||||
should be a `Msg` object, or a list of `Msg` objects.
|
||||
In distribution, placeholder is also allowed.
|
||||
|
||||
Returns:
|
||||
`List[dict]`:
|
||||
The formatted messages in the format that OpenAI Chat API
|
||||
required.
|
||||
"""
|
||||
messages = []
|
||||
for arg in args:
|
||||
if arg is None:
|
||||
continue
|
||||
if isinstance(arg, Msg):
|
||||
if arg.url is not None:
|
||||
messages.append(self._format_msg_with_url(arg))
|
||||
else:
|
||||
messages.append(
|
||||
{
|
||||
"role": arg.role,
|
||||
"name": arg.name,
|
||||
"content": _convert_to_str(arg.content),
|
||||
},
|
||||
)
|
||||
|
||||
elif isinstance(arg, list):
|
||||
messages.extend(self.format(*arg))
|
||||
else:
|
||||
raise TypeError(
|
||||
f"The input should be a Msg object or a list "
|
||||
f"of Msg objects, got {type(arg)}.",
|
||||
)
|
||||
|
||||
return messages
|
||||
|
||||
|
||||
class OpenAIDALLEWrapper(OpenAIWrapperBase):
|
||||
"""The model wrapper for OpenAI's DALL·E API."""
|
||||
|
||||
model_type: str = "openai_dall_e"
|
||||
|
||||
_resolutions: list = [
|
||||
"1792*1024",
|
||||
"1024*1792",
|
||||
"1024*1024",
|
||||
"512*512",
|
||||
"256*256",
|
||||
]
|
||||
|
||||
def _register_default_metrics(self) -> None:
|
||||
# Set monitor accordingly
|
||||
# TODO: set quota to the following metrics
|
||||
self.monitor.register(
|
||||
self._metric("call_counter"),
|
||||
metric_unit="times",
|
||||
)
|
||||
for resolution in self._resolutions:
|
||||
self.monitor.register(
|
||||
self._metric(resolution),
|
||||
metric_unit="image",
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
prompt: str,
|
||||
save_local: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> ModelResponse:
|
||||
"""
|
||||
Args:
|
||||
prompt (`str`):
|
||||
The prompt string to generate images from.
|
||||
save_local: (`bool`, default `False`):
|
||||
Whether to save the generated images locally, and replace
|
||||
the returned image url with the local path.
|
||||
**kwargs (`Any`):
|
||||
The keyword arguments to OpenAI image generation API, e.g.
|
||||
`n`, `quality`, `response_format`, `size`, etc. Please refer to
|
||||
https://platform.openai.com/docs/api-reference/images/create
|
||||
for more detailed arguments.
|
||||
|
||||
Returns:
|
||||
`ModelResponse`:
|
||||
A list of image urls in image_urls field and the
|
||||
raw response in raw field.
|
||||
|
||||
Note:
|
||||
`parse_func`, `fault_handler` and `max_retries` are reserved for
|
||||
`_response_parse_decorator` to parse and check the response
|
||||
generated by model wrapper. Their usages are listed as follows:
|
||||
- `parse_func` is a callable function used to parse and check
|
||||
the response generated by the model, which takes the response
|
||||
as input.
|
||||
- `max_retries` is the maximum number of retries when the
|
||||
`parse_func` raise an exception.
|
||||
- `fault_handler` is a callable function which is called
|
||||
when the response generated by the model is invalid after
|
||||
`max_retries` retries.
|
||||
"""
|
||||
# step1: prepare keyword arguments
|
||||
kwargs = {**self.generate_args, **kwargs}
|
||||
|
||||
# step2: forward to generate response
|
||||
try:
|
||||
response = self.client.images.generate(
|
||||
model=self.model_name,
|
||||
prompt=prompt,
|
||||
**kwargs,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to generate images for prompt '{prompt}': {e}",
|
||||
)
|
||||
raise e
|
||||
|
||||
# step3: record the model api invocation if needed
|
||||
self._save_model_invocation(
|
||||
arguments={
|
||||
"model": self.model_name,
|
||||
"prompt": prompt,
|
||||
**kwargs,
|
||||
},
|
||||
response=response.model_dump(),
|
||||
)
|
||||
|
||||
# step4: update monitor accordingly
|
||||
self.update_monitor(call_counter=1)
|
||||
|
||||
# step5: return response
|
||||
raw_response = response.model_dump()
|
||||
if "data" not in raw_response:
|
||||
if "error" in raw_response:
|
||||
error_msg = raw_response["error"]["message"]
|
||||
else:
|
||||
error_msg = raw_response
|
||||
logger.error(f"Error in OpenAI API call:\n{error_msg}")
|
||||
raise ValueError(f"Error in OpenAI API call:\n{error_msg}")
|
||||
images = raw_response["data"]
|
||||
# Get image urls as a list
|
||||
urls = [_["url"] for _ in images]
|
||||
|
||||
if save_local:
|
||||
# Return local url if save_local is True
|
||||
urls = [file_manager.save_image(_) for _ in urls]
|
||||
return ModelResponse(image_urls=urls, raw=raw_response)
|
||||
|
||||
|
||||
class OpenAIEmbeddingWrapper(OpenAIWrapperBase):
|
||||
"""The model wrapper for OpenAI embedding API."""
|
||||
|
||||
model_type: str = "openai_embedding"
|
||||
|
||||
def _register_default_metrics(self) -> None:
|
||||
# Set monitor accordingly
|
||||
# TODO: set quota to the following metrics
|
||||
self.monitor.register(
|
||||
self._metric("call_counter"),
|
||||
metric_unit="times",
|
||||
)
|
||||
self.monitor.register(
|
||||
self._metric("prompt_tokens"),
|
||||
metric_unit="token",
|
||||
)
|
||||
self.monitor.register(
|
||||
self._metric("total_tokens"),
|
||||
metric_unit="token",
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
texts: Union[list[str], str],
|
||||
**kwargs: Any,
|
||||
) -> ModelResponse:
|
||||
"""Embed the messages with OpenAI embedding API.
|
||||
|
||||
Args:
|
||||
texts (`list[str]` or `str`):
|
||||
The messages used to embed.
|
||||
**kwargs (`Any`):
|
||||
The keyword arguments to OpenAI embedding API,
|
||||
e.g. `encoding_format`, `user`. Please refer to
|
||||
https://platform.openai.com/docs/api-reference/embeddings
|
||||
for more detailed arguments.
|
||||
|
||||
Returns:
|
||||
`ModelResponse`:
|
||||
A list of embeddings in embedding field and the
|
||||
raw response in raw field.
|
||||
|
||||
Note:
|
||||
`parse_func`, `fault_handler` and `max_retries` are reserved for
|
||||
`_response_parse_decorator` to parse and check the response
|
||||
generated by model wrapper. Their usages are listed as follows:
|
||||
- `parse_func` is a callable function used to parse and check
|
||||
the response generated by the model, which takes the response
|
||||
as input.
|
||||
- `max_retries` is the maximum number of retries when the
|
||||
`parse_func` raise an exception.
|
||||
- `fault_handler` is a callable function which is called
|
||||
when the response generated by the model is invalid after
|
||||
`max_retries` retries.
|
||||
"""
|
||||
# step1: prepare keyword arguments
|
||||
kwargs = {**self.generate_args, **kwargs}
|
||||
|
||||
# step2: forward to generate response
|
||||
response = self.client.embeddings.create(
|
||||
input=texts,
|
||||
model=self.model_name,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# step3: record the model api invocation if needed
|
||||
self._save_model_invocation(
|
||||
arguments={
|
||||
"model": self.model_name,
|
||||
"input": texts,
|
||||
**kwargs,
|
||||
},
|
||||
response=response.model_dump(),
|
||||
)
|
||||
|
||||
# step4: update monitor accordingly
|
||||
self.update_monitor(call_counter=1, **response.usage.model_dump())
|
||||
|
||||
# step5: return response
|
||||
response_json = response.model_dump()
|
||||
return ModelResponse(
|
||||
embedding=[_["embedding"] for _ in response_json["data"]],
|
||||
raw=response_json,
|
||||
)
|
||||
302
AlgoriAgent/src/agentscope/models/post_model.py
Normal file
302
AlgoriAgent/src/agentscope/models/post_model.py
Normal file
@@ -0,0 +1,302 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Model wrapper for post-based inference apis."""
|
||||
import json
|
||||
import time
|
||||
from abc import ABC
|
||||
from typing import Any, Union, Sequence, List
|
||||
|
||||
import requests
|
||||
from loguru import logger
|
||||
|
||||
from .model import ModelWrapperBase, ModelResponse
|
||||
from ..constants import _DEFAULT_MAX_RETRIES
|
||||
from ..constants import _DEFAULT_MESSAGES_KEY
|
||||
from ..constants import _DEFAULT_RETRY_INTERVAL
|
||||
from ..message import Msg
|
||||
from ..utils.tools import _convert_to_str
|
||||
|
||||
|
||||
class PostAPIModelWrapperBase(ModelWrapperBase, ABC):
|
||||
"""The base model wrapper for the model deployed on the POST API."""
|
||||
|
||||
model_type: str = "post_api"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config_name: str,
|
||||
api_url: str,
|
||||
headers: dict = None,
|
||||
max_length: int = 2048,
|
||||
timeout: int = 30,
|
||||
json_args: dict = None,
|
||||
post_args: dict = None,
|
||||
max_retries: int = _DEFAULT_MAX_RETRIES,
|
||||
messages_key: str = _DEFAULT_MESSAGES_KEY,
|
||||
retry_interval: int = _DEFAULT_RETRY_INTERVAL,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize the model wrapper.
|
||||
|
||||
Args:
|
||||
config_name (`str`):
|
||||
The id of the model.
|
||||
api_url (`str`):
|
||||
The url of the post request api.
|
||||
headers (`dict`, defaults to `None`):
|
||||
The headers of the api. Defaults to None.
|
||||
max_length (`int`, defaults to `2048`):
|
||||
The maximum length of the model.
|
||||
timeout (`int`, defaults to `30`):
|
||||
The timeout of the api. Defaults to 30.
|
||||
json_args (`dict`, defaults to `None`):
|
||||
The json arguments of the api. Defaults to None.
|
||||
post_args (`dict`, defaults to `None`):
|
||||
The post arguments of the api. Defaults to None.
|
||||
max_retries (`int`, defaults to `3`):
|
||||
The maximum number of retries when the `parse_func` raise an
|
||||
exception.
|
||||
messages_key (`str`, defaults to `inputs`):
|
||||
The key of the input messages in the json argument.
|
||||
retry_interval (`int`, defaults to `1`):
|
||||
The interval between retries when a request fails.
|
||||
|
||||
Note:
|
||||
When an object of `PostApiModelWrapper` is called, the arguments
|
||||
will of post requests will be used as follows:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
request.post(
|
||||
url=api_url,
|
||||
headers=headers,
|
||||
json={
|
||||
messages_key: messages,
|
||||
**json_args
|
||||
},
|
||||
**post_args
|
||||
)
|
||||
"""
|
||||
super().__init__(config_name=config_name)
|
||||
|
||||
self.api_url = api_url
|
||||
self.headers = headers
|
||||
self.max_length = max_length
|
||||
self.timeout = timeout
|
||||
self.json_args = json_args or {}
|
||||
self.post_args = post_args or {}
|
||||
self.max_retries = max_retries
|
||||
self.messages_key = messages_key
|
||||
self.retry_interval = retry_interval
|
||||
|
||||
def _parse_response(self, response: dict) -> ModelResponse:
|
||||
"""Parse the response json data into ModelResponse"""
|
||||
return ModelResponse(raw=response)
|
||||
|
||||
def __call__(self, input_: str, **kwargs: Any) -> ModelResponse:
|
||||
"""Calling the model with requests.post.
|
||||
|
||||
Args:
|
||||
input_ (`str`):
|
||||
The input string to the model.
|
||||
|
||||
Returns:
|
||||
`dict`: A dictionary that contains the response of the model and
|
||||
related
|
||||
information (e.g. cost, time, the number of tokens, etc.).
|
||||
|
||||
Note:
|
||||
`parse_func`, `fault_handler` and `max_retries` are reserved for
|
||||
`_response_parse_decorator` to parse and check the response
|
||||
generated by model wrapper. Their usages are listed as follows:
|
||||
- `parse_func` is a callable function used to parse and check
|
||||
the response generated by the model, which takes the response
|
||||
as input.
|
||||
- `max_retries` is the maximum number of retries when the
|
||||
`parse_func` raise an exception.
|
||||
- `fault_handler` is a callable function which is called
|
||||
when the response generated by the model is invalid after
|
||||
`max_retries` retries.
|
||||
"""
|
||||
# step1: prepare keyword arguments
|
||||
post_args = {**self.post_args, **kwargs}
|
||||
|
||||
request_kwargs = {
|
||||
"url": self.api_url,
|
||||
"json": {self.messages_key: input_, **self.json_args},
|
||||
"headers": self.headers or {},
|
||||
**post_args,
|
||||
}
|
||||
|
||||
# step2: prepare post requests
|
||||
for i in range(1, self.max_retries + 1):
|
||||
response = requests.post(**request_kwargs)
|
||||
|
||||
if response.status_code == requests.codes.ok:
|
||||
break
|
||||
|
||||
if i < self.max_retries:
|
||||
logger.warning(
|
||||
f"Failed to call the model with "
|
||||
f"requests.codes == {response.status_code}, retry "
|
||||
f"{i + 1}/{self.max_retries} times",
|
||||
)
|
||||
time.sleep(i * self.retry_interval)
|
||||
|
||||
# step3: record model invocation
|
||||
# record the model api invocation, which will be skipped if
|
||||
# `FileManager.save_api_invocation` is `False`
|
||||
self._save_model_invocation(
|
||||
arguments=request_kwargs,
|
||||
response=response.json(),
|
||||
)
|
||||
|
||||
# step4: parse the response
|
||||
if response.status_code == requests.codes.ok:
|
||||
return self._parse_response(response.json())
|
||||
else:
|
||||
logger.error(json.dumps(request_kwargs, indent=4))
|
||||
raise RuntimeError(
|
||||
f"Failed to call the model with {response.json()}",
|
||||
)
|
||||
|
||||
|
||||
class PostAPIChatWrapper(PostAPIModelWrapperBase):
|
||||
"""A post api model wrapper compatible with openai chat, e.g., vLLM,
|
||||
FastChat."""
|
||||
|
||||
model_type: str = "post_api_chat"
|
||||
|
||||
def _parse_response(self, response: dict) -> ModelResponse:
|
||||
return ModelResponse(
|
||||
text=response["data"]["response"]["choices"][0]["message"][
|
||||
"content"
|
||||
],
|
||||
)
|
||||
|
||||
def format(
|
||||
self,
|
||||
*args: Union[Msg, Sequence[Msg]],
|
||||
) -> Union[List[dict]]:
|
||||
"""Format the input messages into a list of dict, which is
|
||||
compatible to OpenAI Chat API.
|
||||
|
||||
Args:
|
||||
args (`Union[Msg, Sequence[Msg]]`):
|
||||
The input arguments to be formatted, where each argument
|
||||
should be a `Msg` object, or a list of `Msg` objects.
|
||||
In distribution, placeholder is also allowed.
|
||||
|
||||
Returns:
|
||||
`Union[List[dict]]`:
|
||||
The formatted messages.
|
||||
"""
|
||||
messages = []
|
||||
for arg in args:
|
||||
if arg is None:
|
||||
continue
|
||||
if isinstance(arg, Msg):
|
||||
messages.append(
|
||||
{
|
||||
"role": arg.role,
|
||||
"name": arg.name,
|
||||
"content": _convert_to_str(arg.content),
|
||||
},
|
||||
)
|
||||
elif isinstance(arg, list):
|
||||
messages.extend(self.format(*arg))
|
||||
else:
|
||||
raise TypeError(
|
||||
f"The input should be a Msg object or a list "
|
||||
f"of Msg objects, got {type(arg)}.",
|
||||
)
|
||||
|
||||
return messages
|
||||
|
||||
|
||||
class PostAPIDALLEWrapper(PostAPIModelWrapperBase):
|
||||
"""A post api model wrapper compatible with openai dall_e"""
|
||||
|
||||
model_type: str = "post_api_dall_e"
|
||||
|
||||
deprecated_model_type: str = "post_api_dalle"
|
||||
|
||||
def _parse_response(self, response: dict) -> ModelResponse:
|
||||
if "data" not in response["data"]["response"]:
|
||||
if "error" in response["data"]["response"]:
|
||||
error_msg = response["data"]["response"]["error"]["message"]
|
||||
else:
|
||||
error_msg = response["data"]["response"]
|
||||
logger.error(f"Error in API call:\n{error_msg}")
|
||||
raise ValueError(f"Error in API call:\n{error_msg}")
|
||||
urls = [img["url"] for img in response["data"]["response"]["data"]]
|
||||
return ModelResponse(image_urls=urls)
|
||||
|
||||
def format(
|
||||
self,
|
||||
*args: Union[Msg, Sequence[Msg]],
|
||||
) -> Union[List[dict], str]:
|
||||
raise RuntimeError(
|
||||
f"Model Wrapper [{type(self).__name__}] doesn't "
|
||||
f"need to format the input. Please try to use the "
|
||||
f"model wrapper directly.",
|
||||
)
|
||||
|
||||
|
||||
class PostAPIEmbeddingWrapper(PostAPIModelWrapperBase):
|
||||
"""
|
||||
A post api model wrapper for embedding model
|
||||
"""
|
||||
|
||||
model_type: str = "post_api_embedding"
|
||||
|
||||
def _parse_response(self, response: dict) -> ModelResponse:
|
||||
"""
|
||||
Parse the response json data into ModelResponse with embedding.
|
||||
Args:
|
||||
response (`dict`):
|
||||
The response obtained from the API. This parsing assume the
|
||||
structure of the response is as following:
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
...
|
||||
"response": {
|
||||
"data": [
|
||||
{
|
||||
"embedding": [
|
||||
0.001,
|
||||
...
|
||||
],
|
||||
...
|
||||
}
|
||||
],
|
||||
"model": "xxxx",
|
||||
...
|
||||
},
|
||||
},
|
||||
}
|
||||
"""
|
||||
if "data" not in response["data"]["response"]:
|
||||
if "error" in response["data"]["response"]:
|
||||
error_msg = response["data"]["response"]["error"]["message"]
|
||||
else:
|
||||
error_msg = response["data"]["response"]
|
||||
logger.error(f"Error in embedding API call:\n{error_msg}")
|
||||
raise ValueError(f"Error in embedding API call:\n{error_msg}")
|
||||
embeddings = [
|
||||
data["embedding"] for data in response["data"]["response"]["data"]
|
||||
]
|
||||
return ModelResponse(
|
||||
embedding=embeddings,
|
||||
raw=response,
|
||||
)
|
||||
|
||||
def format(
|
||||
self,
|
||||
*args: Union[Msg, Sequence[Msg]],
|
||||
) -> Union[List[dict], str]:
|
||||
raise RuntimeError(
|
||||
f"Model Wrapper [{type(self).__name__}] doesn't "
|
||||
f"need to format the input. Please try to use the "
|
||||
f"model wrapper directly.",
|
||||
)
|
||||
85
AlgoriAgent/src/agentscope/models/response.py
Normal file
85
AlgoriAgent/src/agentscope/models/response.py
Normal file
@@ -0,0 +1,85 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Parser for model response."""
|
||||
import json
|
||||
from typing import Optional, Sequence, Any
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from agentscope.utils.tools import _is_json_serializable
|
||||
|
||||
|
||||
class ModelResponse:
|
||||
"""Encapsulation of data returned by the model.
|
||||
|
||||
The main purpose of this class is to align the return formats of different
|
||||
models and act as a bridge between models and agents.
|
||||
"""
|
||||
|
||||
text: Optional[str] = None
|
||||
embedding: Optional[Sequence] = None
|
||||
raw: Optional[Any] = None
|
||||
image_urls: Optional[Sequence[str]] = None
|
||||
parsed: Optional[Any] = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
text: str = None,
|
||||
embedding: Sequence = None,
|
||||
image_urls: Sequence[str] = None,
|
||||
raw: Any = None,
|
||||
parsed: Any = None,
|
||||
) -> None:
|
||||
"""Initialize the model response.
|
||||
|
||||
Args:
|
||||
text (`str`, optional):
|
||||
The text field.
|
||||
embedding (`Sequence`, optional):
|
||||
The embedding returned by the model.
|
||||
image_urls (`Sequence[str]`, optional):
|
||||
The image URLs returned by the model.
|
||||
raw (`Any`, optional):
|
||||
The raw data returned by the model.
|
||||
parsed (`Any`, optional):
|
||||
The parsed data returned by the model.
|
||||
"""
|
||||
self.text = text
|
||||
self.embedding = embedding
|
||||
self.image_urls = image_urls
|
||||
self.raw = raw
|
||||
self.parsed = parsed
|
||||
|
||||
def __getattribute__(self, item: str) -> Any:
|
||||
"""Warning for the deprecated json attribute."""
|
||||
if item == "json":
|
||||
logger.warning(
|
||||
"The json attribute in ModelResponse class is deprecated. Use"
|
||||
" parsed attribute instead.",
|
||||
)
|
||||
|
||||
return super().__getattribute__(item)
|
||||
|
||||
def __setattr__(self, key: str, value: Any) -> Optional[Any]:
|
||||
"""Warning for the deprecated json attribute."""
|
||||
if key == "json":
|
||||
logger.warning(
|
||||
"The json attribute in ModelResponse class is deprecated. Use"
|
||||
" parsed attribute instead.",
|
||||
)
|
||||
|
||||
return super().__setattr__(key, value)
|
||||
|
||||
def __str__(self) -> str:
|
||||
if _is_json_serializable(self.raw):
|
||||
raw = self.raw
|
||||
else:
|
||||
raw = str(self.raw)
|
||||
|
||||
serialized_fields = {
|
||||
"text": self.text,
|
||||
"embedding": self.embedding,
|
||||
"image_urls": self.image_urls,
|
||||
"parsed": self.parsed,
|
||||
"raw": raw,
|
||||
}
|
||||
return json.dumps(serialized_fields, indent=4, ensure_ascii=False)
|
||||
348
AlgoriAgent/src/agentscope/models/zhipu_model.py
Normal file
348
AlgoriAgent/src/agentscope/models/zhipu_model.py
Normal file
@@ -0,0 +1,348 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Model wrapper for ZhipuAI models"""
|
||||
from abc import ABC
|
||||
from typing import Union, Any, List, Sequence
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from .model import ModelWrapperBase, ModelResponse
|
||||
from ..message import Msg
|
||||
from ..utils.tools import _convert_to_str
|
||||
|
||||
try:
|
||||
import zhipuai
|
||||
except ImportError:
|
||||
zhipuai = None
|
||||
|
||||
|
||||
class ZhipuAIWrapperBase(ModelWrapperBase, ABC):
|
||||
"""The model wrapper for ZhipuAI API."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config_name: str,
|
||||
model_name: str = None,
|
||||
api_key: str = None,
|
||||
client_args: dict = None,
|
||||
generate_args: dict = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize the zhipuai client.
|
||||
To init the ZhipuAi client, the api_key is required.
|
||||
Other client args include base_url and timeout.
|
||||
The base_url is set to https://open.bigmodel.cn/api/paas/v4
|
||||
if not specified. The timeout arg is set for http request timeout.
|
||||
|
||||
Args:
|
||||
config_name (`str`):
|
||||
The name of the model config.
|
||||
model_name (`str`, default `None`):
|
||||
The name of the model to use in ZhipuAI API.
|
||||
api_key (`str`, default `None`):
|
||||
The API key for ZhipuAI API. If not specified, it will
|
||||
be read from the environment variable.
|
||||
client_args (`dict`, default `None`):
|
||||
The extra keyword arguments to initialize the ZhipuAI client.
|
||||
generate_args (`dict`, default `None`):
|
||||
The extra keyword arguments used in zhipuai api generation,
|
||||
e.g. `temperature`, `seed`.
|
||||
"""
|
||||
|
||||
if model_name is None:
|
||||
model_name = config_name
|
||||
logger.warning("model_name is not set, use config_name instead.")
|
||||
|
||||
super().__init__(config_name=config_name)
|
||||
|
||||
if zhipuai is None:
|
||||
raise ImportError(
|
||||
"Cannot find zhipuai package in current python environment.",
|
||||
)
|
||||
|
||||
self.model_name = model_name
|
||||
self.generate_args = generate_args or {}
|
||||
|
||||
self.client = zhipuai.ZhipuAI(
|
||||
api_key=api_key,
|
||||
**(client_args or {}),
|
||||
)
|
||||
|
||||
self._register_default_metrics()
|
||||
|
||||
def format(
|
||||
self,
|
||||
*args: Union[Msg, Sequence[Msg]],
|
||||
) -> Union[List[dict], str]:
|
||||
raise RuntimeError(
|
||||
f"Model Wrapper [{type(self).__name__}] doesn't "
|
||||
f"need to format the input. Please try to use the "
|
||||
f"model wrapper directly.",
|
||||
)
|
||||
|
||||
|
||||
class ZhipuAIChatWrapper(ZhipuAIWrapperBase):
|
||||
"""The model wrapper for ZhipuAI's chat API."""
|
||||
|
||||
model_type: str = "zhipuai_chat"
|
||||
|
||||
def _register_default_metrics(self) -> None:
|
||||
# Set monitor accordingly
|
||||
# TODO: set quota to the following metrics
|
||||
self.monitor.register(
|
||||
self._metric("call_counter"),
|
||||
metric_unit="times",
|
||||
)
|
||||
self.monitor.register(
|
||||
self._metric("prompt_tokens"),
|
||||
metric_unit="token",
|
||||
)
|
||||
self.monitor.register(
|
||||
self._metric("completion_tokens"),
|
||||
metric_unit="token",
|
||||
)
|
||||
self.monitor.register(
|
||||
self._metric("total_tokens"),
|
||||
metric_unit="token",
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
messages: list,
|
||||
**kwargs: Any,
|
||||
) -> ModelResponse:
|
||||
"""Processes a list of messages to construct a payload for the ZhipuAI
|
||||
API call. It then makes a request to the ZhipuAI API and returns the
|
||||
response. This method also updates monitoring metrics based on the
|
||||
API response.
|
||||
|
||||
Args:
|
||||
messages (`list`):
|
||||
A list of messages to process.
|
||||
**kwargs (`Any`):
|
||||
The keyword arguments to ZhipuAI chat completions API,
|
||||
e.g. `temperature`, `max_tokens`, `top_p`, etc. Please refer to
|
||||
https://open.bigmodel.cn/dev/api
|
||||
for more detailed arguments.
|
||||
|
||||
Returns:
|
||||
`ModelResponse`:
|
||||
The response text in text field, and the raw response in
|
||||
raw field.
|
||||
|
||||
Note:
|
||||
`parse_func`, `fault_handler` and `max_retries` are reserved for
|
||||
`_response_parse_decorator` to parse and check the response
|
||||
generated by model wrapper. Their usages are listed as follows:
|
||||
- `parse_func` is a callable function used to parse and check
|
||||
the response generated by the model, which takes the response
|
||||
as input.
|
||||
- `max_retries` is the maximum number of retries when the
|
||||
`parse_func` raise an exception.
|
||||
- `fault_handler` is a callable function which is called
|
||||
when the response generated by the model is invalid after
|
||||
`max_retries` retries.
|
||||
"""
|
||||
|
||||
# step1: prepare keyword arguments
|
||||
kwargs = {**self.generate_args, **kwargs}
|
||||
|
||||
# step2: checking messages
|
||||
if not isinstance(messages, list):
|
||||
raise ValueError(
|
||||
"ZhipuAI `messages` field expected type `list`, "
|
||||
f"got `{type(messages)}` instead.",
|
||||
)
|
||||
if not all("role" in msg and "content" in msg for msg in messages):
|
||||
raise ValueError(
|
||||
"Each message in the 'messages' list must contain a 'role' "
|
||||
"and 'content' key for ZhipuAI API.",
|
||||
)
|
||||
|
||||
# step3: forward to generate response
|
||||
response = self.client.chat.completions.create(
|
||||
model=self.model_name,
|
||||
messages=messages,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# step4: record the api invocation if needed
|
||||
self._save_model_invocation(
|
||||
arguments={
|
||||
"model": self.model_name,
|
||||
"messages": messages,
|
||||
**kwargs,
|
||||
},
|
||||
response=response.model_dump(),
|
||||
)
|
||||
|
||||
# step5: update monitor accordingly
|
||||
self.update_monitor(call_counter=1, **response.usage.model_dump())
|
||||
|
||||
# step6: return response
|
||||
return ModelResponse(
|
||||
text=response.choices[0].message.content,
|
||||
raw=response.model_dump(),
|
||||
)
|
||||
|
||||
def format(
|
||||
self,
|
||||
*args: Union[Msg, Sequence[Msg]],
|
||||
) -> List[dict]:
|
||||
"""Format the input string and dictionary into the format that
|
||||
ZhipuAI Chat API required.
|
||||
|
||||
In this format function, the input messages are formatted into a
|
||||
single system messages with format "{name}: {content}" for each
|
||||
message. Note this strategy maybe not suitable for all scenarios,
|
||||
and developers are encouraged to implement their own prompt
|
||||
engineering strategies.
|
||||
|
||||
Args:
|
||||
args (`Union[Msg, Sequence[Msg]]`):
|
||||
The input arguments to be formatted, where each argument
|
||||
should be a `Msg` object, or a list of `Msg` objects.
|
||||
In distribution, placeholder is also allowed.
|
||||
|
||||
Returns:
|
||||
`List[dict]`:
|
||||
The formatted messages in the format that ZhipuAI Chat API
|
||||
required.
|
||||
"""
|
||||
|
||||
# Parse all information into a list of messages
|
||||
input_msgs = []
|
||||
for _ in args:
|
||||
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(_)}.",
|
||||
)
|
||||
|
||||
messages = []
|
||||
|
||||
# record dialog history as a list of strings
|
||||
dialogue = []
|
||||
for i, unit in enumerate(input_msgs):
|
||||
if i == 0 and unit.role == "system":
|
||||
# system prompt
|
||||
messages.append(
|
||||
{
|
||||
"role": unit.role,
|
||||
"content": _convert_to_str(unit.content),
|
||||
},
|
||||
)
|
||||
else:
|
||||
# Merge all messages into a dialogue history prompt
|
||||
dialogue.append(
|
||||
f"{unit.name}: {_convert_to_str(unit.content)}",
|
||||
)
|
||||
|
||||
dialogue_history = "\n".join(dialogue)
|
||||
|
||||
user_content_template = "## Dialogue History\n{dialogue_history}"
|
||||
|
||||
messages.append(
|
||||
{
|
||||
"role": "user",
|
||||
"content": user_content_template.format(
|
||||
dialogue_history=dialogue_history,
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
return messages
|
||||
|
||||
|
||||
class ZhipuAIEmbeddingWrapper(ZhipuAIWrapperBase):
|
||||
"""The model wrapper for ZhipuAI embedding API."""
|
||||
|
||||
model_type: str = "zhipuai_embedding"
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
texts: str,
|
||||
**kwargs: Any,
|
||||
) -> ModelResponse:
|
||||
"""Embed the messages with ZhipuAI embedding API.
|
||||
|
||||
Args:
|
||||
texts (`str`):
|
||||
The messages used to embed.
|
||||
**kwargs (`Any`):
|
||||
The keyword arguments to ZhipuAI embedding API,
|
||||
e.g. `encoding_format`, `user`. Please refer to
|
||||
https://open.bigmodel.cn/dev/api#text_embedding
|
||||
for more detailed arguments.
|
||||
|
||||
Returns:
|
||||
`ModelResponse`:
|
||||
A list of embeddings in embedding field and the
|
||||
raw response in raw field.
|
||||
|
||||
Note:
|
||||
`parse_func`, `fault_handler` and `max_retries` are reserved for
|
||||
`_response_parse_decorator` to parse and check the response
|
||||
generated by model wrapper. Their usages are listed as follows:
|
||||
- `parse_func` is a callable function used to parse and check
|
||||
the response generated by the model, which takes the response
|
||||
as input.
|
||||
- `max_retries` is the maximum number of retries when the
|
||||
`parse_func` raise an exception.
|
||||
- `fault_handler` is a callable function which is called
|
||||
when the response generated by the model is invalid after
|
||||
`max_retries` retries.
|
||||
"""
|
||||
# step1: prepare keyword arguments
|
||||
kwargs = {**self.generate_args, **kwargs}
|
||||
|
||||
# step2: forward to generate response
|
||||
response = self.client.embeddings.create(
|
||||
input=texts,
|
||||
model=self.model_name,
|
||||
)
|
||||
|
||||
# step3: record the model api invocation if needed
|
||||
self._save_model_invocation(
|
||||
arguments={
|
||||
"model": self.model_name,
|
||||
"input": texts,
|
||||
**kwargs,
|
||||
},
|
||||
response=response.model_dump(),
|
||||
)
|
||||
|
||||
# step4: update monitor accordingly
|
||||
self.update_monitor(call_counter=1, **response.usage.model_dump())
|
||||
|
||||
# step5: return response
|
||||
response_json = response.model_dump()
|
||||
return ModelResponse(
|
||||
embedding=[_["embedding"] for _ in response_json["data"]],
|
||||
raw=response_json,
|
||||
)
|
||||
|
||||
def _register_default_metrics(self) -> None:
|
||||
# Set monitor accordingly
|
||||
# TODO: set quota to the following metrics
|
||||
self.monitor.register(
|
||||
self._metric("call_counter"),
|
||||
metric_unit="times",
|
||||
)
|
||||
self.monitor.register(
|
||||
self._metric("prompt_tokens"),
|
||||
metric_unit="token",
|
||||
)
|
||||
self.monitor.register(
|
||||
self._metric("completion_tokens"),
|
||||
metric_unit="token",
|
||||
)
|
||||
self.monitor.register(
|
||||
self._metric("total_tokens"),
|
||||
metric_unit="token",
|
||||
)
|
||||
Reference in New Issue
Block a user