mirror of
https://github.com/langgenius/dify.git
synced 2026-03-29 18:09:57 +08:00
chore(api): improve structured output tool call prompt and update handling in LLMNode
This commit is contained in:
@ -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
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user