mirror of
https://github.com/langgenius/dify.git
synced 2026-06-10 18:24:09 +08:00
feat(web): gate /create and /refine slash commands behind feature preview flag (#37094)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a88c15c906
commit
db1aa683bc
@ -12,6 +12,7 @@ examples accurate or the LLM will invent fields.
|
||||
"""
|
||||
|
||||
import json
|
||||
from collections.abc import Iterable
|
||||
from typing import Any
|
||||
|
||||
# Per-node-type configuration cheatsheet.
|
||||
@ -22,11 +23,24 @@ from typing import Any
|
||||
# both ``WorkflowService.sync_draft_workflow``'s structural checks and the
|
||||
# runtime entity validation each node performs when the workflow runs.
|
||||
#
|
||||
# The cheatsheet is assembled DYNAMICALLY per request: the planner decides
|
||||
# which node types the workflow needs, and ``build_node_config_cheatsheet``
|
||||
# stitches together only the snippets for those types (plus the always-needed
|
||||
# wrapper / shared-field / edge-handle preamble, and the containers section
|
||||
# when an iteration / loop is planned). This keeps the builder prompt tight —
|
||||
# a 3-node summariser no longer carries the schema for 12 unrelated node
|
||||
# types — and lets each snippet document its FULL schema (e.g. a "file" start
|
||||
# variable's required ``allowed_file_types``) without bloating every prompt.
|
||||
#
|
||||
# The postprocessor in ``runner.py`` fills missing wrapper fields (``type``,
|
||||
# ``positionAbsolute``, ``width``, ``height``, ``sourcePosition`` /
|
||||
# ``targetPosition``, edge ``data.sourceType`` / ``data.targetType``), so the
|
||||
# LLM only needs to emit semantically meaningful fields.
|
||||
NODE_CONFIG_CHEATSHEET = """\
|
||||
|
||||
# Always-included preamble: the node/edge wrapper shape and the shared
|
||||
# ``data`` fields that apply to every node type, plus the "## Per type" header
|
||||
# the per-type snippets slot under.
|
||||
_CHEATSHEET_PREAMBLE = """\
|
||||
## Node wrapper (every node, top-level)
|
||||
|
||||
{"id": "node1" (digits + letters only — see "Node IDs" below),
|
||||
@ -46,14 +60,26 @@ Children of iteration / loop containers additionally need
|
||||
"desc": "<one-liner>",
|
||||
"selected": false}
|
||||
|
||||
## Per type — additional "data" fields
|
||||
## Per type — additional "data" fields (only the node types in your plan are shown)"""
|
||||
|
||||
|
||||
# node_type → its per-type schema snippet. Keyed by the exact ``node_type``
|
||||
# string the planner emits so ``build_node_config_cheatsheet`` can look each
|
||||
# one up directly. Iteration / loop are documented in the Containers section
|
||||
# (they are subgraphs, not leaf nodes) rather than here.
|
||||
_NODE_SNIPPETS: dict[str, str] = {
|
||||
"start": """\
|
||||
- start:
|
||||
{"variables": [
|
||||
{"variable": "url", "label": "URL", "type": "text-input",
|
||||
"required": true, "max_length": 256, "options": []},
|
||||
{"variable": "topic", "label": "Topic", "type": "paragraph",
|
||||
"required": false, "max_length": 4096, "options": []}
|
||||
"required": false, "max_length": 4096, "options": []},
|
||||
{"variable": "doc", "label": "Document", "type": "file",
|
||||
"required": true,
|
||||
"allowed_file_types": ["document"],
|
||||
"allowed_file_upload_methods": ["local_file", "remote_url"],
|
||||
"allowed_file_extensions": []}
|
||||
]}
|
||||
EVERY user-supplied value referenced by a downstream node
|
||||
(``{{#node-id.var#}}`` in a prompt / answer / template, or
|
||||
@ -62,19 +88,29 @@ Children of iteration / loop containers additionally need
|
||||
If the planner's ``start_inputs`` list is non-empty, use it verbatim
|
||||
(the user prompt section "Start inputs" surfaces it). Types:
|
||||
text-input | paragraph | select | number | file | file-list.
|
||||
For a "file" or "file-list" variable you MUST also set
|
||||
``allowed_file_types`` to a NON-EMPTY subset of
|
||||
["document", "image", "audio", "video", "custom"] — it is a REQUIRED
|
||||
field and the draft fails to load (showing "supported file types is
|
||||
required") without it. Choose by purpose: ["document"] for text
|
||||
extraction (PDF / Word / PPT / Markdown / …), ["image"] for vision,
|
||||
etc. Always set ``allowed_file_upload_methods`` to
|
||||
["local_file", "remote_url"]. Only when you include "custom" must you
|
||||
also set ``allowed_file_extensions`` to a non-empty list like
|
||||
[".epub", ".rtf"]; otherwise leave it [].
|
||||
In Advanced-Chat mode ``sys.query`` and ``sys.files`` are automatic
|
||||
system variables — downstream nodes may reference them; do NOT add
|
||||
them to ``variables``.
|
||||
|
||||
them to ``variables``.""",
|
||||
"end": """\
|
||||
- end (Workflow mode only):
|
||||
{"outputs": [
|
||||
{"variable": "result", "value_selector": ["<src-node-id>", "<out-var>"]}
|
||||
]}
|
||||
|
||||
]}""",
|
||||
"answer": """\
|
||||
- answer (Advanced Chat mode only):
|
||||
{"variables": [],
|
||||
"answer": "<text with {{#<src>.<var>#}} placeholders>"}
|
||||
|
||||
"answer": "<text with {{#<src>.<var>#}} placeholders>"}""",
|
||||
"llm": """\
|
||||
- llm:
|
||||
{"model": {"provider": "<provider>", "name": "<model>", "mode": "chat",
|
||||
"completion_params": {"temperature": 0.7}},
|
||||
@ -100,26 +136,26 @@ Children of iteration / loop containers additionally need
|
||||
values are the translations.
|
||||
Input: {{#node1.text#}}
|
||||
* Each placeholder only resolves the variable from its source node —
|
||||
it cannot be a Jinja template or call a function.
|
||||
|
||||
it cannot be a Jinja template or call a function.""",
|
||||
"knowledge-retrieval": """\
|
||||
- knowledge-retrieval:
|
||||
{"query_variable_selector": ["<src>", "<var>"],
|
||||
"query_attachment_selector": [],
|
||||
"dataset_ids": [],
|
||||
"retrieval_mode": "multiple",
|
||||
"multiple_retrieval_config": {"top_k": 4, "score_threshold": null,
|
||||
"reranking_enable": false}}
|
||||
|
||||
"reranking_enable": false}}""",
|
||||
"code": """\
|
||||
- code (escape hatch — only if no installed tool fits):
|
||||
{"code_language": "python3",
|
||||
"code": "def main(arg1: str) -> dict:\\n return {'result': arg1}",
|
||||
"variables": [{"variable": "arg1", "value_selector": ["<src>", "<var>"]}],
|
||||
"outputs": {"result": {"type": "string", "children": null}}}
|
||||
|
||||
"outputs": {"result": {"type": "string", "children": null}}}""",
|
||||
"template-transform": """\
|
||||
- template-transform:
|
||||
{"template": "Hello {{ name }}",
|
||||
"variables": [{"variable": "name", "value_selector": ["<src>", "<var>"]}]}
|
||||
|
||||
"variables": [{"variable": "name", "value_selector": ["<src>", "<var>"]}]}""",
|
||||
"http-request": """\
|
||||
- http-request (escape hatch — only if no installed tool fits):
|
||||
{"variables": [], "method": "get", "url": "https://example.com",
|
||||
"authorization": {"type": "no-auth", "config": null},
|
||||
@ -129,8 +165,8 @@ Children of iteration / loop containers additionally need
|
||||
"timeout": {"max_connect_timeout": 0, "max_read_timeout": 0,
|
||||
"max_write_timeout": 0},
|
||||
"retry_config": {"retry_enabled": true, "max_retries": 3,
|
||||
"retry_interval": 100}}
|
||||
|
||||
"retry_interval": 100}}""",
|
||||
"tool": """\
|
||||
- tool (PREFERRED for external actions when listed in Available tools):
|
||||
{"provider_id": "<provider>", # provider portion of provider/tool
|
||||
"provider_type": "builtin", # exact value from catalogue
|
||||
@ -144,8 +180,8 @@ Children of iteration / loop containers additionally need
|
||||
Parameter ``type`` is one of:
|
||||
"mixed" — string template referencing variables ({{#...#}})
|
||||
"variable" — direct reference, value is ["<src>", "<var>"]
|
||||
"constant" — literal value
|
||||
|
||||
"constant" — literal value""",
|
||||
"if-else": """\
|
||||
- if-else:
|
||||
{"_targetBranches": [{"id": "true", "name": "IF"},
|
||||
{"id": "false", "name": "ELSE"}],
|
||||
@ -158,8 +194,8 @@ Children of iteration / loop containers additionally need
|
||||
"comparison_operator": "is",
|
||||
"value": "<value>"}]}
|
||||
]}
|
||||
Source handle for downstream edges = the case_id ("true" / "false").
|
||||
|
||||
Source handle for downstream edges = the case_id ("true" / "false").""",
|
||||
"question-classifier": """\
|
||||
- question-classifier:
|
||||
{"query_variable_selector": ["<src>", "<var>"],
|
||||
"model": {"provider": "<p>", "name": "<m>", "mode": "chat",
|
||||
@ -169,8 +205,8 @@ Children of iteration / loop containers additionally need
|
||||
"_targetBranches": [{"id": "1", "name": ""}, {"id": "2", "name": ""}],
|
||||
"vision": {"enabled": false},
|
||||
"instruction": ""}
|
||||
Source handle for downstream edges = the class_id ("1" / "2" / ...).
|
||||
|
||||
Source handle for downstream edges = the class_id ("1" / "2" / ...).""",
|
||||
"parameter-extractor": """\
|
||||
- parameter-extractor:
|
||||
{"query": [["<src>", "<var>"]], # array of value_selector arrays
|
||||
"model": {"provider": "<p>", "name": "<m>", "mode": "chat",
|
||||
@ -179,8 +215,8 @@ Children of iteration / loop containers additionally need
|
||||
"description": "<purpose>", "required": true}],
|
||||
"reasoning_mode": "prompt",
|
||||
"vision": {"enabled": false},
|
||||
"instruction": ""}
|
||||
|
||||
"instruction": ""}""",
|
||||
"document-extractor": """\
|
||||
- document-extractor:
|
||||
{"variable_selector": ["<src>", "<file-var>"], # a file / file-list input
|
||||
"is_array_file": false} # true when the input is a
|
||||
@ -188,8 +224,9 @@ Children of iteration / loop containers additionally need
|
||||
Single output variable ``text``: a string when ``is_array_file`` is false,
|
||||
an array of strings (one per file) when it is true. ``variable_selector``
|
||||
MUST point at a ``start`` variable declared with type "file" / "file-list"
|
||||
(or ``sys.files`` in Advanced-Chat mode).
|
||||
|
||||
(or ``sys.files`` in Advanced-Chat mode). That start variable MUST set a
|
||||
non-empty ``allowed_file_types`` (use ["document"] for document text).""",
|
||||
"variable-aggregator": """\
|
||||
- variable-aggregator (merge mutually-exclusive branches into one output):
|
||||
{"output_type": "string", # VarType of the merged value — one of
|
||||
# string | number | object | array[string] |
|
||||
@ -200,8 +237,8 @@ Children of iteration / loop containers additionally need
|
||||
Output variable: ``output`` (the first branch that actually ran). Place it
|
||||
after an ``if-else`` / ``question-classifier`` to rejoin paths before the
|
||||
``end`` / ``answer`` node. Each entry of ``variables`` is a value_selector
|
||||
array, NOT a placeholder string.
|
||||
|
||||
array, NOT a placeholder string.""",
|
||||
"list-operator": """\
|
||||
- list-operator (filter / sort / slice an array variable):
|
||||
{"variable": ["<src>", "<array-var>"],
|
||||
"filter_by": {"enabled": false, "conditions": []},
|
||||
@ -210,8 +247,12 @@ Children of iteration / loop containers additionally need
|
||||
"limit": {"enabled": false, "size": 10}}
|
||||
Enable only the sub-features you need; ``conditions`` reuse the if-else
|
||||
condition shape (key / comparison_operator / value). Outputs: ``result``
|
||||
(the processed array), ``first_record``, ``last_record``.
|
||||
(the processed array), ``first_record``, ``last_record``.""",
|
||||
}
|
||||
|
||||
|
||||
# Pulled into the cheatsheet only when an iteration / loop appears in the plan.
|
||||
_CONTAINERS_SECTION = """\
|
||||
## Containers — iteration / loop
|
||||
|
||||
These are SUBGRAPH nodes. To use one you MUST emit, in order:
|
||||
@ -270,16 +311,59 @@ These are SUBGRAPH nodes. To use one you MUST emit, in order:
|
||||
|
||||
5. The container's incoming/outgoing edges connect to the container's id
|
||||
(``nodeK``), NOT to inner nodes. The first inner edge connects from
|
||||
``nodeKstart``.
|
||||
``nodeKstart``."""
|
||||
|
||||
|
||||
# Always-included trailer: edge handle conventions for every graph.
|
||||
_EDGE_HANDLES_SECTION = """\
|
||||
## Edge handles
|
||||
|
||||
- Most nodes: sourceHandle "source", targetHandle "target".
|
||||
- if-else cases: sourceHandle is the case_id ("true" / "false" / ...).
|
||||
- question-classifier: sourceHandle is the class_id ("1" / "2" / ...).
|
||||
- iteration-start / sourceHandle "source"; the edge from the *start node
|
||||
loop-start: is what kicks off the first inner step.
|
||||
"""
|
||||
loop-start: is what kicks off the first inner step."""
|
||||
|
||||
|
||||
# Container node types are described in ``_CONTAINERS_SECTION`` rather than as
|
||||
# leaf snippets; their presence in a plan pulls that section in.
|
||||
_CONTAINER_NODE_TYPES = frozenset({"iteration", "loop"})
|
||||
|
||||
|
||||
def build_node_config_cheatsheet(node_types: Iterable[str] | None = None) -> str:
|
||||
"""
|
||||
Assemble the builder cheatsheet for exactly the node types in the plan.
|
||||
|
||||
``node_types`` is the set of ``node_type`` strings the planner chose. We
|
||||
emit the always-on preamble (wrapper / shared fields), then only the
|
||||
per-type snippets for the requested types (``start`` is always included —
|
||||
every graph has one), the Containers section when an iteration / loop is
|
||||
planned, and the edge-handles trailer. Unknown / unrecognised type strings
|
||||
are ignored (the runtime / structural validator catches genuinely bogus
|
||||
types).
|
||||
|
||||
``None`` returns the FULL cheatsheet (every snippet + containers) — used to
|
||||
build the static back-compat constants below and as a safe fallback.
|
||||
"""
|
||||
if node_types is None:
|
||||
requested: set[str] = set(_NODE_SNIPPETS) | set(_CONTAINER_NODE_TYPES)
|
||||
else:
|
||||
requested = {str(t).strip() for t in node_types if str(t).strip()}
|
||||
requested.add("start") # every workflow has exactly one start node
|
||||
|
||||
parts: list[str] = [_CHEATSHEET_PREAMBLE]
|
||||
# Iterate _NODE_SNIPPETS (not ``requested``) to keep a stable, readable order.
|
||||
parts.extend(snippet for node_type, snippet in _NODE_SNIPPETS.items() if node_type in requested)
|
||||
if requested & _CONTAINER_NODE_TYPES:
|
||||
parts.append(_CONTAINERS_SECTION)
|
||||
parts.append(_EDGE_HANDLES_SECTION)
|
||||
return "\n\n".join(parts) + "\n"
|
||||
|
||||
|
||||
# Full cheatsheet (all node types) — retained as a module constant so callers
|
||||
# and tests that want the complete reference can import it directly. The
|
||||
# dynamic per-request prompt is built by ``get_builder_system_prompt``.
|
||||
NODE_CONFIG_CHEATSHEET = build_node_config_cheatsheet()
|
||||
|
||||
|
||||
_BASE_SYSTEM_PROMPT_HEAD = """You are a Dify workflow builder.
|
||||
@ -402,21 +486,24 @@ _ADVANCED_CHAT_MODE_RULES = """# Mode-specific rules — Advanced Chat (Chatflow
|
||||
"""
|
||||
|
||||
|
||||
BUILDER_SYSTEM_PROMPT_WORKFLOW = (
|
||||
_BASE_SYSTEM_PROMPT_HEAD
|
||||
+ _WORKFLOW_MODE_RULES
|
||||
+ _BASE_SYSTEM_PROMPT_TAIL
|
||||
+ NODE_CONFIG_CHEATSHEET
|
||||
+ _BASE_SYSTEM_PROMPT_FOOTER
|
||||
)
|
||||
def _assemble_builder_system_prompt(mode: str, node_types: Iterable[str] | None) -> str:
|
||||
"""Stitch the builder system prompt for ``mode`` around a cheatsheet built
|
||||
for ``node_types`` (``None`` → full cheatsheet)."""
|
||||
mode_rules = _ADVANCED_CHAT_MODE_RULES if mode == "advanced-chat" else _WORKFLOW_MODE_RULES
|
||||
return (
|
||||
_BASE_SYSTEM_PROMPT_HEAD
|
||||
+ mode_rules
|
||||
+ _BASE_SYSTEM_PROMPT_TAIL
|
||||
+ build_node_config_cheatsheet(node_types)
|
||||
+ _BASE_SYSTEM_PROMPT_FOOTER
|
||||
)
|
||||
|
||||
BUILDER_SYSTEM_PROMPT_ADVANCED_CHAT = (
|
||||
_BASE_SYSTEM_PROMPT_HEAD
|
||||
+ _ADVANCED_CHAT_MODE_RULES
|
||||
+ _BASE_SYSTEM_PROMPT_TAIL
|
||||
+ NODE_CONFIG_CHEATSHEET
|
||||
+ _BASE_SYSTEM_PROMPT_FOOTER
|
||||
)
|
||||
|
||||
# Static full-cheatsheet prompts — the back-compat default returned by
|
||||
# ``get_builder_system_prompt`` when the caller doesn't pin a node-type set.
|
||||
BUILDER_SYSTEM_PROMPT_WORKFLOW = _assemble_builder_system_prompt("workflow", None)
|
||||
|
||||
BUILDER_SYSTEM_PROMPT_ADVANCED_CHAT = _assemble_builder_system_prompt("advanced-chat", None)
|
||||
|
||||
|
||||
BUILDER_USER_PROMPT = """# User instruction
|
||||
@ -546,8 +633,16 @@ def format_plan_block(plan_nodes: list[dict[str, Any]]) -> str:
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def get_builder_system_prompt(mode: str) -> str:
|
||||
"""Pick the system prompt branch for Workflow vs Advanced Chat."""
|
||||
if mode == "advanced-chat":
|
||||
return BUILDER_SYSTEM_PROMPT_ADVANCED_CHAT
|
||||
return BUILDER_SYSTEM_PROMPT_WORKFLOW
|
||||
def get_builder_system_prompt(mode: str, node_types: Iterable[str] | None = None) -> str:
|
||||
"""
|
||||
Build the builder system prompt for ``mode``, with a cheatsheet scoped to
|
||||
``node_types`` (the planner's chosen node types).
|
||||
|
||||
When ``node_types`` is ``None`` we return the cached full-cheatsheet
|
||||
constant (back-compat default). When the runner passes the plan's node-type
|
||||
set we assemble a fresh prompt carrying only the relevant per-type schemas,
|
||||
so the builder isn't handed config for node types the workflow never uses.
|
||||
"""
|
||||
if node_types is None:
|
||||
return BUILDER_SYSTEM_PROMPT_ADVANCED_CHAT if mode == "advanced-chat" else BUILDER_SYSTEM_PROMPT_WORKFLOW
|
||||
return _assemble_builder_system_prompt(mode, node_types)
|
||||
|
||||
@ -74,6 +74,21 @@ _DEFAULT_VIEWPORT: GraphViewportDict = {"x": 0.0, "y": 0.0, "zoom": 0.7}
|
||||
_DEFAULT_NODE_WIDTH = 244
|
||||
_DEFAULT_NODE_HEIGHT = 100
|
||||
|
||||
# Start-node input variable types that carry file uploads. Mirrors
|
||||
# ``graphon.variables.input_entities.VariableEntityType.FILE / FILE_LIST``.
|
||||
_FILE_VARIABLE_TYPES = frozenset({"file", "file-list"})
|
||||
|
||||
# Backstop defaults for a file / file-list start variable when the builder
|
||||
# omits the required upload config. ``allowed_file_types`` is a REQUIRED field
|
||||
# (Studio rejects the draft with "supported file types is required" when it's
|
||||
# empty — see ``config-var/config-modal/utils.ts``); we default to every
|
||||
# standard type so no valid upload is rejected. ``custom`` is intentionally
|
||||
# excluded because it would in turn require a non-empty
|
||||
# ``allowed_file_extensions``. The real fix is the builder now documenting and
|
||||
# emitting these fields; this is the safety net that guarantees a loadable draft.
|
||||
_DEFAULT_ALLOWED_FILE_TYPES = ("document", "image", "audio", "video")
|
||||
_DEFAULT_FILE_UPLOAD_METHODS = ("local_file", "remote_url")
|
||||
|
||||
# Token ceiling for the planner call when the caller didn't pin one. The plan
|
||||
# is a short JSON node list (a handful of nodes with labels/purposes), so this
|
||||
# is generous headroom while still bounding a runaway response. The builder is
|
||||
@ -512,8 +527,15 @@ class WorkflowGenerator:
|
||||
tool_catalogue_section=format_builder_tool_catalogue_section(tool_catalogue_text),
|
||||
start_inputs_section=format_start_inputs_section(start_inputs or []),
|
||||
)
|
||||
# Scope the builder cheatsheet to exactly the node types the planner
|
||||
# chose, so the prompt carries each type's FULL schema (e.g. a file
|
||||
# start variable's required ``allowed_file_types``) without dragging in
|
||||
# config for unrelated node types.
|
||||
plan_node_types = {
|
||||
str(node.get("node_type") or "").strip() for node in plan_nodes if str(node.get("node_type") or "").strip()
|
||||
}
|
||||
messages = [
|
||||
SystemPromptMessage(content=get_builder_system_prompt(mode)),
|
||||
SystemPromptMessage(content=get_builder_system_prompt(mode, plan_node_types)),
|
||||
UserPromptMessage(content=user_prompt),
|
||||
]
|
||||
parsed = cls._invoke_and_parse_json(
|
||||
@ -658,6 +680,13 @@ class WorkflowGenerator:
|
||||
# variables before we surface them as errors.
|
||||
cls._reconcile_variable_references(nodes=nodes, mode=mode)
|
||||
|
||||
# Schema backstop: a "file" / "file-list" start variable MUST carry a
|
||||
# non-empty ``allowed_file_types`` or Studio refuses to load the draft
|
||||
# ("supported file types is required"). The builder is now told to set
|
||||
# it, but we fill safe defaults for any variable that still lacks it so
|
||||
# the generated workflow always loads and runs.
|
||||
cls._normalize_start_file_variables(nodes=nodes)
|
||||
|
||||
return cast(GraphDict, {"nodes": nodes, "edges": deduped_edges, "viewport": viewport})
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
@ -693,6 +722,21 @@ class WorkflowGenerator:
|
||||
# remapping when we defensively strip hyphens out of LLM-emitted ids.
|
||||
_ID_FIELDS: ClassVar = frozenset({"start_node_id", "iteration_id", "loop_id", "parentId"})
|
||||
|
||||
# ``data`` keys whose value is a plain string list, never a
|
||||
# ``[node_id, var]`` value-selector — so the reference walker must not read
|
||||
# a 2-element one as a selector. ``default`` holds an input's default value;
|
||||
# ``options`` holds select choices; the ``allowed_file_*`` keys hold a file
|
||||
# variable's upload config (types / extensions / methods).
|
||||
_NON_SELECTOR_LIST_KEYS: ClassVar = frozenset(
|
||||
{
|
||||
"default",
|
||||
"options",
|
||||
"allowed_file_types",
|
||||
"allowed_file_extensions",
|
||||
"allowed_file_upload_methods",
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _reconcile_variable_references(cls, *, nodes: list[dict[str, Any]], mode: WorkflowGenerationMode) -> None:
|
||||
"""
|
||||
@ -747,12 +791,16 @@ class WorkflowGenerator:
|
||||
# Known selector shapes: 2-element [node_id, var] lists.
|
||||
for k, v in value.items():
|
||||
# ``value_selector`` / ``query_variable_selector`` / etc.: a
|
||||
# flat 2-element list of strings.
|
||||
# flat 2-element list of strings. Skip keys whose value is a
|
||||
# plain string list that merely HAPPENS to have two entries —
|
||||
# a 2-option ``select`` or a file variable's two allowed upload
|
||||
# methods are NOT ``[node_id, var]`` selectors and must not be
|
||||
# mistaken for references.
|
||||
if (
|
||||
isinstance(v, list)
|
||||
and len(v) == 2
|
||||
and all(isinstance(x, str) for x in v)
|
||||
and k != "default" # default values for input variables are not selectors
|
||||
and k not in cls._NON_SELECTOR_LIST_KEYS
|
||||
):
|
||||
node_id, var = v[0].strip(), v[1].strip()
|
||||
if node_id and var:
|
||||
@ -923,6 +971,96 @@ class WorkflowGenerator:
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _normalize_start_file_variables(cls, *, nodes: list[dict[str, Any]]) -> None:
|
||||
"""
|
||||
Fill the required upload config on every file / file-list start variable.
|
||||
|
||||
A start variable of type ``file`` / ``file-list`` is invalid without a
|
||||
non-empty ``allowed_file_types`` — Studio rejects the draft with
|
||||
"supported file types is required" (see the front-end validator in
|
||||
``config-var/config-modal/utils.ts``) and the workflow never runs. The
|
||||
builder prompt now documents these fields, but LLMs still drop them, so
|
||||
we backfill safe defaults here:
|
||||
|
||||
* a start variable a ``document-extractor`` consumes but that wasn't
|
||||
declared as a file type → promoted to ``file`` (or ``file-list``
|
||||
when the extractor's ``is_array_file`` is set), defaulting its
|
||||
allowed types to ``["document"]`` (what extraction needs);
|
||||
* empty / missing ``allowed_file_types`` → every standard file type;
|
||||
* ``custom`` present without ``allowed_file_extensions`` → drop
|
||||
``custom`` (it would otherwise require a non-empty extension list);
|
||||
* empty / missing ``allowed_file_upload_methods`` → local + remote;
|
||||
* ensure ``allowed_file_extensions`` is at least an empty list.
|
||||
|
||||
Idempotent: a variable that already declares valid file config is left
|
||||
untouched.
|
||||
"""
|
||||
start_node = next(
|
||||
(n for n in nodes if (n.get("data") or {}).get("type") == BuiltinNodeTypes.START),
|
||||
None,
|
||||
)
|
||||
if start_node is None:
|
||||
return
|
||||
variables = (start_node.get("data") or {}).get("variables")
|
||||
if not isinstance(variables, list):
|
||||
return
|
||||
|
||||
# Start variables a document-extractor reads → whether it wants an
|
||||
# array (file-list). These MUST be file inputs even if the builder
|
||||
# mistyped them (e.g. declared "paragraph"), or the extractor fails at
|
||||
# run time. ``["document"]`` is the right default for text extraction.
|
||||
extractor_file_vars = cls._document_extractor_start_vars(nodes=nodes, start_id=start_node.get("id", ""))
|
||||
|
||||
for var in variables:
|
||||
if not isinstance(var, dict):
|
||||
continue
|
||||
name = var.get("variable")
|
||||
if name in extractor_file_vars and var.get("type") not in _FILE_VARIABLE_TYPES:
|
||||
var["type"] = "file-list" if extractor_file_vars[name] else "file"
|
||||
var.setdefault("allowed_file_types", ["document"])
|
||||
if var.get("type") not in _FILE_VARIABLE_TYPES:
|
||||
continue
|
||||
allowed_types = var.get("allowed_file_types")
|
||||
if not isinstance(allowed_types, list) or not allowed_types:
|
||||
allowed_types = list(_DEFAULT_ALLOWED_FILE_TYPES)
|
||||
var["allowed_file_types"] = allowed_types
|
||||
# ``custom`` demands a non-empty extension list; without one, drop it
|
||||
# so the variable doesn't trip the "file extensions required" check.
|
||||
extensions = var.get("allowed_file_extensions")
|
||||
has_extensions = isinstance(extensions, list) and bool(extensions)
|
||||
if "custom" in allowed_types and not has_extensions:
|
||||
pruned = [t for t in allowed_types if t != "custom"]
|
||||
var["allowed_file_types"] = pruned or list(_DEFAULT_ALLOWED_FILE_TYPES)
|
||||
methods = var.get("allowed_file_upload_methods")
|
||||
if not isinstance(methods, list) or not methods:
|
||||
var["allowed_file_upload_methods"] = list(_DEFAULT_FILE_UPLOAD_METHODS)
|
||||
if not isinstance(var.get("allowed_file_extensions"), list):
|
||||
var["allowed_file_extensions"] = []
|
||||
|
||||
@classmethod
|
||||
def _document_extractor_start_vars(cls, *, nodes: list[dict[str, Any]], start_id: str) -> dict[str, bool]:
|
||||
"""
|
||||
Map start-variable name → ``is_array_file`` for every start variable a
|
||||
``document-extractor`` node reads via its ``variable_selector``.
|
||||
|
||||
When two extractors read the same variable we keep ``True`` (file-list)
|
||||
if any of them wants an array, since a file-list also satisfies a
|
||||
single-file read.
|
||||
"""
|
||||
out: dict[str, bool] = {}
|
||||
if not start_id:
|
||||
return out
|
||||
for node in nodes:
|
||||
data = node.get("data") or {}
|
||||
if data.get("type") != BuiltinNodeTypes.DOCUMENT_EXTRACTOR:
|
||||
continue
|
||||
selector = data.get("variable_selector")
|
||||
if isinstance(selector, list) and len(selector) == 2 and selector[0] == start_id:
|
||||
var_name = selector[1]
|
||||
out[var_name] = out.get(var_name, False) or bool(data.get("is_array_file"))
|
||||
return out
|
||||
|
||||
@classmethod
|
||||
def _fill_node_defaults(cls, node: dict[str, Any]) -> None:
|
||||
"""Ensure every node has the wrapper-level fields the Studio canvas needs."""
|
||||
|
||||
@ -95,6 +95,61 @@ class TestGetBuilderSystemPrompt:
|
||||
assert prompt is BUILDER_SYSTEM_PROMPT_ADVANCED_CHAT
|
||||
assert 'exactly one "answer" node' in prompt
|
||||
|
||||
def test_scopes_cheatsheet_to_planned_node_types(self):
|
||||
# When the runner pins the plan's node-type set, the builder prompt
|
||||
# carries ONLY those types' schemas — no schema for unrelated nodes.
|
||||
prompt = get_builder_system_prompt("workflow", {"start", "llm", "end"})
|
||||
assert "- start:" in prompt
|
||||
assert "- llm:" in prompt
|
||||
assert "- if-else:" not in prompt
|
||||
assert "- tool" not in prompt
|
||||
assert "## Containers" not in prompt
|
||||
# Still a valid, mode-correct prompt.
|
||||
assert 'exactly one "end" node' in prompt
|
||||
|
||||
def test_scoped_prompt_pulls_in_containers_for_iteration(self):
|
||||
prompt = get_builder_system_prompt("workflow", {"start", "iteration", "llm", "end"})
|
||||
assert "## Containers" in prompt
|
||||
|
||||
def test_scoped_prompt_is_smaller_than_full(self):
|
||||
# The whole point of dynamic assembly: a small plan ships a smaller
|
||||
# builder prompt than the full cheatsheet.
|
||||
scoped = get_builder_system_prompt("workflow", {"start", "llm", "end"})
|
||||
assert len(scoped) < len(BUILDER_SYSTEM_PROMPT_WORKFLOW)
|
||||
|
||||
|
||||
class TestBuildNodeConfigCheatsheet:
|
||||
def test_none_returns_full_cheatsheet(self):
|
||||
from core.workflow.generator.prompts.builder_prompts import (
|
||||
NODE_CONFIG_CHEATSHEET,
|
||||
build_node_config_cheatsheet,
|
||||
)
|
||||
|
||||
full = build_node_config_cheatsheet(None)
|
||||
assert full == NODE_CONFIG_CHEATSHEET
|
||||
# Full cheatsheet documents every node type + containers.
|
||||
assert "- tool" in full
|
||||
assert "- if-else:" in full
|
||||
assert "## Containers" in full
|
||||
|
||||
def test_always_includes_start_even_when_omitted(self):
|
||||
# Every workflow has a start node; the assembler force-includes it so
|
||||
# the builder can always declare input variables.
|
||||
from core.workflow.generator.prompts.builder_prompts import build_node_config_cheatsheet
|
||||
|
||||
out = build_node_config_cheatsheet({"llm", "end"})
|
||||
assert "- start:" in out
|
||||
|
||||
def test_start_snippet_documents_file_upload_schema(self):
|
||||
# The bug this fixes: a file start variable needs allowed_file_types,
|
||||
# which the builder never knew about. The snippet must now teach it.
|
||||
from core.workflow.generator.prompts.builder_prompts import build_node_config_cheatsheet
|
||||
|
||||
out = build_node_config_cheatsheet({"start", "document-extractor", "llm", "end"})
|
||||
assert "allowed_file_types" in out
|
||||
assert "allowed_file_upload_methods" in out
|
||||
assert "supported file types" in out # the exact Studio error wording
|
||||
|
||||
|
||||
class TestFormatPlanBlockParentHints:
|
||||
def test_resolves_parent_label_to_node_id(self):
|
||||
|
||||
@ -2269,3 +2269,229 @@ class TestWorkflowGeneratorRefine:
|
||||
assert result["error"] == ""
|
||||
types = [n["data"]["type"] for n in result["graph"]["nodes"]]
|
||||
assert types == ["start", "llm", "end"]
|
||||
|
||||
|
||||
class TestWorkflowGeneratorFileVariables:
|
||||
"""The reported bug: a file-input workflow ("takes in a file, extract its
|
||||
content, summarize") generated a start node whose file variable lacked the
|
||||
required ``allowed_file_types``, so Studio rejected the draft with
|
||||
"supported file types is required". The builder now documents the field and
|
||||
the postprocessor backfills it as a final safety net."""
|
||||
|
||||
@staticmethod
|
||||
def _planner() -> str:
|
||||
return json.dumps(
|
||||
{
|
||||
"title": "File Summarizer",
|
||||
"description": "Summarize an uploaded document into bullet points",
|
||||
"app_name": "File Summarizer",
|
||||
"icon": "📄",
|
||||
"start_inputs": [{"variable": "doc", "label": "Document", "type": "file"}],
|
||||
"nodes": [
|
||||
{"label": "Start", "node_type": "start", "purpose": "Take a file"},
|
||||
{"label": "Extract", "node_type": "document-extractor", "purpose": "Extract text"},
|
||||
{"label": "Summarize", "node_type": "llm", "purpose": "Bullet points"},
|
||||
{"label": "End", "node_type": "end", "purpose": "Return"},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _builder_file_var_missing_allowed_types() -> str:
|
||||
# The builder declares a file variable but (the bug) omits
|
||||
# allowed_file_types / upload methods.
|
||||
return json.dumps(
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node1",
|
||||
"type": "custom",
|
||||
"position": {"x": 0, "y": 0},
|
||||
"data": {
|
||||
"type": "start",
|
||||
"title": "Start",
|
||||
"variables": [{"variable": "doc", "label": "Document", "type": "file"}],
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "node2",
|
||||
"type": "custom",
|
||||
"position": {"x": 0, "y": 0},
|
||||
"data": {
|
||||
"type": "document-extractor",
|
||||
"title": "Extract",
|
||||
"variable_selector": ["node1", "doc"],
|
||||
"is_array_file": False,
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "node3",
|
||||
"type": "custom",
|
||||
"position": {"x": 0, "y": 0},
|
||||
"data": {
|
||||
"type": "llm",
|
||||
"title": "Summarize",
|
||||
"prompt_template": [{"role": "user", "text": "Summarize {{#node2.text#}}"}],
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "node4",
|
||||
"type": "custom",
|
||||
"position": {"x": 0, "y": 0},
|
||||
"data": {
|
||||
"type": "end",
|
||||
"title": "End",
|
||||
"outputs": [{"variable": "summary", "value_selector": ["node3", "text"]}],
|
||||
},
|
||||
},
|
||||
],
|
||||
"edges": [
|
||||
{"id": "e1", "source": "node1", "target": "node2", "type": "custom"},
|
||||
{"id": "e2", "source": "node2", "target": "node3", "type": "custom"},
|
||||
{"id": "e3", "source": "node3", "target": "node4", "type": "custom"},
|
||||
],
|
||||
"viewport": {"x": 0, "y": 0, "zoom": 0.7},
|
||||
}
|
||||
)
|
||||
|
||||
def test_backfills_allowed_file_types_so_draft_loads(self):
|
||||
model_instance = MagicMock()
|
||||
model_instance.invoke_llm.side_effect = [
|
||||
_llm_result(self._planner()),
|
||||
_llm_result(self._builder_file_var_missing_allowed_types()),
|
||||
]
|
||||
|
||||
result = WorkflowGenerator.generate_workflow_graph(
|
||||
model_instance=model_instance,
|
||||
model_parameters={},
|
||||
provider="openai",
|
||||
model_name="gpt-4o",
|
||||
model_mode="chat",
|
||||
mode="workflow",
|
||||
instruction="takes in a file, extracting its content, then summarizing into bullet points",
|
||||
)
|
||||
|
||||
assert result["error"] == ""
|
||||
start = next(n for n in result["graph"]["nodes"] if n["data"]["type"] == "start")
|
||||
doc = next(v for v in start["data"]["variables"] if v["variable"] == "doc")
|
||||
assert doc["type"] == "file"
|
||||
# The required field is now present and non-empty — the whole point.
|
||||
assert doc["allowed_file_types"]
|
||||
assert doc["allowed_file_upload_methods"] == ["local_file", "remote_url"]
|
||||
|
||||
def test_builder_prompt_is_scoped_to_planned_node_types(self):
|
||||
# Capture the system prompt handed to the builder and assert it only
|
||||
# carries the planned node types' schemas (dynamic assembly).
|
||||
captured: list[str] = []
|
||||
|
||||
def _capture(*args, **kwargs):
|
||||
prompt_messages = kwargs.get("prompt_messages") or (args[1] if len(args) > 1 else [])
|
||||
captured.append("\n".join(getattr(m, "content", "") or "" for m in prompt_messages))
|
||||
# Return planner then builder responses in order.
|
||||
idx = len(captured) - 1
|
||||
return _llm_result(self._planner() if idx == 0 else self._builder_file_var_missing_allowed_types())
|
||||
|
||||
model_instance = MagicMock()
|
||||
model_instance.invoke_llm.side_effect = _capture
|
||||
|
||||
WorkflowGenerator.generate_workflow_graph(
|
||||
model_instance=model_instance,
|
||||
model_parameters={},
|
||||
provider="openai",
|
||||
model_name="gpt-4o",
|
||||
model_mode="chat",
|
||||
mode="workflow",
|
||||
instruction="Summarize an uploaded file",
|
||||
)
|
||||
|
||||
builder_prompt = captured[1]
|
||||
assert "- document-extractor:" in builder_prompt
|
||||
assert "- start:" in builder_prompt
|
||||
# No schema for node types absent from the plan.
|
||||
assert "- if-else:" not in builder_prompt
|
||||
assert "- tool" not in builder_prompt
|
||||
|
||||
def test_promotes_mistyped_var_consumed_by_document_extractor(self):
|
||||
# A direct unit test of the backstop: a document-extractor reading a
|
||||
# paragraph-typed start var forces it to a file type.
|
||||
nodes = [
|
||||
{
|
||||
"id": "s",
|
||||
"data": {
|
||||
"type": "start",
|
||||
"variables": [{"variable": "doc", "label": "Doc", "type": "paragraph"}],
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "x",
|
||||
"data": {
|
||||
"type": "document-extractor",
|
||||
"variable_selector": ["s", "doc"],
|
||||
"is_array_file": False,
|
||||
},
|
||||
},
|
||||
]
|
||||
WorkflowGenerator._normalize_start_file_variables(nodes=nodes)
|
||||
doc = nodes[0]["data"]["variables"][0]
|
||||
assert doc["type"] == "file"
|
||||
assert doc["allowed_file_types"] == ["document"]
|
||||
|
||||
def test_drops_custom_file_type_without_extensions(self):
|
||||
nodes = [
|
||||
{
|
||||
"id": "s",
|
||||
"data": {
|
||||
"type": "start",
|
||||
"variables": [
|
||||
{
|
||||
"variable": "f",
|
||||
"label": "F",
|
||||
"type": "file",
|
||||
"allowed_file_types": ["custom"],
|
||||
"allowed_file_extensions": [],
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
]
|
||||
WorkflowGenerator._normalize_start_file_variables(nodes=nodes)
|
||||
f = nodes[0]["data"]["variables"][0]
|
||||
assert "custom" not in f["allowed_file_types"]
|
||||
assert f["allowed_file_types"] # non-empty fallback
|
||||
|
||||
def test_leaves_valid_file_variable_untouched(self):
|
||||
nodes = [
|
||||
{
|
||||
"id": "s",
|
||||
"data": {
|
||||
"type": "start",
|
||||
"variables": [
|
||||
{
|
||||
"variable": "img",
|
||||
"label": "Img",
|
||||
"type": "file",
|
||||
"allowed_file_types": ["image"],
|
||||
"allowed_file_upload_methods": ["local_file"],
|
||||
"allowed_file_extensions": [],
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
]
|
||||
WorkflowGenerator._normalize_start_file_variables(nodes=nodes)
|
||||
img = nodes[0]["data"]["variables"][0]
|
||||
assert img["allowed_file_types"] == ["image"]
|
||||
assert img["allowed_file_upload_methods"] == ["local_file"]
|
||||
|
||||
def test_ignores_non_file_variables(self):
|
||||
nodes = [
|
||||
{
|
||||
"id": "s",
|
||||
"data": {
|
||||
"type": "start",
|
||||
"variables": [{"variable": "t", "label": "T", "type": "text-input"}],
|
||||
},
|
||||
}
|
||||
]
|
||||
WorkflowGenerator._normalize_start_file_variables(nodes=nodes)
|
||||
assert "allowed_file_types" not in nodes[0]["data"]["variables"][0]
|
||||
|
||||
@ -148,6 +148,9 @@ ENABLE_WEBSITE_JINAREADER=true
|
||||
ENABLE_WEBSITE_FIRECRAWL=true
|
||||
ENABLE_WEBSITE_WATERCRAWL=true
|
||||
NEXT_PUBLIC_ENABLE_SINGLE_DOLLAR_LATEX=false
|
||||
# Enable preview features still in development (currently the /create and
|
||||
# /refine slash commands in the "Go to Anything" command palette).
|
||||
NEXT_PUBLIC_ENABLE_FEATURE_PREVIEW=false
|
||||
EXPERIMENTAL_ENABLE_VINEXT=false
|
||||
|
||||
# Storage and default vector store
|
||||
|
||||
@ -24,6 +24,7 @@ ENABLE_WEBSITE_JINAREADER=true
|
||||
ENABLE_WEBSITE_FIRECRAWL=true
|
||||
ENABLE_WEBSITE_WATERCRAWL=true
|
||||
NEXT_PUBLIC_ENABLE_SINGLE_DOLLAR_LATEX=false
|
||||
NEXT_PUBLIC_ENABLE_FEATURE_PREVIEW=false
|
||||
NEXT_PUBLIC_COOKIE_DOMAIN=
|
||||
NEXT_PUBLIC_BATCH_CONCURRENCY=5
|
||||
CSP_WHITELIST=
|
||||
|
||||
@ -84,6 +84,10 @@ NEXT_PUBLIC_ENABLE_WEBSITE_WATERCRAWL=true
|
||||
# Default is false for security reasons to prevent conflicts with regular text
|
||||
NEXT_PUBLIC_ENABLE_SINGLE_DOLLAR_LATEX=false
|
||||
|
||||
# Enable preview features still in development (currently the /create and
|
||||
# /refine slash commands in the "Go to Anything" command palette)
|
||||
NEXT_PUBLIC_ENABLE_FEATURE_PREVIEW=false
|
||||
|
||||
# The maximum number of tree node depth for workflow
|
||||
NEXT_PUBLIC_MAX_TREE_DEPTH=50
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@ const {
|
||||
mockRegister,
|
||||
mockSearch,
|
||||
mockUnregister,
|
||||
featureFlag,
|
||||
} = vi.hoisted(() => ({
|
||||
mockSetTheme: vi.fn(),
|
||||
mockSetLocale: vi.fn(),
|
||||
@ -16,6 +17,14 @@ const {
|
||||
mockRegister: vi.fn(),
|
||||
mockSearch: vi.fn(),
|
||||
mockUnregister: vi.fn(),
|
||||
// Mutable holder so each test can flip the feature-preview flag before render.
|
||||
featureFlag: { enabled: false },
|
||||
}))
|
||||
|
||||
vi.mock('@/config', () => ({
|
||||
get ENABLE_FEATURE_PREVIEW() {
|
||||
return featureFlag.enabled
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('next-themes', () => ({
|
||||
@ -92,9 +101,47 @@ describe('slashAction', () => {
|
||||
describe('SlashCommandProvider', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
// Default: feature preview off, so /create and /refine are NOT registered.
|
||||
featureFlag.enabled = false
|
||||
})
|
||||
|
||||
it('should register commands on mount and unregister them on unmount', () => {
|
||||
it('should not register the /create and /refine preview commands when the feature flag is off', () => {
|
||||
const { unmount } = render(<SlashCommandProvider />)
|
||||
|
||||
expect(mockRegister.mock.calls.map(call => call[0].name)).toEqual([
|
||||
'theme',
|
||||
'language',
|
||||
'forum',
|
||||
'docs',
|
||||
'community',
|
||||
'account',
|
||||
'zen',
|
||||
'go',
|
||||
])
|
||||
expect(mockRegister).toHaveBeenCalledWith(expect.objectContaining({ name: 'theme' }), { setTheme: mockSetTheme })
|
||||
expect(mockRegister).toHaveBeenCalledWith(expect.objectContaining({ name: 'language' }), { setLocale: mockSetLocale })
|
||||
|
||||
unmount()
|
||||
|
||||
// Unregister is always called for the preview commands (a no-op when they
|
||||
// were never registered) so toggling the flag off mid-session stays clean.
|
||||
expect(mockUnregister.mock.calls.map(call => call[0])).toEqual([
|
||||
'theme',
|
||||
'language',
|
||||
'forum',
|
||||
'docs',
|
||||
'community',
|
||||
'account',
|
||||
'zen',
|
||||
'go',
|
||||
'create',
|
||||
'refine',
|
||||
])
|
||||
})
|
||||
|
||||
it('should register the /create and /refine preview commands when the feature flag is on', () => {
|
||||
featureFlag.enabled = true
|
||||
|
||||
const { unmount } = render(<SlashCommandProvider />)
|
||||
|
||||
expect(mockRegister.mock.calls.map(call => call[0].name)).toEqual([
|
||||
@ -109,8 +156,6 @@ describe('SlashCommandProvider', () => {
|
||||
'create',
|
||||
'refine',
|
||||
])
|
||||
expect(mockRegister).toHaveBeenCalledWith(expect.objectContaining({ name: 'theme' }), { setTheme: mockSetTheme })
|
||||
expect(mockRegister).toHaveBeenCalledWith(expect.objectContaining({ name: 'language' }), { setLocale: mockSetLocale })
|
||||
|
||||
unmount()
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ import type { ActionItem } from '../types'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useEffect } from 'react'
|
||||
import { getI18n } from 'react-i18next'
|
||||
import { ENABLE_FEATURE_PREVIEW } from '@/config'
|
||||
import { setLocaleOnClient } from '@/i18n-config'
|
||||
import { accountCommand } from './account'
|
||||
import { executeCommand } from './command-bus'
|
||||
@ -52,8 +53,11 @@ const registerSlashCommands = (deps: Record<string, any>) => {
|
||||
slashCommandRegistry.register(accountCommand, {})
|
||||
slashCommandRegistry.register(zenCommand, {})
|
||||
slashCommandRegistry.register(goCommand, {})
|
||||
slashCommandRegistry.register(createCommand, {})
|
||||
slashCommandRegistry.register(refineCommand, {})
|
||||
// `/create` and `/refine` are preview features, gated behind a flag.
|
||||
if (ENABLE_FEATURE_PREVIEW) {
|
||||
slashCommandRegistry.register(createCommand, {})
|
||||
slashCommandRegistry.register(refineCommand, {})
|
||||
}
|
||||
}
|
||||
|
||||
const unregisterSlashCommands = () => {
|
||||
@ -66,6 +70,7 @@ const unregisterSlashCommands = () => {
|
||||
slashCommandRegistry.unregister('account')
|
||||
slashCommandRegistry.unregister('zen')
|
||||
slashCommandRegistry.unregister('go')
|
||||
// No-op when the preview flag is off and these were never registered.
|
||||
slashCommandRegistry.unregister('create')
|
||||
slashCommandRegistry.unregister('refine')
|
||||
}
|
||||
|
||||
@ -314,6 +314,7 @@ export const ENABLE_WEBSITE_JINAREADER = env.NEXT_PUBLIC_ENABLE_WEBSITE_JINAREAD
|
||||
export const ENABLE_WEBSITE_FIRECRAWL = env.NEXT_PUBLIC_ENABLE_WEBSITE_FIRECRAWL
|
||||
export const ENABLE_WEBSITE_WATERCRAWL = env.NEXT_PUBLIC_ENABLE_WEBSITE_WATERCRAWL
|
||||
export const ENABLE_SINGLE_DOLLAR_LATEX = env.NEXT_PUBLIC_ENABLE_SINGLE_DOLLAR_LATEX
|
||||
export const ENABLE_FEATURE_PREVIEW = env.NEXT_PUBLIC_ENABLE_FEATURE_PREVIEW
|
||||
|
||||
export const VALUE_SELECTOR_DELIMITER = '@@@'
|
||||
|
||||
|
||||
@ -55,6 +55,7 @@ export NEXT_PUBLIC_ENABLE_WEBSITE_JINAREADER=${ENABLE_WEBSITE_JINAREADER:-true}
|
||||
export NEXT_PUBLIC_ENABLE_WEBSITE_FIRECRAWL=${ENABLE_WEBSITE_FIRECRAWL:-true}
|
||||
export NEXT_PUBLIC_ENABLE_WEBSITE_WATERCRAWL=${ENABLE_WEBSITE_WATERCRAWL:-true}
|
||||
export NEXT_PUBLIC_ENABLE_SINGLE_DOLLAR_LATEX=${NEXT_PUBLIC_ENABLE_SINGLE_DOLLAR_LATEX:-false}
|
||||
export NEXT_PUBLIC_ENABLE_FEATURE_PREVIEW=${NEXT_PUBLIC_ENABLE_FEATURE_PREVIEW:-false}
|
||||
export NEXT_PUBLIC_LOOP_NODE_MAX_COUNT=${LOOP_NODE_MAX_COUNT}
|
||||
export NEXT_PUBLIC_MAX_PARALLEL_LIMIT=${MAX_PARALLEL_LIMIT}
|
||||
export NEXT_PUBLIC_MAX_ITERATIONS_NUM=${MAX_ITERATIONS_NUM}
|
||||
|
||||
@ -63,6 +63,12 @@ const clientSchema = {
|
||||
* The deployment edition, SELF_HOSTED
|
||||
*/
|
||||
NEXT_PUBLIC_EDITION: z.enum(['SELF_HOSTED', 'CLOUD']).default('SELF_HOSTED'),
|
||||
/**
|
||||
* Enable preview features that are still in development.
|
||||
* Currently gates the `/create` and `/refine` slash commands in the
|
||||
* "Go to Anything" command palette (Cmd/Ctrl+K).
|
||||
*/
|
||||
NEXT_PUBLIC_ENABLE_FEATURE_PREVIEW: coercedBoolean.default(false),
|
||||
|
||||
/**
|
||||
* Cloud-only system-features defaults.
|
||||
@ -190,6 +196,7 @@ export const env = createEnv({
|
||||
NEXT_PUBLIC_DEPLOY_ENV: isServer ? process.env.NEXT_PUBLIC_DEPLOY_ENV : getRuntimeEnvFromBody('deployEnv'),
|
||||
NEXT_PUBLIC_DISABLE_UPLOAD_IMAGE_AS_ICON: isServer ? process.env.NEXT_PUBLIC_DISABLE_UPLOAD_IMAGE_AS_ICON : getRuntimeEnvFromBody('disableUploadImageAsIcon'),
|
||||
NEXT_PUBLIC_EDITION: isServer ? process.env.NEXT_PUBLIC_EDITION : getRuntimeEnvFromBody('edition'),
|
||||
NEXT_PUBLIC_ENABLE_FEATURE_PREVIEW: isServer ? process.env.NEXT_PUBLIC_ENABLE_FEATURE_PREVIEW : getRuntimeEnvFromBody('enableFeaturePreview'),
|
||||
|
||||
/**
|
||||
* Cloud-only system-features defaults.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user