sop.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. from typing import Optional
  2. import json
  3. import random
  4. import re
  5. import time
  6. from wcferry import WxMsg
  7. from common.log import logger
  8. from common.sql_lite import init_new_db_connection
  9. from enum import Enum
  10. from db.message_records import get_next_answer, create_message_record, check_message_record
  11. from db.contact import get_contact_by_wxid, add_contact_label
  12. from db.sop import get_stage
  13. from .plugin import Plugin
  14. from service.robot import get_robot
  15. class Sop(Plugin):
  16. def __init__(self):
  17. super().__init__()
  18. # 根据 sop 的设置返回回答内容
  19. def answer(self, msg: WxMsg, wx_wxid: Optional[str] = None):
  20. connection = init_new_db_connection()
  21. cursor = connection.cursor()
  22. robot = get_robot()
  23. bot_wxid = wx_wxid # 登陆微信人的wx_id
  24. contact_wxid = msg.sender # 发信息的人的wx_id
  25. receiver = wx_wxid # 接收消息的人
  26. contact_type = 2 if msg.from_group() else 1 # 1-人与机器人聊天 2-群内人与机器人聊天
  27. content = msg.content
  28. backup_node_id = -1 # 备份节点id
  29. need_judge = False # 是否需要判断询问大模型
  30. try:
  31. """
  32. 这里需要取出 执行状态的sop_task任务 然后根据他的sop_stage和sop_node内容判断是否
  33. 满足某个sop_node 要求,最后根据设置的 sop 来返回具体的内容
  34. """
  35. message_record, sop_nodes = get_next_answer(cursor, bot_wxid, contact_wxid, contact_type)
  36. if message_record and sop_nodes:
  37. prompt = f"""# 任务
  38. 请根据历史消息,判断用户回复的内容或深层意图,与哪个节点的意图相匹配。
  39. # 历史消息:
  40. 助手发送:{message_record["content"]}
  41. 用户回复:{content}
  42. # 节点列表:"""
  43. organization_id = message_record["organization_id"]
  44. for index, sop_node in enumerate(sop_nodes):
  45. # condition_list = json.loads(sop_node['condition_list'])
  46. """
  47. 代表任意内容,或者不回复,此时不把node节点内容塞给大模型
  48. """
  49. if sop_node['condition_list'] != '[""]':
  50. need_judge = True
  51. prompt += f"""
  52. 节点 id: {index}
  53. 节点意图:{sop_node['condition_list']}
  54. """
  55. else:
  56. if sop_node['no_reply_condition'] == 0:
  57. backup_node_id = index
  58. # prompt += f"""
  59. # 节点 id: {index}
  60. # 命中条件:用户发送任意内容
  61. # """
  62. prompt += f"""
  63. # 回复要求
  64. - 如果命中节点:则仅回复节点 id 数字(如命中多个节点,则仅回复最小值)
  65. - 如果未命中节点:则仅回复一个单词: None"""
  66. if need_judge:
  67. reply = self.reply(prompt, contact_wxid)
  68. else:
  69. reply = Reply()
  70. reply.content = "None"
  71. # logger.debug("[wxsop] reply: ?" % reply)
  72. if reply.content == "None" and backup_node_id != -1:
  73. reply.content = str(backup_node_id)
  74. if reply.content != "None":
  75. # 将 reply.content 从 str 转换为 int
  76. node_order = int(reply.content)
  77. # sop_nodes[node_order]['action_message']的值为 json str ,将其转换为字典
  78. messages = []
  79. forwards = []
  80. haveVar = False
  81. # 命中后发送的消息内容
  82. if sop_nodes[node_order]['action_message'] is not None:
  83. action_messages = json.loads(sop_nodes[node_order]['action_message'])
  84. for index, message in enumerate(action_messages):
  85. if message['content'] != "":
  86. type = int(message['type'])
  87. if type == 1:
  88. haveVar = self.contains_placeholder(message['content'])
  89. messages.append({
  90. "type": 1,
  91. "message": {
  92. "wxid": receiver,
  93. "msg": message['content']
  94. }
  95. })
  96. else:
  97. messages.append({
  98. "type": 2,
  99. "message": {
  100. "wxid": receiver,
  101. "filepath": message['content'],
  102. "diyfilename": message['meta']['filename']
  103. }
  104. })
  105. if len(messages) == 0:
  106. _ = create_message_record(cursor, 3, bot_wxid, message_record["contact_id"],
  107. contact_type,
  108. contact_wxid, 1, "", {}, 4,
  109. sop_nodes[node_order]["id"], 0, organization_id)
  110. # 命中后转发到人工
  111. if sop_nodes[node_order]['action_forward'] is not None:
  112. action_forward = json.loads(sop_nodes[node_order]['action_forward'])
  113. if action_forward['wxid'] != "":
  114. forward_wxids = self.split_string(action_forward['wxid'])
  115. for forward_wxid in forward_wxids:
  116. for index, message in enumerate(action_forward['action']):
  117. if message['content'] != "":
  118. stype = int(message['type'])
  119. if stype == 1:
  120. haveVar = self.contains_placeholder(message['content'])
  121. forwards.append({
  122. "type": 1,
  123. "message": {
  124. "wxid": forward_wxid,
  125. "msg": message['content']
  126. }
  127. })
  128. else:
  129. forwards.append({
  130. "type": 2,
  131. "message": {
  132. "wxid": forward_wxid,
  133. "filepath": message['content'],
  134. "diyfilename": message['meta']['filename']
  135. }
  136. })
  137. if haveVar:
  138. contactinfo = get_contact_by_wxid(receiver)
  139. else:
  140. contactinfo = {}
  141. for index, message in enumerate(messages):
  142. # 随机睡眠
  143. time.sleep(random.uniform(2.0, 5.0))
  144. if message["type"] == 1:
  145. if haveVar:
  146. message["message"]["msg"] = self.var_replace(message["message"]["msg"], contactinfo)
  147. robot.sendTextMsg(message["message"]["msg"], message["message"]["wxid"])
  148. # _ = wx_hook_request("/SendTextMsg", message["message"], server['private_ip'],
  149. # wxinfo['port'])
  150. _ = create_message_record(cursor, 3, bot_wxid, message_record["contact_id"],
  151. contact_type,
  152. contact_wxid, message["type"],
  153. message['message']['msg'], {}, 4,
  154. sop_nodes[node_order]["id"], index, organization_id)
  155. else:
  156. robot.sendFileMsg(message["message"]["filepath"], message["message"]["wxid"])
  157. # _ = wx_hook_request("/SendFileMsg", message["message"], server['private_ip'],
  158. # wxinfo['port'])
  159. _ = create_message_record(cursor, 3, bot_wxid, message_record["contact_id"],
  160. contact_type,
  161. contact_wxid, message["type"],
  162. message['message']['filepath'],
  163. {"filename": message['message']['diyfilename']}, 4,
  164. sop_nodes[node_order]["id"], index, organization_id)
  165. for message in forwards:
  166. # 随机睡眠
  167. time.sleep(random.uniform(2.0, 5.0))
  168. if message["type"] == 1:
  169. if haveVar:
  170. message["message"]["msg"] = self.var_replace(message["message"]["msg"], contactinfo)
  171. robot.sendTextMsg(message["message"]["msg"], message["message"]["wxid"])
  172. # _ = wx_hook_request("/SendTextMsg", message["message"], server['private_ip'],
  173. # wxinfo['port'])
  174. else:
  175. robot.sendFileMsg(message["message"]["filepath"], message["message"]["wxid"])
  176. # _ = wx_hook_request("/SendFileMsg", message["message"], server['private_ip'],
  177. # wxinfo['port'])
  178. action_label_add = json.loads(sop_nodes[node_order]['action_label_add'])
  179. action_label_del = json.loads(sop_nodes[node_order]['action_label_del'])
  180. action_label_add_list = json.loads(sop_nodes[node_order]['action_label_add_list'])
  181. action_label_del_list = json.loads(sop_nodes[node_order]['action_label_del_list'])
  182. if action_label_add or action_label_del:
  183. stages = get_stage(organization_id)
  184. self.add_tag(bot_wxid, message_record["contact_id"], contact_type, contact_wxid,
  185. action_label_add, action_label_del, action_label_add_list, action_label_del_list,
  186. stages, organization_id)
  187. connection.commit()
  188. return msg.content
  189. except Exception as e:
  190. # 回滚事务
  191. connection.rollback()
  192. print(f"发生错误: {e}")
  193. finally:
  194. # 确保资源被正确释放
  195. cursor.close()
  196. connection.close()
  197. # reply 询问大模型
  198. def reply(self, query, prompt: str = None, sender: str = None):
  199. messages = [{"role": "user", "content": prompt}]
  200. reply_content = self._client_reply(self.openAiClient, messages, sender)
  201. if reply_content is not None:
  202. reply = Reply(ReplyType.TEXT, reply_content)
  203. return reply
  204. else:
  205. return Reply(ReplyType.ERROR, "None")
  206. def contains_placeholder(s: str) -> bool:
  207. pattern = r'\$\{.*?\}'
  208. return bool(re.search(pattern, s))
  209. def var_replace(s: str, contactinfo: dict) -> str:
  210. s = s.replace("${nickname}", contactinfo["nickname"])
  211. return s
  212. def split_string(input_string):
  213. # 定义正则表达式,匹配中文逗号、英文逗号和顿号
  214. pattern = r'[,,、]'
  215. # 使用re.split方法分割字符串
  216. result = re.split(pattern, input_string)
  217. return result
  218. # 为联系人添加标签
  219. def add_tag(self, bot_wxid, contact_id, contact_type, contact_wxid, action_label_add,
  220. action_label_del, action_label_add_list, action_label_del_list, stages, organization_id):
  221. connection = init_new_db_connection()
  222. cursor = connection.cursor()
  223. robot = get_robot()
  224. contact_label_ids = add_contact_label(contact_id, action_label_add, action_label_del, action_label_add_list, action_label_del_list)
  225. match_stages = []
  226. # logger.debug("[wxsop] contact_label_ids: ?" % contact_label_ids)
  227. # logger.debug("[wxsop] stages: ?" % stages)
  228. for stage in stages:
  229. if stage["condition_type"] == 1:
  230. # logger.debug("[wxsop] stage: ?" % stage)
  231. if self.check_filter(stage, contact_label_ids):
  232. match_stages.append(stage)
  233. for stage in match_stages:
  234. is_message_send = check_message_record(cursor, contact_wxid, 3, stage["id"], 0)
  235. if is_message_send:
  236. continue
  237. messages = []
  238. forwards = []
  239. haveVar = False
  240. if stage["action_message"]:
  241. action_message = json.loads(stage['action_message'])
  242. for index, message in enumerate(action_message):
  243. if message['content'] != "":
  244. type = int(message['type'])
  245. if type == 1:
  246. haveVar = self.contains_placeholder(message['content'])
  247. messages.append({
  248. "type": 1,
  249. "message": {
  250. "wxid": contact_wxid,
  251. "msg": message['content']
  252. }
  253. })
  254. else:
  255. messages.append({
  256. "type": 2,
  257. "message": {
  258. "wxid": contact_wxid,
  259. "filepath": message['content'],
  260. "diyfilename": message['meta']['filename']
  261. }
  262. })
  263. if len(messages) == 0:
  264. _ = create_message_record(cursor,3, bot_wxid, contact_id,
  265. contact_type,
  266. contact_wxid, 1, "", {}, 3,
  267. stage["id"], 0, organization_id)
  268. if stage["action_forward"]:
  269. action_forward = json.loads(stage['action_forward'])
  270. if action_forward['wxid'] != "":
  271. forward_wxids = self.split_string(action_forward['wxid'])
  272. for forward_wxid in forward_wxids:
  273. for index, message in enumerate(action_forward['action']):
  274. if message['content'] != "":
  275. stype = int(message['type'])
  276. if stype == 1:
  277. haveVar = self.contains_placeholder(message['content'])
  278. forwards.append({
  279. "type": 1,
  280. "message": {
  281. "wxid": forward_wxid,
  282. "msg": message['content']
  283. }
  284. })
  285. # _ = wx_hook_request("/SendTextMsg", data, server['private_ip'], wxinfo['port'])
  286. else:
  287. forwards.append({
  288. "type": 2,
  289. "message": {
  290. "wxid": forward_wxid,
  291. "filepath": message['content'],
  292. "diyfilename": message['meta']['filename']
  293. }
  294. })
  295. if haveVar:
  296. contactinfo = get_contact_by_wxid(contact_wxid)
  297. else:
  298. contactinfo = {}
  299. for index, message in enumerate(messages):
  300. # 随机睡眠
  301. time.sleep(random.uniform(2.0, 5.0))
  302. if message["type"] == 1:
  303. if haveVar:
  304. message["message"]["msg"] = self.var_replace(message["message"]["msg"], contactinfo)
  305. robot.sendTextMsg(message["message"]["msg"], message["message"]["wxid"])
  306. # _ = wx_hook_request("/SendTextMsg", message["message"], server['private_ip'],
  307. # wxinfo['port'])
  308. _ = create_message_record(cursor, 3, bot_wxid, contact_id,
  309. contact_type,
  310. contact_wxid, message["type"],
  311. message['message']['msg'], {}, 3,
  312. stage["id"], index,
  313. organization_id)
  314. else:
  315. robot.sendFileMsg(message["message"]["filepath"], message["message"]["wxid"])
  316. # _ = wx_hook_request("/SendFileMsg", message["message"], server['private_ip'],
  317. # wxinfo['port'])
  318. #
  319. _ = create_message_record(cursor, 3, bot_wxid, contact_id,
  320. contact_type,
  321. contact_wxid, message["type"],
  322. message['message']['filepath'],
  323. {"filename": message['message'][
  324. 'diyfilename']}, 3,
  325. stage["id"], index,
  326. organization_id)
  327. for message in forwards:
  328. # 随机睡眠
  329. time.sleep(random.uniform(2.0, 5.0))
  330. if message["type"] == 1:
  331. if haveVar:
  332. message["message"]["msg"] = self.var_replace(message["message"]["msg"], contactinfo)
  333. robot.sendTextMsg(message["message"]["msg"], message["message"]["wxid"])
  334. # _ = wx_hook_request("/SendTextMsg", message["message"], server['private_ip'],
  335. # wxinfo['port'])
  336. else:
  337. robot.sendFileMsg(message["message"]["filepath"], message["message"]["wxid"])
  338. # _ = wx_hook_request("/SendFileMsg", message["message"], server['private_ip'],
  339. # wxinfo['port'])
  340. if stage["action_label_add"] or stage["action_label_del"]:
  341. add_labels = json.loads(stage['action_label_add_list'])
  342. rem_labels = json.loads(stage['action_label_del_list'])
  343. self.add_tag(bot_wxid, contact_id, contact_type, contact_wxid, add_labels,
  344. rem_labels, stages, organization_id)
  345. def check_filter(filter, contact_label_ids):
  346. condition_operator = filter['condition_operator']
  347. condition_list = json.loads(filter['condition_list'])
  348. if condition_operator == 1:
  349. # All conditions must be met
  350. for condition in condition_list:
  351. if condition['equal'] == 1 and not set(condition['labelIdList']).issubset(set(contact_label_ids)):
  352. return False
  353. elif condition['equal'] == 2 and set(condition['labelIdList']).issubset(set(contact_label_ids)):
  354. return False
  355. return True
  356. elif condition_operator == 2:
  357. # Any condition can be met
  358. for condition in condition_list:
  359. if condition['equal'] == 1 and set(condition['labelIdList']).issubset(set(contact_label_ids)):
  360. return True
  361. elif condition['equal'] == 2 and not set(condition['labelIdList']).issubset(set(contact_label_ids)):
  362. return True
  363. return False
  364. class ReplyType(Enum):
  365. TEXT = 1 # 文本
  366. VOICE = 2 # 音频文件
  367. IMAGE = 3 # 图片文件
  368. IMAGE_URL = 4 # 图片URL
  369. VIDEO_URL = 5 # 视频URL
  370. FILE = 6 # 文件
  371. CARD = 7 # 微信名片,仅支持ntchat
  372. INVITE_ROOM = 8 # 邀请好友进群
  373. INFO = 9
  374. ERROR = 10
  375. TEXT_ = 11 # 强制文本
  376. VIDEO = 12
  377. MINIAPP = 13 # 小程序
  378. JSON_MULTIPLE_RESP = 14 # JSON多条回复数据
  379. LOCATION = 48 # 位置消息
  380. def __str__(self):
  381. return self.name
  382. class Reply:
  383. def __init__(self, stype: ReplyType = None, content=None):
  384. self.type = stype
  385. self.content = content
  386. def __str__(self):
  387. return "Reply(type={}, content={})".format(self.type, self.content)