Update workflow feature with codex

This commit is contained in:
Xin Wang
2026-02-10 08:12:46 +08:00
parent 6b4391c423
commit bbeffa89ed
8 changed files with 1334 additions and 39 deletions

View File

@@ -1,7 +1,7 @@
from datetime import datetime
from enum import Enum
from typing import List, Optional
from pydantic import BaseModel
from typing import Any, Dict, List, Optional
from pydantic import BaseModel, ConfigDict, Field, model_validator
# ============ Enums ============
@@ -410,24 +410,82 @@ class KnowledgeStats(BaseModel):
# ============ Workflow ============
class WorkflowNode(BaseModel):
name: str
type: str
model_config = ConfigDict(extra="allow")
id: Optional[str] = None
name: str = ""
type: str = "assistant"
isStart: Optional[bool] = None
metadata: dict
metadata: Dict[str, Any] = Field(default_factory=dict)
prompt: Optional[str] = None
messagePlan: Optional[dict] = None
variableExtractionPlan: Optional[dict] = None
tool: Optional[dict] = None
globalNodePlan: Optional[dict] = None
messagePlan: Optional[Dict[str, Any]] = None
variableExtractionPlan: Optional[Dict[str, Any]] = None
tool: Optional[Dict[str, Any]] = None
globalNodePlan: Optional[Dict[str, Any]] = None
assistantId: Optional[str] = None
assistant: Optional[Dict[str, Any]] = None
@model_validator(mode="before")
@classmethod
def _normalize_legacy_node(cls, data: Any) -> Any:
if not isinstance(data, dict):
return data
raw = dict(data)
node_id = raw.get("id") or raw.get("name")
if not node_id:
node_id = f"node_{abs(hash(str(raw))) % 100000}"
raw["id"] = str(node_id)
raw["name"] = str(raw.get("name") or raw["id"])
node_type = str(raw.get("type") or "assistant").lower()
if node_type == "conversation":
node_type = "assistant"
elif node_type == "human":
node_type = "human_transfer"
elif node_type not in {"start", "assistant", "tool", "human_transfer", "end"}:
node_type = "assistant"
raw["type"] = node_type
metadata = raw.get("metadata")
if not isinstance(metadata, dict):
metadata = {}
if "position" not in metadata and isinstance(raw.get("position"), dict):
metadata["position"] = raw.get("position")
raw["metadata"] = metadata
if raw.get("isStart") is None and node_type == "start":
raw["isStart"] = True
return raw
class WorkflowEdge(BaseModel):
from_: str
to: str
label: Optional[str] = None
model_config = ConfigDict(extra="allow")
class Config:
populate_by_name = True
id: Optional[str] = None
fromNodeId: str
toNodeId: str
label: Optional[str] = None
condition: Optional[Dict[str, Any]] = None
priority: int = 100
@model_validator(mode="before")
@classmethod
def _normalize_legacy_edge(cls, data: Any) -> Any:
if not isinstance(data, dict):
return data
raw = dict(data)
from_node = raw.get("fromNodeId") or raw.get("from") or raw.get("from_") or raw.get("source")
to_node = raw.get("toNodeId") or raw.get("to") or raw.get("target")
raw["fromNodeId"] = str(from_node or "")
raw["toNodeId"] = str(to_node or "")
if raw.get("id") is None:
raw["id"] = f"e_{raw['fromNodeId']}_{raw['toNodeId']}"
if raw.get("condition") is None:
if raw.get("label"):
raw["condition"] = {"type": "contains", "source": "user", "value": str(raw["label"])}
else:
raw["condition"] = {"type": "always"}
return raw
class WorkflowBase(BaseModel):
@@ -436,29 +494,85 @@ class WorkflowBase(BaseModel):
createdAt: str = ""
updatedAt: str = ""
globalPrompt: Optional[str] = None
nodes: List[dict] = []
edges: List[dict] = []
nodes: List[WorkflowNode] = Field(default_factory=list)
edges: List[WorkflowEdge] = Field(default_factory=list)
class WorkflowCreate(WorkflowBase):
pass
@model_validator(mode="after")
def _validate_graph(self) -> "WorkflowCreate":
_validate_workflow_graph(self.nodes, self.edges)
return self
class WorkflowUpdate(BaseModel):
name: Optional[str] = None
nodeCount: Optional[int] = None
nodes: Optional[List[dict]] = None
edges: Optional[List[dict]] = None
nodes: Optional[List[WorkflowNode]] = None
edges: Optional[List[WorkflowEdge]] = None
globalPrompt: Optional[str] = None
@model_validator(mode="after")
def _validate_partial_graph(self) -> "WorkflowUpdate":
if self.nodes is not None and self.edges is not None:
_validate_workflow_graph(self.nodes, self.edges)
return self
class WorkflowOut(WorkflowBase):
id: str
@model_validator(mode="before")
@classmethod
def _normalize_db_fields(cls, data: Any) -> Any:
if isinstance(data, dict):
raw = dict(data)
else:
raw = {
"id": getattr(data, "id", None),
"name": getattr(data, "name", None),
"node_count": getattr(data, "node_count", None),
"created_at": getattr(data, "created_at", None),
"updated_at": getattr(data, "updated_at", None),
"global_prompt": getattr(data, "global_prompt", None),
"nodes": getattr(data, "nodes", None),
"edges": getattr(data, "edges", None),
}
if "nodeCount" not in raw and raw.get("node_count") is not None:
raw["nodeCount"] = raw["node_count"]
if "createdAt" not in raw and raw.get("created_at") is not None:
raw["createdAt"] = raw["created_at"]
if "updatedAt" not in raw and raw.get("updated_at") is not None:
raw["updatedAt"] = raw["updated_at"]
if "globalPrompt" not in raw and raw.get("global_prompt") is not None:
raw["globalPrompt"] = raw["global_prompt"]
return raw
class Config:
from_attributes = True
def _validate_workflow_graph(nodes: List[WorkflowNode], edges: List[WorkflowEdge]) -> None:
if not nodes:
raise ValueError("Workflow must include at least one node")
node_ids = [node.id for node in nodes if node.id]
if len(node_ids) != len(set(node_ids)):
raise ValueError("Workflow node ids must be unique")
starts = [node for node in nodes if node.isStart or node.type == "start"]
if not starts:
raise ValueError("Workflow must define a start node (isStart=true or type=start)")
known = set(node_ids)
for edge in edges:
if edge.fromNodeId not in known:
raise ValueError(f"Workflow edge fromNodeId not found: {edge.fromNodeId}")
if edge.toNodeId not in known:
raise ValueError(f"Workflow edge toNodeId not found: {edge.toNodeId}")
# ============ Call Record ============
class TranscriptSegment(BaseModel):
turnIndex: int