mirror of
https://github.com/langgenius/dify.git
synced 2026-06-09 09:57:32 +08:00
Compare commits
13 Commits
zhsama/mul
...
feat/pull-
| Author | SHA1 | Date | |
|---|---|---|---|
| ea37904c75 | |||
| d69e7eb12a | |||
| c44aaf1883 | |||
| 4b91969d0f | |||
| 92c54d3c9d | |||
| 267de1861d | |||
| 5e49b27dba | |||
| 6f74a66c8a | |||
| 68fd7c021c | |||
| e1e64ae430 | |||
| 6e9a5139b4 | |||
| f44305af0d | |||
| 1bdc47220b |
@ -6,6 +6,11 @@ from typing import Any, Protocol, cast
|
||||
|
||||
import json_repair
|
||||
|
||||
from core.llm_generator.output_models import (
|
||||
CodeNodeStructuredOutput,
|
||||
InstructionModifyOutput,
|
||||
SuggestedQuestionsOutput,
|
||||
)
|
||||
from core.llm_generator.output_parser.rule_config_generator import RuleConfigGeneratorOutputParser
|
||||
from core.llm_generator.output_parser.suggested_questions_after_answer import SuggestedQuestionsAfterAnswerOutputParser
|
||||
from core.llm_generator.prompts import (
|
||||
@ -470,7 +475,7 @@ class LLMGenerator:
|
||||
*prompt_messages,
|
||||
]
|
||||
|
||||
from core.llm_generator.output_parser.structured_output import invoke_llm_with_structured_output
|
||||
from core.llm_generator.output_parser.structured_output import invoke_llm_with_pydantic_model
|
||||
|
||||
# Get model instance and schema
|
||||
provider = model_config.get("provider", "")
|
||||
@ -487,15 +492,13 @@ class LLMGenerator:
|
||||
return cls._error_response(f"Model schema not found for {model_name}")
|
||||
|
||||
model_parameters = model_config.get("completion_params", {})
|
||||
json_schema = cls._get_code_node_json_schema()
|
||||
|
||||
try:
|
||||
response = invoke_llm_with_structured_output(
|
||||
response = invoke_llm_with_pydantic_model(
|
||||
provider=provider,
|
||||
model_schema=model_schema,
|
||||
model_instance=model_instance,
|
||||
prompt_messages=complete_messages,
|
||||
json_schema=json_schema,
|
||||
output_model=CodeNodeStructuredOutput,
|
||||
model_parameters=model_parameters,
|
||||
stream=False,
|
||||
tenant_id=tenant_id,
|
||||
@ -541,7 +544,7 @@ class LLMGenerator:
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from core.llm_generator.output_parser.structured_output import invoke_llm_with_structured_output
|
||||
from core.llm_generator.output_parser.structured_output import invoke_llm_with_pydantic_model
|
||||
from services.workflow_service import WorkflowService
|
||||
|
||||
# Get workflow context (reuse existing logic)
|
||||
@ -602,15 +605,13 @@ class LLMGenerator:
|
||||
|
||||
completion_params = model_config.get("completion_params", {}) if model_config else {}
|
||||
model_parameters = {**completion_params, "max_tokens": 256}
|
||||
json_schema = cls._get_suggested_questions_json_schema()
|
||||
|
||||
try:
|
||||
response = invoke_llm_with_structured_output(
|
||||
response = invoke_llm_with_pydantic_model(
|
||||
provider=model_instance.provider,
|
||||
model_schema=model_schema,
|
||||
model_instance=model_instance,
|
||||
prompt_messages=prompt_messages,
|
||||
json_schema=json_schema,
|
||||
output_model=SuggestedQuestionsOutput,
|
||||
model_parameters=model_parameters,
|
||||
stream=False,
|
||||
tenant_id=tenant_id,
|
||||
@ -644,58 +645,6 @@ Sources: {", ".join(sources)}
|
||||
Target: {parameter_info.get("name")}({param_type}) - {param_desc}
|
||||
Output 3 short, practical questions in {language}."""
|
||||
|
||||
@classmethod
|
||||
def _get_suggested_questions_json_schema(cls) -> dict:
|
||||
"""Return JSON Schema for suggested questions."""
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"questions": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"minItems": 3,
|
||||
"maxItems": 3,
|
||||
"description": "3 suggested questions",
|
||||
},
|
||||
},
|
||||
"required": ["questions"],
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _get_code_node_json_schema(cls) -> dict:
|
||||
"""Return JSON Schema for structured output."""
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"variables": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"variable": {"type": "string", "description": "Variable name in code"},
|
||||
"value_selector": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Path like [node_id, output_name]",
|
||||
},
|
||||
},
|
||||
"required": ["variable", "value_selector"],
|
||||
},
|
||||
},
|
||||
"code": {"type": "string", "description": "Generated code with main function"},
|
||||
"outputs": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"properties": {"type": {"type": "string"}},
|
||||
},
|
||||
"description": "Output definitions, key is output name",
|
||||
},
|
||||
"explanation": {"type": "string", "description": "Brief explanation of the code"},
|
||||
},
|
||||
"required": ["variables", "code", "outputs", "explanation"],
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _get_upstream_nodes(cls, graph_dict: Mapping[str, Any], node_id: str) -> list[dict]:
|
||||
"""
|
||||
@ -1011,6 +960,10 @@ Parameter: {parameter_info.get("name")} ({param_type}) - {parameter_info.get("de
|
||||
provider=model_config.get("provider", ""),
|
||||
model=model_config.get("name", ""),
|
||||
)
|
||||
model_name = model_config.get("name", "")
|
||||
model_schema = model_instance.model_type_instance.get_model_schema(model_name, model_instance.credentials)
|
||||
if not model_schema:
|
||||
return {"error": f"Model schema not found for {model_name}"}
|
||||
match node_type:
|
||||
case "llm" | "agent":
|
||||
system_prompt = LLM_MODIFY_PROMPT_SYSTEM
|
||||
@ -1034,20 +987,18 @@ Parameter: {parameter_info.get("name")} ({param_type}) - {parameter_info.get("de
|
||||
model_parameters = {"temperature": 0.4}
|
||||
|
||||
try:
|
||||
response: LLMResult = model_instance.invoke_llm(
|
||||
prompt_messages=list(prompt_messages), model_parameters=model_parameters, stream=False
|
||||
)
|
||||
from core.llm_generator.output_parser.structured_output import invoke_llm_with_pydantic_model
|
||||
|
||||
generated_raw = response.message.get_text_content()
|
||||
first_brace = generated_raw.find("{")
|
||||
last_brace = generated_raw.rfind("}")
|
||||
if first_brace == -1 or last_brace == -1 or last_brace < first_brace:
|
||||
raise ValueError(f"Could not find a valid JSON object in response: {generated_raw}")
|
||||
json_str = generated_raw[first_brace : last_brace + 1]
|
||||
data = json_repair.loads(json_str)
|
||||
if not isinstance(data, dict):
|
||||
raise TypeError(f"Expected a JSON object, but got {type(data).__name__}")
|
||||
return data
|
||||
response = invoke_llm_with_pydantic_model(
|
||||
provider=model_instance.provider,
|
||||
model_schema=model_schema,
|
||||
model_instance=model_instance,
|
||||
prompt_messages=list(prompt_messages),
|
||||
output_model=InstructionModifyOutput,
|
||||
model_parameters=model_parameters,
|
||||
stream=False,
|
||||
)
|
||||
return response.structured_output or {}
|
||||
except InvokeError as e:
|
||||
error = str(e)
|
||||
return {"error": f"Failed to generate code. Error: {error}"}
|
||||
|
||||
34
api/core/llm_generator/output_models.py
Normal file
34
api/core/llm_generator/output_models.py
Normal file
@ -0,0 +1,34 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from core.variables.types import SegmentType
|
||||
from core.workflow.nodes.base.entities import VariableSelector
|
||||
|
||||
|
||||
class SuggestedQuestionsOutput(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
questions: list[str] = Field(min_length=3, max_length=3)
|
||||
|
||||
|
||||
class CodeNodeOutput(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
type: SegmentType
|
||||
|
||||
|
||||
class CodeNodeStructuredOutput(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
variables: list[VariableSelector]
|
||||
code: str
|
||||
outputs: dict[str, CodeNodeOutput]
|
||||
explanation: str
|
||||
|
||||
|
||||
class InstructionModifyOutput(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
modified: str
|
||||
message: str
|
||||
@ -2,10 +2,10 @@ import json
|
||||
from collections.abc import Generator, Mapping, Sequence
|
||||
from copy import deepcopy
|
||||
from enum import StrEnum
|
||||
from typing import Any, Literal, cast, overload
|
||||
from typing import Any, Literal, TypeVar, cast, overload
|
||||
|
||||
import json_repair
|
||||
from pydantic import TypeAdapter, ValidationError
|
||||
from pydantic import BaseModel, TypeAdapter, ValidationError
|
||||
|
||||
from core.llm_generator.output_parser.errors import OutputParserError
|
||||
from core.llm_generator.output_parser.file_ref import convert_file_refs_in_output
|
||||
@ -44,6 +44,9 @@ class SpecialModelType(StrEnum):
|
||||
OLLAMA = "ollama"
|
||||
|
||||
|
||||
T = TypeVar("T", bound=BaseModel)
|
||||
|
||||
|
||||
@overload
|
||||
def invoke_llm_with_structured_output(
|
||||
*,
|
||||
@ -129,7 +132,6 @@ def invoke_llm_with_structured_output(
|
||||
file IDs in the output will be automatically converted to File objects.
|
||||
:return: full response or stream response chunk generator result
|
||||
"""
|
||||
|
||||
# handle native json schema
|
||||
model_parameters_with_json_schema: dict[str, Any] = {
|
||||
**(model_parameters or {}),
|
||||
@ -234,6 +236,87 @@ def invoke_llm_with_structured_output(
|
||||
return generator()
|
||||
|
||||
|
||||
@overload
|
||||
def invoke_llm_with_pydantic_model(
|
||||
*,
|
||||
provider: str,
|
||||
model_schema: AIModelEntity,
|
||||
model_instance: ModelInstance,
|
||||
prompt_messages: Sequence[PromptMessage],
|
||||
output_model: type[T],
|
||||
model_parameters: Mapping | None = None,
|
||||
tools: Sequence[PromptMessageTool] | None = None,
|
||||
stop: list[str] | None = None,
|
||||
stream: Literal[False] = False,
|
||||
user: str | None = None,
|
||||
callbacks: list[Callback] | None = None,
|
||||
tenant_id: str | None = None,
|
||||
) -> LLMResultWithStructuredOutput: ...
|
||||
|
||||
|
||||
def invoke_llm_with_pydantic_model(
|
||||
*,
|
||||
provider: str,
|
||||
model_schema: AIModelEntity,
|
||||
model_instance: ModelInstance,
|
||||
prompt_messages: Sequence[PromptMessage],
|
||||
output_model: type[T],
|
||||
model_parameters: Mapping | None = None,
|
||||
tools: Sequence[PromptMessageTool] | None = None,
|
||||
stop: list[str] | None = None,
|
||||
stream: bool = False,
|
||||
user: str | None = None,
|
||||
callbacks: list[Callback] | None = None,
|
||||
tenant_id: str | None = None,
|
||||
) -> LLMResultWithStructuredOutput:
|
||||
"""
|
||||
Invoke large language model with a Pydantic output model.
|
||||
|
||||
This helper generates a JSON schema from the Pydantic model, invokes the
|
||||
structured-output LLM path, and validates the result in non-streaming mode.
|
||||
"""
|
||||
if stream:
|
||||
raise ValueError("invoke_llm_with_pydantic_model only supports stream=False")
|
||||
|
||||
json_schema = _schema_from_pydantic(output_model)
|
||||
result = invoke_llm_with_structured_output(
|
||||
provider=provider,
|
||||
model_schema=model_schema,
|
||||
model_instance=model_instance,
|
||||
prompt_messages=prompt_messages,
|
||||
json_schema=json_schema,
|
||||
model_parameters=model_parameters,
|
||||
tools=tools,
|
||||
stop=stop,
|
||||
stream=False,
|
||||
user=user,
|
||||
callbacks=callbacks,
|
||||
tenant_id=tenant_id,
|
||||
)
|
||||
|
||||
structured_output = result.structured_output
|
||||
if structured_output is None:
|
||||
raise OutputParserError("Structured output is empty")
|
||||
|
||||
validated_output = _validate_structured_output(output_model, structured_output)
|
||||
return result.model_copy(update={"structured_output": validated_output})
|
||||
|
||||
|
||||
def _schema_from_pydantic(output_model: type[BaseModel]) -> dict[str, Any]:
|
||||
return output_model.model_json_schema()
|
||||
|
||||
|
||||
def _validate_structured_output(
|
||||
output_model: type[T],
|
||||
structured_output: Mapping[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
validated_output = output_model.model_validate(structured_output)
|
||||
except ValidationError as exc:
|
||||
raise OutputParserError(f"Structured output validation failed: {exc}") from exc
|
||||
return validated_output.model_dump(mode="python")
|
||||
|
||||
|
||||
def _handle_native_json_schema(
|
||||
provider: str,
|
||||
model_schema: AIModelEntity,
|
||||
|
||||
@ -106,14 +106,17 @@ class MentionGraphService:
|
||||
]
|
||||
|
||||
structured_output = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
parameter_schema.name: {
|
||||
"type": parameter_schema.type,
|
||||
"description": parameter_schema.description,
|
||||
}
|
||||
},
|
||||
"required": [parameter_schema.name],
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
parameter_schema.name: {
|
||||
"type": parameter_schema.type,
|
||||
"description": parameter_schema.description,
|
||||
}
|
||||
},
|
||||
"required": [parameter_schema.name],
|
||||
"additionalProperties": False,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@ -2,9 +2,13 @@ from decimal import Decimal
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from core.llm_generator.output_parser.errors import OutputParserError
|
||||
from core.llm_generator.output_parser.structured_output import invoke_llm_with_structured_output
|
||||
from core.llm_generator.output_parser.structured_output import (
|
||||
invoke_llm_with_pydantic_model,
|
||||
invoke_llm_with_structured_output,
|
||||
)
|
||||
from core.model_runtime.entities.llm_entities import (
|
||||
LLMResult,
|
||||
LLMResultChunk,
|
||||
@ -461,3 +465,68 @@ def test_model_specific_schema_preparation():
|
||||
|
||||
# For Gemini, the schema should not have additionalProperties and boolean should be converted to string
|
||||
assert "json_schema" in call_args.kwargs["model_parameters"]
|
||||
|
||||
|
||||
class ExampleOutput(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
name: str
|
||||
|
||||
|
||||
def test_structured_output_with_pydantic_model():
|
||||
model_schema = get_model_entity("openai", "gpt-4o", support_structure_output=True)
|
||||
model_instance = get_model_instance()
|
||||
model_instance.invoke_llm.return_value = LLMResult(
|
||||
model="gpt-4o",
|
||||
message=AssistantPromptMessage(content='{"name": "test"}'),
|
||||
usage=create_mock_usage(prompt_tokens=8, completion_tokens=4),
|
||||
)
|
||||
|
||||
prompt_messages = [UserPromptMessage(content="Return a JSON object with name.")]
|
||||
|
||||
result = invoke_llm_with_pydantic_model(
|
||||
provider="openai",
|
||||
model_schema=model_schema,
|
||||
model_instance=model_instance,
|
||||
prompt_messages=prompt_messages,
|
||||
output_model=ExampleOutput,
|
||||
stream=False,
|
||||
)
|
||||
|
||||
assert isinstance(result, LLMResultWithStructuredOutput)
|
||||
assert result.structured_output == {"name": "test"}
|
||||
|
||||
|
||||
def test_structured_output_with_pydantic_model_streaming_rejected():
|
||||
model_schema = get_model_entity("openai", "gpt-4o", support_structure_output=True)
|
||||
model_instance = get_model_instance()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
invoke_llm_with_pydantic_model(
|
||||
provider="openai",
|
||||
model_schema=model_schema,
|
||||
model_instance=model_instance,
|
||||
prompt_messages=[UserPromptMessage(content="test")],
|
||||
output_model=ExampleOutput,
|
||||
stream=True,
|
||||
)
|
||||
|
||||
|
||||
def test_structured_output_with_pydantic_model_validation_error():
|
||||
model_schema = get_model_entity("openai", "gpt-4o", support_structure_output=True)
|
||||
model_instance = get_model_instance()
|
||||
model_instance.invoke_llm.return_value = LLMResult(
|
||||
model="gpt-4o",
|
||||
message=AssistantPromptMessage(content='{"name": 123}'),
|
||||
usage=create_mock_usage(prompt_tokens=8, completion_tokens=4),
|
||||
)
|
||||
|
||||
with pytest.raises(OutputParserError):
|
||||
invoke_llm_with_pydantic_model(
|
||||
provider="openai",
|
||||
model_schema=model_schema,
|
||||
model_instance=model_instance,
|
||||
prompt_messages=[UserPromptMessage(content="test")],
|
||||
output_model=ExampleOutput,
|
||||
stream=False,
|
||||
)
|
||||
|
||||
@ -203,7 +203,7 @@ const Annotation: FC<Props> = (props) => {
|
||||
</Filter>
|
||||
{isLoading
|
||||
? <Loading type="app" />
|
||||
// eslint-disable-next-line sonarjs/no-nested-conditional
|
||||
|
||||
: total > 0
|
||||
? (
|
||||
<List
|
||||
|
||||
@ -134,7 +134,6 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
|
||||
},
|
||||
] as const
|
||||
|
||||
// eslint-disable-next-line sonarjs/no-nested-template-literals, sonarjs/no-nested-conditional
|
||||
const [instructionFromSessionStorage, setInstruction] = useSessionStorageState<string>(`improve-instruction-${flowId}${isBasicMode ? '' : `-${nodeId}${editorId ? `-${editorId}` : ''}`}`)
|
||||
const instruction = instructionFromSessionStorage || ''
|
||||
const [ideaOutput, setIdeaOutput] = useState<string>('')
|
||||
|
||||
@ -175,7 +175,7 @@ describe('SettingsModal', () => {
|
||||
renderSettingsModal()
|
||||
fireEvent.click(screen.getByText('appOverview.overview.appInfo.settings.more.entry'))
|
||||
const privacyInput = screen.getByPlaceholderText('appOverview.overview.appInfo.settings.more.privacyPolicyPlaceholder')
|
||||
// eslint-disable-next-line sonarjs/no-clear-text-protocols
|
||||
|
||||
fireEvent.change(privacyInput, { target: { value: 'ftp://invalid-url' } })
|
||||
|
||||
fireEvent.click(screen.getByText('common.operation.save'))
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "12",
|
||||
"height": "12",
|
||||
"viewBox": "0 0 12 12",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M5.14286 5.14286V3.42857L8 5.71429L5.14286 8V6.28571H0V5.14286H5.14286ZM0.83303 7.42857H2.04658C2.72474 9.10389 4.36721 10.28571 6.28571 10.28571C8.81049 10.28571 10.85717 8.23903 10.85717 5.71429C10.85717 3.18956 8.81049 1.14285 6.28571 1.14285C4.36721 1.14285 2.72474 2.32467 2.04658 4H0.83303C1.56118 1.68165 3.72706 0 6.28571 0C9.4416 0 12 2.55837 12 5.71429C12 8.87014 9.4416 11.42854 6.28571 11.42854C3.72706 11.42854 1.56118 9.74691 0.83303 7.42857Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "AssembleVariablesAlt"
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import type { IconData } from '@/app/components/base/icons/IconBase'
|
||||
import * as React from 'react'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import data from './AssembleVariablesAlt.json'
|
||||
|
||||
const Icon = (
|
||||
{
|
||||
ref,
|
||||
...props
|
||||
}: React.SVGProps<SVGSVGElement> & {
|
||||
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>
|
||||
},
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />
|
||||
|
||||
Icon.displayName = 'AssembleVariablesAlt'
|
||||
|
||||
export default Icon
|
||||
@ -1,4 +1,5 @@
|
||||
export { default as AssembleVariables } from './AssembleVariables'
|
||||
export { default as AssembleVariablesAlt } from './AssembleVariablesAlt'
|
||||
export { default as AtSign } from './AtSign'
|
||||
export { default as Bookmark } from './Bookmark'
|
||||
export { default as Check } from './Check'
|
||||
|
||||
@ -205,7 +205,7 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any
|
||||
}
|
||||
catch {
|
||||
try {
|
||||
// eslint-disable-next-line no-new-func, sonarjs/code-eval
|
||||
// eslint-disable-next-line no-new-func
|
||||
const result = new Function(`return ${trimmedContent}`)()
|
||||
if (typeof result === 'object' && result !== null) {
|
||||
setFinalChartOption(result)
|
||||
@ -250,7 +250,7 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any
|
||||
}
|
||||
catch {
|
||||
try {
|
||||
// eslint-disable-next-line no-new-func, sonarjs/code-eval
|
||||
// eslint-disable-next-line no-new-func
|
||||
const result = new Function(`return ${trimmedContent}`)()
|
||||
if (typeof result === 'object' && result !== null) {
|
||||
setFinalChartOption(result)
|
||||
|
||||
@ -155,14 +155,13 @@ export type TriggerFn = (
|
||||
text: string,
|
||||
editor: LexicalEditor,
|
||||
) => MenuTextMatch | null
|
||||
export const PUNCTUATION = '\\.,\\+\\*\\?\\$\\@\\|#{}\\(\\)\\^\\-\\[\\]\\\\/!%\'"~=<>_:;'
|
||||
export function useBasicTypeaheadTriggerMatch(
|
||||
trigger: string,
|
||||
{ minLength = 1, maxLength = 75 }: { minLength?: number, maxLength?: number },
|
||||
): TriggerFn {
|
||||
return useCallback(
|
||||
(text: string) => {
|
||||
const validChars = `[${PUNCTUATION}\\s]`
|
||||
const validChars = '[^\\n]'
|
||||
const TypeaheadTriggerRegex = new RegExp(
|
||||
'(.*)('
|
||||
+ `[${trigger}]`
|
||||
|
||||
@ -32,6 +32,8 @@ import { PickerBlockMenuOption } from './menu'
|
||||
import { PromptMenuItem } from './prompt-option'
|
||||
import { VariableMenuItem } from './variable-option'
|
||||
|
||||
const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
|
||||
export const usePromptOptions = (
|
||||
contextBlock?: ContextBlockType,
|
||||
queryBlock?: QueryBlockType,
|
||||
@ -154,7 +156,7 @@ export const useVariableOptions = (
|
||||
if (!queryString)
|
||||
return baseOptions
|
||||
|
||||
const regex = new RegExp(queryString, 'i')
|
||||
const regex = new RegExp(escapeRegExp(queryString), 'i')
|
||||
|
||||
return baseOptions.filter(option => regex.test(option.key))
|
||||
}, [editor, queryString, variableBlock])
|
||||
@ -232,7 +234,7 @@ export const useExternalToolOptions = (
|
||||
if (!queryString)
|
||||
return baseToolOptions
|
||||
|
||||
const regex = new RegExp(queryString, 'i')
|
||||
const regex = new RegExp(escapeRegExp(queryString), 'i')
|
||||
|
||||
return baseToolOptions.filter(option => regex.test(option.key))
|
||||
}, [editor, queryString, externalToolBlockType])
|
||||
|
||||
@ -25,7 +25,9 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext
|
||||
import { LexicalTypeaheadMenuPlugin } from '@lexical/react/LexicalTypeaheadMenuPlugin'
|
||||
import {
|
||||
$getRoot,
|
||||
$getSelection,
|
||||
$insertNodes,
|
||||
$isRangeSelection,
|
||||
KEY_ESCAPE_COMMAND,
|
||||
} from 'lexical'
|
||||
import {
|
||||
@ -91,11 +93,26 @@ const ComponentPicker = ({
|
||||
],
|
||||
})
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const useExternalSearch = triggerString === '/' || triggerString === '@'
|
||||
const checkForTriggerMatch = useBasicTypeaheadTriggerMatch(triggerString, {
|
||||
minLength: 0,
|
||||
maxLength: 0,
|
||||
maxLength: useExternalSearch ? 75 : 0,
|
||||
})
|
||||
|
||||
const getMatchFromSelection = useCallback(() => {
|
||||
const selection = $getSelection()
|
||||
if (!$isRangeSelection(selection) || !selection.isCollapsed())
|
||||
return null
|
||||
const anchor = selection.anchor
|
||||
if (anchor.type !== 'text')
|
||||
return null
|
||||
const anchorNode = anchor.getNode()
|
||||
if (!anchorNode.isSimpleText())
|
||||
return null
|
||||
const text = anchorNode.getTextContent().slice(0, anchor.offset)
|
||||
return checkForTriggerMatch(text, editor)
|
||||
}, [checkForTriggerMatch, editor])
|
||||
|
||||
const [queryString, setQueryString] = useState<string | null>(null)
|
||||
|
||||
eventEmitter?.useSubscription((v: any) => {
|
||||
@ -116,6 +133,7 @@ const ComponentPicker = ({
|
||||
currentBlock,
|
||||
errorMessageBlock,
|
||||
lastRunBlock,
|
||||
useExternalSearch ? (queryString ?? undefined) : undefined,
|
||||
)
|
||||
|
||||
const onSelectOption = useCallback(
|
||||
@ -137,7 +155,10 @@ const ComponentPicker = ({
|
||||
|
||||
const handleSelectWorkflowVariable = useCallback((variables: string[]) => {
|
||||
editor.update(() => {
|
||||
const needRemove = $splitNodeContainingQuery(checkForTriggerMatch(triggerString, editor)!)
|
||||
const match = getMatchFromSelection()
|
||||
if (!match)
|
||||
return
|
||||
const needRemove = $splitNodeContainingQuery(match)
|
||||
if (needRemove)
|
||||
needRemove.remove()
|
||||
})
|
||||
@ -157,7 +178,7 @@ const ComponentPicker = ({
|
||||
else {
|
||||
editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, variables)
|
||||
}
|
||||
}, [editor, currentBlock?.generatorType, checkForTriggerMatch, triggerString])
|
||||
}, [editor, currentBlock?.generatorType, getMatchFromSelection])
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
const escapeEvent = new KeyboardEvent('keydown', { key: 'Escape' })
|
||||
@ -166,7 +187,7 @@ const ComponentPicker = ({
|
||||
|
||||
const handleSelectAssembleVariables = useCallback((): ValueSelector | null => {
|
||||
editor.update(() => {
|
||||
const match = checkForTriggerMatch(triggerString, editor)
|
||||
const match = getMatchFromSelection()
|
||||
if (!match)
|
||||
return
|
||||
const needRemove = $splitNodeContainingQuery(match)
|
||||
@ -178,11 +199,14 @@ const ComponentPicker = ({
|
||||
editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, assembleVariables)
|
||||
handleClose()
|
||||
return assembleVariables ?? null
|
||||
}, [editor, checkForTriggerMatch, triggerString, workflowVariableBlock, handleClose])
|
||||
}, [editor, getMatchFromSelection, workflowVariableBlock, handleClose])
|
||||
|
||||
const handleSelectAgent = useCallback((agent: { id: string, title: string }) => {
|
||||
editor.update(() => {
|
||||
const needRemove = $splitNodeContainingQuery(checkForTriggerMatch(triggerString, editor)!)
|
||||
const match = getMatchFromSelection()
|
||||
if (!match)
|
||||
return
|
||||
const needRemove = $splitNodeContainingQuery(match)
|
||||
if (needRemove)
|
||||
needRemove.remove()
|
||||
|
||||
@ -198,7 +222,7 @@ const ComponentPicker = ({
|
||||
})
|
||||
agentBlock?.onSelect?.(agent)
|
||||
handleClose()
|
||||
}, [editor, checkForTriggerMatch, triggerString, agentBlock, handleClose])
|
||||
}, [editor, getMatchFromSelection, agentBlock, handleClose])
|
||||
|
||||
const isAgentTrigger = triggerString === '@' && agentBlock?.show
|
||||
const showAssembleVariables = triggerString === '/'
|
||||
@ -247,6 +271,9 @@ const ComponentPicker = ({
|
||||
onBlur={handleClose}
|
||||
maxHeightClass="max-h-[34vh]"
|
||||
autoFocus={false}
|
||||
hideSearch={useExternalSearch}
|
||||
externalSearchText={useExternalSearch ? (queryString ?? '') : undefined}
|
||||
enableKeyboardNavigation={useExternalSearch}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
@ -270,6 +297,9 @@ const ComponentPicker = ({
|
||||
onAssembleVariables={showAssembleVariables ? handleSelectAssembleVariables : undefined}
|
||||
autoFocus={false}
|
||||
isInCodeGeneratorInstructionEditor={currentBlock?.generatorType === GeneratorType.code}
|
||||
hideSearch={useExternalSearch}
|
||||
externalSearchText={useExternalSearch ? (queryString ?? '') : undefined}
|
||||
enableKeyboardNavigation={useExternalSearch}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
@ -311,7 +341,7 @@ const ComponentPicker = ({
|
||||
}
|
||||
</>
|
||||
)
|
||||
}, [isAgentTrigger, agentNodes, allFlattenOptions.length, workflowVariableBlock?.show, floatingStyles, isPositioned, refs, handleSelectAgent, handleClose, workflowVariableOptions, isSupportFileVar, currentBlock?.generatorType, handleSelectWorkflowVariable, queryString, workflowVariableBlock?.showManageInputField, workflowVariableBlock?.onManageInputField, showAssembleVariables, handleSelectAssembleVariables])
|
||||
}, [isAgentTrigger, agentNodes, allFlattenOptions.length, workflowVariableBlock?.show, floatingStyles, isPositioned, refs, handleSelectAgent, handleClose, workflowVariableOptions, isSupportFileVar, currentBlock?.generatorType, handleSelectWorkflowVariable, queryString, workflowVariableBlock?.showManageInputField, workflowVariableBlock?.onManageInputField, showAssembleVariables, handleSelectAssembleVariables, useExternalSearch])
|
||||
|
||||
return (
|
||||
<LexicalTypeaheadMenuPlugin
|
||||
|
||||
@ -28,10 +28,10 @@ const RuleDetail = ({
|
||||
case 'mode':
|
||||
value = !sourceData?.mode
|
||||
? value
|
||||
// eslint-disable-next-line sonarjs/no-nested-conditional
|
||||
|
||||
: sourceData.mode === ProcessMode.general
|
||||
? (t('embedding.custom', { ns: 'datasetDocuments' }) as string)
|
||||
// eslint-disable-next-line sonarjs/no-nested-conditional
|
||||
|
||||
: `${t('embedding.hierarchical', { ns: 'datasetDocuments' })} · ${sourceData?.rules?.parent_mode === 'paragraph'
|
||||
? t('parentMode.paragraph', { ns: 'dataset' })
|
||||
: t('parentMode.fullDoc', { ns: 'dataset' })}`
|
||||
@ -70,7 +70,7 @@ const RuleDetail = ({
|
||||
src={
|
||||
retrievalMethod === RETRIEVE_METHOD.fullText
|
||||
? retrievalIcon.fullText
|
||||
// eslint-disable-next-line sonarjs/no-nested-conditional
|
||||
|
||||
: retrievalMethod === RETRIEVE_METHOD.hybrid
|
||||
? retrievalIcon.hybrid
|
||||
: retrievalIcon.vector
|
||||
|
||||
@ -403,7 +403,7 @@ const Form = () => {
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
// eslint-disable-next-line sonarjs/no-nested-conditional
|
||||
|
||||
: indexMethod
|
||||
? (
|
||||
<>
|
||||
|
||||
@ -7,22 +7,28 @@ import { useShallow } from 'zustand/react/shallow'
|
||||
import { useIsChatMode, useWorkflowVariables } from '@/app/components/workflow/hooks'
|
||||
import Panel from '@/app/components/workflow/panel'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import ConfigPanel from './config-panel'
|
||||
|
||||
type SubGraphChildrenProps = {
|
||||
agentName: string
|
||||
extractorNodeId: string
|
||||
mentionConfig: MentionConfig
|
||||
onMentionConfigChange: (config: MentionConfig) => void
|
||||
}
|
||||
type SubGraphChildrenProps
|
||||
= | {
|
||||
variant: 'agent'
|
||||
title: string
|
||||
extractorNodeId: string
|
||||
mentionConfig: MentionConfig
|
||||
onMentionConfigChange: (config: MentionConfig) => void
|
||||
}
|
||||
| {
|
||||
variant: 'assemble'
|
||||
title: string
|
||||
extractorNodeId: string
|
||||
}
|
||||
|
||||
const SubGraphChildren: FC<SubGraphChildrenProps> = ({
|
||||
agentName,
|
||||
extractorNodeId,
|
||||
mentionConfig,
|
||||
onMentionConfigChange,
|
||||
}) => {
|
||||
const SubGraphChildren: FC<SubGraphChildrenProps> = (props) => {
|
||||
const {
|
||||
variant,
|
||||
title,
|
||||
extractorNodeId,
|
||||
} = props
|
||||
const { getNodeAvailableVars } = useWorkflowVariables()
|
||||
const isChatMode = useIsChatMode()
|
||||
const nodePanelWidth = useStore(s => s.nodePanelWidth)
|
||||
@ -32,7 +38,7 @@ const SubGraphChildren: FC<SubGraphChildrenProps> = ({
|
||||
}))
|
||||
|
||||
const extractorNode = useReactFlowStore(useShallow((s) => {
|
||||
return s.getNodes().find(node => node.data.type === BlockEnum.LLM)
|
||||
return s.getNodes().find(node => node.id === extractorNodeId)
|
||||
}))
|
||||
|
||||
const availableNodes = useMemo(() => {
|
||||
@ -51,8 +57,10 @@ const SubGraphChildren: FC<SubGraphChildrenProps> = ({
|
||||
return vars.filter(item => item.nodeId === extractorNode.id)
|
||||
}, [extractorNode, getNodeAvailableVars, isChatMode])
|
||||
|
||||
const agentProps = variant === 'agent' ? props : null
|
||||
|
||||
const panelRight = useMemo(() => {
|
||||
if (selectedNode)
|
||||
if (!agentProps || selectedNode)
|
||||
return null
|
||||
|
||||
return (
|
||||
@ -62,17 +70,25 @@ const SubGraphChildren: FC<SubGraphChildrenProps> = ({
|
||||
style={{ width: `${nodePanelWidth}px` }}
|
||||
>
|
||||
<ConfigPanel
|
||||
agentName={agentName}
|
||||
agentName={title}
|
||||
extractorNodeId={extractorNodeId}
|
||||
mentionConfig={mentionConfig}
|
||||
mentionConfig={agentProps.mentionConfig}
|
||||
availableNodes={availableNodes}
|
||||
availableVars={availableVars}
|
||||
onMentionConfigChange={onMentionConfigChange}
|
||||
onMentionConfigChange={agentProps.onMentionConfigChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}, [agentName, availableNodes, availableVars, extractorNodeId, mentionConfig, nodePanelWidth, onMentionConfigChange, selectedNode])
|
||||
}, [agentProps, availableNodes, availableVars, extractorNodeId, nodePanelWidth, selectedNode, title])
|
||||
|
||||
if (variant === 'assemble') {
|
||||
return (
|
||||
<Panel
|
||||
withHeader={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Panel
|
||||
|
||||
@ -9,35 +9,46 @@ import { useStoreApi } from 'reactflow'
|
||||
import { WorkflowWithInnerContext } from '@/app/components/workflow'
|
||||
import { useSetWorkflowVarsWithValue } from '@/app/components/workflow/hooks/use-fetch-workflow-inspect-vars'
|
||||
import { useInspectVarsCrudCommon } from '@/app/components/workflow/hooks/use-inspect-vars-crud-common'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { FlowType } from '@/types/common'
|
||||
import { useAvailableNodesMetaData } from '../hooks'
|
||||
import SubGraphChildren from './sub-graph-children'
|
||||
|
||||
type SubGraphMainProps = {
|
||||
type SubGraphMainBaseProps = {
|
||||
nodes: Node[]
|
||||
edges: Edge[]
|
||||
viewport: Viewport
|
||||
agentName: string
|
||||
title: string
|
||||
extractorNodeId: string
|
||||
configsMap?: HooksStoreShape['configsMap']
|
||||
mentionConfig: MentionConfig
|
||||
onMentionConfigChange: (config: MentionConfig) => void
|
||||
selectableNodeTypes?: BlockEnum[]
|
||||
onSave?: (nodes: Node[], edges: Edge[]) => void
|
||||
onSyncWorkflowDraft?: SyncWorkflowDraft
|
||||
}
|
||||
|
||||
const SubGraphMain: FC<SubGraphMainProps> = ({
|
||||
nodes,
|
||||
edges,
|
||||
viewport,
|
||||
agentName,
|
||||
extractorNodeId,
|
||||
configsMap,
|
||||
mentionConfig,
|
||||
onMentionConfigChange,
|
||||
onSave,
|
||||
onSyncWorkflowDraft,
|
||||
}) => {
|
||||
type SubGraphMainProps
|
||||
= | (SubGraphMainBaseProps & {
|
||||
variant: 'agent'
|
||||
mentionConfig: MentionConfig
|
||||
onMentionConfigChange: (config: MentionConfig) => void
|
||||
})
|
||||
| (SubGraphMainBaseProps & {
|
||||
variant: 'assemble'
|
||||
})
|
||||
|
||||
const SubGraphMain: FC<SubGraphMainProps> = (props) => {
|
||||
const {
|
||||
nodes,
|
||||
edges,
|
||||
viewport,
|
||||
variant,
|
||||
title,
|
||||
extractorNodeId,
|
||||
configsMap,
|
||||
selectableNodeTypes,
|
||||
onSave,
|
||||
onSyncWorkflowDraft,
|
||||
} = props
|
||||
const reactFlowStore = useStoreApi()
|
||||
const availableNodesMetaData = useAvailableNodesMetaData()
|
||||
const flowType = configsMap?.flowType ?? FlowType.appFlow
|
||||
@ -76,32 +87,53 @@ const SubGraphMain: FC<SubGraphMainProps> = ({
|
||||
}
|
||||
}, [handleSyncSubGraphDraft, onSyncWorkflowDraft])
|
||||
|
||||
const resolvedSelectableTypes = useMemo(() => {
|
||||
if (selectableNodeTypes && selectableNodeTypes.length > 0)
|
||||
return selectableNodeTypes
|
||||
return variant === 'agent' ? [BlockEnum.LLM] : [BlockEnum.Code]
|
||||
}, [selectableNodeTypes, variant])
|
||||
|
||||
const hooksStore = useMemo(() => ({
|
||||
interactionMode: 'subgraph',
|
||||
subGraphSelectableNodeTypes: resolvedSelectableTypes,
|
||||
availableNodesMetaData,
|
||||
configsMap,
|
||||
fetchInspectVars,
|
||||
...inspectVarsCrud,
|
||||
doSyncWorkflowDraft: handleSyncWorkflowDraft,
|
||||
syncWorkflowDraftWhenPageClose: handleSyncSubGraphDraft,
|
||||
}), [availableNodesMetaData, configsMap, fetchInspectVars, handleSyncSubGraphDraft, handleSyncWorkflowDraft, inspectVarsCrud])
|
||||
}), [availableNodesMetaData, configsMap, fetchInspectVars, handleSyncSubGraphDraft, handleSyncWorkflowDraft, inspectVarsCrud, resolvedSelectableTypes])
|
||||
|
||||
const subGraphChildren = variant === 'agent'
|
||||
? (
|
||||
<SubGraphChildren
|
||||
variant="agent"
|
||||
title={title}
|
||||
extractorNodeId={extractorNodeId}
|
||||
mentionConfig={props.mentionConfig}
|
||||
onMentionConfigChange={props.onMentionConfigChange}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<SubGraphChildren
|
||||
variant="assemble"
|
||||
title={title}
|
||||
extractorNodeId={extractorNodeId}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<WorkflowWithInnerContext
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
viewport={viewport}
|
||||
// eslint-disable-next-line ts/no-explicit-any -- TODO: remove after typing boundary
|
||||
hooksStore={hooksStore as any}
|
||||
allowSelectionWhenReadOnly
|
||||
canvasReadOnly
|
||||
interactionMode="subgraph"
|
||||
>
|
||||
<SubGraphChildren
|
||||
agentName={agentName}
|
||||
extractorNodeId={extractorNodeId}
|
||||
mentionConfig={mentionConfig}
|
||||
onMentionConfigChange={onMentionConfigChange}
|
||||
/>
|
||||
{subGraphChildren}
|
||||
</WorkflowWithInnerContext>
|
||||
)
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import { memo, useEffect, useMemo } from 'react'
|
||||
import WorkflowWithDefaultContext from '@/app/components/workflow'
|
||||
import { NODE_WIDTH_X_OFFSET, START_INITIAL_POSITION } from '@/app/components/workflow/constants'
|
||||
import { WorkflowContextProvider } from '@/app/components/workflow/context'
|
||||
import { CUSTOM_SUB_GRAPH_START_NODE } from '@/app/components/workflow/nodes/sub-graph-start/constants'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { BlockEnum, EditionType, isPromptMessageContext, PromptRole } from '@/app/components/workflow/types'
|
||||
import SubGraphMain from './components/sub-graph-main'
|
||||
@ -18,7 +19,7 @@ const SUB_GRAPH_ENTRY_POSITION = {
|
||||
x: START_INITIAL_POSITION.x,
|
||||
y: 150,
|
||||
}
|
||||
const SUB_GRAPH_LLM_POSITION = {
|
||||
const SUB_GRAPH_EXTRACTOR_POSITION = {
|
||||
x: SUB_GRAPH_ENTRY_POSITION.x + NODE_WIDTH_X_OFFSET - SUB_GRAPH_EDGE_GAP,
|
||||
y: SUB_GRAPH_ENTRY_POSITION.y,
|
||||
}
|
||||
@ -33,19 +34,19 @@ const SubGraphContent: FC<SubGraphProps> = (props) => {
|
||||
const {
|
||||
toolNodeId,
|
||||
paramKey,
|
||||
agentName,
|
||||
agentNodeId,
|
||||
mentionConfig,
|
||||
onMentionConfigChange,
|
||||
extractorNode,
|
||||
toolParamValue,
|
||||
parentAvailableNodes,
|
||||
parentAvailableVars,
|
||||
configsMap,
|
||||
selectableNodeTypes,
|
||||
onSave,
|
||||
onSyncWorkflowDraft,
|
||||
} = props
|
||||
|
||||
const isAgentVariant = props.variant === 'agent'
|
||||
const sourceTitle = isAgentVariant ? (props.agentName || '') : (props.title || '')
|
||||
const resolvedAgentNodeId = isAgentVariant ? props.agentNodeId : ''
|
||||
|
||||
const setParentAvailableVars = useStore(state => state.setParentAvailableVars)
|
||||
const setParentAvailableNodes = useStore(state => state.setParentAvailableNodes)
|
||||
|
||||
@ -55,28 +56,47 @@ const SubGraphContent: FC<SubGraphProps> = (props) => {
|
||||
}, [parentAvailableNodes, parentAvailableVars, setParentAvailableNodes, setParentAvailableVars])
|
||||
|
||||
const promptText = useMemo(() => {
|
||||
if (!toolParamValue)
|
||||
if (!isAgentVariant || !toolParamValue)
|
||||
return ''
|
||||
// Reason: escape agent id before building a regex pattern.
|
||||
const escapedAgentId = agentNodeId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
const escapedAgentId = resolvedAgentNodeId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
const leadingPattern = new RegExp(`^\\{\\{[@#]${escapedAgentId}\\.context[@#]\\}\\}`)
|
||||
return toolParamValue.replace(leadingPattern, '')
|
||||
}, [agentNodeId, toolParamValue])
|
||||
}, [isAgentVariant, resolvedAgentNodeId, toolParamValue])
|
||||
|
||||
const startNode = useMemo(() => {
|
||||
if (!isAgentVariant) {
|
||||
return {
|
||||
id: 'subgraph-source',
|
||||
type: CUSTOM_SUB_GRAPH_START_NODE,
|
||||
position: SUB_GRAPH_ENTRY_POSITION,
|
||||
data: {
|
||||
type: BlockEnum.Start,
|
||||
title: sourceTitle,
|
||||
desc: '',
|
||||
selected: false,
|
||||
iconType: 'assemble',
|
||||
variables: [],
|
||||
},
|
||||
selected: false,
|
||||
selectable: false,
|
||||
draggable: false,
|
||||
connectable: false,
|
||||
focusable: false,
|
||||
deletable: false,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'subgraph-source',
|
||||
type: 'custom',
|
||||
type: CUSTOM_SUB_GRAPH_START_NODE,
|
||||
position: SUB_GRAPH_ENTRY_POSITION,
|
||||
data: {
|
||||
type: BlockEnum.Start,
|
||||
title: agentName,
|
||||
title: sourceTitle,
|
||||
desc: '',
|
||||
_connectedSourceHandleIds: ['source'],
|
||||
_connectedTargetHandleIds: [],
|
||||
_subGraphEntry: true,
|
||||
_iconTypeOverride: BlockEnum.Agent,
|
||||
selected: false,
|
||||
iconType: 'agent',
|
||||
variables: [],
|
||||
},
|
||||
selected: false,
|
||||
@ -86,65 +106,83 @@ const SubGraphContent: FC<SubGraphProps> = (props) => {
|
||||
focusable: false,
|
||||
deletable: false,
|
||||
}
|
||||
}, [agentName])
|
||||
}, [isAgentVariant, sourceTitle])
|
||||
|
||||
const extractorDisplayNode = useMemo(() => {
|
||||
if (!extractorNode)
|
||||
return null
|
||||
if (isAgentVariant) {
|
||||
const extractorNode = props.extractorNode
|
||||
if (!extractorNode)
|
||||
return null
|
||||
|
||||
const applyPromptText = (item: PromptItem) => {
|
||||
if (item.edition_type === EditionType.jinja2) {
|
||||
return {
|
||||
...item,
|
||||
text: promptText,
|
||||
jinja2_text: promptText,
|
||||
}
|
||||
}
|
||||
return { ...item, text: promptText }
|
||||
}
|
||||
|
||||
const nextPromptTemplate = (() => {
|
||||
const template = extractorNode.data.prompt_template
|
||||
if (!Array.isArray(template))
|
||||
return applyPromptText(template as PromptItem)
|
||||
|
||||
const userIndex = template.findIndex(
|
||||
item => !isPromptMessageContext(item) && (item as PromptItem).role === PromptRole.user,
|
||||
)
|
||||
if (userIndex >= 0) {
|
||||
return template.map((item, index) => {
|
||||
if (index !== userIndex)
|
||||
return item
|
||||
return applyPromptText(item as PromptItem)
|
||||
}) as PromptTemplateItem[]
|
||||
}
|
||||
|
||||
const useJinja = template.some(
|
||||
item => !isPromptMessageContext(item) && (item as PromptItem).edition_type === EditionType.jinja2,
|
||||
)
|
||||
const defaultUserPrompt: PromptItem = useJinja
|
||||
? {
|
||||
role: PromptRole.user,
|
||||
const applyPromptText = (item: PromptItem) => {
|
||||
if (item.edition_type === EditionType.jinja2) {
|
||||
return {
|
||||
...item,
|
||||
text: promptText,
|
||||
jinja2_text: promptText,
|
||||
edition_type: EditionType.jinja2,
|
||||
}
|
||||
: { role: PromptRole.user, text: promptText }
|
||||
return [...template, defaultUserPrompt] as PromptTemplateItem[]
|
||||
})()
|
||||
}
|
||||
return { ...item, text: promptText }
|
||||
}
|
||||
|
||||
const nextPromptTemplate = (() => {
|
||||
const template = extractorNode.data.prompt_template
|
||||
if (!Array.isArray(template))
|
||||
return applyPromptText(template as PromptItem)
|
||||
|
||||
const userIndex = template.findIndex(
|
||||
item => !isPromptMessageContext(item) && (item as PromptItem).role === PromptRole.user,
|
||||
)
|
||||
if (userIndex >= 0) {
|
||||
return template.map((item, index) => {
|
||||
if (index !== userIndex)
|
||||
return item
|
||||
return applyPromptText(item as PromptItem)
|
||||
}) as PromptTemplateItem[]
|
||||
}
|
||||
|
||||
const useJinja = template.some(
|
||||
item => !isPromptMessageContext(item) && (item as PromptItem).edition_type === EditionType.jinja2,
|
||||
)
|
||||
const defaultUserPrompt: PromptItem = useJinja
|
||||
? {
|
||||
role: PromptRole.user,
|
||||
text: promptText,
|
||||
jinja2_text: promptText,
|
||||
edition_type: EditionType.jinja2,
|
||||
}
|
||||
: { role: PromptRole.user, text: promptText }
|
||||
return [...template, defaultUserPrompt] as PromptTemplateItem[]
|
||||
})()
|
||||
|
||||
return {
|
||||
...extractorNode,
|
||||
hidden: false,
|
||||
selected: false,
|
||||
position: SUB_GRAPH_EXTRACTOR_POSITION,
|
||||
data: {
|
||||
...extractorNode.data,
|
||||
selected: false,
|
||||
prompt_template: nextPromptTemplate,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const extractorNode = props.extractorNode
|
||||
if (!extractorNode)
|
||||
return null
|
||||
|
||||
return {
|
||||
...extractorNode,
|
||||
hidden: false,
|
||||
selected: false,
|
||||
position: SUB_GRAPH_LLM_POSITION,
|
||||
position: SUB_GRAPH_EXTRACTOR_POSITION,
|
||||
data: {
|
||||
...extractorNode.data,
|
||||
selected: false,
|
||||
prompt_template: nextPromptTemplate,
|
||||
},
|
||||
}
|
||||
}, [extractorNode, promptText])
|
||||
}, [isAgentVariant, promptText, props.extractorNode])
|
||||
|
||||
const nodesSource = useMemo(() => {
|
||||
if (!extractorDisplayNode)
|
||||
@ -168,30 +206,54 @@ const SubGraphContent: FC<SubGraphProps> = (props) => {
|
||||
selectable: false,
|
||||
data: {
|
||||
sourceType: BlockEnum.Start,
|
||||
targetType: BlockEnum.LLM,
|
||||
targetType: isAgentVariant ? BlockEnum.LLM : BlockEnum.Code,
|
||||
_isTemp: true,
|
||||
_isSubGraphTemp: true,
|
||||
},
|
||||
},
|
||||
]
|
||||
}, [extractorDisplayNode, startNode])
|
||||
}, [extractorDisplayNode, isAgentVariant, startNode])
|
||||
|
||||
const { nodes, edges } = useSubGraphNodes(nodesSource, edgesSource)
|
||||
|
||||
if (isAgentVariant) {
|
||||
return (
|
||||
<WorkflowWithDefaultContext
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
>
|
||||
<SubGraphMain
|
||||
variant="agent"
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
viewport={defaultViewport}
|
||||
title={sourceTitle}
|
||||
extractorNodeId={`${toolNodeId}_ext_${paramKey}`}
|
||||
configsMap={configsMap}
|
||||
mentionConfig={props.mentionConfig}
|
||||
onMentionConfigChange={props.onMentionConfigChange}
|
||||
selectableNodeTypes={selectableNodeTypes}
|
||||
onSave={onSave}
|
||||
onSyncWorkflowDraft={onSyncWorkflowDraft}
|
||||
/>
|
||||
</WorkflowWithDefaultContext>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<WorkflowWithDefaultContext
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
>
|
||||
<SubGraphMain
|
||||
variant="assemble"
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
viewport={defaultViewport}
|
||||
agentName={agentName}
|
||||
title={sourceTitle}
|
||||
extractorNodeId={`${toolNodeId}_ext_${paramKey}`}
|
||||
configsMap={configsMap}
|
||||
mentionConfig={mentionConfig}
|
||||
onMentionConfigChange={onMentionConfigChange}
|
||||
selectableNodeTypes={selectableNodeTypes}
|
||||
onSave={onSave}
|
||||
onSyncWorkflowDraft={onSyncWorkflowDraft}
|
||||
/>
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import type { StateCreator } from 'zustand'
|
||||
import type { Shape as HooksStoreShape } from '@/app/components/workflow/hooks-store'
|
||||
import type { MentionConfig } from '@/app/components/workflow/nodes/_base/types'
|
||||
import type { CodeNodeType } from '@/app/components/workflow/nodes/code/types'
|
||||
import type { LLMNodeType } from '@/app/components/workflow/nodes/llm/types'
|
||||
import type { Edge, Node, NodeOutPutVar, ValueSelector } from '@/app/components/workflow/types'
|
||||
import type { BlockEnum, Edge, Node, NodeOutPutVar, ValueSelector } from '@/app/components/workflow/types'
|
||||
|
||||
export type SyncWorkflowDraftCallback = {
|
||||
onSuccess?: () => void
|
||||
@ -15,23 +16,38 @@ export type SyncWorkflowDraft = (
|
||||
callback?: SyncWorkflowDraftCallback,
|
||||
) => Promise<void>
|
||||
|
||||
export type SubGraphProps = {
|
||||
export type SubGraphVariant = 'agent' | 'assemble'
|
||||
|
||||
type BaseSubGraphProps = {
|
||||
toolNodeId: string
|
||||
paramKey: string
|
||||
sourceVariable: ValueSelector
|
||||
agentNodeId: string
|
||||
agentName: string
|
||||
configsMap?: HooksStoreShape['configsMap']
|
||||
mentionConfig: MentionConfig
|
||||
onMentionConfigChange: (config: MentionConfig) => void
|
||||
extractorNode?: Node<LLMNodeType>
|
||||
toolParamValue?: string
|
||||
parentAvailableNodes?: Node[]
|
||||
parentAvailableVars?: NodeOutPutVar[]
|
||||
selectableNodeTypes?: BlockEnum[]
|
||||
onSave?: (nodes: Node[], edges: Edge[]) => void
|
||||
onSyncWorkflowDraft?: SyncWorkflowDraft
|
||||
}
|
||||
|
||||
export type AgentSubGraphProps = BaseSubGraphProps & {
|
||||
variant: 'agent'
|
||||
sourceVariable: ValueSelector
|
||||
agentNodeId: string
|
||||
agentName: string
|
||||
mentionConfig: MentionConfig
|
||||
onMentionConfigChange: (config: MentionConfig) => void
|
||||
extractorNode?: Node<LLMNodeType>
|
||||
}
|
||||
|
||||
export type AssembleSubGraphProps = BaseSubGraphProps & {
|
||||
variant: 'assemble'
|
||||
title: string
|
||||
extractorNode?: Node<CodeNodeType>
|
||||
}
|
||||
|
||||
export type SubGraphProps = AgentSubGraphProps | AssembleSubGraphProps
|
||||
|
||||
export type SubGraphSliceShape = {
|
||||
parentAvailableVars: NodeOutPutVar[]
|
||||
parentAvailableNodes: Node[]
|
||||
|
||||
@ -23,7 +23,7 @@ describe('GetSchema', () => {
|
||||
it('shows an error when the URL is not http', () => {
|
||||
fireEvent.click(screen.getByText('tools.createTool.importFromUrl'))
|
||||
const input = screen.getByPlaceholderText('tools.createTool.importFromUrlPlaceHolder')
|
||||
// eslint-disable-next-line sonarjs/no-clear-text-protocols
|
||||
|
||||
fireEvent.change(input, { target: { value: 'ftp://invalid' } })
|
||||
fireEvent.click(screen.getByText('common.operation.ok'))
|
||||
|
||||
|
||||
@ -46,6 +46,7 @@ export type CommonHooksFnMap = {
|
||||
handleWorkflowTriggerWebhookRunInWorkflow: (params: { nodeId: string }) => void
|
||||
handleWorkflowTriggerPluginRunInWorkflow: (nodeId?: string) => void
|
||||
handleWorkflowRunAllTriggersInWorkflow: (nodeIds: string[]) => void
|
||||
subGraphSelectableNodeTypes?: BlockEnum[]
|
||||
availableNodesMetaData?: AvailableNodesMetaData
|
||||
getWorkflowRunAndTraceUrl: (runId?: string) => { runUrl: string, traceUrl: string }
|
||||
exportCheck?: () => Promise<void>
|
||||
@ -93,6 +94,7 @@ export const createHooksStore = ({
|
||||
handleWorkflowTriggerWebhookRunInWorkflow = noop,
|
||||
handleWorkflowTriggerPluginRunInWorkflow = noop,
|
||||
handleWorkflowRunAllTriggersInWorkflow = noop,
|
||||
subGraphSelectableNodeTypes,
|
||||
availableNodesMetaData = {
|
||||
nodes: [],
|
||||
},
|
||||
@ -136,6 +138,7 @@ export const createHooksStore = ({
|
||||
handleWorkflowTriggerWebhookRunInWorkflow,
|
||||
handleWorkflowTriggerPluginRunInWorkflow,
|
||||
handleWorkflowRunAllTriggersInWorkflow,
|
||||
subGraphSelectableNodeTypes,
|
||||
availableNodesMetaData,
|
||||
getWorkflowRunAndTraceUrl,
|
||||
exportCheck,
|
||||
|
||||
@ -89,6 +89,8 @@ import CustomIterationStartNode from './nodes/iteration-start'
|
||||
import { CUSTOM_ITERATION_START_NODE } from './nodes/iteration-start/constants'
|
||||
import CustomLoopStartNode from './nodes/loop-start'
|
||||
import { CUSTOM_LOOP_START_NODE } from './nodes/loop-start/constants'
|
||||
import CustomSubGraphStartNode from './nodes/sub-graph-start'
|
||||
import { CUSTOM_SUB_GRAPH_START_NODE } from './nodes/sub-graph-start/constants'
|
||||
import CustomNoteNode from './note-node'
|
||||
import { CUSTOM_NOTE_NODE } from './note-node/constants'
|
||||
import Operator from './operator'
|
||||
@ -119,6 +121,7 @@ const nodeTypes = {
|
||||
[CUSTOM_NODE]: CustomNode,
|
||||
[CUSTOM_NOTE_NODE]: CustomNoteNode,
|
||||
[CUSTOM_SIMPLE_NODE]: CustomSimpleNode,
|
||||
[CUSTOM_SUB_GRAPH_START_NODE]: CustomSubGraphStartNode,
|
||||
[CUSTOM_ITERATION_START_NODE]: CustomIterationStartNode,
|
||||
[CUSTOM_LOOP_START_NODE]: CustomLoopStartNode,
|
||||
[CUSTOM_DATA_SOURCE_EMPTY_NODE]: CustomDataSourceEmptyNode,
|
||||
@ -355,6 +358,7 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
||||
const dataSourceList = useStore(s => s.dataSourceList)
|
||||
// buildInTools, customTools, workflowTools, mcpTools, dataSourceList
|
||||
const configsMap = useHooksStore(s => s.configsMap)
|
||||
const subGraphSelectableNodeTypes = useHooksStore(s => s.subGraphSelectableNodeTypes)
|
||||
const [isLoadedVars, setIsLoadedVars] = useState(false)
|
||||
const [vars, setVars] = useState<VarInInspect[]>([])
|
||||
useEffect(() => {
|
||||
@ -393,12 +397,17 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
||||
|
||||
const handleNodeClickInMode = useCallback<NodeMouseHandler>(
|
||||
(event, node) => {
|
||||
if (isSubGraph && node.data.type !== BlockEnum.LLM)
|
||||
return
|
||||
if (isSubGraph) {
|
||||
const allowTypes = subGraphSelectableNodeTypes?.length
|
||||
? subGraphSelectableNodeTypes
|
||||
: [BlockEnum.LLM]
|
||||
if (!allowTypes.includes(node.data.type))
|
||||
return
|
||||
}
|
||||
|
||||
handleNodeClick(event, node)
|
||||
},
|
||||
[handleNodeClick, isSubGraph],
|
||||
[handleNodeClick, isSubGraph, subGraphSelectableNodeTypes],
|
||||
)
|
||||
|
||||
return (
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
import type { FC } from 'react'
|
||||
import type { BlockEnum } from '@/app/components/workflow/types'
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Input from '@/app/components/base/input'
|
||||
import BlockIcon from '@/app/components/workflow/block-icon'
|
||||
@ -17,9 +17,12 @@ export type AgentNode = {
|
||||
type ItemProps = {
|
||||
node: AgentNode
|
||||
onSelect: (node: AgentNode) => void
|
||||
isHighlighted?: boolean
|
||||
onSetHighlight?: () => void
|
||||
registerRef?: (element: HTMLButtonElement | null) => void
|
||||
}
|
||||
|
||||
const Item: FC<ItemProps> = ({ node, onSelect }) => {
|
||||
const Item: FC<ItemProps> = ({ node, onSelect, isHighlighted, onSetHighlight, registerRef }) => {
|
||||
const [isHovering, setIsHovering] = useState(false)
|
||||
|
||||
return (
|
||||
@ -27,10 +30,15 @@ const Item: FC<ItemProps> = ({ node, onSelect }) => {
|
||||
type="button"
|
||||
className={cn(
|
||||
'relative flex h-6 w-full cursor-pointer items-center rounded-md border-none bg-transparent px-3 text-left',
|
||||
isHovering && 'bg-state-base-hover',
|
||||
(isHovering || isHighlighted) && 'bg-state-base-hover',
|
||||
)}
|
||||
onMouseEnter={() => setIsHovering(true)}
|
||||
ref={registerRef}
|
||||
onMouseEnter={() => {
|
||||
setIsHovering(true)
|
||||
onSetHighlight?.()
|
||||
}}
|
||||
onMouseLeave={() => setIsHovering(false)}
|
||||
onFocus={onSetHighlight}
|
||||
onClick={() => onSelect(node)}
|
||||
onMouseDown={e => e.preventDefault()}
|
||||
>
|
||||
@ -58,6 +66,8 @@ type Props = {
|
||||
searchBoxClassName?: string
|
||||
maxHeightClass?: string
|
||||
autoFocus?: boolean
|
||||
externalSearchText?: string
|
||||
enableKeyboardNavigation?: boolean
|
||||
}
|
||||
|
||||
const AgentNodeList: FC<Props> = ({
|
||||
@ -69,9 +79,15 @@ const AgentNodeList: FC<Props> = ({
|
||||
searchBoxClassName,
|
||||
maxHeightClass,
|
||||
autoFocus = true,
|
||||
externalSearchText,
|
||||
enableKeyboardNavigation = false,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [searchText, setSearchText] = useState('')
|
||||
const normalizedSearchText = externalSearchText === undefined ? searchText : externalSearchText
|
||||
const normalizedSearchTextTrimmed = normalizedSearchText.trim()
|
||||
const normalizedSearchTextLower = normalizedSearchTextTrimmed.toLowerCase()
|
||||
const shouldShowSearchInput = !hideSearch && externalSearchText === undefined
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
@ -80,15 +96,107 @@ const AgentNodeList: FC<Props> = ({
|
||||
}
|
||||
}
|
||||
|
||||
const filteredNodes = nodes.filter((node) => {
|
||||
if (!searchText)
|
||||
const filteredNodes = useMemo(() => nodes.filter((node) => {
|
||||
if (!normalizedSearchTextTrimmed)
|
||||
return true
|
||||
return node.title.toLowerCase().includes(searchText.toLowerCase())
|
||||
})
|
||||
return node.title.toLowerCase().includes(normalizedSearchTextLower)
|
||||
}), [nodes, normalizedSearchTextLower, normalizedSearchTextTrimmed])
|
||||
|
||||
const [activeIndex, setActiveIndex] = useState(-1)
|
||||
const itemRefs = useRef<Array<HTMLButtonElement | null>>([])
|
||||
const lastInteractionRef = useRef<'keyboard' | 'mouse' | 'filter' | null>(null)
|
||||
const filteredNodesRef = useRef(filteredNodes)
|
||||
const activeIndexRef = useRef(activeIndex)
|
||||
const onCloseRef = useRef(onClose)
|
||||
const resolvedActiveIndex = useMemo(() => {
|
||||
if (!enableKeyboardNavigation || filteredNodes.length === 0)
|
||||
return -1
|
||||
if (activeIndex < 0 || activeIndex >= filteredNodes.length)
|
||||
return 0
|
||||
return activeIndex
|
||||
}, [activeIndex, enableKeyboardNavigation, filteredNodes.length])
|
||||
|
||||
useEffect(() => {
|
||||
itemRefs.current = []
|
||||
}, [filteredNodes.length])
|
||||
|
||||
useEffect(() => {
|
||||
filteredNodesRef.current = filteredNodes
|
||||
}, [filteredNodes])
|
||||
|
||||
useEffect(() => {
|
||||
activeIndexRef.current = resolvedActiveIndex
|
||||
}, [resolvedActiveIndex])
|
||||
|
||||
useEffect(() => {
|
||||
onCloseRef.current = onClose
|
||||
}, [onClose])
|
||||
|
||||
const handleHighlightIndex = useCallback((index: number, source: 'keyboard' | 'mouse' | 'filter') => {
|
||||
lastInteractionRef.current = source
|
||||
setActiveIndex(index)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!enableKeyboardNavigation || filteredNodes.length === 0) {
|
||||
lastInteractionRef.current = 'filter'
|
||||
return
|
||||
}
|
||||
if (activeIndex < 0 || activeIndex >= filteredNodes.length)
|
||||
lastInteractionRef.current = 'filter'
|
||||
}, [activeIndex, enableKeyboardNavigation, filteredNodes.length])
|
||||
|
||||
useEffect(() => {
|
||||
if (!enableKeyboardNavigation || resolvedActiveIndex < 0)
|
||||
return
|
||||
if (lastInteractionRef.current !== 'keyboard')
|
||||
return
|
||||
const target = itemRefs.current[resolvedActiveIndex]
|
||||
if (target)
|
||||
target.scrollIntoView({ block: 'nearest' })
|
||||
lastInteractionRef.current = null
|
||||
}, [enableKeyboardNavigation, filteredNodes.length, resolvedActiveIndex])
|
||||
|
||||
const handleSelectItem = useCallback((node: AgentNode) => {
|
||||
onSelect(node)
|
||||
}, [onSelect])
|
||||
|
||||
useEffect(() => {
|
||||
if (!enableKeyboardNavigation)
|
||||
return
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
const nodes = filteredNodesRef.current
|
||||
if (nodes.length === 0)
|
||||
return
|
||||
if (!['ArrowDown', 'ArrowUp', 'Enter', 'Escape'].includes(event.key))
|
||||
return
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
if (event.key === 'Escape') {
|
||||
onCloseRef.current?.()
|
||||
return
|
||||
}
|
||||
if (event.key === 'Enter') {
|
||||
const index = activeIndexRef.current
|
||||
if (index < 0 || index >= nodes.length)
|
||||
return
|
||||
handleSelectItem(nodes[index])
|
||||
return
|
||||
}
|
||||
const delta = event.key === 'ArrowDown' ? 1 : -1
|
||||
const baseIndex = activeIndexRef.current < 0 ? 0 : activeIndexRef.current
|
||||
const nextIndex = Math.min(Math.max(baseIndex + delta, 0), nodes.length - 1)
|
||||
handleHighlightIndex(nextIndex, 'keyboard')
|
||||
}
|
||||
document.addEventListener('keydown', handleKeyDown, true)
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown, true)
|
||||
}
|
||||
}, [enableKeyboardNavigation, handleHighlightIndex, handleSelectItem])
|
||||
|
||||
return (
|
||||
<>
|
||||
{!hideSearch && (
|
||||
{shouldShowSearchInput && (
|
||||
<>
|
||||
<div className={cn('mx-2 mb-2 mt-2', searchBoxClassName)}>
|
||||
<Input
|
||||
@ -114,11 +222,18 @@ const AgentNodeList: FC<Props> = ({
|
||||
{filteredNodes.length > 0
|
||||
? (
|
||||
<div className={cn('max-h-[85vh] overflow-y-auto py-1', maxHeightClass)}>
|
||||
{filteredNodes.map(node => (
|
||||
{filteredNodes.map((node, index) => (
|
||||
<Item
|
||||
key={node.id}
|
||||
node={node}
|
||||
onSelect={onSelect}
|
||||
isHighlighted={enableKeyboardNavigation && index === resolvedActiveIndex}
|
||||
onSetHighlight={enableKeyboardNavigation ? () => handleHighlightIndex(index, 'mouse') : undefined}
|
||||
registerRef={enableKeyboardNavigation
|
||||
? (element) => {
|
||||
itemRefs.current[index] = element
|
||||
}
|
||||
: undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -4,16 +4,13 @@ import type { CodeLanguage } from '../../code/types'
|
||||
import type { GenRes } from '@/service/debug'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { GetCodeGeneratorResModal } from '@/app/components/app/configuration/config/code-generator/get-code-generator-res'
|
||||
import { ActionButton } from '@/app/components/base/action-button'
|
||||
import { Generator } from '@/app/components/base/icons/src/vender/other'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { useHooksStore } from '../../../hooks-store'
|
||||
import { useStore } from '../../../store'
|
||||
import { BlockEnum } from '../../../types'
|
||||
import ContextGenerateModal from '../../tool/components/context-generate-modal'
|
||||
|
||||
type Props = {
|
||||
nodeId: string
|
||||
@ -31,39 +28,12 @@ const CodeGenerateBtn: FC<Props> = ({
|
||||
onGenerated,
|
||||
}) => {
|
||||
const [showAutomatic, { setTrue: showAutomaticTrue, setFalse: showAutomaticFalse }] = useBoolean(false)
|
||||
const nodes = useStore(s => s.nodes)
|
||||
const handleAutomaticRes = useCallback((res: GenRes) => {
|
||||
onGenerated?.(res.modified)
|
||||
showAutomaticFalse()
|
||||
}, [onGenerated, showAutomaticFalse])
|
||||
const configsMap = useHooksStore(s => s.configsMap)
|
||||
|
||||
const parseExtractorNodeId = useCallback((id: string) => {
|
||||
const marker = '_ext_'
|
||||
const index = id.lastIndexOf(marker)
|
||||
if (index < 0)
|
||||
return null
|
||||
const parentId = id.slice(0, index)
|
||||
const paramKey = id.slice(index + marker.length)
|
||||
if (!parentId || !paramKey)
|
||||
return null
|
||||
return { parentId, paramKey }
|
||||
}, [])
|
||||
|
||||
const contextGenerateConfig = useMemo(() => {
|
||||
const targetNode = nodes.find(node => node.id === nodeId)
|
||||
const isCodeNode = targetNode?.data?.type === BlockEnum.Code
|
||||
const parentNodeId = (targetNode?.data as { parent_node_id?: string })?.parent_node_id
|
||||
const parsed = parseExtractorNodeId(nodeId)
|
||||
if (!isCodeNode || !parentNodeId || !parsed?.paramKey)
|
||||
return null
|
||||
return {
|
||||
toolNodeId: parentNodeId || parsed.parentId,
|
||||
paramKey: parsed.paramKey,
|
||||
codeNodeId: nodeId,
|
||||
}
|
||||
}, [nodeId, nodes, parseExtractorNodeId])
|
||||
|
||||
return (
|
||||
<div className={cn(className)}>
|
||||
<ActionButton
|
||||
@ -73,28 +43,16 @@ const CodeGenerateBtn: FC<Props> = ({
|
||||
<Generator className="h-4 w-4 text-primary-600" />
|
||||
</ActionButton>
|
||||
{showAutomatic && (
|
||||
contextGenerateConfig
|
||||
? (
|
||||
<ContextGenerateModal
|
||||
isShow={showAutomatic}
|
||||
onClose={showAutomaticFalse}
|
||||
toolNodeId={contextGenerateConfig.toolNodeId}
|
||||
paramKey={contextGenerateConfig.paramKey}
|
||||
codeNodeId={contextGenerateConfig.codeNodeId}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<GetCodeGeneratorResModal
|
||||
mode={AppModeEnum.CHAT}
|
||||
isShow={showAutomatic}
|
||||
codeLanguages={codeLanguages}
|
||||
onClose={showAutomaticFalse}
|
||||
onFinished={handleAutomaticRes}
|
||||
flowId={configsMap?.flowId || ''}
|
||||
nodeId={nodeId}
|
||||
currentCode={currentCode}
|
||||
/>
|
||||
)
|
||||
<GetCodeGeneratorResModal
|
||||
mode={AppModeEnum.CHAT}
|
||||
isShow={showAutomatic}
|
||||
codeLanguages={codeLanguages}
|
||||
onClose={showAutomaticFalse}
|
||||
onFinished={handleAutomaticRes}
|
||||
flowId={configsMap?.flowId || ''}
|
||||
nodeId={nodeId}
|
||||
currentCode={currentCode}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -474,7 +474,6 @@ const formatItem = (
|
||||
break
|
||||
}
|
||||
|
||||
// eslint-disable-next-line sonarjs/no-duplicated-branches
|
||||
case BlockEnum.VariableAggregator: {
|
||||
const { output_type, advanced_settings }
|
||||
= data as VariableAssignerNodeType
|
||||
@ -1875,7 +1874,7 @@ export const updateNodeVars = (
|
||||
}
|
||||
break
|
||||
}
|
||||
// eslint-disable-next-line sonarjs/no-duplicated-branches
|
||||
|
||||
case BlockEnum.VariableAggregator: {
|
||||
const payload = data as VariableAssignerNodeType
|
||||
if (payload.variables) {
|
||||
|
||||
@ -6,7 +6,7 @@ import type { NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflo
|
||||
import { useHover } from 'ahooks'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import { AssembleVariables, CodeAssistant, MagicEdit } from '@/app/components/base/icons/src/vender/line/general'
|
||||
@ -43,6 +43,31 @@ type ItemProps = {
|
||||
zIndex?: number
|
||||
className?: string
|
||||
preferSchemaType?: boolean
|
||||
isHighlighted?: boolean
|
||||
onSetHighlight?: () => void
|
||||
registerRef?: (element: HTMLDivElement | null) => void
|
||||
}
|
||||
|
||||
const buildValueSelector = ({
|
||||
nodeId,
|
||||
objPath,
|
||||
itemData,
|
||||
isFlat,
|
||||
}: {
|
||||
nodeId: string
|
||||
objPath: string[]
|
||||
itemData: Var
|
||||
isFlat?: boolean
|
||||
}): ValueSelector => {
|
||||
if (isFlat)
|
||||
return [itemData.variable]
|
||||
const isSys = itemData.variable.startsWith('sys.')
|
||||
const isEnv = itemData.variable.startsWith('env.')
|
||||
const isChatVar = itemData.variable.startsWith('conversation.')
|
||||
const isRagVariable = itemData.isRagVariable
|
||||
if (isSys || isEnv || isChatVar || isRagVariable)
|
||||
return [...objPath, ...itemData.variable.split('.')]
|
||||
return [nodeId, ...objPath, itemData.variable]
|
||||
}
|
||||
|
||||
const Item: FC<ItemProps> = ({
|
||||
@ -60,6 +85,9 @@ const Item: FC<ItemProps> = ({
|
||||
zIndex,
|
||||
className,
|
||||
preferSchemaType,
|
||||
isHighlighted,
|
||||
onSetHighlight,
|
||||
registerRef,
|
||||
}) => {
|
||||
const isStructureOutput = itemData.type === VarType.object && (itemData.children as StructuredOutput)?.schema?.properties
|
||||
const isFile = itemData.type === VarType.file && !isStructureOutput
|
||||
@ -123,6 +151,10 @@ const Item: FC<ItemProps> = ({
|
||||
})()
|
||||
|
||||
const itemRef = useRef<HTMLDivElement>(null)
|
||||
const setItemRef = useCallback((element: HTMLDivElement | null) => {
|
||||
itemRef.current = element
|
||||
registerRef?.(element)
|
||||
}, [registerRef])
|
||||
const [isItemHovering, setIsItemHovering] = useState(false)
|
||||
useHover(itemRef, {
|
||||
onChange: (hovering) => {
|
||||
@ -152,15 +184,12 @@ const Item: FC<ItemProps> = ({
|
||||
if (!isSupportFileVar && isFile)
|
||||
return
|
||||
|
||||
if (isFlat) {
|
||||
onChange([itemData.variable], itemData)
|
||||
}
|
||||
else if (isSys || isEnv || isChatVar || isRagVariable) { // system variable | environment variable | conversation variable
|
||||
onChange([...objPath, ...itemData.variable.split('.')], itemData)
|
||||
}
|
||||
else {
|
||||
onChange([nodeId, ...objPath, itemData.variable], itemData)
|
||||
}
|
||||
onChange(buildValueSelector({
|
||||
nodeId,
|
||||
objPath,
|
||||
itemData,
|
||||
isFlat,
|
||||
}), itemData)
|
||||
}
|
||||
const variableCategory = useMemo(() => {
|
||||
if (isEnv)
|
||||
@ -181,14 +210,15 @@ const Item: FC<ItemProps> = ({
|
||||
>
|
||||
<PortalToFollowElemTrigger className="w-full">
|
||||
<div
|
||||
ref={itemRef}
|
||||
ref={setItemRef}
|
||||
className={cn(
|
||||
(isObj || isStructureOutput) ? ' pr-1' : 'pr-[18px]',
|
||||
isHovering && ((isObj || isStructureOutput) ? 'bg-components-panel-on-panel-item-bg-hover' : 'bg-state-base-hover'),
|
||||
(isHovering || isHighlighted) && ((isObj || isStructureOutput) ? 'bg-components-panel-on-panel-item-bg-hover' : 'bg-state-base-hover'),
|
||||
'relative flex h-6 w-full cursor-pointer items-center rounded-md pl-3',
|
||||
className,
|
||||
)}
|
||||
onClick={handleChosen}
|
||||
onMouseEnter={onSetHighlight}
|
||||
onMouseDown={e => e.preventDefault()}
|
||||
>
|
||||
<div className="flex w-0 grow items-center">
|
||||
@ -259,6 +289,8 @@ type Props = {
|
||||
onAssembleVariables?: () => ValueSelector | null
|
||||
autoFocus?: boolean
|
||||
preferSchemaType?: boolean
|
||||
externalSearchText?: string
|
||||
enableKeyboardNavigation?: boolean
|
||||
}
|
||||
const VarReferenceVars: FC<Props> = ({
|
||||
hideSearch,
|
||||
@ -278,9 +310,15 @@ const VarReferenceVars: FC<Props> = ({
|
||||
onAssembleVariables,
|
||||
autoFocus = true,
|
||||
preferSchemaType,
|
||||
externalSearchText,
|
||||
enableKeyboardNavigation = false,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [searchText, setSearchText] = useState('')
|
||||
const normalizedSearchText = externalSearchText === undefined ? searchText : externalSearchText
|
||||
const normalizedSearchTextTrimmed = normalizedSearchText.trim()
|
||||
const normalizedSearchTextLower = normalizedSearchTextTrimmed.toLowerCase()
|
||||
const shouldShowSearchInput = !hideSearch && externalSearchText === undefined
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
@ -296,35 +334,159 @@ const VarReferenceVars: FC<Props> = ({
|
||||
onClose?.()
|
||||
}
|
||||
|
||||
const filteredVars = vars.filter((v) => {
|
||||
const children = v.vars.filter(v => checkKeys([v.variable], false).isValid || isSpecialVar(v.variable.split('.')[0]))
|
||||
return children.length > 0
|
||||
}).filter((node) => {
|
||||
if (!searchText)
|
||||
return node
|
||||
const children = node.vars.filter((v) => {
|
||||
const searchTextLower = searchText.toLowerCase()
|
||||
return v.variable.toLowerCase().includes(searchTextLower) || node.title.toLowerCase().includes(searchTextLower)
|
||||
const validatedVars = useMemo(() => {
|
||||
const res: NodeOutPutVar[] = []
|
||||
vars.forEach((node) => {
|
||||
const nodeVars = node.vars.filter(v => checkKeys([v.variable], false).isValid || isSpecialVar(v.variable.split('.')[0]))
|
||||
if (nodeVars.length === 0)
|
||||
return
|
||||
res.push({
|
||||
...node,
|
||||
vars: nodeVars,
|
||||
})
|
||||
})
|
||||
return children.length > 0
|
||||
}).map((node) => {
|
||||
let vars = node.vars.filter(v => checkKeys([v.variable], false).isValid || isSpecialVar(v.variable.split('.')[0]))
|
||||
if (searchText) {
|
||||
const searchTextLower = searchText.toLowerCase()
|
||||
if (!node.title.toLowerCase().includes(searchTextLower))
|
||||
vars = vars.filter(v => v.variable.toLowerCase().includes(searchText.toLowerCase()))
|
||||
}
|
||||
return res
|
||||
}, [vars])
|
||||
|
||||
return {
|
||||
...node,
|
||||
vars,
|
||||
const filteredVars = useMemo(() => {
|
||||
if (!normalizedSearchTextTrimmed)
|
||||
return validatedVars
|
||||
const res: NodeOutPutVar[] = []
|
||||
validatedVars.forEach((node) => {
|
||||
const titleLower = node.title.toLowerCase()
|
||||
const matchedByTitle = titleLower.includes(normalizedSearchTextLower)
|
||||
const nodeVars = matchedByTitle
|
||||
? node.vars
|
||||
: node.vars.filter(v => v.variable.toLowerCase().includes(normalizedSearchTextLower))
|
||||
if (nodeVars.length === 0)
|
||||
return
|
||||
res.push({
|
||||
...node,
|
||||
vars: nodeVars,
|
||||
})
|
||||
})
|
||||
return res
|
||||
}, [normalizedSearchTextLower, normalizedSearchTextTrimmed, validatedVars])
|
||||
|
||||
const flatItems = useMemo(() => {
|
||||
const items: Array<{ node: NodeOutPutVar, itemData: Var }> = []
|
||||
filteredVars.forEach((node) => {
|
||||
node.vars.forEach((itemData) => {
|
||||
items.push({ node, itemData })
|
||||
})
|
||||
})
|
||||
return items
|
||||
}, [filteredVars])
|
||||
const [activeIndex, setActiveIndex] = useState(-1)
|
||||
const itemRefs = useRef<Array<HTMLDivElement | null>>([])
|
||||
const lastInteractionRef = useRef<'keyboard' | 'mouse' | 'filter' | null>(null)
|
||||
const flatItemsRef = useRef(flatItems)
|
||||
const activeIndexRef = useRef(activeIndex)
|
||||
const onCloseRef = useRef(onClose)
|
||||
const resolvedActiveIndex = useMemo(() => {
|
||||
if (!enableKeyboardNavigation || flatItems.length === 0)
|
||||
return -1
|
||||
if (activeIndex < 0 || activeIndex >= flatItems.length)
|
||||
return 0
|
||||
return activeIndex
|
||||
}, [activeIndex, enableKeyboardNavigation, flatItems.length])
|
||||
|
||||
useEffect(() => {
|
||||
itemRefs.current = []
|
||||
}, [flatItems.length])
|
||||
|
||||
useEffect(() => {
|
||||
flatItemsRef.current = flatItems
|
||||
}, [flatItems])
|
||||
|
||||
useEffect(() => {
|
||||
activeIndexRef.current = resolvedActiveIndex
|
||||
}, [resolvedActiveIndex])
|
||||
|
||||
useEffect(() => {
|
||||
onCloseRef.current = onClose
|
||||
}, [onClose])
|
||||
|
||||
const handleHighlightIndex = useCallback((index: number, source: 'keyboard' | 'mouse' | 'filter') => {
|
||||
lastInteractionRef.current = source
|
||||
setActiveIndex(index)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!enableKeyboardNavigation || flatItems.length === 0) {
|
||||
lastInteractionRef.current = 'filter'
|
||||
return
|
||||
}
|
||||
})
|
||||
if (activeIndex < 0 || activeIndex >= flatItems.length)
|
||||
lastInteractionRef.current = 'filter'
|
||||
}, [activeIndex, enableKeyboardNavigation, flatItems.length])
|
||||
|
||||
useEffect(() => {
|
||||
if (!enableKeyboardNavigation || resolvedActiveIndex < 0)
|
||||
return
|
||||
if (lastInteractionRef.current !== 'keyboard')
|
||||
return
|
||||
const target = itemRefs.current[resolvedActiveIndex]
|
||||
if (target)
|
||||
target.scrollIntoView({ block: 'nearest' })
|
||||
lastInteractionRef.current = null
|
||||
}, [enableKeyboardNavigation, flatItems.length, resolvedActiveIndex])
|
||||
|
||||
const handleSelectItem = useCallback((item: { node: NodeOutPutVar, itemData: Var }) => {
|
||||
const isStructureOutput = item.itemData.type === VarType.object
|
||||
&& (item.itemData.children as StructuredOutput | undefined)?.schema?.properties
|
||||
const isFile = item.itemData.type === VarType.file && !isStructureOutput
|
||||
if (!isSupportFileVar && isFile)
|
||||
return
|
||||
const valueSelector = buildValueSelector({
|
||||
nodeId: item.node.nodeId,
|
||||
objPath: [],
|
||||
itemData: item.itemData,
|
||||
isFlat: item.node.isFlat,
|
||||
})
|
||||
onChange(valueSelector, item.itemData)
|
||||
onClose?.()
|
||||
}, [onChange, onClose, isSupportFileVar])
|
||||
|
||||
useEffect(() => {
|
||||
if (!enableKeyboardNavigation)
|
||||
return
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
const items = flatItemsRef.current
|
||||
if (items.length === 0)
|
||||
return
|
||||
if (!['ArrowDown', 'ArrowUp', 'Enter', 'Escape'].includes(event.key))
|
||||
return
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
if (event.key === 'Escape') {
|
||||
onCloseRef.current?.()
|
||||
return
|
||||
}
|
||||
if (event.key === 'Enter') {
|
||||
const index = activeIndexRef.current
|
||||
if (index < 0 || index >= items.length)
|
||||
return
|
||||
handleSelectItem(items[index])
|
||||
return
|
||||
}
|
||||
const delta = event.key === 'ArrowDown' ? 1 : -1
|
||||
const baseIndex = activeIndexRef.current < 0 ? 0 : activeIndexRef.current
|
||||
const nextIndex = Math.min(Math.max(baseIndex + delta, 0), items.length - 1)
|
||||
handleHighlightIndex(nextIndex, 'keyboard')
|
||||
}
|
||||
document.addEventListener('keydown', handleKeyDown, true)
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown, true)
|
||||
}
|
||||
}, [enableKeyboardNavigation, handleHighlightIndex, handleSelectItem])
|
||||
|
||||
let runningIndex = -1
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
!hideSearch && (
|
||||
shouldShowSearchInput && (
|
||||
<>
|
||||
<div className={cn('var-search-input-wrapper mx-2 mb-2 mt-2', searchBoxClassName)} onClick={e => e.stopPropagation()}>
|
||||
<Input
|
||||
@ -351,6 +513,25 @@ const VarReferenceVars: FC<Props> = ({
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
showAssembleVariables && (
|
||||
<div className="flex items-center border-t border-divider-subtle pt-1">
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-6 w-full items-center rounded-md pl-3 pr-[18px] text-text-secondary hover:bg-state-base-hover"
|
||||
onClick={handleAssembleVariables}
|
||||
onMouseDown={e => e.preventDefault()}
|
||||
>
|
||||
<span className="mr-1 flex h-4 w-4 items-center justify-center rounded bg-util-colors-blue-blue-500">
|
||||
<AssembleVariables className="h-3 w-3 text-text-primary-on-surface" />
|
||||
</span>
|
||||
<span className="system-xs-medium truncate" title={t('nodes.tool.assembleVariables', { ns: 'workflow' })}>
|
||||
{t('nodes.tool.assembleVariables', { ns: 'workflow' })}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{filteredVars.length > 0
|
||||
? (
|
||||
<div className={cn('max-h-[85vh] overflow-y-auto', maxHeightClass)}>
|
||||
@ -366,24 +547,35 @@ const VarReferenceVars: FC<Props> = ({
|
||||
{item.title}
|
||||
</div>
|
||||
)}
|
||||
{item.vars.map((v, j) => (
|
||||
<Item
|
||||
key={j}
|
||||
title={item.title}
|
||||
nodeId={item.nodeId}
|
||||
objPath={[]}
|
||||
itemData={v}
|
||||
onChange={onChange}
|
||||
itemWidth={itemWidth}
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
isException={v.isException}
|
||||
isLoopVar={item.isLoop}
|
||||
isFlat={item.isFlat}
|
||||
isInCodeGeneratorInstructionEditor={isInCodeGeneratorInstructionEditor}
|
||||
zIndex={zIndex}
|
||||
preferSchemaType={preferSchemaType}
|
||||
/>
|
||||
))}
|
||||
{item.vars.map((v, j) => {
|
||||
runningIndex += 1
|
||||
const itemIndex = runningIndex
|
||||
return (
|
||||
<Item
|
||||
key={j}
|
||||
title={item.title}
|
||||
nodeId={item.nodeId}
|
||||
objPath={[]}
|
||||
itemData={v}
|
||||
onChange={onChange}
|
||||
itemWidth={itemWidth}
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
isException={v.isException}
|
||||
isLoopVar={item.isLoop}
|
||||
isFlat={item.isFlat}
|
||||
isInCodeGeneratorInstructionEditor={isInCodeGeneratorInstructionEditor}
|
||||
zIndex={zIndex}
|
||||
preferSchemaType={preferSchemaType}
|
||||
isHighlighted={enableKeyboardNavigation && itemIndex === resolvedActiveIndex}
|
||||
onSetHighlight={enableKeyboardNavigation ? () => handleHighlightIndex(itemIndex, 'mouse') : undefined}
|
||||
registerRef={enableKeyboardNavigation
|
||||
? (element) => {
|
||||
itemRefs.current[itemIndex] = element
|
||||
}
|
||||
: undefined}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{item.isFlat && !filteredVars[i + 1]?.isFlat && !!filteredVars.find(item => !item.isFlat) && (
|
||||
<div className="relative mt-[14px] flex items-center space-x-1">
|
||||
<div className="h-0 w-3 shrink-0 border border-divider-subtle"></div>
|
||||
@ -404,25 +596,6 @@ const VarReferenceVars: FC<Props> = ({
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
showAssembleVariables && (
|
||||
<div className="flex items-center border-t border-divider-subtle pt-1">
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-6 w-full items-center rounded-md pl-3 pr-[18px] text-text-secondary hover:bg-state-base-hover"
|
||||
onClick={handleAssembleVariables}
|
||||
onMouseDown={e => e.preventDefault()}
|
||||
>
|
||||
<span className="mr-1 flex h-4 w-4 items-center justify-center rounded bg-util-colors-blue-blue-500">
|
||||
<AssembleVariables className="h-3 w-3 text-text-primary-on-surface" />
|
||||
</span>
|
||||
<span className="system-xs-medium truncate" title={t('nodes.tool.assembleVariables', { ns: 'workflow' })}>
|
||||
{t('nodes.tool.assembleVariables', { ns: 'workflow' })}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -625,7 +625,7 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
</div>
|
||||
<Split />
|
||||
{
|
||||
allowGraphActions && hasRetryNode(data.type) && (
|
||||
hasRetryNode(data.type) && (
|
||||
<RetryOnPanel
|
||||
id={id}
|
||||
data={data}
|
||||
@ -633,7 +633,7 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
)
|
||||
}
|
||||
{
|
||||
allowGraphActions && hasErrorHandleNode(data.type) && (
|
||||
hasErrorHandleNode(data.type) && (
|
||||
<ErrorHandleOnPanel
|
||||
id={id}
|
||||
data={data}
|
||||
|
||||
@ -56,17 +56,21 @@ const useConfig = (id: string, payload: CodeNodeType) => {
|
||||
setInputs,
|
||||
})
|
||||
|
||||
const [outputKeyOrders, setOutputKeyOrders] = useState<string[]>([])
|
||||
const [outputKeyOrders, setOutputKeyOrders] = useState<string[]>(() => Object.keys(payload.outputs || {}))
|
||||
const syncOutputKeyOrders = useCallback((outputs: OutputVar) => {
|
||||
setOutputKeyOrders(Object.keys(outputs))
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
if (inputs.code) {
|
||||
if (inputs.outputs && Object.keys(inputs.outputs).length > 0)
|
||||
syncOutputKeyOrders(inputs.outputs)
|
||||
const outputKeys = inputs.outputs ? Object.keys(inputs.outputs) : []
|
||||
if (outputKeys.length > 0 && outputKeyOrders.length === 0)
|
||||
syncOutputKeyOrders(inputs.outputs)
|
||||
|
||||
const hasExistingConfig = Boolean(inputs.code)
|
||||
|| (inputs.variables?.length ?? 0) > 0
|
||||
|| outputKeys.length > 0
|
||||
|
||||
if (hasExistingConfig)
|
||||
return
|
||||
}
|
||||
|
||||
const isReady = defaultConfig && Object.keys(defaultConfig).length > 0
|
||||
if (isReady) {
|
||||
@ -76,7 +80,7 @@ const useConfig = (id: string, payload: CodeNodeType) => {
|
||||
})
|
||||
syncOutputKeyOrders(defaultConfig.outputs)
|
||||
}
|
||||
}, [defaultConfig])
|
||||
}, [defaultConfig, inputs.code, inputs.outputs, inputs.variables, outputKeyOrders.length, setInputs, syncOutputKeyOrders])
|
||||
|
||||
const handleCodeChange = useCallback((code: string) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
|
||||
@ -139,20 +139,22 @@ const Panel: FC<NodePanelProps<DataSourceNodeType>> = ({ id, data }) => {
|
||||
|
||||
return (
|
||||
<div key={outputItem.name}>
|
||||
{outputItem.value?.type === 'object' ? (
|
||||
<StructureOutputItem
|
||||
rootClassName="code-sm-semibold text-text-secondary"
|
||||
payload={wrapStructuredVarItem(outputItem, schemaType)}
|
||||
/>
|
||||
) : (
|
||||
<VarItem
|
||||
name={outputItem.name}
|
||||
// eslint-disable-next-line sonarjs/no-nested-template-literals
|
||||
type={`${outputItem.type.toLocaleLowerCase()}${schemaType ? ` (${schemaType})` : ''}`}
|
||||
description={outputItem.description}
|
||||
isIndent={hasObjectOutput}
|
||||
/>
|
||||
)}
|
||||
{outputItem.value?.type === 'object'
|
||||
? (
|
||||
<StructureOutputItem
|
||||
rootClassName="code-sm-semibold text-text-secondary"
|
||||
payload={wrapStructuredVarItem(outputItem, schemaType)}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<VarItem
|
||||
name={outputItem.name}
|
||||
|
||||
type={`${outputItem.type.toLocaleLowerCase()}${schemaType ? ` (${schemaType})` : ''}`}
|
||||
description={outputItem.description}
|
||||
isIndent={hasObjectOutput}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
@ -100,7 +100,7 @@ const RetrievalConfig: FC<Props> = ({
|
||||
score_threshold: configs.score_threshold_enabled ? (configs.score_threshold ?? DATASET_DEFAULT.score_threshold) : null,
|
||||
reranking_model: retrieval_mode === RETRIEVE_TYPE.oneWay
|
||||
? undefined
|
||||
// eslint-disable-next-line sonarjs/no-nested-conditional
|
||||
|
||||
: (!configs.reranking_model?.reranking_provider_name
|
||||
? undefined
|
||||
: {
|
||||
|
||||
@ -135,7 +135,7 @@ export const getMultipleRetrievalConfig = (
|
||||
vector_setting: {
|
||||
vector_weight: allHighQualityVectorSearch
|
||||
? DEFAULT_WEIGHTED_SCORE.allHighQualityVectorSearch.semantic
|
||||
// eslint-disable-next-line sonarjs/no-nested-conditional
|
||||
|
||||
: allHighQualityFullTextSearch
|
||||
? DEFAULT_WEIGHTED_SCORE.allHighQualityFullTextSearch.semantic
|
||||
: DEFAULT_WEIGHTED_SCORE.other.semantic,
|
||||
@ -145,7 +145,7 @@ export const getMultipleRetrievalConfig = (
|
||||
keyword_setting: {
|
||||
keyword_weight: allHighQualityVectorSearch
|
||||
? DEFAULT_WEIGHTED_SCORE.allHighQualityVectorSearch.keyword
|
||||
// eslint-disable-next-line sonarjs/no-nested-conditional
|
||||
|
||||
: allHighQualityFullTextSearch
|
||||
? DEFAULT_WEIGHTED_SCORE.allHighQualityFullTextSearch.keyword
|
||||
: DEFAULT_WEIGHTED_SCORE.other.keyword,
|
||||
@ -232,7 +232,6 @@ export const getMultipleRetrievalConfig = (
|
||||
result.reranking_mode = RerankingModeEnum.RerankingModel
|
||||
result.reranking_enable = true
|
||||
|
||||
// eslint-disable-next-line sonarjs/nested-control-flow
|
||||
if ((!result.reranking_model?.provider || !result.reranking_model?.model) && isFallbackRerankModelValid) {
|
||||
result.reranking_model = {
|
||||
provider: fallbackRerankModel.provider || '',
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export const CUSTOM_SUB_GRAPH_START_NODE = 'custom-sub-graph-start'
|
||||
60
web/app/components/workflow/nodes/sub-graph-start/index.tsx
Normal file
60
web/app/components/workflow/nodes/sub-graph-start/index.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import type { NodeProps } from 'reactflow'
|
||||
import type { CommonNodeType } from '@/app/components/workflow/types'
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { AssembleVariablesAlt } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { Agent } from '@/app/components/base/icons/src/vender/workflow'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { NodeSourceHandle } from '@/app/components/workflow/nodes/_base/components/node-handle'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
type SubGraphStartNodeData = CommonNodeType<{
|
||||
tooltip?: string
|
||||
iconType?: string
|
||||
}>
|
||||
|
||||
type IconComponent = typeof Agent
|
||||
|
||||
const iconMap: Record<string, IconComponent> = {
|
||||
agent: Agent,
|
||||
assemble: AssembleVariablesAlt,
|
||||
}
|
||||
|
||||
const SubGraphStartNode = ({ id, data }: NodeProps<SubGraphStartNodeData>) => {
|
||||
const { t } = useTranslation()
|
||||
const iconType = data?.iconType || 'agent'
|
||||
const Icon = iconMap[iconType] || Agent
|
||||
const rawTitle = data?.title?.trim() || ''
|
||||
const showTitle = iconType === 'agent' && !!rawTitle
|
||||
const displayTitle = showTitle && (rawTitle.startsWith('@') ? rawTitle : `@${rawTitle}`)
|
||||
const tooltip = data?.tooltip
|
||||
|| (iconType === 'assemble' ? t('blocks.start', { ns: 'workflow' }) : (data?.title || t('blocks.start', { ns: 'workflow' })))
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'nodrag group mt-1 flex h-11 items-center justify-center rounded-2xl border border-workflow-block-border bg-workflow-block-bg shadow-xs',
|
||||
showTitle ? 'gap-1.5 px-2' : 'w-11',
|
||||
)}
|
||||
>
|
||||
<Tooltip popupContent={tooltip} asChild={false}>
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded-full border-[0.5px] border-components-panel-border-subtle bg-util-colors-blue-brand-blue-brand-500">
|
||||
<Icon className="h-3 w-3 text-text-primary-on-surface" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
{showTitle && (
|
||||
<span className="system-xs-medium max-w-[160px] truncate text-text-secondary">
|
||||
{displayTitle}
|
||||
</span>
|
||||
)}
|
||||
<NodeSourceHandle
|
||||
id={id}
|
||||
data={data}
|
||||
handleClassName="!top-1/2 !-right-[9px] !-translate-y-1/2"
|
||||
handleId="source"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(SubGraphStartNode)
|
||||
@ -1,538 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import type { CodeNodeType } from '@/app/components/workflow/nodes/code/types'
|
||||
import type { OutputVar } from '@/app/components/workflow/nodes/code/types'
|
||||
import type { ContextGenerateMessage, ContextGenerateResponse } from '@/service/debug'
|
||||
import type { AppModeEnum, CompletionParams, Model, ModelModeType } from '@/types/app'
|
||||
import {
|
||||
RiSendPlaneLine,
|
||||
} from '@remixicon/react'
|
||||
import { useSessionStorageState } from 'ahooks'
|
||||
import useBoolean from 'ahooks/lib/useBoolean'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import LoadingAnim from '@/app/components/base/chat/chat/loading-anim'
|
||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
|
||||
import VersionSelector from '@/app/components/app/configuration/config/automatic/version-selector'
|
||||
import ResPlaceholder from '@/app/components/app/configuration/config/automatic/res-placeholder'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import { useNodeDataUpdate } from '@/app/components/workflow/hooks/use-node-data-update'
|
||||
import { useHooksStore } from '@/app/components/workflow/hooks-store'
|
||||
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
import { NodeRunningStatus, VarType } from '@/app/components/workflow/types'
|
||||
import { generateContext } from '@/service/debug'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import useContextGenData from './use-context-gen-data'
|
||||
|
||||
type Props = {
|
||||
isShow: boolean
|
||||
onClose: () => void
|
||||
toolNodeId: string
|
||||
paramKey: string
|
||||
codeNodeId: string
|
||||
}
|
||||
|
||||
const minCodeHeight = 220
|
||||
const minOutputHeight = 160
|
||||
const splitHandleHeight = 6
|
||||
|
||||
const normalizeCodeLanguage = (value?: string) => {
|
||||
if (value === CodeLanguage.javascript)
|
||||
return CodeLanguage.javascript
|
||||
if (value === CodeLanguage.python3)
|
||||
return CodeLanguage.python3
|
||||
return CodeLanguage.python3
|
||||
}
|
||||
|
||||
const normalizeOutputs = (outputs?: Record<string, { type: string }>) => {
|
||||
const next: OutputVar = {}
|
||||
Object.entries(outputs || {}).forEach(([key, value]) => {
|
||||
const type = Object.values(VarType).includes(value?.type as VarType)
|
||||
? value.type as VarType
|
||||
: VarType.string
|
||||
next[key] = {
|
||||
type,
|
||||
children: null,
|
||||
}
|
||||
})
|
||||
return next
|
||||
}
|
||||
|
||||
const mapOutputsToResponse = (outputs?: OutputVar) => {
|
||||
const next: Record<string, { type: string }> = {}
|
||||
Object.entries(outputs || {}).forEach(([key, value]) => {
|
||||
next[key] = { type: value.type }
|
||||
})
|
||||
return next
|
||||
}
|
||||
|
||||
const ContextGenerateModal: FC<Props> = ({
|
||||
isShow,
|
||||
onClose,
|
||||
toolNodeId,
|
||||
paramKey,
|
||||
codeNodeId,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const configsMap = useHooksStore(s => s.configsMap)
|
||||
const nodes = useStore(s => s.nodes)
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate()
|
||||
|
||||
const flowId = configsMap?.flowId || ''
|
||||
const storageKey = useMemo(() => {
|
||||
const segments = [flowId || 'unknown', toolNodeId, paramKey].filter(Boolean)
|
||||
return segments.join('-')
|
||||
}, [flowId, paramKey, toolNodeId])
|
||||
|
||||
const codeNode = useMemo(() => {
|
||||
return nodes.find(node => node.id === codeNodeId)
|
||||
}, [codeNodeId, nodes])
|
||||
const codeNodeData = codeNode?.data as CodeNodeType | undefined
|
||||
|
||||
const fallbackVersion = useMemo<ContextGenerateResponse | null>(() => {
|
||||
if (!codeNodeData)
|
||||
return null
|
||||
return {
|
||||
variables: (codeNodeData.variables || []).map(variable => ({
|
||||
variable: variable.variable,
|
||||
value_selector: Array.isArray(variable.value_selector) ? variable.value_selector : [],
|
||||
})),
|
||||
code_language: codeNodeData.code_language,
|
||||
code: codeNodeData.code || '',
|
||||
outputs: mapOutputsToResponse(codeNodeData.outputs),
|
||||
message: '',
|
||||
error: '',
|
||||
}
|
||||
}, [codeNodeData])
|
||||
|
||||
const {
|
||||
versions,
|
||||
addVersion,
|
||||
current,
|
||||
currentVersionIndex,
|
||||
setCurrentVersionIndex,
|
||||
} = useContextGenData({
|
||||
storageKey,
|
||||
})
|
||||
|
||||
const [promptMessages, setPromptMessages] = useSessionStorageState<ContextGenerateMessage[]>(
|
||||
`${storageKey}-messages`,
|
||||
{ defaultValue: [] },
|
||||
)
|
||||
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const [isGenerating, { setTrue: setGeneratingTrue, setFalse: setGeneratingFalse }] = useBoolean(false)
|
||||
|
||||
const defaultCompletionParams = {
|
||||
temperature: 0.7,
|
||||
max_tokens: 0,
|
||||
top_p: 0,
|
||||
echo: false,
|
||||
stop: [],
|
||||
presence_penalty: 0,
|
||||
frequency_penalty: 0,
|
||||
}
|
||||
const localModel = localStorage.getItem('auto-gen-model')
|
||||
? JSON.parse(localStorage.getItem('auto-gen-model') as string) as Model
|
||||
: null
|
||||
const [model, setModel] = React.useState<Model>(localModel || {
|
||||
name: '',
|
||||
provider: '',
|
||||
mode: AppModeEnum.CHAT as unknown as ModelModeType.chat,
|
||||
completion_params: defaultCompletionParams,
|
||||
})
|
||||
|
||||
const {
|
||||
defaultModel,
|
||||
} = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.textGeneration)
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultModel) {
|
||||
const localModel = localStorage.getItem('auto-gen-model')
|
||||
? JSON.parse(localStorage.getItem('auto-gen-model') || '')
|
||||
: null
|
||||
if (localModel) {
|
||||
setModel({
|
||||
...localModel,
|
||||
completion_params: {
|
||||
...defaultCompletionParams,
|
||||
...localModel.completion_params,
|
||||
},
|
||||
})
|
||||
}
|
||||
else {
|
||||
setModel(prev => ({
|
||||
...prev,
|
||||
name: defaultModel.model,
|
||||
provider: defaultModel.provider.provider,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}, [defaultModel])
|
||||
|
||||
const handleModelChange = useCallback((newValue: { modelId: string, provider: string, mode?: string, features?: string[] }) => {
|
||||
const newModel = {
|
||||
...model,
|
||||
provider: newValue.provider,
|
||||
name: newValue.modelId,
|
||||
mode: newValue.mode as ModelModeType,
|
||||
}
|
||||
setModel(newModel)
|
||||
localStorage.setItem('auto-gen-model', JSON.stringify(newModel))
|
||||
}, [model])
|
||||
|
||||
const handleCompletionParamsChange = useCallback((newParams: FormValue) => {
|
||||
const newModel = {
|
||||
...model,
|
||||
completion_params: newParams as CompletionParams,
|
||||
}
|
||||
setModel(newModel)
|
||||
localStorage.setItem('auto-gen-model', JSON.stringify(newModel))
|
||||
}, [model])
|
||||
|
||||
const chatListRef = useRef<HTMLDivElement>(null)
|
||||
useEffect(() => {
|
||||
if (!chatListRef.current)
|
||||
return
|
||||
chatListRef.current.scrollTop = chatListRef.current.scrollHeight
|
||||
}, [promptMessages, isGenerating])
|
||||
|
||||
const handleGenerate = useCallback(async () => {
|
||||
const trimmed = inputValue.trim()
|
||||
if (!trimmed || isGenerating)
|
||||
return
|
||||
if (!flowId || !toolNodeId || !paramKey)
|
||||
return
|
||||
|
||||
const nextMessages = [...(promptMessages || []), { role: 'user', content: trimmed }]
|
||||
setPromptMessages(nextMessages)
|
||||
setInputValue('')
|
||||
setGeneratingTrue()
|
||||
try {
|
||||
const response = await generateContext({
|
||||
workflow_id: flowId,
|
||||
node_id: toolNodeId,
|
||||
parameter_name: paramKey,
|
||||
language: normalizeCodeLanguage(current?.code_language || codeNodeData?.code_language) as 'python3' | 'javascript',
|
||||
prompt_messages: nextMessages,
|
||||
model_config: {
|
||||
provider: model.provider,
|
||||
name: model.name,
|
||||
completion_params: model.completion_params,
|
||||
},
|
||||
})
|
||||
|
||||
if (response.error) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: response.error,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const assistantMessage = response.message || t('nodes.tool.contextGenerate.defaultAssistantMessage', { ns: 'workflow' })
|
||||
setPromptMessages([...nextMessages, { role: 'assistant', content: assistantMessage }])
|
||||
addVersion(response)
|
||||
}
|
||||
finally {
|
||||
setGeneratingFalse()
|
||||
}
|
||||
}, [
|
||||
addVersion,
|
||||
codeNodeData?.code_language,
|
||||
current?.code_language,
|
||||
flowId,
|
||||
inputValue,
|
||||
isGenerating,
|
||||
model.completion_params,
|
||||
model.name,
|
||||
model.provider,
|
||||
paramKey,
|
||||
promptMessages,
|
||||
setPromptMessages,
|
||||
setGeneratingFalse,
|
||||
setGeneratingTrue,
|
||||
t,
|
||||
toolNodeId,
|
||||
])
|
||||
|
||||
const displayVersion = current || fallbackVersion
|
||||
const displayCodeLanguage = normalizeCodeLanguage(displayVersion?.code_language)
|
||||
const displayOutputData = useMemo(() => {
|
||||
if (!displayVersion)
|
||||
return {}
|
||||
return {
|
||||
variables: displayVersion.variables,
|
||||
outputs: displayVersion.outputs,
|
||||
}
|
||||
}, [displayVersion])
|
||||
|
||||
const applyToNode = useCallback((closeOnApply: boolean) => {
|
||||
if (!current || !codeNodeData)
|
||||
return
|
||||
|
||||
const nextOutputs = normalizeOutputs(current.outputs)
|
||||
const nextVariables = current.variables.map(item => ({
|
||||
variable: item.variable,
|
||||
value_selector: Array.isArray(item.value_selector) ? item.value_selector : [],
|
||||
}))
|
||||
|
||||
handleNodeDataUpdateWithSyncDraft({
|
||||
id: codeNodeId,
|
||||
data: {
|
||||
...codeNodeData,
|
||||
code_language: normalizeCodeLanguage(current.code_language),
|
||||
code: current.code,
|
||||
outputs: nextOutputs,
|
||||
variables: nextVariables,
|
||||
},
|
||||
})
|
||||
|
||||
if (closeOnApply)
|
||||
onClose()
|
||||
}, [codeNodeData, codeNodeId, current, handleNodeDataUpdateWithSyncDraft, onClose])
|
||||
|
||||
const handleRun = useCallback(() => {
|
||||
if (!codeNodeId)
|
||||
return
|
||||
if (current)
|
||||
applyToNode(false)
|
||||
const store = workflowStore.getState()
|
||||
store.setInitShowLastRunTab(true)
|
||||
store.setPendingSingleRun({
|
||||
nodeId: codeNodeId,
|
||||
action: 'run',
|
||||
})
|
||||
}, [applyToNode, codeNodeId, current, workflowStore])
|
||||
|
||||
const isRunning = useMemo(() => {
|
||||
const target = nodes.find(node => node.id === codeNodeId)
|
||||
return target?.data?._singleRunningStatus === NodeRunningStatus.Running
|
||||
}, [codeNodeId, nodes])
|
||||
|
||||
const rightContainerRef = useRef<HTMLDivElement>(null)
|
||||
const [codePanelHeight, setCodePanelHeight] = useState(360)
|
||||
const draggingRef = useRef(false)
|
||||
const dragStartRef = useRef({ startY: 0, startHeight: 0 })
|
||||
|
||||
const handleResizeStart = useCallback((event: React.MouseEvent<HTMLDivElement>) => {
|
||||
draggingRef.current = true
|
||||
dragStartRef.current = {
|
||||
startY: event.clientY,
|
||||
startHeight: codePanelHeight,
|
||||
}
|
||||
document.body.style.userSelect = 'none'
|
||||
}, [codePanelHeight])
|
||||
|
||||
useEffect(() => {
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
if (!draggingRef.current)
|
||||
return
|
||||
|
||||
const containerHeight = rightContainerRef.current?.offsetHeight || 0
|
||||
const maxHeight = Math.max(minCodeHeight, containerHeight - minOutputHeight - splitHandleHeight)
|
||||
const delta = event.clientY - dragStartRef.current.startY
|
||||
const nextHeight = Math.min(Math.max(dragStartRef.current.startHeight + delta, minCodeHeight), maxHeight)
|
||||
setCodePanelHeight(nextHeight)
|
||||
}
|
||||
|
||||
const handleMouseUp = () => {
|
||||
if (draggingRef.current) {
|
||||
draggingRef.current = false
|
||||
document.body.style.userSelect = ''
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('mousemove', handleMouseMove)
|
||||
window.addEventListener('mouseup', handleMouseUp)
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', handleMouseMove)
|
||||
window.removeEventListener('mouseup', handleMouseUp)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const canRun = !!displayVersion?.code || !!codeNodeData?.code
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow={isShow}
|
||||
onClose={onClose}
|
||||
className="min-w-[1140px] !p-0"
|
||||
>
|
||||
<div className="relative flex h-[680px] flex-wrap">
|
||||
<div className="flex h-full w-[420px] shrink-0 flex-col border-r border-divider-regular p-6">
|
||||
<div className="mb-4 text-lg font-bold leading-[28px] text-text-primary">
|
||||
{t('nodes.tool.contextGenerate.title', { ns: 'workflow' })}
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<ModelParameterModal
|
||||
popupClassName="!w-[520px]"
|
||||
portalToFollowElemContentClassName="z-[1000]"
|
||||
isAdvancedMode={true}
|
||||
provider={model.provider}
|
||||
completionParams={model.completion_params}
|
||||
modelId={model.name}
|
||||
setModel={handleModelChange}
|
||||
onCompletionParamsChange={handleCompletionParamsChange}
|
||||
hideDebugWithMultipleModel
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
ref={chatListRef}
|
||||
className="flex-1 space-y-2 overflow-y-auto pr-1"
|
||||
>
|
||||
{(promptMessages || []).map((message, index) => {
|
||||
const isUser = message.role === 'user'
|
||||
return (
|
||||
<div
|
||||
key={`${message.role}-${index}`}
|
||||
className={cn('flex', isUser ? 'justify-end' : 'justify-start')}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'max-w-[320px] whitespace-pre-wrap rounded-2xl px-4 py-3 text-sm',
|
||||
isUser
|
||||
? 'bg-background-gradient-bg-fill-chat-bubble-bg-3 text-text-primary'
|
||||
: 'bg-chat-bubble-bg text-text-primary',
|
||||
)}
|
||||
>
|
||||
{message.content}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{isGenerating && (
|
||||
<div className="flex justify-start">
|
||||
<div className="flex items-center gap-2 rounded-2xl bg-chat-bubble-bg px-4 py-3 text-sm text-text-primary">
|
||||
<LoadingAnim type="text" />
|
||||
<span>{t('nodes.tool.contextGenerate.generating', { ns: 'workflow' })}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-4 flex items-center gap-2">
|
||||
<Input
|
||||
value={inputValue}
|
||||
onChange={e => setInputValue(e.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter')
|
||||
handleGenerate()
|
||||
}}
|
||||
placeholder={t('nodes.tool.contextGenerate.inputPlaceholder', { ns: 'workflow' }) as string}
|
||||
disabled={isGenerating}
|
||||
/>
|
||||
<Button
|
||||
variant="primary"
|
||||
className="shrink-0 px-3"
|
||||
disabled={!inputValue.trim() || isGenerating}
|
||||
onClick={handleGenerate}
|
||||
>
|
||||
<RiSendPlaneLine className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-full w-0 grow flex-col bg-background-default-subtle p-6 pb-0">
|
||||
<div className="mb-3 flex shrink-0 items-center justify-between">
|
||||
<div>
|
||||
<div className="text-base font-semibold leading-[160%] text-text-secondary">
|
||||
{t('nodes.tool.contextGenerate.codeBlock', { ns: 'workflow' })}
|
||||
</div>
|
||||
{versions.length > 0 && (
|
||||
<VersionSelector
|
||||
versionLen={versions.length}
|
||||
value={currentVersionIndex || 0}
|
||||
onChange={setCurrentVersionIndex}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
onClick={handleRun}
|
||||
disabled={!canRun || isGenerating || isRunning}
|
||||
>
|
||||
{t('nodes.tool.contextGenerate.run', { ns: 'workflow' })}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => applyToNode(true)}
|
||||
disabled={!current || isGenerating}
|
||||
>
|
||||
{t('nodes.tool.contextGenerate.apply', { ns: 'workflow' })}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div ref={rightContainerRef} className="flex h-full flex-col overflow-hidden">
|
||||
{isGenerating && !displayVersion && (
|
||||
<div className="flex h-full flex-col items-center justify-center space-y-3">
|
||||
<Loading />
|
||||
<div className="text-[13px] text-text-tertiary">
|
||||
{t('nodes.tool.contextGenerate.generating', { ns: 'workflow' })}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!isGenerating && !displayVersion && (
|
||||
<ResPlaceholder />
|
||||
)}
|
||||
{displayVersion && (
|
||||
<div className="flex h-full flex-col overflow-hidden">
|
||||
<div
|
||||
className="flex min-h-[220px] flex-col overflow-hidden rounded-lg border border-components-panel-border bg-components-panel-bg"
|
||||
style={{ height: codePanelHeight }}
|
||||
>
|
||||
<div className="px-3 pb-1 pt-2 text-xs font-semibold uppercase text-text-tertiary">
|
||||
{t('nodes.tool.contextGenerate.code', { ns: 'workflow' })}
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden px-3 pb-3">
|
||||
<CodeEditor
|
||||
noWrapper
|
||||
isExpand
|
||||
readOnly
|
||||
language={displayCodeLanguage}
|
||||
value={displayVersion.code || ''}
|
||||
className="h-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="flex h-[6px] cursor-row-resize items-center justify-center"
|
||||
onMouseDown={handleResizeStart}
|
||||
>
|
||||
<div className="h-1 w-8 rounded-full bg-divider-subtle" />
|
||||
</div>
|
||||
<div className="flex min-h-[160px] flex-1 flex-col overflow-hidden rounded-lg border border-components-panel-border bg-components-panel-bg">
|
||||
<div className="px-3 pb-1 pt-2 text-xs font-semibold uppercase text-text-tertiary">
|
||||
{t('nodes.tool.contextGenerate.output', { ns: 'workflow' })}
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden px-3 pb-3">
|
||||
<CodeEditor
|
||||
noWrapper
|
||||
isExpand
|
||||
readOnly
|
||||
isJSONStringifyBeauty
|
||||
language={CodeLanguage.json}
|
||||
value={displayOutputData}
|
||||
className="h-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(ContextGenerateModal)
|
||||
@ -1,38 +0,0 @@
|
||||
import type { ContextGenerateResponse } from '@/service/debug'
|
||||
import { useSessionStorageState } from 'ahooks'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
type Params = {
|
||||
storageKey: string
|
||||
}
|
||||
|
||||
const keyPrefix = 'context-gen-'
|
||||
|
||||
const useContextGenData = ({ storageKey }: Params) => {
|
||||
const [versions, setVersions] = useSessionStorageState<ContextGenerateResponse[]>(`${keyPrefix}${storageKey}-versions`, {
|
||||
defaultValue: [],
|
||||
})
|
||||
|
||||
const [currentVersionIndex, setCurrentVersionIndex] = useSessionStorageState<number>(`${keyPrefix}${storageKey}-version-index`, {
|
||||
defaultValue: 0,
|
||||
})
|
||||
|
||||
const current = versions?.[currentVersionIndex || 0]
|
||||
|
||||
const addVersion = useCallback((version: ContextGenerateResponse) => {
|
||||
setCurrentVersionIndex(() => versions?.length || 0)
|
||||
setVersions((prev) => {
|
||||
return [...(prev || []), version]
|
||||
})
|
||||
}, [setCurrentVersionIndex, setVersions, versions?.length])
|
||||
|
||||
return {
|
||||
versions,
|
||||
addVersion,
|
||||
currentVersionIndex,
|
||||
setCurrentVersionIndex,
|
||||
current,
|
||||
}
|
||||
}
|
||||
|
||||
export default useContextGenData
|
||||
@ -2,6 +2,7 @@ import type { FC } from 'react'
|
||||
import { RiCloseLine, RiEqualizer2Line } from '@remixicon/react'
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { AssembleVariables } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import AlertTriangle from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback/AlertTriangle'
|
||||
import { Agent } from '@/app/components/base/icons/src/vender/workflow'
|
||||
import { cn } from '@/utils/classnames'
|
||||
@ -34,8 +35,11 @@ const AgentHeaderBar: FC<AgentHeaderBarProps> = ({
|
||||
: 'border-components-panel-border-subtle bg-components-badge-white-to-dark',
|
||||
)}
|
||||
>
|
||||
<div className="flex h-4 w-4 items-center justify-center rounded bg-util-colors-indigo-indigo-500">
|
||||
<Agent className="h-3 w-3 text-text-primary-on-surface" />
|
||||
<div className={cn('flex h-4 w-4 items-center justify-center rounded', showAtPrefix
|
||||
? 'bg-util-colors-indigo-indigo-500'
|
||||
: 'bg-util-colors-blue-blue-500')}
|
||||
>
|
||||
{showAtPrefix ? <Agent className="h-3 w-3 text-text-primary-on-surface" /> : <AssembleVariables className="h-3 w-3 text-text-primary-on-surface" />}
|
||||
</div>
|
||||
<span className="system-xs-medium text-text-secondary">
|
||||
{showAtPrefix && '@'}
|
||||
|
||||
@ -26,20 +26,26 @@ import { VarKindType as VarKindTypeEnum } from '@/app/components/workflow/nodes/
|
||||
import { Type } from '@/app/components/workflow/nodes/llm/types'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { BlockEnum, EditionType, isPromptMessageContext, PromptRole, VarType } from '@/app/components/workflow/types'
|
||||
import { generateNewNode, getNodeCustomTypeByNodeDataType } from '@/app/components/workflow/utils'
|
||||
import { generateNewNode, getNodeCustomTypeByNodeDataType, mergeNodeDefaultData } from '@/app/components/workflow/utils'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import { useStrategyProviders } from '@/service/use-strategy'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import SubGraphModal from '../sub-graph-modal'
|
||||
import AgentHeaderBar from './agent-header-bar'
|
||||
import Placeholder from './placeholder'
|
||||
import ContextGenerateModal from '../context-generate-modal'
|
||||
|
||||
/**
|
||||
* Matches agent context variable syntax: {{@nodeId.context@}}
|
||||
* Example: {{@agent-123.context@}} -> captures "agent-123"
|
||||
* Example: {{@agent-123.context@}}
|
||||
*/
|
||||
const AGENT_CONTEXT_VAR_PATTERN = /\{\{@([^.@#]+)\.context@\}\}/g
|
||||
const AGENT_CONTEXT_VAR_PATTERN = /\{\{@[^.@#]+\.context@\}\}/g
|
||||
const AGENT_CONTEXT_VAR_PREFIX = '{{@'
|
||||
const AGENT_CONTEXT_VAR_SUFFIX = '.context@}}'
|
||||
const getAgentNodeIdFromContextVar = (placeholder: string) => {
|
||||
if (!placeholder.startsWith(AGENT_CONTEXT_VAR_PREFIX) || !placeholder.endsWith(AGENT_CONTEXT_VAR_SUFFIX))
|
||||
return ''
|
||||
return placeholder.slice(AGENT_CONTEXT_VAR_PREFIX.length, -AGENT_CONTEXT_VAR_SUFFIX.length)
|
||||
}
|
||||
|
||||
const buildAssemblePlaceholder = (toolNodeId?: string, paramKey?: string) => {
|
||||
if (!toolNodeId || !paramKey)
|
||||
@ -163,10 +169,10 @@ const MixedVariableTextInput = ({
|
||||
const nodes = useNodes<CommonNodeType>()
|
||||
const controlPromptEditorRerenderKey = useStore(s => s.controlPromptEditorRerenderKey)
|
||||
const setControlPromptEditorRerenderKey = useStore(s => s.setControlPromptEditorRerenderKey)
|
||||
const nodesDefaultConfigs = useStore(s => s.nodesDefaultConfigs)
|
||||
const { nodesMap: nodesMetaDataMap } = useNodesMetaData()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const [isSubGraphModalOpen, setIsSubGraphModalOpen] = useState(false)
|
||||
const [isContextGenerateModalOpen, setIsContextGenerateModalOpen] = useState(false)
|
||||
|
||||
const nodesByIdMap = useMemo(() => {
|
||||
return availableNodes.reduce((acc, node) => {
|
||||
@ -214,8 +220,9 @@ const MixedVariableTextInput = ({
|
||||
}) => {
|
||||
if (!toolNodeId)
|
||||
return null
|
||||
const defaultValue = nodesMetaDataMap?.[payload.nodeType]?.defaultValue as Partial<LLMNodeType | CodeNodeType> | undefined
|
||||
if (!defaultValue)
|
||||
const metaDefault = nodesMetaDataMap?.[payload.nodeType]?.defaultValue as Partial<LLMNodeType | CodeNodeType> | undefined
|
||||
const appDefault = nodesDefaultConfigs?.[payload.nodeType] as Partial<LLMNodeType | CodeNodeType> | undefined
|
||||
if (!metaDefault && !appDefault)
|
||||
return null
|
||||
|
||||
const { getNodes, setNodes } = reactFlowStore.getState()
|
||||
@ -226,15 +233,22 @@ const MixedVariableTextInput = ({
|
||||
const nextNodes = shouldReplace
|
||||
? currentNodes.filter(node => node.id !== payload.extractorNodeId)
|
||||
: currentNodes
|
||||
const mergedData = mergeNodeDefaultData({
|
||||
nodeType: payload.nodeType,
|
||||
metaDefault,
|
||||
appDefault,
|
||||
overrideData: payload.data,
|
||||
})
|
||||
const resolvedTitle = mergedData.title ?? metaDefault?.title ?? appDefault?.title ?? ''
|
||||
const resolvedDesc = mergedData.desc ?? metaDefault?.desc ?? appDefault?.desc ?? ''
|
||||
const { newNode } = generateNewNode({
|
||||
id: payload.extractorNodeId,
|
||||
type: getNodeCustomTypeByNodeDataType(payload.nodeType),
|
||||
data: {
|
||||
...defaultValue,
|
||||
...payload.data,
|
||||
...mergedData,
|
||||
type: payload.nodeType,
|
||||
title: defaultValue?.title ?? '',
|
||||
desc: defaultValue.desc || '',
|
||||
title: resolvedTitle,
|
||||
desc: resolvedDesc,
|
||||
parent_node_id: toolNodeId,
|
||||
},
|
||||
position: {
|
||||
@ -249,7 +263,7 @@ const MixedVariableTextInput = ({
|
||||
}
|
||||
|
||||
return existingNode
|
||||
}, [handleSyncWorkflowDraft, nodesMetaDataMap, reactFlowStore, toolNodeId])
|
||||
}, [handleSyncWorkflowDraft, nodesDefaultConfigs, nodesMetaDataMap, reactFlowStore, toolNodeId])
|
||||
|
||||
const ensureAssembleExtractorNode = useCallback(() => {
|
||||
if (!assembleExtractorNodeId)
|
||||
@ -311,8 +325,9 @@ const MixedVariableTextInput = ({
|
||||
|
||||
const matches = text.matchAll(AGENT_CONTEXT_VAR_PATTERN)
|
||||
for (const match of matches) {
|
||||
const variablePath = match[1]
|
||||
const nodeId = variablePath.split('.')[0]
|
||||
const nodeId = getAgentNodeIdFromContextVar(match[0])
|
||||
if (!nodeId)
|
||||
continue
|
||||
const node = nodesByIdMap[nodeId]
|
||||
if (node && contextNodeIds.has(nodeId)) {
|
||||
return {
|
||||
@ -328,6 +343,14 @@ const MixedVariableTextInput = ({
|
||||
return detectAgentFromText(value)
|
||||
}, [detectAgentFromText, value])
|
||||
|
||||
// Check if value only contains agent context variable without other user input
|
||||
const isOnlyAgentContext = useMemo(() => {
|
||||
if (!detectedAgentFromValue || !value)
|
||||
return false
|
||||
const valueWithoutAgentContext = value.replace(AGENT_CONTEXT_VAR_PATTERN, '').trim()
|
||||
return valueWithoutAgentContext === ''
|
||||
}, [detectedAgentFromValue, value])
|
||||
|
||||
const agentNodes = useMemo(() => {
|
||||
if (!contextNodeIds.size)
|
||||
return []
|
||||
@ -463,8 +486,8 @@ const MixedVariableTextInput = ({
|
||||
if (!agentNodeId || !onChange)
|
||||
return
|
||||
|
||||
const valueWithoutAgentVars = value.replace(AGENT_CONTEXT_VAR_PATTERN, (match, variablePath) => {
|
||||
const nodeId = variablePath.split('.')[0]
|
||||
const valueWithoutAgentVars = value.replace(AGENT_CONTEXT_VAR_PATTERN, (match) => {
|
||||
const nodeId = getAgentNodeIdFromContextVar(match)
|
||||
return nodeId === agentNodeId ? '' : match
|
||||
})
|
||||
|
||||
@ -477,7 +500,8 @@ const MixedVariableTextInput = ({
|
||||
if (!onChange)
|
||||
return
|
||||
|
||||
const valueWithoutTrigger = value.replace(/@$/, '')
|
||||
// compute words after the latest '@' and delete them
|
||||
const valueWithoutTrigger = value.replace(/@[^@\n]*$/, '')
|
||||
const newValue = `{{@${agent.id}.context@}}${valueWithoutTrigger}`
|
||||
|
||||
if (toolNodeId && paramKey) {
|
||||
@ -519,7 +543,6 @@ const MixedVariableTextInput = ({
|
||||
ensureAssembleExtractorNode()
|
||||
onChange?.(assemblePlaceholder, VarKindTypeEnum.mixed, null)
|
||||
setControlPromptEditorRerenderKey(Date.now())
|
||||
setIsContextGenerateModalOpen(true)
|
||||
return [extractorNodeId, 'result']
|
||||
}, [assembleExtractorNodeId, assemblePlaceholder, ensureAssembleExtractorNode, onChange, paramKey, setControlPromptEditorRerenderKey, toolNodeId])
|
||||
|
||||
@ -540,10 +563,6 @@ const MixedVariableTextInput = ({
|
||||
setIsSubGraphModalOpen(false)
|
||||
}, [])
|
||||
|
||||
const handleCloseContextGenerateModal = useCallback(() => {
|
||||
setIsContextGenerateModalOpen(false)
|
||||
}, [])
|
||||
|
||||
const sourceVariable: ValueSelector | undefined = detectedAgentFromValue
|
||||
? [detectedAgentFromValue.nodeId, 'context']
|
||||
: undefined
|
||||
@ -559,6 +578,7 @@ const MixedVariableTextInput = ({
|
||||
<AgentHeaderBar
|
||||
agentName={t('nodes.tool.assembleVariables', { ns: 'workflow' })}
|
||||
onRemove={handleAssembleRemove}
|
||||
onViewInternals={handleOpenSubGraphModal}
|
||||
hasWarning={hasAssembleWarning}
|
||||
showAtPrefix={false}
|
||||
/>
|
||||
@ -572,44 +592,64 @@ const MixedVariableTextInput = ({
|
||||
/>
|
||||
)}
|
||||
{!isAssembleValue && (
|
||||
<PromptEditor
|
||||
key={controlPromptEditorRerenderKey}
|
||||
wrapperClassName="min-h-8 px-2 py-1"
|
||||
className="caret:text-text-accent"
|
||||
editable={!readOnly}
|
||||
value={value}
|
||||
workflowVariableBlock={{
|
||||
show: !disableVariableInsertion,
|
||||
variables: nodesOutputVars || [],
|
||||
workflowNodesMap,
|
||||
showManageInputField,
|
||||
onManageInputField,
|
||||
showAssembleVariables: !disableVariableInsertion && !!toolNodeId && !!paramKey,
|
||||
onAssembleVariables: handleAssembleSelect,
|
||||
}}
|
||||
agentBlock={{
|
||||
show: agentNodes.length > 0 && !detectedAgentFromValue,
|
||||
agentNodes,
|
||||
onSelect: handleAgentSelect,
|
||||
}}
|
||||
placeholder={<Placeholder disableVariableInsertion={disableVariableInsertion} hasSelectedAgent={!!detectedAgentFromValue} />}
|
||||
onChange={(text) => {
|
||||
const hasPlaceholder = new RegExp(AGENT_CONTEXT_VAR_PATTERN.source).test(text)
|
||||
if (hasPlaceholder)
|
||||
syncExtractorPromptFromText(text)
|
||||
if (detectedAgentFromValue && !hasPlaceholder) {
|
||||
removeExtractorNode()
|
||||
onChange?.(text, VarKindTypeEnum.mixed, null)
|
||||
return
|
||||
}
|
||||
onChange?.(text)
|
||||
}}
|
||||
/>
|
||||
<div className="relative">
|
||||
<PromptEditor
|
||||
key={controlPromptEditorRerenderKey}
|
||||
wrapperClassName="min-h-8 px-2 py-1"
|
||||
className="caret:text-text-accent"
|
||||
editable={!readOnly}
|
||||
value={value}
|
||||
workflowVariableBlock={{
|
||||
show: !disableVariableInsertion,
|
||||
variables: nodesOutputVars || [],
|
||||
workflowNodesMap,
|
||||
showManageInputField,
|
||||
onManageInputField,
|
||||
showAssembleVariables: !disableVariableInsertion && !!toolNodeId && !!paramKey,
|
||||
onAssembleVariables: handleAssembleSelect,
|
||||
}}
|
||||
agentBlock={{
|
||||
show: agentNodes.length > 0 && !detectedAgentFromValue,
|
||||
agentNodes,
|
||||
onSelect: handleAgentSelect,
|
||||
}}
|
||||
placeholder={<Placeholder disableVariableInsertion={disableVariableInsertion} hasSelectedAgent={!!detectedAgentFromValue} />}
|
||||
onChange={(text) => {
|
||||
const hasPlaceholder = new RegExp(AGENT_CONTEXT_VAR_PATTERN.source).test(text)
|
||||
if (hasPlaceholder)
|
||||
syncExtractorPromptFromText(text)
|
||||
if (detectedAgentFromValue && !hasPlaceholder) {
|
||||
removeExtractorNode()
|
||||
onChange?.(text, VarKindTypeEnum.mixed, null)
|
||||
return
|
||||
}
|
||||
onChange?.(text)
|
||||
}}
|
||||
/>
|
||||
{isOnlyAgentContext && paramKey && (
|
||||
<div className="pointer-events-none absolute left-0 top-0 flex h-full w-full items-center px-2 py-1">
|
||||
<span className="system-sm-regular text-components-input-text-placeholder">
|
||||
{t('nodes.tool.agentPlaceholder', { ns: 'workflow', paramKey })}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{toolNodeId && detectedAgentFromValue && sourceVariable && (
|
||||
{toolNodeId && paramKey && isAssembleValue && (
|
||||
<SubGraphModal
|
||||
isOpen={isSubGraphModalOpen}
|
||||
onClose={handleCloseSubGraphModal}
|
||||
variant="assemble"
|
||||
toolNodeId={toolNodeId}
|
||||
paramKey={paramKey}
|
||||
title={t('nodes.tool.assembleVariables', { ns: 'workflow' })}
|
||||
/>
|
||||
)}
|
||||
{toolNodeId && paramKey && !isAssembleValue && detectedAgentFromValue && sourceVariable && (
|
||||
<SubGraphModal
|
||||
isOpen={isSubGraphModalOpen}
|
||||
onClose={handleCloseSubGraphModal}
|
||||
variant="agent"
|
||||
toolNodeId={toolNodeId}
|
||||
paramKey={paramKey}
|
||||
sourceVariable={sourceVariable}
|
||||
@ -617,15 +657,6 @@ const MixedVariableTextInput = ({
|
||||
agentNodeId={detectedAgentFromValue.nodeId}
|
||||
/>
|
||||
)}
|
||||
{toolNodeId && paramKey && (
|
||||
<ContextGenerateModal
|
||||
isShow={isContextGenerateModalOpen}
|
||||
onClose={handleCloseContextGenerateModal}
|
||||
toolNodeId={toolNodeId}
|
||||
paramKey={paramKey}
|
||||
codeNodeId={assembleExtractorNodeId || `${toolNodeId}_ext_${paramKey}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
import type { FC } from 'react'
|
||||
import type { SubGraphModalProps } from './types'
|
||||
import type { MentionConfig } from '@/app/components/workflow/nodes/_base/types'
|
||||
import type { CodeNodeType } from '@/app/components/workflow/nodes/code/types'
|
||||
import type { LLMNodeType } from '@/app/components/workflow/nodes/llm/types'
|
||||
import type { ToolNodeType } from '@/app/components/workflow/nodes/tool/types'
|
||||
import type { Node, PromptItem, PromptTemplateItem } from '@/app/components/workflow/types'
|
||||
@ -11,24 +12,29 @@ import { noop } from 'es-toolkit/function'
|
||||
import { Fragment, memo, useCallback, useEffect, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore as useReactFlowStore, useStoreApi } from 'reactflow'
|
||||
import { AssembleVariablesAlt } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { Agent } from '@/app/components/base/icons/src/vender/workflow'
|
||||
import { useIsChatMode, useNodesSyncDraft, useWorkflow, useWorkflowVariables } from '@/app/components/workflow/hooks'
|
||||
import { useHooksStore } from '@/app/components/workflow/hooks-store'
|
||||
import { VarKindType } from '@/app/components/workflow/nodes/_base/types'
|
||||
import { useStore as useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { BlockEnum, EditionType, isPromptMessageContext, PromptRole } from '@/app/components/workflow/types'
|
||||
import { BlockEnum, EditionType, isPromptMessageContext, PromptRole, VarType } from '@/app/components/workflow/types'
|
||||
import SubGraphCanvas from './sub-graph-canvas'
|
||||
|
||||
const SubGraphModal: FC<SubGraphModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
toolNodeId,
|
||||
paramKey,
|
||||
sourceVariable,
|
||||
agentName,
|
||||
agentNodeId,
|
||||
}) => {
|
||||
const SubGraphModal: FC<SubGraphModalProps> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const { isOpen, onClose, variant, toolNodeId, paramKey } = props
|
||||
const isAgentVariant = variant === 'agent'
|
||||
const resolvedAgentNodeId = isAgentVariant ? props.agentNodeId : ''
|
||||
const agentName = isAgentVariant ? props.agentName : ''
|
||||
const assembleTitle = !isAgentVariant ? props.title : ''
|
||||
const modalTitle = useMemo(() => {
|
||||
const baseTitle = isAgentVariant
|
||||
? agentName
|
||||
: (assembleTitle || t('nodes.tool.assembleVariables', { ns: 'workflow' }))
|
||||
const prefix = isAgentVariant && baseTitle ? '@' : ''
|
||||
return `${prefix}${baseTitle} ${t('subGraphModal.title', { ns: 'workflow' })}`.trim()
|
||||
}, [agentName, assembleTitle, isAgentVariant, t])
|
||||
const reactflowStore = useStoreApi()
|
||||
const workflowNodes = useWorkflowStore(state => state.nodes)
|
||||
const workflowEdges = useReactFlowStore(state => state.edges)
|
||||
@ -41,13 +47,16 @@ const SubGraphModal: FC<SubGraphModalProps> = ({
|
||||
|
||||
const extractorNodeId = `${toolNodeId}_ext_${paramKey}`
|
||||
const extractorNode = useMemo(() => {
|
||||
return workflowNodes.find(node => node.id === extractorNodeId) as Node<LLMNodeType> | undefined
|
||||
return workflowNodes.find(node => node.id === extractorNodeId) as Node<LLMNodeType | CodeNodeType> | undefined
|
||||
}, [extractorNodeId, workflowNodes])
|
||||
const toolNode = useMemo(() => {
|
||||
return workflowNodes.find(node => node.id === toolNodeId)
|
||||
}, [toolNodeId, workflowNodes])
|
||||
const toolParam = (toolNode?.data as ToolNodeType | undefined)?.tool_parameters?.[paramKey]
|
||||
const toolParamValue = toolParam?.value as string | undefined
|
||||
const assemblePlaceholder = useMemo(() => {
|
||||
return `{{#${toolNodeId}_ext_${paramKey}.result#}}`
|
||||
}, [paramKey, toolNodeId])
|
||||
|
||||
const parentBeforeNodes = useMemo(() => {
|
||||
if (!isOpen)
|
||||
@ -56,25 +65,28 @@ const SubGraphModal: FC<SubGraphModalProps> = ({
|
||||
}, [getBeforeNodesInSameBranch, isOpen, toolNodeId, workflowEdges, workflowNodes])
|
||||
|
||||
const parentContextNodes = useMemo(() => {
|
||||
if (!parentBeforeNodes.length)
|
||||
if (!parentBeforeNodes.length || !isAgentVariant)
|
||||
return []
|
||||
return parentBeforeNodes.filter(node => node.data.type === BlockEnum.Agent || node.data.type === BlockEnum.LLM)
|
||||
}, [parentBeforeNodes])
|
||||
}, [isAgentVariant, parentBeforeNodes])
|
||||
|
||||
const parentContextNodeIds = useMemo(() => {
|
||||
return parentContextNodes.map(node => node.id)
|
||||
}, [parentContextNodes])
|
||||
const parentAvailableNodes = useMemo(() => {
|
||||
if (!isOpen)
|
||||
return []
|
||||
return isAgentVariant ? parentContextNodes : parentBeforeNodes
|
||||
}, [isAgentVariant, isOpen, parentBeforeNodes, parentContextNodes])
|
||||
|
||||
const parentAvailableVars = useMemo(() => {
|
||||
if (!parentContextNodeIds.length)
|
||||
if (!parentAvailableNodes.length)
|
||||
return []
|
||||
const vars = getNodeAvailableVars({
|
||||
beforeNodes: parentContextNodes,
|
||||
beforeNodes: parentAvailableNodes,
|
||||
isChatMode,
|
||||
filterVar: () => true,
|
||||
})
|
||||
return vars.filter(nodeVar => parentContextNodeIds.includes(nodeVar.nodeId))
|
||||
}, [getNodeAvailableVars, isChatMode, parentContextNodeIds, parentContextNodes])
|
||||
const availableNodeIds = new Set(parentAvailableNodes.map(node => node.id))
|
||||
return vars.filter(nodeVar => availableNodeIds.has(nodeVar.nodeId))
|
||||
}, [getNodeAvailableVars, isChatMode, parentAvailableNodes])
|
||||
|
||||
const mentionConfig = useMemo<MentionConfig>(() => {
|
||||
const current = toolParam?.mention_config
|
||||
@ -91,6 +103,9 @@ const SubGraphModal: FC<SubGraphModalProps> = ({
|
||||
}, [extractorNodeId, paramKey, toolParam?.mention_config])
|
||||
|
||||
const handleMentionConfigChange = useCallback((config: MentionConfig) => {
|
||||
if (!isAgentVariant)
|
||||
return
|
||||
|
||||
const { getNodes, setNodes } = reactflowStore.getState()
|
||||
const nextNodes = getNodes().map((node) => {
|
||||
if (node.id !== toolNodeId)
|
||||
@ -118,10 +133,10 @@ const SubGraphModal: FC<SubGraphModalProps> = ({
|
||||
})
|
||||
setNodes(nextNodes)
|
||||
handleSyncWorkflowDraft()
|
||||
}, [handleSyncWorkflowDraft, paramKey, reactflowStore, toolNodeId])
|
||||
}, [handleSyncWorkflowDraft, isAgentVariant, paramKey, reactflowStore, toolNodeId])
|
||||
|
||||
useEffect(() => {
|
||||
if (!toolParam || (toolParam.type && toolParam.type !== VarKindType.mention))
|
||||
if (!isAgentVariant || !toolParam || (toolParam.type && toolParam.type !== VarKindType.mention))
|
||||
return
|
||||
|
||||
const current = toolParam.mention_config
|
||||
@ -132,7 +147,7 @@ const SubGraphModal: FC<SubGraphModalProps> = ({
|
||||
|
||||
if (needsExtractor || needsNullStrategy || needsOutputSelector || needsDefaultValue)
|
||||
handleMentionConfigChange(mentionConfig)
|
||||
}, [handleMentionConfigChange, mentionConfig, toolParam])
|
||||
}, [handleMentionConfigChange, isAgentVariant, mentionConfig, toolParam])
|
||||
|
||||
const getUserPromptText = useCallback((promptTemplate?: PromptTemplateItem[] | PromptItem) => {
|
||||
if (!promptTemplate)
|
||||
@ -156,23 +171,46 @@ const SubGraphModal: FC<SubGraphModalProps> = ({
|
||||
|
||||
// TODO: handle external workflow updates while sub-graph modal is open.
|
||||
const handleSave = useCallback((subGraphNodes: Node[]) => {
|
||||
const extractorNodeData = subGraphNodes.find(node => node.id === extractorNodeId) as Node<LLMNodeType> | undefined
|
||||
const extractorNodeData = subGraphNodes.find(node => node.id === extractorNodeId) as Node<LLMNodeType | CodeNodeType> | undefined
|
||||
if (!extractorNodeData)
|
||||
return
|
||||
|
||||
const userPromptText = getUserPromptText(extractorNodeData.data?.prompt_template)
|
||||
const placeholder = `{{@${agentNodeId}.context@}}`
|
||||
const nextValue = `${placeholder}${userPromptText}`
|
||||
const ensureAssembleOutputs = (payload: CodeNodeType) => {
|
||||
const outputs = payload.outputs || {}
|
||||
if (outputs.result)
|
||||
return payload
|
||||
return {
|
||||
...payload,
|
||||
outputs: {
|
||||
...outputs,
|
||||
result: {
|
||||
type: VarType.string,
|
||||
children: null,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const userPromptText = isAgentVariant
|
||||
? getUserPromptText((extractorNodeData.data as LLMNodeType).prompt_template)
|
||||
: ''
|
||||
const placeholder = isAgentVariant && resolvedAgentNodeId ? `{{@${resolvedAgentNodeId}.context@}}` : ''
|
||||
const nextValue = isAgentVariant
|
||||
? `${placeholder}${userPromptText}`
|
||||
: assemblePlaceholder
|
||||
|
||||
const { getNodes, setNodes } = reactflowStore.getState()
|
||||
const nextNodes = getNodes().map((node) => {
|
||||
if (node.id === extractorNodeId) {
|
||||
const nextData = isAgentVariant
|
||||
? extractorNodeData.data
|
||||
: ensureAssembleOutputs(extractorNodeData.data as CodeNodeType)
|
||||
return {
|
||||
...node,
|
||||
hidden: true,
|
||||
data: {
|
||||
...node.data,
|
||||
...extractorNodeData.data,
|
||||
...nextData,
|
||||
parent_node_id: toolNodeId,
|
||||
},
|
||||
}
|
||||
@ -200,7 +238,7 @@ const SubGraphModal: FC<SubGraphModalProps> = ({
|
||||
})
|
||||
setNodes(nextNodes)
|
||||
setControlPromptEditorRerenderKey(Date.now())
|
||||
}, [agentNodeId, extractorNodeId, getUserPromptText, paramKey, reactflowStore, setControlPromptEditorRerenderKey, toolNodeId])
|
||||
}, [assemblePlaceholder, extractorNodeId, getUserPromptText, isAgentVariant, paramKey, reactflowStore, resolvedAgentNodeId, setControlPromptEditorRerenderKey, toolNodeId])
|
||||
|
||||
return (
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
@ -215,13 +253,12 @@ const SubGraphModal: FC<SubGraphModalProps> = ({
|
||||
<div className="flex h-14 shrink-0 items-center justify-between border-b border-divider-subtle px-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded bg-util-colors-indigo-indigo-500">
|
||||
<Agent className="h-4 w-4 text-text-primary-on-surface" />
|
||||
{isAgentVariant
|
||||
? <Agent className="h-4 w-4 text-text-primary-on-surface" />
|
||||
: <AssembleVariablesAlt className="h-4 w-4 text-text-primary-on-surface" />}
|
||||
</div>
|
||||
<span className="system-md-semibold text-text-primary">
|
||||
@
|
||||
{agentName}
|
||||
{' '}
|
||||
{t('subGraphModal.title', { ns: 'workflow' })}
|
||||
{modalTitle}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
@ -234,22 +271,41 @@ const SubGraphModal: FC<SubGraphModalProps> = ({
|
||||
</div>
|
||||
|
||||
<div className="bg-workflow-canvas-wrapper relative flex-1 overflow-hidden">
|
||||
<SubGraphCanvas
|
||||
toolNodeId={toolNodeId}
|
||||
paramKey={paramKey}
|
||||
sourceVariable={sourceVariable}
|
||||
agentNodeId={agentNodeId}
|
||||
agentName={agentName}
|
||||
configsMap={configsMap}
|
||||
mentionConfig={mentionConfig}
|
||||
onMentionConfigChange={handleMentionConfigChange}
|
||||
extractorNode={extractorNode}
|
||||
toolParamValue={toolParamValue}
|
||||
parentAvailableNodes={parentContextNodes}
|
||||
parentAvailableVars={parentAvailableVars}
|
||||
onSave={handleSave}
|
||||
onSyncWorkflowDraft={doSyncWorkflowDraft}
|
||||
/>
|
||||
{variant === 'agent'
|
||||
? (
|
||||
<SubGraphCanvas
|
||||
variant="agent"
|
||||
toolNodeId={toolNodeId}
|
||||
paramKey={paramKey}
|
||||
sourceVariable={props.sourceVariable}
|
||||
agentNodeId={props.agentNodeId}
|
||||
agentName={props.agentName}
|
||||
configsMap={configsMap}
|
||||
mentionConfig={mentionConfig}
|
||||
onMentionConfigChange={handleMentionConfigChange}
|
||||
extractorNode={extractorNode as Node<LLMNodeType> | undefined}
|
||||
toolParamValue={toolParamValue}
|
||||
parentAvailableNodes={parentAvailableNodes}
|
||||
parentAvailableVars={parentAvailableVars}
|
||||
onSave={handleSave}
|
||||
onSyncWorkflowDraft={doSyncWorkflowDraft}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<SubGraphCanvas
|
||||
variant="assemble"
|
||||
toolNodeId={toolNodeId}
|
||||
paramKey={paramKey}
|
||||
title={props.title}
|
||||
configsMap={configsMap}
|
||||
extractorNode={extractorNode as Node<CodeNodeType> | undefined}
|
||||
toolParamValue={toolParamValue}
|
||||
parentAvailableNodes={parentAvailableNodes}
|
||||
parentAvailableVars={parentAvailableVars}
|
||||
onSave={handleSave}
|
||||
onSyncWorkflowDraft={doSyncWorkflowDraft}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</DialogPanel>
|
||||
</TransitionChild>
|
||||
|
||||
@ -4,40 +4,10 @@ import type { SubGraphCanvasProps } from './types'
|
||||
import { memo } from 'react'
|
||||
import SubGraph from '@/app/components/sub-graph'
|
||||
|
||||
const SubGraphCanvas: FC<SubGraphCanvasProps> = ({
|
||||
toolNodeId,
|
||||
paramKey,
|
||||
sourceVariable,
|
||||
agentNodeId,
|
||||
agentName,
|
||||
configsMap,
|
||||
mentionConfig,
|
||||
onMentionConfigChange,
|
||||
extractorNode,
|
||||
toolParamValue,
|
||||
parentAvailableNodes,
|
||||
parentAvailableVars,
|
||||
onSave,
|
||||
onSyncWorkflowDraft,
|
||||
}) => {
|
||||
const SubGraphCanvas: FC<SubGraphCanvasProps> = (props) => {
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<SubGraph
|
||||
toolNodeId={toolNodeId}
|
||||
paramKey={paramKey}
|
||||
sourceVariable={sourceVariable}
|
||||
agentNodeId={agentNodeId}
|
||||
agentName={agentName}
|
||||
configsMap={configsMap}
|
||||
mentionConfig={mentionConfig}
|
||||
onMentionConfigChange={onMentionConfigChange}
|
||||
extractorNode={extractorNode}
|
||||
toolParamValue={toolParamValue}
|
||||
parentAvailableNodes={parentAvailableNodes}
|
||||
parentAvailableVars={parentAvailableVars}
|
||||
onSave={onSave}
|
||||
onSyncWorkflowDraft={onSyncWorkflowDraft}
|
||||
/>
|
||||
<SubGraph {...props} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,34 +1,25 @@
|
||||
import type { SyncWorkflowDraft } from '@/app/components/sub-graph/types'
|
||||
import type { Shape as HooksStoreShape } from '@/app/components/workflow/hooks-store'
|
||||
import type { MentionConfig } from '@/app/components/workflow/nodes/_base/types'
|
||||
import type { LLMNodeType } from '@/app/components/workflow/nodes/llm/types'
|
||||
import type { NodeOutPutVar, Edge as WorkflowEdge, Node as WorkflowNode } from '@/app/components/workflow/types'
|
||||
import type { SubGraphProps } from '@/app/components/sub-graph/types'
|
||||
import type { ValueSelector } from '@/app/components/workflow/types'
|
||||
|
||||
type WorkflowValueSelector = string[]
|
||||
|
||||
export type SubGraphModalProps = {
|
||||
type BaseSubGraphModalProps = {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
toolNodeId: string
|
||||
paramKey: string
|
||||
sourceVariable: WorkflowValueSelector
|
||||
}
|
||||
|
||||
type AgentSubGraphModalProps = BaseSubGraphModalProps & {
|
||||
variant: 'agent'
|
||||
sourceVariable: ValueSelector
|
||||
agentName: string
|
||||
agentNodeId: string
|
||||
}
|
||||
|
||||
export type SubGraphCanvasProps = {
|
||||
toolNodeId: string
|
||||
paramKey: string
|
||||
sourceVariable: WorkflowValueSelector
|
||||
agentNodeId: string
|
||||
agentName: string
|
||||
configsMap?: HooksStoreShape['configsMap']
|
||||
mentionConfig: MentionConfig
|
||||
onMentionConfigChange: (config: MentionConfig) => void
|
||||
extractorNode?: WorkflowNode<LLMNodeType>
|
||||
toolParamValue?: string
|
||||
parentAvailableNodes?: WorkflowNode[]
|
||||
parentAvailableVars?: NodeOutPutVar[]
|
||||
onSave?: (nodes: WorkflowNode[], edges: WorkflowEdge[]) => void
|
||||
onSyncWorkflowDraft?: SyncWorkflowDraft
|
||||
type AssembleSubGraphModalProps = BaseSubGraphModalProps & {
|
||||
variant: 'assemble'
|
||||
title: string
|
||||
}
|
||||
|
||||
export type SubGraphModalProps = AgentSubGraphModalProps | AssembleSubGraphModalProps
|
||||
|
||||
export type SubGraphCanvasProps = SubGraphProps
|
||||
|
||||
@ -20,7 +20,14 @@ import { useStrategyProviders } from '@/service/use-strategy'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { VarType } from './types'
|
||||
|
||||
const AGENT_CONTEXT_VAR_PATTERN = /\{\{@([^.@#]+)\.context@\}\}/g
|
||||
const AGENT_CONTEXT_VAR_PATTERN = /\{\{@[^.@#]+\.context@\}\}/g
|
||||
const AGENT_CONTEXT_VAR_PREFIX = '{{@'
|
||||
const AGENT_CONTEXT_VAR_SUFFIX = '.context@}}'
|
||||
const getAgentNodeIdFromContextVar = (placeholder: string) => {
|
||||
if (!placeholder.startsWith(AGENT_CONTEXT_VAR_PREFIX) || !placeholder.endsWith(AGENT_CONTEXT_VAR_SUFFIX))
|
||||
return ''
|
||||
return placeholder.slice(AGENT_CONTEXT_VAR_PREFIX.length, -AGENT_CONTEXT_VAR_SUFFIX.length)
|
||||
}
|
||||
type AgentCheckValidContext = {
|
||||
provider?: StrategyPluginDetail
|
||||
strategy?: StrategyDetail
|
||||
@ -80,7 +87,7 @@ const Node: FC<NodeProps<ToolNodeType>> = ({
|
||||
return
|
||||
const matches = value.matchAll(AGENT_CONTEXT_VAR_PATTERN)
|
||||
for (const match of matches) {
|
||||
const agentNodeId = match[1]
|
||||
const agentNodeId = getAgentNodeIdFromContextVar(match[0])
|
||||
if (!agentNodeId)
|
||||
continue
|
||||
const entryKey = `${paramKey}:${agentNodeId}`
|
||||
|
||||
@ -124,20 +124,22 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
|
||||
// TODO empty object type always match `qa_structured` schema type
|
||||
return (
|
||||
<div key={outputItem.name}>
|
||||
{outputItem.value?.type === 'object' ? (
|
||||
<StructureOutputItem
|
||||
rootClassName="code-sm-semibold text-text-secondary"
|
||||
payload={wrapStructuredVarItem(outputItem, schemaType)}
|
||||
/>
|
||||
) : (
|
||||
<VarItem
|
||||
name={outputItem.name}
|
||||
// eslint-disable-next-line sonarjs/no-nested-template-literals
|
||||
type={`${outputItem.type.toLocaleLowerCase()}${schemaType ? ` (${schemaType})` : ''}`}
|
||||
description={outputItem.description}
|
||||
isIndent={hasObjectOutput}
|
||||
/>
|
||||
)}
|
||||
{outputItem.value?.type === 'object'
|
||||
? (
|
||||
<StructureOutputItem
|
||||
rootClassName="code-sm-semibold text-text-secondary"
|
||||
payload={wrapStructuredVarItem(outputItem, schemaType)}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<VarItem
|
||||
name={outputItem.name}
|
||||
|
||||
type={`${outputItem.type.toLocaleLowerCase()}${schemaType ? ` (${schemaType})` : ''}`}
|
||||
description={outputItem.description}
|
||||
isIndent={hasObjectOutput}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
@ -18,5 +18,4 @@ export function getSelectedNode(
|
||||
return $isAtNodeEnd(anchor) ? anchorNode : focusNode
|
||||
}
|
||||
|
||||
// eslint-disable-next-line sonarjs/empty-string-repetition
|
||||
export const urlRegExp = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=+$,\w]+@)[A-Za-z0-9.-]+)((?:\/[+~%/.\w-]*)?\??[-+=&;%@.\w]*#?\w*)?)/
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
} from '@/app/components/workflow/constants'
|
||||
import { CUSTOM_ITERATION_START_NODE } from '@/app/components/workflow/nodes/iteration-start/constants'
|
||||
import { CUSTOM_LOOP_START_NODE } from '@/app/components/workflow/nodes/loop-start/constants'
|
||||
import { CUSTOM_SUB_GRAPH_START_NODE } from '@/app/components/workflow/nodes/sub-graph-start/constants'
|
||||
import {
|
||||
BlockEnum,
|
||||
} from '@/app/components/workflow/types'
|
||||
@ -442,6 +443,7 @@ const normaliseChildLayout = (
|
||||
const startNode = nodes.find(node =>
|
||||
node.type === CUSTOM_ITERATION_START_NODE
|
||||
|| node.type === CUSTOM_LOOP_START_NODE
|
||||
|| node.type === CUSTOM_SUB_GRAPH_START_NODE
|
||||
|| node.data?.type === BlockEnum.LoopStart
|
||||
|| node.data?.type === BlockEnum.IterationStart,
|
||||
)
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import type { CodeNodeType, OutputVar } from '../nodes/code/types'
|
||||
import type { IterationNodeType } from '../nodes/iteration/types'
|
||||
import type { LoopNodeType } from '../nodes/loop/types'
|
||||
import type {
|
||||
CommonNodeType,
|
||||
Node,
|
||||
} from '../types'
|
||||
import {
|
||||
@ -20,6 +22,60 @@ import {
|
||||
BlockEnum,
|
||||
} from '../types'
|
||||
|
||||
type MergeNodeDefaultDataParams<T extends CommonNodeType<Record<string, unknown>>> = {
|
||||
nodeType: BlockEnum
|
||||
metaDefault?: Partial<T>
|
||||
appDefault?: Partial<T>
|
||||
baseData?: Partial<T>
|
||||
overrideData?: Partial<T>
|
||||
}
|
||||
|
||||
const pickNonEmptyArray = <T>(value?: T[]) => {
|
||||
return Array.isArray(value) && value.length > 0 ? value : undefined
|
||||
}
|
||||
|
||||
export const mergeNodeDefaultData = <T extends CommonNodeType<Record<string, unknown>>>({
|
||||
nodeType,
|
||||
metaDefault,
|
||||
appDefault,
|
||||
baseData,
|
||||
overrideData,
|
||||
}: MergeNodeDefaultDataParams<T>) => {
|
||||
const merged = {
|
||||
...(metaDefault || {}),
|
||||
...(appDefault || {}),
|
||||
...(baseData || {}),
|
||||
...(overrideData || {}),
|
||||
} as Partial<T>
|
||||
|
||||
if (nodeType === BlockEnum.Code) {
|
||||
const codeMetaDefault = (metaDefault || {}) as Partial<CodeNodeType>
|
||||
const codeAppDefault = (appDefault || {}) as Partial<CodeNodeType>
|
||||
const codeBase = (baseData || {}) as Partial<CodeNodeType>
|
||||
const codeOverride = (overrideData || {}) as Partial<CodeNodeType>
|
||||
const codeDefaults = {
|
||||
...codeMetaDefault,
|
||||
...codeAppDefault,
|
||||
}
|
||||
|
||||
const outputs: OutputVar = {
|
||||
...(codeDefaults.outputs || {}),
|
||||
...(codeBase.outputs || {}),
|
||||
...(codeOverride.outputs || {}),
|
||||
}
|
||||
if (Object.keys(outputs).length > 0)
|
||||
(merged as Partial<CodeNodeType>).outputs = outputs
|
||||
|
||||
const resolvedVariables = pickNonEmptyArray(codeBase.variables)
|
||||
?? pickNonEmptyArray(codeOverride.variables)
|
||||
?? pickNonEmptyArray(codeDefaults.variables)
|
||||
if (resolvedVariables)
|
||||
(merged as Partial<CodeNodeType>).variables = resolvedVariables
|
||||
}
|
||||
|
||||
return merged
|
||||
}
|
||||
|
||||
export function generateNewNode<T = {}>({ data, position, id, zIndex, type, ...rest }: Omit<Node<T>, 'id'> & { id?: string }): {
|
||||
newNode: Node
|
||||
newIterationStartNode?: Node
|
||||
|
||||
@ -0,0 +1,60 @@
|
||||
import type { NodeProps } from 'reactflow'
|
||||
import type { CommonNodeType } from '@/app/components/workflow/types'
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { AssembleVariablesAlt } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { Agent } from '@/app/components/base/icons/src/vender/workflow'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { NodeSourceHandle } from '../../node-handle'
|
||||
|
||||
type SubGraphStartNodeData = CommonNodeType<{
|
||||
tooltip?: string
|
||||
iconType?: string
|
||||
}>
|
||||
|
||||
type IconComponent = typeof Agent
|
||||
|
||||
const iconMap: Record<string, IconComponent> = {
|
||||
agent: Agent,
|
||||
assemble: AssembleVariablesAlt,
|
||||
}
|
||||
|
||||
const SubGraphStartNode = ({ id, data }: NodeProps<SubGraphStartNodeData>) => {
|
||||
const { t } = useTranslation()
|
||||
const iconType = data?.iconType || 'agent'
|
||||
const Icon = iconMap[iconType] || Agent
|
||||
const rawTitle = data?.title?.trim() || ''
|
||||
const showTitle = iconType === 'agent' && !!rawTitle
|
||||
const displayTitle = showTitle && (rawTitle.startsWith('@') ? rawTitle : `@${rawTitle}`)
|
||||
const tooltip = data?.tooltip
|
||||
|| (iconType === 'assemble' ? t('blocks.start', { ns: 'workflow' }) : (data?.title || t('blocks.start', { ns: 'workflow' })))
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'nodrag group mt-1 flex h-11 items-center justify-center rounded-2xl border border-workflow-block-border bg-workflow-block-bg shadow-xs',
|
||||
showTitle ? 'gap-1.5 px-2' : 'w-11',
|
||||
)}
|
||||
>
|
||||
<Tooltip popupContent={tooltip} asChild={false}>
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded-full border-[0.5px] border-components-panel-border-subtle bg-util-colors-blue-brand-blue-brand-500">
|
||||
<Icon className="h-3 w-3 text-text-primary-on-surface" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
{showTitle && (
|
||||
<span className="system-xs-medium max-w-[160px] truncate text-text-secondary">
|
||||
{displayTitle}
|
||||
</span>
|
||||
)}
|
||||
<NodeSourceHandle
|
||||
id={id}
|
||||
data={data}
|
||||
handleClassName="!top-1/2 !-right-[9px] !-translate-y-1/2"
|
||||
handleId="source"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(SubGraphStartNode)
|
||||
@ -29,6 +29,7 @@ import {
|
||||
import CustomConnectionLine from '@/app/components/workflow/custom-connection-line'
|
||||
import { CUSTOM_ITERATION_START_NODE } from '@/app/components/workflow/nodes/iteration-start/constants'
|
||||
import { CUSTOM_LOOP_START_NODE } from '@/app/components/workflow/nodes/loop-start/constants'
|
||||
import { CUSTOM_SUB_GRAPH_START_NODE } from '@/app/components/workflow/nodes/sub-graph-start/constants'
|
||||
import { CUSTOM_NOTE_NODE } from '@/app/components/workflow/note-node/constants'
|
||||
import { CUSTOM_SIMPLE_NODE } from '@/app/components/workflow/simple-node/constants'
|
||||
import {
|
||||
@ -40,6 +41,7 @@ import CustomEdge from './components/custom-edge'
|
||||
import CustomNode from './components/nodes'
|
||||
import IterationStartNode from './components/nodes/iteration-start'
|
||||
import LoopStartNode from './components/nodes/loop-start'
|
||||
import SubGraphStartNode from './components/nodes/sub-graph-start'
|
||||
import CustomNoteNode from './components/note-node'
|
||||
import ZoomInOut from './components/zoom-in-out'
|
||||
import 'reactflow/dist/style.css'
|
||||
@ -49,6 +51,7 @@ const nodeTypes = {
|
||||
[CUSTOM_NODE]: CustomNode,
|
||||
[CUSTOM_NOTE_NODE]: CustomNoteNode,
|
||||
[CUSTOM_SIMPLE_NODE]: CustomNode,
|
||||
[CUSTOM_SUB_GRAPH_START_NODE]: SubGraphStartNode,
|
||||
[CUSTOM_ITERATION_START_NODE]: IterationStartNode,
|
||||
[CUSTOM_LOOP_START_NODE]: LoopStartNode,
|
||||
}
|
||||
|
||||
@ -3280,9 +3280,6 @@
|
||||
}
|
||||
},
|
||||
"app/components/workflow/nodes/data-source/panel.tsx": {
|
||||
"style/multiline-ternary": {
|
||||
"count": 2
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 3
|
||||
}
|
||||
@ -3634,11 +3631,6 @@
|
||||
"count": 7
|
||||
}
|
||||
},
|
||||
"app/components/workflow/nodes/tool/components/mixed-variable-text-input/index.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/workflow/nodes/tool/components/mixed-variable-text-input/placeholder.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
@ -3665,9 +3657,6 @@
|
||||
}
|
||||
},
|
||||
"app/components/workflow/nodes/tool/panel.tsx": {
|
||||
"style/multiline-ternary": {
|
||||
"count": 2
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 2
|
||||
}
|
||||
@ -4549,4 +4538,4 @@
|
||||
"count": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -766,8 +766,9 @@
|
||||
"nodes.templateTransform.codeSupportTip": "Only supports Jinja2",
|
||||
"nodes.templateTransform.inputVars": "Input Variables",
|
||||
"nodes.templateTransform.outputVars.output": "Transformed content",
|
||||
"nodes.tool.authorize": "Authorize",
|
||||
"nodes.tool.agentPlaceholder": "Tell me the {{paramKey}}...",
|
||||
"nodes.tool.assembleVariables": "Assemble variables",
|
||||
"nodes.tool.authorize": "Authorize",
|
||||
"nodes.tool.inputVars": "Input Variables",
|
||||
"nodes.tool.insertPlaceholder1": "Type or press",
|
||||
"nodes.tool.insertPlaceholder2": "insert variable",
|
||||
|
||||
@ -769,23 +769,11 @@ const translation = {
|
||||
'assignedVarsDescription': 'Assigned variables must be writable variables, such as conversation variables.',
|
||||
},
|
||||
tool: {
|
||||
assembleVariables: 'Assemble Variables',
|
||||
authorize: 'Authorize',
|
||||
inputVars: 'Input Variables',
|
||||
settings: 'Settings',
|
||||
insertPlaceholder1: 'Type or press',
|
||||
insertPlaceholder2: 'insert variable',
|
||||
contextGenerate: {
|
||||
title: 'Assemble Variables',
|
||||
codeBlock: 'Code Block',
|
||||
code: 'Code',
|
||||
output: 'Output',
|
||||
run: 'Run',
|
||||
apply: 'Apply',
|
||||
generating: 'Generating...',
|
||||
inputPlaceholder: 'Ask for change...',
|
||||
defaultAssistantMessage: 'I\'ve finished, please have a check on it.',
|
||||
},
|
||||
outputVars: {
|
||||
text: 'tool generated content',
|
||||
files: {
|
||||
|
||||
@ -764,8 +764,9 @@
|
||||
"nodes.templateTransform.codeSupportTip": "Jinja2 のみをサポートしています",
|
||||
"nodes.templateTransform.inputVars": "入力変数",
|
||||
"nodes.templateTransform.outputVars.output": "変換されたコンテンツ",
|
||||
"nodes.tool.authorize": "認証する",
|
||||
"nodes.tool.agentPlaceholder": "{{paramKey}} を教えてください...",
|
||||
"nodes.tool.assembleVariables": "変数を組み立てる",
|
||||
"nodes.tool.authorize": "認証する",
|
||||
"nodes.tool.inputVars": "入力変数",
|
||||
"nodes.tool.insertPlaceholder1": "タイプするか押してください",
|
||||
"nodes.tool.insertPlaceholder2": "変数を挿入する",
|
||||
|
||||
@ -764,8 +764,9 @@
|
||||
"nodes.templateTransform.codeSupportTip": "只支持 Jinja2",
|
||||
"nodes.templateTransform.inputVars": "输入变量",
|
||||
"nodes.templateTransform.outputVars.output": "转换后内容",
|
||||
"nodes.tool.authorize": "授权",
|
||||
"nodes.tool.agentPlaceholder": "告诉我 {{paramKey}}...",
|
||||
"nodes.tool.assembleVariables": "组装变量",
|
||||
"nodes.tool.authorize": "授权",
|
||||
"nodes.tool.inputVars": "输入变量",
|
||||
"nodes.tool.insertPlaceholder1": "键入",
|
||||
"nodes.tool.insertPlaceholder2": "插入变量",
|
||||
|
||||
@ -769,23 +769,11 @@ const translation = {
|
||||
'assignedVarsDescription': '赋值变量必须是可写入的变量,例如会话变量。',
|
||||
},
|
||||
tool: {
|
||||
assembleVariables: '组合变量',
|
||||
authorize: '授权',
|
||||
inputVars: '输入变量',
|
||||
settings: '设置',
|
||||
insertPlaceholder1: '键入',
|
||||
insertPlaceholder2: '插入变量',
|
||||
contextGenerate: {
|
||||
title: '组合变量',
|
||||
codeBlock: '代码块',
|
||||
code: '代码',
|
||||
output: '输出',
|
||||
run: '运行',
|
||||
apply: '应用',
|
||||
generating: '生成中...',
|
||||
inputPlaceholder: '输入修改需求...',
|
||||
defaultAssistantMessage: '已完成,请检查。',
|
||||
},
|
||||
outputVars: {
|
||||
text: '工具生成的内容',
|
||||
files: {
|
||||
|
||||
@ -764,8 +764,9 @@
|
||||
"nodes.templateTransform.codeSupportTip": "只支持 Jinja2",
|
||||
"nodes.templateTransform.inputVars": "輸入變數",
|
||||
"nodes.templateTransform.outputVars.output": "轉換後內容",
|
||||
"nodes.tool.authorize": "授權",
|
||||
"nodes.tool.agentPlaceholder": "告訴我 {{paramKey}}...",
|
||||
"nodes.tool.assembleVariables": "組裝變數",
|
||||
"nodes.tool.authorize": "授權",
|
||||
"nodes.tool.inputVars": "輸入變數",
|
||||
"nodes.tool.insertPlaceholder1": "輸入或按壓",
|
||||
"nodes.tool.insertPlaceholder2": "插入變數",
|
||||
|
||||
@ -747,7 +747,6 @@ const translation = {
|
||||
'varNotSet': '未設置變數',
|
||||
},
|
||||
tool: {
|
||||
assembleVariables: '組合變數',
|
||||
authorize: '授權',
|
||||
inputVars: '輸入變數',
|
||||
outputVars: {
|
||||
@ -764,17 +763,6 @@ const translation = {
|
||||
insertPlaceholder2: '插入變數',
|
||||
insertPlaceholder1: '輸入或按壓',
|
||||
settings: '設定',
|
||||
contextGenerate: {
|
||||
title: '組合變數',
|
||||
codeBlock: '程式碼區塊',
|
||||
code: '程式碼',
|
||||
output: '輸出',
|
||||
run: '執行',
|
||||
apply: '套用',
|
||||
generating: '生成中...',
|
||||
inputPlaceholder: '輸入修改需求...',
|
||||
defaultAssistantMessage: '已完成,請檢查。',
|
||||
},
|
||||
},
|
||||
questionClassifiers: {
|
||||
model: '模型',
|
||||
|
||||
@ -106,7 +106,7 @@ function parseTsContent(content: string): NestedTranslation {
|
||||
// Use Function constructor to safely evaluate the object literal
|
||||
// This handles JS object syntax like unquoted keys, template literals, etc.
|
||||
try {
|
||||
// eslint-disable-next-line no-new-func, sonarjs/code-eval
|
||||
// eslint-disable-next-line no-new-func
|
||||
const fn = new Function(`return (${cleaned})`)
|
||||
return fn() as NestedTranslation
|
||||
}
|
||||
@ -123,7 +123,7 @@ function parseTsContent(content: string): NestedTranslation {
|
||||
function getMainBranchFile(filePath: string): string | null {
|
||||
try {
|
||||
const relativePath = `./i18n/${LOCALE}/${filePath}`
|
||||
// eslint-disable-next-line sonarjs/os-command
|
||||
|
||||
return execSync(`git show main:${relativePath}`, {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
@ -148,12 +148,12 @@ function getTranslationFiles(): string[] {
|
||||
function getMainBranchNamespaces(): string[] {
|
||||
try {
|
||||
const relativePath = `./i18n/${LOCALE}`
|
||||
// eslint-disable-next-line sonarjs/os-command
|
||||
|
||||
const output = execSync(`git ls-tree --name-only main ${relativePath}/`, {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
})
|
||||
// eslint-disable-next-line sonarjs/os-command
|
||||
|
||||
return output
|
||||
.trim()
|
||||
.split('\n')
|
||||
|
||||
@ -25,38 +25,6 @@ export type CodeGenRes = {
|
||||
error?: string
|
||||
}
|
||||
|
||||
export type ContextGenerateMessage = {
|
||||
role: 'user' | 'assistant' | 'system'
|
||||
content: string
|
||||
}
|
||||
|
||||
export type ContextGenerateRequest = {
|
||||
workflow_id: string
|
||||
node_id: string
|
||||
parameter_name: string
|
||||
language?: 'python3' | 'javascript'
|
||||
prompt_messages: ContextGenerateMessage[]
|
||||
model_config: {
|
||||
provider: string
|
||||
name: string
|
||||
completion_params?: Record<string, any>
|
||||
}
|
||||
}
|
||||
|
||||
export type ContextGenerateVariable = {
|
||||
variable: string
|
||||
value_selector: string[]
|
||||
}
|
||||
|
||||
export type ContextGenerateResponse = {
|
||||
variables: ContextGenerateVariable[]
|
||||
code_language: string
|
||||
code: string
|
||||
outputs: Record<string, { type: string }>
|
||||
message: string
|
||||
error: string
|
||||
}
|
||||
|
||||
export const sendChatMessage = async (appId: string, body: Record<string, any>, { onData, onCompleted, onThought, onFile, onError, getAbortController, onMessageEnd, onMessageReplace }: {
|
||||
onData: IOnData
|
||||
onCompleted: IOnCompleted
|
||||
@ -125,12 +93,6 @@ export const generateRule = (body: Record<string, any>) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const generateContext = (body: ContextGenerateRequest) => {
|
||||
return post<ContextGenerateResponse>('/context-generate', {
|
||||
body,
|
||||
})
|
||||
}
|
||||
|
||||
export const fetchModelParams = (providerName: string, modelId: string) => {
|
||||
return get(`workspaces/current/model-providers/${providerName}/models/parameter-rules`, {
|
||||
params: {
|
||||
|
||||
Reference in New Issue
Block a user