feat: unify workflow context picker — contextBuilder multi-select, lift type-blocking, user-language labels, backend serialization, fix circular ref crash
This commit is contained in:
parent
60b2fcf56b
commit
f96325f804
14 changed files with 99 additions and 66 deletions
|
|
@ -12,17 +12,30 @@ import uuid
|
||||||
from typing import Dict, Any, List, Optional
|
from typing import Dict, Any, List, Optional
|
||||||
|
|
||||||
|
|
||||||
def _make_json_serializable(obj: Any) -> Any:
|
_INTERNAL_SKIP_KEYS = frozenset({"_context", "_orderedNodes"})
|
||||||
|
|
||||||
|
|
||||||
|
def _make_json_serializable(obj: Any, _depth: int = 0) -> Any:
|
||||||
"""
|
"""
|
||||||
Recursively convert bytes to base64 strings so structures can be JSON-serialized
|
Recursively convert bytes to base64 strings so structures can be JSON-serialized
|
||||||
for storage in JSONB columns.
|
for storage in JSONB columns.
|
||||||
|
|
||||||
|
Internal runtime keys (_context, _orderedNodes) are skipped — they hold live
|
||||||
|
Python objects (including back-references to nodeOutputs) and must never be
|
||||||
|
stored. A depth guard prevents runaway recursion on unexpected circular refs.
|
||||||
"""
|
"""
|
||||||
|
if _depth > 50:
|
||||||
|
return None
|
||||||
if isinstance(obj, bytes):
|
if isinstance(obj, bytes):
|
||||||
return base64.b64encode(obj).decode("ascii")
|
return base64.b64encode(obj).decode("ascii")
|
||||||
if isinstance(obj, dict):
|
if isinstance(obj, dict):
|
||||||
return {k: _make_json_serializable(v) for k, v in obj.items()}
|
return {
|
||||||
|
k: _make_json_serializable(v, _depth + 1)
|
||||||
|
for k, v in obj.items()
|
||||||
|
if k not in _INTERNAL_SKIP_KEYS
|
||||||
|
}
|
||||||
if isinstance(obj, list):
|
if isinstance(obj, list):
|
||||||
return [_make_json_serializable(v) for v in obj]
|
return [_make_json_serializable(v, _depth + 1) for v in obj]
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
from modules.datamodels.datamodelUam import User
|
from modules.datamodels.datamodelUam import User
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,10 @@ AI_NODES = [
|
||||||
{"name": "resultType", "type": "str", "required": False, "frontendType": "select",
|
{"name": "resultType", "type": "str", "required": False, "frontendType": "select",
|
||||||
"frontendOptions": {"options": ["txt", "json", "md", "csv", "xml", "html", "pdf", "docx", "xlsx", "pptx", "png", "jpg"]},
|
"frontendOptions": {"options": ["txt", "json", "md", "csv", "xml", "html", "pdf", "docx", "xlsx", "pptx", "png", "jpg"]},
|
||||||
"description": t("Ausgabeformat"), "default": "txt"},
|
"description": t("Ausgabeformat"), "default": "txt"},
|
||||||
{"name": "documentList", "type": "DocumentList", "required": False, "frontendType": "dataRef",
|
{"name": "documentList", "type": "DocumentList", "required": False, "frontendType": "hidden",
|
||||||
"description": t("Dokumentenliste (Upstream-Output binden)"), "default": ""},
|
"description": t("Dokumente aus vorherigen Schritten"), "default": ""},
|
||||||
{"name": "context", "type": "str", "required": False, "frontendType": "dataRef",
|
{"name": "context", "type": "Any", "required": False, "frontendType": "contextBuilder",
|
||||||
"description": t("Kontextdaten fuer den Prompt (Upstream-Output binden)"), "default": ""},
|
"description": t("Daten aus vorherigen Schritten"), "default": ""},
|
||||||
{"name": "documentTheme", "type": "str", "required": False, "frontendType": "select",
|
{"name": "documentTheme", "type": "str", "required": False, "frontendType": "select",
|
||||||
"frontendOptions": {"options": ["general", "finance", "legal", "technical", "hr"]},
|
"frontendOptions": {"options": ["general", "finance", "legal", "technical", "hr"]},
|
||||||
"description": t("Dokument-Thema (Style-Hinweis fuer den Renderer)"), "default": "general"},
|
"description": t("Dokument-Thema (Style-Hinweis fuer den Renderer)"), "default": "general"},
|
||||||
|
|
@ -37,7 +37,7 @@ AI_NODES = [
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
"inputPorts": {0: {"accepts": [
|
"inputPorts": {0: {"accepts": [
|
||||||
"DocumentList", "AiResult", "TextResult", "Transit", "LoopItem", "ActionResult",
|
"FormPayload", "DocumentList", "AiResult", "TextResult", "Transit", "LoopItem", "ActionResult",
|
||||||
]}},
|
]}},
|
||||||
"outputPorts": {0: {"schema": "AiResult"}},
|
"outputPorts": {0: {"schema": "AiResult"}},
|
||||||
"meta": {"icon": "mdi-robot", "color": "#9C27B0", "usesAi": True},
|
"meta": {"icon": "mdi-robot", "color": "#9C27B0", "usesAi": True},
|
||||||
|
|
@ -52,14 +52,14 @@ AI_NODES = [
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{"name": "prompt", "type": "str", "required": True, "frontendType": "textarea",
|
{"name": "prompt", "type": "str", "required": True, "frontendType": "textarea",
|
||||||
"description": t("Recherche-Anfrage")},
|
"description": t("Recherche-Anfrage")},
|
||||||
{"name": "context", "type": "str", "required": False, "frontendType": "dataRef",
|
{"name": "context", "type": "Any", "required": False, "frontendType": "contextBuilder",
|
||||||
"description": t("Optionaler Kontext aus Upstream-Node (wird dem Prompt vorangestellt)"), "default": ""},
|
"description": t("Daten aus vorherigen Schritten"), "default": ""},
|
||||||
{"name": "documentList", "type": "DocumentList", "required": False, "frontendType": "dataRef",
|
{"name": "documentList", "type": "DocumentList", "required": False, "frontendType": "hidden",
|
||||||
"description": t("Optionale Dokumentenliste aus Upstream-Node (Text wird dem Prompt hinzugefügt)"), "default": ""},
|
"description": t("Dokumente aus vorherigen Schritten"), "default": ""},
|
||||||
] + _AI_COMMON_PARAMS,
|
] + _AI_COMMON_PARAMS,
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
"inputPorts": {0: {"accepts": ["Transit", "AiResult", "DocumentList", "ActionResult"]}},
|
"inputPorts": {0: {"accepts": ["FormPayload", "Transit", "AiResult", "DocumentList", "ActionResult"]}},
|
||||||
"outputPorts": {0: {"schema": "AiResult"}},
|
"outputPorts": {0: {"schema": "AiResult"}},
|
||||||
"meta": {"icon": "mdi-magnify", "color": "#9C27B0", "usesAi": True},
|
"meta": {"icon": "mdi-magnify", "color": "#9C27B0", "usesAi": True},
|
||||||
"_method": "ai",
|
"_method": "ai",
|
||||||
|
|
@ -72,7 +72,7 @@ AI_NODES = [
|
||||||
"description": t("Dokumentinhalt zusammenfassen"),
|
"description": t("Dokumentinhalt zusammenfassen"),
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{"name": "documentList", "type": "DocumentList", "required": True, "frontendType": "dataRef",
|
{"name": "documentList", "type": "DocumentList", "required": True, "frontendType": "dataRef",
|
||||||
"description": t("Dokumentenliste (Upstream-Output binden)"), "default": ""},
|
"description": t("Dokumente aus vorherigen Schritten")},
|
||||||
{"name": "summaryLength", "type": "str", "required": False, "frontendType": "select",
|
{"name": "summaryLength", "type": "str", "required": False, "frontendType": "select",
|
||||||
"frontendOptions": {"options": ["brief", "medium", "detailed"]},
|
"frontendOptions": {"options": ["brief", "medium", "detailed"]},
|
||||||
"description": t("Kurz, mittel oder ausführlich"), "default": "medium"},
|
"description": t("Kurz, mittel oder ausführlich"), "default": "medium"},
|
||||||
|
|
@ -92,7 +92,7 @@ AI_NODES = [
|
||||||
"description": t("Dokument in Zielsprache übersetzen"),
|
"description": t("Dokument in Zielsprache übersetzen"),
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{"name": "documentList", "type": "DocumentList", "required": True, "frontendType": "dataRef",
|
{"name": "documentList", "type": "DocumentList", "required": True, "frontendType": "dataRef",
|
||||||
"description": t("Dokumentenliste (Upstream-Output binden)"), "default": ""},
|
"description": t("Dokumente aus vorherigen Schritten")},
|
||||||
{"name": "targetLanguage", "type": "str", "required": True, "frontendType": "text",
|
{"name": "targetLanguage", "type": "str", "required": True, "frontendType": "text",
|
||||||
"description": t("Zielsprache (z.B. de, en, French)")},
|
"description": t("Zielsprache (z.B. de, en, French)")},
|
||||||
] + _AI_COMMON_PARAMS,
|
] + _AI_COMMON_PARAMS,
|
||||||
|
|
@ -111,7 +111,7 @@ AI_NODES = [
|
||||||
"description": t("Dokument in anderes Format konvertieren"),
|
"description": t("Dokument in anderes Format konvertieren"),
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{"name": "documentList", "type": "DocumentList", "required": True, "frontendType": "dataRef",
|
{"name": "documentList", "type": "DocumentList", "required": True, "frontendType": "dataRef",
|
||||||
"description": t("Dokumentenliste (Upstream-Output binden)"), "default": ""},
|
"description": t("Dokumente aus vorherigen Schritten")},
|
||||||
{"name": "targetFormat", "type": "str", "required": True, "frontendType": "select",
|
{"name": "targetFormat", "type": "str", "required": True, "frontendType": "select",
|
||||||
"frontendOptions": {"options": ["docx", "pdf", "xlsx", "csv", "txt", "html", "json", "md"]},
|
"frontendOptions": {"options": ["docx", "pdf", "xlsx", "csv", "txt", "html", "json", "md"]},
|
||||||
"description": t("Zielformat")},
|
"description": t("Zielformat")},
|
||||||
|
|
@ -132,14 +132,14 @@ AI_NODES = [
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{"name": "prompt", "type": "str", "required": True, "frontendType": "textarea",
|
{"name": "prompt", "type": "str", "required": True, "frontendType": "textarea",
|
||||||
"description": t("Generierungs-Prompt")},
|
"description": t("Generierungs-Prompt")},
|
||||||
{"name": "context", "type": "str", "required": False, "frontendType": "dataRef",
|
{"name": "context", "type": "Any", "required": False, "frontendType": "contextBuilder",
|
||||||
"description": t("Optionaler Kontext aus Upstream-Node (wird dem Prompt vorangestellt)"), "default": ""},
|
"description": t("Daten aus vorherigen Schritten"), "default": ""},
|
||||||
{"name": "documentList", "type": "DocumentList", "required": False, "frontendType": "dataRef",
|
{"name": "documentList", "type": "DocumentList", "required": False, "frontendType": "hidden",
|
||||||
"description": t("Optionale Dokumentenliste als Vorlage/Referenz"), "default": ""},
|
"description": t("Dokumente aus vorherigen Schritten"), "default": ""},
|
||||||
] + _AI_COMMON_PARAMS,
|
] + _AI_COMMON_PARAMS,
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
"inputPorts": {0: {"accepts": ["Transit", "AiResult", "DocumentList", "ActionResult"]}},
|
"inputPorts": {0: {"accepts": ["FormPayload", "Transit", "AiResult", "DocumentList", "ActionResult"]}},
|
||||||
"outputPorts": {0: {"schema": "DocumentList"}},
|
"outputPorts": {0: {"schema": "DocumentList"}},
|
||||||
"meta": {"icon": "mdi-file-plus", "color": "#9C27B0", "usesAi": True},
|
"meta": {"icon": "mdi-file-plus", "color": "#9C27B0", "usesAi": True},
|
||||||
"_method": "ai",
|
"_method": "ai",
|
||||||
|
|
@ -156,14 +156,14 @@ AI_NODES = [
|
||||||
{"name": "resultType", "type": "str", "required": False, "frontendType": "select",
|
{"name": "resultType", "type": "str", "required": False, "frontendType": "select",
|
||||||
"frontendOptions": {"options": ["py", "js", "ts", "html", "java", "cpp", "txt", "json", "csv", "xml"]},
|
"frontendOptions": {"options": ["py", "js", "ts", "html", "java", "cpp", "txt", "json", "csv", "xml"]},
|
||||||
"description": t("Datei-Endung der erzeugten Code-Datei"), "default": "py"},
|
"description": t("Datei-Endung der erzeugten Code-Datei"), "default": "py"},
|
||||||
{"name": "context", "type": "str", "required": False, "frontendType": "dataRef",
|
{"name": "context", "type": "Any", "required": False, "frontendType": "contextBuilder",
|
||||||
"description": t("Optionaler Kontext aus Upstream-Node (wird dem Prompt vorangestellt)"), "default": ""},
|
"description": t("Daten aus vorherigen Schritten"), "default": ""},
|
||||||
{"name": "documentList", "type": "DocumentList", "required": False, "frontendType": "dataRef",
|
{"name": "documentList", "type": "DocumentList", "required": False, "frontendType": "hidden",
|
||||||
"description": t("Optionale Dokumentenliste als Referenz"), "default": ""},
|
"description": t("Dokumente aus vorherigen Schritten"), "default": ""},
|
||||||
] + _AI_COMMON_PARAMS,
|
] + _AI_COMMON_PARAMS,
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
"inputPorts": {0: {"accepts": ["Transit", "AiResult", "DocumentList", "ActionResult"]}},
|
"inputPorts": {0: {"accepts": ["FormPayload", "Transit", "AiResult", "DocumentList", "ActionResult"]}},
|
||||||
"outputPorts": {0: {"schema": "AiResult"}},
|
"outputPorts": {0: {"schema": "AiResult"}},
|
||||||
"meta": {"icon": "mdi-code-tags", "color": "#9C27B0", "usesAi": True},
|
"meta": {"icon": "mdi-code-tags", "color": "#9C27B0", "usesAi": True},
|
||||||
"_method": "ai",
|
"_method": "ai",
|
||||||
|
|
|
||||||
|
|
@ -62,8 +62,8 @@ EMAIL_NODES = [
|
||||||
{"name": "connectionReference", "type": "str", "required": True, "frontendType": "userConnection",
|
{"name": "connectionReference", "type": "str", "required": True, "frontendType": "userConnection",
|
||||||
"frontendOptions": {"authority": "msft"},
|
"frontendOptions": {"authority": "msft"},
|
||||||
"description": t("E-Mail-Konto")},
|
"description": t("E-Mail-Konto")},
|
||||||
{"name": "context", "type": "str", "required": False, "frontendType": "templateTextarea",
|
{"name": "context", "type": "Any", "required": False, "frontendType": "templateTextarea",
|
||||||
"description": t("Kontext / Brief-Beschreibung für die KI-Komposition"), "default": ""},
|
"description": t("Daten aus vorherigen Schritten (oder direkte Beschreibung)"), "default": ""},
|
||||||
{"name": "to", "type": "str", "required": False, "frontendType": "text",
|
{"name": "to", "type": "str", "required": False, "frontendType": "text",
|
||||||
"description": t("Empfänger (komma-separiert, optional für Entwurf)"), "default": ""},
|
"description": t("Empfänger (komma-separiert, optional für Entwurf)"), "default": ""},
|
||||||
{"name": "documentList", "type": "str", "required": False, "frontendType": "hidden",
|
{"name": "documentList", "type": "str", "required": False, "frontendType": "hidden",
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,8 @@ FILE_NODES = [
|
||||||
{"name": "language", "type": "str", "required": False, "frontendType": "select",
|
{"name": "language", "type": "str", "required": False, "frontendType": "select",
|
||||||
"frontendOptions": {"options": ["de", "en", "fr"]},
|
"frontendOptions": {"options": ["de", "en", "fr"]},
|
||||||
"description": t("Sprache"), "default": "de"},
|
"description": t("Sprache"), "default": "de"},
|
||||||
{"name": "context", "type": "str", "required": False, "frontendType": "dataRef",
|
{"name": "context", "type": "Any", "required": False, "frontendType": "contextBuilder",
|
||||||
"description": t("Inhalt aus Upstream-Node (binden via DataRef oder Wire)"), "default": ""},
|
"description": t("Daten aus vorherigen Schritten"), "default": ""},
|
||||||
],
|
],
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ TRUSTEE_NODES = [
|
||||||
# is List[ActionDocument] (see datamodelChat.ActionResult). The
|
# is List[ActionDocument] (see datamodelChat.ActionResult). The
|
||||||
# DataPicker uses this string to filter compatible upstream paths.
|
# DataPicker uses this string to filter compatible upstream paths.
|
||||||
{"name": "documentList", "type": "List[ActionDocument]", "required": True, "frontendType": "dataRef",
|
{"name": "documentList", "type": "List[ActionDocument]", "required": True, "frontendType": "dataRef",
|
||||||
"description": t("Dokumentenliste — gebunden via DataRef.")},
|
"description": t("Dokumente aus vorherigen Schritten")},
|
||||||
dict(_TRUSTEE_INSTANCE_PARAM),
|
dict(_TRUSTEE_INSTANCE_PARAM),
|
||||||
],
|
],
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
|
|
@ -95,7 +95,7 @@ TRUSTEE_NODES = [
|
||||||
"description": t("Trustee-Positionen in Buchhaltungssystem übertragen."),
|
"description": t("Trustee-Positionen in Buchhaltungssystem übertragen."),
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{"name": "documentList", "type": "List[ActionDocument]", "required": True, "frontendType": "dataRef",
|
{"name": "documentList", "type": "List[ActionDocument]", "required": True, "frontendType": "dataRef",
|
||||||
"description": t("Verarbeitete Dokumentenliste — gebunden via DataRef.")},
|
"description": t("Dokumente aus vorherigen Schritten")},
|
||||||
dict(_TRUSTEE_INSTANCE_PARAM),
|
dict(_TRUSTEE_INSTANCE_PARAM),
|
||||||
],
|
],
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,8 @@ class PortField(BaseModel):
|
||||||
# FeatureInstanceRef.featureCode). Pickers/validators use it to filter compatible
|
# FeatureInstanceRef.featureCode). Pickers/validators use it to filter compatible
|
||||||
# producers by sub-type. Type must be "str" when discriminator is True.
|
# producers by sub-type. Type must be "str" when discriminator is True.
|
||||||
discriminator: bool = False
|
discriminator: bool = False
|
||||||
|
# Surfaces this field at the top of the DataPicker list as the most common pick.
|
||||||
|
recommended: bool = False
|
||||||
|
|
||||||
|
|
||||||
class PortSchema(BaseModel):
|
class PortSchema(BaseModel):
|
||||||
|
|
@ -153,7 +155,7 @@ PORT_TYPE_CATALOG: Dict[str, PortSchema] = {
|
||||||
]),
|
]),
|
||||||
"DocumentList": PortSchema(name="DocumentList", fields=[
|
"DocumentList": PortSchema(name="DocumentList", fields=[
|
||||||
PortField(name="documents", type="List[Document]",
|
PortField(name="documents", type="List[Document]",
|
||||||
description="Dokumentenliste"),
|
description="Dokumente aus vorherigen Schritten", recommended=True),
|
||||||
PortField(name="connection", type="ConnectionRef", required=False,
|
PortField(name="connection", type="ConnectionRef", required=False,
|
||||||
description="Verbindung, mit der die Liste erzeugt wurde"),
|
description="Verbindung, mit der die Liste erzeugt wurde"),
|
||||||
PortField(name="source", type="SharePointFolderRef", required=False,
|
PortField(name="source", type="SharePointFolderRef", required=False,
|
||||||
|
|
|
||||||
|
|
@ -398,5 +398,11 @@ def resolveParameterReferences(value: Any, nodeOutputs: Dict[str, Any]) -> Any:
|
||||||
return str(data) if data is not None else m.group(0)
|
return str(data) if data is not None else m.group(0)
|
||||||
return re.sub(r"\{\{\s*([^}]+)\s*\}\}", repl, value)
|
return re.sub(r"\{\{\s*([^}]+)\s*\}\}", repl, value)
|
||||||
if isinstance(value, list):
|
if isinstance(value, list):
|
||||||
|
# contextBuilder: list where every item is a `{"type":"ref",...}` envelope.
|
||||||
|
# Resolve each ref and join the serialised parts into a single prompt string.
|
||||||
|
if value and all(isinstance(v, dict) and v.get("type") == "ref" for v in value):
|
||||||
|
from modules.workflows.methods.methodAi._common import serialize_context
|
||||||
|
parts = [serialize_context(resolveParameterReferences(v, nodeOutputs)) for v in value]
|
||||||
|
return "\n\n".join(p for p in parts if p)
|
||||||
return [resolveParameterReferences(v, nodeOutputs) for v in value]
|
return [resolveParameterReferences(v, nodeOutputs) for v in value]
|
||||||
return value
|
return value
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,27 @@
|
||||||
|
|
||||||
"""Shared helpers for AI workflow actions."""
|
"""Shared helpers for AI workflow actions."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def serialize_context(val: Any) -> str:
|
||||||
|
"""Convert any context value to a readable string for use in AI prompts.
|
||||||
|
|
||||||
|
- None / empty string → ""
|
||||||
|
- str → as-is
|
||||||
|
- dict / list → pretty-printed JSON
|
||||||
|
- anything else → str()
|
||||||
|
"""
|
||||||
|
if val is None or val == "" or val == []:
|
||||||
|
return ""
|
||||||
|
if isinstance(val, str):
|
||||||
|
return val.strip()
|
||||||
|
try:
|
||||||
|
return json.dumps(val, ensure_ascii=False, indent=2)
|
||||||
|
except Exception:
|
||||||
|
return str(val)
|
||||||
|
|
||||||
|
|
||||||
def applyCommonAiParams(parameters: dict, request) -> None:
|
def applyCommonAiParams(parameters: dict, request) -> None:
|
||||||
"""Apply common AI parameters (requireNeutralization, allowedModels) from node to request."""
|
"""Apply common AI parameters (requireNeutralization, allowedModels) from node to request."""
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,10 @@ from modules.serviceCenter.services.serviceBilling.mainServiceBilling import Bil
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
async def generateCode(self, parameters: Dict[str, Any]) -> ActionResult:
|
async def generateCode(self, parameters: Dict[str, Any]) -> ActionResult:
|
||||||
base_prompt = parameters.get("prompt") or ""
|
from modules.workflows.methods.methodAi._common import serialize_context
|
||||||
context_val = parameters.get("context")
|
base_prompt = (parameters.get("prompt") or "").strip()
|
||||||
if context_val and isinstance(context_val, str) and context_val.strip():
|
context_val = serialize_context(parameters.get("context"))
|
||||||
prompt = f"Kontext:\n{context_val.strip()}\n\n{base_prompt.strip()}"
|
prompt = f"Kontext:\n{context_val}\n\n{base_prompt}" if context_val else base_prompt
|
||||||
else:
|
|
||||||
prompt = base_prompt
|
|
||||||
if not prompt.strip():
|
if not prompt.strip():
|
||||||
return ActionResult.isFailure(error="prompt is required")
|
return ActionResult.isFailure(error="prompt is required")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,10 @@ from modules.serviceCenter.services.serviceBilling.mainServiceBilling import Bil
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
async def generateDocument(self, parameters: Dict[str, Any]) -> ActionResult:
|
async def generateDocument(self, parameters: Dict[str, Any]) -> ActionResult:
|
||||||
base_prompt = parameters.get("prompt") or ""
|
from modules.workflows.methods.methodAi._common import serialize_context
|
||||||
context_val = parameters.get("context")
|
base_prompt = (parameters.get("prompt") or "").strip()
|
||||||
if context_val and isinstance(context_val, str) and context_val.strip():
|
context_val = serialize_context(parameters.get("context"))
|
||||||
prompt = f"Kontext:\n{context_val.strip()}\n\n{base_prompt.strip()}"
|
prompt = f"Kontext:\n{context_val}\n\n{base_prompt}" if context_val else base_prompt
|
||||||
else:
|
|
||||||
prompt = base_prompt
|
|
||||||
if not prompt.strip():
|
if not prompt.strip():
|
||||||
return ActionResult.isFailure(error="prompt is required")
|
return ActionResult.isFailure(error="prompt is required")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -220,17 +220,12 @@ async def process(self, parameters: Dict[str, Any]) -> ActionResult:
|
||||||
mimeMap = {"txt": "text/plain", "json": "application/json", "html": "text/html", "md": "text/markdown", "csv": "text/csv", "xml": "application/xml"}
|
mimeMap = {"txt": "text/plain", "json": "application/json", "html": "text/html", "md": "text/markdown", "csv": "text/csv", "xml": "application/xml"}
|
||||||
output_mime_type = mimeMap.get(normalized_result_type, "text/plain") if normalized_result_type else "text/plain"
|
output_mime_type = mimeMap.get(normalized_result_type, "text/plain") if normalized_result_type else "text/plain"
|
||||||
|
|
||||||
# Normalize context: workflow refs may resolve to dict/list instead of str
|
# Normalize context: serialize any non-string value (dict/list/int/…) to text
|
||||||
paramContext = parameters.get("context")
|
from modules.workflows.methods.methodAi._common import serialize_context
|
||||||
if paramContext is not None and not isinstance(paramContext, str):
|
paramContext = serialize_context(parameters.get("context"))
|
||||||
try:
|
parameters["context"] = paramContext
|
||||||
paramContext = json.dumps(paramContext, ensure_ascii=False, default=str)
|
if paramContext:
|
||||||
parameters["context"] = paramContext
|
logger.info(f"ai.process: context serialized ({len(paramContext)} chars)")
|
||||||
logger.info(f"ai.process: Serialized non-string context ({type(parameters.get('context')).__name__}) to JSON ({len(paramContext)} chars)")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"ai.process: Failed to serialize context: {e}")
|
|
||||||
paramContext = str(paramContext)
|
|
||||||
parameters["context"] = paramContext
|
|
||||||
|
|
||||||
# Phase 7.3: Pass documentList and/or contentParts to AI service
|
# Phase 7.3: Pass documentList and/or contentParts to AI service
|
||||||
contentParts: Optional[List[ContentPart]] = inline_content_parts
|
contentParts: Optional[List[ContentPart]] = inline_content_parts
|
||||||
|
|
@ -257,7 +252,7 @@ async def process(self, parameters: Dict[str, Any]) -> ActionResult:
|
||||||
self.services.chat.progressLogUpdate(operationId, 0.6, "Calling AI (simple mode)")
|
self.services.chat.progressLogUpdate(operationId, 0.6, "Calling AI (simple mode)")
|
||||||
|
|
||||||
context_parts = []
|
context_parts = []
|
||||||
paramContext = parameters.get("context")
|
paramContext = parameters.get("context") # already serialized above
|
||||||
if paramContext and isinstance(paramContext, str) and paramContext.strip():
|
if paramContext and isinstance(paramContext, str) and paramContext.strip():
|
||||||
context_parts.append(paramContext.strip())
|
context_parts.append(paramContext.strip())
|
||||||
if documentList and len(documentList.references) > 0:
|
if documentList and len(documentList.references) > 0:
|
||||||
|
|
|
||||||
|
|
@ -15,15 +15,15 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def _build_research_prompt(parameters: Dict[str, Any]) -> str:
|
def _build_research_prompt(parameters: Dict[str, Any]) -> str:
|
||||||
"""Assemble the final research prompt from prompt + optional context/documentList."""
|
"""Assemble the final research prompt from prompt + optional context/documentList."""
|
||||||
|
from modules.workflows.methods.methodAi._common import serialize_context
|
||||||
base_prompt = (parameters.get("prompt") or "").strip()
|
base_prompt = (parameters.get("prompt") or "").strip()
|
||||||
context_val = parameters.get("context")
|
context_val = serialize_context(parameters.get("context"))
|
||||||
doc_list = parameters.get("documentList")
|
doc_list = parameters.get("documentList")
|
||||||
|
|
||||||
parts: list[str] = []
|
parts: list[str] = []
|
||||||
|
|
||||||
# Prepend context string if provided
|
if context_val:
|
||||||
if context_val and isinstance(context_val, str) and context_val.strip():
|
parts.append(f"Kontext:\n{context_val}")
|
||||||
parts.append(f"Kontext:\n{context_val.strip()}")
|
|
||||||
|
|
||||||
# Extract text from documentList items if provided
|
# Extract text from documentList items if provided
|
||||||
if doc_list:
|
if doc_list:
|
||||||
|
|
|
||||||
|
|
@ -74,10 +74,9 @@ async def create(self, parameters: Dict[str, Any]) -> ActionResult:
|
||||||
Create a file from context (text/markdown from upstream AI node).
|
Create a file from context (text/markdown from upstream AI node).
|
||||||
Uses GenerationService.renderReport to produce docx, pdf, txt, md, html, xlsx, etc.
|
Uses GenerationService.renderReport to produce docx, pdf, txt, md, html, xlsx, etc.
|
||||||
"""
|
"""
|
||||||
context = parameters.get("context", "") or parameters.get("text", "") or ""
|
from modules.workflows.methods.methodAi._common import serialize_context
|
||||||
if not isinstance(context, str):
|
raw_context = parameters.get("context", "") or parameters.get("text", "") or ""
|
||||||
context = str(context) if context else ""
|
context = serialize_context(raw_context)
|
||||||
context = context.strip()
|
|
||||||
|
|
||||||
if not context:
|
if not context:
|
||||||
return ActionResult.isFailure(error="context is required (connect an AI node or provide text)")
|
return ActionResult.isFailure(error="context is required (connect an AI node or provide text)")
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,8 @@ async def composeAndDraftEmailWithContext(self, parameters: Dict[str, Any]) -> A
|
||||||
try:
|
try:
|
||||||
connectionReference = parameters.get("connectionReference")
|
connectionReference = parameters.get("connectionReference")
|
||||||
to = parameters.get("to") or [] # Optional for drafts - can save draft without recipients
|
to = parameters.get("to") or [] # Optional for drafts - can save draft without recipients
|
||||||
context = parameters.get("context")
|
from modules.workflows.methods.methodAi._common import serialize_context
|
||||||
|
context = serialize_context(parameters.get("context")) or None
|
||||||
documentList = parameters.get("documentList") or []
|
documentList = parameters.get("documentList") or []
|
||||||
replySourceDocuments = parameters.get("replySourceDocuments") or [] # Original email(s) for reply attachment
|
replySourceDocuments = parameters.get("replySourceDocuments") or [] # Original email(s) for reply attachment
|
||||||
# ``attachments`` (added in 2026-04 for the PWG pilot) is a list of
|
# ``attachments`` (added in 2026-04 for the PWG pilot) is a list of
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue