diff --git a/api/core/llm_generator/output_parser/structured_output.py b/api/core/llm_generator/output_parser/structured_output.py index e2bdd01d3f..3843378f1d 100644 --- a/api/core/llm_generator/output_parser/structured_output.py +++ b/api/core/llm_generator/output_parser/structured_output.py @@ -9,7 +9,7 @@ from pydantic import BaseModel, TypeAdapter, ValidationError from core.llm_generator.output_parser.errors import OutputParserError from core.llm_generator.output_parser.file_ref import detect_file_path_fields -from core.llm_generator.prompts import STRUCTURED_OUTPUT_PROMPT +from core.llm_generator.prompts import STRUCTURED_OUTPUT_PROMPT, STRUCTURED_OUTPUT_TOOL_CALL_PROMPT from core.model_manager import ModelInstance from core.model_runtime.callbacks.base_callback import Callback from core.model_runtime.entities.llm_entities import ( @@ -88,6 +88,7 @@ def invoke_llm_with_structured_output( # Determine structured output strategy + use_tool_call = False if model_schema.support_structure_output: # Priority 1: Native JSON schema support model_parameters_with_json_schema = _handle_native_json_schema( @@ -97,12 +98,14 @@ def invoke_llm_with_structured_output( # Priority 2: Tool call based structured output structured_output_tool = _create_structured_output_tool(json_schema) tools = [structured_output_tool] + use_tool_call = True else: # Priority 3: Prompt-based fallback _set_response_format(model_parameters_with_json_schema, model_schema.parameter_rules) prompt_messages = _handle_prompt_based_schema( prompt_messages=prompt_messages, structured_output_schema=json_schema, + use_tool_call=use_tool_call, ) llm_result = model_instance.invoke_llm( @@ -354,28 +357,39 @@ def _set_response_format(model_parameters: dict[str, Any], rules: list[Parameter def _handle_prompt_based_schema( - prompt_messages: Sequence[PromptMessage], structured_output_schema: Mapping[str, Any] + prompt_messages: Sequence[PromptMessage], + structured_output_schema: Mapping[str, Any], + *, + use_tool_call: bool = False, ) -> list[PromptMessage]: """ - Handle structured output for models without native JSON schema support. - This function modifies the prompt messages to include schema-based output requirements. + Inject structured output instructions into the system prompt. + + When use_tool_call is True, the prompt explicitly instructs the model to call the + `structured_output` tool instead of outputting raw JSON, which significantly + improves tool-call compliance for models that otherwise tend to respond with + plain text. Args: prompt_messages: Original sequence of prompt messages + structured_output_schema: JSON schema for the expected output + use_tool_call: If True, use tool-call-specific prompt that forces the model + to invoke the structured_output tool rather than emitting JSON text. Returns: list[PromptMessage]: Updated prompt messages with structured output requirements """ - # Convert schema to string format - schema_str = json.dumps(structured_output_schema, ensure_ascii=False) + if use_tool_call: + # Tool call mode: schema is already in the tool definition, no need to duplicate + structured_output_prompt = STRUCTURED_OUTPUT_TOOL_CALL_PROMPT + else: + schema_str = json.dumps(structured_output_schema, ensure_ascii=False) + structured_output_prompt = STRUCTURED_OUTPUT_PROMPT.replace("{{schema}}", schema_str) - # Find existing system prompt with schema placeholder system_prompt = next( (prompt for prompt in prompt_messages if isinstance(prompt, SystemPromptMessage)), None, ) - structured_output_prompt = STRUCTURED_OUTPUT_PROMPT.replace("{{schema}}", schema_str) - # Prepare system prompt content system_prompt_content = ( structured_output_prompt + "\n\n" + system_prompt.content if system_prompt and isinstance(system_prompt.content, str) @@ -383,8 +397,6 @@ def _handle_prompt_based_schema( ) system_prompt = SystemPromptMessage(content=system_prompt_content) - # Extract content from the last user message - filtered_prompts = [prompt for prompt in prompt_messages if not isinstance(prompt, SystemPromptMessage)] updated_prompt = [system_prompt] + filtered_prompts diff --git a/api/core/llm_generator/prompts.py b/api/core/llm_generator/prompts.py index e2e732d3e5..3f2c3ee44b 100644 --- a/api/core/llm_generator/prompts.py +++ b/api/core/llm_generator/prompts.py @@ -323,6 +323,11 @@ Here is the JSON schema: {{schema}} """ # noqa: E501 +STRUCTURED_OUTPUT_TOOL_CALL_PROMPT = """You have access to a tool called `structured_output`. You MUST call this tool to provide your final answer. +Do NOT write JSON directly in your message. Instead, always invoke the `structured_output` tool with the appropriate arguments. +If you respond without calling the tool, your answer will be considered invalid. +""" # noqa: E501 + LLM_MODIFY_PROMPT_SYSTEM = """ Both your input and output should be in JSON format. diff --git a/api/core/workflow/nodes/llm/node.py b/api/core/workflow/nodes/llm/node.py index 0390882449..c26d95e823 100644 --- a/api/core/workflow/nodes/llm/node.py +++ b/api/core/workflow/nodes/llm/node.py @@ -30,7 +30,6 @@ from core.llm_generator.output_parser.file_ref import ( ) from core.llm_generator.output_parser.structured_output import ( invoke_llm_with_structured_output, - parse_structured_output_text, ) from core.memory.base import BaseMemory from core.model_manager import ModelInstance, ModelManager @@ -342,8 +341,6 @@ class LLMNode(Node[LLMNodeData]): stop=stop, variable_pool=variable_pool, tool_dependencies=tool_dependencies, - structured_output_schema=structured_output_schema, - structured_output_file_paths=structured_output_file_paths, ) elif self.tool_call_enabled: generator = self._invoke_llm_with_tools( @@ -568,6 +565,7 @@ class LLMNode(Node[LLMNodeData]): if not model_schema: raise ValueError(f"Model schema not found for {node_data_model.name}") + invoke_result: LLMResult | Generator[LLMResultChunk | LLMStructuredOutput, None, None] if structured_output_schema: request_start_time = time.perf_counter() @@ -1708,18 +1706,6 @@ class LLMNode(Node[LLMNodeData]): ) return saved_file - def _parse_structured_output_from_text( - self, - *, - result_text: str, - structured_output_schema: Mapping[str, Any], - ) -> dict[str, Any]: - """Parse structured output from tool-run text using the provided schema.""" - try: - return parse_structured_output_text(result_text=result_text, json_schema=structured_output_schema) - except OutputParserError as exc: - raise LLMNodeError(f"Failed to parse structured output: {exc}") from exc - @staticmethod def _normalize_sandbox_file_path(path: str) -> str: raw = path.strip() @@ -2058,9 +2044,7 @@ class LLMNode(Node[LLMNodeData]): stop: Sequence[str] | None, variable_pool: VariablePool, tool_dependencies: ToolDependencies | None, - structured_output_schema: Mapping[str, Any] | None, - structured_output_file_paths: Sequence[str] | None, - ) -> Generator[NodeEventBase | LLMStructuredOutput, None, LLMGenerationData]: + ) -> Generator[NodeEventBase, None, LLMGenerationData]: result: LLMGenerationData | None = None # FIXME(Mairuis): Async processing for bash session. @@ -2087,36 +2071,6 @@ class LLMNode(Node[LLMNodeData]): result = yield from self._process_tool_outputs(outputs) - if result is not None and structured_output_schema: - structured_output = self._parse_structured_output_from_text( - result_text=result.text, - structured_output_schema=structured_output_schema, - ) - - file_paths = list(structured_output_file_paths or []) - if file_paths: - resolved_count = 0 - - def resolve_file(path: str) -> File: - nonlocal resolved_count - if resolved_count >= MAX_OUTPUT_FILES: - raise LLMNodeError("Structured output files exceed the sandbox output limit") - resolved_count += 1 - return self._resolve_sandbox_file_path(sandbox=sandbox, path=path) - - structured_output, structured_output_files = convert_sandbox_file_paths_in_output( - output=structured_output, - file_path_fields=file_paths, - file_resolver=resolve_file, - ) - else: - structured_output_files = [] - - if structured_output_files: - result.files.extend(structured_output_files) - - yield LLMStructuredOutput(structured_output=structured_output) - if result is None: raise LLMNodeError("SandboxSession exited unexpectedly")