sync: migrate ai-operator to Gitea (2026-08-10)
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
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 ""
|
||||
}
|
||||
Reference in New Issue
Block a user