You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

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