conversation_message_task.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. import decimal
  2. import json
  3. from typing import Optional, Union
  4. from core.callback_handler.entity.agent_loop import AgentLoop
  5. from core.callback_handler.entity.dataset_query import DatasetQueryObj
  6. from core.callback_handler.entity.llm_message import LLMMessage
  7. from core.callback_handler.entity.chain_result import ChainResult
  8. from core.constant import llm_constant
  9. from core.llm.llm_builder import LLMBuilder
  10. from core.llm.provider.llm_provider_service import LLMProviderService
  11. from core.prompt.prompt_builder import PromptBuilder
  12. from core.prompt.prompt_template import OutLinePromptTemplate
  13. from events.message_event import message_was_created
  14. from extensions.ext_database import db
  15. from extensions.ext_redis import redis_client
  16. from models.dataset import DatasetQuery
  17. from models.model import AppModelConfig, Conversation, Account, Message, EndUser, App, MessageAgentThought, MessageChain
  18. from models.provider import ProviderType, Provider
  19. class ConversationMessageTask:
  20. def __init__(self, task_id: str, app: App, app_model_config: AppModelConfig, user: Account,
  21. inputs: dict, query: str, streaming: bool,
  22. conversation: Optional[Conversation] = None, is_override: bool = False):
  23. self.task_id = task_id
  24. self.app = app
  25. self.tenant_id = app.tenant_id
  26. self.app_model_config = app_model_config
  27. self.is_override = is_override
  28. self.user = user
  29. self.inputs = inputs
  30. self.query = query
  31. self.streaming = streaming
  32. self.conversation = conversation
  33. self.is_new_conversation = False
  34. self.message = None
  35. self.model_dict = self.app_model_config.model_dict
  36. self.model_name = self.model_dict.get('name')
  37. self.mode = app.mode
  38. self.init()
  39. self._pub_handler = PubHandler(
  40. user=self.user,
  41. task_id=self.task_id,
  42. message=self.message,
  43. conversation=self.conversation,
  44. chain_pub=False, # disabled currently
  45. agent_thought_pub=False # disabled currently
  46. )
  47. def init(self):
  48. provider_name = LLMBuilder.get_default_provider(self.app.tenant_id)
  49. self.model_dict['provider'] = provider_name
  50. override_model_configs = None
  51. if self.is_override:
  52. override_model_configs = {
  53. "model": self.app_model_config.model_dict,
  54. "pre_prompt": self.app_model_config.pre_prompt,
  55. "agent_mode": self.app_model_config.agent_mode_dict,
  56. "opening_statement": self.app_model_config.opening_statement,
  57. "suggested_questions": self.app_model_config.suggested_questions_list,
  58. "suggested_questions_after_answer": self.app_model_config.suggested_questions_after_answer_dict,
  59. "more_like_this": self.app_model_config.more_like_this_dict,
  60. "user_input_form": self.app_model_config.user_input_form_list,
  61. }
  62. introduction = ''
  63. system_instruction = ''
  64. system_instruction_tokens = 0
  65. if self.mode == 'chat':
  66. introduction = self.app_model_config.opening_statement
  67. if introduction:
  68. prompt_template = OutLinePromptTemplate.from_template(template=PromptBuilder.process_template(introduction))
  69. prompt_inputs = {k: self.inputs[k] for k in prompt_template.input_variables if k in self.inputs}
  70. try:
  71. introduction = prompt_template.format(**prompt_inputs)
  72. except KeyError:
  73. pass
  74. if self.app_model_config.pre_prompt:
  75. pre_prompt = PromptBuilder.process_template(self.app_model_config.pre_prompt)
  76. system_message = PromptBuilder.to_system_message(pre_prompt, self.inputs)
  77. system_instruction = system_message.content
  78. llm = LLMBuilder.to_llm(self.tenant_id, self.model_name)
  79. system_instruction_tokens = llm.get_messages_tokens([system_message])
  80. if not self.conversation:
  81. self.is_new_conversation = True
  82. self.conversation = Conversation(
  83. app_id=self.app_model_config.app_id,
  84. app_model_config_id=self.app_model_config.id,
  85. model_provider=self.model_dict.get('provider'),
  86. model_id=self.model_name,
  87. override_model_configs=json.dumps(override_model_configs) if override_model_configs else None,
  88. mode=self.mode,
  89. name='',
  90. inputs=self.inputs,
  91. introduction=introduction,
  92. system_instruction=system_instruction,
  93. system_instruction_tokens=system_instruction_tokens,
  94. status='normal',
  95. from_source=('console' if isinstance(self.user, Account) else 'api'),
  96. from_end_user_id=(self.user.id if isinstance(self.user, EndUser) else None),
  97. from_account_id=(self.user.id if isinstance(self.user, Account) else None),
  98. )
  99. db.session.add(self.conversation)
  100. db.session.flush()
  101. self.message = Message(
  102. app_id=self.app_model_config.app_id,
  103. model_provider=self.model_dict.get('provider'),
  104. model_id=self.model_name,
  105. override_model_configs=json.dumps(override_model_configs) if override_model_configs else None,
  106. conversation_id=self.conversation.id,
  107. inputs=self.inputs,
  108. query=self.query,
  109. message="",
  110. message_tokens=0,
  111. message_unit_price=0,
  112. answer="",
  113. answer_tokens=0,
  114. answer_unit_price=0,
  115. provider_response_latency=0,
  116. total_price=0,
  117. currency=llm_constant.model_currency,
  118. from_source=('console' if isinstance(self.user, Account) else 'api'),
  119. from_end_user_id=(self.user.id if isinstance(self.user, EndUser) else None),
  120. from_account_id=(self.user.id if isinstance(self.user, Account) else None),
  121. agent_based=self.app_model_config.agent_mode_dict.get('enabled'),
  122. )
  123. db.session.add(self.message)
  124. db.session.flush()
  125. def append_message_text(self, text: str):
  126. self._pub_handler.pub_text(text)
  127. def save_message(self, llm_message: LLMMessage, by_stopped: bool = False):
  128. model_name = self.app_model_config.model_dict.get('name')
  129. message_tokens = llm_message.prompt_tokens
  130. answer_tokens = llm_message.completion_tokens
  131. message_unit_price = llm_constant.model_prices[model_name]['prompt']
  132. answer_unit_price = llm_constant.model_prices[model_name]['completion']
  133. total_price = self.calc_total_price(message_tokens, message_unit_price, answer_tokens, answer_unit_price)
  134. self.message.message = llm_message.prompt
  135. self.message.message_tokens = message_tokens
  136. self.message.message_unit_price = message_unit_price
  137. self.message.answer = llm_message.completion.strip() if llm_message.completion else ''
  138. self.message.answer_tokens = answer_tokens
  139. self.message.answer_unit_price = answer_unit_price
  140. self.message.provider_response_latency = llm_message.latency
  141. self.message.total_price = total_price
  142. self.update_provider_quota()
  143. db.session.commit()
  144. message_was_created.send(
  145. self.message,
  146. conversation=self.conversation,
  147. is_first_message=self.is_new_conversation
  148. )
  149. if not by_stopped:
  150. self.end()
  151. def update_provider_quota(self):
  152. llm_provider_service = LLMProviderService(
  153. tenant_id=self.app.tenant_id,
  154. provider_name=self.message.model_provider,
  155. )
  156. provider = llm_provider_service.get_provider_db_record()
  157. if provider and provider.provider_type == ProviderType.SYSTEM.value:
  158. db.session.query(Provider).filter(
  159. Provider.tenant_id == self.app.tenant_id,
  160. Provider.quota_limit > Provider.quota_used
  161. ).update({'quota_used': Provider.quota_used + 1})
  162. def init_chain(self, chain_result: ChainResult):
  163. message_chain = MessageChain(
  164. message_id=self.message.id,
  165. type=chain_result.type,
  166. input=json.dumps(chain_result.prompt),
  167. output=''
  168. )
  169. db.session.add(message_chain)
  170. db.session.flush()
  171. return message_chain
  172. def on_chain_end(self, message_chain: MessageChain, chain_result: ChainResult):
  173. message_chain.output = json.dumps(chain_result.completion)
  174. self._pub_handler.pub_chain(message_chain)
  175. def on_agent_end(self, message_chain: MessageChain, agent_model_name: str,
  176. agent_loop: AgentLoop):
  177. agent_message_unit_price = llm_constant.model_prices[agent_model_name]['prompt']
  178. agent_answer_unit_price = llm_constant.model_prices[agent_model_name]['completion']
  179. loop_message_tokens = agent_loop.prompt_tokens
  180. loop_answer_tokens = agent_loop.completion_tokens
  181. loop_total_price = self.calc_total_price(
  182. loop_message_tokens,
  183. agent_message_unit_price,
  184. loop_answer_tokens,
  185. agent_answer_unit_price
  186. )
  187. message_agent_loop = MessageAgentThought(
  188. message_id=self.message.id,
  189. message_chain_id=message_chain.id,
  190. position=agent_loop.position,
  191. thought=agent_loop.thought,
  192. tool=agent_loop.tool_name,
  193. tool_input=agent_loop.tool_input,
  194. observation=agent_loop.tool_output,
  195. tool_process_data='', # currently not support
  196. message=agent_loop.prompt,
  197. message_token=loop_message_tokens,
  198. message_unit_price=agent_message_unit_price,
  199. answer=agent_loop.completion,
  200. answer_token=loop_answer_tokens,
  201. answer_unit_price=agent_answer_unit_price,
  202. latency=agent_loop.latency,
  203. tokens=agent_loop.prompt_tokens + agent_loop.completion_tokens,
  204. total_price=loop_total_price,
  205. currency=llm_constant.model_currency,
  206. created_by_role=('account' if isinstance(self.user, Account) else 'end_user'),
  207. created_by=self.user.id
  208. )
  209. db.session.add(message_agent_loop)
  210. db.session.flush()
  211. self._pub_handler.pub_agent_thought(message_agent_loop)
  212. def on_dataset_query_end(self, dataset_query_obj: DatasetQueryObj):
  213. dataset_query = DatasetQuery(
  214. dataset_id=dataset_query_obj.dataset_id,
  215. content=dataset_query_obj.query,
  216. source='app',
  217. source_app_id=self.app.id,
  218. created_by_role=('account' if isinstance(self.user, Account) else 'end_user'),
  219. created_by=self.user.id
  220. )
  221. db.session.add(dataset_query)
  222. def calc_total_price(self, message_tokens, message_unit_price, answer_tokens, answer_unit_price):
  223. message_tokens_per_1k = (decimal.Decimal(message_tokens) / 1000).quantize(decimal.Decimal('0.001'),
  224. rounding=decimal.ROUND_HALF_UP)
  225. answer_tokens_per_1k = (decimal.Decimal(answer_tokens) / 1000).quantize(decimal.Decimal('0.001'),
  226. rounding=decimal.ROUND_HALF_UP)
  227. total_price = message_tokens_per_1k * message_unit_price + answer_tokens_per_1k * answer_unit_price
  228. return total_price.quantize(decimal.Decimal('0.0000001'), rounding=decimal.ROUND_HALF_UP)
  229. def end(self):
  230. self._pub_handler.pub_end()
  231. class PubHandler:
  232. def __init__(self, user: Union[Account | EndUser], task_id: str,
  233. message: Message, conversation: Conversation,
  234. chain_pub: bool = False, agent_thought_pub: bool = False):
  235. self._channel = PubHandler.generate_channel_name(user, task_id)
  236. self._stopped_cache_key = PubHandler.generate_stopped_cache_key(user, task_id)
  237. self._task_id = task_id
  238. self._message = message
  239. self._conversation = conversation
  240. self._chain_pub = chain_pub
  241. self._agent_thought_pub = agent_thought_pub
  242. @classmethod
  243. def generate_channel_name(cls, user: Union[Account | EndUser], task_id: str):
  244. if not user:
  245. raise ValueError("user is required")
  246. user_str = 'account-' + user.id if isinstance(user, Account) else 'end-user-' + user.id
  247. return "generate_result:{}-{}".format(user_str, task_id)
  248. @classmethod
  249. def generate_stopped_cache_key(cls, user: Union[Account | EndUser], task_id: str):
  250. user_str = 'account-' + user.id if isinstance(user, Account) else 'end-user-' + user.id
  251. return "generate_result_stopped:{}-{}".format(user_str, task_id)
  252. def pub_text(self, text: str):
  253. content = {
  254. 'event': 'message',
  255. 'data': {
  256. 'task_id': self._task_id,
  257. 'message_id': self._message.id,
  258. 'text': text,
  259. 'mode': self._conversation.mode,
  260. 'conversation_id': self._conversation.id
  261. }
  262. }
  263. redis_client.publish(self._channel, json.dumps(content))
  264. if self._is_stopped():
  265. self.pub_end()
  266. raise ConversationTaskStoppedException()
  267. def pub_chain(self, message_chain: MessageChain):
  268. if self._chain_pub:
  269. content = {
  270. 'event': 'chain',
  271. 'data': {
  272. 'task_id': self._task_id,
  273. 'message_id': self._message.id,
  274. 'chain_id': message_chain.id,
  275. 'type': message_chain.type,
  276. 'input': json.loads(message_chain.input),
  277. 'output': json.loads(message_chain.output),
  278. 'mode': self._conversation.mode,
  279. 'conversation_id': self._conversation.id
  280. }
  281. }
  282. redis_client.publish(self._channel, json.dumps(content))
  283. if self._is_stopped():
  284. self.pub_end()
  285. raise ConversationTaskStoppedException()
  286. def pub_agent_thought(self, message_agent_thought: MessageAgentThought):
  287. if self._agent_thought_pub:
  288. content = {
  289. 'event': 'agent_thought',
  290. 'data': {
  291. 'task_id': self._task_id,
  292. 'message_id': self._message.id,
  293. 'chain_id': message_agent_thought.message_chain_id,
  294. 'agent_thought_id': message_agent_thought.id,
  295. 'position': message_agent_thought.position,
  296. 'thought': message_agent_thought.thought,
  297. 'tool': message_agent_thought.tool,
  298. 'tool_input': message_agent_thought.tool_input,
  299. 'observation': message_agent_thought.observation,
  300. 'answer': message_agent_thought.answer,
  301. 'mode': self._conversation.mode,
  302. 'conversation_id': self._conversation.id
  303. }
  304. }
  305. redis_client.publish(self._channel, json.dumps(content))
  306. if self._is_stopped():
  307. self.pub_end()
  308. raise ConversationTaskStoppedException()
  309. def pub_end(self):
  310. content = {
  311. 'event': 'end',
  312. }
  313. redis_client.publish(self._channel, json.dumps(content))
  314. @classmethod
  315. def pub_error(cls, user: Union[Account | EndUser], task_id: str, e):
  316. content = {
  317. 'error': type(e).__name__,
  318. 'description': e.description if getattr(e, 'description', None) is not None else str(e)
  319. }
  320. channel = cls.generate_channel_name(user, task_id)
  321. redis_client.publish(channel, json.dumps(content))
  322. def _is_stopped(self):
  323. return redis_client.get(self._stopped_cache_key) is not None
  324. @classmethod
  325. def stop(cls, user: Union[Account | EndUser], task_id: str):
  326. stopped_cache_key = cls.generate_stopped_cache_key(user, task_id)
  327. redis_client.setex(stopped_cache_key, 600, 1)
  328. class ConversationTaskStoppedException(Exception):
  329. pass