Files
story/internal/vikingmemory/client.go
T

190 lines
6.4 KiB
Go
Raw Normal View History

2026-09-06 17:04:11 +08:00
// Package vikingmemory integrates Volcano's managed memory collection, not OpenViking.
package vikingmemory
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"data-terminal-cloud/internal/outbox"
)
const AssistantID = "platform_companion_v1"
type Options struct {
2026-09-06 17:27:11 +08:00
BaseURL, ResourceID, CollectionName, APIKey, EventType, ProfileType string
Timeout time.Duration
2026-09-06 17:04:11 +08:00
}
type Client struct {
options Options
http *http.Client
}
type Message struct {
Role string `json:"role"`
Content string `json:"content"`
RoleID string `json:"role_id"`
Time int64 `json:"time"`
}
type Item struct {
ID string `json:"id"`
UserID []string `json:"user_id"`
AssistantID []string `json:"assistant_id"`
SessionID string `json:"session_id"`
Score float64 `json:"score"`
MemoryInfo map[string]json.RawMessage `json:"memory_info"`
Content string `json:"-"`
}
func NewClient(options Options) (*Client, error) {
if options.BaseURL == "" {
options.BaseURL = "https://api-knowledgebase.mlp.cn-beijing.volces.com"
}
u, e := url.Parse(options.BaseURL)
if e != nil || u.Scheme != "https" || u.Host == "" || u.User != nil || u.RawQuery != "" || u.Fragment != "" {
return nil, errors.New("Viking requires an HTTPS endpoint")
}
if options.ResourceID == "" || strings.TrimSpace(options.APIKey) == "" || options.EventType == "" || options.ProfileType == "" || options.Timeout <= 0 {
return nil, errors.New("Viking resource, key, event/profile types and timeout are required")
}
options.BaseURL = strings.TrimRight(options.BaseURL, "/")
options.APIKey = strings.TrimSpace(options.APIKey)
return &Client{options: options, http: &http.Client{Timeout: options.Timeout, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}}, nil
}
func UserID(tenantID, subjectID string) string {
digest := sha256.Sum256([]byte(tenantID + ":" + subjectID))
return "u_" + hex.EncodeToString(digest[:])
}
func SessionID(conversationID string, batch int) string {
return "c_" + strings.ReplaceAll(conversationID, "-", "") + fmt.Sprintf("_%d", batch)
}
func (c *Client) call(ctx context.Context, path string, payload map[string]any, target any) error {
payload["resource_id"] = c.options.ResourceID
body, err := json.Marshal(payload)
if err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.options.BaseURL+path, bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+c.options.APIKey)
req.Header.Set("Content-Type", "application/json")
resp, err := c.http.Do(req)
if err != nil {
return outbox.Retryable(errors.New("Viking request unavailable"))
}
defer resp.Body.Close()
var result struct {
Code *int `json:"code"`
Data json.RawMessage `json:"data"`
}
err = json.NewDecoder(io.LimitReader(resp.Body, 2*1024*1024)).Decode(&result)
if resp.StatusCode < 200 || resp.StatusCode >= 300 || err != nil || result.Code == nil || *result.Code != 0 {
code := -1
if result.Code != nil {
code = *result.Code
}
failure := fmt.Errorf("Viking %s HTTP %d code %d", path, resp.StatusCode, code)
if resp.StatusCode == 429 || resp.StatusCode >= 500 || (resp.StatusCode >= 200 && resp.StatusCode < 300) {
return outbox.Retryable(failure)
}
return failure
}
if target != nil {
if err := json.Unmarshal(result.Data, target); err != nil {
return errors.New("Viking returned invalid data")
}
}
return nil
}
func (c *Client) AddSession(ctx context.Context, sessionID, userID string, messages []Message, when int64) error {
if sessionID == "" || userID == "" || len(messages) == 0 || len(messages) > 100 {
return errors.New("invalid Viking session")
}
return c.call(ctx, "/api/memory/session/add", map[string]any{"session_id": sessionID, "messages": messages, "metadata": map[string]any{"default_user_id": userID, "default_assistant_id": AssistantID, "time": when}, "extract_memory_type": []string{c.options.EventType, c.options.ProfileType}}, nil)
}
func (c *Client) Search(ctx context.Context, userID, query string, limit int) ([]Item, error) {
if userID == "" || limit < 1 || limit > 5 {
return nil, errors.New("Viking user and bounded limit are required")
}
items := []Item{}
remaining := 800
for _, kind := range []struct{ path, rule string }{{"profile", c.options.ProfileType}, {"event", c.options.EventType}} {
payload := map[string]any{"filter": map[string]any{"user_id": []string{userID}, "assistant_id": []string{AssistantID}, "memory_type": []string{kind.rule}}, "limit": limit}
if strings.TrimSpace(query) != "" {
payload["query"] = query
}
var data struct {
Items []Item `json:"result_list"`
}
if err := c.call(ctx, "/api/memory/"+kind.path+"/search", payload, &data); err != nil {
return nil, err
}
for _, item := range data.Items {
if !contains(item.UserID, userID) || !contains(item.AssistantID, AssistantID) {
continue
}
parts := []string{}
for _, key := range []string{"summary", "user_profile"} {
raw := item.MemoryInfo[key]
if len(raw) == 0 {
continue
}
var text string
if json.Unmarshal(raw, &text) != nil {
text = string(raw)
}
if text != "" && text != "{}" {
parts = append(parts, text)
}
}
content := []rune(strings.Join(parts, ";"))
if len(content) > 400 {
content = content[:400]
}
if len(content) > remaining {
content = content[:remaining]
}
if len(content) == 0 {
continue
}
remaining -= len(content)
item.Content = string(content)
items = append(items, item)
if len(items) >= limit || remaining <= 0 {
return items, nil
}
}
}
return items, nil
}
func contains(values []string, expected string) bool {
for _, v := range values {
if v == expected {
return true
}
}
return false
}
func (c *Client) DeleteUser(ctx context.Context, userID string) error {
if userID == "" {
return errors.New("Viking deletion requires an exact user")
}
for _, kind := range []string{"event", "profile"} {
if err := c.call(ctx, "/api/memory/"+kind+"/batch_delete", map[string]any{"delete_type": "full", "filter": map[string]any{"user_id": []string{userID}, "assistant_id": []string{AssistantID}}}, nil); err != nil {
return err
}
}
return nil
}