thread_runner.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  1. from functools import partial
  2. import logging
  3. import json
  4. from typing import List
  5. from concurrent.futures import Executor
  6. from sqlalchemy.orm import Session
  7. from app.models.token_relation import RelationType
  8. from config.config import settings
  9. from config.llm import llm_settings, tool_settings
  10. from app.core.runner.llm_backend import LLMBackend
  11. from app.core.runner.llm_callback_handler import LLMCallbackHandler
  12. from app.core.runner.memory import Memory, find_memory
  13. from app.core.runner.pub_handler import StreamEventHandler
  14. from app.core.runner.utils import message_util as msg_util
  15. from app.core.runner.utils.tool_call_util import (
  16. tool_call_recognize,
  17. internal_tool_call_invoke,
  18. tool_call_request,
  19. tool_call_id,
  20. tool_call_output,
  21. )
  22. from app.core.tools import find_tools, BaseTool
  23. from app.libs.thread_executor import get_executor_for_config, run_with_executor
  24. from app.models.message import Message, MessageUpdate
  25. from app.models.run import Run
  26. from app.models.run_step import RunStep
  27. from app.models.token_relation import RelationType
  28. from app.services.assistant.assistant import AssistantService
  29. from app.services.file.file import FileService
  30. from app.services.message.message import MessageService
  31. from app.services.run.run import RunService
  32. from app.services.run.run_step import RunStepService
  33. from app.services.token.token import TokenService
  34. from app.services.token.token_relation import TokenRelationService
  35. class ThreadRunner:
  36. """
  37. ThreadRunner 封装 run 的执行逻辑
  38. """
  39. tool_executor: Executor = get_executor_for_config(
  40. tool_settings.TOOL_WORKER_NUM, "tool_worker_"
  41. )
  42. def __init__(
  43. self, run_id: str, token_id: str, session: Session, stream: bool = False
  44. ):
  45. self.run_id = run_id
  46. self.token_id = token_id
  47. self.session = session
  48. self.stream = stream
  49. self.max_step = llm_settings.LLM_MAX_STEP
  50. self.event_handler: StreamEventHandler = None
  51. def run(self):
  52. """
  53. 完成一次 run 的执行,基本步骤
  54. 1. 初始化,获取 run 以及相关 tools, 构造 system instructions;
  55. 2. 开始循环,查询已有 run step, 进行 chat message 生成;
  56. 3. 调用 llm 并解析返回结果;
  57. 4. 根据返回结果,生成新的 run step(tool calls 处理) 或者 message
  58. """
  59. # TODO: 重构,将 run 的状态变更逻辑放到 RunService 中
  60. run = RunService.get_run_sync(session=self.session, run_id=self.run_id)
  61. self.event_handler = StreamEventHandler(
  62. run_id=self.run_id, is_stream=self.stream
  63. )
  64. run = RunService.to_in_progress(session=self.session, run_id=self.run_id)
  65. self.event_handler.pub_run_in_progress(run)
  66. logging.info("processing ThreadRunner task, run_id: %s", self.run_id)
  67. # get memory from assistant metadata
  68. # format likes {"memory": {"type": "window", "window_size": 20, "max_token_size": 4000}}
  69. ast = AssistantService.get_assistant_sync(
  70. session=self.session, assistant_id=run.assistant_id
  71. )
  72. metadata = ast.metadata_ or {}
  73. memory = find_memory(metadata.get("memory", {}))
  74. instructions = (
  75. [run.instructions or ""] if run.instructions else [ast.instructions or ""]
  76. )
  77. asst_ids = []
  78. ids = []
  79. if ast.tool_resources and "file_search" in ast.tool_resources:
  80. ids = (
  81. ast.tool_resources.get("file_search")
  82. .get("vector_stores")[0]
  83. .get("folder_ids")
  84. )
  85. if ids:
  86. asst_ids += ids
  87. ids = (
  88. ast.tool_resources.get("file_search")
  89. .get("vector_stores")[0]
  90. .get("file_ids")
  91. )
  92. if ids:
  93. asst_ids += ids
  94. if len(asst_ids) > 0:
  95. if len(run.file_ids) > 0:
  96. run.tools.append({"type": "knowledge_search"})
  97. else:
  98. for tool in run.tools:
  99. if tool.get("type") == "file_search":
  100. tool["type"] = "knowledge_search"
  101. tools = find_tools(run, self.session)
  102. for tool in tools:
  103. tool.configure(session=self.session, run=run)
  104. instruction_supplement = tool.instruction_supplement()
  105. if instruction_supplement:
  106. instructions += [instruction_supplement or ""]
  107. instruction = "\n".join(instructions)
  108. llm = self.__init_llm_backend(run.assistant_id)
  109. loop = True
  110. while loop:
  111. #print("looplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooplooploop" )
  112. run_steps = RunStepService.get_run_step_list(
  113. session=self.session, run_id=self.run_id, thread_id=run.thread_id
  114. )
  115. loop = self.__run_step(llm, run, run_steps, instruction, tools, memory)
  116. # 任务结束
  117. self.event_handler.pub_run_completed(run)
  118. self.event_handler.pub_done()
  119. def __run_step(
  120. self,
  121. llm: LLMBackend,
  122. run: Run,
  123. run_steps: List[RunStep],
  124. instruction: str,
  125. tools: List[BaseTool],
  126. memory: Memory,
  127. ):
  128. """
  129. 执行 run step
  130. """
  131. logging.info("step %s is running", run_steps)
  132. if instruction == "":
  133. instruction = (
  134. "You are a multilingual AI assistant.\n"
  135. + "- Detect user language; reply in same language unless told otherwise.\n"
  136. + "- Default to English if detection is unclear.\n"
  137. + "- Give concise, accurate, and safe answers; admit when unsure.\n"
  138. + "- Keep tone and style consistent; adapt examples to user's context.\n"
  139. + "- For code, include explanations and comments in user's language.\n"
  140. + "- If a question is ambiguous, ask for clarification.\n"
  141. )
  142. assistant_system_message = [msg_util.system_message(instruction)]
  143. # 获取已有 message 上下文记录
  144. chat_messages = self.__generate_chat_messages(
  145. MessageService.get_message_list(
  146. session=self.session, thread_id=run.thread_id
  147. ),
  148. run,
  149. )
  150. tool_call_messages = []
  151. for step in run_steps:
  152. logging.info("stepstep %s is running", step)
  153. if step.type == "tool_calls" and step.status == "completed":
  154. tool_call_messages += (
  155. self.__convert_assistant_tool_calls_to_chat_messages(step)
  156. )
  157. # tool_call_messages = tool_call_messages
  158. # memory
  159. messages = (
  160. assistant_system_message
  161. + memory.integrate_context(chat_messages)
  162. + tool_call_messages
  163. )
  164. logging.info("messages: run %s", run)
  165. logging.info(messages)
  166. logging.info(tools)
  167. # 判断本次是否允许调用工具
  168. use_tools = len(run_steps) < self.max_step
  169. tool_choice = "auto" if use_tools else "none"
  170. # 当不允许调用工具时,不传递 tools 参数,避免与 tool_choice='none' 冲突
  171. tools_param = [tool.openai_function for tool in tools] if use_tools else None
  172. # 仅在流式且需要 usage 统计时才传递 stream_options
  173. stream_options = run.stream_options if self.stream else None
  174. response_stream = llm.run(
  175. messages=messages,
  176. model=run.model,
  177. tools=tools_param, # 不使用工具时为 None
  178. tool_choice=tool_choice,
  179. stream=self.stream,
  180. stream_options=stream_options,
  181. extra_body=run.extra_body,
  182. temperature=run.temperature or 1.0,
  183. top_p=run.top_p,
  184. response_format=run.response_format,
  185. parallel_tool_calls=run.parallel_tool_calls if use_tools else None, # 同样可条件传递
  186. audio=run.audio,
  187. modalities=run.modalities,
  188. )
  189. '''
  190. response_stream = llm.run(
  191. messages=messages,
  192. model=run.model,
  193. tools=[tool.openai_function for tool in tools],
  194. tool_choice="auto" if len(run_steps) < self.max_step else "none",
  195. stream=self.stream,
  196. stream_options=run.stream_options,
  197. extra_body=run.extra_body,
  198. temperature=run.temperature or 1.0,
  199. top_p=run.top_p,
  200. response_format=run.response_format,
  201. parallel_tool_calls=run.parallel_tool_calls,
  202. audio=run.audio,
  203. modalities=run.modalities,
  204. )
  205. '''
  206. # create message callback
  207. create_message_callback = partial(
  208. MessageService.new_message,
  209. session=self.session,
  210. assistant_id=run.assistant_id,
  211. thread_id=run.thread_id,
  212. run_id=run.id,
  213. role="assistant",
  214. )
  215. # create 'message creation' run step callback
  216. def _create_message_creation_run_step(message_id):
  217. return RunStepService.new_run_step(
  218. session=self.session,
  219. type="message_creation",
  220. assistant_id=run.assistant_id,
  221. thread_id=run.thread_id,
  222. run_id=run.id,
  223. step_details={
  224. "type": "message_creation",
  225. "message_creation": {"message_id": message_id},
  226. },
  227. )
  228. llm_callback_handler = LLMCallbackHandler(
  229. run_id=run.id,
  230. on_step_create_func=_create_message_creation_run_step,
  231. on_message_create_func=create_message_callback,
  232. event_handler=self.event_handler,
  233. )
  234. if self.stream == False and hasattr(response_stream, "choices"):
  235. response_stream = [response_stream]
  236. response_msg = llm_callback_handler.handle_llm_response(response_stream)
  237. #print("response_msg================================================================response_msg")
  238. #print(llm_callback_handler)
  239. message_creation_run_step = llm_callback_handler.step
  240. #print("444444444444444444444444455555555577777777777777777777777")
  241. logging.info("chat_response_message: %s", response_msg)
  242. if msg_util.is_tool_call(response_msg):
  243. # tool & tool_call definition dict
  244. tool_calls = [
  245. tool_call_recognize(tool_call, tools)
  246. for tool_call in response_msg.tool_calls
  247. ]
  248. # new run step for tool calls
  249. new_run_step = RunStepService.new_run_step(
  250. session=self.session,
  251. type="tool_calls",
  252. assistant_id=run.assistant_id,
  253. thread_id=run.thread_id,
  254. run_id=run.id,
  255. step_details={
  256. "reasoning_content": response_msg.reasoning_content,
  257. "type": "tool_calls",
  258. "tool_calls": [tool_call_dict for _, tool_call_dict in tool_calls],
  259. },
  260. )
  261. self.event_handler.pub_run_step_created(new_run_step)
  262. self.event_handler.pub_run_step_in_progress(new_run_step)
  263. internal_tool_calls = list(
  264. filter(lambda _tool_calls: _tool_calls[0] is not None, tool_calls)
  265. )
  266. external_tool_call_dict = [
  267. tool_call_dict for tool, tool_call_dict in tool_calls if tool is None
  268. ]
  269. # 为减少线程同步逻辑,依次处理内/外 tool_call 调用
  270. if internal_tool_calls:
  271. try:
  272. #print("==========================internal_tool_callsinternal_tool_callsinternal_tool_calls")
  273. #print(internal_tool_calls)
  274. ## 线程执行有问题 可以改成异步, 这里如果是filesearch要确定只执行一次
  275. tool_calls_with_outputs = run_with_executor(
  276. executor=ThreadRunner.tool_executor,
  277. func=internal_tool_call_invoke,
  278. tasks=internal_tool_calls,
  279. timeout=tool_settings.TOOL_WORKER_EXECUTION_TIMEOUT,
  280. )
  281. new_run_step = RunStepService.update_step_details(
  282. session=self.session,
  283. run_step_id=new_run_step.id,
  284. step_details={
  285. "type": "tool_calls",
  286. #"reasoning_content": response_msg.reasoning_content,
  287. "tool_calls": tool_calls_with_outputs,
  288. },
  289. completed=not external_tool_call_dict,
  290. )
  291. #print("llm_callback_handler.message================================================================llm_callback_handler.message")
  292. #print(llm_callback_handler.message)
  293. self.event_handler.pub_message_delta_tool(
  294. message_id=llm_callback_handler.message.id,
  295. index=0,
  296. content=json.dumps(tool_calls_with_outputs)
  297. )
  298. except Exception as e:
  299. RunStepService.to_failed(
  300. session=self.session, run_step_id=new_run_step.id, last_error=e
  301. )
  302. raise e
  303. #print("aaaaaaaaaaaaaaa===============================================================8888888888888888888888888")
  304. #print(external_tool_call_dict)
  305. if external_tool_call_dict:
  306. # run 设置为 action required,等待业务完成更新并再次拉起
  307. run = RunService.to_requires_action(
  308. session=self.session,
  309. run_id=run.id,
  310. required_action={
  311. "type": "submit_tool_outputs",
  312. "submit_tool_outputs": {"tool_calls": external_tool_call_dict},
  313. },
  314. )
  315. self.event_handler.pub_run_step_delta(
  316. step_id=new_run_step.id,
  317. step_details={
  318. "type": "tool_calls",
  319. "tool_calls": external_tool_call_dict,
  320. },
  321. )
  322. #print(run)
  323. self.event_handler.pub_run_requires_action(run)
  324. else:
  325. self.event_handler.pub_run_step_completed(new_run_step)
  326. return True
  327. else:
  328. if response_msg.content == "":
  329. response_msg.content = (
  330. '[{"text": {"value": "", "annotations": []}, "type": "text"}]'
  331. )
  332. if response_msg.reasoning_content == "":
  333. response_msg.reasoning_content = None
  334. # 无 tool call 信息,message 生成结束,更新状态
  335. new_message = MessageService.modify_message_sync(
  336. session=self.session,
  337. thread_id=run.thread_id,
  338. message_id=llm_callback_handler.message.id,
  339. body=MessageUpdate(content=response_msg.content, reasoning_content=response_msg.reasoning_content),
  340. )
  341. self.event_handler.pub_message_completed(new_message)
  342. new_step = RunStepService.update_step_details(
  343. session=self.session,
  344. run_step_id=message_creation_run_step.id,
  345. step_details={
  346. "type": "message_creation",
  347. "message_creation": {"message_id": new_message.id},
  348. },
  349. completed=True,
  350. )
  351. RunService.to_completed(session=self.session, run_id=run.id)
  352. self.event_handler.pub_run_step_completed(new_step)
  353. return False
  354. def __init_llm_backend(self, assistant_id):
  355. #print("settings.AUTH_ENABLE", settings.AUTH_ENABLE)
  356. if settings.AUTH_ENABLE:
  357. # init llm backend with token id
  358. if self.token_id:
  359. token_id = self.token_id
  360. else:
  361. token_id = TokenRelationService.get_token_id_by_relation(
  362. session=self.session,
  363. relation_type=RelationType.Assistant,
  364. relation_id=assistant_id,
  365. )
  366. #print("token_idtoken_idtoken_idtoken_idtoken_idtoken_idtoken_idtoken_idtoken_idtoken_idtoken_idtoken_id")
  367. #print(self.token_id)
  368. #print(token_id)
  369. try:
  370. if token_id is not None and len(token_id) > 0:
  371. token = TokenService.get_token_by_id(self.session, token_id)
  372. #print(token)
  373. return LLMBackend(
  374. base_url=token.llm_base_url, api_key=token.llm_api_key
  375. )
  376. except Exception as e:
  377. print(e)
  378. token = {
  379. "llm_base_url": "http://172.16.12.13:3000/v1",
  380. "llm_api_key": "sk-vTqeBKDC2j6osbGt89A2202dAd1c4fE8B1D294388b569e54",
  381. }
  382. return LLMBackend(
  383. base_url=token.get("llm_base_url"), api_key=token.get("llm_api_key")
  384. )
  385. else:
  386. # init llm backend with llm settings
  387. return LLMBackend(
  388. base_url=llm_settings.OPENAI_API_BASE,
  389. api_key=llm_settings.OPENAI_API_KEY,
  390. )
  391. def __generate_chat_messages(self, messages: List[Message], run: Run):
  392. """
  393. 根据历史信息生成 chat message
  394. """
  395. chat_messages = []
  396. is_audio_num = 0
  397. for message in messages:
  398. role = message.role
  399. if role == "user":
  400. message_content = []
  401. """
  402. if message.file_ids:
  403. files = FileService.get_file_list_by_ids(
  404. session=self.session, file_ids=message.file_ids
  405. )
  406. for file in files:
  407. chat_messages.append(
  408. msg_util.new_message(
  409. role,
  410. f'The file "{file.filename}" can be used as a reference',
  411. )
  412. )
  413. else:
  414. """
  415. for content in message.content:
  416. if content["type"] == "text":
  417. message_content.append(
  418. {"type": "text", "text": content["text"]["value"]}
  419. )
  420. elif content["type"] == "image_url" and run.audio is None:
  421. message_content.append(content)
  422. elif (
  423. content.get("type") == "input_audio"
  424. and run.audio is not None
  425. and is_audio_num < 2
  426. ):
  427. message_content.append(content)
  428. is_audio_num += 1
  429. chat_messages.append(msg_util.new_message(role, message_content))
  430. elif role == "assistant":
  431. assistant_content = ""
  432. for content in message.content:
  433. if content["type"] == "text":
  434. assistant_content += content["text"]["value"]
  435. if assistant_content:
  436. chat_messages.append(msg_util.new_message(role, assistant_content, message.reasoning_content))
  437. #chat_messages.append(msg_util.new_message(role, assistant_content, message.reasoning_content))
  438. chat_messages.reverse() # 倒序排列,最新的消息在前面
  439. return chat_messages # 暂时只支持5条消息,后续正价token上限
  440. def __convert_assistant_tool_calls_to_chat_messages(self, run_step: RunStep):
  441. """
  442. 根据 run step 执行结果生成 message 信息
  443. 每个 tool call run step 包含两部分,调用与结果(结果可能为多个信息)
  444. """
  445. tool_calls = run_step.step_details["tool_calls"]
  446. reasoning_content = run_step.step_details.get("reasoning_content")
  447. tool_call_requests = [
  448. msg_util.tool_calls(
  449. [tool_call_request(tool_call) for tool_call in tool_calls],
  450. reasoning_content
  451. )
  452. ]
  453. logging.info("tool_calls: %s", tool_calls)
  454. tool_call_outputs = [
  455. msg_util.tool_call_result(
  456. tool_call_id(tool_call), tool_call_output(tool_call)
  457. )
  458. for tool_call in tool_calls
  459. ]
  460. logging.info("tool_call_outputs: %s", tool_call_outputs)
  461. return tool_call_requests + tool_call_outputs