""" Service for generating Nested Node LLM graph structures. This service creates graph structures containing LLM nodes configured for extracting values from list[PromptMessage] variables. """ from typing import Any from sqlalchemy.orm import Session from core.model_runtime.entities import LLMMode from core.workflow.enums import NodeType from services.model_provider_service import ModelProviderService from services.workflow.entities import NestedNodeGraphRequest, NestedNodeGraphResponse, NestedNodeParameterSchema class NestedNodeGraphService: """Service for generating Nested Node LLM graph structures.""" def __init__(self, session: Session): self._session = session def generate_nested_node_id(self, node_id: str, parameter_name: str) -> str: """Generate nested node ID following the naming convention. Format: {node_id}_ext_{parameter_name} """ return f"{node_id}_ext_{parameter_name}" def generate_nested_node_graph(self, tenant_id: str, request: NestedNodeGraphRequest) -> NestedNodeGraphResponse: """Generate a complete graph structure containing a Nested Node LLM node. Args: tenant_id: The tenant ID for fetching default model config request: The nested node graph generation request Returns: Complete graph structure with nodes, edges, and viewport """ node_id = self.generate_nested_node_id(request.parent_node_id, request.parameter_key) model_config = self._get_default_model_config(tenant_id) node = self._build_nested_node_llm_node( node_id=node_id, parent_node_id=request.parent_node_id, context_source=request.context_source, parameter_schema=request.parameter_schema, model_config=model_config, ) graph = { "nodes": [node], "edges": [], "viewport": {}, } return NestedNodeGraphResponse(graph=graph) def _get_default_model_config(self, tenant_id: str) -> dict[str, Any]: """Get the default LLM model configuration for the tenant.""" model_provider_service = ModelProviderService() default_model = model_provider_service.get_default_model_of_model_type( tenant_id=tenant_id, model_type="llm", ) if default_model: return { "provider": default_model.provider.provider, "name": default_model.model, "mode": LLMMode.CHAT.value, "completion_params": {}, } # Fallback to empty config if no default model is configured return { "provider": "", "name": "", "mode": LLMMode.CHAT.value, "completion_params": {}, } def _build_nested_node_llm_node( self, *, node_id: str, parent_node_id: str, context_source: list[str], parameter_schema: NestedNodeParameterSchema, model_config: dict[str, Any], ) -> dict[str, Any]: """Build the Nested Node LLM node structure. The node uses: - $context in prompt_template to reference the PromptMessage list - structured_output for extracting the specific parameter - parent_node_id to associate with the parent node """ prompt_template = [ { "role": "system", "text": "Extract the required parameter value from the conversation context above.", "skill": False, }, {"$context": context_source}, {"role": "user", "text": "", "skill": False}, ] structured_output = { "schema": { "type": "object", "properties": { parameter_schema.name: { "type": parameter_schema.type, "description": parameter_schema.description, } }, "required": [parameter_schema.name], "additionalProperties": False, } } return { "id": node_id, "position": {"x": 0, "y": 0}, "data": { "type": NodeType.LLM.value, # BaseNodeData fields "title": f"NestedNode: {parameter_schema.name}", "desc": f"Extract {parameter_schema.name} from conversation context", "version": "1", "error_strategy": None, "default_value": None, "retry_config": {"max_retries": 0}, "parent_node_id": parent_node_id, # LLMNodeData fields "model": model_config, "prompt_template": prompt_template, "prompt_config": {"jinja2_variables": []}, "memory": None, "context": { "enabled": False, "variable_selector": None, }, "vision": { "enabled": False, "configs": { "variable_selector": ["sys", "files"], "detail": "high", }, }, "structured_output_enabled": True, "structured_output": structured_output, "computer_use": False, "tool_settings": [], }, }