mongo.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. import threading
  2. from pymongo import MongoClient
  3. from bson import ObjectId
  4. from datetime import datetime
  5. from dotenv import load_dotenv
  6. import os
  7. load_dotenv()
  8. MONGO_URI = os.getenv("ARK_LOGS_MONGO_URI")
  9. client = MongoClient(MONGO_URI, serverSelectionTimeoutMS=5000)
  10. # 数据库
  11. db = client["arklogs"]
  12. # 豆包大模型的对话日志
  13. chat_logs = db["chat_logs"]
  14. # 聊天历史记录
  15. chat_history_col = db["chat_history"]
  16. # 兴趣圈集合
  17. circle_prompts = db["circle_prompt"]
  18. # 历史人物集合
  19. historical_figures = db["historical_figures"]
  20. def _ensure_index():
  21. try:
  22. chat_logs.create_index([("user_id", 1), ("asked_at", -1)])
  23. chat_history_col.create_index([("user_id", 1), ("session_id", 1), ("timestamp", -1)])
  24. except Exception:
  25. pass
  26. def _do_save_chat_log(doc: dict):
  27. try:
  28. _ensure_index()
  29. chat_logs.insert_one(doc)
  30. except Exception as e:
  31. print(f"MongoDB 日志写入失败: {e}")
  32. def save_chat_log(
  33. user_id: str,
  34. question: str,
  35. stream_mode: bool,
  36. raw_response: str = None,
  37. status: str = "success",
  38. error: str = None,
  39. ):
  40. """
  41. 运维功能:保存聊天原始响应日志到 MongoDB(异步写入,不阻塞调用方)【日志审计】
  42. Args:
  43. user_id: 提问人
  44. question: 提问的问题
  45. stream_mode: 回答方式(流式或非流式)
  46. raw_response: API 原始响应的 repr 字符串
  47. status: 响应状态 success | error
  48. error: 异常时的错误信息
  49. """
  50. doc = {
  51. "user_id": user_id,
  52. "question": question,
  53. "stream_mode": stream_mode,
  54. "raw_response": raw_response,
  55. "status": status,
  56. "error": error,
  57. "asked_at": datetime.now(),
  58. }
  59. threading.Thread(target=_do_save_chat_log, args=(doc,), daemon=True).start()
  60. def save_chat_history(
  61. user_id: str,
  62. session_id: str,
  63. role: str,
  64. content: str,
  65. timestamp: datetime,
  66. response_id: str = None,
  67. thinking: str = None,
  68. searching: str = None,
  69. attachments: list = None,
  70. input_tokens: int = None,
  71. output_tokens: int = None,
  72. total_tokens: int = None,
  73. app_name:str=None,
  74. ):
  75. """
  76. 业务功能: 保存用户跟chat机器人的聊天记录,用于会话存储
  77. """
  78. try:
  79. _ensure_index()
  80. doc = {
  81. "user_id": user_id,
  82. "session_id": session_id,
  83. "role": role,
  84. "content": content,
  85. "thinking": thinking,
  86. "searching": searching,
  87. "response_id": response_id,
  88. "input_tokens": input_tokens,
  89. "output_tokens": output_tokens,
  90. "total_tokens": total_tokens,
  91. "timestamp": timestamp,
  92. "app_name": app_name,
  93. }
  94. if attachments:
  95. doc["attachments"] = attachments
  96. chat_history_col.insert_one(doc)
  97. except Exception as e:
  98. print(f"MongoDB 聊天历史写入失败: {e}")
  99. def get_chat_history(user_id: str, session_id: str) -> list:
  100. try:
  101. docs = chat_history_col.find(
  102. {"user_id": user_id, "session_id": session_id},
  103. {"_id": 0}
  104. ).sort("timestamp", 1)
  105. return list(docs)
  106. except Exception as e:
  107. print(f"MongoDB 聊天历史读取失败: {e}")
  108. return []
  109. def get_last_response_id(user_id: str, session_id: str) -> str | None:
  110. try:
  111. doc = chat_history_col.find_one(
  112. {"user_id": user_id, "session_id": session_id, "role": "assistant", "response_id": {"$ne": None}},
  113. {"response_id": 1, "_id": 0},
  114. sort=[("timestamp", -1)]
  115. )
  116. return doc["response_id"] if doc else None
  117. except Exception as e:
  118. print(f"MongoDB 查询 response_id 失败: {e}")
  119. return None
  120. def delete_chat_history(user_id: str, session_id: str) -> int:
  121. try:
  122. result = chat_history_col.delete_many({"user_id": user_id, "session_id": session_id})
  123. return result.deleted_count
  124. except Exception as e:
  125. print(f"MongoDB 聊天历史删除失败: {e}")
  126. return 0
  127. def get_sessions(user_id: str) -> list:
  128. try:
  129. pipeline = [
  130. {"$match": {"user_id": user_id, "role": "user"}},
  131. {"$sort": {"timestamp": 1}},
  132. {"$group": {
  133. "_id": "$session_id",
  134. "createdAt": {"$first": "$timestamp"},
  135. "preview": {"$first": "$content"},
  136. }},
  137. {"$sort": {"createdAt": -1}},
  138. {"$project": {
  139. "_id": 0,
  140. "sessionId": "$_id",
  141. "createdAt": 1,
  142. "preview": {"$substrCP": ["$preview", 0, 20]},
  143. }},
  144. ]
  145. return list(chat_history_col.aggregate(pipeline))
  146. except Exception as e:
  147. print(f"MongoDB 会话列表查询失败: {e}")
  148. return []
  149. _DEFAULT_PROMPT_CONFIG = {
  150. "name": "兴趣圈",
  151. "role": "活跃用户",
  152. "style": "自然亲切,有活人感",
  153. "keywords": [],
  154. "forbidden": [],
  155. }
  156. def get_circle_prompt(app_name: str) -> dict:
  157. try:
  158. doc = circle_prompts.find_one({"appName": app_name})
  159. return doc if doc else _DEFAULT_PROMPT_CONFIG
  160. except Exception:
  161. return _DEFAULT_PROMPT_CONFIG
  162. def upsert_circle_prompt(data: dict) -> None:
  163. circle_prompts.update_one(
  164. {"appName": data["appName"]},
  165. {"$set": data},
  166. upsert=True,
  167. )
  168. # ===================== 历史人物 =====================
  169. def get_all_figures() -> list:
  170. try:
  171. docs = historical_figures.find({})
  172. return [{"_id": str(doc["_id"]), **{k: v for k, v in doc.items() if k != "_id"}} for doc in docs]
  173. except Exception as e:
  174. print(f"MongoDB 历史人物列表查询失败: {e}")
  175. return []
  176. def get_figure_by_id(figure_id: str) -> dict | None:
  177. try:
  178. doc = historical_figures.find_one({"_id": ObjectId(figure_id)})
  179. if doc:
  180. doc["_id"] = str(doc["_id"])
  181. return doc
  182. except Exception as e:
  183. print(f"MongoDB 历史人物查询失败: {e}")
  184. return None
  185. def insert_figure(data: dict) -> str:
  186. try:
  187. result = historical_figures.insert_one(data)
  188. return str(result.inserted_id)
  189. except Exception as e:
  190. print(f"MongoDB 历史人物新增失败: {e}")
  191. return None
  192. def update_figure(figure_id: str, data: dict) -> int:
  193. try:
  194. result = historical_figures.update_one(
  195. {"_id": ObjectId(figure_id)},
  196. {"$set": data},
  197. )
  198. return result.matched_count
  199. except Exception as e:
  200. print(f"MongoDB 历史人物修改失败: {e}")
  201. return 0
  202. def delete_figure(figure_id: str) -> int:
  203. try:
  204. result = historical_figures.delete_one({"_id": ObjectId(figure_id)})
  205. return result.deleted_count
  206. except Exception as e:
  207. print(f"MongoDB 历史人物删除失败: {e}")
  208. return 0
  209. # ===================== 群聊AI回复历史 =====================
  210. # 群聊AI回复记录集合(只存AI回复,不存群消息)
  211. group_chat_history_col = db["group_chat_history"]
  212. def _ensure_group_chat_index():
  213. try:
  214. group_chat_history_col.create_index([("session_id", 1), ("timestamp", -1)])
  215. group_chat_history_col.create_index([("group_id", 1), ("ai_role", 1), ("timestamp", -1)])
  216. except Exception:
  217. pass
  218. def save_group_ai_reply(
  219. session_id: str,
  220. group_id: str,
  221. ai_role: str,
  222. content: str,
  223. response_id: str,
  224. trigger_message: str,
  225. trigger_user_id: str,
  226. timestamp: datetime,
  227. app_name: str = "souyue_im",
  228. ):
  229. """
  230. 业务功能:保存AI在群聊中的回复
  231. 注意:只存储AI回复,不存储群消息(群消息通过腾讯云API获取)
  232. Args:
  233. session_id: 会话ID(群ID + 小时窗口)
  234. group_id: 群ID
  235. ai_role: AI角色(entrepreneur | femininity)
  236. content: AI回复内容
  237. response_id: 大模型response ID
  238. trigger_message: 触发AI的原始消息
  239. trigger_user_id: 触发用户ID
  240. timestamp: 时间戳
  241. app_name: 应用名称
  242. """
  243. try:
  244. _ensure_group_chat_index()
  245. doc = {
  246. "session_id": session_id,
  247. "group_id": group_id,
  248. "role": "assistant",
  249. "user_id": f"ai_{ai_role}",
  250. "content": content,
  251. "ai_role": ai_role,
  252. "response_id": response_id,
  253. "trigger_message": trigger_message,
  254. "trigger_user_id": trigger_user_id,
  255. "timestamp": timestamp,
  256. "app_name": app_name,
  257. }
  258. group_chat_history_col.insert_one(doc)
  259. except Exception as e:
  260. print(f"MongoDB 群聊AI回复写入失败: {e}")
  261. def get_last_group_response_id(session_id: str, ai_role: str) -> str | None:
  262. """
  263. 获取指定AI的最后一次回复的response_id
  264. Args:
  265. session_id: 会话ID
  266. ai_role: AI角色(必须指定,防止串线)
  267. Returns:
  268. response_id 或 None
  269. """
  270. try:
  271. doc = group_chat_history_col.find_one(
  272. {
  273. "session_id": session_id,
  274. "ai_role": ai_role,
  275. "response_id": {"$ne": None}
  276. },
  277. {"response_id": 1, "_id": 0},
  278. sort=[("timestamp", -1)]
  279. )
  280. return doc["response_id"] if doc else None
  281. except Exception as e:
  282. print(f"MongoDB 查询群聊 response_id 失败: {e}")
  283. return None
  284. def get_group_ai_reply_count(session_id: str, ai_role: str = None) -> int:
  285. """
  286. 获取会话中的AI回复次数
  287. Args:
  288. session_id: 会话ID
  289. ai_role: AI角色(可选,不传则统计所有AI)
  290. Returns:
  291. 回复次数
  292. """
  293. try:
  294. query = {"session_id": session_id}
  295. if ai_role:
  296. query["ai_role"] = ai_role
  297. return group_chat_history_col.count_documents(query)
  298. except Exception as e:
  299. print(f"MongoDB 查询AI回复次数失败: {e}")
  300. return 0