feat: add reasoning format processing to LLMNode for <think> tag handling (#23313)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
05cd7e2d8a
commit
edf4a1b652
30 changed files with 366 additions and 5 deletions
|
|
@ -156,6 +156,7 @@ class LLMResult(BaseModel):
|
||||||
message: AssistantPromptMessage
|
message: AssistantPromptMessage
|
||||||
usage: LLMUsage
|
usage: LLMUsage
|
||||||
system_fingerprint: Optional[str] = None
|
system_fingerprint: Optional[str] = None
|
||||||
|
reasoning_content: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class LLMStructuredOutput(BaseModel):
|
class LLMStructuredOutput(BaseModel):
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ class ModelInvokeCompletedEvent(BaseModel):
|
||||||
text: str
|
text: str
|
||||||
usage: LLMUsage
|
usage: LLMUsage
|
||||||
finish_reason: str | None = None
|
finish_reason: str | None = None
|
||||||
|
reasoning_content: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class RunRetryEvent(BaseModel):
|
class RunRetryEvent(BaseModel):
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from collections.abc import Mapping, Sequence
|
from collections.abc import Mapping, Sequence
|
||||||
from typing import Any, Optional
|
from typing import Any, Literal, Optional
|
||||||
|
|
||||||
from pydantic import BaseModel, Field, field_validator
|
from pydantic import BaseModel, Field, field_validator
|
||||||
|
|
||||||
|
|
@ -68,6 +68,23 @@ class LLMNodeData(BaseNodeData):
|
||||||
structured_output: Mapping[str, Any] | None = None
|
structured_output: Mapping[str, Any] | None = None
|
||||||
# We used 'structured_output_enabled' in the past, but it's not a good name.
|
# We used 'structured_output_enabled' in the past, but it's not a good name.
|
||||||
structured_output_switch_on: bool = Field(False, alias="structured_output_enabled")
|
structured_output_switch_on: bool = Field(False, alias="structured_output_enabled")
|
||||||
|
reasoning_format: Literal["separated", "tagged"] = Field(
|
||||||
|
# Keep tagged as default for backward compatibility
|
||||||
|
default="tagged",
|
||||||
|
description=(
|
||||||
|
"""
|
||||||
|
Strategy for handling model reasoning output.
|
||||||
|
|
||||||
|
separated: Return clean text (without <think> tags) + reasoning_content field.
|
||||||
|
Recommended for new workflows. Enables safe downstream parsing and
|
||||||
|
workflow variable access: {{#node_id.reasoning_content#}}
|
||||||
|
|
||||||
|
tagged : Return original text (with <think> tags) + reasoning_content field.
|
||||||
|
Maintains full backward compatibility while still providing reasoning_content
|
||||||
|
for workflow automation. Frontend thinking panels work as before.
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
@field_validator("prompt_config", mode="before")
|
@field_validator("prompt_config", mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,9 @@ import base64
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
from collections.abc import Generator, Mapping, Sequence
|
from collections.abc import Generator, Mapping, Sequence
|
||||||
from typing import TYPE_CHECKING, Any, Optional, Union
|
from typing import TYPE_CHECKING, Any, Literal, Optional, Union
|
||||||
|
|
||||||
from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity
|
from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity
|
||||||
from core.file import FileType, file_manager
|
from core.file import FileType, file_manager
|
||||||
|
|
@ -99,6 +100,9 @@ class LLMNode(BaseNode):
|
||||||
|
|
||||||
_node_data: LLMNodeData
|
_node_data: LLMNodeData
|
||||||
|
|
||||||
|
# Compiled regex for extracting <think> blocks (with compatibility for attributes)
|
||||||
|
_THINK_PATTERN = re.compile(r"<think[^>]*>(.*?)</think>", re.IGNORECASE | re.DOTALL)
|
||||||
|
|
||||||
# Instance attributes specific to LLMNode.
|
# Instance attributes specific to LLMNode.
|
||||||
# Output variable for file
|
# Output variable for file
|
||||||
_file_outputs: list["File"]
|
_file_outputs: list["File"]
|
||||||
|
|
@ -167,6 +171,7 @@ class LLMNode(BaseNode):
|
||||||
result_text = ""
|
result_text = ""
|
||||||
usage = LLMUsage.empty_usage()
|
usage = LLMUsage.empty_usage()
|
||||||
finish_reason = None
|
finish_reason = None
|
||||||
|
reasoning_content = None
|
||||||
variable_pool = self.graph_runtime_state.variable_pool
|
variable_pool = self.graph_runtime_state.variable_pool
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -256,6 +261,7 @@ class LLMNode(BaseNode):
|
||||||
file_saver=self._llm_file_saver,
|
file_saver=self._llm_file_saver,
|
||||||
file_outputs=self._file_outputs,
|
file_outputs=self._file_outputs,
|
||||||
node_id=self.node_id,
|
node_id=self.node_id,
|
||||||
|
reasoning_format=self._node_data.reasoning_format,
|
||||||
)
|
)
|
||||||
|
|
||||||
structured_output: LLMStructuredOutput | None = None
|
structured_output: LLMStructuredOutput | None = None
|
||||||
|
|
@ -264,9 +270,20 @@ class LLMNode(BaseNode):
|
||||||
if isinstance(event, RunStreamChunkEvent):
|
if isinstance(event, RunStreamChunkEvent):
|
||||||
yield event
|
yield event
|
||||||
elif isinstance(event, ModelInvokeCompletedEvent):
|
elif isinstance(event, ModelInvokeCompletedEvent):
|
||||||
|
# Raw text
|
||||||
result_text = event.text
|
result_text = event.text
|
||||||
usage = event.usage
|
usage = event.usage
|
||||||
finish_reason = event.finish_reason
|
finish_reason = event.finish_reason
|
||||||
|
reasoning_content = event.reasoning_content or ""
|
||||||
|
|
||||||
|
# For downstream nodes, determine clean text based on reasoning_format
|
||||||
|
if self._node_data.reasoning_format == "tagged":
|
||||||
|
# Keep <think> tags for backward compatibility
|
||||||
|
clean_text = result_text
|
||||||
|
else:
|
||||||
|
# Extract clean text from <think> tags
|
||||||
|
clean_text, _ = LLMNode._split_reasoning(result_text, self._node_data.reasoning_format)
|
||||||
|
|
||||||
# deduct quota
|
# deduct quota
|
||||||
llm_utils.deduct_llm_quota(tenant_id=self.tenant_id, model_instance=model_instance, usage=usage)
|
llm_utils.deduct_llm_quota(tenant_id=self.tenant_id, model_instance=model_instance, usage=usage)
|
||||||
break
|
break
|
||||||
|
|
@ -284,7 +301,12 @@ class LLMNode(BaseNode):
|
||||||
"model_name": model_config.model,
|
"model_name": model_config.model,
|
||||||
}
|
}
|
||||||
|
|
||||||
outputs = {"text": result_text, "usage": jsonable_encoder(usage), "finish_reason": finish_reason}
|
outputs = {
|
||||||
|
"text": clean_text,
|
||||||
|
"reasoning_content": reasoning_content,
|
||||||
|
"usage": jsonable_encoder(usage),
|
||||||
|
"finish_reason": finish_reason,
|
||||||
|
}
|
||||||
if structured_output:
|
if structured_output:
|
||||||
outputs["structured_output"] = structured_output.structured_output
|
outputs["structured_output"] = structured_output.structured_output
|
||||||
if self._file_outputs is not None:
|
if self._file_outputs is not None:
|
||||||
|
|
@ -338,6 +360,7 @@ class LLMNode(BaseNode):
|
||||||
file_saver: LLMFileSaver,
|
file_saver: LLMFileSaver,
|
||||||
file_outputs: list["File"],
|
file_outputs: list["File"],
|
||||||
node_id: str,
|
node_id: str,
|
||||||
|
reasoning_format: Literal["separated", "tagged"] = "tagged",
|
||||||
) -> Generator[NodeEvent | LLMStructuredOutput, None, None]:
|
) -> Generator[NodeEvent | LLMStructuredOutput, None, None]:
|
||||||
model_schema = model_instance.model_type_instance.get_model_schema(
|
model_schema = model_instance.model_type_instance.get_model_schema(
|
||||||
node_data_model.name, model_instance.credentials
|
node_data_model.name, model_instance.credentials
|
||||||
|
|
@ -374,6 +397,7 @@ class LLMNode(BaseNode):
|
||||||
file_saver=file_saver,
|
file_saver=file_saver,
|
||||||
file_outputs=file_outputs,
|
file_outputs=file_outputs,
|
||||||
node_id=node_id,
|
node_id=node_id,
|
||||||
|
reasoning_format=reasoning_format,
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
@ -383,6 +407,7 @@ class LLMNode(BaseNode):
|
||||||
file_saver: LLMFileSaver,
|
file_saver: LLMFileSaver,
|
||||||
file_outputs: list["File"],
|
file_outputs: list["File"],
|
||||||
node_id: str,
|
node_id: str,
|
||||||
|
reasoning_format: Literal["separated", "tagged"] = "tagged",
|
||||||
) -> Generator[NodeEvent | LLMStructuredOutput, None, None]:
|
) -> Generator[NodeEvent | LLMStructuredOutput, None, None]:
|
||||||
# For blocking mode
|
# For blocking mode
|
||||||
if isinstance(invoke_result, LLMResult):
|
if isinstance(invoke_result, LLMResult):
|
||||||
|
|
@ -390,6 +415,7 @@ class LLMNode(BaseNode):
|
||||||
invoke_result=invoke_result,
|
invoke_result=invoke_result,
|
||||||
saver=file_saver,
|
saver=file_saver,
|
||||||
file_outputs=file_outputs,
|
file_outputs=file_outputs,
|
||||||
|
reasoning_format=reasoning_format,
|
||||||
)
|
)
|
||||||
yield event
|
yield event
|
||||||
return
|
return
|
||||||
|
|
@ -430,13 +456,66 @@ class LLMNode(BaseNode):
|
||||||
except OutputParserError as e:
|
except OutputParserError as e:
|
||||||
raise LLMNodeError(f"Failed to parse structured output: {e}")
|
raise LLMNodeError(f"Failed to parse structured output: {e}")
|
||||||
|
|
||||||
yield ModelInvokeCompletedEvent(text=full_text_buffer.getvalue(), usage=usage, finish_reason=finish_reason)
|
# Extract reasoning content from <think> tags in the main text
|
||||||
|
full_text = full_text_buffer.getvalue()
|
||||||
|
|
||||||
|
if reasoning_format == "tagged":
|
||||||
|
# Keep <think> tags in text for backward compatibility
|
||||||
|
clean_text = full_text
|
||||||
|
reasoning_content = ""
|
||||||
|
else:
|
||||||
|
# Extract clean text and reasoning from <think> tags
|
||||||
|
clean_text, reasoning_content = LLMNode._split_reasoning(full_text, reasoning_format)
|
||||||
|
|
||||||
|
yield ModelInvokeCompletedEvent(
|
||||||
|
# Use clean_text for separated mode, full_text for tagged mode
|
||||||
|
text=clean_text if reasoning_format == "separated" else full_text,
|
||||||
|
usage=usage,
|
||||||
|
finish_reason=finish_reason,
|
||||||
|
# Reasoning content for workflow variables and downstream nodes
|
||||||
|
reasoning_content=reasoning_content,
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _image_file_to_markdown(file: "File", /):
|
def _image_file_to_markdown(file: "File", /):
|
||||||
text_chunk = f"})"
|
text_chunk = f"})"
|
||||||
return text_chunk
|
return text_chunk
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _split_reasoning(
|
||||||
|
cls, text: str, reasoning_format: Literal["separated", "tagged"] = "tagged"
|
||||||
|
) -> tuple[str, str]:
|
||||||
|
"""
|
||||||
|
Split reasoning content from text based on reasoning_format strategy.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: Full text that may contain <think> blocks
|
||||||
|
reasoning_format: Strategy for handling reasoning content
|
||||||
|
- "separated": Remove <think> tags and return clean text + reasoning_content field
|
||||||
|
- "tagged": Keep <think> tags in text, return empty reasoning_content
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple of (clean_text, reasoning_content)
|
||||||
|
"""
|
||||||
|
|
||||||
|
if reasoning_format == "tagged":
|
||||||
|
return text, ""
|
||||||
|
|
||||||
|
# Find all <think>...</think> blocks (case-insensitive)
|
||||||
|
matches = cls._THINK_PATTERN.findall(text)
|
||||||
|
|
||||||
|
# Extract reasoning content from all <think> blocks
|
||||||
|
reasoning_content = "\n".join(match.strip() for match in matches) if matches else ""
|
||||||
|
|
||||||
|
# Remove all <think>...</think> blocks from original text
|
||||||
|
clean_text = cls._THINK_PATTERN.sub("", text)
|
||||||
|
|
||||||
|
# Clean up extra whitespace
|
||||||
|
clean_text = re.sub(r"\n\s*\n", "\n\n", clean_text).strip()
|
||||||
|
|
||||||
|
# Separated mode: always return clean text and reasoning_content
|
||||||
|
return clean_text, reasoning_content or ""
|
||||||
|
|
||||||
def _transform_chat_messages(
|
def _transform_chat_messages(
|
||||||
self, messages: Sequence[LLMNodeChatModelMessage] | LLMNodeCompletionModelPromptTemplate, /
|
self, messages: Sequence[LLMNodeChatModelMessage] | LLMNodeCompletionModelPromptTemplate, /
|
||||||
) -> Sequence[LLMNodeChatModelMessage] | LLMNodeCompletionModelPromptTemplate:
|
) -> Sequence[LLMNodeChatModelMessage] | LLMNodeCompletionModelPromptTemplate:
|
||||||
|
|
@ -964,6 +1043,7 @@ class LLMNode(BaseNode):
|
||||||
invoke_result: LLMResult,
|
invoke_result: LLMResult,
|
||||||
saver: LLMFileSaver,
|
saver: LLMFileSaver,
|
||||||
file_outputs: list["File"],
|
file_outputs: list["File"],
|
||||||
|
reasoning_format: Literal["separated", "tagged"] = "tagged",
|
||||||
) -> ModelInvokeCompletedEvent:
|
) -> ModelInvokeCompletedEvent:
|
||||||
buffer = io.StringIO()
|
buffer = io.StringIO()
|
||||||
for text_part in LLMNode._save_multimodal_output_and_convert_result_to_markdown(
|
for text_part in LLMNode._save_multimodal_output_and_convert_result_to_markdown(
|
||||||
|
|
@ -973,10 +1053,24 @@ class LLMNode(BaseNode):
|
||||||
):
|
):
|
||||||
buffer.write(text_part)
|
buffer.write(text_part)
|
||||||
|
|
||||||
|
# Extract reasoning content from <think> tags in the main text
|
||||||
|
full_text = buffer.getvalue()
|
||||||
|
|
||||||
|
if reasoning_format == "tagged":
|
||||||
|
# Keep <think> tags in text for backward compatibility
|
||||||
|
clean_text = full_text
|
||||||
|
reasoning_content = ""
|
||||||
|
else:
|
||||||
|
# Extract clean text and reasoning from <think> tags
|
||||||
|
clean_text, reasoning_content = LLMNode._split_reasoning(full_text, reasoning_format)
|
||||||
|
|
||||||
return ModelInvokeCompletedEvent(
|
return ModelInvokeCompletedEvent(
|
||||||
text=buffer.getvalue(),
|
# Use clean_text for separated mode, full_text for tagged mode
|
||||||
|
text=clean_text if reasoning_format == "separated" else full_text,
|
||||||
usage=invoke_result.usage,
|
usage=invoke_result.usage,
|
||||||
finish_reason=None,
|
finish_reason=None,
|
||||||
|
# Reasoning content for workflow variables and downstream nodes
|
||||||
|
reasoning_content=reasoning_content,
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@ def llm_node_data() -> LLMNodeData:
|
||||||
detail=ImagePromptMessageContent.DETAIL.HIGH,
|
detail=ImagePromptMessageContent.DETAIL.HIGH,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
reasoning_format="tagged",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -689,3 +690,66 @@ class TestSaveMultimodalOutputAndConvertResultToMarkdown:
|
||||||
assert list(gen) == []
|
assert list(gen) == []
|
||||||
mock_file_saver.save_binary_string.assert_not_called()
|
mock_file_saver.save_binary_string.assert_not_called()
|
||||||
mock_file_saver.save_remote_url.assert_not_called()
|
mock_file_saver.save_remote_url.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
class TestReasoningFormat:
|
||||||
|
"""Test cases for reasoning_format functionality"""
|
||||||
|
|
||||||
|
def test_split_reasoning_separated_mode(self):
|
||||||
|
"""Test separated mode: tags are removed and content is extracted"""
|
||||||
|
|
||||||
|
text_with_think = """
|
||||||
|
<think>I need to explain what Dify is. It's an open source AI platform.
|
||||||
|
</think>Dify is an open source AI platform.
|
||||||
|
"""
|
||||||
|
|
||||||
|
clean_text, reasoning_content = LLMNode._split_reasoning(text_with_think, "separated")
|
||||||
|
|
||||||
|
assert clean_text == "Dify is an open source AI platform."
|
||||||
|
assert reasoning_content == "I need to explain what Dify is. It's an open source AI platform."
|
||||||
|
|
||||||
|
def test_split_reasoning_tagged_mode(self):
|
||||||
|
"""Test tagged mode: original text is preserved"""
|
||||||
|
|
||||||
|
text_with_think = """
|
||||||
|
<think>I need to explain what Dify is. It's an open source AI platform.
|
||||||
|
</think>Dify is an open source AI platform.
|
||||||
|
"""
|
||||||
|
|
||||||
|
clean_text, reasoning_content = LLMNode._split_reasoning(text_with_think, "tagged")
|
||||||
|
|
||||||
|
# Original text unchanged
|
||||||
|
assert clean_text == text_with_think
|
||||||
|
# Empty reasoning content in tagged mode
|
||||||
|
assert reasoning_content == ""
|
||||||
|
|
||||||
|
def test_split_reasoning_no_think_blocks(self):
|
||||||
|
"""Test behavior when no <think> tags are present"""
|
||||||
|
|
||||||
|
text_without_think = "This is a simple answer without any thinking blocks."
|
||||||
|
|
||||||
|
clean_text, reasoning_content = LLMNode._split_reasoning(text_without_think, "separated")
|
||||||
|
|
||||||
|
assert clean_text == text_without_think
|
||||||
|
assert reasoning_content == ""
|
||||||
|
|
||||||
|
def test_reasoning_format_default_value(self):
|
||||||
|
"""Test that reasoning_format defaults to 'tagged' for backward compatibility"""
|
||||||
|
|
||||||
|
node_data = LLMNodeData(
|
||||||
|
title="Test LLM",
|
||||||
|
model=ModelConfig(provider="openai", name="gpt-3.5-turbo", mode="chat", completion_params={}),
|
||||||
|
prompt_template=[],
|
||||||
|
context=ContextConfig(enabled=False),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert node_data.reasoning_format == "tagged"
|
||||||
|
|
||||||
|
text_with_think = """
|
||||||
|
<think>I need to explain what Dify is. It's an open source AI platform.
|
||||||
|
</think>Dify is an open source AI platform.
|
||||||
|
"""
|
||||||
|
clean_text, reasoning_content = LLMNode._split_reasoning(text_with_think, node_data.reasoning_format)
|
||||||
|
|
||||||
|
assert clean_text == text_with_think
|
||||||
|
assert reasoning_content == ""
|
||||||
|
|
|
||||||
|
|
@ -479,6 +479,10 @@ export const LLM_OUTPUT_STRUCT: Var[] = [
|
||||||
variable: 'text',
|
variable: 'text',
|
||||||
type: VarType.string,
|
type: VarType.string,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
variable: 'reasoning_content',
|
||||||
|
type: VarType.string,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
variable: 'usage',
|
variable: 'usage',
|
||||||
type: VarType.object,
|
type: VarType.object,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import React from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||||
|
import Switch from '@/app/components/base/switch'
|
||||||
|
|
||||||
|
type ReasoningFormatConfigProps = {
|
||||||
|
value?: 'tagged' | 'separated'
|
||||||
|
onChange: (value: 'tagged' | 'separated') => void
|
||||||
|
readonly?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReasoningFormatConfig: FC<ReasoningFormatConfigProps> = ({
|
||||||
|
value = 'tagged',
|
||||||
|
onChange,
|
||||||
|
readonly = false,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
title={t('workflow.nodes.llm.reasoningFormat.title')}
|
||||||
|
tooltip={t('workflow.nodes.llm.reasoningFormat.tooltip')}
|
||||||
|
operations={
|
||||||
|
// ON = separated, OFF = tagged
|
||||||
|
<Switch
|
||||||
|
defaultValue={value === 'separated'}
|
||||||
|
onChange={enabled => onChange(enabled ? 'separated' : 'tagged')}
|
||||||
|
size='md'
|
||||||
|
disabled={readonly}
|
||||||
|
key={value}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div />
|
||||||
|
</Field>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ReasoningFormatConfig
|
||||||
|
|
@ -17,6 +17,7 @@ import type { NodePanelProps } from '@/app/components/workflow/types'
|
||||||
import Tooltip from '@/app/components/base/tooltip'
|
import Tooltip from '@/app/components/base/tooltip'
|
||||||
import Editor from '@/app/components/workflow/nodes/_base/components/prompt/editor'
|
import Editor from '@/app/components/workflow/nodes/_base/components/prompt/editor'
|
||||||
import StructureOutput from './components/structure-output'
|
import StructureOutput from './components/structure-output'
|
||||||
|
import ReasoningFormatConfig from './components/reasoning-format-config'
|
||||||
import Switch from '@/app/components/base/switch'
|
import Switch from '@/app/components/base/switch'
|
||||||
import { RiAlertFill, RiQuestionLine } from '@remixicon/react'
|
import { RiAlertFill, RiQuestionLine } from '@remixicon/react'
|
||||||
import { fetchAndMergeValidCompletionParams } from '@/utils/completion-params'
|
import { fetchAndMergeValidCompletionParams } from '@/utils/completion-params'
|
||||||
|
|
@ -61,6 +62,7 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
|
||||||
handleStructureOutputEnableChange,
|
handleStructureOutputEnableChange,
|
||||||
handleStructureOutputChange,
|
handleStructureOutputChange,
|
||||||
filterJinja2InputVar,
|
filterJinja2InputVar,
|
||||||
|
handleReasoningFormatChange,
|
||||||
} = useConfig(id, data)
|
} = useConfig(id, data)
|
||||||
|
|
||||||
const model = inputs.model
|
const model = inputs.model
|
||||||
|
|
@ -239,6 +241,14 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
|
||||||
config={inputs.vision?.configs}
|
config={inputs.vision?.configs}
|
||||||
onConfigChange={handleVisionResolutionChange}
|
onConfigChange={handleVisionResolutionChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Reasoning Format */}
|
||||||
|
<ReasoningFormatConfig
|
||||||
|
// Default to tagged for backward compatibility
|
||||||
|
value={inputs.reasoning_format || 'tagged'}
|
||||||
|
onChange={handleReasoningFormatChange}
|
||||||
|
readonly={readOnly}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Split />
|
<Split />
|
||||||
<OutputVars
|
<OutputVars
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ export type LLMNodeType = CommonNodeType & {
|
||||||
}
|
}
|
||||||
structured_output_enabled?: boolean
|
structured_output_enabled?: boolean
|
||||||
structured_output?: StructuredOutput
|
structured_output?: StructuredOutput
|
||||||
|
reasoning_format?: 'tagged' | 'separated'
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Type {
|
export enum Type {
|
||||||
|
|
|
||||||
|
|
@ -315,6 +315,14 @@ const useConfig = (id: string, payload: LLMNodeType) => {
|
||||||
return [VarType.arrayObject, VarType.array, VarType.number, VarType.string, VarType.secret, VarType.arrayString, VarType.arrayNumber, VarType.file, VarType.arrayFile].includes(varPayload.type)
|
return [VarType.arrayObject, VarType.array, VarType.number, VarType.string, VarType.secret, VarType.arrayString, VarType.arrayNumber, VarType.file, VarType.arrayFile].includes(varPayload.type)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// reasoning format
|
||||||
|
const handleReasoningFormatChange = useCallback((reasoningFormat: 'tagged' | 'separated') => {
|
||||||
|
const newInputs = produce(inputs, (draft) => {
|
||||||
|
draft.reasoning_format = reasoningFormat
|
||||||
|
})
|
||||||
|
setInputs(newInputs)
|
||||||
|
}, [inputs, setInputs])
|
||||||
|
|
||||||
const {
|
const {
|
||||||
availableVars,
|
availableVars,
|
||||||
availableNodesWithParent,
|
availableNodesWithParent,
|
||||||
|
|
@ -355,6 +363,7 @@ const useConfig = (id: string, payload: LLMNodeType) => {
|
||||||
setStructuredOutputCollapsed,
|
setStructuredOutputCollapsed,
|
||||||
handleStructureOutputEnableChange,
|
handleStructureOutputEnableChange,
|
||||||
filterJinja2InputVar,
|
filterJinja2InputVar,
|
||||||
|
handleReasoningFormatChange,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -470,6 +470,12 @@ const translation = {
|
||||||
instruction: 'Anleitung',
|
instruction: 'Anleitung',
|
||||||
regenerate: 'Regenerieren',
|
regenerate: 'Regenerieren',
|
||||||
},
|
},
|
||||||
|
reasoningFormat: {
|
||||||
|
tooltip: 'Inhalte aus Denk-Tags extrahieren und im Feld reasoning_content speichern.',
|
||||||
|
separated: 'Separate Denk tags',
|
||||||
|
title: 'Aktivieren Sie die Trennung von Argumentations-Tags',
|
||||||
|
tagged: 'Behalte die Denk-Tags',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
knowledgeRetrieval: {
|
knowledgeRetrieval: {
|
||||||
queryVariable: 'Abfragevariable',
|
queryVariable: 'Abfragevariable',
|
||||||
|
|
|
||||||
|
|
@ -449,6 +449,12 @@ const translation = {
|
||||||
variable: 'Variable',
|
variable: 'Variable',
|
||||||
},
|
},
|
||||||
sysQueryInUser: 'sys.query in user message is required',
|
sysQueryInUser: 'sys.query in user message is required',
|
||||||
|
reasoningFormat: {
|
||||||
|
title: 'Enable reasoning tag separation',
|
||||||
|
tagged: 'Keep think tags',
|
||||||
|
separated: 'Separate think tags',
|
||||||
|
tooltip: 'Extract content from think tags and store it in the reasoning_content field.',
|
||||||
|
},
|
||||||
jsonSchema: {
|
jsonSchema: {
|
||||||
title: 'Structured Output Schema',
|
title: 'Structured Output Schema',
|
||||||
instruction: 'Instruction',
|
instruction: 'Instruction',
|
||||||
|
|
|
||||||
|
|
@ -470,6 +470,12 @@ const translation = {
|
||||||
import: 'Importar desde JSON',
|
import: 'Importar desde JSON',
|
||||||
resetDefaults: 'Restablecer',
|
resetDefaults: 'Restablecer',
|
||||||
},
|
},
|
||||||
|
reasoningFormat: {
|
||||||
|
tagged: 'Mantén las etiquetas de pensamiento',
|
||||||
|
separated: 'Separar etiquetas de pensamiento',
|
||||||
|
title: 'Habilitar la separación de etiquetas de razonamiento',
|
||||||
|
tooltip: 'Extraer contenido de las etiquetas de pensamiento y almacenarlo en el campo reasoning_content.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
knowledgeRetrieval: {
|
knowledgeRetrieval: {
|
||||||
queryVariable: 'Variable de consulta',
|
queryVariable: 'Variable de consulta',
|
||||||
|
|
|
||||||
|
|
@ -470,6 +470,12 @@ const translation = {
|
||||||
fieldNamePlaceholder: 'نام میدان',
|
fieldNamePlaceholder: 'نام میدان',
|
||||||
generationTip: 'شما میتوانید از زبان طبیعی برای ایجاد سریع یک طرحواره JSON استفاده کنید.',
|
generationTip: 'شما میتوانید از زبان طبیعی برای ایجاد سریع یک طرحواره JSON استفاده کنید.',
|
||||||
},
|
},
|
||||||
|
reasoningFormat: {
|
||||||
|
separated: 'تگهای تفکر جداگانه',
|
||||||
|
title: 'فعالسازی جداسازی برچسبهای استدلال',
|
||||||
|
tagged: 'به فکر برچسبها باشید',
|
||||||
|
tooltip: 'محتوا را از تگهای تفکر استخراج کرده و در فیلد reasoning_content ذخیره کنید.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
knowledgeRetrieval: {
|
knowledgeRetrieval: {
|
||||||
queryVariable: 'متغیر جستجو',
|
queryVariable: 'متغیر جستجو',
|
||||||
|
|
|
||||||
|
|
@ -470,6 +470,12 @@ const translation = {
|
||||||
generateJsonSchema: 'Générer un schéma JSON',
|
generateJsonSchema: 'Générer un schéma JSON',
|
||||||
resultTip: 'Voici le résultat généré. Si vous n\'êtes pas satisfait, vous pouvez revenir en arrière et modifier votre demande.',
|
resultTip: 'Voici le résultat généré. Si vous n\'êtes pas satisfait, vous pouvez revenir en arrière et modifier votre demande.',
|
||||||
},
|
},
|
||||||
|
reasoningFormat: {
|
||||||
|
title: 'Activer la séparation des balises de raisonnement',
|
||||||
|
tagged: 'Gardez les étiquettes de pensée',
|
||||||
|
separated: 'Séparer les balises de réflexion',
|
||||||
|
tooltip: 'Extraire le contenu des balises think et le stocker dans le champ reasoning_content.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
knowledgeRetrieval: {
|
knowledgeRetrieval: {
|
||||||
queryVariable: 'Variable de requête',
|
queryVariable: 'Variable de requête',
|
||||||
|
|
|
||||||
|
|
@ -483,6 +483,12 @@ const translation = {
|
||||||
required: 'आवश्यक',
|
required: 'आवश्यक',
|
||||||
addChildField: 'बच्चे का क्षेत्र जोड़ें',
|
addChildField: 'बच्चे का क्षेत्र जोड़ें',
|
||||||
},
|
},
|
||||||
|
reasoningFormat: {
|
||||||
|
title: 'कारण संबंध टैग विभाजन सक्षम करें',
|
||||||
|
separated: 'अलग सोच टैग',
|
||||||
|
tagged: 'टैग्स के बारे में सोचते रहें',
|
||||||
|
tooltip: 'थिंक टैग से सामग्री निकाले और इसे reasoning_content क्षेत्र में संग्रहित करें।',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
knowledgeRetrieval: {
|
knowledgeRetrieval: {
|
||||||
queryVariable: 'प्रश्न वेरिएबल',
|
queryVariable: 'प्रश्न वेरिएबल',
|
||||||
|
|
|
||||||
|
|
@ -487,6 +487,12 @@ const translation = {
|
||||||
generating: 'Generazione dello schema JSON...',
|
generating: 'Generazione dello schema JSON...',
|
||||||
generatedResult: 'Risultato generato',
|
generatedResult: 'Risultato generato',
|
||||||
},
|
},
|
||||||
|
reasoningFormat: {
|
||||||
|
title: 'Abilita la separazione dei tag di ragionamento',
|
||||||
|
tagged: 'Continua a pensare ai tag',
|
||||||
|
separated: 'Tag di pensiero separati',
|
||||||
|
tooltip: 'Estrai il contenuto dai tag think e conservalo nel campo reasoning_content.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
knowledgeRetrieval: {
|
knowledgeRetrieval: {
|
||||||
queryVariable: 'Variabile Query',
|
queryVariable: 'Variabile Query',
|
||||||
|
|
|
||||||
|
|
@ -477,6 +477,12 @@ const translation = {
|
||||||
saveSchema: '編集中のフィールドを確定してから保存してください。',
|
saveSchema: '編集中のフィールドを確定してから保存してください。',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
reasoningFormat: {
|
||||||
|
tagged: 'タグを考え続けてください',
|
||||||
|
separated: '思考タグを分ける',
|
||||||
|
title: '推論タグの分離を有効にする',
|
||||||
|
tooltip: 'thinkタグから内容を抽出し、それをreasoning_contentフィールドに保存します。',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
knowledgeRetrieval: {
|
knowledgeRetrieval: {
|
||||||
queryVariable: '検索変数',
|
queryVariable: '検索変数',
|
||||||
|
|
|
||||||
|
|
@ -497,6 +497,12 @@ const translation = {
|
||||||
doc: '구조화된 출력에 대해 더 알아보세요.',
|
doc: '구조화된 출력에 대해 더 알아보세요.',
|
||||||
import: 'JSON 에서 가져오기',
|
import: 'JSON 에서 가져오기',
|
||||||
},
|
},
|
||||||
|
reasoningFormat: {
|
||||||
|
title: '추론 태그 분리 활성화',
|
||||||
|
separated: '추론 태그 분리',
|
||||||
|
tooltip: '추론 태그에서 내용을 추출하고 이를 reasoning_content 필드에 저장합니다',
|
||||||
|
tagged: '추론 태그 유지',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
knowledgeRetrieval: {
|
knowledgeRetrieval: {
|
||||||
queryVariable: '쿼리 변수',
|
queryVariable: '쿼리 변수',
|
||||||
|
|
|
||||||
|
|
@ -470,6 +470,12 @@ const translation = {
|
||||||
back: 'Tył',
|
back: 'Tył',
|
||||||
addField: 'Dodaj pole',
|
addField: 'Dodaj pole',
|
||||||
},
|
},
|
||||||
|
reasoningFormat: {
|
||||||
|
tooltip: 'Wyodrębnij treść z tagów think i przechowaj ją w polu reasoning_content.',
|
||||||
|
separated: 'Oddziel tagi myślenia',
|
||||||
|
tagged: 'Zachowaj myśl tagi',
|
||||||
|
title: 'Włącz separację tagów uzasadnienia',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
knowledgeRetrieval: {
|
knowledgeRetrieval: {
|
||||||
queryVariable: 'Zmienna zapytania',
|
queryVariable: 'Zmienna zapytania',
|
||||||
|
|
|
||||||
|
|
@ -470,6 +470,12 @@ const translation = {
|
||||||
apply: 'Aplicar',
|
apply: 'Aplicar',
|
||||||
required: 'obrigatório',
|
required: 'obrigatório',
|
||||||
},
|
},
|
||||||
|
reasoningFormat: {
|
||||||
|
tagged: 'Mantenha as tags de pensamento',
|
||||||
|
title: 'Ativar separação de tags de raciocínio',
|
||||||
|
separated: 'Separe as tags de pensamento',
|
||||||
|
tooltip: 'Extraia o conteúdo das tags de pensamento e armazene-o no campo reasoning_content.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
knowledgeRetrieval: {
|
knowledgeRetrieval: {
|
||||||
queryVariable: 'Variável de consulta',
|
queryVariable: 'Variável de consulta',
|
||||||
|
|
|
||||||
|
|
@ -470,6 +470,12 @@ const translation = {
|
||||||
back: 'Înapoi',
|
back: 'Înapoi',
|
||||||
promptPlaceholder: 'Descrie schema ta JSON...',
|
promptPlaceholder: 'Descrie schema ta JSON...',
|
||||||
},
|
},
|
||||||
|
reasoningFormat: {
|
||||||
|
tagged: 'Ține minte etichetele',
|
||||||
|
separated: 'Etichete de gândire separate',
|
||||||
|
title: 'Activează separarea etichetelor de raționare',
|
||||||
|
tooltip: 'Extrage conținutul din etichetele think și stochează-l în câmpul reasoning_content.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
knowledgeRetrieval: {
|
knowledgeRetrieval: {
|
||||||
queryVariable: 'Variabilă de interogare',
|
queryVariable: 'Variabilă de interogare',
|
||||||
|
|
|
||||||
|
|
@ -470,6 +470,12 @@ const translation = {
|
||||||
generating: 'Генерация схемы JSON...',
|
generating: 'Генерация схемы JSON...',
|
||||||
promptTooltip: 'Преобразуйте текстовое описание в стандартизированную структуру JSON Schema.',
|
promptTooltip: 'Преобразуйте текстовое описание в стандартизированную структуру JSON Schema.',
|
||||||
},
|
},
|
||||||
|
reasoningFormat: {
|
||||||
|
tagged: 'Продолжайте думать о тегах',
|
||||||
|
title: 'Включите разделение тегов на основе логики',
|
||||||
|
tooltip: 'Извлечь содержимое из тегов think и сохранить его в поле reasoning_content.',
|
||||||
|
separated: 'Отдельные теги для мышления',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
knowledgeRetrieval: {
|
knowledgeRetrieval: {
|
||||||
queryVariable: 'Переменная запроса',
|
queryVariable: 'Переменная запроса',
|
||||||
|
|
|
||||||
|
|
@ -477,6 +477,12 @@ const translation = {
|
||||||
context: 'kontekst',
|
context: 'kontekst',
|
||||||
addMessage: 'Dodaj sporočilo',
|
addMessage: 'Dodaj sporočilo',
|
||||||
vision: 'vizija',
|
vision: 'vizija',
|
||||||
|
reasoningFormat: {
|
||||||
|
tagged: 'Ohranite oznake za razmišljanje',
|
||||||
|
title: 'Omogoči ločevanje oznak za razsojanje',
|
||||||
|
tooltip: 'Izvleći vsebino iz miselnih oznak in jo shraniti v polje reasoning_content.',
|
||||||
|
separated: 'Ločite oznake za razmišljanje',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
knowledgeRetrieval: {
|
knowledgeRetrieval: {
|
||||||
outputVars: {
|
outputVars: {
|
||||||
|
|
|
||||||
|
|
@ -470,6 +470,12 @@ const translation = {
|
||||||
stringValidations: 'การตรวจสอบสตริง',
|
stringValidations: 'การตรวจสอบสตริง',
|
||||||
required: 'จำเป็นต้องใช้',
|
required: 'จำเป็นต้องใช้',
|
||||||
},
|
},
|
||||||
|
reasoningFormat: {
|
||||||
|
tagged: 'รักษาความคิดเกี่ยวกับแท็ก',
|
||||||
|
separated: 'แยกแท็กความคิดเห็น',
|
||||||
|
tooltip: 'ดึงเนื้อหาจากแท็กคิดและเก็บไว้ในฟิลด์ reasoning_content.',
|
||||||
|
title: 'เปิดใช้งานการแยกแท็กการเหตุผล',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
knowledgeRetrieval: {
|
knowledgeRetrieval: {
|
||||||
queryVariable: 'ตัวแปรแบบสอบถาม',
|
queryVariable: 'ตัวแปรแบบสอบถาม',
|
||||||
|
|
|
||||||
|
|
@ -470,6 +470,12 @@ const translation = {
|
||||||
addChildField: 'Çocuk Alanı Ekle',
|
addChildField: 'Çocuk Alanı Ekle',
|
||||||
resultTip: 'İşte oluşturulan sonuç. Eğer memnun değilseniz, geri dönüp isteminizi değiştirebilirsiniz.',
|
resultTip: 'İşte oluşturulan sonuç. Eğer memnun değilseniz, geri dönüp isteminizi değiştirebilirsiniz.',
|
||||||
},
|
},
|
||||||
|
reasoningFormat: {
|
||||||
|
separated: 'Ayrı düşünce etiketleri',
|
||||||
|
title: 'Akıl yürütme etiket ayrımını etkinleştir',
|
||||||
|
tagged: 'Etiketleri düşünmeye devam et',
|
||||||
|
tooltip: 'Düşünce etiketlerinden içeriği çıkarın ve bunu reasoning_content alanında saklayın.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
knowledgeRetrieval: {
|
knowledgeRetrieval: {
|
||||||
queryVariable: 'Sorgu Değişkeni',
|
queryVariable: 'Sorgu Değişkeni',
|
||||||
|
|
|
||||||
|
|
@ -470,6 +470,12 @@ const translation = {
|
||||||
title: 'Структурована схема виходу',
|
title: 'Структурована схема виходу',
|
||||||
doc: 'Дізнайтеся більше про структурований вихід',
|
doc: 'Дізнайтеся більше про структурований вихід',
|
||||||
},
|
},
|
||||||
|
reasoningFormat: {
|
||||||
|
separated: 'Окремі теги для думок',
|
||||||
|
tagged: 'Продовжуйте думати про мітки',
|
||||||
|
title: 'Увімкніть розділення тегів для міркування',
|
||||||
|
tooltip: 'Витягніть вміст з тегів think і зберігайте його в полі reasoning_content.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
knowledgeRetrieval: {
|
knowledgeRetrieval: {
|
||||||
queryVariable: 'Змінна запиту',
|
queryVariable: 'Змінна запиту',
|
||||||
|
|
|
||||||
|
|
@ -470,6 +470,12 @@ const translation = {
|
||||||
addChildField: 'Thêm trường trẻ em',
|
addChildField: 'Thêm trường trẻ em',
|
||||||
title: 'Sơ đồ đầu ra có cấu trúc',
|
title: 'Sơ đồ đầu ra có cấu trúc',
|
||||||
},
|
},
|
||||||
|
reasoningFormat: {
|
||||||
|
tagged: 'Giữ lại thẻ suy nghĩ',
|
||||||
|
tooltip: 'Trích xuất nội dung từ các thẻ think và lưu nó vào trường reasoning_content.',
|
||||||
|
separated: 'Tách biệt các thẻ suy nghĩ',
|
||||||
|
title: 'Bật chế độ phân tách nhãn lý luận',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
knowledgeRetrieval: {
|
knowledgeRetrieval: {
|
||||||
queryVariable: 'Biến truy vấn',
|
queryVariable: 'Biến truy vấn',
|
||||||
|
|
|
||||||
|
|
@ -477,6 +477,12 @@ const translation = {
|
||||||
saveSchema: '请先完成当前字段的编辑',
|
saveSchema: '请先完成当前字段的编辑',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
reasoningFormat: {
|
||||||
|
tooltip: '从think标签中提取内容,并将其存储在reasoning_content字段中。',
|
||||||
|
title: '启用推理标签分离',
|
||||||
|
tagged: '保持思考标签',
|
||||||
|
separated: '分开思考标签',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
knowledgeRetrieval: {
|
knowledgeRetrieval: {
|
||||||
queryVariable: '查询变量',
|
queryVariable: '查询变量',
|
||||||
|
|
|
||||||
|
|
@ -470,6 +470,12 @@ const translation = {
|
||||||
required: '必需的',
|
required: '必需的',
|
||||||
resultTip: '這是生成的結果。如果您不滿意,可以回去修改您的提示。',
|
resultTip: '這是生成的結果。如果您不滿意,可以回去修改您的提示。',
|
||||||
},
|
},
|
||||||
|
reasoningFormat: {
|
||||||
|
title: '啟用推理標籤分離',
|
||||||
|
tooltip: '從 think 標籤中提取內容並將其存儲在 reasoning_content 欄位中。',
|
||||||
|
tagged: '保持思考標籤',
|
||||||
|
separated: '分開思考標籤',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
knowledgeRetrieval: {
|
knowledgeRetrieval: {
|
||||||
queryVariable: '查詢變量',
|
queryVariable: '查詢變量',
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue