230 lines
8.0 KiB
Go
230 lines
8.0 KiB
Go
package voiceprovider
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"context"
|
||
|
|
"encoding/json"
|
||
|
|
"errors"
|
||
|
|
"fmt"
|
||
|
|
"io"
|
||
|
|
"net/http"
|
||
|
|
"strings"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
type StartRequest struct {
|
||
|
|
TaskRef string `json:"task_ref"`
|
||
|
|
RoomID string `json:"room_id"`
|
||
|
|
DeviceUserID string `json:"device_user_id"`
|
||
|
|
AgentUserID string `json:"agent_user_id"`
|
||
|
|
ConversationID string `json:"conversation_id"`
|
||
|
|
EngineMode string `json:"engine_mode"`
|
||
|
|
ContextSummary string `json:"context_summary,omitempty"`
|
||
|
|
PromptBundleVersion string `json:"prompt_bundle_version,omitempty"`
|
||
|
|
AgentBundleVersion string `json:"agent_bundle_version_id,omitempty"`
|
||
|
|
PersonaVersion string `json:"persona_version_id,omitempty"`
|
||
|
|
VoiceVersion string `json:"voice_version_id,omitempty"`
|
||
|
|
ModelVersion string `json:"model_version_id,omitempty"`
|
||
|
|
RoutingVersion string `json:"routing_version_id,omitempty"`
|
||
|
|
SystemPrompt string `json:"system_prompt,omitempty"`
|
||
|
|
PersonaTone string `json:"persona_tone,omitempty"`
|
||
|
|
VoiceResourceRef string `json:"voice_resource_ref,omitempty"`
|
||
|
|
ModelResourceRef string `json:"model_resource_ref,omitempty"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type CallbackEvent struct {
|
||
|
|
Provider string `json:"provider"`
|
||
|
|
EventID string `json:"event_id"`
|
||
|
|
TaskRef string `json:"task_ref"`
|
||
|
|
ConversationID string `json:"conversation_id"`
|
||
|
|
EventType string `json:"event_type"`
|
||
|
|
Status string `json:"status"`
|
||
|
|
OccurredAt time.Time `json:"occurred_at"`
|
||
|
|
Turns []FinalTurn `json:"turns,omitempty"`
|
||
|
|
Usage []UsageMetric `json:"usage,omitempty"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type UsageMetric struct {
|
||
|
|
UsageCategory string `json:"usage_category"`
|
||
|
|
MetricCode string `json:"metric_code"`
|
||
|
|
Quantity string `json:"quantity"`
|
||
|
|
Unit string `json:"unit"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type FinalTurn struct {
|
||
|
|
TurnKey string `json:"turn_key"`
|
||
|
|
SequenceNo int64 `json:"sequence_no"`
|
||
|
|
Speaker string `json:"speaker"`
|
||
|
|
StartAt time.Time `json:"start_at"`
|
||
|
|
EndAt time.Time `json:"end_at"`
|
||
|
|
FinalText string `json:"final_text"`
|
||
|
|
Interrupted bool `json:"interrupted"`
|
||
|
|
ProviderRef string `json:"provider_ref,omitempty"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type StartResponse struct {
|
||
|
|
TaskRef string `json:"task_ref"`
|
||
|
|
Status string `json:"status"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type StopRequest struct {
|
||
|
|
TaskRef string `json:"task_ref"`
|
||
|
|
RoomID string `json:"room_id,omitempty"`
|
||
|
|
ConversationID string `json:"conversation_id"`
|
||
|
|
Reason string `json:"reason"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type StopResponse struct {
|
||
|
|
TaskRef string `json:"task_ref"`
|
||
|
|
Status string `json:"status"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type QueryRequest struct {
|
||
|
|
TaskRef string `json:"task_ref"`
|
||
|
|
ConversationID string `json:"conversation_id"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type QueryResponse struct {
|
||
|
|
TaskRef string `json:"task_ref"`
|
||
|
|
Status string `json:"status"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type Client interface {
|
||
|
|
Start(context.Context, StartRequest) (StartResponse, error)
|
||
|
|
Query(context.Context, QueryRequest) (QueryResponse, error)
|
||
|
|
Stop(context.Context, StopRequest) (StopResponse, error)
|
||
|
|
}
|
||
|
|
|
||
|
|
type Error struct {
|
||
|
|
Operation string
|
||
|
|
StatusCode int
|
||
|
|
Code string
|
||
|
|
Temporary bool
|
||
|
|
Cause error
|
||
|
|
}
|
||
|
|
|
||
|
|
func (e *Error) Error() string {
|
||
|
|
if e.Code != "" {
|
||
|
|
return fmt.Sprintf("provider %s returned %s: %v", e.Operation, e.Code, e.Cause)
|
||
|
|
}
|
||
|
|
if e.StatusCode > 0 {
|
||
|
|
return fmt.Sprintf("provider %s returned HTTP %d", e.Operation, e.StatusCode)
|
||
|
|
}
|
||
|
|
return fmt.Sprintf("provider %s failed: %v", e.Operation, e.Cause)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (e *Error) Unwrap() error { return e.Cause }
|
||
|
|
|
||
|
|
func IsTemporary(err error) bool {
|
||
|
|
var providerError *Error
|
||
|
|
return errors.As(err, &providerError) && providerError.Temporary
|
||
|
|
}
|
||
|
|
|
||
|
|
func IsNotFound(err error) bool {
|
||
|
|
var providerError *Error
|
||
|
|
return errors.As(err, &providerError) && providerError.StatusCode == http.StatusNotFound
|
||
|
|
}
|
||
|
|
|
||
|
|
type HTTPClient struct {
|
||
|
|
baseURL string
|
||
|
|
apiKey string
|
||
|
|
client *http.Client
|
||
|
|
}
|
||
|
|
|
||
|
|
func NewHTTPClient(baseURL, apiKey string, timeout time.Duration) (*HTTPClient, error) {
|
||
|
|
if strings.TrimSpace(baseURL) == "" || strings.TrimSpace(apiKey) == "" || timeout <= 0 {
|
||
|
|
return nil, errors.New("provider base URL, API key and positive timeout are required")
|
||
|
|
}
|
||
|
|
return &HTTPClient{baseURL: strings.TrimRight(baseURL, "/"), apiKey: apiKey, client: &http.Client{Timeout: timeout}}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (c *HTTPClient) Start(ctx context.Context, input StartRequest) (StartResponse, error) {
|
||
|
|
body, err := json.Marshal(input)
|
||
|
|
if err != nil {
|
||
|
|
return StartResponse{}, err
|
||
|
|
}
|
||
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/mock/v1/voice-chats:start", bytes.NewReader(body))
|
||
|
|
if err != nil {
|
||
|
|
return StartResponse{}, err
|
||
|
|
}
|
||
|
|
request.Header.Set("Content-Type", "application/json")
|
||
|
|
request.Header.Set("X-Mock-Provider-Key", c.apiKey)
|
||
|
|
response, err := c.client.Do(request)
|
||
|
|
if err != nil {
|
||
|
|
return StartResponse{}, &Error{Operation: "start", Temporary: true, Cause: err}
|
||
|
|
}
|
||
|
|
defer response.Body.Close()
|
||
|
|
responseBody, err := io.ReadAll(io.LimitReader(response.Body, 1<<20))
|
||
|
|
if err != nil {
|
||
|
|
return StartResponse{}, &Error{Operation: "start", Temporary: true, Cause: err}
|
||
|
|
}
|
||
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
||
|
|
return StartResponse{}, &Error{Operation: "start", StatusCode: response.StatusCode, Temporary: response.StatusCode == http.StatusTooManyRequests || response.StatusCode >= 500}
|
||
|
|
}
|
||
|
|
var result StartResponse
|
||
|
|
if err := json.Unmarshal(responseBody, &result); err != nil || result.TaskRef != input.TaskRef || result.Status == "" {
|
||
|
|
return StartResponse{}, errors.New("provider start returned an invalid response")
|
||
|
|
}
|
||
|
|
return result, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (c *HTTPClient) Stop(ctx context.Context, input StopRequest) (StopResponse, error) {
|
||
|
|
body, err := json.Marshal(input)
|
||
|
|
if err != nil {
|
||
|
|
return StopResponse{}, err
|
||
|
|
}
|
||
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/mock/v1/voice-chats:stop", bytes.NewReader(body))
|
||
|
|
if err != nil {
|
||
|
|
return StopResponse{}, err
|
||
|
|
}
|
||
|
|
request.Header.Set("Content-Type", "application/json")
|
||
|
|
request.Header.Set("X-Mock-Provider-Key", c.apiKey)
|
||
|
|
response, err := c.client.Do(request)
|
||
|
|
if err != nil {
|
||
|
|
return StopResponse{}, &Error{Operation: "stop", Temporary: true, Cause: err}
|
||
|
|
}
|
||
|
|
defer response.Body.Close()
|
||
|
|
responseBody, err := io.ReadAll(io.LimitReader(response.Body, 1<<20))
|
||
|
|
if err != nil {
|
||
|
|
return StopResponse{}, &Error{Operation: "stop", Temporary: true, Cause: err}
|
||
|
|
}
|
||
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
||
|
|
return StopResponse{}, &Error{Operation: "stop", StatusCode: response.StatusCode, Temporary: response.StatusCode == http.StatusTooManyRequests || response.StatusCode >= 500}
|
||
|
|
}
|
||
|
|
var result StopResponse
|
||
|
|
if err := json.Unmarshal(responseBody, &result); err != nil || result.TaskRef != input.TaskRef || result.Status == "" {
|
||
|
|
return StopResponse{}, errors.New("provider stop returned an invalid response")
|
||
|
|
}
|
||
|
|
return result, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (c *HTTPClient) Query(ctx context.Context, input QueryRequest) (QueryResponse, error) {
|
||
|
|
body, err := json.Marshal(input)
|
||
|
|
if err != nil {
|
||
|
|
return QueryResponse{}, err
|
||
|
|
}
|
||
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/mock/v1/voice-chats:query", bytes.NewReader(body))
|
||
|
|
if err != nil {
|
||
|
|
return QueryResponse{}, err
|
||
|
|
}
|
||
|
|
request.Header.Set("Content-Type", "application/json")
|
||
|
|
request.Header.Set("X-Mock-Provider-Key", c.apiKey)
|
||
|
|
response, err := c.client.Do(request)
|
||
|
|
if err != nil {
|
||
|
|
return QueryResponse{}, &Error{Operation: "query", Temporary: true, Cause: err}
|
||
|
|
}
|
||
|
|
defer response.Body.Close()
|
||
|
|
responseBody, err := io.ReadAll(io.LimitReader(response.Body, 1<<20))
|
||
|
|
if err != nil {
|
||
|
|
return QueryResponse{}, &Error{Operation: "query", Temporary: true, Cause: err}
|
||
|
|
}
|
||
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
||
|
|
return QueryResponse{}, &Error{Operation: "query", StatusCode: response.StatusCode, Temporary: response.StatusCode == http.StatusTooManyRequests || response.StatusCode >= 500}
|
||
|
|
}
|
||
|
|
var result QueryResponse
|
||
|
|
if err := json.Unmarshal(responseBody, &result); err != nil || result.TaskRef != input.TaskRef || result.Status == "" {
|
||
|
|
return QueryResponse{}, errors.New("provider query returned an invalid response")
|
||
|
|
}
|
||
|
|
return result, nil
|
||
|
|
}
|