mirror of
https://github.com/FoundationAgents/MetaGPT.git
synced 2026-07-02 16:01:04 +02:00
feat: merge send18:dev
This commit is contained in:
commit
7effe7f74c
92 changed files with 4830 additions and 302 deletions
|
|
@ -4,9 +4,11 @@
|
|||
@Time : 2023/5/5 22:59
|
||||
@Author : alexanderwu
|
||||
@File : __init__.py
|
||||
@Modified By: mashenquan, 2023/9/8. Add `MetaGPTLLMAPI`
|
||||
"""
|
||||
|
||||
from metagpt.provider.openai_api import OpenAIGPTAPI
|
||||
from metagpt.provider.metagpt_llm_api import MetaGPTLLMAPI
|
||||
|
||||
|
||||
__all__ = ["OpenAIGPTAPI"]
|
||||
__all__ = ["OpenAIGPTAPI", "MetaGPTLLMAPI"]
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
@Time : 2023/5/5 23:04
|
||||
@Author : alexanderwu
|
||||
@File : base_gpt_api.py
|
||||
@Desc : mashenquan, 2023/8/22. + try catch
|
||||
"""
|
||||
import json
|
||||
from abc import abstractmethod
|
||||
|
|
|
|||
278
metagpt/provider/metagpt_llm_api.py
Normal file
278
metagpt/provider/metagpt_llm_api.py
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@Time : 2023/8/30
|
||||
@Author : mashenquan
|
||||
@File : metagpt_llm_api.py
|
||||
@Desc : MetaGPT LLM related APIs
|
||||
"""
|
||||
|
||||
from metagpt.provider.openai_api import OpenAIGPTAPI
|
||||
# from metagpt.provider.base_gpt_api import BaseGPTAPI
|
||||
# from metagpt.provider.openai_api import RateLimiter
|
||||
|
||||
|
||||
class MetaGPTLLMAPI(OpenAIGPTAPI):
|
||||
"""MetaGPT LLM api"""
|
||||
|
||||
def __init__(self):
|
||||
super(MetaGPTLLMAPI, self).__init__()
|
||||
|
||||
# def __init__(self):
|
||||
# self.__init_openai(CONFIG)
|
||||
# self.llm = openai
|
||||
# self.model = CONFIG.openai_api_model
|
||||
# self.auto_max_tokens = False
|
||||
# self._cost_manager = CostManager()
|
||||
# RateLimiter.__init__(self, rpm=self.rpm)
|
||||
#
|
||||
# def __init_openai(self, config):
|
||||
# openai.api_key = config.openai_api_key
|
||||
# if config.openai_api_base:
|
||||
# openai.api_base = config.openai_api_base
|
||||
# if config.openai_api_type:
|
||||
# openai.api_type = config.openai_api_type
|
||||
# openai.api_version = config.openai_api_version
|
||||
# self.rpm = int(config.get("RPM", 10))
|
||||
#
|
||||
# async def _achat_completion_stream(self, messages: list[dict]) -> str:
|
||||
# response = await openai.ChatCompletion.acreate(**self._cons_kwargs(messages), stream=True)
|
||||
#
|
||||
# # create variables to collect the stream of chunks
|
||||
# collected_chunks = []
|
||||
# collected_messages = []
|
||||
# # iterate through the stream of events
|
||||
# async for chunk in response:
|
||||
# collected_chunks.append(chunk) # save the event response
|
||||
# choices = chunk["choices"]
|
||||
# if len(choices) > 0:
|
||||
# chunk_message = chunk["choices"][0].get("delta", {}) # extract the message
|
||||
# collected_messages.append(chunk_message) # save the message
|
||||
# if "content" in chunk_message:
|
||||
# print(chunk_message["content"], end="")
|
||||
# print()
|
||||
#
|
||||
# full_reply_content = "".join([m.get("content", "") for m in collected_messages])
|
||||
# usage = self._calc_usage(messages, full_reply_content)
|
||||
# self._update_costs(usage)
|
||||
# return full_reply_content
|
||||
#
|
||||
# def _cons_kwargs(self, messages: list[dict], **configs) -> dict:
|
||||
# kwargs = {
|
||||
# "messages": messages,
|
||||
# "max_tokens": self.get_max_tokens(messages),
|
||||
# "n": 1,
|
||||
# "stop": None,
|
||||
# "temperature": 0.3,
|
||||
# "timeout": 3,
|
||||
# }
|
||||
# if configs:
|
||||
# kwargs.update(configs)
|
||||
#
|
||||
# if CONFIG.openai_api_type == "azure":
|
||||
# if CONFIG.deployment_name and CONFIG.deployment_id:
|
||||
# raise ValueError("You can only use one of the `deployment_id` or `deployment_name` model")
|
||||
# elif not CONFIG.deployment_name and not CONFIG.deployment_id:
|
||||
# raise ValueError("You must specify `DEPLOYMENT_NAME` or `DEPLOYMENT_ID` parameter")
|
||||
# kwargs_mode = (
|
||||
# {"engine": CONFIG.deployment_name}
|
||||
# if CONFIG.deployment_name
|
||||
# else {"deployment_id": CONFIG.deployment_id}
|
||||
# )
|
||||
# else:
|
||||
# kwargs_mode = {"model": self.model}
|
||||
# kwargs.update(kwargs_mode)
|
||||
# return kwargs
|
||||
#
|
||||
# async def _achat_completion(self, messages: list[dict]) -> dict:
|
||||
# rsp = await self.llm.ChatCompletion.acreate(**self._cons_kwargs(messages))
|
||||
# self._update_costs(rsp.get("usage"))
|
||||
# return rsp
|
||||
#
|
||||
# def _chat_completion(self, messages: list[dict]) -> dict:
|
||||
# rsp = self.llm.ChatCompletion.create(**self._cons_kwargs(messages))
|
||||
# self._update_costs(rsp)
|
||||
# return rsp
|
||||
#
|
||||
# def completion(self, messages: list[dict]) -> dict:
|
||||
# # if isinstance(messages[0], Message):
|
||||
# # messages = self.messages_to_dict(messages)
|
||||
# return self._chat_completion(messages)
|
||||
#
|
||||
# async def acompletion(self, messages: list[dict]) -> dict:
|
||||
# # if isinstance(messages[0], Message):
|
||||
# # messages = self.messages_to_dict(messages)
|
||||
# return await self._achat_completion(messages)
|
||||
#
|
||||
# @retry(
|
||||
# wait=wait_random_exponential(min=1, max=60),
|
||||
# stop=stop_after_attempt(6),
|
||||
# after=after_log(logger, logger.level("WARNING").name),
|
||||
# retry=retry_if_exception_type(APIConnectionError),
|
||||
# retry_error_callback=log_and_reraise,
|
||||
# )
|
||||
# async def acompletion_text(self, messages: list[dict], stream=False) -> str:
|
||||
# """when streaming, print each token in place."""
|
||||
# if stream:
|
||||
# return await self._achat_completion_stream(messages)
|
||||
# rsp = await self._achat_completion(messages)
|
||||
# return self.get_choice_text(rsp)
|
||||
#
|
||||
# def _func_configs(self, messages: list[dict], **kwargs) -> dict:
|
||||
# """
|
||||
# Note: Keep kwargs consistent with the parameters in the https://platform.openai.com/docs/api-reference/chat/create
|
||||
# """
|
||||
# if "tools" not in kwargs:
|
||||
# configs = {
|
||||
# "tools": [{"type": "function", "function": GENERAL_FUNCTION_SCHEMA}],
|
||||
# "tool_choice": GENERAL_TOOL_CHOICE,
|
||||
# }
|
||||
# kwargs.update(configs)
|
||||
#
|
||||
# return self._cons_kwargs(messages, **kwargs)
|
||||
#
|
||||
# def _chat_completion_function(self, messages: list[dict], **kwargs) -> dict:
|
||||
# rsp = self.llm.ChatCompletion.create(**self._func_configs(messages, **kwargs))
|
||||
# self._update_costs(rsp.get("usage"))
|
||||
# return rsp
|
||||
#
|
||||
# async def _achat_completion_function(self, messages: list[dict], **chat_configs) -> dict:
|
||||
# rsp = await self.llm.ChatCompletion.acreate(**self._func_configs(messages, **chat_configs))
|
||||
# self._update_costs(rsp.get("usage"))
|
||||
# return rsp
|
||||
#
|
||||
# def _process_message(self, messages: Union[str, Message, list[dict], list[Message], list[str]]) -> list[dict]:
|
||||
# """convert messages to list[dict]."""
|
||||
# if isinstance(messages, list):
|
||||
# messages = [Message(msg) if isinstance(msg, str) else msg for msg in messages]
|
||||
# return [msg if isinstance(msg, dict) else msg.to_dict() for msg in messages]
|
||||
#
|
||||
# if isinstance(messages, Message):
|
||||
# messages = [messages.to_dict()]
|
||||
# elif isinstance(messages, str):
|
||||
# messages = [{"role": "user", "content": messages}]
|
||||
# else:
|
||||
# raise ValueError(
|
||||
# f"Only support messages type are: str, Message, list[dict], but got {type(messages).__name__}!"
|
||||
# )
|
||||
# return messages
|
||||
#
|
||||
# def ask_code(self, messages: Union[str, Message, list[dict]], **kwargs) -> dict:
|
||||
# """Use function of tools to ask a code.
|
||||
#
|
||||
# Note: Keep kwargs consistent with the parameters in the https://platform.openai.com/docs/api-reference/chat/create
|
||||
#
|
||||
# Examples:
|
||||
#
|
||||
# >>> llm = OpenAIGPTAPI()
|
||||
# >>> llm.ask_code("Write a python hello world code.")
|
||||
# {'language': 'python', 'code': "print('Hello, World!')"}
|
||||
# >>> msg = [{'role': 'user', 'content': "Write a python hello world code."}]
|
||||
# >>> llm.ask_code(msg)
|
||||
# {'language': 'python', 'code': "print('Hello, World!')"}
|
||||
# """
|
||||
# messages = self._process_message(messages)
|
||||
# rsp = self._chat_completion_function(messages, **kwargs)
|
||||
# return self.get_choice_function_arguments(rsp)
|
||||
#
|
||||
# async def aask_code(self, messages: Union[str, Message, list[dict]], **kwargs) -> dict:
|
||||
# """Use function of tools to ask a code.
|
||||
#
|
||||
# Note: Keep kwargs consistent with the parameters in the https://platform.openai.com/docs/api-reference/chat/create
|
||||
#
|
||||
# Examples:
|
||||
#
|
||||
# >>> llm = OpenAIGPTAPI()
|
||||
# >>> rsp = await llm.ask_code("Write a python hello world code.")
|
||||
# >>> rsp
|
||||
# {'language': 'python', 'code': "print('Hello, World!')"}
|
||||
# >>> msg = [{'role': 'user', 'content': "Write a python hello world code."}]
|
||||
# >>> rsp = await llm.aask_code(msg) # -> {'language': 'python', 'code': "print('Hello, World!')"}
|
||||
# """
|
||||
# messages = self._process_message(messages)
|
||||
# rsp = await self._achat_completion_function(messages, **kwargs)
|
||||
# return self.get_choice_function_arguments(rsp)
|
||||
#
|
||||
# def _calc_usage(self, messages: list[dict], rsp: str) -> dict:
|
||||
# usage = {}
|
||||
# if CONFIG.calc_usage:
|
||||
# try:
|
||||
# prompt_tokens = count_message_tokens(messages, self.model)
|
||||
# completion_tokens = count_string_tokens(rsp, self.model)
|
||||
# usage["prompt_tokens"] = prompt_tokens
|
||||
# usage["completion_tokens"] = completion_tokens
|
||||
# return usage
|
||||
# except Exception as e:
|
||||
# logger.error("usage calculation failed!", e)
|
||||
# else:
|
||||
# return usage
|
||||
#
|
||||
# async def acompletion_batch(self, batch: list[list[dict]]) -> list[dict]:
|
||||
# """Return full JSON"""
|
||||
# split_batches = self.split_batches(batch)
|
||||
# all_results = []
|
||||
#
|
||||
# for small_batch in split_batches:
|
||||
# logger.info(small_batch)
|
||||
# await self.wait_if_needed(len(small_batch))
|
||||
#
|
||||
# future = [self.acompletion(prompt) for prompt in small_batch]
|
||||
# results = await asyncio.gather(*future)
|
||||
# logger.info(results)
|
||||
# all_results.extend(results)
|
||||
#
|
||||
# return all_results
|
||||
#
|
||||
# async def acompletion_batch_text(self, batch: list[list[dict]]) -> list[str]:
|
||||
# """Only return plain text"""
|
||||
# raw_results = await self.acompletion_batch(batch)
|
||||
# results = []
|
||||
# for idx, raw_result in enumerate(raw_results, start=1):
|
||||
# result = self.get_choice_text(raw_result)
|
||||
# results.append(result)
|
||||
# logger.info(f"Result of task {idx}: {result}")
|
||||
# return results
|
||||
#
|
||||
# def _update_costs(self, usage: dict):
|
||||
# if CONFIG.calc_usage:
|
||||
# try:
|
||||
# prompt_tokens = int(usage["prompt_tokens"])
|
||||
# completion_tokens = int(usage["completion_tokens"])
|
||||
# self._cost_manager.update_cost(prompt_tokens, completion_tokens, self.model)
|
||||
# except Exception as e:
|
||||
# logger.error("updating costs failed!", e)
|
||||
#
|
||||
# def get_costs(self) -> Costs:
|
||||
# return self._cost_manager.get_costs()
|
||||
#
|
||||
# def get_max_tokens(self, messages: list[dict]):
|
||||
# if not self.auto_max_tokens:
|
||||
# return CONFIG.max_tokens_rsp
|
||||
# return get_max_completion_tokens(messages, self.model, CONFIG.max_tokens_rsp)
|
||||
#
|
||||
# def moderation(self, content: Union[str, list[str]]):
|
||||
# try:
|
||||
# if not content:
|
||||
# logger.error("content cannot be empty!")
|
||||
# else:
|
||||
# rsp = self._moderation(content=content)
|
||||
# return rsp
|
||||
# except Exception as e:
|
||||
# logger.error(f"moderating failed:{e}")
|
||||
#
|
||||
# def _moderation(self, content: Union[str, list[str]]):
|
||||
# rsp = self.llm.Moderation.create(input=content)
|
||||
# return rsp
|
||||
#
|
||||
# async def amoderation(self, content: Union[str, list[str]]):
|
||||
# try:
|
||||
# if not content:
|
||||
# logger.error("content cannot be empty!")
|
||||
# else:
|
||||
# rsp = await self._amoderation(content=content)
|
||||
# return rsp
|
||||
# except Exception as e:
|
||||
# logger.error(f"moderating failed:{e}")
|
||||
#
|
||||
# async def _amoderation(self, content: Union[str, list[str]]):
|
||||
# rsp = await self.llm.Moderation.acreate(input=content)
|
||||
# return rsp
|
||||
|
|
@ -8,13 +8,13 @@
|
|||
@Modified By: mashenquan, 2023/11/21. Fix bug: ReadTimeout.
|
||||
@Modified By: mashenquan, 2023/12/1. Fix bug: Unclosed connection caused by openai 0.x.
|
||||
"""
|
||||
import asyncio
|
||||
import time
|
||||
from typing import NamedTuple, Union
|
||||
|
||||
import openai
|
||||
from typing import Union
|
||||
from openai import APIConnectionError, AsyncAzureOpenAI, AsyncOpenAI, RateLimitError
|
||||
from openai.types import CompletionUsage
|
||||
import asyncio
|
||||
import time
|
||||
import openai
|
||||
from tenacity import (
|
||||
after_log,
|
||||
retry,
|
||||
|
|
@ -22,15 +22,14 @@ from tenacity import (
|
|||
stop_after_attempt,
|
||||
wait_random_exponential,
|
||||
)
|
||||
|
||||
from metagpt.config import CONFIG
|
||||
from metagpt.llm import LLMType
|
||||
from metagpt.logs import logger
|
||||
from metagpt.provider.base_gpt_api import BaseGPTAPI
|
||||
from metagpt.provider.constant import GENERAL_FUNCTION_SCHEMA, GENERAL_TOOL_CHOICE
|
||||
from metagpt.schema import Message
|
||||
from metagpt.utils.singleton import Singleton
|
||||
from metagpt.utils.cost_manager import Costs
|
||||
from metagpt.utils.token_counter import (
|
||||
TOKEN_COSTS,
|
||||
count_message_tokens,
|
||||
count_string_tokens,
|
||||
get_max_completion_tokens,
|
||||
|
|
@ -62,75 +61,6 @@ class RateLimiter:
|
|||
self.last_call_time = time.time()
|
||||
|
||||
|
||||
class Costs(NamedTuple):
|
||||
total_prompt_tokens: int
|
||||
total_completion_tokens: int
|
||||
total_cost: float
|
||||
total_budget: float
|
||||
|
||||
|
||||
class CostManager(metaclass=Singleton):
|
||||
"""计算使用接口的开销"""
|
||||
|
||||
def __init__(self):
|
||||
self.total_prompt_tokens = 0
|
||||
self.total_completion_tokens = 0
|
||||
self.total_cost = 0
|
||||
self.total_budget = 0
|
||||
|
||||
def update_cost(self, prompt_tokens, completion_tokens, model):
|
||||
"""
|
||||
Update the total cost, prompt tokens, and completion tokens.
|
||||
|
||||
Args:
|
||||
prompt_tokens (int): The number of tokens used in the prompt.
|
||||
completion_tokens (int): The number of tokens used in the completion.
|
||||
model (str): The model used for the API call.
|
||||
"""
|
||||
self.total_prompt_tokens += prompt_tokens
|
||||
self.total_completion_tokens += completion_tokens
|
||||
cost = (
|
||||
prompt_tokens * TOKEN_COSTS[model]["prompt"] + completion_tokens * TOKEN_COSTS[model]["completion"]
|
||||
) / 1000
|
||||
self.total_cost += cost
|
||||
logger.info(
|
||||
f"Total running cost: ${self.total_cost:.3f} | Max budget: ${CONFIG.max_budget:.3f} | "
|
||||
f"Current cost: ${cost:.3f}, prompt_tokens: {prompt_tokens}, completion_tokens: {completion_tokens}"
|
||||
)
|
||||
CONFIG.total_cost = self.total_cost
|
||||
|
||||
def get_total_prompt_tokens(self):
|
||||
"""
|
||||
Get the total number of prompt tokens.
|
||||
|
||||
Returns:
|
||||
int: The total number of prompt tokens.
|
||||
"""
|
||||
return self.total_prompt_tokens
|
||||
|
||||
def get_total_completion_tokens(self):
|
||||
"""
|
||||
Get the total number of completion tokens.
|
||||
|
||||
Returns:
|
||||
int: The total number of completion tokens.
|
||||
"""
|
||||
return self.total_completion_tokens
|
||||
|
||||
def get_total_cost(self):
|
||||
"""
|
||||
Get the total cost of API calls.
|
||||
|
||||
Returns:
|
||||
float: The total cost of API calls.
|
||||
"""
|
||||
return self.total_cost
|
||||
|
||||
def get_costs(self) -> Costs:
|
||||
"""Get all costs"""
|
||||
return Costs(self.total_prompt_tokens, self.total_completion_tokens, self.total_cost, self.total_budget)
|
||||
|
||||
|
||||
def log_and_reraise(retry_state):
|
||||
logger.error(f"Retry attempts exhausted. Last exception: {retry_state.outcome.exception()}")
|
||||
logger.warning(
|
||||
|
|
@ -161,7 +91,6 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter):
|
|||
else:
|
||||
# https://github.com/openai/openai-python#async-usage
|
||||
self._client = AsyncOpenAI(api_key=CONFIG.openai_api_key, base_url=CONFIG.openai_api_base)
|
||||
self._cost_manager = CostManager()
|
||||
RateLimiter.__init__(self, rpm=self.rpm)
|
||||
|
||||
async def _achat_completion_stream(self, messages: list[dict], timeout=3) -> str:
|
||||
|
|
@ -362,12 +291,15 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter):
|
|||
|
||||
def _update_costs(self, usage: CompletionUsage):
|
||||
if CONFIG.calc_usage:
|
||||
prompt_tokens = usage.prompt_tokens
|
||||
completion_tokens = usage.completion_tokens
|
||||
self._cost_manager.update_cost(prompt_tokens, completion_tokens, self.model)
|
||||
try:
|
||||
prompt_tokens = usage.prompt_tokens
|
||||
completion_tokens = usage.completion_tokens
|
||||
CONFIG.cost_manager.update_cost(prompt_tokens, completion_tokens, self.model)
|
||||
except Exception as e:
|
||||
logger.error("updating costs failed!", e)
|
||||
|
||||
def get_costs(self) -> Costs:
|
||||
return self._cost_manager.get_costs()
|
||||
return CONFIG.cost_manager.get_costs()
|
||||
|
||||
def get_max_tokens(self, messages: list[dict]):
|
||||
if not self.auto_max_tokens:
|
||||
|
|
@ -410,3 +342,10 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter):
|
|||
return loop
|
||||
else:
|
||||
raise e
|
||||
|
||||
async def get_summary(self, text: str, max_words=200, keep_language: bool = False, **kwargs) -> str:
|
||||
from metagpt.memory.brain_memory import BrainMemory
|
||||
|
||||
memory = BrainMemory(llm_type=LLMType.OPENAI.value, historical_summary=text, cacheable=False)
|
||||
return await memory.summarize(llm=self, max_words=max_words, keep_language=keep_language)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue