package callflow import ( "encoding/json" "errors" "fmt" "strings" "time" ) // FinalCallFacts are observed or approved call facts, never defaults inferred // from the Agent SDK. The reason code is nil if no actual SIP code is known. type FinalCallFacts struct { TaskID string CallerProfileID string Callee string TrunkID string StartedAt time.Time EndedAt time.Time Outcome string ReasonCode *int ReasonMessage string } // FinalResultPayload creates the one current MQ result body. Only final // user-side ASR text with observed capture timing is included; unplayed LLM // replies, interim ASR and hypothetical recordings are never transcribed. func FinalResultPayload(facts FinalCallFacts, call Result) ([]byte, error) { if strings.TrimSpace(facts.TaskID) == "" || strings.TrimSpace(facts.CallerProfileID) == "" || strings.TrimSpace(facts.Callee) == "" || strings.TrimSpace(facts.TrunkID) == "" || strings.TrimSpace(facts.ReasonMessage) == "" { return nil, errors.New("final call result is missing approved identity or observed reason") } if facts.StartedAt.IsZero() || facts.EndedAt.IsZero() || facts.EndedAt.Before(facts.StartedAt) { return nil, errors.New("final call result has no valid observed time range") } if facts.Outcome != "answered" && facts.Outcome != "no_answer" && facts.Outcome != "failed" { return nil, errors.New("final call result has no approved outcome") } if facts.Outcome == "no_answer" && len(call.Turns) != 0 { return nil, errors.New("no-answer call cannot contain completed user turns") } if facts.ReasonCode != nil && (*facts.ReasonCode < 100 || *facts.ReasonCode > 699) { return nil, errors.New("final call result has an invalid SIP response code") } if len(call.CaptureWindows) < len(call.Turns) { return nil, errors.New("final ASR text has no corresponding observed media window") } type transcriptSegment struct { SegmentID string `json:"segment_id"` TurnID string `json:"turn_id"` Role string `json:"role"` StartMS int64 `json:"start_ms"` EndMS int64 `json:"end_ms"` Text string `json:"text"` } segments := make([]transcriptSegment, 0, len(call.Turns)) optOut := false callDuration := facts.EndedAt.Sub(facts.StartedAt).Milliseconds() for i, turn := range call.Turns { window := call.CaptureWindows[i] if window.StartedAt.Before(facts.StartedAt) || window.EndedAt.Before(window.StartedAt) || window.EndedAt.After(facts.EndedAt) { return nil, fmt.Errorf("turn %d has no valid observed capture times", i+1) } start, end := window.StartedAt.Sub(facts.StartedAt).Milliseconds(), window.EndedAt.Sub(facts.StartedAt).Milliseconds() if start < 0 || end < start || end > callDuration { return nil, fmt.Errorf("turn %d capture is outside confirmed call duration", i+1) } if turn.EndedByKeyword { if strings.TrimSpace(turn.Transcript) == "" { return nil, fmt.Errorf("turn %d claims opt-out without final user ASR text", i+1) } optOut = true } if turn.Transcript != "" { segments = append(segments, transcriptSegment{ SegmentID: fmt.Sprintf("segment-%d", len(segments)+1), TurnID: fmt.Sprintf("turn-%d", i+1), Role: "user", StartMS: start, EndMS: end, Text: turn.Transcript, }) } } return json.Marshal(struct { TaskID string `json:"task_id"` CallerProfileID string `json:"caller_profile_id"` Callee string `json:"callee"` TrunkID string `json:"trunk_id"` StartedAt string `json:"started_at"` EndedAt string `json:"ended_at"` DurationMS int64 `json:"duration_ms"` Outcome string `json:"outcome"` ReasonCode *int `json:"reason_code"` ReasonMessage string `json:"reason_message"` Transcript []transcriptSegment `json:"transcript"` OptOut bool `json:"opt_out"` Recording map[string]any `json:"recording"` }{ TaskID: facts.TaskID, CallerProfileID: facts.CallerProfileID, Callee: facts.Callee, TrunkID: facts.TrunkID, StartedAt: facts.StartedAt.UTC().Format(time.RFC3339Nano), EndedAt: facts.EndedAt.UTC().Format(time.RFC3339Nano), DurationMS: callDuration, Outcome: facts.Outcome, ReasonCode: facts.ReasonCode, ReasonMessage: facts.ReasonMessage, Transcript: segments, OptOut: optOut, Recording: map[string]any{}, }) }