117 lines
3.8 KiB
Go
117 lines
3.8 KiB
Go
package providermemory
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"context"
|
||
|
|
"encoding/json"
|
||
|
|
"errors"
|
||
|
|
"fmt"
|
||
|
|
"io"
|
||
|
|
"net/http"
|
||
|
|
"net/url"
|
||
|
|
"strings"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"data-terminal-cloud/internal/outbox"
|
||
|
|
)
|
||
|
|
|
||
|
|
type Memory struct {
|
||
|
|
TenantID string `json:"tenant_id"`
|
||
|
|
HouseholdID string `json:"household_id"`
|
||
|
|
SubjectID string `json:"subject_id"`
|
||
|
|
MemoryID string `json:"memory_id"`
|
||
|
|
MemoryVersion string `json:"memory_version_id"`
|
||
|
|
VersionNo int64 `json:"version_no"`
|
||
|
|
Content string `json:"content"`
|
||
|
|
ContentDigest string `json:"content_digest"`
|
||
|
|
IdempotencyKey string `json:"idempotency_key"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type Client interface {
|
||
|
|
Upsert(context.Context, Memory) (string, error)
|
||
|
|
Delete(context.Context, string, string) error
|
||
|
|
}
|
||
|
|
|
||
|
|
type HTTPClient struct {
|
||
|
|
baseURL string
|
||
|
|
apiKey string
|
||
|
|
client *http.Client
|
||
|
|
}
|
||
|
|
|
||
|
|
func NewHTTPClient(baseURL, apiKey string, timeout time.Duration) (*HTTPClient, error) {
|
||
|
|
parsed, err := url.Parse(strings.TrimRight(strings.TrimSpace(baseURL), "/"))
|
||
|
|
if err != nil || parsed.Scheme == "" || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") {
|
||
|
|
return nil, errors.New("provider memory base URL must be an absolute HTTP(S) URL")
|
||
|
|
}
|
||
|
|
if strings.TrimSpace(apiKey) == "" || timeout <= 0 {
|
||
|
|
return nil, errors.New("provider memory API key and positive timeout are required")
|
||
|
|
}
|
||
|
|
return &HTTPClient{baseURL: parsed.String(), apiKey: strings.TrimSpace(apiKey), client: &http.Client{Timeout: timeout}}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (c *HTTPClient) Upsert(ctx context.Context, memory Memory) (string, error) {
|
||
|
|
payload, err := json.Marshal(memory)
|
||
|
|
if err != nil {
|
||
|
|
return "", err
|
||
|
|
}
|
||
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/v1/memories:upsert", bytes.NewReader(payload))
|
||
|
|
if err != nil {
|
||
|
|
return "", err
|
||
|
|
}
|
||
|
|
request.Header.Set("Authorization", "Bearer "+c.apiKey)
|
||
|
|
request.Header.Set("Content-Type", "application/json")
|
||
|
|
request.Header.Set("Idempotency-Key", memory.IdempotencyKey)
|
||
|
|
response, err := c.client.Do(request)
|
||
|
|
if err != nil {
|
||
|
|
return "", outbox.Retryable(fmt.Errorf("call provider memory upsert: %w", err))
|
||
|
|
}
|
||
|
|
defer response.Body.Close()
|
||
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
||
|
|
return "", providerError("upsert", response)
|
||
|
|
}
|
||
|
|
var result struct {
|
||
|
|
ProviderRef string `json:"provider_ref"`
|
||
|
|
}
|
||
|
|
if err := json.NewDecoder(io.LimitReader(response.Body, 64*1024)).Decode(&result); err != nil {
|
||
|
|
return "", outbox.Retryable(errors.New("provider memory upsert returned invalid JSON"))
|
||
|
|
}
|
||
|
|
result.ProviderRef = strings.TrimSpace(result.ProviderRef)
|
||
|
|
if result.ProviderRef == "" || len(result.ProviderRef) > 300 {
|
||
|
|
return "", outbox.Retryable(errors.New("provider memory upsert returned an invalid reference"))
|
||
|
|
}
|
||
|
|
return result.ProviderRef, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (c *HTTPClient) Delete(ctx context.Context, providerRef, idempotencyKey string) error {
|
||
|
|
providerRef = strings.TrimSpace(providerRef)
|
||
|
|
if providerRef == "" {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodDelete, c.baseURL+"/v1/memories/"+url.PathEscape(providerRef), nil)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
request.Header.Set("Authorization", "Bearer "+c.apiKey)
|
||
|
|
request.Header.Set("Idempotency-Key", idempotencyKey)
|
||
|
|
response, err := c.client.Do(request)
|
||
|
|
if err != nil {
|
||
|
|
return outbox.Retryable(fmt.Errorf("call provider memory delete: %w", err))
|
||
|
|
}
|
||
|
|
defer response.Body.Close()
|
||
|
|
if response.StatusCode == http.StatusNotFound || response.StatusCode == http.StatusGone {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
||
|
|
return providerError("delete", response)
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func providerError(action string, response *http.Response) error {
|
||
|
|
err := fmt.Errorf("provider memory %s returned HTTP %d", action, response.StatusCode)
|
||
|
|
if response.StatusCode == http.StatusTooManyRequests || response.StatusCode >= 500 {
|
||
|
|
return outbox.Retryable(err)
|
||
|
|
}
|
||
|
|
return err
|
||
|
|
}
|