Compare commits

...

18 Commits

Author SHA1 Message Date
Yeuoly
0e2d43e25f Merge branch 'feat/llm-support-jinia-fe' into feat/workflow-llm-jina-prompt 2024-05-01 13:07:17 +08:00
Yeuoly
e0a66aeab8 Merge branch 'main' into feat/workflow-llm-jina-prompt 2024-05-01 13:04:34 +08:00
Yeuoly
682024a136 single step debug 2024-04-30 20:04:25 +08:00
Joel
2cbb4c8e8f fix:line num align 2024-04-30 17:36:03 +08:00
Yeuoly
5a7cf65179 fix 2024-04-30 16:43:58 +08:00
Joel
5cc9bf3258 Merge branch 'main' into feat/llm-support-jinia-fe 2024-04-30 16:28:42 +08:00
Joel
29ef66958f feat: check var 2024-04-30 16:11:41 +08:00
Joel
b849b034d8 fix: handle varlist not passed 2024-04-30 15:46:49 +08:00
Joel
944d738342 fix: inputs not auto sync 2024-04-30 15:37:45 +08:00
Joel
72ddd1eb2d feat: support vars 2024-04-30 15:01:48 +08:00
Joel
ec7e3e9aed fix: prompt show value error 2024-04-30 11:18:38 +08:00
Joel
35c0789cc2 feat: jina select 2024-04-30 11:10:36 +08:00
Yeuoly
87d8b3651f add dataset parser 2024-04-30 01:10:53 +08:00
Joel
a47f03e847 feat: add jinja2 switcher 2024-04-29 18:44:12 +08:00
Joel
340072c6e4 feat: make jinjia into prompt editor 2024-04-29 14:36:39 +08:00
Joel
e9d2d7b07f feat: add code editor 2024-04-29 11:55:21 +08:00
Yeuoly
6aaac2fe80 compatible 2024-04-29 11:46:09 +08:00
Yeuoly
3bbfe14771 support jinja2 llm prompt template 2024-04-29 11:15:16 +08:00
29 changed files with 909 additions and 207 deletions

View File

@@ -0,0 +1,17 @@
from core.helper.code_executor.code_executor import CodeExecutor
class Jinja2Formatter:
@classmethod
def format(cls, template: str, inputs: str) -> str:
"""
Format template
:param template: template
:param inputs: inputs
:return:
"""
result = CodeExecutor.execute_workflow_code_template(
language='jinja2', code=template, inputs=inputs
)
return result['result']

View File

@@ -2,6 +2,7 @@ from typing import Optional, Union
from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity
from core.file.file_obj import FileVar
from core.helper.code_executor.jinja2_formatter import Jinja2Formatter
from core.memory.token_buffer_memory import TokenBufferMemory
from core.model_runtime.entities.message_entities import (
AssistantPromptMessage,
@@ -80,29 +81,35 @@ class AdvancedPromptTransform(PromptTransform):
prompt_messages = []
prompt_template = PromptTemplateParser(template=raw_prompt, with_variable_tmpl=self.with_variable_tmpl)
prompt_inputs = {k: inputs[k] for k in prompt_template.variable_keys if k in inputs}
if prompt_template.edition_type == 'basic' or not prompt_template.edition_type:
prompt_template = PromptTemplateParser(template=raw_prompt, with_variable_tmpl=self.with_variable_tmpl)
prompt_inputs = {k: inputs[k] for k in prompt_template.variable_keys if k in inputs}
prompt_inputs = self._set_context_variable(context, prompt_template, prompt_inputs)
prompt_inputs = self._set_context_variable(context, prompt_template, prompt_inputs)
if memory and memory_config:
role_prefix = memory_config.role_prefix
prompt_inputs = self._set_histories_variable(
memory=memory,
memory_config=memory_config,
raw_prompt=raw_prompt,
role_prefix=role_prefix,
prompt_template=prompt_template,
prompt_inputs=prompt_inputs,
model_config=model_config
if memory and memory_config:
role_prefix = memory_config.role_prefix
prompt_inputs = self._set_histories_variable(
memory=memory,
memory_config=memory_config,
raw_prompt=raw_prompt,
role_prefix=role_prefix,
prompt_template=prompt_template,
prompt_inputs=prompt_inputs,
model_config=model_config
)
if query:
prompt_inputs = self._set_query_variable(query, prompt_template, prompt_inputs)
prompt = prompt_template.format(
prompt_inputs
)
else:
prompt = raw_prompt
prompt_inputs = inputs
if query:
prompt_inputs = self._set_query_variable(query, prompt_template, prompt_inputs)
prompt = prompt_template.format(
prompt_inputs
)
prompt = Jinja2Formatter.format(prompt, prompt_inputs)
if files:
prompt_message_contents = [TextPromptMessageContent(data=prompt)]
@@ -135,14 +142,22 @@ class AdvancedPromptTransform(PromptTransform):
for prompt_item in raw_prompt_list:
raw_prompt = prompt_item.text
prompt_template = PromptTemplateParser(template=raw_prompt, with_variable_tmpl=self.with_variable_tmpl)
prompt_inputs = {k: inputs[k] for k in prompt_template.variable_keys if k in inputs}
if prompt_item.edition_type == 'basic' or not prompt_item.edition_type:
prompt_template = PromptTemplateParser(template=raw_prompt, with_variable_tmpl=self.with_variable_tmpl)
prompt_inputs = {k: inputs[k] for k in prompt_template.variable_keys if k in inputs}
prompt_inputs = self._set_context_variable(context, prompt_template, prompt_inputs)
prompt_inputs = self._set_context_variable(context, prompt_template, prompt_inputs)
prompt = prompt_template.format(
prompt_inputs
)
prompt = prompt_template.format(
prompt_inputs
)
elif prompt_item.edition_type == 'jinja2':
prompt = raw_prompt
prompt_inputs = inputs
prompt = Jinja2Formatter.format(prompt, prompt_inputs)
else:
raise ValueError(f'Invalid edition type: {prompt_item.edition_type}')
if prompt_item.role == PromptMessageRole.USER:
prompt_messages.append(UserPromptMessage(content=prompt))

View File

@@ -1,4 +1,4 @@
from typing import Optional
from typing import Literal, Optional
from pydantic import BaseModel
@@ -11,6 +11,7 @@ class ChatModelMessage(BaseModel):
"""
text: str
role: PromptMessageRole
edition_type: Optional[Literal['basic', 'jinja2']]
class CompletionModelPromptTemplate(BaseModel):
@@ -18,6 +19,7 @@ class CompletionModelPromptTemplate(BaseModel):
Completion Model Prompt Template.
"""
text: str
edition_type: Optional[Literal['basic', 'jinja2']]
class MemoryConfig(BaseModel):

View File

@@ -4,6 +4,7 @@ from pydantic import BaseModel
from core.prompt.entities.advanced_prompt_entities import ChatModelMessage, CompletionModelPromptTemplate, MemoryConfig
from core.workflow.entities.base_node_data_entities import BaseNodeData
from core.workflow.entities.variable_entities import VariableSelector
class ModelConfig(BaseModel):
@@ -37,13 +38,31 @@ class VisionConfig(BaseModel):
enabled: bool
configs: Optional[Configs] = None
class PromptConfig(BaseModel):
"""
Prompt Config.
"""
jinja2_variables: Optional[list[VariableSelector]] = None
class LLMNodeChatModelMessage(ChatModelMessage):
"""
LLM Node Chat Model Message.
"""
jinja2_text: Optional[str] = None
class LLMNodeCompletionModelPromptTemplate(CompletionModelPromptTemplate):
"""
LLM Node Chat Model Prompt Template.
"""
jinja2_text: Optional[str] = None
class LLMNodeData(BaseNodeData):
"""
LLM Node Data.
"""
model: ModelConfig
prompt_template: Union[list[ChatModelMessage], CompletionModelPromptTemplate]
prompt_template: Union[list[LLMNodeChatModelMessage], LLMNodeCompletionModelPromptTemplate]
prompt_config: Optional[PromptConfig] = None
memory: Optional[MemoryConfig] = None
context: ContextConfig
vision: VisionConfig

View File

@@ -1,4 +1,6 @@
import json
from collections.abc import Generator
from copy import deepcopy
from typing import Optional, cast
from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity
@@ -17,11 +19,15 @@ from core.model_runtime.utils.encoders import jsonable_encoder
from core.prompt.advanced_prompt_transform import AdvancedPromptTransform
from core.prompt.entities.advanced_prompt_entities import CompletionModelPromptTemplate, MemoryConfig
from core.prompt.utils.prompt_message_util import PromptMessageUtil
from core.workflow.entities.base_node_data_entities import BaseNodeData
from core.workflow.entities.node_entities import NodeRunMetadataKey, NodeRunResult, NodeType, SystemVariable
from core.workflow.entities.variable_pool import VariablePool
from core.workflow.nodes.base_node import BaseNode
from core.workflow.nodes.llm.entities import LLMNodeData, ModelConfig
from core.workflow.nodes.llm.entities import (
LLMNodeChatModelMessage,
LLMNodeCompletionModelPromptTemplate,
LLMNodeData,
ModelConfig,
)
from core.workflow.utils.variable_template_parser import VariableTemplateParser
from extensions.ext_database import db
from models.model import Conversation
@@ -39,16 +45,24 @@ class LLMNode(BaseNode):
:param variable_pool: variable pool
:return:
"""
node_data = self.node_data
node_data = cast(self._node_data_cls, node_data)
node_data = cast(LLMNodeData, deepcopy(self.node_data))
node_inputs = None
process_data = None
try:
# init messages template
node_data.prompt_template = self._transform_chat_messages(node_data.prompt_template)
# fetch variables and fetch values from variable pool
inputs = self._fetch_inputs(node_data, variable_pool)
# fetch jinja2 inputs
jinja_inputs = self._fetch_jinja_inputs(node_data, variable_pool)
# merge inputs
inputs.update(jinja_inputs)
node_inputs = {}
# fetch files
@@ -183,6 +197,86 @@ class LLMNode(BaseNode):
usage = LLMUsage.empty_usage()
return full_text, usage
def _transform_chat_messages(self,
messages: list[LLMNodeChatModelMessage] | LLMNodeCompletionModelPromptTemplate
) -> list[LLMNodeChatModelMessage] | LLMNodeCompletionModelPromptTemplate:
"""
Transform chat messages
:param messages: chat messages
:return:
"""
if isinstance(messages, LLMNodeCompletionModelPromptTemplate):
if messages.edition_type == 'jinja2':
messages.text = messages.jinja2_text
return messages
for message in messages:
if message.edition_type == 'jinja2':
message.text = message.jinja2_text
return messages
def _fetch_jinja_inputs(self, node_data: LLMNodeData, variable_pool: VariablePool) -> dict[str, str]:
"""
Fetch jinja inputs
:param node_data: node data
:param variable_pool: variable pool
:return:
"""
variables = {}
if not node_data.prompt_config:
return variables
for variable_selector in node_data.prompt_config.jinja2_variables or []:
variable = variable_selector.variable
value = variable_pool.get_variable_value(
variable_selector=variable_selector.value_selector
)
def parse_dict(d: dict) -> str:
"""
Parse dict into string
"""
# check if it's a context structure
if 'metadata' in d and '_source' in d['metadata'] and 'content' in d:
return d['content']
# else, parse the dict
try:
return json.dumps(d, ensure_ascii=False)
except Exception:
return str(d)
if isinstance(value, str):
value = value
elif isinstance(value, list):
result = ''
for item in value:
if isinstance(item, dict):
result += parse_dict(item)
elif isinstance(item, str):
result += item
elif isinstance(item, int | float):
result += str(item)
else:
result += str(item)
result += '\n'
value = result.strip()
elif isinstance(value, dict):
value = parse_dict(value)
elif isinstance(value, int | float):
value = str(value)
else:
value = str(value)
variables[variable] = value
return variables
def _fetch_inputs(self, node_data: LLMNodeData, variable_pool: VariablePool) -> dict[str, str]:
"""
@@ -531,14 +625,12 @@ class LLMNode(BaseNode):
db.session.commit()
@classmethod
def _extract_variable_selector_to_variable_mapping(cls, node_data: BaseNodeData) -> dict[str, list[str]]:
def _extract_variable_selector_to_variable_mapping(cls, node_data: LLMNodeData) -> dict[str, list[str]]:
"""
Extract variable selector to variable mapping
:param node_data: node data
:return:
"""
node_data = node_data
node_data = cast(cls._node_data_cls, node_data)
prompt_template = node_data.prompt_template
@@ -571,6 +663,22 @@ class LLMNode(BaseNode):
if node_data.memory:
variable_mapping['#sys.query#'] = ['sys', SystemVariable.QUERY.value]
if node_data.prompt_config:
enable_jinja = False
if isinstance(prompt_template, list):
for prompt in prompt_template:
if prompt.edition_type == 'jinja2':
enable_jinja = True
break
else:
if prompt_template.edition_type == 'jinja2':
enable_jinja = True
if enable_jinja:
for variable_selector in node_data.prompt_config.jinja2_variables or []:
variable_mapping[variable_selector.variable] = variable_selector.value_selector
return variable_mapping
@classmethod
@@ -588,7 +696,8 @@ class LLMNode(BaseNode):
"prompts": [
{
"role": "system",
"text": "You are a helpful AI assistant."
"text": "You are a helpful AI assistant.",
"edition_type": "basic"
}
]
},
@@ -600,7 +709,8 @@ class LLMNode(BaseNode):
"prompt": {
"text": "Here is the chat histories between human and assistant, inside "
"<histories></histories> XML tags.\n\n<histories>\n{{"
"#histories#}}\n</histories>\n\n\nHuman: {{#sys.query#}}\n\nAssistant:"
"#histories#}}\n</histories>\n\n\nHuman: {{#sys.query#}}\n\nAssistant:",
"edition_type": "basic"
},
"stop": ["Human:"]
}

View File

@@ -3,3 +3,4 @@ pytest~=8.1.1
pytest-benchmark~=4.0.0
pytest-env~=1.1.3
pytest-mock~=3.14.0
jinja2~=3.1.2

View File

@@ -3,6 +3,7 @@ from typing import Literal
import pytest
from _pytest.monkeypatch import MonkeyPatch
from jinja2 import Template
from core.helper.code_executor.code_executor import CodeExecutor
@@ -18,7 +19,7 @@ class MockedCodeExecutor:
}
elif language == 'jinja2':
return {
"result": "3"
"result": Template(code).render(inputs)
}
@pytest.fixture

View File

@@ -1,3 +1,4 @@
import json
import os
from unittest.mock import MagicMock
@@ -19,6 +20,7 @@ from models.workflow import WorkflowNodeExecutionStatus
"""FOR MOCK FIXTURES, DO NOT REMOVE"""
from tests.integration_tests.model_runtime.__mock.openai import setup_openai_mock
from tests.integration_tests.workflow.nodes.__mock.code_executor import setup_code_executor_mock
@pytest.mark.parametrize('setup_openai_mock', [['chat']], indirect=True)
@@ -116,3 +118,118 @@ def test_execute_llm(setup_openai_mock):
assert result.status == WorkflowNodeExecutionStatus.SUCCEEDED
assert result.outputs['text'] is not None
assert result.outputs['usage']['total_tokens'] > 0
@pytest.mark.parametrize('setup_code_executor_mock', [['none']], indirect=True)
@pytest.mark.parametrize('setup_openai_mock', [['chat']], indirect=True)
def test_execute_llm_with_jinja2(setup_code_executor_mock, setup_openai_mock):
"""
Test execute LLM node with jinja2
"""
node = LLMNode(
tenant_id='1',
app_id='1',
workflow_id='1',
user_id='1',
user_from=UserFrom.ACCOUNT,
config={
'id': 'llm',
'data': {
'title': '123',
'type': 'llm',
'model': {
'provider': 'openai',
'name': 'gpt-3.5-turbo',
'mode': 'chat',
'completion_params': {}
},
'prompt_config': {
'jinja2_variables': [{
'variable': 'sys_query',
'value_selector': ['sys', 'query']
}, {
'variable': 'output',
'value_selector': ['abc', 'output']
}]
},
'prompt_template': [
{
'role': 'system',
'text': 'you are a helpful assistant.\ntoday\'s weather is {{#abc.output#}}',
'jinja2_text': 'you are a helpful assistant.\ntoday\'s weather is {{output}}.',
'edition_type': 'jinja2'
},
{
'role': 'user',
'text': '{{#sys.query#}}',
'jinja2_text': '{{sys_query}}',
'edition_type': 'basic'
}
],
'memory': None,
'context': {
'enabled': False
},
'vision': {
'enabled': False
}
}
}
)
# construct variable pool
pool = VariablePool(system_variables={
SystemVariable.QUERY: 'what\'s the weather today?',
SystemVariable.FILES: [],
SystemVariable.CONVERSATION_ID: 'abababa',
SystemVariable.USER_ID: 'aaa'
}, user_inputs={})
pool.append_variable(node_id='abc', variable_key_list=['output'], value='sunny')
credentials = {
'openai_api_key': os.environ.get('OPENAI_API_KEY')
}
provider_instance = ModelProviderFactory().get_provider_instance('openai')
model_type_instance = provider_instance.get_model_instance(ModelType.LLM)
provider_model_bundle = ProviderModelBundle(
configuration=ProviderConfiguration(
tenant_id='1',
provider=provider_instance.get_provider_schema(),
preferred_provider_type=ProviderType.CUSTOM,
using_provider_type=ProviderType.CUSTOM,
system_configuration=SystemConfiguration(
enabled=False
),
custom_configuration=CustomConfiguration(
provider=CustomProviderConfiguration(
credentials=credentials
)
)
),
provider_instance=provider_instance,
model_type_instance=model_type_instance
)
model_instance = ModelInstance(provider_model_bundle=provider_model_bundle, model='gpt-3.5-turbo')
model_config = ModelConfigWithCredentialsEntity(
model='gpt-3.5-turbo',
provider='openai',
mode='chat',
credentials=credentials,
parameters={},
model_schema=model_type_instance.get_model_schema('gpt-3.5-turbo'),
provider_model_bundle=provider_model_bundle
)
# Mock db.session.close()
db.session.close = MagicMock()
node._fetch_model_config = MagicMock(return_value=tuple([model_instance, model_config]))
# execute node
result = node.run(pool)
assert result.status == WorkflowNodeExecutionStatus.SUCCEEDED
assert 'sunny' in json.dumps(result.process_data)
assert 'what\'s the weather today?' in json.dumps(result.process_data)

View File

@@ -0,0 +1,13 @@
<svg width="24" height="12" viewBox="0 0 24 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Jinja Icon">
<g id="Vector">
<path d="M7.46013 5.99982C7.46013 4.87982 7.48013 3.92982 7.53013 3.16982V3.06982L6.13013 3.23982L6.15013 3.32982C6.29013 4.03982 6.36013 4.93982 6.36013 5.99982C6.36013 6.93982 6.33013 7.78982 6.28013 8.51982V8.60982H7.55013V8.51982C7.49013 7.72982 7.46013 6.87982 7.46013 5.99982Z" fill="#667085"/>
<path d="M3.33016 1.31998C3.38016 2.31998 3.38016 5.13998 3.38016 7.00998V7.77998C3.38016 8.21998 3.35016 8.58998 3.28016 8.85998C3.22016 9.12998 3.11016 9.34998 2.96016 9.52998C2.82016 9.70998 2.62016 9.83998 2.37016 9.92998C2.12016 10.01 1.82016 10.06 1.49016 10.06C1.19016 10.06 0.900156 9.99998 0.620156 9.87998L0.520156 9.83998L0.410156 10.83L0.480156 10.85C0.800156 10.93 1.16016 10.97 1.56016 10.97C2.08016 10.97 2.53016 10.9 2.90016 10.77C3.28016 10.64 3.59016 10.43 3.83016 10.15C4.07016 9.87998 4.25016 9.52998 4.36016 9.13998C4.47016 8.74998 4.53016 8.23998 4.53016 7.64998C4.53016 6.78998 4.59016 3.54998 4.59016 3.17998C4.61016 2.47998 4.63016 1.86998 4.66016 1.31998V1.22998H3.33016V1.31998Z" fill="#667085"/>
<path d="M7.08021 0.919922C6.82022 0.919922 6.60021 0.999922 6.45021 1.14992C6.30021 1.29992 6.22021 1.47992 6.22021 1.68992C6.22021 1.87992 6.28021 2.04992 6.41021 2.18992C6.54022 2.31992 6.73022 2.38992 6.96022 2.38992C7.23022 2.38992 7.44021 2.30992 7.59021 2.15992C7.74021 1.99992 7.81021 1.81992 7.81021 1.60992C7.81021 1.42992 7.74021 1.25992 7.61021 1.12992C7.48021 0.989922 7.30021 0.919922 7.08021 0.919922Z" fill="#667085"/>
<path d="M15.6102 3.30981C15.7702 4.07981 15.8502 5.25981 15.8502 6.81981C15.8502 8.26981 15.7902 9.23981 15.6702 9.67981C15.5902 9.96981 15.3802 10.2598 15.0302 10.5198L14.9702 10.5698L15.3502 11.0998H15.4002C16.4302 10.8198 16.9602 10.0598 16.9602 8.83981C16.9602 8.64981 16.9502 8.30981 16.9202 7.80981C16.9002 7.31981 16.8902 6.90981 16.8902 6.59981C16.8902 5.44981 16.9202 4.28981 16.9902 3.15981V3.05981L15.5802 3.21981L15.6002 3.30981H15.6102Z" fill="#667085"/>
<path d="M14.2901 5.77C14.2901 5.7 14.2901 5.56 14.3001 5.36C14.3001 5.15 14.3101 5.01 14.3101 4.94C14.3101 4.22 14.1101 3.71 13.7201 3.43C13.3401 3.15 12.8001 3 12.1101 3C11.4201 3 10.7901 3.24 10.2001 3.71L10.0901 3.06L8.8501 3.22L8.8701 3.31C9.0501 4.11 9.1401 4.95 9.1401 5.8C9.1401 6.36 9.1101 7.27 9.0401 8.52V8.61H10.3101V8.53C10.2901 7.07 10.2801 5.71 10.2801 4.49C10.7401 4.14 11.2501 3.96 11.7901 3.96C12.2401 3.96 12.5801 4.06 12.8201 4.26C13.0501 4.45 13.1701 4.82 13.1701 5.36C13.1701 6.5 13.1301 7.56 13.0401 8.53V8.62H14.3101V8.54C14.2901 7.35 14.2801 6.42 14.2801 5.79L14.2901 5.77Z" fill="#667085"/>
<path d="M16.5302 0.919922C16.2702 0.919922 16.0502 0.999922 15.9002 1.14992C15.7502 1.29992 15.6702 1.47992 15.6702 1.68992C15.6702 1.87992 15.7302 2.04992 15.8602 2.18992C15.9902 2.31992 16.1802 2.38992 16.4102 2.38992C16.6702 2.38992 16.8902 2.30992 17.0302 2.15992C17.1802 1.99992 17.2502 1.81992 17.2502 1.60992C17.2502 1.42992 17.1802 1.25992 17.0502 1.12992C16.9202 0.989922 16.7402 0.919922 16.5202 0.919922H16.5302Z" fill="#667085"/>
<path d="M23.1802 8.51001C23.0702 8.00001 23.0202 7.40001 23.0202 6.73001C23.0202 6.57001 23.0202 6.26001 23.0402 5.83001C23.0602 5.38001 23.0702 5.06001 23.0702 4.88001C23.0702 4.20001 22.8602 3.71001 22.4502 3.43001C22.0402 3.15001 21.4702 3.01001 20.7302 3.01001C19.9402 3.01001 19.2302 3.09001 18.6102 3.25001H18.5602L18.4302 4.20001L18.5502 4.17001C19.1602 4.03001 19.7802 3.96001 20.4102 3.96001C20.9302 3.96001 21.3202 4.03001 21.5702 4.18001C21.8102 4.31001 21.9302 4.59001 21.9302 5.01001C21.9302 5.09001 21.9302 5.16001 21.9302 5.23001C20.5102 5.25001 19.5602 5.44001 19.0302 5.79001C18.4802 6.15001 18.2002 6.63001 18.2002 7.23001C18.2002 7.72001 18.3802 8.10001 18.7402 8.36001C19.0902 8.62001 19.5102 8.75001 19.9902 8.75001C20.8202 8.75001 21.5002 8.55001 22.0102 8.17001C22.0102 8.30001 22.0402 8.44001 22.0802 8.58001L22.1002 8.64001L23.2202 8.60001L23.2002 8.50001L23.1802 8.51001ZM20.2802 6.18001C20.6502 6.08001 21.2002 6.03001 21.9102 6.03001C21.9102 6.45001 21.9202 6.92001 21.9402 7.42001C21.5602 7.69001 21.0502 7.83001 20.4302 7.83001C19.7002 7.83001 19.3502 7.61001 19.3502 7.16001C19.3502 6.68001 19.6602 6.36001 20.2802 6.18001Z" fill="#667085"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,98 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "24",
"height": "12",
"viewBox": "0 0 24 12",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "Jinja Icon"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "Vector"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M7.46013 5.99982C7.46013 4.87982 7.48013 3.92982 7.53013 3.16982V3.06982L6.13013 3.23982L6.15013 3.32982C6.29013 4.03982 6.36013 4.93982 6.36013 5.99982C6.36013 6.93982 6.33013 7.78982 6.28013 8.51982V8.60982H7.55013V8.51982C7.49013 7.72982 7.46013 6.87982 7.46013 5.99982Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M3.33016 1.31998C3.38016 2.31998 3.38016 5.13998 3.38016 7.00998V7.77998C3.38016 8.21998 3.35016 8.58998 3.28016 8.85998C3.22016 9.12998 3.11016 9.34998 2.96016 9.52998C2.82016 9.70998 2.62016 9.83998 2.37016 9.92998C2.12016 10.01 1.82016 10.06 1.49016 10.06C1.19016 10.06 0.900156 9.99998 0.620156 9.87998L0.520156 9.83998L0.410156 10.83L0.480156 10.85C0.800156 10.93 1.16016 10.97 1.56016 10.97C2.08016 10.97 2.53016 10.9 2.90016 10.77C3.28016 10.64 3.59016 10.43 3.83016 10.15C4.07016 9.87998 4.25016 9.52998 4.36016 9.13998C4.47016 8.74998 4.53016 8.23998 4.53016 7.64998C4.53016 6.78998 4.59016 3.54998 4.59016 3.17998C4.61016 2.47998 4.63016 1.86998 4.66016 1.31998V1.22998H3.33016V1.31998Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M7.08021 0.919922C6.82022 0.919922 6.60021 0.999922 6.45021 1.14992C6.30021 1.29992 6.22021 1.47992 6.22021 1.68992C6.22021 1.87992 6.28021 2.04992 6.41021 2.18992C6.54022 2.31992 6.73022 2.38992 6.96022 2.38992C7.23022 2.38992 7.44021 2.30992 7.59021 2.15992C7.74021 1.99992 7.81021 1.81992 7.81021 1.60992C7.81021 1.42992 7.74021 1.25992 7.61021 1.12992C7.48021 0.989922 7.30021 0.919922 7.08021 0.919922Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M15.6102 3.30981C15.7702 4.07981 15.8502 5.25981 15.8502 6.81981C15.8502 8.26981 15.7902 9.23981 15.6702 9.67981C15.5902 9.96981 15.3802 10.2598 15.0302 10.5198L14.9702 10.5698L15.3502 11.0998H15.4002C16.4302 10.8198 16.9602 10.0598 16.9602 8.83981C16.9602 8.64981 16.9502 8.30981 16.9202 7.80981C16.9002 7.31981 16.8902 6.90981 16.8902 6.59981C16.8902 5.44981 16.9202 4.28981 16.9902 3.15981V3.05981L15.5802 3.21981L15.6002 3.30981H15.6102Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M14.2901 5.77C14.2901 5.7 14.2901 5.56 14.3001 5.36C14.3001 5.15 14.3101 5.01 14.3101 4.94C14.3101 4.22 14.1101 3.71 13.7201 3.43C13.3401 3.15 12.8001 3 12.1101 3C11.4201 3 10.7901 3.24 10.2001 3.71L10.0901 3.06L8.8501 3.22L8.8701 3.31C9.0501 4.11 9.1401 4.95 9.1401 5.8C9.1401 6.36 9.1101 7.27 9.0401 8.52V8.61H10.3101V8.53C10.2901 7.07 10.2801 5.71 10.2801 4.49C10.7401 4.14 11.2501 3.96 11.7901 3.96C12.2401 3.96 12.5801 4.06 12.8201 4.26C13.0501 4.45 13.1701 4.82 13.1701 5.36C13.1701 6.5 13.1301 7.56 13.0401 8.53V8.62H14.3101V8.54C14.2901 7.35 14.2801 6.42 14.2801 5.79L14.2901 5.77Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M16.5302 0.919922C16.2702 0.919922 16.0502 0.999922 15.9002 1.14992C15.7502 1.29992 15.6702 1.47992 15.6702 1.68992C15.6702 1.87992 15.7302 2.04992 15.8602 2.18992C15.9902 2.31992 16.1802 2.38992 16.4102 2.38992C16.6702 2.38992 16.8902 2.30992 17.0302 2.15992C17.1802 1.99992 17.2502 1.81992 17.2502 1.60992C17.2502 1.42992 17.1802 1.25992 17.0502 1.12992C16.9202 0.989922 16.7402 0.919922 16.5202 0.919922H16.5302Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M23.1802 8.51001C23.0702 8.00001 23.0202 7.40001 23.0202 6.73001C23.0202 6.57001 23.0202 6.26001 23.0402 5.83001C23.0602 5.38001 23.0702 5.06001 23.0702 4.88001C23.0702 4.20001 22.8602 3.71001 22.4502 3.43001C22.0402 3.15001 21.4702 3.01001 20.7302 3.01001C19.9402 3.01001 19.2302 3.09001 18.6102 3.25001H18.5602L18.4302 4.20001L18.5502 4.17001C19.1602 4.03001 19.7802 3.96001 20.4102 3.96001C20.9302 3.96001 21.3202 4.03001 21.5702 4.18001C21.8102 4.31001 21.9302 4.59001 21.9302 5.01001C21.9302 5.09001 21.9302 5.16001 21.9302 5.23001C20.5102 5.25001 19.5602 5.44001 19.0302 5.79001C18.4802 6.15001 18.2002 6.63001 18.2002 7.23001C18.2002 7.72001 18.3802 8.10001 18.7402 8.36001C19.0902 8.62001 19.5102 8.75001 19.9902 8.75001C20.8202 8.75001 21.5002 8.55001 22.0102 8.17001C22.0102 8.30001 22.0402 8.44001 22.0802 8.58001L22.1002 8.64001L23.2202 8.60001L23.2002 8.50001L23.1802 8.51001ZM20.2802 6.18001C20.6502 6.08001 21.2002 6.03001 21.9102 6.03001C21.9102 6.45001 21.9202 6.92001 21.9402 7.42001C21.5602 7.69001 21.0502 7.83001 20.4302 7.83001C19.7002 7.83001 19.3502 7.61001 19.3502 7.16001C19.3502 6.68001 19.6602 6.36001 20.2802 6.18001Z",
"fill": "currentColor"
},
"children": []
}
]
}
]
}
]
},
"name": "Jinja"
}

View File

@@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './Jinja.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'Jinja'
export default Icon

View File

@@ -4,6 +4,7 @@ export { default as End } from './End'
export { default as Home } from './Home'
export { default as Http } from './Http'
export { default as IfElse } from './IfElse'
export { default as Jinja } from './Jinja'
export { default as KnowledgeRetrieval } from './KnowledgeRetrieval'
export { default as Llm } from './Llm'
export { default as QuestionClassifier } from './QuestionClassifier'

View File

@@ -5,7 +5,7 @@ import { Switch as OriginalSwitch } from '@headlessui/react'
type SwitchProps = {
onChange: (value: boolean) => void
size?: 'md' | 'lg' | 'l'
size?: 'sm' | 'md' | 'lg' | 'l'
defaultValue?: boolean
disabled?: boolean
}
@@ -19,18 +19,21 @@ const Switch = ({ onChange, size = 'lg', defaultValue = false, disabled = false
lg: 'h-6 w-11',
l: 'h-5 w-9',
md: 'h-4 w-7',
sm: 'h-3 w-5',
}
const circleStyle = {
lg: 'h-5 w-5',
l: 'h-4 w-4',
md: 'h-3 w-3',
sm: 'h-2 w-2',
}
const translateLeft = {
lg: 'translate-x-5',
l: 'translate-x-4',
md: 'translate-x-3',
sm: 'translate-x-2',
}
return (
<OriginalSwitch

View File

@@ -1,6 +1,7 @@
'use client'
import type { FC } from 'react'
import React, { useState } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import { useBoolean } from 'ahooks'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
export type TooltipProps = {
position?: 'top' | 'right' | 'bottom' | 'left'
@@ -22,6 +23,39 @@ const Tooltip: FC<TooltipProps> = ({
hideArrow,
}) => {
const [open, setOpen] = useState(false)
const [isHoverPopup, {
setTrue: setHoverPopup,
setFalse: setNotHoverPopup,
}] = useBoolean(false)
const isHoverPopupRef = useRef(isHoverPopup)
useEffect(() => {
isHoverPopupRef.current = isHoverPopup
}, [isHoverPopup])
const [isHoverTrigger, {
setTrue: setHoverTrigger,
setFalse: setNotHoverTrigger,
}] = useBoolean(false)
const isHoverTriggerRef = useRef(isHoverTrigger)
useEffect(() => {
isHoverTriggerRef.current = isHoverTrigger
}, [isHoverTrigger])
const handleLeave = (isTrigger: boolean) => {
if (isTrigger)
setNotHoverTrigger()
else
setNotHoverPopup()
// give time to move to the popup
setTimeout(() => {
if (!isHoverPopupRef.current && !isHoverTriggerRef.current)
setOpen(false)
}, 500)
}
return (
<PortalToFollowElem
@@ -32,15 +66,24 @@ const Tooltip: FC<TooltipProps> = ({
>
<PortalToFollowElemTrigger
onClick={() => triggerMethod === 'click' && setOpen(v => !v)}
onMouseEnter={() => triggerMethod === 'hover' && setOpen(true)}
onMouseLeave={() => triggerMethod === 'hover' && setOpen(false)}
onMouseEnter={() => {
if (triggerMethod === 'hover') {
setHoverTrigger()
setOpen(true)
}
}}
onMouseLeave={() => triggerMethod === 'hover' && handleLeave(true)}
>
{children}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent
className="z-[9999]"
>
<div className='relative px-3 py-2 text-xs font-normal text-gray-700 bg-white rounded-md shadow-lg'>
<div
className='relative px-3 py-2 text-xs font-normal text-gray-700 bg-white rounded-md shadow-lg'
onMouseEnter={() => triggerMethod === 'hover' && setHoverPopup()}
onMouseLeave={() => triggerMethod === 'hover' && handleLeave(false)}
>
{popupContent}
{!hideArrow && arrow}
</div>

View File

@@ -6,30 +6,24 @@ import { useTranslation } from 'react-i18next'
import type { Props as EditorProps } from '.'
import Editor from '.'
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
import type { Variable } from '@/app/components/workflow/types'
import type { NodeOutPutVar, Variable } from '@/app/components/workflow/types'
const TO_WINDOW_OFFSET = 8
type Props = {
nodeId: string
availableVars: NodeOutPutVar[]
varList: Variable[]
onAddVar: (payload: Variable) => void
onAddVar?: (payload: Variable) => void
} & EditorProps
const CodeEditor: FC<Props> = ({
nodeId,
availableVars,
varList,
onAddVar,
...editorProps
}) => {
const { t } = useTranslation()
const { availableVars } = useAvailableVarList(nodeId, {
onlyLeafNodeVar: false,
filterVar: () => true,
})
const isLeftBraceRef = useRef(false)
const editorRef = useRef(null)
@@ -76,7 +70,8 @@ const CodeEditor: FC<Props> = ({
if (popupPosition.y + height > window.innerHeight - TO_WINDOW_OFFSET)
newPopupPosition.y = window.innerHeight - height - TO_WINDOW_OFFSET
setPopupPosition(newPopupPosition)
if (newPopupPosition.x !== popupPosition.x || newPopupPosition.y !== popupPosition.y)
setPopupPosition(newPopupPosition)
}
}, [isShowVarPicker, popupPosition])
@@ -124,7 +119,7 @@ const CodeEditor: FC<Props> = ({
value_selector: varValue,
}
onAddVar(newVar)
onAddVar?.(newVar)
}
const editor: any = editorRef.current
const monaco: any = monacoRef.current

View File

@@ -14,7 +14,7 @@ export type Props = {
value?: string | object
placeholder?: string
onChange?: (value: string) => void
title: JSX.Element
title?: JSX.Element
language: CodeLanguage
headerRight?: JSX.Element
readOnly?: boolean
@@ -22,6 +22,7 @@ export type Props = {
height?: number
isInNode?: boolean
onMount?: (editor: any, monaco: any) => void
noWrapper?: boolean
}
const languageMap = {
@@ -30,11 +31,20 @@ const languageMap = {
[CodeLanguage.json]: 'json',
}
const DEFAULT_THEME = {
base: 'vs',
inherit: true,
rules: [],
colors: {
'editor.background': '#F2F4F7', // #00000000 transparent. But it will has a blue border
},
}
const CodeEditor: FC<Props> = ({
value = '',
placeholder = '',
onChange = () => { },
title,
title = '',
headerRight,
language,
readOnly,
@@ -42,9 +52,11 @@ const CodeEditor: FC<Props> = ({
height,
isInNode,
onMount,
noWrapper,
}) => {
const [isFocus, setIsFocus] = React.useState(false)
const [isMounted, setIsMounted] = React.useState(false)
const minHeight = height || 200
const handleEditorChange = (value: string | undefined) => {
onChange(value || '')
}
@@ -60,6 +72,8 @@ const CodeEditor: FC<Props> = ({
setIsFocus(false)
})
monaco.editor.defineTheme('default-theme', DEFAULT_THEME)
monaco.editor.defineTheme('blur-theme', {
base: 'vs',
inherit: true,
@@ -79,6 +93,7 @@ const CodeEditor: FC<Props> = ({
})
onMount?.(editor, monaco)
setIsMounted(true)
}
const outPutValue = (() => {
@@ -92,43 +107,61 @@ const CodeEditor: FC<Props> = ({
}
})()
const theme = (() => {
if (noWrapper)
return 'default-theme'
return isFocus ? 'focus-theme' : 'blur-theme'
})()
const main = (
<>
{/* https://www.npmjs.com/package/@monaco-editor/react */}
<Editor
className='h-full'
// language={language === CodeLanguage.javascript ? 'javascript' : 'python'}
language={languageMap[language] || 'javascript'}
theme={isMounted ? theme : 'default-theme'} // sometimes not load the default theme
value={outPutValue}
onChange={handleEditorChange}
// https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor.IEditorOptions.html
options={{
readOnly,
domReadOnly: true,
quickSuggestions: false,
minimap: { enabled: false },
lineNumbersMinChars: 1, // would change line num width
wordWrap: 'on', // auto line wrap
// lineNumbers: (num) => {
// return <div>{num}</div>
// }
}}
onMount={handleEditorDidMount}
/>
{!outPutValue && <div className='pointer-events-none absolute left-[36px] top-0 leading-[18px] text-[13px] font-normal text-gray-300'>{placeholder}</div>}
</>
)
return (
<div>
<Base
className='relative'
title={title}
value={outPutValue}
headerRight={headerRight}
isFocus={isFocus && !readOnly}
minHeight={height || 200}
isInNode={isInNode}
>
<>
{/* https://www.npmjs.com/package/@monaco-editor/react */}
<Editor
className='h-full'
// language={language === CodeLanguage.javascript ? 'javascript' : 'python'}
language={languageMap[language] || 'javascript'}
theme={isFocus ? 'focus-theme' : 'blur-theme'}
{noWrapper
? <div className='relative no-wrapper' style={{
// minHeight,
height: minHeight,
}}>{main}</div>
: (
<Base
className='relative'
title={title}
value={outPutValue}
onChange={handleEditorChange}
// https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor.IEditorOptions.html
options={{
readOnly,
domReadOnly: true,
quickSuggestions: false,
minimap: { enabled: false },
lineNumbersMinChars: 1, // would change line num width
wordWrap: 'on', // auto line wrap
// lineNumbers: (num) => {
// return <div>{num}</div>
// }
}}
onMount={handleEditorDidMount}
/>
{!outPutValue && <div className='pointer-events-none absolute left-[36px] top-0 leading-[18px] text-[13px] font-normal text-gray-300'>{placeholder}</div>}
</>
</Base>
headerRight={headerRight}
isFocus={isFocus && !readOnly}
minHeight={minHeight}
isInNode={isInNode}
>
{main}
</Base>
)}
</div>
)
}

View File

@@ -2,6 +2,15 @@
padding-left: 10px;
}
.no-wrapper .margin-view-overlays {
padding-left: 0;
/* width: 23px !important; */
}
/* .no-wrapper .margin {
width: 23px !important;
} */
/* hide readonly tooltip */
.monaco-editor-overlaymessage {
display: none !important;

View File

@@ -1,16 +1,19 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import React, { useCallback, useRef } from 'react'
import cn from 'classnames'
import copy from 'copy-to-clipboard'
import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks'
import {
BlockEnum,
type Node,
type NodeOutPutVar,
import { BlockEnum, EditionType } from '../../../../types'
import type {
Node,
NodeOutPutVar,
Variable,
} from '../../../../types'
import Wrap from '../editor/wrap'
import { CodeLanguage } from '../../../code/types'
import ToggleExpandBtn from '@/app/components/workflow/nodes/_base/components/toggle-expand-btn'
import useToggleExpend from '@/app/components/workflow/nodes/_base/hooks/use-toggle-expend'
import PromptEditor from '@/app/components/base/prompt-editor'
@@ -21,6 +24,10 @@ import { useEventEmitterContextContext } from '@/context/event-emitter'
import { PROMPT_EDITOR_INSERT_QUICKLY } from '@/app/components/base/prompt-editor/plugins/update-block'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
import TooltipPlus from '@/app/components/base/tooltip-plus'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor/editor-support-vars'
import Switch from '@/app/components/base/switch'
import { Jinja } from '@/app/components/base/icons/src/vender/workflow'
type Props = {
className?: string
headerClassName?: string
@@ -42,6 +49,12 @@ type Props = {
}
nodesOutputVars?: NodeOutPutVar[]
availableNodes?: Node[]
// for jinja
isSupportJinja?: boolean
editionType?: EditionType
onEditionTypeChange?: (editionType: EditionType) => void
varList?: Variable[]
handleAddVariable?: (payload: any) => void
}
const Editor: FC<Props> = ({
@@ -61,6 +74,11 @@ const Editor: FC<Props> = ({
hasSetBlockStatus,
nodesOutputVars,
availableNodes = [],
isSupportJinja,
editionType,
onEditionTypeChange,
varList = [],
handleAddVariable,
}) => {
const { t } = useTranslation()
const { eventEmitter } = useEventEmitterContextContext()
@@ -85,20 +103,6 @@ const Editor: FC<Props> = ({
setTrue: setFocus,
setFalse: setBlur,
}] = useBoolean(false)
const hideTooltipRunId = useRef(0)
const [isShowInsertToolTip, setIsShowInsertTooltip] = useState(false)
useEffect(() => {
if (isFocus) {
clearTimeout(hideTooltipRunId.current)
setIsShowInsertTooltip(true)
}
else {
hideTooltipRunId.current = setTimeout(() => {
setIsShowInsertTooltip(false)
}, 100) as any
}
}, [isFocus])
const handleInsertVariable = () => {
setFocus()
@@ -116,6 +120,29 @@ const Editor: FC<Props> = ({
<div className='w-px h-3 ml-2 mr-2 bg-gray-200'></div>
{/* Operations */}
<div className='flex items-center space-x-2'>
{isSupportJinja && (
<TooltipPlus
popupContent={
<div>
<div>{t('workflow.common.enableJinja')}</div>
<a className='text-[#155EEF]' target='_blank' href='https://jinja.palletsprojects.com/en/2.10.x/'>{t('workflow.common.learnMore')}</a>
</div>
}
hideArrow
>
<div className={cn(editionType === EditionType.jinja2 && 'border-black/5 bg-white', 'mb-1 flex h-[22px] items-center px-1.5 rounded-[5px] border border-transparent hover:border-black/5 space-x-0.5')}>
<Jinja className='w-6 h-3 text-gray-300' />
<Switch
size='sm'
defaultValue={editionType === EditionType.jinja2}
onChange={(checked) => {
onEditionTypeChange?.(checked ? EditionType.jinja2 : EditionType.basic)
}}
/>
</div>
</TooltipPlus>
)}
{!readOnly && (
<TooltipPlus
popupContent={`${t('workflow.common.insertVarTip')}`}
@@ -142,57 +169,74 @@ const Editor: FC<Props> = ({
{/* Min: 80 Max: 560. Header: 24 */}
<div className={cn('pb-2', isExpand && 'flex flex-col grow')}>
<div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative px-3 min-h-[56px] overflow-y-auto')}>
<PromptEditor
instanceId={instanceId}
compact
className='min-h-[56px]'
style={isExpand ? { height: editorExpandHeight - 5 } : {}}
value={value}
contextBlock={{
show: justVar ? false : isShowContext,
selectable: !hasSetBlockStatus?.context,
canNotAddContext: true,
}}
historyBlock={{
show: justVar ? false : isShowHistory,
selectable: !hasSetBlockStatus?.history,
history: {
user: 'Human',
assistant: 'Assistant',
},
}}
queryBlock={{
show: false, // use [sys.query] instead of query block
selectable: false,
}}
workflowVariableBlock={{
show: true,
variables: nodesOutputVars || [],
workflowNodesMap: availableNodes.reduce((acc, node) => {
acc[node.id] = {
title: node.data.title,
type: node.data.type,
}
if (node.data.type === BlockEnum.Start) {
acc.sys = {
title: t('workflow.blocks.start'),
type: BlockEnum.Start,
}
}
return acc
}, {} as any),
}}
onChange={onChange}
onBlur={setBlur}
onFocus={setFocus}
editable={!readOnly}
/>
{/* to patch Editor not support dynamic change editable status */}
{readOnly && <div className='absolute inset-0 z-10'></div>}
</div>
{!(isSupportJinja && editionType === EditionType.jinja2)
? (
<div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative px-3 min-h-[56px] overflow-y-auto')}>
<PromptEditor
instanceId={instanceId}
compact
className='min-h-[56px]'
style={isExpand ? { height: editorExpandHeight - 5 } : {}}
value={value}
contextBlock={{
show: justVar ? false : isShowContext,
selectable: !hasSetBlockStatus?.context,
canNotAddContext: true,
}}
historyBlock={{
show: justVar ? false : isShowHistory,
selectable: !hasSetBlockStatus?.history,
history: {
user: 'Human',
assistant: 'Assistant',
},
}}
queryBlock={{
show: false, // use [sys.query] instead of query block
selectable: false,
}}
workflowVariableBlock={{
show: true,
variables: nodesOutputVars || [],
workflowNodesMap: availableNodes.reduce((acc, node) => {
acc[node.id] = {
title: node.data.title,
type: node.data.type,
}
if (node.data.type === BlockEnum.Start) {
acc.sys = {
title: t('workflow.blocks.start'),
type: BlockEnum.Start,
}
}
return acc
}, {} as any),
}}
onChange={onChange}
onBlur={setBlur}
onFocus={setFocus}
editable={!readOnly}
/>
{/* to patch Editor not support dynamic change editable status */}
{readOnly && <div className='absolute inset-0 z-10'></div>}
</div>
)
: (
<div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative px-3 min-h-[56px] overflow-y-auto')}>
<CodeEditor
availableVars={nodesOutputVars || []}
varList={varList}
onAddVar={handleAddVariable}
isInNode
readOnly={readOnly}
language={CodeLanguage.python3}
value={value}
onChange={onChange}
noWrapper
/>
</div>
)}
</div>
</div>
</div>
</Wrap>

View File

@@ -3,7 +3,8 @@ import type { FC } from 'react'
import React, { useEffect, useState } from 'react'
import { uniqueId } from 'lodash-es'
import { useTranslation } from 'react-i18next'
import type { PromptItem } from '../../../types'
import type { PromptItem, Variable } from '../../../types'
import { EditionType } from '../../../types'
import Editor from '@/app/components/workflow/nodes/_base/components/prompt/editor'
import TypeSelector from '@/app/components/workflow/nodes/_base/components/selector'
import TooltipPlus from '@/app/components/base/tooltip-plus'
@@ -24,6 +25,7 @@ type Props = {
payload: PromptItem
handleChatModeMessageRoleChange: (role: PromptRole) => void
onPromptChange: (p: string) => void
onEditionTypeChange: (editionType: EditionType) => void
onRemove: () => void
isShowContext: boolean
hasSetBlockStatus: {
@@ -33,6 +35,8 @@ type Props = {
}
availableVars: any
availableNodes: any
varList: Variable[]
handleAddVariable: (payload: any) => void
}
const roleOptions = [
@@ -64,17 +68,21 @@ const ConfigPromptItem: FC<Props> = ({
isChatApp,
payload,
onPromptChange,
onEditionTypeChange,
onRemove,
isShowContext,
hasSetBlockStatus,
availableVars,
availableNodes,
varList,
handleAddVariable,
}) => {
const { t } = useTranslation()
const [instanceId, setInstanceId] = useState(uniqueId())
useEffect(() => {
setInstanceId(`${id}-${uniqueId()}`)
}, [id])
return (
<Editor
className={className}
@@ -107,7 +115,7 @@ const ConfigPromptItem: FC<Props> = ({
</TooltipPlus>
</div>
}
value={payload.text}
value={payload.edition_type === EditionType.jinja2 ? (payload.jinja2_text || '') : payload.text}
onChange={onPromptChange}
readOnly={readOnly}
showRemove={canRemove}
@@ -118,6 +126,11 @@ const ConfigPromptItem: FC<Props> = ({
hasSetBlockStatus={hasSetBlockStatus}
nodesOutputVars={availableVars}
availableNodes={availableNodes}
isSupportJinja
editionType={payload.edition_type}
onEditionTypeChange={onEditionTypeChange}
varList={varList}
handleAddVariable={handleAddVariable}
/>
)
}

View File

@@ -6,8 +6,8 @@ import produce from 'immer'
import { ReactSortable } from 'react-sortablejs'
import { v4 as uuid4 } from 'uuid'
import cn from 'classnames'
import type { PromptItem, ValueSelector, Var } from '../../../types'
import { PromptRole } from '../../../types'
import type { PromptItem, ValueSelector, Var, Variable } from '../../../types'
import { EditionType, PromptRole } from '../../../types'
import useAvailableVarList from '../../_base/hooks/use-available-var-list'
import ConfigPromptItem from './config-prompt-item'
import Editor from '@/app/components/workflow/nodes/_base/components/prompt/editor'
@@ -30,6 +30,8 @@ type Props = {
history: boolean
query: boolean
}
varList?: Variable[]
handleAddVariable: (payload: any) => void
}
const ConfigPrompt: FC<Props> = ({
@@ -42,10 +44,12 @@ const ConfigPrompt: FC<Props> = ({
onChange,
isShowContext,
hasSetBlockStatus,
varList = [],
handleAddVariable,
}) => {
const { t } = useTranslation()
const payloadWithIds = (isChatModel && Array.isArray(payload))
? payload.map((item, i) => {
? payload.map((item) => {
const id = uuid4()
return {
id: item.id || id,
@@ -67,7 +71,16 @@ const ConfigPrompt: FC<Props> = ({
const handleChatModePromptChange = useCallback((index: number) => {
return (prompt: string) => {
const newPrompt = produce(payload as PromptItem[], (draft) => {
draft[index].text = prompt
draft[index][draft[index].edition_type === EditionType.jinja2 ? 'jinja2_text' : 'text'] = prompt
})
onChange(newPrompt)
}
}, [onChange, payload])
const handleChatModeEditionTypeChange = useCallback((index: number) => {
return (editionType: EditionType) => {
const newPrompt = produce(payload as PromptItem[], (draft) => {
draft[index].edition_type = editionType
})
onChange(newPrompt)
}
@@ -106,7 +119,14 @@ const ConfigPrompt: FC<Props> = ({
const handleCompletionPromptChange = useCallback((prompt: string) => {
const newPrompt = produce(payload as PromptItem, (draft) => {
draft.text = prompt
draft[draft.edition_type === EditionType.jinja2 ? 'jinja2_text' : 'text'] = prompt
})
onChange(newPrompt)
}, [onChange, payload])
const handleCompletionEditionTypeChange = useCallback((editionType: EditionType) => {
const newPrompt = produce(payload as PromptItem, (draft) => {
draft.edition_type = editionType
})
onChange(newPrompt)
}, [onChange, payload])
@@ -161,11 +181,14 @@ const ConfigPrompt: FC<Props> = ({
isChatApp={isChatApp}
payload={item}
onPromptChange={handleChatModePromptChange(index)}
onEditionTypeChange={handleChatModeEditionTypeChange(index)}
onRemove={handleRemove(index)}
isShowContext={isShowContext}
hasSetBlockStatus={hasSetBlockStatus}
availableVars={availableVars}
availableNodes={availableNodes}
varList={varList}
handleAddVariable={handleAddVariable}
/>
</div>
@@ -187,7 +210,7 @@ const ConfigPrompt: FC<Props> = ({
<Editor
instanceId={`${nodeId}-chat-workflow-llm-prompt-editor`}
title={<span className='capitalize'>{t(`${i18nPrefix}.prompt`)}</span>}
value={(payload as PromptItem).text}
value={(payload as PromptItem).edition_type === EditionType.basic ? (payload as PromptItem).text : ((payload as PromptItem).jinja2_text || '')}
onChange={handleCompletionPromptChange}
readOnly={readOnly}
isChatModel={isChatModel}
@@ -196,6 +219,11 @@ const ConfigPrompt: FC<Props> = ({
hasSetBlockStatus={hasSetBlockStatus}
nodesOutputVars={availableVars}
availableNodes={availableNodes}
isSupportJinja
editionType={(payload as PromptItem).edition_type}
varList={varList}
onEditionTypeChange={handleCompletionEditionTypeChange}
handleAddVariable={handleAddVariable}
/>
</div>
)}

View File

@@ -1,7 +1,6 @@
import { BlockEnum } from '../../types'
import { type NodeDefault, PromptRole } from '../../types'
import { BlockEnum, EditionType } from '../../types'
import { type NodeDefault, type PromptItem, PromptRole } from '../../types'
import type { LLMNodeType } from './types'
import type { PromptItem } from '@/models/debug'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
const i18nPrefix = 'workflow.errorMsg'
@@ -16,7 +15,6 @@ const nodeDefault: NodeDefault<LLMNodeType> = {
temperature: 0.7,
},
},
variables: [],
prompt_template: [{
role: PromptRole.system,
text: '',
@@ -57,6 +55,23 @@ const nodeDefault: NodeDefault<LLMNodeType> = {
if (isChatModel && !!payload.memory.query_prompt_template && !payload.memory.query_prompt_template.includes('{{#sys.query#}}'))
errorMessages = t('workflow.nodes.llm.sysQueryInUser')
}
if (!errorMessages) {
const isChatModel = payload.model.mode === 'chat'
const isShowVars = (() => {
if (isChatModel)
return (payload.prompt_template as PromptItem[]).some(item => item.edition_type === EditionType.jinja2)
return (payload.prompt_template as PromptItem).edition_type === EditionType.jinja2
})()
if (isShowVars && payload.prompt_config?.jinja2_variables) {
payload.prompt_config?.jinja2_variables.forEach((i) => {
if (!errorMessages && !i.variable)
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variable`) })
if (!errorMessages && !i.value_selector.length)
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variableValue`) })
})
}
}
return {
isValid: !errorMessages,
errorMessage: errorMessages,

View File

@@ -7,6 +7,8 @@ import useConfig from './use-config'
import ResolutionPicker from './components/resolution-picker'
import type { LLMNodeType } from './types'
import ConfigPrompt from './components/config-prompt'
import VarList from '@/app/components/workflow/nodes/_base/components/variable/var-list'
import AddButton2 from '@/app/components/base/button/add-button'
import Field from '@/app/components/workflow/nodes/_base/components/field'
import Split from '@/app/components/workflow/nodes/_base/components/split'
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
@@ -44,7 +46,12 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
filterVar,
availableVars,
availableNodes,
isShowVars,
handlePromptChange,
handleAddEmptyVariable,
handleAddVariable,
handleVarListChange,
handleVarNameChange,
handleSyeQueryChange,
handleMemoryChange,
handleVisionResolutionEnabledChange,
@@ -169,9 +176,29 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
payload={inputs.prompt_template}
onChange={handlePromptChange}
hasSetBlockStatus={hasSetBlockStatus}
varList={inputs.prompt_config?.jinja2_variables || []}
handleAddVariable={handleAddVariable}
/>
)}
{isShowVars && (
<Field
title={t('workflow.nodes.templateTransform.inputVars')}
operations={
!readOnly ? <AddButton2 onClick={handleAddEmptyVariable} /> : undefined
}
>
<VarList
nodeId={id}
readonly={readOnly}
list={inputs.prompt_config?.jinja2_variables || []}
onChange={handleVarListChange}
onVarNameChange={handleVarNameChange}
filterVar={filterVar}
/>
</Field>
)}
{/* Memory put place examples. */}
{isChatMode && isChatModel && !!inputs.memory && (
<div className='mt-4'>

View File

@@ -3,8 +3,10 @@ import type { CommonNodeType, Memory, ModelConfig, PromptItem, ValueSelector, Va
export type LLMNodeType = CommonNodeType & {
model: ModelConfig
variables: Variable[]
prompt_template: PromptItem[] | PromptItem
prompt_config?: {
jinja2_variables?: Variable[]
}
memory?: Memory
context: {
enabled: boolean

View File

@@ -1,8 +1,7 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import produce from 'immer'
import useVarList from '../_base/hooks/use-var-list'
import { VarType } from '../../types'
import type { Memory, ValueSelector, Var } from '../../types'
import { EditionType, VarType } from '../../types'
import type { Memory, PromptItem, ValueSelector, Var, Variable } from '../../types'
import { useStore } from '../../store'
import {
useIsChatMode,
@@ -18,7 +17,6 @@ import {
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
import useOneStepRun from '@/app/components/workflow/nodes/_base/hooks/use-one-step-run'
import type { PromptItem } from '@/models/debug'
import { RETRIEVAL_OUTPUT_STRUCT } from '@/app/components/workflow/constants'
import { checkHasContextBlock, checkHasHistoryBlock, checkHasQueryBlock } from '@/app/components/base/prompt-editor/constants'
@@ -29,20 +27,21 @@ const useConfig = (id: string, payload: LLMNodeType) => {
const defaultConfig = useStore(s => s.nodesDefaultConfigs)[payload.type]
const [defaultRolePrefix, setDefaultRolePrefix] = useState<{ user: string; assistant: string }>({ user: '', assistant: '' })
const { inputs, setInputs: doSetInputs } = useNodeCrud<LLMNodeType>(id, payload)
const inputRef = useRef(inputs)
const setInputs = useCallback((newInputs: LLMNodeType) => {
if (newInputs.memory && !newInputs.memory.role_prefix) {
const newPayload = produce(newInputs, (draft) => {
draft.memory!.role_prefix = defaultRolePrefix
})
doSetInputs(newPayload)
inputRef.current = newPayload
return
}
doSetInputs(newInputs)
inputRef.current = newInputs
}, [doSetInputs, defaultRolePrefix])
const inputRef = useRef(inputs)
useEffect(() => {
inputRef.current = inputs
}, [inputs])
// model
const model = inputs.model
const modelMode = inputs.model?.mode
@@ -178,11 +177,80 @@ const useConfig = (id: string, payload: LLMNodeType) => {
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isShowVisionConfig, modelChanged])
// variables
const { handleVarListChange, handleAddVariable } = useVarList<LLMNodeType>({
inputs,
setInputs,
})
const isShowVars = (() => {
if (isChatModel)
return (inputs.prompt_template as PromptItem[]).some(item => item.edition_type === EditionType.jinja2)
return (inputs.prompt_template as PromptItem).edition_type === EditionType.jinja2
})()
const handleAddEmptyVariable = useCallback(() => {
const newInputs = produce(inputRef.current, (draft) => {
if (!draft.prompt_config) {
draft.prompt_config = {
jinja2_variables: [],
}
}
if (!draft.prompt_config.jinja2_variables)
draft.prompt_config.jinja2_variables = []
draft.prompt_config.jinja2_variables.push({
variable: '',
value_selector: [],
})
})
setInputs(newInputs)
}, [setInputs])
const handleAddVariable = useCallback((payload: Variable) => {
const newInputs = produce(inputRef.current, (draft) => {
if (!draft.prompt_config) {
draft.prompt_config = {
jinja2_variables: [],
}
}
if (!draft.prompt_config.jinja2_variables)
draft.prompt_config.jinja2_variables = []
draft.prompt_config.jinja2_variables.push(payload)
})
setInputs(newInputs)
}, [setInputs])
const handleVarListChange = useCallback((newList: Variable[]) => {
const newInputs = produce(inputRef.current, (draft) => {
if (!draft.prompt_config) {
draft.prompt_config = {
jinja2_variables: [],
}
}
if (!draft.prompt_config.jinja2_variables)
draft.prompt_config.jinja2_variables = []
draft.prompt_config.jinja2_variables = newList
})
setInputs(newInputs)
}, [setInputs])
const handleVarNameChange = useCallback((oldName: string, newName: string) => {
const newInputs = produce(inputRef.current, (draft) => {
if (isChatModel) {
const promptTemplate = draft.prompt_template as PromptItem[]
promptTemplate.filter(item => item.edition_type === EditionType.jinja2).forEach((item) => {
item.jinja2_text = (item.jinja2_text || '').replaceAll(`{{ ${oldName} }}`, `{{ ${newName} }}`)
})
}
else {
if ((draft.prompt_template as PromptItem).edition_type !== EditionType.jinja2)
return
const promptTemplate = draft.prompt_template as PromptItem
promptTemplate.jinja2_text = (promptTemplate.jinja2_text || '').replaceAll(`{{ ${oldName} }}`, `{{ ${newName} }}`)
}
})
setInputs(newInputs)
}, [isChatModel, setInputs])
// context
const handleContextVarChange = useCallback((newVar: ValueSelector | string) => {
@@ -194,11 +262,11 @@ const useConfig = (id: string, payload: LLMNodeType) => {
}, [inputs, setInputs])
const handlePromptChange = useCallback((newPrompt: PromptItem[] | PromptItem) => {
const newInputs = produce(inputs, (draft) => {
const newInputs = produce(inputRef.current, (draft) => {
draft.prompt_template = newPrompt
})
setInputs(newInputs)
}, [inputs, setInputs])
}, [setInputs])
const handleMemoryChange = useCallback((newMemory?: Memory) => {
const newInputs = produce(inputs, (draft) => {
@@ -286,6 +354,7 @@ const useConfig = (id: string, payload: LLMNodeType) => {
runInputData,
setRunInputData,
runResult,
toVarInputs,
} = useOneStepRun<LLMNodeType>({
id,
data: inputs,
@@ -295,23 +364,6 @@ const useConfig = (id: string, payload: LLMNodeType) => {
},
})
// const handleRun = (submitData: Record<string, any>) => {
// console.log(submitData)
// const res = produce(submitData, (draft) => {
// debugger
// if (draft.contexts) {
// draft['#context#'] = draft.contexts
// delete draft.contexts
// }
// if (draft.visionFiles) {
// draft['#files#'] = draft.visionFiles
// delete draft.visionFiles
// }
// })
// doHandleRun(res)
// }
const inputVarValues = (() => {
const vars: Record<string, any> = {}
Object.keys(runInputData)
@@ -348,7 +400,7 @@ const useConfig = (id: string, payload: LLMNodeType) => {
}, [runInputData, setRunInputData])
const allVarStrArr = (() => {
const arr = isChatModel ? (inputs.prompt_template as PromptItem[]).map(item => item.text) : [(inputs.prompt_template as PromptItem).text]
const arr = isChatModel ? (inputs.prompt_template as PromptItem[]).filter(item => item.edition_type !== EditionType.jinja2).map(item => item.text) : [(inputs.prompt_template as PromptItem).text]
if (isChatMode && isChatModel && !!inputs.memory) {
arr.push('{{#sys.query#}}')
arr.push(inputs.memory.query_prompt_template)
@@ -357,7 +409,13 @@ const useConfig = (id: string, payload: LLMNodeType) => {
return arr
})()
const varInputs = getInputVars(allVarStrArr)
const varInputs = (() => {
const vars = getInputVars(allVarStrArr)
if (isShowVars)
return [...vars, ...toVarInputs(inputs.prompt_config?.jinja2_variables || [])]
return vars
})()
return {
readOnly,
@@ -370,8 +428,11 @@ const useConfig = (id: string, payload: LLMNodeType) => {
isShowVisionConfig,
handleModelChanged,
handleCompletionParamsChange,
isShowVars,
handleVarListChange,
handleVarNameChange,
handleAddVariable,
handleAddEmptyVariable,
handleContextVarChange,
filterInputVar,
filterVar,

View File

@@ -26,6 +26,7 @@ const Panel: FC<NodePanelProps<TemplateTransformNodeType>> = ({
const {
readOnly,
inputs,
availableVars,
handleVarListChange,
handleVarNameChange,
handleAddVariable,
@@ -65,7 +66,7 @@ const Panel: FC<NodePanelProps<TemplateTransformNodeType>> = ({
</Field>
<Split />
<CodeEditor
nodeId={id}
availableVars={availableVars}
varList={inputs.variables}
onAddVar={handleAddVariable}
isInNode

View File

@@ -10,6 +10,7 @@ import useOneStepRun from '@/app/components/workflow/nodes/_base/hooks/use-one-s
import {
useNodesReadOnly,
} from '@/app/components/workflow/hooks'
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
const useConfig = (id: string, payload: TemplateTransformNodeType) => {
const { nodesReadOnly: readOnly } = useNodesReadOnly()
@@ -22,6 +23,11 @@ const useConfig = (id: string, payload: TemplateTransformNodeType) => {
inputsRef.current = newPayload
}, [doSetInputs])
const { availableVars } = useAvailableVarList(id, {
onlyLeafNodeVar: false,
filterVar: () => true,
})
const { handleAddVariable: handleAddEmptyVariable } = useVarList<TemplateTransformNodeType>({
inputs,
setInputs,
@@ -108,6 +114,7 @@ const useConfig = (id: string, payload: TemplateTransformNodeType) => {
return {
readOnly,
inputs,
availableVars,
handleVarListChange,
handleVarNameChange,
handleAddVariable,

View File

@@ -128,10 +128,17 @@ export enum PromptRole {
assistant = 'assistant',
}
export enum EditionType {
basic = 'basic',
jinja2 = 'jinja2',
}
export type PromptItem = {
id?: string
role?: PromptRole
text: string
edition_type?: EditionType
jinja2_text?: string
}
export enum MemoryRole {

View File

@@ -52,6 +52,8 @@ const translation = {
jinjaEditorPlaceholder: 'Type \'/\' or \'{\' to insert variable',
viewOnly: 'View Only',
showRunHistory: 'Show Run History',
enableJinja: 'Enable Jinja template support',
learnMore: 'Learn More',
},
errorMsg: {
fieldRequired: '{{field}} is required',

View File

@@ -52,6 +52,8 @@ const translation = {
jinjaEditorPlaceholder: '输入 “/” 或 “{” 插入变量',
viewOnly: '只读',
showRunHistory: '显示运行历史',
enableJinja: '开启支持 Jinja 模板',
learnMore: '了解更多',
},
errorMsg: {
fieldRequired: '{{field}} 不能为空',