You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

251 lines
14KB

  1. from chatgpt_tool_hub.apps import AppFactory
  2. from chatgpt_tool_hub.apps.app import App
  3. from chatgpt_tool_hub.tools.tool_register import main_tool_register
  4. import plugins
  5. from bridge.bridge import Bridge
  6. from bridge.context import ContextType
  7. from bridge.reply import Reply, ReplyType
  8. from common import const
  9. from config import conf, get_appdata_dir
  10. from plugins import *
  11. @plugins.register(
  12. name="tool",
  13. desc="Arming your ChatGPT bot with various tools",
  14. version="0.5",
  15. author="goldfishh",
  16. desire_priority=0,
  17. )
  18. class Tool(Plugin):
  19. def __init__(self):
  20. super().__init__()
  21. try:
  22. self.handlers[Event.ON_HANDLE_CONTEXT] = self.on_handle_context
  23. self.app = self._reset_app()
  24. if not self.tool_config.get("tools"):
  25. raise Exception("config.json not found")
  26. logger.info("[tool] inited")
  27. except Exception as e:
  28. logger.warn("[tool] init failed, ignore ")
  29. raise e
  30. def get_help_text(self, verbose=False, **kwargs):
  31. help_text = "这是一个能让chatgpt联网,搜索,数字运算的插件,将赋予强大且丰富的扩展能力。"
  32. trigger_prefix = conf().get("plugin_trigger_prefix", "$")
  33. if not verbose:
  34. return help_text
  35. help_text += "\n使用说明:\n"
  36. help_text += f"{trigger_prefix}tool " + "命令: 根据给出的{命令}模型来选择使用哪些工具尽力为你得到结果。\n"
  37. help_text += f"{trigger_prefix}tool 工具名 " + "命令: 根据给出的{命令}使用指定工具尽力为你得到结果。\n"
  38. help_text += f"{trigger_prefix}tool reset: 重置工具。\n\n"
  39. help_text += f"已加载工具列表: \n"
  40. for idx, tool in enumerate(main_tool_register.get_registered_tool_names()):
  41. if idx != 0:
  42. help_text += ", "
  43. help_text += f"{tool}"
  44. return help_text
  45. def on_handle_context(self, e_context: EventContext):
  46. if e_context["context"].type != ContextType.TEXT:
  47. return
  48. # 暂时不支持未来扩展的bot
  49. if Bridge().get_bot_type("chat") not in (
  50. const.CHATGPT,
  51. const.OPEN_AI,
  52. const.CHATGPTONAZURE,
  53. const.LINKAI,
  54. ):
  55. return
  56. content = e_context["context"].content
  57. content_list = e_context["context"].content.split(maxsplit=1)
  58. if not content or len(content_list) < 1:
  59. e_context.action = EventAction.CONTINUE
  60. return
  61. logger.debug("[tool] on_handle_context. content: %s" % content)
  62. reply = Reply()
  63. reply.type = ReplyType.TEXT
  64. trigger_prefix = conf().get("plugin_trigger_prefix", "$")
  65. # todo: 有些工具必须要api-key,需要修改config文件,所以这里没有实现query增删tool的功能
  66. if content.startswith(f"{trigger_prefix}tool"):
  67. if len(content_list) == 1:
  68. logger.debug("[tool]: get help")
  69. reply.content = self.get_help_text()
  70. e_context["reply"] = reply
  71. e_context.action = EventAction.BREAK_PASS
  72. return
  73. elif len(content_list) > 1:
  74. if content_list[1].strip() == "reset":
  75. logger.debug("[tool]: reset config")
  76. self.app = self._reset_app()
  77. reply.content = "重置工具成功"
  78. e_context["reply"] = reply
  79. e_context.action = EventAction.BREAK_PASS
  80. return
  81. elif content_list[1].startswith("reset"):
  82. logger.debug("[tool]: remind")
  83. e_context["context"].content = "请你随机用一种聊天风格,提醒用户:如果想重置tool插件,reset之后不要加任何字符"
  84. e_context.action = EventAction.BREAK
  85. return
  86. query = content_list[1].strip()
  87. use_one_tool = False
  88. for tool_name in main_tool_register.get_registered_tool_names():
  89. if query.startswith(tool_name):
  90. use_one_tool = True
  91. query = query[len(tool_name):]
  92. break
  93. # Don't modify bot name
  94. all_sessions = Bridge().get_bot("chat").sessions
  95. user_session = all_sessions.session_query(query, e_context["context"]["session_id"]).messages
  96. logger.debug("[tool]: just-go")
  97. try:
  98. if use_one_tool:
  99. _func, _ = main_tool_register.get_registered_tool()[tool_name]
  100. tool = _func(**self.app_kwargs)
  101. _reply = tool.run(query)
  102. else:
  103. # chatgpt-tool-hub will reply you with many tools
  104. _reply = self.app.ask(query, user_session)
  105. e_context.action = EventAction.BREAK_PASS
  106. all_sessions.session_reply(_reply, e_context["context"]["session_id"])
  107. except Exception as e:
  108. logger.exception(e)
  109. logger.error(str(e))
  110. e_context["context"].content = "请你随机用一种聊天风格,提醒用户:这个问题tool插件暂时无法处理"
  111. reply.type = ReplyType.ERROR
  112. e_context.action = EventAction.BREAK
  113. return
  114. reply.content = _reply
  115. e_context["reply"] = reply
  116. return
  117. def _read_json(self) -> dict:
  118. default_config = {"tools": [], "kwargs": {}}
  119. return super().load_config() or default_config
  120. def _build_tool_kwargs(self, kwargs: dict):
  121. tool_model_name = kwargs.get("model_name")
  122. request_timeout = kwargs.get("request_timeout")
  123. return {
  124. # 全局配置相关
  125. "log": False, # tool 日志开关
  126. "debug": kwargs.get("debug", False), # 输出更多日志
  127. "no_default": kwargs.get("no_default", False), # 不要默认的工具,只加载自己导入的工具
  128. "think_depth": kwargs.get("think_depth", 2), # 一个问题最多使用多少次工具
  129. "proxy": conf().get("proxy", ""), # 科学上网
  130. "request_timeout": request_timeout if request_timeout else conf().get("request_timeout", 120),
  131. "temperature": kwargs.get("temperature", 0), # llm 温度,建议设置0
  132. # LLM配置相关
  133. "llm_api_key": conf().get("open_ai_api_key", ""), # 如果llm api用key鉴权,传入这里
  134. "llm_api_base_url": conf().get("open_ai_api_base", "https://api.openai.com/v1"), # 支持openai接口的llm服务地址前缀
  135. "deployment_id": conf().get("azure_deployment_id", ""), # azure openai会用到
  136. # note: 目前tool暂未对其他模型测试,但这里仍对配置来源做了优先级区分,一般插件配置可覆盖全局配置
  137. "model_name": tool_model_name if tool_model_name else conf().get("model", const.GPT35),
  138. # 工具配置相关
  139. # for arxiv tool
  140. "arxiv_simple": kwargs.get("arxiv_simple", True), # 返回内容更精简
  141. "arxiv_top_k_results": kwargs.get("arxiv_top_k_results", 2), # 只返回前k个搜索结果
  142. "arxiv_sort_by": kwargs.get("arxiv_sort_by", "relevance"), # 搜索排序方式 ["relevance","lastUpdatedDate","submittedDate"]
  143. "arxiv_sort_order": kwargs.get("arxiv_sort_order", "descending"), # 搜索排序方式 ["ascending", "descending"]
  144. "arxiv_output_type": kwargs.get("arxiv_output_type", "text"), # 搜索结果类型 ["text", "pdf", "all"]
  145. # for bing-search tool
  146. "bing_subscription_key": kwargs.get("bing_subscription_key", ""),
  147. "bing_search_url": kwargs.get("bing_search_url", "https://api.bing.microsoft.com/v7.0/search"), # 必应搜索的endpoint地址,无需修改
  148. "bing_search_top_k_results": kwargs.get("bing_search_top_k_results", 2), # 只返回前k个搜索结果
  149. "bing_search_simple": kwargs.get("bing_search_simple", True), # 返回内容更精简
  150. "bing_search_output_type": kwargs.get("bing_search_output_type", "text"), # 搜索结果类型 ["text", "json"]
  151. # for email tool
  152. "email_nickname_mapping": kwargs.get("email_nickname_mapping", "{}"), # 关于人的代号对应的邮箱地址,可以不输入邮箱地址发送邮件。键为代号值为邮箱地址
  153. "email_smtp_host": kwargs.get("email_smtp_host", ""), # 例如 'smtp.qq.com'
  154. "email_smtp_port": kwargs.get("email_smtp_port", ""), # 例如 587
  155. "email_sender": kwargs.get("email_sender", ""), # 发送者的邮件地址
  156. "email_authorization_code": kwargs.get("email_authorization_code", ""), # 发送者验证秘钥(可能不是登录密码)
  157. # for google-search tool
  158. "google_api_key": kwargs.get("google_api_key", ""),
  159. "google_cse_id": kwargs.get("google_cse_id", ""),
  160. "google_simple": kwargs.get("google_simple", True), # 返回内容更精简
  161. "google_output_type": kwargs.get("google_output_type", "text"), # 搜索结果类型 ["text", "json"]
  162. # for finance-news tool
  163. "finance_news_filter": kwargs.get("finance_news_filter", False), # 是否开启过滤
  164. "finance_news_filter_list": kwargs.get("finance_news_filter_list", []), # 过滤词列表
  165. "finance_news_simple": kwargs.get("finance_news_simple", True), # 返回内容更精简
  166. "finance_news_repeat_news": kwargs.get("finance_news_repeat_news", False), # 是否过滤不返回。该tool每次返回约50条新闻,可能有重复新闻
  167. # for morning-news tool
  168. "morning_news_api_key": kwargs.get("morning_news_api_key", ""), # api-key
  169. "morning_news_simple": kwargs.get("morning_news_simple", True), # 返回内容更精简
  170. "morning_news_output_type": kwargs.get("morning_news_output_type", "text"), # 搜索结果类型 ["text", "image"]
  171. # for news-api tool
  172. "news_api_key": kwargs.get("news_api_key", ""),
  173. # for searxng-search tool
  174. "searxng_search_host": kwargs.get("searxng_search_host", ""),
  175. "searxng_search_top_k_results": kwargs.get("searxng_search_top_k_results", 2), # 只返回前k个搜索结果
  176. "searxng_search_output_type": kwargs.get("searxng_search_output_type", "text"), # 搜索结果类型 ["text", "json"]
  177. # for sms tool
  178. "sms_nickname_mapping": kwargs.get("sms_nickname_mapping", "{}"), # 关于人的代号对应的手机号,可以不输入手机号发送sms。键为代号值为手机号
  179. "sms_username": kwargs.get("sms_username", ""), # smsbao用户名
  180. "sms_apikey": kwargs.get("sms_apikey", ""), # smsbao
  181. # for stt tool
  182. "stt_api_key": kwargs.get("stt_api_key", ""), # azure
  183. "stt_api_region": kwargs.get("stt_api_region", ""), # azure
  184. "stt_recognition_language": kwargs.get("stt_recognition_language", "zh-CN"), # 识别的语言类型 部分:en-US ja-JP ko-KR yue-CN zh-CN
  185. # for tts tool
  186. "tts_api_key": kwargs.get("tts_api_key", ""), # azure
  187. "tts_api_region": kwargs.get("tts_api_region", ""), # azure
  188. "tts_auto_detect": kwargs.get("tts_auto_detect", True), # 是否自动检测语音的语言
  189. "tts_speech_id": kwargs.get("tts_speech_id", "zh-CN-XiaozhenNeural"), # 输出语音ID
  190. # for summary tool
  191. "summary_max_segment_length": kwargs.get("summary_max_segment_length", 2500), # 每2500tokens分段,多段触发总结tool
  192. # for terminal tool
  193. "terminal_nsfc_filter": kwargs.get("terminal_nsfc_filter", True), # 是否过滤llm输出的危险命令
  194. "terminal_return_err_output": kwargs.get("terminal_return_err_output", True), # 是否输出错误信息
  195. "terminal_timeout": kwargs.get("terminal_timeout", 20), # 允许命令最长执行时间
  196. # for visual tool
  197. "caption_api_key": kwargs.get("caption_api_key", ""), # ali dashscope apikey
  198. # for browser tool
  199. "browser_use_summary": kwargs.get("browser_use_summary", True), # 是否对返回结果使用tool功能
  200. # for url-get tool
  201. "url_get_use_summary": kwargs.get("url_get_use_summary", True), # 是否对返回结果使用tool功能
  202. # for wechat tool
  203. "wechat_hot_reload": kwargs.get("wechat_hot_reload", True), # 是否使用热重载的方式发送wechat
  204. "wechat_cpt_path": kwargs.get("wechat_cpt_path", os.path.join(get_appdata_dir(), "itchat.pkl")), # wechat 配置文件(`itchat.pkl`)
  205. "wechat_send_group": kwargs.get("wechat_send_group", False), # 是否向群组发送消息
  206. "wechat_nickname_mapping": kwargs.get("wechat_nickname_mapping", "{}"), # 关于人的代号映射关系。键为代号值为微信名(昵称、备注名均可)
  207. # for wikipedia tool
  208. "wikipedia_top_k_results": kwargs.get("wikipedia_top_k_results", 2), # 只返回前k个搜索结果
  209. # for wolfram-alpha tool
  210. "wolfram_alpha_appid": kwargs.get("wolfram_alpha_appid", ""),
  211. }
  212. def _filter_tool_list(self, tool_list: list):
  213. valid_list = []
  214. for tool in tool_list:
  215. if tool in main_tool_register.get_registered_tool_names():
  216. valid_list.append(tool)
  217. else:
  218. logger.warning("[tool] filter invalid tool: " + repr(tool))
  219. return valid_list
  220. def _reset_app(self) -> App:
  221. self.tool_config = self._read_json()
  222. self.app_kwargs = self._build_tool_kwargs(self.tool_config.get("kwargs", {}))
  223. app = AppFactory()
  224. app.init_env(**self.app_kwargs)
  225. # filter not support tool
  226. tool_list = self._filter_tool_list(self.tool_config.get("tools", []))
  227. return app.create_app(tools_list=tool_list, **self.app_kwargs)