feat: update LLM node skills API to extract tool dependencies and change endpoint to POST

This commit is contained in:
Harry
2026-03-11 14:41:19 +08:00
parent 0776e16fdc
commit dbc87dbd3b
4 changed files with 32 additions and 105 deletions

View File

@ -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/<uuid:app_id>/workflows/draft/nodes/<string:node_id>/skills")
@console_ns.route("/apps/<uuid:app_id>/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/<uuid:app_id>/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]}

View File

@ -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
# ------------------------------------------------------------------

View File

@ -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<string, unknown>,
})
},
enabled: isQueryEnabled,
placeholderData: previous => previous,
gcTime: 0,
})
const toolDependencies = useMemo<ToolDependency[]>(

View File

@ -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<string, unknown>
}>())
.output(type<{
node_id: string
tool_dependencies: {
type: string
provider: string