chore: establish local platform baseline
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user