104 lines
3.2 KiB
Go
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 ""
|
|
}
|