app_dsl_service.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726
  1. import logging
  2. import uuid
  3. from collections.abc import Mapping
  4. from enum import StrEnum
  5. from typing import Optional
  6. from urllib.parse import urlparse
  7. from uuid import uuid4
  8. import yaml # type: ignore
  9. from packaging import version
  10. from pydantic import BaseModel, Field
  11. from sqlalchemy import select
  12. from sqlalchemy.orm import Session
  13. from core.helper import ssrf_proxy
  14. from core.model_runtime.utils.encoders import jsonable_encoder
  15. from core.plugin.entities.plugin import PluginDependency
  16. from core.workflow.nodes.enums import NodeType
  17. from core.workflow.nodes.knowledge_retrieval.entities import KnowledgeRetrievalNodeData
  18. from core.workflow.nodes.llm.entities import LLMNodeData
  19. from core.workflow.nodes.parameter_extractor.entities import ParameterExtractorNodeData
  20. from core.workflow.nodes.question_classifier.entities import QuestionClassifierNodeData
  21. from core.workflow.nodes.tool.entities import ToolNodeData
  22. from events.app_event import app_model_config_was_updated, app_was_created
  23. from extensions.ext_redis import redis_client
  24. from factories import variable_factory
  25. from models import Account, App, AppMode
  26. from models.model import AppModelConfig
  27. from models.workflow import Workflow
  28. from services.plugin.dependencies_analysis import DependenciesAnalysisService
  29. from services.workflow_service import WorkflowService
  30. logger = logging.getLogger(__name__)
  31. IMPORT_INFO_REDIS_KEY_PREFIX = "app_import_info:"
  32. CHECK_DEPENDENCIES_REDIS_KEY_PREFIX = "app_check_dependencies:"
  33. IMPORT_INFO_REDIS_EXPIRY = 10 * 60 # 10 minutes
  34. DSL_MAX_SIZE = 10 * 1024 * 1024 # 10MB
  35. CURRENT_DSL_VERSION = "0.1.5"
  36. class ImportMode(StrEnum):
  37. YAML_CONTENT = "yaml-content"
  38. YAML_URL = "yaml-url"
  39. class ImportStatus(StrEnum):
  40. COMPLETED = "completed"
  41. COMPLETED_WITH_WARNINGS = "completed-with-warnings"
  42. PENDING = "pending"
  43. FAILED = "failed"
  44. class Import(BaseModel):
  45. id: str
  46. status: ImportStatus
  47. app_id: Optional[str] = None
  48. app_mode: Optional[str] = None
  49. current_dsl_version: str = CURRENT_DSL_VERSION
  50. imported_dsl_version: str = ""
  51. error: str = ""
  52. class CheckDependenciesResult(BaseModel):
  53. leaked_dependencies: list[PluginDependency] = Field(default_factory=list)
  54. def _check_version_compatibility(imported_version: str) -> ImportStatus:
  55. """Determine import status based on version comparison"""
  56. try:
  57. current_ver = version.parse(CURRENT_DSL_VERSION)
  58. imported_ver = version.parse(imported_version)
  59. except version.InvalidVersion:
  60. return ImportStatus.FAILED
  61. # Compare major version and minor version
  62. if current_ver.major != imported_ver.major or current_ver.minor != imported_ver.minor:
  63. return ImportStatus.PENDING
  64. if current_ver.micro != imported_ver.micro:
  65. return ImportStatus.COMPLETED_WITH_WARNINGS
  66. return ImportStatus.COMPLETED
  67. class PendingData(BaseModel):
  68. import_mode: str
  69. yaml_content: str
  70. name: str | None
  71. description: str | None
  72. icon_type: str | None
  73. icon: str | None
  74. icon_background: str | None
  75. app_id: str | None
  76. class CheckDependenciesPendingData(BaseModel):
  77. dependencies: list[PluginDependency]
  78. app_id: str | None
  79. class AppDslService:
  80. def __init__(self, session: Session):
  81. self._session = session
  82. def import_app(
  83. self,
  84. *,
  85. account: Account,
  86. import_mode: str,
  87. yaml_content: Optional[str] = None,
  88. yaml_url: Optional[str] = None,
  89. name: Optional[str] = None,
  90. description: Optional[str] = None,
  91. icon_type: Optional[str] = None,
  92. icon: Optional[str] = None,
  93. icon_background: Optional[str] = None,
  94. app_id: Optional[str] = None,
  95. ) -> Import:
  96. """Import an app from YAML content or URL."""
  97. import_id = str(uuid.uuid4())
  98. # Validate import mode
  99. try:
  100. mode = ImportMode(import_mode)
  101. except ValueError:
  102. raise ValueError(f"Invalid import_mode: {import_mode}")
  103. # Get YAML content
  104. content: str = ""
  105. if mode == ImportMode.YAML_URL:
  106. if not yaml_url:
  107. return Import(
  108. id=import_id,
  109. status=ImportStatus.FAILED,
  110. error="yaml_url is required when import_mode is yaml-url",
  111. )
  112. try:
  113. parsed_url = urlparse(yaml_url)
  114. if (
  115. parsed_url.scheme == "https"
  116. and parsed_url.netloc == "github.com"
  117. and parsed_url.path.endswith((".yml", ".yaml"))
  118. ):
  119. yaml_url = yaml_url.replace("https://github.com", "https://raw.githubusercontent.com")
  120. yaml_url = yaml_url.replace("/blob/", "/")
  121. response = ssrf_proxy.get(yaml_url.strip(), follow_redirects=True, timeout=(10, 10))
  122. response.raise_for_status()
  123. content = response.content.decode()
  124. if len(content) > DSL_MAX_SIZE:
  125. return Import(
  126. id=import_id,
  127. status=ImportStatus.FAILED,
  128. error="File size exceeds the limit of 10MB",
  129. )
  130. if not content:
  131. return Import(
  132. id=import_id,
  133. status=ImportStatus.FAILED,
  134. error="Empty content from url",
  135. )
  136. except Exception as e:
  137. return Import(
  138. id=import_id,
  139. status=ImportStatus.FAILED,
  140. error=f"Error fetching YAML from URL: {str(e)}",
  141. )
  142. elif mode == ImportMode.YAML_CONTENT:
  143. if not yaml_content:
  144. return Import(
  145. id=import_id,
  146. status=ImportStatus.FAILED,
  147. error="yaml_content is required when import_mode is yaml-content",
  148. )
  149. content = yaml_content
  150. # Process YAML content
  151. try:
  152. # Parse YAML to validate format
  153. data = yaml.safe_load(content)
  154. if not isinstance(data, dict):
  155. return Import(
  156. id=import_id,
  157. status=ImportStatus.FAILED,
  158. error="Invalid YAML format: content must be a mapping",
  159. )
  160. # Validate and fix DSL version
  161. if not data.get("version"):
  162. data["version"] = "0.1.0"
  163. if not data.get("kind") or data.get("kind") != "app":
  164. data["kind"] = "app"
  165. imported_version = data.get("version", "0.1.0")
  166. # check if imported_version is a float-like string
  167. if not isinstance(imported_version, str):
  168. raise ValueError(f"Invalid version type, expected str, got {type(imported_version)}")
  169. status = _check_version_compatibility(imported_version)
  170. # Extract app data
  171. app_data = data.get("app")
  172. if not app_data:
  173. return Import(
  174. id=import_id,
  175. status=ImportStatus.FAILED,
  176. error="Missing app data in YAML content",
  177. )
  178. # If app_id is provided, check if it exists
  179. app = None
  180. if app_id:
  181. stmt = select(App).where(App.id == app_id, App.tenant_id == account.current_tenant_id)
  182. app = self._session.scalar(stmt)
  183. if not app:
  184. return Import(
  185. id=import_id,
  186. status=ImportStatus.FAILED,
  187. error="App not found",
  188. )
  189. if app.mode not in [AppMode.WORKFLOW, AppMode.ADVANCED_CHAT]:
  190. return Import(
  191. id=import_id,
  192. status=ImportStatus.FAILED,
  193. error="Only workflow or advanced chat apps can be overwritten",
  194. )
  195. # If major version mismatch, store import info in Redis
  196. if status == ImportStatus.PENDING:
  197. pending_data = PendingData(
  198. import_mode=import_mode,
  199. yaml_content=content,
  200. name=name,
  201. description=description,
  202. icon_type=icon_type,
  203. icon=icon,
  204. icon_background=icon_background,
  205. app_id=app_id,
  206. )
  207. redis_client.setex(
  208. f"{IMPORT_INFO_REDIS_KEY_PREFIX}{import_id}",
  209. IMPORT_INFO_REDIS_EXPIRY,
  210. pending_data.model_dump_json(),
  211. )
  212. return Import(
  213. id=import_id,
  214. status=status,
  215. app_id=app_id,
  216. imported_dsl_version=imported_version,
  217. )
  218. # Extract dependencies
  219. dependencies = data.get("dependencies", [])
  220. check_dependencies_pending_data = None
  221. if dependencies:
  222. check_dependencies_pending_data = [PluginDependency.model_validate(d) for d in dependencies]
  223. elif imported_version <= "0.1.5":
  224. if "workflow" in data:
  225. graph = data.get("workflow", {}).get("graph", {})
  226. dependencies_list = self._extract_dependencies_from_workflow_graph(graph)
  227. else:
  228. dependencies_list = self._extract_dependencies_from_model_config(data.get("model_config", {}))
  229. check_dependencies_pending_data = DependenciesAnalysisService.generate_latest_dependencies(
  230. dependencies_list
  231. )
  232. # Create or update app
  233. app = self._create_or_update_app(
  234. app=app,
  235. data=data,
  236. account=account,
  237. name=name,
  238. description=description,
  239. icon_type=icon_type,
  240. icon=icon,
  241. icon_background=icon_background,
  242. dependencies=check_dependencies_pending_data,
  243. )
  244. return Import(
  245. id=import_id,
  246. status=status,
  247. app_id=app.id,
  248. app_mode=app.mode,
  249. imported_dsl_version=imported_version,
  250. )
  251. except yaml.YAMLError as e:
  252. return Import(
  253. id=import_id,
  254. status=ImportStatus.FAILED,
  255. error=f"Invalid YAML format: {str(e)}",
  256. )
  257. except Exception as e:
  258. logger.exception("Failed to import app")
  259. return Import(
  260. id=import_id,
  261. status=ImportStatus.FAILED,
  262. error=str(e),
  263. )
  264. def confirm_import(self, *, import_id: str, account: Account) -> Import:
  265. """
  266. Confirm an import that requires confirmation
  267. """
  268. redis_key = f"{IMPORT_INFO_REDIS_KEY_PREFIX}{import_id}"
  269. pending_data = redis_client.get(redis_key)
  270. if not pending_data:
  271. return Import(
  272. id=import_id,
  273. status=ImportStatus.FAILED,
  274. error="Import information expired or does not exist",
  275. )
  276. try:
  277. if not isinstance(pending_data, str | bytes):
  278. return Import(
  279. id=import_id,
  280. status=ImportStatus.FAILED,
  281. error="Invalid import information",
  282. )
  283. pending_data = PendingData.model_validate_json(pending_data)
  284. data = yaml.safe_load(pending_data.yaml_content)
  285. app = None
  286. if pending_data.app_id:
  287. stmt = select(App).where(App.id == pending_data.app_id, App.tenant_id == account.current_tenant_id)
  288. app = self._session.scalar(stmt)
  289. # Create or update app
  290. app = self._create_or_update_app(
  291. app=app,
  292. data=data,
  293. account=account,
  294. name=pending_data.name,
  295. description=pending_data.description,
  296. icon_type=pending_data.icon_type,
  297. icon=pending_data.icon,
  298. icon_background=pending_data.icon_background,
  299. )
  300. # Delete import info from Redis
  301. redis_client.delete(redis_key)
  302. return Import(
  303. id=import_id,
  304. status=ImportStatus.COMPLETED,
  305. app_id=app.id,
  306. app_mode=app.mode,
  307. current_dsl_version=CURRENT_DSL_VERSION,
  308. imported_dsl_version=data.get("version", "0.1.0"),
  309. )
  310. except Exception as e:
  311. logger.exception("Error confirming import")
  312. return Import(
  313. id=import_id,
  314. status=ImportStatus.FAILED,
  315. error=str(e),
  316. )
  317. def check_dependencies(
  318. self,
  319. *,
  320. app_model: App,
  321. ) -> CheckDependenciesResult:
  322. """Check dependencies"""
  323. # Get dependencies from Redis
  324. redis_key = f"{CHECK_DEPENDENCIES_REDIS_KEY_PREFIX}{app_model.id}"
  325. dependencies = redis_client.get(redis_key)
  326. if not dependencies:
  327. return CheckDependenciesResult()
  328. # Extract dependencies
  329. dependencies = CheckDependenciesPendingData.model_validate_json(dependencies)
  330. # Get leaked dependencies
  331. leaked_dependencies = DependenciesAnalysisService.get_leaked_dependencies(
  332. tenant_id=app_model.tenant_id, dependencies=dependencies.dependencies
  333. )
  334. return CheckDependenciesResult(
  335. leaked_dependencies=leaked_dependencies,
  336. )
  337. def _create_or_update_app(
  338. self,
  339. *,
  340. app: Optional[App],
  341. data: dict,
  342. account: Account,
  343. name: Optional[str] = None,
  344. description: Optional[str] = None,
  345. icon_type: Optional[str] = None,
  346. icon: Optional[str] = None,
  347. icon_background: Optional[str] = None,
  348. dependencies: Optional[list[PluginDependency]] = None,
  349. ) -> App:
  350. """Create a new app or update an existing one."""
  351. app_data = data.get("app", {})
  352. app_mode = app_data.get("mode")
  353. if not app_mode:
  354. raise ValueError("loss app mode")
  355. app_mode = AppMode(app_mode)
  356. # Set icon type
  357. icon_type_value = icon_type or app_data.get("icon_type")
  358. if icon_type_value in ["emoji", "link"]:
  359. icon_type = icon_type_value
  360. else:
  361. icon_type = "emoji"
  362. icon = icon or str(app_data.get("icon", ""))
  363. if app:
  364. # Update existing app
  365. app.name = name or app_data.get("name", app.name)
  366. app.description = description or app_data.get("description", app.description)
  367. app.icon_type = icon_type
  368. app.icon = icon
  369. app.icon_background = icon_background or app_data.get("icon_background", app.icon_background)
  370. app.updated_by = account.id
  371. else:
  372. if account.current_tenant_id is None:
  373. raise ValueError("Current tenant is not set")
  374. # Create new app
  375. app = App()
  376. app.id = str(uuid4())
  377. app.tenant_id = account.current_tenant_id
  378. app.mode = app_mode.value
  379. app.name = name or app_data.get("name", "")
  380. app.description = description or app_data.get("description", "")
  381. app.icon_type = icon_type
  382. app.icon = icon
  383. app.icon_background = icon_background or app_data.get("icon_background", "#FFFFFF")
  384. app.enable_site = True
  385. app.enable_api = True
  386. app.use_icon_as_answer_icon = app_data.get("use_icon_as_answer_icon", False)
  387. app.created_by = account.id
  388. app.updated_by = account.id
  389. self._session.add(app)
  390. self._session.commit()
  391. app_was_created.send(app, account=account)
  392. # save dependencies
  393. if dependencies:
  394. redis_client.setex(
  395. f"{CHECK_DEPENDENCIES_REDIS_KEY_PREFIX}{app.id}",
  396. IMPORT_INFO_REDIS_EXPIRY,
  397. CheckDependenciesPendingData(app_id=app.id, dependencies=dependencies).model_dump_json(),
  398. )
  399. # Initialize app based on mode
  400. if app_mode in {AppMode.ADVANCED_CHAT, AppMode.WORKFLOW}:
  401. workflow_data = data.get("workflow")
  402. if not workflow_data or not isinstance(workflow_data, dict):
  403. raise ValueError("Missing workflow data for workflow/advanced chat app")
  404. environment_variables_list = workflow_data.get("environment_variables", [])
  405. environment_variables = [
  406. variable_factory.build_environment_variable_from_mapping(obj) for obj in environment_variables_list
  407. ]
  408. conversation_variables_list = workflow_data.get("conversation_variables", [])
  409. conversation_variables = [
  410. variable_factory.build_conversation_variable_from_mapping(obj) for obj in conversation_variables_list
  411. ]
  412. workflow_service = WorkflowService()
  413. current_draft_workflow = workflow_service.get_draft_workflow(app_model=app)
  414. if current_draft_workflow:
  415. unique_hash = current_draft_workflow.unique_hash
  416. else:
  417. unique_hash = None
  418. workflow_service.sync_draft_workflow(
  419. app_model=app,
  420. graph=workflow_data.get("graph", {}),
  421. features=workflow_data.get("features", {}),
  422. unique_hash=unique_hash,
  423. account=account,
  424. environment_variables=environment_variables,
  425. conversation_variables=conversation_variables,
  426. )
  427. elif app_mode in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.COMPLETION}:
  428. # Initialize model config
  429. model_config = data.get("model_config")
  430. if not model_config or not isinstance(model_config, dict):
  431. raise ValueError("Missing model_config for chat/agent-chat/completion app")
  432. # Initialize or update model config
  433. if not app.app_model_config:
  434. app_model_config = AppModelConfig().from_model_config_dict(model_config)
  435. app_model_config.id = str(uuid4())
  436. app_model_config.app_id = app.id
  437. app_model_config.created_by = account.id
  438. app_model_config.updated_by = account.id
  439. app.app_model_config_id = app_model_config.id
  440. self._session.add(app_model_config)
  441. app_model_config_was_updated.send(app, app_model_config=app_model_config)
  442. else:
  443. raise ValueError("Invalid app mode")
  444. return app
  445. @classmethod
  446. def export_dsl(cls, app_model: App, include_secret: bool = False) -> str:
  447. """
  448. Export app
  449. :param app_model: App instance
  450. :return:
  451. """
  452. app_mode = AppMode.value_of(app_model.mode)
  453. export_data = {
  454. "version": CURRENT_DSL_VERSION,
  455. "kind": "app",
  456. "app": {
  457. "name": app_model.name,
  458. "mode": app_model.mode,
  459. "icon": "🤖" if app_model.icon_type == "image" else app_model.icon,
  460. "icon_background": "#FFEAD5" if app_model.icon_type == "image" else app_model.icon_background,
  461. "description": app_model.description,
  462. "use_icon_as_answer_icon": app_model.use_icon_as_answer_icon,
  463. },
  464. }
  465. if app_mode in {AppMode.ADVANCED_CHAT, AppMode.WORKFLOW}:
  466. cls._append_workflow_export_data(
  467. export_data=export_data, app_model=app_model, include_secret=include_secret
  468. )
  469. else:
  470. cls._append_model_config_export_data(export_data, app_model)
  471. return yaml.dump(export_data, allow_unicode=True) # type: ignore
  472. @classmethod
  473. def _append_workflow_export_data(cls, *, export_data: dict, app_model: App, include_secret: bool) -> None:
  474. """
  475. Append workflow export data
  476. :param export_data: export data
  477. :param app_model: App instance
  478. """
  479. workflow_service = WorkflowService()
  480. workflow = workflow_service.get_draft_workflow(app_model)
  481. if not workflow:
  482. raise ValueError("Missing draft workflow configuration, please check.")
  483. export_data["workflow"] = workflow.to_dict(include_secret=include_secret)
  484. dependencies = cls._extract_dependencies_from_workflow(workflow)
  485. export_data["dependencies"] = [
  486. jsonable_encoder(d.model_dump())
  487. for d in DependenciesAnalysisService.generate_dependencies(
  488. tenant_id=app_model.tenant_id, dependencies=dependencies
  489. )
  490. ]
  491. @classmethod
  492. def _append_model_config_export_data(cls, export_data: dict, app_model: App) -> None:
  493. """
  494. Append model config export data
  495. :param export_data: export data
  496. :param app_model: App instance
  497. """
  498. app_model_config = app_model.app_model_config
  499. if not app_model_config:
  500. raise ValueError("Missing app configuration, please check.")
  501. export_data["model_config"] = app_model_config.to_dict()
  502. dependencies = cls._extract_dependencies_from_model_config(app_model_config.to_dict())
  503. export_data["dependencies"] = [
  504. jsonable_encoder(d.model_dump())
  505. for d in DependenciesAnalysisService.generate_dependencies(
  506. tenant_id=app_model.tenant_id, dependencies=dependencies
  507. )
  508. ]
  509. @classmethod
  510. def _extract_dependencies_from_workflow(cls, workflow: Workflow) -> list[str]:
  511. """
  512. Extract dependencies from workflow
  513. :param workflow: Workflow instance
  514. :return: dependencies list format like ["langgenius/google"]
  515. """
  516. graph = workflow.graph_dict
  517. dependencies = cls._extract_dependencies_from_workflow_graph(graph)
  518. return dependencies
  519. @classmethod
  520. def _extract_dependencies_from_workflow_graph(cls, graph: Mapping) -> list[str]:
  521. """
  522. Extract dependencies from workflow graph
  523. :param graph: Workflow graph
  524. :return: dependencies list format like ["langgenius/google"]
  525. """
  526. dependencies = []
  527. for node in graph.get("nodes", []):
  528. try:
  529. typ = node.get("data", {}).get("type")
  530. match typ:
  531. case NodeType.TOOL.value:
  532. tool_entity = ToolNodeData(**node["data"])
  533. dependencies.append(
  534. DependenciesAnalysisService.analyze_tool_dependency(tool_entity.provider_id),
  535. )
  536. case NodeType.LLM.value:
  537. llm_entity = LLMNodeData(**node["data"])
  538. dependencies.append(
  539. DependenciesAnalysisService.analyze_model_provider_dependency(llm_entity.model.provider),
  540. )
  541. case NodeType.QUESTION_CLASSIFIER.value:
  542. question_classifier_entity = QuestionClassifierNodeData(**node["data"])
  543. dependencies.append(
  544. DependenciesAnalysisService.analyze_model_provider_dependency(
  545. question_classifier_entity.model.provider
  546. ),
  547. )
  548. case NodeType.PARAMETER_EXTRACTOR.value:
  549. parameter_extractor_entity = ParameterExtractorNodeData(**node["data"])
  550. dependencies.append(
  551. DependenciesAnalysisService.analyze_model_provider_dependency(
  552. parameter_extractor_entity.model.provider
  553. ),
  554. )
  555. case NodeType.KNOWLEDGE_RETRIEVAL.value:
  556. knowledge_retrieval_entity = KnowledgeRetrievalNodeData(**node["data"])
  557. if knowledge_retrieval_entity.retrieval_mode == "multiple":
  558. if knowledge_retrieval_entity.multiple_retrieval_config:
  559. if (
  560. knowledge_retrieval_entity.multiple_retrieval_config.reranking_mode
  561. == "reranking_model"
  562. ):
  563. if knowledge_retrieval_entity.multiple_retrieval_config.reranking_model:
  564. dependencies.append(
  565. DependenciesAnalysisService.analyze_model_provider_dependency(
  566. knowledge_retrieval_entity.multiple_retrieval_config.reranking_model.provider
  567. ),
  568. )
  569. elif (
  570. knowledge_retrieval_entity.multiple_retrieval_config.reranking_mode
  571. == "weighted_score"
  572. ):
  573. if knowledge_retrieval_entity.multiple_retrieval_config.weights:
  574. vector_setting = (
  575. knowledge_retrieval_entity.multiple_retrieval_config.weights.vector_setting
  576. )
  577. dependencies.append(
  578. DependenciesAnalysisService.analyze_model_provider_dependency(
  579. vector_setting.embedding_provider_name
  580. ),
  581. )
  582. elif knowledge_retrieval_entity.retrieval_mode == "single":
  583. model_config = knowledge_retrieval_entity.single_retrieval_config
  584. if model_config:
  585. dependencies.append(
  586. DependenciesAnalysisService.analyze_model_provider_dependency(
  587. model_config.model.provider
  588. ),
  589. )
  590. case _:
  591. # TODO: Handle default case or unknown node types
  592. pass
  593. except Exception as e:
  594. logger.exception("Error extracting node dependency", exc_info=e)
  595. return dependencies
  596. @classmethod
  597. def _extract_dependencies_from_model_config(cls, model_config: Mapping) -> list[str]:
  598. """
  599. Extract dependencies from model config
  600. :param model_config: model config dict
  601. :return: dependencies list format like ["langgenius/google"]
  602. """
  603. dependencies = []
  604. try:
  605. # completion model
  606. model_dict = model_config.get("model", {})
  607. if model_dict:
  608. dependencies.append(
  609. DependenciesAnalysisService.analyze_model_provider_dependency(model_dict.get("provider", ""))
  610. )
  611. # reranking model
  612. dataset_configs = model_config.get("dataset_configs", {})
  613. if dataset_configs:
  614. for dataset_config in dataset_configs.get("datasets", {}).get("datasets", []):
  615. if dataset_config.get("reranking_model"):
  616. dependencies.append(
  617. DependenciesAnalysisService.analyze_model_provider_dependency(
  618. dataset_config.get("reranking_model", {})
  619. .get("reranking_provider_name", {})
  620. .get("provider")
  621. )
  622. )
  623. # tools
  624. agent_configs = model_config.get("agent_mode", {})
  625. if agent_configs:
  626. for agent_config in agent_configs.get("tools", []):
  627. dependencies.append(
  628. DependenciesAnalysisService.analyze_tool_dependency(agent_config.get("provider_id"))
  629. )
  630. except Exception as e:
  631. logger.exception("Error extracting model config dependency", exc_info=e)
  632. return dependencies
  633. @classmethod
  634. def get_leaked_dependencies(cls, tenant_id: str, dsl_dependencies: list[dict]) -> list[PluginDependency]:
  635. """
  636. Returns the leaked dependencies in current workspace
  637. """
  638. dependencies = [PluginDependency(**dep) for dep in dsl_dependencies]
  639. if not dependencies:
  640. return []
  641. return DependenciesAnalysisService.get_leaked_dependencies(tenant_id=tenant_id, dependencies=dependencies)