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.

workflow_converter.py 25KB

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