From dbc87dbd3b77b649e92f0b34f1c1c11134ca3ad4 Mon Sep 17 00:00:00 2001 From: Harry Date: Wed, 11 Mar 2026 14:41:19 +0800 Subject: [PATCH] feat: update LLM node skills API to extract tool dependencies and change endpoint to POST --- api/controllers/console/app/skills.py | 82 ++++--------------- api/services/skill_service.py | 24 ------ .../workflow/nodes/llm/use-node-skills.ts | 24 +++--- web/contract/console/workflow.ts | 7 +- 4 files changed, 32 insertions(+), 105 deletions(-) diff --git a/api/controllers/console/app/skills.py b/api/controllers/console/app/skills.py index 8364b1daf6..39ad903bc2 100644 --- a/api/controllers/console/app/skills.py +++ b/api/controllers/console/app/skills.py @@ -1,88 +1,38 @@ +from flask import request from flask_restx import Resource from controllers.console import console_ns -from controllers.console.app.error import DraftWorkflowNotExist from controllers.console.app.wraps import get_app_model from controllers.console.wraps import account_initialization_required, current_account_with_tenant, setup_required -from core.skill.entities.api_entities import NodeSkillInfo from libs.login import login_required from models import App -from models._workflow_exc import NodeNotFoundError from models.model import AppMode from services.skill_service import SkillService -from services.workflow_service import WorkflowService -@console_ns.route("/apps//workflows/draft/nodes//skills") +@console_ns.route("/apps//workflows/draft/nodes/llm/skills") class NodeSkillsApi(Resource): - """API for retrieving skill references for a specific workflow node.""" + """Extract tool dependencies from an LLM node's skill prompts. + + The client sends the full node ``data`` object in the request body. + The server real-time builds a ``SkillBundle`` from the current draft + ``.md`` assets and resolves transitive tool dependencies — no cached + bundle is used. + """ - @console_ns.doc("get_node_skills") - @console_ns.doc(description="Get skill references for a specific node in the draft workflow") - @console_ns.doc(params={"app_id": "Application ID", "node_id": "Node ID"}) - @console_ns.response(200, "Node skills retrieved successfully") - @console_ns.response(404, "Workflow or node not found") @setup_required @login_required @account_initialization_required @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) - def get(self, app_model: App, node_id: str): - """ - Get skill information for a specific node in the draft workflow. - - Returns information about skill references in the node, including: - - skill_references: List of prompt messages marked as skills - - tool_references: Aggregated tool references from all skill prompts - - file_references: Aggregated file references from all skill prompts - """ + def post(self, app_model: App): current_user, _ = current_account_with_tenant() - workflow_service = WorkflowService() - workflow = workflow_service.get_draft_workflow(app_model=app_model) + node_data = request.get_json(force=True) + if not isinstance(node_data, dict): + return {"tool_dependencies": []} - if not workflow: - raise DraftWorkflowNotExist() - - try: - skill_info = SkillService.get_node_skill_info( - app=app_model, - workflow=workflow, - node_id=node_id, - user_id=current_user.id, - ) - except NodeNotFoundError: - return NodeSkillInfo.empty(node_id=node_id).model_dump() - return skill_info.model_dump() - - -@console_ns.route("/apps//workflows/draft/skills") -class WorkflowSkillsApi(Resource): - """API for retrieving all skill references in a workflow.""" - - @console_ns.doc("get_workflow_skills") - @console_ns.doc(description="Get all skill references in the draft workflow") - @console_ns.doc(params={"app_id": "Application ID"}) - @console_ns.response(200, "Workflow skills retrieved successfully") - @console_ns.response(404, "Workflow not found") - @setup_required - @login_required - @account_initialization_required - @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) - def get(self, app_model: App): - """ - Get skill information for all nodes in the draft workflow that have skill references. - - Returns a list of nodes with their skill information. - """ - current_user, _ = current_account_with_tenant() - workflow_service = WorkflowService() - workflow = workflow_service.get_draft_workflow(app_model=app_model) - - if not workflow: - raise DraftWorkflowNotExist() - - skills_info = SkillService.get_workflow_skills( + tool_deps = SkillService.extract_tool_dependencies( app=app_model, - workflow=workflow, + node_data=node_data, user_id=current_user.id, ) - return {"nodes": [info.model_dump() for info in skills_info]} + return {"tool_dependencies": [d.model_dump() for d in tool_deps]} diff --git a/api/services/skill_service.py b/api/services/skill_service.py index 352f5a50ab..df8f66ac75 100644 --- a/api/services/skill_service.py +++ b/api/services/skill_service.py @@ -21,7 +21,6 @@ from typing import Any, cast from core.app.entities.app_asset_entities import AppAssetFileTree, AppAssetNode from core.sandbox.entities.config import AppAssets from core.skill.assembler import SkillBundleAssembler, SkillDocumentAssembler -from core.skill.entities.api_entities import NodeSkillInfo from core.skill.entities.skill_bundle import SkillBundle from core.skill.entities.skill_document import SkillDocument from core.skill.entities.skill_metadata import SkillMetadata @@ -29,7 +28,6 @@ from core.skill.entities.tool_dependencies import ToolDependencies, ToolDependen from core.skill.skill_manager import SkillManager from core.workflow.enums import NodeType from models.model import App -from models.workflow import Workflow from services.app_asset_service import AppAssetService logger = logging.getLogger(__name__) @@ -69,28 +67,6 @@ class SkillService: return SkillService._resolve_prompt_dependencies(node_data, bundle) - # ------------------------------------------------------------------ - # Whole-workflow: reads persisted draft + cached bundle - # ------------------------------------------------------------------ - - @staticmethod - def get_workflow_skills(app: App, workflow: Workflow, user_id: str) -> list[NodeSkillInfo]: - """Get skill information for all LLM nodes in a persisted workflow. - - Uses the cached ``SkillBundle`` (Redis / S3). This method is - kept for the whole-workflow GET endpoint. - """ - result: list[NodeSkillInfo] = [] - - for node_id, node_data in workflow.walk_nodes(specific_node_type=NodeType.LLM): - if not SkillService._has_skill(dict(node_data)): - continue - - tool_dependencies = SkillService._extract_tool_dependencies_cached(app, dict(node_data), user_id) - result.append(NodeSkillInfo(node_id=node_id, tool_dependencies=tool_dependencies)) - - return result - # ------------------------------------------------------------------ # Internal helpers # ------------------------------------------------------------------ diff --git a/web/app/components/workflow/nodes/llm/use-node-skills.ts b/web/app/components/workflow/nodes/llm/use-node-skills.ts index 6247339d62..3a87138539 100644 --- a/web/app/components/workflow/nodes/llm/use-node-skills.ts +++ b/web/app/components/workflow/nodes/llm/use-node-skills.ts @@ -2,6 +2,7 @@ import { useQuery } from '@tanstack/react-query' import { useMemo } from 'react' +import { useStoreApi } from 'reactflow' import { useStore as useAppStore } from '@/app/components/app/store' import { consoleClient, consoleQuery } from '@/service/client' @@ -19,32 +20,33 @@ type UseNodeSkillsParams = { export function useNodeSkills({ nodeId, promptTemplateKey, enabled = true }: UseNodeSkillsParams) { const appId = useAppStore(s => s.appDetail?.id) + const store = useStoreApi() const isQueryEnabled = enabled && !!appId && !!nodeId const queryKey = useMemo(() => { return [ ...consoleQuery.workflowDraft.nodeSkills.queryKey({ input: { - params: { - appId: appId ?? '', - nodeId, - }, + params: { appId: appId ?? '' }, + body: {}, }, }), + nodeId, promptTemplateKey, ] }, [appId, nodeId, promptTemplateKey]) const { data, isLoading } = useQuery({ queryKey, - queryFn: () => consoleClient.workflowDraft.nodeSkills({ - params: { - appId: appId ?? '', - nodeId, - }, - }), + queryFn: () => { + const node = store.getState().getNodes().find(n => n.id === nodeId) + return consoleClient.workflowDraft.nodeSkills({ + params: { appId: appId ?? '' }, + body: (node?.data ?? {}) as Record, + }) + }, enabled: isQueryEnabled, - placeholderData: previous => previous, + gcTime: 0, }) const toolDependencies = useMemo( diff --git a/web/contract/console/workflow.ts b/web/contract/console/workflow.ts index 0a4fe8b811..8a0543e2ff 100644 --- a/web/contract/console/workflow.ts +++ b/web/contract/console/workflow.ts @@ -83,17 +83,16 @@ export const workflowDraftUpdateFeaturesContract = base export const workflowDraftNodeSkillsContract = base .route({ - path: '/apps/{appId}/workflows/draft/nodes/{nodeId}/skills', - method: 'GET', + path: '/apps/{appId}/workflows/draft/nodes/llm/skills', + method: 'POST', }) .input(type<{ params: { appId: string - nodeId: string } + body: Record }>()) .output(type<{ - node_id: string tool_dependencies: { type: string provider: string