workflow_converter.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689
  1. import json
  2. from typing import Optional
  3. from core.app.app_config.entities import (
  4. DatasetEntity,
  5. DatasetRetrieveConfigEntity,
  6. EasyUIBasedAppConfig,
  7. ExternalDataVariableEntity,
  8. ModelConfigEntity,
  9. PromptTemplateEntity,
  10. VariableEntity,
  11. )
  12. from core.app.apps.agent_chat.app_config_manager import AgentChatAppConfigManager
  13. from core.app.apps.chat.app_config_manager import ChatAppConfigManager
  14. from core.app.apps.completion.app_config_manager import CompletionAppConfigManager
  15. from core.file.file_obj import FileExtraConfig
  16. from core.helper import encrypter
  17. from core.model_runtime.entities.llm_entities import LLMMode
  18. from core.model_runtime.utils.encoders import jsonable_encoder
  19. from core.prompt.simple_prompt_transform import SimplePromptTransform
  20. from core.workflow.entities.node_entities import NodeType
  21. from events.app_event import app_was_created
  22. from extensions.ext_database import db
  23. from models.account import Account
  24. from models.api_based_extension import APIBasedExtension, APIBasedExtensionPoint
  25. from models.model import App, AppMode, AppModelConfig
  26. from models.workflow import Workflow, WorkflowType
  27. class WorkflowConverter:
  28. """
  29. App Convert to Workflow Mode
  30. """
  31. def convert_to_workflow(self, app_model: App,
  32. account: Account,
  33. name: str,
  34. icon: str,
  35. icon_background: str) -> App:
  36. """
  37. Convert app to workflow
  38. - basic mode of chatbot app
  39. - expert mode of chatbot app
  40. - completion app
  41. :param app_model: App instance
  42. :param account: Account
  43. :param name: new app name
  44. :param icon: new app icon
  45. :param icon_background: new app icon background
  46. :return: new App instance
  47. """
  48. # convert app model config
  49. workflow = self.convert_app_model_config_to_workflow(
  50. app_model=app_model,
  51. app_model_config=app_model.app_model_config,
  52. account_id=account.id
  53. )
  54. # create new app
  55. new_app = App()
  56. new_app.tenant_id = app_model.tenant_id
  57. new_app.name = name if name else app_model.name + '(workflow)'
  58. new_app.mode = AppMode.ADVANCED_CHAT.value \
  59. if app_model.mode == AppMode.CHAT.value else AppMode.WORKFLOW.value
  60. new_app.icon = icon if icon else app_model.icon
  61. new_app.icon_background = icon_background if icon_background else app_model.icon_background
  62. new_app.enable_site = app_model.enable_site
  63. new_app.enable_api = app_model.enable_api
  64. new_app.api_rpm = app_model.api_rpm
  65. new_app.api_rph = app_model.api_rph
  66. new_app.is_demo = False
  67. new_app.is_public = app_model.is_public
  68. db.session.add(new_app)
  69. db.session.flush()
  70. db.session.commit()
  71. workflow.app_id = new_app.id
  72. db.session.commit()
  73. app_was_created.send(new_app, account=account)
  74. return new_app
  75. def convert_app_model_config_to_workflow(self, app_model: App,
  76. app_model_config: AppModelConfig,
  77. account_id: str) -> Workflow:
  78. """
  79. Convert app model config to workflow mode
  80. :param app_model: App instance
  81. :param app_model_config: AppModelConfig instance
  82. :param account_id: Account ID
  83. :return:
  84. """
  85. # get new app mode
  86. new_app_mode = self._get_new_app_mode(app_model)
  87. # convert app model config
  88. app_config = self._convert_to_app_config(
  89. app_model=app_model,
  90. app_model_config=app_model_config
  91. )
  92. # init workflow graph
  93. graph = {
  94. "nodes": [],
  95. "edges": []
  96. }
  97. # Convert list:
  98. # - variables -> start
  99. # - model_config -> llm
  100. # - prompt_template -> llm
  101. # - file_upload -> llm
  102. # - external_data_variables -> http-request
  103. # - dataset -> knowledge-retrieval
  104. # - show_retrieve_source -> knowledge-retrieval
  105. # convert to start node
  106. start_node = self._convert_to_start_node(
  107. variables=app_config.variables
  108. )
  109. graph['nodes'].append(start_node)
  110. # convert to http request node
  111. external_data_variable_node_mapping = {}
  112. if app_config.external_data_variables:
  113. http_request_nodes, external_data_variable_node_mapping = self._convert_to_http_request_node(
  114. app_model=app_model,
  115. variables=app_config.variables,
  116. external_data_variables=app_config.external_data_variables
  117. )
  118. for http_request_node in http_request_nodes:
  119. graph = self._append_node(graph, http_request_node)
  120. # convert to knowledge retrieval node
  121. if app_config.dataset:
  122. knowledge_retrieval_node = self._convert_to_knowledge_retrieval_node(
  123. new_app_mode=new_app_mode,
  124. dataset_config=app_config.dataset,
  125. model_config=app_config.model
  126. )
  127. if knowledge_retrieval_node:
  128. graph = self._append_node(graph, knowledge_retrieval_node)
  129. # convert to llm node
  130. llm_node = self._convert_to_llm_node(
  131. original_app_mode=AppMode.value_of(app_model.mode),
  132. new_app_mode=new_app_mode,
  133. graph=graph,
  134. model_config=app_config.model,
  135. prompt_template=app_config.prompt_template,
  136. file_upload=app_config.additional_features.file_upload,
  137. external_data_variable_node_mapping=external_data_variable_node_mapping
  138. )
  139. graph = self._append_node(graph, llm_node)
  140. if new_app_mode == AppMode.WORKFLOW:
  141. # convert to end node by app mode
  142. end_node = self._convert_to_end_node()
  143. graph = self._append_node(graph, end_node)
  144. else:
  145. answer_node = self._convert_to_answer_node()
  146. graph = self._append_node(graph, answer_node)
  147. app_model_config_dict = app_config.app_model_config_dict
  148. # features
  149. if new_app_mode == AppMode.ADVANCED_CHAT:
  150. features = {
  151. "opening_statement": app_model_config_dict.get("opening_statement"),
  152. "suggested_questions": app_model_config_dict.get("suggested_questions"),
  153. "suggested_questions_after_answer": app_model_config_dict.get("suggested_questions_after_answer"),
  154. "speech_to_text": app_model_config_dict.get("speech_to_text"),
  155. "text_to_speech": app_model_config_dict.get("text_to_speech"),
  156. "file_upload": app_model_config_dict.get("file_upload"),
  157. "sensitive_word_avoidance": app_model_config_dict.get("sensitive_word_avoidance"),
  158. "retriever_resource": app_model_config_dict.get("retriever_resource"),
  159. }
  160. else:
  161. features = {
  162. "text_to_speech": app_model_config_dict.get("text_to_speech"),
  163. "file_upload": app_model_config_dict.get("file_upload"),
  164. "sensitive_word_avoidance": app_model_config_dict.get("sensitive_word_avoidance"),
  165. }
  166. # create workflow record
  167. workflow = Workflow(
  168. tenant_id=app_model.tenant_id,
  169. app_id=app_model.id,
  170. type=WorkflowType.from_app_mode(new_app_mode).value,
  171. version='draft',
  172. graph=json.dumps(graph),
  173. features=json.dumps(features),
  174. created_by=account_id,
  175. environment_variables=[],
  176. conversation_variables=[],
  177. )
  178. db.session.add(workflow)
  179. db.session.commit()
  180. return workflow
  181. def _convert_to_app_config(self, app_model: App,
  182. app_model_config: AppModelConfig) -> EasyUIBasedAppConfig:
  183. app_mode = AppMode.value_of(app_model.mode)
  184. if app_mode == AppMode.AGENT_CHAT or app_model.is_agent:
  185. app_model.mode = AppMode.AGENT_CHAT.value
  186. app_config = AgentChatAppConfigManager.get_app_config(
  187. app_model=app_model,
  188. app_model_config=app_model_config
  189. )
  190. elif app_mode == AppMode.CHAT:
  191. app_config = ChatAppConfigManager.get_app_config(
  192. app_model=app_model,
  193. app_model_config=app_model_config
  194. )
  195. elif app_mode == AppMode.COMPLETION:
  196. app_config = CompletionAppConfigManager.get_app_config(
  197. app_model=app_model,
  198. app_model_config=app_model_config
  199. )
  200. else:
  201. raise ValueError("Invalid app mode")
  202. return app_config
  203. def _convert_to_start_node(self, variables: list[VariableEntity]) -> dict:
  204. """
  205. Convert to Start Node
  206. :param variables: list of variables
  207. :return:
  208. """
  209. return {
  210. "id": "start",
  211. "position": None,
  212. "data": {
  213. "title": "START",
  214. "type": NodeType.START.value,
  215. "variables": [jsonable_encoder(v) for v in variables]
  216. }
  217. }
  218. def _convert_to_http_request_node(self, app_model: App,
  219. variables: list[VariableEntity],
  220. external_data_variables: list[ExternalDataVariableEntity]) \
  221. -> tuple[list[dict], dict[str, str]]:
  222. """
  223. Convert API Based Extension to HTTP Request Node
  224. :param app_model: App instance
  225. :param variables: list of variables
  226. :param external_data_variables: list of external data variables
  227. :return:
  228. """
  229. index = 1
  230. nodes = []
  231. external_data_variable_node_mapping = {}
  232. tenant_id = app_model.tenant_id
  233. for external_data_variable in external_data_variables:
  234. tool_type = external_data_variable.type
  235. if tool_type != "api":
  236. continue
  237. tool_variable = external_data_variable.variable
  238. tool_config = external_data_variable.config
  239. # get params from config
  240. api_based_extension_id = tool_config.get("api_based_extension_id")
  241. # get api_based_extension
  242. api_based_extension = self._get_api_based_extension(
  243. tenant_id=tenant_id,
  244. api_based_extension_id=api_based_extension_id
  245. )
  246. if not api_based_extension:
  247. raise ValueError("[External data tool] API query failed, variable: {}, "
  248. "error: api_based_extension_id is invalid"
  249. .format(tool_variable))
  250. # decrypt api_key
  251. api_key = encrypter.decrypt_token(
  252. tenant_id=tenant_id,
  253. token=api_based_extension.api_key
  254. )
  255. inputs = {}
  256. for v in variables:
  257. inputs[v.variable] = '{{#start.' + v.variable + '#}}'
  258. request_body = {
  259. 'point': APIBasedExtensionPoint.APP_EXTERNAL_DATA_TOOL_QUERY.value,
  260. 'params': {
  261. 'app_id': app_model.id,
  262. 'tool_variable': tool_variable,
  263. 'inputs': inputs,
  264. 'query': '{{#sys.query#}}' if app_model.mode == AppMode.CHAT.value else ''
  265. }
  266. }
  267. request_body_json = json.dumps(request_body)
  268. request_body_json = request_body_json.replace(r'\{\{', '{{').replace(r'\}\}', '}}')
  269. http_request_node = {
  270. "id": f"http_request_{index}",
  271. "position": None,
  272. "data": {
  273. "title": f"HTTP REQUEST {api_based_extension.name}",
  274. "type": NodeType.HTTP_REQUEST.value,
  275. "method": "post",
  276. "url": api_based_extension.api_endpoint,
  277. "authorization": {
  278. "type": "api-key",
  279. "config": {
  280. "type": "bearer",
  281. "api_key": api_key
  282. }
  283. },
  284. "headers": "",
  285. "params": "",
  286. "body": {
  287. "type": "json",
  288. "data": request_body_json
  289. }
  290. }
  291. }
  292. nodes.append(http_request_node)
  293. # append code node for response body parsing
  294. code_node = {
  295. "id": f"code_{index}",
  296. "position": None,
  297. "data": {
  298. "title": f"Parse {api_based_extension.name} Response",
  299. "type": NodeType.CODE.value,
  300. "variables": [{
  301. "variable": "response_json",
  302. "value_selector": [http_request_node['id'], "body"]
  303. }],
  304. "code_language": "python3",
  305. "code": "import json\n\ndef main(response_json: str) -> str:\n response_body = json.loads("
  306. "response_json)\n return {\n \"result\": response_body[\"result\"]\n }",
  307. "outputs": {
  308. "result": {
  309. "type": "string"
  310. }
  311. }
  312. }
  313. }
  314. nodes.append(code_node)
  315. external_data_variable_node_mapping[external_data_variable.variable] = code_node['id']
  316. index += 1
  317. return nodes, external_data_variable_node_mapping
  318. def _convert_to_knowledge_retrieval_node(self, new_app_mode: AppMode,
  319. dataset_config: DatasetEntity,
  320. model_config: ModelConfigEntity) \
  321. -> Optional[dict]:
  322. """
  323. Convert datasets to Knowledge Retrieval Node
  324. :param new_app_mode: new app mode
  325. :param dataset_config: dataset
  326. :param model_config: model config
  327. :return:
  328. """
  329. retrieve_config = dataset_config.retrieve_config
  330. if new_app_mode == AppMode.ADVANCED_CHAT:
  331. query_variable_selector = ["sys", "query"]
  332. elif retrieve_config.query_variable:
  333. # fetch query variable
  334. query_variable_selector = ["start", retrieve_config.query_variable]
  335. else:
  336. return None
  337. return {
  338. "id": "knowledge_retrieval",
  339. "position": None,
  340. "data": {
  341. "title": "KNOWLEDGE RETRIEVAL",
  342. "type": NodeType.KNOWLEDGE_RETRIEVAL.value,
  343. "query_variable_selector": query_variable_selector,
  344. "dataset_ids": dataset_config.dataset_ids,
  345. "retrieval_mode": retrieve_config.retrieve_strategy.value,
  346. "single_retrieval_config": {
  347. "model": {
  348. "provider": model_config.provider,
  349. "name": model_config.model,
  350. "mode": model_config.mode,
  351. "completion_params": {
  352. **model_config.parameters,
  353. "stop": model_config.stop,
  354. }
  355. }
  356. }
  357. if retrieve_config.retrieve_strategy == DatasetRetrieveConfigEntity.RetrieveStrategy.SINGLE
  358. else None,
  359. "multiple_retrieval_config": {
  360. "top_k": retrieve_config.top_k,
  361. "score_threshold": retrieve_config.score_threshold,
  362. "reranking_model": retrieve_config.reranking_model
  363. }
  364. if retrieve_config.retrieve_strategy == DatasetRetrieveConfigEntity.RetrieveStrategy.MULTIPLE
  365. else None,
  366. }
  367. }
  368. def _convert_to_llm_node(self, original_app_mode: AppMode,
  369. new_app_mode: AppMode,
  370. graph: dict,
  371. model_config: ModelConfigEntity,
  372. prompt_template: PromptTemplateEntity,
  373. file_upload: Optional[FileExtraConfig] = None,
  374. external_data_variable_node_mapping: dict[str, str] = None) -> dict:
  375. """
  376. Convert to LLM Node
  377. :param original_app_mode: original app mode
  378. :param new_app_mode: new app mode
  379. :param graph: graph
  380. :param model_config: model config
  381. :param prompt_template: prompt template
  382. :param file_upload: file upload config (optional)
  383. :param external_data_variable_node_mapping: external data variable node mapping
  384. """
  385. # fetch start and knowledge retrieval node
  386. start_node = next(filter(lambda n: n['data']['type'] == NodeType.START.value, graph['nodes']))
  387. knowledge_retrieval_node = next(filter(
  388. lambda n: n['data']['type'] == NodeType.KNOWLEDGE_RETRIEVAL.value,
  389. graph['nodes']
  390. ), None)
  391. role_prefix = None
  392. # Chat Model
  393. if model_config.mode == LLMMode.CHAT.value:
  394. if prompt_template.prompt_type == PromptTemplateEntity.PromptType.SIMPLE:
  395. # get prompt template
  396. prompt_transform = SimplePromptTransform()
  397. prompt_template_config = prompt_transform.get_prompt_template(
  398. app_mode=original_app_mode,
  399. provider=model_config.provider,
  400. model=model_config.model,
  401. pre_prompt=prompt_template.simple_prompt_template,
  402. has_context=knowledge_retrieval_node is not None,
  403. query_in_prompt=False
  404. )
  405. template = prompt_template_config['prompt_template'].template
  406. if not template:
  407. prompts = []
  408. else:
  409. template = self._replace_template_variables(
  410. template,
  411. start_node['data']['variables'],
  412. external_data_variable_node_mapping
  413. )
  414. prompts = [
  415. {
  416. "role": 'user',
  417. "text": template
  418. }
  419. ]
  420. else:
  421. advanced_chat_prompt_template = prompt_template.advanced_chat_prompt_template
  422. prompts = []
  423. for m in advanced_chat_prompt_template.messages:
  424. if advanced_chat_prompt_template:
  425. text = m.text
  426. text = self._replace_template_variables(
  427. text,
  428. start_node['data']['variables'],
  429. external_data_variable_node_mapping
  430. )
  431. prompts.append({
  432. "role": m.role.value,
  433. "text": text
  434. })
  435. # Completion Model
  436. else:
  437. if prompt_template.prompt_type == PromptTemplateEntity.PromptType.SIMPLE:
  438. # get prompt template
  439. prompt_transform = SimplePromptTransform()
  440. prompt_template_config = prompt_transform.get_prompt_template(
  441. app_mode=original_app_mode,
  442. provider=model_config.provider,
  443. model=model_config.model,
  444. pre_prompt=prompt_template.simple_prompt_template,
  445. has_context=knowledge_retrieval_node is not None,
  446. query_in_prompt=False
  447. )
  448. template = prompt_template_config['prompt_template'].template
  449. template = self._replace_template_variables(
  450. template,
  451. start_node['data']['variables'],
  452. external_data_variable_node_mapping
  453. )
  454. prompts = {
  455. "text": template
  456. }
  457. prompt_rules = prompt_template_config['prompt_rules']
  458. role_prefix = {
  459. "user": prompt_rules.get('human_prefix', 'Human'),
  460. "assistant": prompt_rules.get('assistant_prefix', 'Assistant')
  461. }
  462. else:
  463. advanced_completion_prompt_template = prompt_template.advanced_completion_prompt_template
  464. if advanced_completion_prompt_template:
  465. text = advanced_completion_prompt_template.prompt
  466. text = self._replace_template_variables(
  467. text,
  468. start_node['data']['variables'],
  469. external_data_variable_node_mapping
  470. )
  471. else:
  472. text = ""
  473. text = text.replace('{{#query#}}', '{{#sys.query#}}')
  474. prompts = {
  475. "text": text,
  476. }
  477. if advanced_completion_prompt_template.role_prefix:
  478. role_prefix = {
  479. "user": advanced_completion_prompt_template.role_prefix.user,
  480. "assistant": advanced_completion_prompt_template.role_prefix.assistant
  481. }
  482. memory = None
  483. if new_app_mode == AppMode.ADVANCED_CHAT:
  484. memory = {
  485. "role_prefix": role_prefix,
  486. "window": {
  487. "enabled": False
  488. }
  489. }
  490. completion_params = model_config.parameters
  491. completion_params.update({"stop": model_config.stop})
  492. return {
  493. "id": "llm",
  494. "position": None,
  495. "data": {
  496. "title": "LLM",
  497. "type": NodeType.LLM.value,
  498. "model": {
  499. "provider": model_config.provider,
  500. "name": model_config.model,
  501. "mode": model_config.mode,
  502. "completion_params": completion_params
  503. },
  504. "prompt_template": prompts,
  505. "memory": memory,
  506. "context": {
  507. "enabled": knowledge_retrieval_node is not None,
  508. "variable_selector": ["knowledge_retrieval", "result"]
  509. if knowledge_retrieval_node is not None else None
  510. },
  511. "vision": {
  512. "enabled": file_upload is not None,
  513. "variable_selector": ["sys", "files"] if file_upload is not None else None,
  514. "configs": {
  515. "detail": file_upload.image_config['detail']
  516. } if file_upload is not None else None
  517. }
  518. }
  519. }
  520. def _replace_template_variables(self, template: str,
  521. variables: list[dict],
  522. external_data_variable_node_mapping: dict[str, str] = None) -> str:
  523. """
  524. Replace Template Variables
  525. :param template: template
  526. :param variables: list of variables
  527. :param external_data_variable_node_mapping: external data variable node mapping
  528. :return:
  529. """
  530. for v in variables:
  531. template = template.replace('{{' + v['variable'] + '}}', '{{#start.' + v['variable'] + '#}}')
  532. if external_data_variable_node_mapping:
  533. for variable, code_node_id in external_data_variable_node_mapping.items():
  534. template = template.replace('{{' + variable + '}}',
  535. '{{#' + code_node_id + '.result#}}')
  536. return template
  537. def _convert_to_end_node(self) -> dict:
  538. """
  539. Convert to End Node
  540. :return:
  541. """
  542. # for original completion app
  543. return {
  544. "id": "end",
  545. "position": None,
  546. "data": {
  547. "title": "END",
  548. "type": NodeType.END.value,
  549. "outputs": [{
  550. "variable": "result",
  551. "value_selector": ["llm", "text"]
  552. }]
  553. }
  554. }
  555. def _convert_to_answer_node(self) -> dict:
  556. """
  557. Convert to Answer Node
  558. :return:
  559. """
  560. # for original chat app
  561. return {
  562. "id": "answer",
  563. "position": None,
  564. "data": {
  565. "title": "ANSWER",
  566. "type": NodeType.ANSWER.value,
  567. "answer": "{{#llm.text#}}"
  568. }
  569. }
  570. def _create_edge(self, source: str, target: str) -> dict:
  571. """
  572. Create Edge
  573. :param source: source node id
  574. :param target: target node id
  575. :return:
  576. """
  577. return {
  578. "id": f"{source}-{target}",
  579. "source": source,
  580. "target": target
  581. }
  582. def _append_node(self, graph: dict, node: dict) -> dict:
  583. """
  584. Append Node to Graph
  585. :param graph: Graph, include: nodes, edges
  586. :param node: Node to append
  587. :return:
  588. """
  589. previous_node = graph['nodes'][-1]
  590. graph['nodes'].append(node)
  591. graph['edges'].append(self._create_edge(previous_node['id'], node['id']))
  592. return graph
  593. def _get_new_app_mode(self, app_model: App) -> AppMode:
  594. """
  595. Get new app mode
  596. :param app_model: App instance
  597. :return: AppMode
  598. """
  599. if app_model.mode == AppMode.COMPLETION.value:
  600. return AppMode.WORKFLOW
  601. else:
  602. return AppMode.ADVANCED_CHAT
  603. def _get_api_based_extension(self, tenant_id: str, api_based_extension_id: str) -> APIBasedExtension:
  604. """
  605. Get API Based Extension
  606. :param tenant_id: tenant id
  607. :param api_based_extension_id: api based extension id
  608. :return:
  609. """
  610. return db.session.query(APIBasedExtension).filter(
  611. APIBasedExtension.tenant_id == tenant_id,
  612. APIBasedExtension.id == api_based_extension_id
  613. ).first()