diff --git a/.github/workflows/update-cmdpriv-template.yml b/.github/workflows/update-cmdpriv-template.yml
index 04862f3b..b8e77f0c 100644
--- a/.github/workflows/update-cmdpriv-template.yml
+++ b/.github/workflows/update-cmdpriv-template.yml
@@ -26,7 +26,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
- python -m pip install --upgrade yiri-mirai openai colorlog func_timeout dulwich Pillow
+ python -m pip install --upgrade yiri-mirai openai colorlog func_timeout dulwich Pillow CallingGPT
- name: Copy Scripts
run: |
diff --git a/README.md b/README.md
index eb7ef015..9107931e 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,7 @@

+> 2023/7/29 支持使用GPT的Function Calling功能实现类似ChatGPT Plugin的效果,请见[Wiki中的内容函数节](https://github.com/RockChinQ/QChatGPT/wiki/%E6%8F%92%E4%BB%B6%E5%BC%80%E5%8F%91#%E5%86%85%E5%AE%B9%E5%87%BD%E6%95%B0)
> 2023/4/24 支持使用go-cqhttp登录QQ,请查看[此文档](https://github.com/RockChinQ/QChatGPT/wiki/go-cqhttp%E9%85%8D%E7%BD%AE)
> 2023/3/18 现已支持GPT-4 API(内测),请查看`config-template.py`中的`completion_api_params`
> 2023/3/15 逆向库已支持New Bing,使用方法查看[插件文档](https://github.com/RockChinQ/revLibs)
@@ -111,6 +112,7 @@
✅支持插件加载🧩
- 自行实现插件加载器及相关支持
+ - 支持GPT的Function Calling功能
- 详细查看[插件使用页](https://github.com/RockChinQ/QChatGPT/wiki/%E6%8F%92%E4%BB%B6%E4%BD%BF%E7%94%A8)
@@ -280,6 +282,8 @@ python3 main.py
详见[Wiki插件使用页](https://github.com/RockChinQ/QChatGPT/wiki/%E6%8F%92%E4%BB%B6%E4%BD%BF%E7%94%A8)
开发教程见[Wiki插件开发页](https://github.com/RockChinQ/QChatGPT/wiki/%E6%8F%92%E4%BB%B6%E5%BC%80%E5%8F%91)
+⭐我们已经支持了[GPT的Function Calling能力](https://platform.openai.com/docs/guides/gpt/function-calling),请查看wiki的插件开发页以查看如何在QChatGPT中使用此功能
+
查看插件列表
@@ -295,6 +299,7 @@ python3 main.py
[插件列表](https://github.com/stars/RockChinQ/lists/qchatgpt-%E6%8F%92%E4%BB%B6),欢迎提出issue以提交新的插件
+- [WebwlkrPlugin](https://github.com/RockChinQ/WebwlkrPlugin) - 让机器人能联网!!
- [revLibs](https://github.com/RockChinQ/revLibs) - 将ChatGPT网页版接入此项目,关于[官方接口和网页版有什么区别](https://github.com/RockChinQ/QChatGPT/wiki/%E5%AE%98%E6%96%B9%E6%8E%A5%E5%8F%A3%E3%80%81ChatGPT%E7%BD%91%E9%A1%B5%E7%89%88%E3%80%81ChatGPT-API%E5%8C%BA%E5%88%AB)
- [Switcher](https://github.com/RockChinQ/Switcher) - 支持通过指令切换使用的模型
- [hello_plugin](https://github.com/RockChinQ/hello_plugin) - `hello_plugin` 的储存库形式,插件开发模板
diff --git a/main.py b/main.py
index 218021c8..15d57010 100644
--- a/main.py
+++ b/main.py
@@ -47,7 +47,7 @@ def init_db():
def ensure_dependencies():
import pkg.utils.pkgmgr as pkgmgr
- pkgmgr.run_pip(["install", "openai", "Pillow", "nakuru-project-idk", "--upgrade",
+ pkgmgr.run_pip(["install", "openai", "Pillow", "nakuru-project-idk", "CallingGPT", "--upgrade",
"-i", "https://pypi.tuna.tsinghua.edu.cn/simple",
"--trusted-host", "pypi.tuna.tsinghua.edu.cn"])
@@ -178,9 +178,14 @@ def start(first_time_init=False):
logging.error(e)
traceback.print_exc()
+ # 配置OpenAI proxy
+ import openai
+ openai.proxy = None # 先重置,因为重载后可能需要清除proxy
+ if "http_proxy" in config.openai_config and config.openai_config["http_proxy"] is not None:
+ openai.proxy = config.openai_config["http_proxy"]
+
# 配置openai api_base
if "reverse_proxy" in config.openai_config and config.openai_config["reverse_proxy"] is not None:
- import openai
openai.api_base = config.openai_config["reverse_proxy"]
# 主启动流程
diff --git a/pkg/openai/api/__init__.py b/pkg/openai/api/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/pkg/openai/api/chat_completion.py b/pkg/openai/api/chat_completion.py
new file mode 100644
index 00000000..f9cab135
--- /dev/null
+++ b/pkg/openai/api/chat_completion.py
@@ -0,0 +1,195 @@
+import openai
+import json
+import logging
+
+from .model import RequestBase
+
+from ..funcmgr import get_func_schema_list, execute_function, get_func, get_func_schema, ContentFunctionNotFoundError
+
+
+class ChatCompletionRequest(RequestBase):
+ """调用ChatCompletion接口的请求类。
+
+ 此类保证每一次返回的角色为assistant的信息的finish_reason一定为stop。
+ 若有函数调用响应,本类的返回瀑布是:函数调用请求->函数调用结果->...->assistant的信息->stop。
+ """
+ model: str
+ messages: list[dict[str, str]]
+ kwargs: dict
+
+ stopped: bool = False
+
+ pending_func_call: dict = None
+
+ pending_msg: str
+
+ def flush_pending_msg(self):
+ self.append_message(
+ role="assistant",
+ content=self.pending_msg
+ )
+ self.pending_msg = ""
+
+ def append_message(self, role: str, content: str, name: str=None):
+ msg = {
+ "role": role,
+ "content": content
+ }
+
+ if name is not None:
+ msg['name'] = name
+
+ self.messages.append(msg)
+
+ def __init__(
+ self,
+ model: str,
+ messages: list[dict[str, str]],
+ **kwargs
+ ):
+ self.model = model
+ self.messages = messages.copy()
+
+ self.kwargs = kwargs
+
+ self.req_func = openai.ChatCompletion.acreate
+
+ self.pending_func_call = None
+
+ self.stopped = False
+
+ self.pending_msg = ""
+
+ def __iter__(self):
+ return self
+
+ def __next__(self) -> dict:
+ if self.stopped:
+ raise StopIteration()
+
+ if self.pending_func_call is None: # 没有待处理的函数调用请求
+
+ args = {
+ "model": self.model,
+ "messages": self.messages,
+ }
+
+ funcs = get_func_schema_list()
+
+ if len(funcs) > 0:
+ args['functions'] = funcs
+
+ # 拼接kwargs
+ args = {**args, **self.kwargs}
+
+ resp = self._req(**args)
+
+ choice0 = resp["choices"][0]
+
+ # 如果不是函数调用,且finish_reason为stop,则停止迭代
+ if 'function_call' not in choice0['message'] and choice0["finish_reason"] == "stop":
+ self.stopped = True
+
+ if 'function_call' in choice0['message']:
+ self.pending_func_call = choice0['message']['function_call']
+
+ self.append_message(
+ role="assistant",
+ content="function call: "+json.dumps(self.pending_func_call, ensure_ascii=False)
+ )
+
+ return {
+ "id": resp["id"],
+ "choices": [
+ {
+ "index": choice0["index"],
+ "message": {
+ "role": "assistant",
+ "type": "function_call",
+ "content": None,
+ "function_call": choice0['message']['function_call']
+ },
+ "finish_reason": "function_call"
+ }
+ ],
+ "usage": resp["usage"]
+ }
+ else:
+
+ # self.pending_msg += choice0['message']['content']
+ # 普通回复一定处于最后方,故不用再追加进内部messages
+
+ return {
+ "id": resp["id"],
+ "choices": [
+ {
+ "index": choice0["index"],
+ "message": {
+ "role": "assistant",
+ "type": "text",
+ "content": choice0['message']['content']
+ },
+ "finish_reason": "stop"
+ }
+ ],
+ "usage": resp["usage"]
+ }
+ else: # 处理函数调用请求
+
+ cp_pending_func_call = self.pending_func_call.copy()
+
+ self.pending_func_call = None
+
+ func_name = cp_pending_func_call['name']
+ arguments = {}
+
+ try:
+
+ try:
+ arguments = json.loads(cp_pending_func_call['arguments'])
+ # 若不是json格式的异常处理
+ except json.decoder.JSONDecodeError:
+ # 获取函数的参数列表
+ func_schema = get_func_schema(func_name)
+
+ arguments = {
+ func_schema['parameters']['required'][0]: cp_pending_func_call['arguments']
+ }
+
+ logging.info("执行函数调用: name={}, arguments={}".format(func_name, arguments))
+
+ # 执行函数调用
+ ret = execute_function(func_name, arguments)
+
+ logging.info("函数执行完成。")
+
+ self.append_message(
+ role="function",
+ content=json.dumps(ret, ensure_ascii=False),
+ name=func_name
+ )
+
+ return {
+ "id": -1,
+ "choices": [
+ {
+ "index": -1,
+ "message": {
+ "role": "function",
+ "type": "function_return",
+ "function_name": func_name,
+ "content": json.dumps(ret, ensure_ascii=False)
+ },
+ "finish_reason": "function_return"
+ }
+ ],
+ "usage": {
+ "prompt_tokens": 0,
+ "completion_tokens": 0,
+ "total_tokens": 0
+ }
+ }
+
+ except ContentFunctionNotFoundError:
+ raise Exception("没有找到函数: {}".format(func_name))
+
diff --git a/pkg/openai/api/completion.py b/pkg/openai/api/completion.py
new file mode 100644
index 00000000..ee0d34e9
--- /dev/null
+++ b/pkg/openai/api/completion.py
@@ -0,0 +1,111 @@
+import openai
+
+from .model import RequestBase
+
+
+class CompletionRequest(RequestBase):
+ """调用Completion接口的请求类。
+
+ 调用方可以一直next completion直到finish_reason为stop。
+ """
+
+ model: str
+ prompt: str
+ kwargs: dict
+
+ stopped: bool = False
+
+ def __init__(
+ self,
+ model: str,
+ messages: list[dict[str, str]],
+ **kwargs
+ ):
+ self.model = model
+ self.prompt = ""
+
+ for message in messages:
+ self.prompt += message["role"] + ": " + message["content"] + "\n"
+
+ self.prompt += "assistant: "
+
+ self.kwargs = kwargs
+
+ self.req_func = openai.Completion.acreate
+
+ def __iter__(self):
+ return self
+
+ def __next__(self) -> dict:
+ """调用Completion接口,返回生成的文本
+
+ {
+ "id": "id",
+ "choices": [
+ {
+ "index": 0,
+ "message": {
+ "role": "assistant",
+ "type": "text",
+ "content": "message"
+ },
+ "finish_reason": "reason"
+ }
+ ],
+ "usage": {
+ "prompt_tokens": 10,
+ "completion_tokens": 20,
+ "total_tokens": 30
+ }
+ }
+ """
+
+ if self.stopped:
+ raise StopIteration()
+
+ resp = self._req(
+ model=self.model,
+ prompt=self.prompt,
+ **self.kwargs
+ )
+
+ if resp["choices"][0]["finish_reason"] == "stop":
+ self.stopped = True
+
+ choice0 = resp["choices"][0]
+
+ self.prompt += choice0["text"]
+
+ return {
+ "id": resp["id"],
+ "choices": [
+ {
+ "index": choice0["index"],
+ "message": {
+ "role": "assistant",
+ "type": "text",
+ "content": choice0["text"]
+ },
+ "finish_reason": choice0["finish_reason"]
+ }
+ ],
+ "usage": resp["usage"]
+ }
+
+if __name__ == "__main__":
+ import os
+
+ openai.api_key = os.environ["OPENAI_API_KEY"]
+
+ for resp in CompletionRequest(
+ model="text-davinci-003",
+ messages=[
+ {
+ "role": "user",
+ "content": "Hello, who are you?"
+ }
+ ]
+ ):
+ print(resp)
+ if resp["choices"][0]["finish_reason"] == "stop":
+ break
diff --git a/pkg/openai/api/model.py b/pkg/openai/api/model.py
new file mode 100644
index 00000000..f58ece0e
--- /dev/null
+++ b/pkg/openai/api/model.py
@@ -0,0 +1,49 @@
+# 定义不同接口请求的模型
+import threading
+import asyncio
+
+import openai
+
+
+class RequestBase:
+
+ req_func: callable
+
+ def __init__(self, *args, **kwargs):
+ raise NotImplementedError
+
+ def _req(self, **kwargs):
+ """处理代理问题"""
+
+ ret: dict = {}
+ exception: Exception = None
+
+ async def awrapper(**kwargs):
+ nonlocal ret, exception
+
+ try:
+ ret = await self.req_func(**kwargs)
+ return ret
+ except Exception as e:
+ exception = e
+
+ loop = asyncio.new_event_loop()
+
+ thr = threading.Thread(
+ target=loop.run_until_complete,
+ args=(awrapper(**kwargs),)
+ )
+
+ thr.start()
+ thr.join()
+
+ if exception is not None:
+ raise exception
+
+ return ret
+
+ def __iter__(self):
+ raise self
+
+ def __next__(self):
+ raise NotImplementedError
diff --git a/pkg/openai/funcmgr.py b/pkg/openai/funcmgr.py
new file mode 100644
index 00000000..06c72e25
--- /dev/null
+++ b/pkg/openai/funcmgr.py
@@ -0,0 +1,47 @@
+# 封装了function calling的一些支持函数
+import logging
+
+
+from pkg.plugin import host
+
+
+class ContentFunctionNotFoundError(Exception):
+ pass
+
+
+def get_func_schema_list() -> list:
+ """从plugin包中的函数结构中获取并处理成受GPT支持的格式"""
+ if not host.__enable_content_functions__:
+ return []
+
+ schemas = []
+
+ for func in host.__callable_functions__:
+ if func['enabled']:
+ fun_cp = func.copy()
+
+ del fun_cp['enabled']
+
+ schemas.append(fun_cp)
+
+ return schemas
+
+def get_func(name: str) -> callable:
+ if name not in host.__function_inst_map__:
+ raise ContentFunctionNotFoundError("没有找到内容函数: {}".format(name))
+
+ return host.__function_inst_map__[name]
+
+def get_func_schema(name: str) -> dict:
+ for func in host.__callable_functions__:
+ if func['name'] == name:
+ return func
+ raise ContentFunctionNotFoundError("没有找到内容函数: {}".format(name))
+
+def execute_function(name: str, kwargs: dict) -> any:
+ """执行函数调用"""
+
+ logging.debug("executing function: name='{}', kwargs={}".format(name, kwargs))
+
+ func = get_func(name)
+ return func(**kwargs)
diff --git a/pkg/openai/manager.py b/pkg/openai/manager.py
index 8a368c3c..c607f220 100644
--- a/pkg/openai/manager.py
+++ b/pkg/openai/manager.py
@@ -5,7 +5,9 @@ import openai
import pkg.openai.keymgr
import pkg.utils.context
import pkg.audit.gatherer
-from pkg.openai.modelmgr import ModelRequest, create_openai_model_request
+from pkg.openai.modelmgr import select_request_cls
+
+from pkg.openai.api.model import RequestBase
class OpenAIInteract:
@@ -33,45 +35,24 @@ class OpenAIInteract:
pkg.utils.context.set_openai_manager(self)
- # 请求OpenAI Completion
- def request_completion(self, prompts) -> tuple[str, int]:
- """请求补全接口回复
-
- Parameters:
- prompts (str): 提示语
-
- Returns:
- str: 回复
+ def request_completion(self, messages: list):
+ """请求补全接口回复=
"""
-
+ # 选择接口请求类
config = pkg.utils.context.get_config()
- # 根据模型选择使用的接口
- ai: ModelRequest = create_openai_model_request(
- config.completion_api_params['model'],
- 'user',
- config.openai_config["http_proxy"] if "http_proxy" in config.openai_config else None
- )
- ai.request(
- prompts,
- **config.completion_api_params
- )
- response = ai.get_response()
+ request: RequestBase
- logging.debug("OpenAI response: %s", response)
+ model: str = config.completion_api_params['model']
- # 记录使用量
- current_round_token = 0
- if 'model' in config.completion_api_params:
- self.audit_mgr.report_text_model_usage(config.completion_api_params['model'],
- ai.get_total_tokens())
- current_round_token = ai.get_total_tokens()
- elif 'engine' in config.completion_api_params:
- self.audit_mgr.report_text_model_usage(config.completion_api_params['engine'],
- response['usage']['total_tokens'])
- current_round_token = response['usage']['total_tokens']
+ cp_parmas = config.completion_api_params.copy()
+ del cp_parmas['model']
- return ai.get_message(), current_round_token
+ request = select_request_cls(model, messages, cp_parmas)
+
+ # 请求接口
+ for resp in request:
+ yield resp
def request_image(self, prompt) -> dict:
"""请求图片接口回复
diff --git a/pkg/openai/modelmgr.py b/pkg/openai/modelmgr.py
index f16105cb..8e6c7f20 100644
--- a/pkg/openai/modelmgr.py
+++ b/pkg/openai/modelmgr.py
@@ -8,6 +8,10 @@ Completion - text-davinci-003 等模型
import openai, logging, threading, asyncio
import openai.error as aiE
+from pkg.openai.api.model import RequestBase
+from pkg.openai.api.completion import CompletionRequest
+from pkg.openai.api.chat_completion import ChatCompletionRequest
+
COMPLETION_MODELS = {
'text-davinci-003',
'text-davinci-002',
@@ -39,153 +43,9 @@ IMAGE_MODELS = {
}
-class ModelRequest:
- """模型接口请求父类"""
-
- can_chat = False
- runtime: threading.Thread = None
- ret = {}
- proxy: str = None
- request_ready = True
- error_info: str = "若在没有任何错误的情况下看到这句话,请带着配置文件上报Issues"
-
- def __init__(self, model_name, user_name, request_fun, http_proxy:str = None, time_out = None):
- self.model_name = model_name
- self.user_name = user_name
- self.request_fun = request_fun
- self.time_out = time_out
- if http_proxy != None:
- self.proxy = http_proxy
- openai.proxy = self.proxy
- self.request_ready = False
-
- async def __a_request__(self, **kwargs):
- """异步请求"""
-
- try:
- self.ret: dict = await self.request_fun(**kwargs)
- self.request_ready = True
- except aiE.APIConnectionError as e:
- self.error_info = "{}\n请检查网络连接或代理是否正常".format(e)
- raise ConnectionError(self.error_info)
- except ValueError as e:
- self.error_info = "{}\n该错误可能是由于http_proxy格式设置错误引起的"
- except Exception as e:
- self.error_info = "{}\n由于请求异常产生的未知错误,请查看日志".format(e)
- raise type(e)(self.error_info)
-
- def request(self, **kwargs):
- """向接口发起请求"""
-
- if self.proxy != None: #异步请求
- self.request_ready = False
- loop = asyncio.new_event_loop()
- self.runtime = threading.Thread(
- target=loop.run_until_complete,
- args=(self.__a_request__(**kwargs),)
- )
- self.runtime.start()
- else: #同步请求
- self.ret = self.request_fun(**kwargs)
-
- def __msg_handle__(self, msg):
- """将prompt dict转换成接口需要的格式"""
- return msg
-
- def ret_handle(self):
- '''
- API消息返回处理函数
- 若重写该方法,应检查异步线程状态,或在需要检查处super该方法
- '''
- if self.runtime != None and isinstance(self.runtime, threading.Thread):
- self.runtime.join(self.time_out)
- if self.request_ready:
- return
- raise Exception(self.error_info)
-
- def get_total_tokens(self):
- try:
- return self.ret['usage']['total_tokens']
- except:
- return 0
-
- def get_message(self):
- return self.message
-
- def get_response(self):
- return self.ret
-
-
-class ChatCompletionModel(ModelRequest):
- """ChatCompletion接口的请求实现"""
-
- Chat_role = ['system', 'user', 'assistant']
- def __init__(self, model_name, user_name, http_proxy:str = None, **kwargs):
- if http_proxy == None:
- request_fun = openai.ChatCompletion.create
- else:
- request_fun = openai.ChatCompletion.acreate
- self.can_chat = True
- super().__init__(model_name, user_name, request_fun, http_proxy, **kwargs)
-
- def request(self, prompts, **kwargs):
- prompts = self.__msg_handle__(prompts)
- kwargs['messages'] = prompts
- super().request(**kwargs)
- self.ret_handle()
-
- def __msg_handle__(self, msgs):
- temp_msgs = []
- # 把msgs拷贝进temp_msgs
- for msg in msgs:
- temp_msgs.append(msg.copy())
- return temp_msgs
-
- def get_message(self):
- return self.ret["choices"][0]["message"]['content'] #需要时直接加载加快请求速度,降低内存消耗
-
-
-class CompletionModel(ModelRequest):
- """Completion接口的请求实现"""
-
- def __init__(self, model_name, user_name, http_proxy:str = None, **kwargs):
- if http_proxy == None:
- request_fun = openai.Completion.create
- else:
- request_fun = openai.Completion.acreate
- super().__init__(model_name, user_name, request_fun, http_proxy, **kwargs)
-
- def request(self, prompts, **kwargs):
- prompts = self.__msg_handle__(prompts)
- kwargs['prompt'] = prompts
- super().request(**kwargs)
- self.ret_handle()
-
- def __msg_handle__(self, msgs):
- prompt = ''
- for msg in msgs:
- prompt = prompt + "{}: {}\n".format(msg['role'], msg['content'])
- # for msg in msgs:
- # if msg['role'] == 'assistant':
- # prompt = prompt + "{}\n".format(msg['content'])
- # else:
- # prompt = prompt + "{}:{}\n".format(msg['role'] , msg['content'])
- prompt = prompt + "assistant: "
- return prompt
-
- def get_message(self):
- return self.ret["choices"][0]["text"]
-
-
-def create_openai_model_request(model_name: str, user_name: str = 'user', http_proxy:str = None) -> ModelRequest:
- """使用给定的模型名称创建模型请求对象"""
+def select_request_cls(model_name: str, messages: list, args: dict) -> RequestBase:
if model_name in CHAT_COMPLETION_MODELS:
- model = ChatCompletionModel(model_name, user_name, http_proxy)
+ return ChatCompletionRequest(model_name, messages, **args)
elif model_name in COMPLETION_MODELS:
- model = CompletionModel(model_name, user_name, http_proxy)
- else :
- log = "找不到模型[{}],请检查配置文件".format(model_name)
- logging.error(log)
- raise IndexError(log)
- logging.debug("使用接口[{}]创建模型请求[{}]".format(model.__class__.__name__, model_name))
- return model
+ return CompletionRequest(model_name, messages, **args)
+ raise ValueError("不支持模型[{}],请检查配置文件".format(model_name))
\ No newline at end of file
diff --git a/pkg/openai/sess.py b/pkg/openai/sess.py
new file mode 100644
index 00000000..898e8b04
--- /dev/null
+++ b/pkg/openai/sess.py
@@ -0,0 +1,79 @@
+import time
+import threading
+import logging
+
+
+sessions = {}
+
+
+class SessionOfflineStatus:
+ ON_GOING = "on_going"
+ EXPLICITLY_CLOSED = "explicitly_closed"
+
+
+def reset_session_prompt(session_name, prompt):
+ pass
+
+
+def load_sessions():
+ pass
+
+
+def get_session(session_name: str) -> 'Session':
+ pass
+
+
+def dump_session(session_name: str):
+ pass
+
+
+class Session:
+ name: str = ''
+
+ default_prompt: list = []
+ """会话系统提示语"""
+
+ messages: list = []
+ """保存消息历史记录"""
+
+ token_counts: list = []
+ """记录每回合的token数量"""
+
+ create_ts: int = 0
+ """会话创建时间戳"""
+
+ last_active_ts: int = 0
+ """会话最后活跃时间戳"""
+
+ just_switched_to_exist_session: bool = False
+
+ response_lock = None
+
+ def __init__(self, name: str):
+ self.name = name
+ self.default_prompt = self.get_runtime_default_prompt()
+ logging.debug("prompt is: {}".format(self.default_prompt))
+ self.messages = []
+ self.token_counts = []
+ self.create_ts = int(time.time())
+ self.last_active_ts = int(time.time())
+
+ self.response_lock = threading.Lock()
+
+ self.schedule()
+
+ def get_runtime_default_prompt(self, use_default: str = None) -> list:
+ """从提示词管理器中获取所需提示词"""
+ import pkg.openai.dprompt as dprompt
+
+ if use_default is None:
+ use_default = dprompt.mode_inst().get_using_name()
+
+ current_default_prompt, _ = dprompt.mode_inst().get_prompt(use_default)
+ return current_default_prompt
+
+ def schedule(self):
+ """定时会话过期检查任务"""
+
+ def expire_check_timer_loop(self):
+ """会话过期检查任务"""
diff --git a/pkg/openai/session.py b/pkg/openai/session.py
index 06b00757..9c718f50 100644
--- a/pkg/openai/session.py
+++ b/pkg/openai/session.py
@@ -222,22 +222,67 @@ class Session:
for token_count in counts:
total_token_before_query += token_count
+ res_text = ""
+
+ pending_msgs = []
+
+ total_tokens = 0
+
+ for resp in pkg.utils.context.get_openai_manager().request_completion(prompts):
+ if resp['choices'][0]['message']['type'] == 'text': # 普通回复
+ res_text += resp['choices'][0]['message']['content']
+
+ total_tokens += resp['usage']['total_tokens']
+
+ pending_msgs.append(
+ {
+ "role": "assistant",
+ "content": resp['choices'][0]['message']['content']
+ }
+ )
+
+ elif resp['choices'][0]['message']['type'] == 'function_call':
+ # self.prompt.append(
+ # {
+ # "role": "assistant",
+ # "content": "function call: "+json.dumps(resp['choices'][0]['message']['function_call'])
+ # }
+ # )
+
+ total_tokens += resp['usage']['total_tokens']
+ elif resp['choices'][0]['message']['type'] == 'function_return':
+ # self.prompt.append(
+ # {
+ # "role": "function",
+ # "name": resp['choices'][0]['message']['function_name'],
+ # "content": json.dumps(resp['choices'][0]['message']['content'])
+ # }
+ # )
+
+ # total_tokens += resp['usage']['total_tokens']
+ pass
+
+
+
# 向API请求补全
- message, total_token = pkg.utils.context.get_openai_manager().request_completion(
- prompts,
- )
+ # message, total_token = pkg.utils.context.get_openai_manager().request_completion(
+ # prompts,
+ # )
# 成功获取,处理回复
- res_test = message
- res_ans = res_test.strip()
+ # res_test = message
+ res_ans = res_text.strip()
# 将此次对话的双方内容加入到prompt中
+ # self.prompt.append({'role': 'user', 'content': text})
+ # self.prompt.append({'role': 'assistant', 'content': res_ans})
self.prompt.append({'role': 'user', 'content': text})
- self.prompt.append({'role': 'assistant', 'content': res_ans})
+ # 添加pending_msgs
+ self.prompt += pending_msgs
# 向token_counts中添加本回合的token数量
- self.token_counts.append(total_token-total_token_before_query)
- logging.debug("本回合使用token: {}, session counts: {}".format(total_token-total_token_before_query, self.token_counts))
+ self.token_counts.append(total_tokens-total_token_before_query)
+ logging.debug("本回合使用token: {}, session counts: {}".format(total_tokens-total_token_before_query, self.token_counts))
if self.just_switched_to_exist_session:
self.just_switched_to_exist_session = False
diff --git a/pkg/plugin/host.py b/pkg/plugin/host.py
index 4249c37e..12269bf4 100644
--- a/pkg/plugin/host.py
+++ b/pkg/plugin/host.py
@@ -16,6 +16,8 @@ import pkg.qqbot.adapter as msadapter
from mirai import Mirai
+from CallingGPT.session.session import Session
+
__plugins__ = {}
"""插件列表
@@ -42,6 +44,15 @@ __plugins__ = {}
__plugins_order__ = []
"""插件顺序"""
+__enable_content_functions__ = True
+"""是否启用内容函数"""
+
+__callable_functions__ = []
+"""供GPT调用的函数结构"""
+
+__function_inst_map__: dict[str, callable] = {}
+"""函数名:实例 映射"""
+
def generate_plugin_order():
"""根据__plugin__生成插件初始顺序,无视是否启用"""
@@ -102,6 +113,10 @@ def load_plugins():
# 加载插件顺序
settings.load_settings()
+ # 输出已注册的内容函数列表
+ logging.debug("registered content functions: {}".format(__callable_functions__))
+ logging.debug("function instance map: {}".format(__function_inst_map__))
+
def initialize_plugins():
"""初始化插件"""
@@ -300,7 +315,9 @@ class PluginHost:
"""插件宿主"""
def __init__(self):
+ """初始化插件宿主"""
context.set_plugin_host(self)
+ self.calling_gpt_session = Session([])
def get_runtime_context(self) -> context:
"""获取运行时上下文(pkg.utils.context模块的对象)
diff --git a/pkg/plugin/models.py b/pkg/plugin/models.py
index 180f0745..40756757 100644
--- a/pkg/plugin/models.py
+++ b/pkg/plugin/models.py
@@ -133,12 +133,18 @@ KeySwitched = "key_switched"
"""
-def on(event: str):
+def on(*args, **kwargs):
"""注册事件监听器
- :param
- event: str 事件名称
"""
- return Plugin.on(event)
+ return Plugin.on(*args, **kwargs)
+
+def func(*args, **kwargs):
+ """注册内容函数,声明此函数为一个内容函数,在对话中将发送此函数给GPT以供其调用
+ 此函数可以具有任意的参数,但必须按照[此文档](https://github.com/RockChinQ/CallingGPT/wiki/1.-Function-Format#function-format)
+ 所述的格式编写函数的docstring。
+ 此功能仅支持在使用gpt-3.5或gpt-4系列模型时使用。
+ """
+ return Plugin.func(*args, **kwargs)
__current_registering_plugin__ = ""
@@ -176,6 +182,34 @@ class Plugin:
return wrapper
+ @classmethod
+ def func(cls, name: str=None):
+ """内容函数装饰器
+ """
+ global __current_registering_plugin__
+ from CallingGPT.entities.namespace import get_func_schema
+
+ def wrapper(func):
+
+ function_schema = get_func_schema(func)
+ function_schema['name'] = __current_registering_plugin__ + '-' + (func.__name__ if name is None else name)
+
+ function_schema['enabled'] = True
+
+ host.__function_inst_map__[function_schema['name']] = function_schema['function']
+
+ del function_schema['function']
+
+ # logging.debug("registering content function: p='{}', f='{}', s={}".format(__current_registering_plugin__, func, function_schema))
+
+ host.__callable_functions__.append(
+ function_schema
+ )
+
+ return func
+
+ return wrapper
+
def register(name: str, description: str, version: str, author: str):
"""注册插件, 此函数作为装饰器使用
diff --git a/pkg/plugin/settings.py b/pkg/plugin/settings.py
index f68ef2c3..92fcfe77 100644
--- a/pkg/plugin/settings.py
+++ b/pkg/plugin/settings.py
@@ -8,7 +8,10 @@ import logging
def wrapper_dict_from_runtime_context() -> dict:
"""从变量中包装settings.json的数据字典"""
settings = {
- "order": []
+ "order": [],
+ "functions": {
+ "enabled": host.__enable_content_functions__
+ }
}
for plugin_name in host.__plugins_order__:
@@ -22,6 +25,11 @@ def apply_settings(settings: dict):
if "order" in settings:
host.__plugins_order__ = settings["order"]
+ if "functions" in settings:
+ if "enabled" in settings["functions"]:
+ host.__enable_content_functions__ = settings["functions"]["enabled"]
+ # logging.debug("set content function enabled: {}".format(host.__enable_content_functions__))
+
def dump_settings():
"""保存settings.json数据"""
@@ -78,6 +86,17 @@ def load_settings():
settings["order"].append(plugin_name)
settings_modified = True
+ if "functions" not in settings:
+ settings["functions"] = {
+ "enabled": host.__enable_content_functions__
+ }
+ settings_modified = True
+ elif "enabled" not in settings["functions"]:
+ settings["functions"]["enabled"] = host.__enable_content_functions__
+ settings_modified = True
+
+ logging.info("已全局{}内容函数。".format("启用" if settings["functions"]["enabled"] else "禁用"))
+
apply_settings(settings)
if settings_modified:
diff --git a/pkg/plugin/switch.py b/pkg/plugin/switch.py
index 1b15a112..041ec128 100644
--- a/pkg/plugin/switch.py
+++ b/pkg/plugin/switch.py
@@ -28,6 +28,11 @@ def apply_switch(switch: dict):
for plugin_name in switch:
host.__plugins__[plugin_name]["enabled"] = switch[plugin_name]["enabled"]
+ # 查找此插件的所有内容函数
+ for func in host.__callable_functions__:
+ if func['name'].startswith(plugin_name + '-'):
+ func['enabled'] = switch[plugin_name]["enabled"]
+
def dump_switch():
"""保存开关数据"""
diff --git a/pkg/qqbot/cmds/funcs/func.py b/pkg/qqbot/cmds/funcs/func.py
new file mode 100644
index 00000000..9199292a
--- /dev/null
+++ b/pkg/qqbot/cmds/funcs/func.py
@@ -0,0 +1,28 @@
+from ..aamgr import AbstractCommandNode, Context
+import logging
+
+
+@AbstractCommandNode.register(
+ parent=None,
+ name="func",
+ description="管理内容函数",
+ usage="!func",
+ aliases=[],
+ privilege=1
+)
+class FuncCommand(AbstractCommandNode):
+ @classmethod
+ def process(cls, ctx: Context) -> tuple[bool, list]:
+ from pkg.plugin.models import host
+
+ reply = []
+
+ reply_str = "当前已加载的内容函数:\n\n"
+
+ index = 1
+ for func in host.__callable_functions__:
+ reply_str += "{}. {}{}:\n{}\n\n".format(index, ("(已禁用) " if not func['enabled'] else ""), func['name'], func['description'])
+
+ reply = [reply_str]
+
+ return True, reply
diff --git a/requirements.txt b/requirements.txt
index 64160e9f..60a072f8 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -7,4 +7,5 @@ websockets
urllib3~=1.26.10
func_timeout~=4.3.5
Pillow
-nakuru-project-idk
\ No newline at end of file
+nakuru-project-idk
+CallingGPT
\ No newline at end of file
diff --git a/res/templates/cmdpriv-template.json b/res/templates/cmdpriv-template.json
index fea372bc..18bc3db9 100644
--- a/res/templates/cmdpriv-template.json
+++ b/res/templates/cmdpriv-template.json
@@ -1,6 +1,7 @@
{
"comment": "以下为命令权限,请设置到cmdpriv.json中。关于此功能的说明,请查看:https://github.com/RockChinQ/QChatGPT/wiki/%E5%8A%9F%E8%83%BD%E4%BD%BF%E7%94%A8#%E5%91%BD%E4%BB%A4%E6%9D%83%E9%99%90%E6%8E%A7%E5%88%B6",
"draw": 1,
+ "func": 1,
"plugin": 2,
"plugin.get": 2,
"plugin.update": 2,
diff --git a/res/wiki/插件使用.md b/res/wiki/插件使用.md
index f0db3f12..143c7e85 100644
--- a/res/wiki/插件使用.md
+++ b/res/wiki/插件使用.md
@@ -33,6 +33,8 @@ QChatGPT 插件使用Wiki
!plugin del <插件名> 删除插件(需要管理员权限)
!plugin on <插件名> 启用插件(需要管理员权限)
!plugin off <插件名> 禁用插件(需要管理员权限)
+
+!func 列出所有内容函数
```
### 控制插件执行顺序
@@ -42,4 +44,9 @@ QChatGPT 插件使用Wiki
### 启用或关闭插件
无需卸载即可管理插件的开关
-编辑`plugins`目录下的`switch.json`文件,将相应的插件的`enabled`字段设置为`true/false(开/关)`,之后重启程序或执行热重载即可控制插件开关
\ No newline at end of file
+编辑`plugins`目录下的`switch.json`文件,将相应的插件的`enabled`字段设置为`true/false(开/关)`,之后重启程序或执行热重载即可控制插件开关
+
+### 控制全局内容函数开关
+
+内容函数是基于[GPT的Function Calling能力](https://platform.openai.com/docs/guides/gpt/function-calling)实现的,这是一种嵌入对话中,由GPT自动调用的函数。
+每个插件可以自行注册内容函数,您可以在`plugins`目录下的`settings.json`中设置`functions`下的`enabled`为`true`或`false`控制这些内容函数的启用或禁用。
\ No newline at end of file
diff --git a/res/wiki/插件开发.md b/res/wiki/插件开发.md
index e3c6a7bb..6888f72c 100644
--- a/res/wiki/插件开发.md
+++ b/res/wiki/插件开发.md
@@ -113,6 +113,182 @@ class HelloPlugin(Plugin):
- 一个目录内可以存放多个Python程序文件,以独立出插件的各个功能,便于开发者管理,但不建议在一个目录内注册多个插件
- 插件需要的依赖库请在插件目录下的`requirements.txt`中指定,程序从储存库获取此插件时将自动安装依赖
+## 🪝内容函数
+
+通过[GPT的Function Calling能力](https://platform.openai.com/docs/guides/gpt/function-calling)实现的`内容函数`,这是一种嵌入对话中,由GPT自动调用的函数。
+
+
+示例:联网插件
+
+加载含有联网功能的内容函数的插件[WebwlkrPlugin](https://github.com/RockChinQ/WebwlkrPlugin),向机器人询问在线内容
+
+```
+# 控制台输出
+[2023-07-29 17:37:18.698] message.py (26) - [INFO] : [person_1010553892]发送消息:介绍一下这个项目:https://git...
+[2023-07-29 17:37:21.292] util.py (67) - [INFO] : message='OpenAI API response' path=https://api.openai.com/v1/chat/completions processing_ms=1902 request_id=941afc13b2e1bba1e7877b92a970cdea response_code=200
+[2023-07-29 17:37:21.293] chat_completion.py (159) - [INFO] : 执行函数调用: name=Webwlkr-access_the_web, arguments={'url': 'https://github.com/RockChinQ/QChatGPT', 'brief_len': 512}
+[2023-07-29 17:37:21.848] chat_completion.py (164) - [INFO] : 函数执行完成。
+```
+
+
+
+
+
+### 内容函数编写步骤
+
+1️⃣ 请先按照上方步骤编写您的插件基础结构,现在请删除(当然你也可以不删,只是为了简洁)上述插件内容的诸个由`@on`装饰的类函数
+
+
+删除后的结构
+
+```python
+from pkg.plugin.models import *
+from pkg.plugin.host import EventContext, PluginHost
+
+"""
+在收到私聊或群聊消息"hello"时,回复"hello, <发送者id>!"或"hello, everyone!"
+"""
+
+
+# 注册插件
+@register(name="Hello", description="hello world", version="0.1", author="RockChinQ")
+class HelloPlugin(Plugin):
+
+ # 插件加载时触发
+ # plugin_host (pkg.plugin.host.PluginHost) 提供了与主程序交互的一些方法,详细请查看其源码
+ def __init__(self, plugin_host: PluginHost):
+ pass
+
+ # 插件卸载时触发
+ def __del__(self):
+ pass
+```
+
+
+
+2️⃣ 现在我们将以下函数添加到刚刚删除的函数的位置
+
+```Python
+
+# 要添加的函数
+
+@func(name="access_the_web") # 设置函数名称
+def _(url: str):
+ """Call this function to search about the question before you answer any questions.
+ - Do not search through baidu.com at any time.
+ - If you need to search somthing, visit https://www.google.com/search?q=xxx.
+ - If user ask you to open a url (start with http:// or https://), visit it directly.
+ - Summary the plain content result by yourself, DO NOT directly output anything in the result you got.
+
+ Args:
+ url(str): url to visit
+
+ Returns:
+ str: plain text content of the web page
+ """
+ import requests
+ from bs4 import BeautifulSoup
+ # 你需要先使用
+ # pip install beautifulsoup4
+ # 安装依赖
+
+ r = requests.get(
+ url,
+ timeout=10,
+ headers={
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 Edg/115.0.1901.183"
+ }
+ )
+ soup = BeautifulSoup(r.text, 'html.parser')
+
+ s = soup.get_text()
+
+ # 删除多余的空行或仅有\t和空格的行
+ s = re.sub(r'\n\s*\n', '\n', s)
+
+ if len(s) >= 512: # 截取获取到的网页纯文本内容的前512个字
+ return s[:512]
+
+ return s
+
+```
+
+现在这个文件内容应该是这样
+
+```python
+from pkg.plugin.models import *
+from pkg.plugin.host import EventContext, PluginHost
+
+"""
+在收到私聊或群聊消息"hello"时,回复"hello, <发送者id>!"或"hello, everyone!"
+"""
+
+
+# 注册插件
+@register(name="Hello", description="hello world", version="0.1", author="RockChinQ")
+class HelloPlugin(Plugin):
+
+ # 插件加载时触发
+ # plugin_host (pkg.plugin.host.PluginHost) 提供了与主程序交互的一些方法,详细请查看其源码
+ def __init__(self, plugin_host: PluginHost):
+ pass
+
+ @func(name="access_the_web")
+ def _(url: str):
+ """Call this function to search about the question before you answer any questions.
+ - Do not search through baidu.com at any time.
+ - If you need to search somthing, visit https://www.google.com/search?q=xxx.
+ - If user ask you to open a url (start with http:// or https://), visit it directly.
+ - Summary the plain content result by yourself, DO NOT directly output anything in the result you got.
+
+ Args:
+ url(str): url to visit
+
+ Returns:
+ str: plain text content of the web page
+ """
+ import requests
+ from bs4 import BeautifulSoup
+ # 你需要先使用
+ # pip install beautifulsoup4
+ # 安装依赖
+
+ r = requests.get(
+ url,
+ timeout=10,
+ headers={
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 Edg/115.0.1901.183"
+ }
+ )
+ soup = BeautifulSoup(r.text, 'html.parser')
+
+ s = soup.get_text()
+
+ # 删除多余的空行或仅有\t和空格的行
+ s = re.sub(r'\n\s*\n', '\n', s)
+
+ if len(s) >= 512: # 截取获取到的网页纯文本内容的前512个字
+ return s[:512]
+
+ return s
+
+ # 插件卸载时触发
+ def __del__(self):
+ pass
+```
+
+
+
+#### 请注意:
+
+- 函数的注释必须严格按照要求的格式进行书写,具体格式请查看[此文档](https://github.com/RockChinQ/CallingGPT/wiki/1.-Function-Format#function-format)
+- 内容函数和`以@on装饰的行为函数`可以同时存在于同一个插件,并同时受到`switch.json`中的插件开关的控制
+- 务必确保您使用的模型支持函数调用功能,可以到`config.py`的`completion_api_params`中修改模型,推荐使用`gpt-3.5-turbo-16k`
+
+3️⃣ 现在您的程序已具备网络访问功能,重启程序,询问机器人有关在线的内容或直接发送文章链接请求其总结。
+
+- 这仅仅是一个示例,需要更高效的网络访问能力支持插件,请查看[WebwlkrPlugin](https://github.com/RockChinQ/WebwlkrPlugin)
+
## 📄API参考
### 说明