Update workflow feature with codex
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user