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