Changeset 0.29.0 (#195)

This commit is contained in:
2026-03-17 16:28:47 +00:00
parent 128cbb8174
commit 5d637d3a90
129 changed files with 9418 additions and 3016 deletions

View File

@@ -7,7 +7,6 @@ import (
"log"
"time"
"git.gobha.me/xcaliber/chat-switchboard/database"
"git.gobha.me/xcaliber/chat-switchboard/models"
"git.gobha.me/xcaliber/chat-switchboard/store"
"git.gobha.me/xcaliber/chat-switchboard/webhook"
@@ -79,18 +78,13 @@ func (t *workflowAdvanceTool) Execute(ctx context.Context, execCtx ExecutionCont
}
// Read current workflow state
var workflowID string
var currentStage int
var status string
var wfID *string
err := database.DB.QueryRowContext(ctx, database.Q(`
SELECT workflow_id, COALESCE(current_stage, 0), COALESCE(workflow_status, 'active')
FROM channels WHERE id = $1 AND type = 'workflow'
`), channelID).Scan(&wfID, &currentStage, &status)
if err != nil || wfID == nil {
ws, err := t.stores.Channels.GetWorkflowStatus(ctx, channelID)
if err != nil || ws == nil || ws.WorkflowID == nil {
return "", fmt.Errorf("not a workflow channel")
}
workflowID = *wfID
workflowID := *ws.WorkflowID
currentStage := ws.CurrentStage
status := ws.Status
if status != "active" {
return "", fmt.Errorf("workflow is %s, cannot advance", status)
@@ -103,17 +97,12 @@ func (t *workflowAdvanceTool) Execute(ctx context.Context, execCtx ExecutionCont
}
// Merge collected data into stage_data
mergedData := MergeWorkflowStageData(ctx, channelID, args.Data)
mergedData := MergeWorkflowStageData(ctx, t.stores.Channels, channelID, args.Data)
nextStage := currentStage + 1
if nextStage >= len(stages) {
// Workflow complete
_, err = database.DB.ExecContext(ctx, database.Q(`
UPDATE channels
SET current_stage = $1, workflow_status = 'completed',
stage_data = $2, last_activity_at = $3, ai_mode = 'off'
WHERE id = $4
`), nextStage, mergedData, time.Now().UTC(), channelID)
err = t.stores.Channels.CompleteWorkflow(ctx, channelID, nextStage, json.RawMessage(mergedData))
if err != nil {
return "", fmt.Errorf("failed to complete workflow: %w", err)
}
@@ -133,11 +122,7 @@ func (t *workflowAdvanceTool) Execute(ctx context.Context, execCtx ExecutionCont
}
// Advance to next stage
_, err = database.DB.ExecContext(ctx, database.Q(`
UPDATE channels
SET current_stage = $1, stage_data = $2, last_activity_at = $3
WHERE id = $4
`), nextStage, mergedData, time.Now().UTC(), channelID)
err = t.stores.Channels.AdvanceWorkflowStage(ctx, channelID, nextStage, json.RawMessage(mergedData))
if err != nil {
return "", fmt.Errorf("failed to advance: %w", err)
}
@@ -148,7 +133,7 @@ func (t *workflowAdvanceTool) Execute(ctx context.Context, execCtx ExecutionCont
// Create assignment if next stage has an assignment team
nextStageDef := stages[nextStage]
if nextStageDef.AssignmentTeamID != nil {
CreateWorkflowAssignment(ctx, channelID, nextStage, *nextStageDef.AssignmentTeamID)
CreateWorkflowAssignment(ctx, t.stores, channelID, nextStage, *nextStageDef.AssignmentTeamID)
}
result, _ := json.Marshal(map[string]interface{}{
@@ -163,11 +148,9 @@ func (t *workflowAdvanceTool) Execute(ctx context.Context, execCtx ExecutionCont
// ── Shared helpers (used by both tool and handler) ─
// MergeWorkflowStageData reads current stage_data, merges new data in Go, returns JSON string.
func MergeWorkflowStageData(ctx context.Context, channelID string, newData json.RawMessage) string {
var existing []byte
_ = database.DB.QueryRowContext(ctx, database.Q(`
SELECT COALESCE(stage_data, '{}') FROM channels WHERE id = $1
`), channelID).Scan(&existing)
// v0.29.0: channelStore parameter replaces raw database.DB access.
func MergeWorkflowStageData(ctx context.Context, channelStore store.ChannelStore, channelID string, newData json.RawMessage) string {
existing, _ := channelStore.GetStageData(ctx, channelID)
if len(newData) == 0 || string(newData) == "null" {
if len(existing) == 0 {
@@ -204,10 +187,11 @@ func CreateWorkflowStageNote(ctx context.Context, stores store.Stores, channelID
content := string(data)
// Find workflow channel owner for the note's user_id
var userID string
_ = database.DB.QueryRowContext(ctx, database.Q(`
SELECT user_id FROM channels WHERE id = $1
`), channelID).Scan(&userID)
ch, err := stores.Channels.GetByID(ctx, channelID)
if err != nil || ch == nil {
return
}
userID := ch.UserID
if userID == "" {
return
}
@@ -226,28 +210,18 @@ func CreateWorkflowStageNote(ctx context.Context, stores store.Stores, channelID
// CreateWorkflowAssignment creates an unassigned workflow assignment entry.
// Returns the assignment ID (for round-robin auto-assignment).
func CreateWorkflowAssignment(ctx context.Context, channelID string, stage int, teamID string) string {
id := store.NewID()
if database.IsSQLite() {
_, err := database.DB.ExecContext(ctx, `
INSERT INTO workflow_assignments (id, channel_id, stage, team_id)
VALUES (?, ?, ?, ?)
`, id, channelID, stage, teamID)
if err != nil {
log.Printf("⚠️ Failed to create workflow assignment: %v", err)
return ""
}
return id
// v0.29.0: accepts stores parameter, delegates to WorkflowStore.
func CreateWorkflowAssignment(ctx context.Context, stores store.Stores, channelID string, stage int, teamID string) string {
a := &store.WorkflowAssignment{
ChannelID: channelID,
Stage: stage,
TeamID: teamID,
}
_, err := database.DB.ExecContext(ctx, `
INSERT INTO workflow_assignments (id, channel_id, stage, team_id)
VALUES ($1, $2, $3, $4)
`, id, channelID, stage, teamID)
if err != nil {
if err := stores.Workflows.CreateAssignment(ctx, a); err != nil {
log.Printf("⚠️ Failed to create workflow assignment: %v", err)
return ""
}
return id
return a.ID
}
// ── Workflow Completion Triggers (v0.27.3) ──
@@ -307,10 +281,11 @@ func TriggerWorkflowOnComplete(ctx context.Context, stores store.Stores, workflo
targetData = sourceData
}
var ownerID string
_ = database.DB.QueryRowContext(ctx, database.Q(
`SELECT user_id FROM channels WHERE id = $1`,
), channelID).Scan(&ownerID)
ch2, err2 := stores.Channels.GetByID(ctx, channelID)
if err2 != nil || ch2 == nil {
return
}
ownerID := ch2.UserID
if ownerID == "" {
return
}
@@ -338,17 +313,12 @@ func TriggerWorkflowOnComplete(ctx context.Context, stores store.Stores, workflo
}
initialData, _ := json.Marshal(targetData)
_, err = database.DB.ExecContext(ctx, database.Q(`
UPDATE channels
SET workflow_id = $1, workflow_version = $2, current_stage = 0,
stage_data = $3, workflow_status = 'active',
last_activity_at = $4, ai_mode = 'auto'
WHERE id = $5
`), target.ID, ver.VersionNumber, string(initialData), time.Now().UTC(), ch.ID)
err = stores.Channels.SetWorkflowInstance(ctx, ch.ID, target.ID, ver.VersionNumber, initialData, "active")
if err != nil {
log.Printf("[workflow] on_complete: failed to set workflow columns: %v", err)
return
}
_ = stores.Channels.Update(ctx, ch.ID, map[string]interface{}{"ai_mode": "auto"})
// Bind first stage persona
if len(stages) > 0 && stages[0].PersonaID != nil {