Files

128 lines
4.0 KiB
Go

package embeddingprovider
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"math"
"net/http"
"net/url"
"strings"
"time"
"data-terminal-cloud/internal/developmentembedding"
"data-terminal-cloud/internal/outbox"
)
const Dimensions = developmentembedding.Dimensions
type Result struct {
Vector []float64
ProviderRef string
}
type Provider interface {
Embed(context.Context, string) (Result, error)
}
type Local struct{}
func (Local) Embed(_ context.Context, content string) (Result, error) {
if strings.TrimSpace(content) == "" {
return Result{}, errors.New("embedding content is required")
}
return Result{Vector: developmentembedding.Vector(content), ProviderRef: "local-deterministic-hash-v1"}, nil
}
type Ark struct {
endpoint string
apiKey string
model string
client *http.Client
}
func NewArk(baseURL, apiKey, model string, timeout time.Duration) (*Ark, error) {
baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/")
apiKey = strings.TrimSpace(apiKey)
model = strings.TrimSpace(model)
parsed, err := url.Parse(baseURL)
if err != nil || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") {
return nil, errors.New("Ark base URL must be an absolute HTTP(S) URL")
}
if apiKey == "" || model == "" || timeout <= 0 {
return nil, errors.New("Ark API key, embedding model and positive timeout are required")
}
return &Ark{endpoint: baseURL + "/embeddings", apiKey: apiKey, model: model, client: &http.Client{Timeout: timeout}}, nil
}
func (a *Ark) Embed(ctx context.Context, content string) (Result, error) {
content = strings.TrimSpace(content)
if content == "" {
return Result{}, errors.New("embedding content is required")
}
body, err := json.Marshal(map[string]any{
"model": a.model, "input": content, "encoding_format": "float", "dimensions": Dimensions,
})
if err != nil {
return Result{}, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, a.endpoint, bytes.NewReader(body))
if err != nil {
return Result{}, fmt.Errorf("build Ark embedding request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+a.apiKey)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "voice-platform-memory-embedding/1.0")
resp, err := a.client.Do(req)
if err != nil {
return Result{}, outbox.Retryable(fmt.Errorf("call Ark embedding model: %w", err))
}
defer resp.Body.Close()
responseBody, readErr := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
if readErr != nil {
return Result{}, outbox.Retryable(fmt.Errorf("read Ark embedding response: %w", readErr))
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
requestErr := fmt.Errorf("Ark embedding model returned HTTP %d", resp.StatusCode)
if resp.StatusCode == http.StatusRequestTimeout || resp.StatusCode == http.StatusTooEarly || resp.StatusCode == http.StatusTooManyRequests || resp.StatusCode >= 500 {
return Result{}, outbox.Retryable(requestErr)
}
return Result{}, requestErr
}
var response struct {
Data []struct {
Index int `json:"index"`
Embedding []float64 `json:"embedding"`
} `json:"data"`
}
if err = json.Unmarshal(responseBody, &response); err != nil || len(response.Data) != 1 || response.Data[0].Index != 0 {
return Result{}, outbox.Retryable(errors.New("Ark embedding response has no usable vector"))
}
if err = Validate(response.Data[0].Embedding); err != nil {
return Result{}, outbox.Retryable(err)
}
return Result{Vector: response.Data[0].Embedding, ProviderRef: "ark:" + a.model}, nil
}
func Validate(vector []float64) error {
if len(vector) != Dimensions {
return fmt.Errorf("embedding dimensions=%d want=%d", len(vector), Dimensions)
}
for _, value := range vector {
if math.IsNaN(value) || math.IsInf(value, 0) {
return errors.New("embedding contains a non-finite value")
}
}
return nil
}
func Literal(vector []float64) (string, error) {
if err := Validate(vector); err != nil {
return "", err
}
return developmentembedding.Literal(vector), nil
}