Changeset 0.35.0 (#209)
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
"chat-switchboard/models"
|
||||
"chat-switchboard/store"
|
||||
"chat-switchboard/webhook"
|
||||
"chat-switchboard/workflow"
|
||||
)
|
||||
|
||||
// ── Late Registration ────────────────────────
|
||||
@@ -22,7 +23,8 @@ func RegisterWorkflowTools(stores store.Stores) {
|
||||
return
|
||||
}
|
||||
Register(&workflowAdvanceTool{stores: stores})
|
||||
log.Println("✅ workflow tools registered (workflow_advance)")
|
||||
Register(&workflowRouteTool{stores: stores})
|
||||
log.Println("✅ workflow tools registered (workflow_advance, workflow_route)")
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
@@ -98,7 +100,10 @@ func (t *workflowAdvanceTool) Execute(ctx context.Context, execCtx ExecutionCont
|
||||
|
||||
// Merge collected data into stage_data
|
||||
mergedData := MergeWorkflowStageData(ctx, t.stores.Channels, channelID, args.Data)
|
||||
nextStage := currentStage + 1
|
||||
nextStage, err := workflow.ResolveNextStage(stages, currentStage, json.RawMessage(mergedData))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("routing error: %w", err)
|
||||
}
|
||||
|
||||
if nextStage >= len(stages) {
|
||||
// Workflow complete
|
||||
@@ -151,6 +156,117 @@ func (t *workflowAdvanceTool) Execute(ctx context.Context, execCtx ExecutionCont
|
||||
return string(result), nil
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// workflow_route (v0.35.0)
|
||||
// ═══════════════════════════════════════════
|
||||
//
|
||||
// Called by a stage persona to route the workflow to a specific named stage.
|
||||
// Enables AI-triggered routing (escalation, correction loops) and
|
||||
// conditional branching driven by persona judgment.
|
||||
|
||||
type workflowRouteTool struct {
|
||||
BaseTool
|
||||
stores store.Stores
|
||||
}
|
||||
|
||||
func (t *workflowRouteTool) Availability() Require { return RequireWorkflow }
|
||||
|
||||
func (t *workflowRouteTool) Definition() ToolDef {
|
||||
return ToolDef{
|
||||
Name: "workflow_route",
|
||||
DisplayName: "Route Workflow",
|
||||
Category: "workflow",
|
||||
Description: "Route the workflow to a specific stage by name. Use this when the " +
|
||||
"conversation should jump to a particular stage instead of advancing sequentially. " +
|
||||
"Common patterns: escalation to human review, loop back for correction, " +
|
||||
"skip stages based on collected information.",
|
||||
Parameters: JSONSchema(map[string]interface{}{
|
||||
"target_stage": Prop("string", "Name of the stage to route to (case-insensitive)."),
|
||||
"reason": Prop("string", "Brief explanation of why this routing decision was made."),
|
||||
}, []string{"target_stage", "reason"}),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *workflowRouteTool) Execute(ctx context.Context, execCtx ExecutionContext, argsJSON string) (string, error) {
|
||||
var args struct {
|
||||
TargetStage string `json:"target_stage"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
|
||||
return "", fmt.Errorf("invalid arguments: %w", err)
|
||||
}
|
||||
if args.TargetStage == "" {
|
||||
return "", fmt.Errorf("target_stage is required")
|
||||
}
|
||||
|
||||
channelID := execCtx.ChannelID
|
||||
if channelID == "" {
|
||||
return "", fmt.Errorf("no channel context")
|
||||
}
|
||||
|
||||
ws, err := t.stores.Channels.GetWorkflowStatus(ctx, channelID)
|
||||
if err != nil || ws == nil || ws.WorkflowID == nil {
|
||||
return "", fmt.Errorf("not a workflow channel")
|
||||
}
|
||||
if ws.Status != "active" {
|
||||
return "", fmt.Errorf("workflow is %s, cannot route", ws.Status)
|
||||
}
|
||||
|
||||
stages, err := t.stores.Workflows.ListStages(ctx, *ws.WorkflowID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to load stages: %w", err)
|
||||
}
|
||||
|
||||
targetOrdinal, err := workflow.ResolveStageByName(stages, args.TargetStage)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot resolve target: %w", err)
|
||||
}
|
||||
|
||||
// Record routing decision in stage_data
|
||||
routeEntry, _ := json.Marshal(map[string]any{
|
||||
"from": ws.CurrentStage, "to": targetOrdinal,
|
||||
"reason": args.Reason, "ts": time.Now().UTC().Format(time.RFC3339),
|
||||
})
|
||||
historyPatch, _ := json.Marshal(map[string]any{
|
||||
"_route_history_latest": json.RawMessage(routeEntry),
|
||||
})
|
||||
mergedData := MergeWorkflowStageData(ctx, t.stores.Channels, channelID, json.RawMessage(historyPatch))
|
||||
|
||||
if targetOrdinal >= len(stages) {
|
||||
// Route beyond last stage = complete
|
||||
err = t.stores.Channels.CompleteWorkflow(ctx, channelID, targetOrdinal, json.RawMessage(mergedData))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to complete workflow: %w", err)
|
||||
}
|
||||
go TriggerWorkflowOnComplete(ctx, t.stores, *ws.WorkflowID, channelID, mergedData)
|
||||
result, _ := json.Marshal(map[string]any{
|
||||
"status": "completed", "message": "Workflow completed via routing.",
|
||||
})
|
||||
return string(result), nil
|
||||
}
|
||||
|
||||
err = t.stores.Channels.AdvanceWorkflowStage(ctx, channelID, targetOrdinal, json.RawMessage(mergedData))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to route: %w", err)
|
||||
}
|
||||
|
||||
targetDef := stages[targetOrdinal]
|
||||
|
||||
// Create assignment if target stage has an assignment team
|
||||
if targetDef.AssignmentTeamID != nil {
|
||||
CreateWorkflowAssignment(ctx, t.stores, channelID, targetOrdinal, *targetDef.AssignmentTeamID)
|
||||
}
|
||||
|
||||
result, _ := json.Marshal(map[string]any{
|
||||
"status": "active",
|
||||
"current_stage": targetOrdinal,
|
||||
"stage_name": targetDef.Name,
|
||||
"stage_mode": targetDef.StageMode,
|
||||
"message": fmt.Sprintf("Routed to stage %d: %s (reason: %s)", targetOrdinal, targetDef.Name, args.Reason),
|
||||
})
|
||||
return string(result), nil
|
||||
}
|
||||
|
||||
// ── Shared helpers (used by both tool and handler) ─
|
||||
|
||||
// MergeWorkflowStageData reads current stage_data, merges new data in Go, returns JSON string.
|
||||
|
||||
Reference in New Issue
Block a user