Files
story/internal/providermemory/client.go
T

117 lines
3.8 KiB
Go
Raw Normal View History

2026-08-29 21:56:51 +08:00
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
}