diff --git a/README.md b/README.md index 4bc480a01..d2b5f2006 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ ### Installation Video Guide ### Traditional Installation ```bash -# Step 1: Ensure that NPM is installed on your system. Then install mermaid-js. (If you don't have npm in your computer, please go to the Node.js offical website to install Node.js https://nodejs.org/ and then you will have npm tool in your computer.) +# Step 1: Ensure that NPM is installed on your system. Then install mermaid-js. (If you don't have npm in your computer, please go to the Node.js official website to install Node.js https://nodejs.org/ and then you will have npm tool in your computer.) npm --version sudo npm install -g @mermaid-js/mermaid-cli @@ -127,7 +127,7 @@ # Step 3: Clone the repository to your local machine, and install it. - **Use your own Browsers** - pyppeteer alow you use installed browsers, please set the following envirment + pyppeteer allows you use installed browsers, please set the following envirment ```bash export PUPPETEER_EXECUTABLE_PATH = /path/to/your/chromium or edge or chrome diff --git a/config/config.yaml b/config/config.yaml index 444f55efd..b2c50991d 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -12,6 +12,13 @@ OPENAI_API_MODEL: "gpt-4" MAX_TOKENS: 1500 RPM: 10 +#### if Spark +#SPARK_APPID : "YOUR_APPID" +#SPARK_API_SECRET : "YOUR_APISecret" +#SPARK_API_KEY : "YOUR_APIKey" +#DOMAIN : "generalv2" +#SPARK_URL : "ws://spark-api.xf-yun.com/v2.1/chat" + #### if Anthropic #Anthropic_API_KEY: "YOUR_API_KEY" diff --git a/examples/agent_creator.py b/examples/agent_creator.py index e03a88c6b..325e7c260 100644 --- a/examples/agent_creator.py +++ b/examples/agent_creator.py @@ -91,7 +91,7 @@ if __name__ == "__main__": msg = """ Write an agent called SimpleTester that will take any code snippet (str) and do the following: - 1. write a testing code (str) for testing the given code snippet, save the testing code as a .py file in the current working diretory; + 1. write a testing code (str) for testing the given code snippet, save the testing code as a .py file in the current working directory; 2. run the testing code. You can use pytest as the testing framework. """ diff --git a/metagpt/config.py b/metagpt/config.py index 53271133b..27455d38d 100644 --- a/metagpt/config.py +++ b/metagpt/config.py @@ -46,7 +46,7 @@ class Config(metaclass=Singleton): self.openai_api_key = self._get("OPENAI_API_KEY") self.anthropic_api_key = self._get("Anthropic_API_KEY") if (not self.openai_api_key or "YOUR_API_KEY" == self.openai_api_key) and ( - not self.anthropic_api_key or "YOUR_API_KEY" == self.anthropic_api_key + not self.anthropic_api_key or "YOUR_API_KEY" == self.anthropic_api_key ): raise NotConfiguredException("Set OPENAI_API_KEY or Anthropic_API_KEY first") self.openai_api_base = self._get("OPENAI_API_BASE") @@ -62,6 +62,12 @@ class Config(metaclass=Singleton): self.deployment_name = self._get("DEPLOYMENT_NAME") self.deployment_id = self._get("DEPLOYMENT_ID") + self.spark_appid = self._get("SPARK_APPID") + self.spark_api_secret = self._get("SPARK_API_SECRET") + self.spark_api_key = self._get("SPARK_API_KEY") + self.domain = self._get("DOMAIN") + self.spark_url = self._get("SPARK_URL") + self.claude_api_key = self._get("Anthropic_API_KEY") self.serpapi_api_key = self._get("SERPAPI_API_KEY") self.serper_api_key = self._get("SERPER_API_KEY") diff --git a/metagpt/provider/spark_api.py b/metagpt/provider/spark_api.py new file mode 100644 index 000000000..55f7000ec --- /dev/null +++ b/metagpt/provider/spark_api.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +@Time : 2023/7/21 11:15 +@Author : Leo Xiao +@File : anthropic_api.py +""" +import _thread as thread +import base64 +import datetime +import hashlib +import hmac +import json +import ssl +from time import mktime +from typing import Optional +from urllib.parse import urlencode +from urllib.parse import urlparse +from wsgiref.handlers import format_date_time + +import websocket # 使用websocket_client + +from metagpt.config import CONFIG +from metagpt.logs import logger +from metagpt.provider.base_gpt_api import BaseGPTAPI + + +class SparkAPI(BaseGPTAPI): + + def __init__(self): + logger.warning('当前方法无法支持异步运行。当你使用acompletion时,并不能并行访问。') + + def ask(self, msg: str) -> str: + message = [self._default_system_msg(), self._user_msg(msg)] + rsp = self.completion(message) + return rsp + + async def aask(self, msg: str, system_msgs: Optional[list[str]] = None) -> str: + if system_msgs: + message = self._system_msgs(system_msgs) + [self._user_msg(msg)] + else: + message = [self._default_system_msg(), self._user_msg(msg)] + rsp = await self.acompletion(message) + logger.debug(message) + return rsp + + def get_choice_text(self, rsp: dict) -> str: + return rsp["payload"]["choices"]["text"][-1]["content"] + + async def acompletion_text(self, messages: list[dict], stream=False) -> str: + # 不支持 + logger.error('该功能禁用。') + w = GetMessageFromWeb(messages) + return w.run() + + async def acompletion(self, messages: list[dict]): + # 不支持异步 + w = GetMessageFromWeb(messages) + return w.run() + + def completion(self, messages: list[dict]): + w = GetMessageFromWeb(messages) + return w.run() + + +class GetMessageFromWeb: + class WsParam: + """ + 该类适合讯飞星火大部分接口的调用。 + 输入 app_id, api_key, api_secret, spark_url以初始化, + create_url方法返回接口url + """ + + # 初始化 + def __init__(self, app_id, api_key, api_secret, spark_url, message=None): + self.app_id = app_id + self.api_key = api_key + self.api_secret = api_secret + self.host = urlparse(spark_url).netloc + self.path = urlparse(spark_url).path + self.spark_url = spark_url + self.message = message + + # 生成url + def create_url(self): + # 生成RFC1123格式的时间戳 + now = datetime.datetime.now() + date = format_date_time(mktime(now.timetuple())) + + # 拼接字符串 + signature_origin = "host: " + self.host + "\n" + signature_origin += "date: " + date + "\n" + signature_origin += "GET " + self.path + " HTTP/1.1" + + # 进行hmac-sha256进行加密 + signature_sha = hmac.new(self.api_secret.encode('utf-8'), signature_origin.encode('utf-8'), + digestmod=hashlib.sha256).digest() + + signature_sha_base64 = base64.b64encode(signature_sha).decode(encoding='utf-8') + + authorization_origin = f'api_key="{self.api_key}", algorithm="hmac-sha256", headers="host date request-line", signature="{signature_sha_base64}"' + + authorization = base64.b64encode(authorization_origin.encode('utf-8')).decode(encoding='utf-8') + + # 将请求的鉴权参数组合为字典 + v = { + "authorization": authorization, + "date": date, + "host": self.host + } + # 拼接鉴权参数,生成url + url = self.spark_url + '?' + urlencode(v) + # 此处打印出建立连接时候的url,参考本demo的时候可取消上方打印的注释,比对相同参数时生成的url与自己代码生成的url是否一致 + return url + + def __init__(self, text): + self.text = text + self.ret = '' + self.spark_appid = CONFIG.spark_appid + self.spark_api_secret = CONFIG.spark_api_secret + self.spark_api_key = CONFIG.spark_api_key + self.domain = CONFIG.domain + self.spark_url = CONFIG.spark_url + + def on_message(self, ws, message): + data = json.loads(message) + code = data['header']['code'] + + if code != 0: + ws.close() # 请求错误,则关闭socket + logger.critical(f'回答获取失败,响应信息反序列化之后为: {data}') + return + else: + choices = data["payload"]["choices"] + seq = choices["seq"] # 服务端是流式返回,seq为返回的数据序号 + status = choices["status"] # 服务端是流式返回,status用于判断信息是否传送完毕 + content = choices["text"][0]["content"] # 本次接收到的回答文本 + self.ret += content + if status == 2: + ws.close() + + # 收到websocket错误的处理 + def on_error(self, ws, error): + # on_message方法处理接收到的信息,出现任何错误,都会调用这个方法 + logger.critical(f'通讯连接出错,【错误提示: {error}】') + + # 收到websocket关闭的处理 + def on_close(self, ws, one, two): + pass + + # 处理请求数据 + def gen_params(self): + + data = { + "header": { + "app_id": self.spark_appid, + "uid": "1234" + }, + "parameter": { + "chat": { + # domain为必传参数 + "domain": self.domain, + + # 以下为可微调,非必传参数 + # 注意:官方建议,temperature和top_k修改一个即可 + "max_tokens": 2048, # 默认2048,模型回答的tokens的最大长度,即允许它输出文本的最长字数 + "temperature": 0.5, # 取值为[0,1],默认为0.5。取值越高随机性越强、发散性越高,即相同的问题得到的不同答案的可能性越高 + "top_k": 4, # 取值为[1,6],默认为4。从k个候选中随机选择一个(非等概率) + } + }, + "payload": { + "message": { + "text": self.text + } + } + } + return data + + def send(self, ws, *args): + data = json.dumps(self.gen_params()) + ws.send(data) + + # 收到websocket连接建立的处理 + def on_open(self, ws): + thread.start_new_thread(self.send, (ws,)) + + # 处理收到的 websocket消息,出现任何错误,调用on_error方法 + def run(self): + return self._run(self.text) + + def _run(self, text_list): + + ws_param = self.WsParam( + self.spark_appid, + self.spark_api_key, + self.spark_api_secret, + self.spark_url, + text_list) + ws_url = ws_param.create_url() + + websocket.enableTrace(False) # 默认禁用 WebSocket 的跟踪功能 + ws = websocket.WebSocketApp(ws_url, on_message=self.on_message, on_error=self.on_error, on_close=self.on_close, + on_open=self.on_open) + ws.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE}) + return self.ret diff --git a/metagpt/tools/code_interpreter.py b/metagpt/tools/code_interpreter.py index 97398ccfd..e41eaab72 100644 --- a/metagpt/tools/code_interpreter.py +++ b/metagpt/tools/code_interpreter.py @@ -1,11 +1,11 @@ import re -from typing import List, Callable +from typing import List, Callable, Dict from pathlib import Path import wrapt import textwrap import inspect -from interpreter.interpreter import Interpreter +from interpreter.core.core import Interpreter from metagpt.logs import logger from metagpt.config import CONFIG @@ -41,13 +41,13 @@ class OpenCodeInterpreter(object): interpreter.auto_run = auto_run interpreter.model = CONFIG.openai_api_model or "gpt-3.5-turbo" interpreter.api_key = CONFIG.openai_api_key - interpreter.api_base = CONFIG.openai_api_base + # interpreter.api_base = CONFIG.openai_api_base self.interpreter = interpreter def chat(self, query: str, reset: bool = True): if reset: self.interpreter.reset() - return self.interpreter.chat(query, return_messages=True) + return self.interpreter.chat(query) @staticmethod def extract_function(query_respond: List, function_name: str, *, language: str = 'python', @@ -61,11 +61,30 @@ class OpenCodeInterpreter(object): assert language == 'python', f"Expect python language for default function_format, but got {language}." function_format = """def {function_name}():\n{code}""" # Extract the code module in the open-interpreter respond message. - code = [item['function_call']['parsed_arguments']['code'] for item in query_respond - if "function_call" in item - and "parsed_arguments" in item["function_call"] - and 'language' in item["function_call"]['parsed_arguments'] - and item["function_call"]['parsed_arguments']['language'] == language] + # The query_respond of open-interpreter before v0.1.4 is: + # [{'role': 'user', 'content': your query string}, + # {'role': 'assistant', 'content': plan from llm, 'function_call': { + # "name": "run_code", "arguments": "{"language": "python", "code": code of first plan}, + # "parsed_arguments": {"language": "python", "code": code of first plan} + # ...] + if "function_call" in query_respond[1]: + code = [item['function_call']['parsed_arguments']['code'] for item in query_respond + if "function_call" in item + and "parsed_arguments" in item["function_call"] + and 'language' in item["function_call"]['parsed_arguments'] + and item["function_call"]['parsed_arguments']['language'] == language] + # The query_respond of open-interpreter v0.1.7 is: + # [{'role': 'user', 'message': your query string}, + # {'role': 'assistant', 'message': plan from llm, 'language': 'python', + # 'code': code of first plan, 'output': output of first plan code}, + # ...] + elif "code" in query_respond[1]: + code = [item['code'] for item in query_respond + if "code" in item + and 'language' in item + and item['language'] == language] + else: + raise ValueError(f"Unexpect message format in query_respond: {query_respond[1].keys()}") # add indent. indented_code_str = textwrap.indent("\n".join(code), ' ' * 4) # Return the code after deduplication. @@ -94,13 +113,49 @@ class OpenInterpreterDecorator(object): self.code_file_path = code_file_path self.clear_code = clear_code + def _have_code(self, rsp: List[Dict]): + # Is there any code generated? + return 'code' in rsp[1] and rsp[1]['code'] not in ("", None) + + def _is_faild_plan(self, rsp: List[Dict]): + # is faild plan? + func_code = OpenCodeInterpreter.extract_function(rsp, 'function') + # If there is no more than 1 '\n', the plan execution fails. + if isinstance(func_code, str) and func_code.count('\n') <= 1: + return True + return False + + def _check_respond(self, query: str, interpreter: OpenCodeInterpreter, respond: List[Dict], max_try: int = 3): + for _ in range(max_try): + # TODO: If no code or faild plan is generated, execute chat again, repeating no more than max_try times. + if self._have_code(respond) and not self._is_faild_plan(respond): + break + elif not self._have_code(respond): + logger.warning(f"llm did not return executable code, resend the query: \n{query}") + respond = interpreter.chat(query) + elif self._is_faild_plan(respond): + logger.warning(f"llm did not generate successful plan, resend the query: \n{query}") + respond = interpreter.chat(query) + + # Post-processing of respond + if not self._have_code(respond): + error_msg = f"OpenCodeInterpreter do not generate code for query: \n{query}" + logger.error(error_msg) + raise ValueError(error_msg) + + if self._is_faild_plan(respond): + error_msg = f"OpenCodeInterpreter do not generate code for query: \n{query}" + logger.error(error_msg) + raise ValueError(error_msg) + return respond + def __call__(self, wrapped): @wrapt.decorator async def wrapper(wrapped: Callable, instance, args, kwargs): # Get the decorated function name. func_name = wrapped.__name__ # If the script exists locally and clearcode is not required, execute the function from the script. - if Path(self.code_file_path).is_file() and not self.clear_code: + if self.code_file_path and Path(self.code_file_path).is_file() and not self.clear_code: return run_function_script(self.code_file_path, func_name, *args, **kwargs) # Auto run generate code by using open-interpreter. @@ -108,6 +163,8 @@ class OpenInterpreterDecorator(object): query = gen_query(wrapped, args, kwargs) logger.info(f"query for OpenCodeInterpreter: \n {query}") respond = interpreter.chat(query) + # Make sure the response is as expected. + respond = self._check_respond(query, interpreter, respond, 3) # Assemble the code blocks generated by open-interpreter into a function without parameters. func_code = interpreter.extract_function(respond, func_name) # Clone the `func_code` into wrapped, that is, @@ -121,9 +178,10 @@ class OpenInterpreterDecorator(object): # execute this function. try: res = run_function_code(code, func_name, *args, **kwargs) - if self.save_code: + if self.save_code and self.code_file_path: cf._save(self.code_file_path, code) except Exception as e: + logger.error(f"Could not evaluate Python code \n{logger_code}: \nError: {e}") raise Exception("Could not evaluate Python code", e) return res return wrapper(wrapped) diff --git a/requirements.txt b/requirements.txt index de861ded9..093298775 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ langchain==0.0.231 loguru==0.6.0 meilisearch==0.21.0 numpy==1.24.3 -openai==0.27.8 +openai openpyxl beautifulsoup4==4.12.2 pandas==2.0.3 @@ -23,7 +23,7 @@ pydantic==1.10.8 #pymilvus==2.2.8 pytest==7.2.2 python_docx==0.8.11 -PyYAML==6.0 +PyYAML==6.0.1 # sentence_transformers==2.2.2 setuptools==65.6.3 tenacity==8.2.2 @@ -39,7 +39,8 @@ typing_extensions==4.5.0 libcst==1.0.1 qdrant-client==1.4.0 pytest-mock==3.11.1 -open-interpreter==0.1.4; python_version>"3.9" +open-interpreter==0.1.7; python_version>"3.9" ta==0.10.2 -semantic-kernel==0.3.10.dev0 - +semantic-kernel==0.3.13.dev0 +wrapt==1.15.0 +websocket-client==0.58.0 diff --git a/tests/metagpt/provider/test_spark_api.py b/tests/metagpt/provider/test_spark_api.py new file mode 100644 index 000000000..bfa2bf76f --- /dev/null +++ b/tests/metagpt/provider/test_spark_api.py @@ -0,0 +1,11 @@ +from metagpt.logs import logger +from metagpt.provider.spark_api import SparkAPI + + +def test_message(): + llm = SparkAPI() + + logger.info(llm.ask('只回答"收到了"这三个字。')) + result = llm.ask('写一篇五百字的日记') + logger.info(result) + assert len(result) > 100