mirror of
https://github.com/langgenius/dify.git
synced 2026-11-12 14:21:29 +08:00
133 lines
5.8 KiB
Python
133 lines
5.8 KiB
Python
"""Real cognition for Dify Builder mock test inputs.
|
|
|
|
Given the start node's StartSchema, produce plausible run inputs keyed by each
|
|
declared variable. LLM-assisted, degrading to deterministic type-based defaults
|
|
on model-None / provider-error / parse-fail (never raises). Structured samples
|
|
use strict JSON decoding and at most two completions; nested JSON for text
|
|
fields is serialized here rather than escaped by the model."""
|
|
|
|
import json
|
|
from collections.abc import Callable
|
|
from typing import Any
|
|
|
|
from core.dify_builder.models import Inputs, StartSchema
|
|
from services.dify_builder import trace_diagnostics as diagnostics
|
|
from services.dify_builder.agent import llm
|
|
from services.dify_builder.application.cognition_policy import CognitionPolicyDeniedError
|
|
|
|
_DEFAULT_BY_TYPE = {
|
|
"number": 1,
|
|
"float": 1.0,
|
|
"checkbox": True,
|
|
"boolean": True,
|
|
}
|
|
|
|
|
|
def _variables(schema: StartSchema) -> list[dict[str, Any]]:
|
|
raw = schema.get("variables") if isinstance(schema, dict) else None
|
|
return [v for v in raw if isinstance(v, dict) and v.get("variable")] if isinstance(raw, list) else []
|
|
|
|
|
|
def _default_value(var: dict[str, Any]) -> Any:
|
|
vtype = str(var.get("type") or "")
|
|
options = var.get("options")
|
|
if isinstance(options, list) and options:
|
|
return options[0]
|
|
return _DEFAULT_BY_TYPE.get(vtype, "test")
|
|
|
|
|
|
def _deterministic(vars_: list[dict[str, Any]]) -> Inputs:
|
|
return {str(v["variable"]): _default_value(v) for v in vars_}
|
|
|
|
|
|
def _reject_constant(value: str) -> Any:
|
|
raise ValueError(f"Non-JSON constant: {value}")
|
|
|
|
|
|
def _generated_inputs(data: Any, vars_: list[dict[str, Any]]) -> Inputs:
|
|
if not isinstance(data, dict) or not isinstance(data.get("inputs"), dict):
|
|
raise ValueError("Expected an inputs object")
|
|
produced = data["inputs"]
|
|
text_names = {str(v["variable"]) for v in vars_ if v.get("type") in {"text-input", "paragraph"}}
|
|
json_fields = data.get("json_text_fields", [])
|
|
if (
|
|
not isinstance(json_fields, list)
|
|
or any(not isinstance(name, str) or name not in text_names or name not in produced for name in json_fields)
|
|
or len(set(json_fields)) != len(json_fields)
|
|
):
|
|
raise ValueError("json_text_fields must list distinct declared text fields present in inputs")
|
|
for name in json_fields:
|
|
value = produced[name]
|
|
if isinstance(value, str):
|
|
value = json.loads(value, parse_constant=_reject_constant)
|
|
if not isinstance(value, dict | list):
|
|
raise ValueError("JSON text samples must contain an object or array")
|
|
# Legacy responses without metadata retain ordinary strings exactly. Never
|
|
# guess content encoding from field names or the appearance of a string.
|
|
result: Inputs = {}
|
|
for v in vars_:
|
|
name = str(v["variable"])
|
|
value = produced[name] if name in produced else _default_value(v)
|
|
if name in text_names and isinstance(value, dict | list):
|
|
value = json.dumps(value, ensure_ascii=False, allow_nan=False)
|
|
result[name] = value
|
|
return result
|
|
|
|
|
|
def generate(
|
|
model,
|
|
schema: StartSchema,
|
|
prior_failed: Inputs,
|
|
on_reasoning: Callable[[str], None] | None = None,
|
|
*,
|
|
business_context: str = "",
|
|
) -> Inputs:
|
|
vars_ = _variables(schema)
|
|
if not vars_:
|
|
return {}
|
|
if model is None:
|
|
return _deterministic(vars_)
|
|
system = (
|
|
"You generate plausible TEST inputs for a workflow's start form. Given the input "
|
|
"variables and business context, return realistic values keyed by each variable's name. "
|
|
"Follow the goal, confirmed requirements, approved plan and field hints when choosing "
|
|
"sample shapes and values. For text-input/paragraph fields carrying JSON, return an actual JSON "
|
|
"object or array as the field value, not an escaped JSON string; the application serializes it once. "
|
|
"Include json_text_fields listing those declared text fields (an empty list for ordinary text). "
|
|
"These samples are suggestions for user review, not proof of business acceptance. Avoid any values "
|
|
"listed as previously-failing. Reply with ONLY strict JSON: "
|
|
'{"inputs": {"<variable>": <value>}, "json_text_fields": ["<JSON text variable>"]}.'
|
|
)
|
|
listing = "\n".join(
|
|
f"- {v['variable']} (type={v.get('type', '?')}) options={v.get('options')} "
|
|
f"label={v.get('label')} hint={v.get('hint')} placeholder={v.get('placeholder')} required={v.get('required')}"
|
|
for v in vars_
|
|
)
|
|
user = f"VARIABLES:\n{listing}\n\nPREVIOUSLY-FAILING (avoid): {prior_failed}"
|
|
if business_context:
|
|
user += f"\n\nBUSINESS CONTEXT:\n{business_context}"
|
|
for attempt in (1, 2):
|
|
try:
|
|
text = llm.invoke_text(model, system=system, user=user, on_reasoning=on_reasoning)
|
|
except CognitionPolicyDeniedError:
|
|
raise
|
|
except Exception:
|
|
diagnostics.report("deterministic_fallback", fallback_used=True)
|
|
return _deterministic(vars_)
|
|
try:
|
|
data = json.loads(text, parse_constant=_reject_constant)
|
|
except (ValueError, TypeError, RecursionError):
|
|
diagnostics.report("json_decode_failed", attempt=attempt, fallback_used=attempt == 2)
|
|
else:
|
|
try:
|
|
return _generated_inputs(data, vars_)
|
|
except (ValueError, TypeError, RecursionError):
|
|
diagnostics.report("response_schema_invalid", attempt=attempt, fallback_used=attempt == 2)
|
|
if attempt == 1:
|
|
user += (
|
|
"\n\nThe previous sample was invalid. Return one strict JSON object with an inputs object. "
|
|
"Use actual objects/arrays for JSON text fields and list only their declared text names "
|
|
"in json_text_fields. Do not use NaN, Infinity, comments, fences or trailing content."
|
|
)
|
|
return _deterministic(vars_)
|