mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-22 08:38:13 +02:00
feat: add workflow graph constraints fixtures
This commit is contained in:
parent
6d93be3ef6
commit
5a358d4d29
38 changed files with 447 additions and 49 deletions
|
|
@ -26,7 +26,7 @@ from api.services.pipecat.service_factory import (
|
|||
)
|
||||
from api.services.workflow.dto import ReactFlowDTO
|
||||
from api.services.workflow.pipecat_engine import PipecatEngine
|
||||
from api.services.workflow.workflow import WorkflowGraph
|
||||
from api.services.workflow.workflow_graph import WorkflowGraph
|
||||
|
||||
|
||||
class LoopTalkPipelineBuilder:
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ from api.services.pipecat.ws_sender_registry import get_ws_sender
|
|||
from api.services.telephony import registry as telephony_registry
|
||||
from api.services.workflow.dto import ReactFlowDTO
|
||||
from api.services.workflow.pipecat_engine import PipecatEngine
|
||||
from api.services.workflow.workflow import WorkflowGraph
|
||||
from api.services.workflow.workflow_graph import WorkflowGraph
|
||||
from pipecat.audio.turn.smart_turn.base_smart_turn import SmartTurnParams
|
||||
from pipecat.audio.turn.smart_turn.local_smart_turn_v3 import LocalSmartTurnAnalyzerV3
|
||||
from pipecat.audio.vad.silero import SileroVADAnalyzer
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ transcript after completion."""
|
|||
|
||||
from api.services.workflow.node_specs._base import (
|
||||
DisplayOptions,
|
||||
GraphConstraints,
|
||||
NodeCategory,
|
||||
NodeExample,
|
||||
NodeSpec,
|
||||
|
|
@ -193,4 +194,10 @@ SPEC = NodeSpec(
|
|||
},
|
||||
),
|
||||
],
|
||||
# QA runs post-call against the saved transcript (run_integrations
|
||||
# scans by type), never as a graph step. Reject any edge into or out
|
||||
# of a QA node.
|
||||
graph_constraints=GraphConstraints(
|
||||
min_incoming=0, max_incoming=0, min_outgoing=0, max_outgoing=0
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -240,9 +240,11 @@ SPEC = NodeSpec(
|
|||
},
|
||||
),
|
||||
],
|
||||
# `min_outgoing` is intentionally unset: a startCall is allowed to
|
||||
# sit on the canvas without an outgoing edge (e.g. a workflow with
|
||||
# just a greeting). Only constraint: nothing flows INTO the start.
|
||||
graph_constraints=GraphConstraints(
|
||||
min_incoming=0,
|
||||
max_incoming=0,
|
||||
min_outgoing=1,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
after the workflow completes."""
|
||||
|
||||
from api.services.workflow.node_specs._base import (
|
||||
GraphConstraints,
|
||||
NodeCategory,
|
||||
NodeExample,
|
||||
NodeSpec,
|
||||
|
|
@ -132,4 +133,10 @@ SPEC = NodeSpec(
|
|||
},
|
||||
),
|
||||
],
|
||||
# Webhooks fire post-call (run_integrations scans nodes by type),
|
||||
# never as a graph step. Reject any edge into or out of a webhook so
|
||||
# the editor can't wire one into the conversation flow.
|
||||
graph_constraints=GraphConstraints(
|
||||
min_incoming=0, max_incoming=0, min_outgoing=0, max_outgoing=0
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ from pipecat.utils.enums import EndTaskReason
|
|||
from api.db import db_client
|
||||
from api.services.pipecat.audio_playback import play_audio
|
||||
from api.services.workflow.disposition_mapper import apply_disposition_mapping
|
||||
from api.services.workflow.workflow import Node, WorkflowGraph
|
||||
from api.services.workflow.workflow_graph import Node, WorkflowGraph
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pipecat.frames.frames import Frame
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Callable, Optional
|
|||
|
||||
if TYPE_CHECKING:
|
||||
from api.services.workflow.pipecat_engine_custom_tools import CustomToolManager
|
||||
from api.services.workflow.workflow import Node, WorkflowGraph
|
||||
from api.services.workflow.workflow_graph import Node, WorkflowGraph
|
||||
|
||||
from api.services.workflow.pipecat_engine_custom_tools import get_function_schema
|
||||
from api.services.workflow.tools.knowledge_base import get_knowledge_base_tool
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from typing import Dict, List, Set
|
|||
|
||||
from api.services.workflow.dto import EdgeDataDTO, NodeType, ReactFlowDTO
|
||||
from api.services.workflow.errors import ItemKind, WorkflowError
|
||||
from api.services.workflow.node_specs import REGISTRY
|
||||
|
||||
# Regex for matching {{ variable }} template placeholders.
|
||||
# Captures: group(1) = variable path, group(2) = filter name, group(3) = filter value.
|
||||
|
|
@ -259,41 +260,67 @@ class WorkflowGraph:
|
|||
return errors
|
||||
|
||||
def _assert_connection_counts(self):
|
||||
"""Enforce per-type incoming/outgoing edge constraints.
|
||||
|
||||
Driven by `NodeSpec.graph_constraints` so a single source of truth
|
||||
in the spec dictates what's legal. Types without a `graph_constraints`
|
||||
block are unconstrained (e.g. agentNode on the outgoing side).
|
||||
"""
|
||||
errors: list[WorkflowError] = []
|
||||
|
||||
out_deg = Counter()
|
||||
in_deg = Counter()
|
||||
for n in self.nodes.values(): # init counters
|
||||
for n in self.nodes.values():
|
||||
out_deg[n.id] = in_deg[n.id] = 0
|
||||
for src, n in self.nodes.items(): # compute degrees
|
||||
for src, n in self.nodes.items():
|
||||
for m in n.out.values():
|
||||
out_deg[src] += 1
|
||||
in_deg[m.id] += 1
|
||||
|
||||
for n in self.nodes.values():
|
||||
spec = REGISTRY.get(n.node_type.value)
|
||||
if spec is None or spec.graph_constraints is None:
|
||||
continue
|
||||
gc = spec.graph_constraints
|
||||
in_d, out_d = in_deg[n.id], out_deg[n.id]
|
||||
label = spec.display_name
|
||||
|
||||
match n.node_type:
|
||||
case NodeType.endNode:
|
||||
if in_d < 1 or out_d != 0:
|
||||
errors.append(
|
||||
WorkflowError(
|
||||
kind=ItemKind.node,
|
||||
id=n.id,
|
||||
field=None,
|
||||
message=f"EndNode must have at least 1 incoming edge",
|
||||
)
|
||||
)
|
||||
case NodeType.agentNode:
|
||||
if in_d < 1:
|
||||
errors.append(
|
||||
WorkflowError(
|
||||
kind=ItemKind.node,
|
||||
id=n.id,
|
||||
field=None,
|
||||
message=f"Worker must have at least 1 incoming edge",
|
||||
)
|
||||
)
|
||||
if gc.max_incoming is not None and in_d > gc.max_incoming:
|
||||
msg = (
|
||||
f"{label} cannot have incoming edges"
|
||||
if gc.max_incoming == 0
|
||||
else f"{label} can have at most {gc.max_incoming} incoming edge(s)"
|
||||
)
|
||||
errors.append(
|
||||
WorkflowError(kind=ItemKind.node, id=n.id, field=None, message=msg)
|
||||
)
|
||||
if gc.min_incoming is not None and in_d < gc.min_incoming:
|
||||
errors.append(
|
||||
WorkflowError(
|
||||
kind=ItemKind.node,
|
||||
id=n.id,
|
||||
field=None,
|
||||
message=f"{label} must have at least {gc.min_incoming} incoming edge(s)",
|
||||
)
|
||||
)
|
||||
if gc.max_outgoing is not None and out_d > gc.max_outgoing:
|
||||
msg = (
|
||||
f"{label} cannot have outgoing edges"
|
||||
if gc.max_outgoing == 0
|
||||
else f"{label} can have at most {gc.max_outgoing} outgoing edge(s)"
|
||||
)
|
||||
errors.append(
|
||||
WorkflowError(kind=ItemKind.node, id=n.id, field=None, message=msg)
|
||||
)
|
||||
if gc.min_outgoing is not None and out_d < gc.min_outgoing:
|
||||
errors.append(
|
||||
WorkflowError(
|
||||
kind=ItemKind.node,
|
||||
id=n.id,
|
||||
field=None,
|
||||
message=f"{label} must have at least {gc.min_outgoing} outgoing edge(s)",
|
||||
)
|
||||
)
|
||||
|
||||
return errors
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue