app_dsl_service.py 28 KB

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