workflow_converter.py 26 KB

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