Files

104 lines
3.2 KiB
Go

package realtime
import (
"ai-operator/internal/ai"
"encoding/base64"
"encoding/json"
"time"
)
type GenericRealtimeEvent struct {
Type string
Raw map[string]any
}
func ParseServerEvent(data []byte) (ai.VoiceEvent, GenericRealtimeEvent, error) {
var base map[string]any
if err := json.Unmarshal(data, &base); err != nil {
return ai.VoiceEvent{}, GenericRealtimeEvent{}, err
}
typ, _ := base["type"].(string)
ev := ai.VoiceEvent{At: time.Now().UTC(), Metadata: map[string]any{"openai_type": typ}}
switch typ {
case "session.created":
ev.Type = ai.VoiceEventSessionStarted
return ev, GenericRealtimeEvent{Type: typ, Raw: base}, nil
case "session.updated":
ev.Type = ai.VoiceEventSessionUpdated
return ev, GenericRealtimeEvent{Type: typ, Raw: base}, nil
case "error":
ev.Type = ai.VoiceEventError
if e, ok := base["error"].(map[string]any); ok {
ev.Error = toStr(e["message"])
} else {
ev.Error = "openai error"
}
return ev, GenericRealtimeEvent{Type: typ, Raw: base}, nil
case "input_audio_buffer.speech_started":
ev.Type = ai.VoiceEventInterruption
return ev, GenericRealtimeEvent{Type: typ, Raw: base}, nil
case "response.output_audio.delta":
ev.Type = ai.VoiceEventAssistantAudioDelta
b, err := base64.StdEncoding.DecodeString(toStr(base["delta"]))
if err != nil {
return ai.VoiceEvent{}, GenericRealtimeEvent{}, err
}
ev.Audio = b
return ev, GenericRealtimeEvent{Type: typ, Raw: base}, nil
case "response.output_audio.done":
ev.Type = ai.VoiceEventAssistantAudioDone
return ev, GenericRealtimeEvent{Type: typ, Raw: base}, nil
case "response.output_audio_transcript.delta", "response.output_text.delta":
ev.Type = ai.VoiceEventAssistantTranscriptDelta
ev.Text = toStr(base["delta"])
return ev, GenericRealtimeEvent{Type: typ, Raw: base}, nil
case "response.output_audio_transcript.done", "response.output_text.done":
ev.Type = ai.VoiceEventAssistantTranscriptDone
ev.Text = toStr(base["transcript"]) + toStr(base["text"])
return ev, GenericRealtimeEvent{Type: typ, Raw: base}, nil
case "rate_limits.updated":
ev.Type = ai.VoiceEventRateLimitsUpdated
ev.Metadata = base
return ev, GenericRealtimeEvent{Type: typ, Raw: base}, nil
case "response.done":
if tc := extractToolCall(base); tc != nil {
ev.Type = ai.VoiceEventToolCall
ev.ToolCall = tc
return ev, GenericRealtimeEvent{Type: typ, Raw: base}, nil
}
ev.Type = ai.VoiceEventAssistantAudioDone
return ev, GenericRealtimeEvent{Type: typ, Raw: base}, nil
default:
return ai.VoiceEvent{}, GenericRealtimeEvent{Type: typ, Raw: base}, nil
}
}
func extractToolCall(m map[string]any) *ai.ToolCall {
resp, ok := m["response"].(map[string]any)
if !ok {
return nil
}
outs, ok := resp["output"].([]any)
if !ok {
return nil
}
for _, o := range outs {
item, ok := o.(map[string]any)
if !ok {
continue
}
if toStr(item["type"]) == "function_call" {
raw := toStr(item["arguments"])
args := map[string]any{}
_ = json.Unmarshal([]byte(raw), &args)
return &ai.ToolCall{ID: toStr(item["call_id"]), Name: toStr(item["name"]), Arguments: args, RawArguments: raw}
}
}
return nil
}
func toStr(v any) string {
if s, ok := v.(string); ok {
return s
}
return ""
}