Files
晨苒 627b673a5c refactor: multi-provider memory adapters with scan-based builtin (#227)
* refactor: restructure memory into multi-provider adapters, remove manifest.json dependency

- Rename internal/memory/provider to internal/memory/adapters with per-provider subdirectories (builtin, mem0, openviking)
- Replace manifest.json-based delete/update with scan-based index from daily files
- Add mem0 and openviking provider adapters with HTTP client, chat hooks, MCP tools, and CRUD
- Wire provider lifecycle into registry (auto-instantiate on create, evict on update/delete)
- Split docker-compose into base stack + optional overlays (qdrant, browser, mem0, openviking)
- Update admin UI to support dynamic provider config schema rendering

* chore(lint): fix all golangci-lint issues for clean CI

* refactor(docker): replace compose overlay files with profiles

* feat(memory): add built-in memory multi modes

* fix(ci): golangci lint

* feat(memory): edit built-in memory sparse design
2026-03-14 06:04:13 +08:00

164 lines
4.5 KiB
Go

package openviking
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
adapters "github.com/memohai/memoh/internal/memory/adapters"
)
type openVikingClient struct {
baseURL string
apiKey string
httpClient *http.Client
}
func newOpenVikingClient(config map[string]any) (*openVikingClient, error) {
baseURL := adapters.StringFromConfig(config, "base_url")
if baseURL == "" {
return nil, errors.New("openviking: base_url is required")
}
baseURL = strings.TrimRight(baseURL, "/")
return &openVikingClient{
baseURL: baseURL,
apiKey: adapters.StringFromConfig(config, "api_key"),
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
}, nil
}
type ovMemory struct {
ID string `json:"id"`
Content string `json:"content"`
CreatedAt string `json:"created_at,omitempty"`
UpdatedAt string `json:"updated_at,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
Score float64 `json:"score,omitempty"`
}
type ovAddRequest struct {
AgentID string `json:"agent_id"`
Content string `json:"content"`
}
type ovSearchRequest struct {
Query string `json:"query"`
AgentID string `json:"agent_id"`
Limit int `json:"limit,omitempty"`
}
type ovUpdateRequest struct {
Content string `json:"content"`
}
func (c *openVikingClient) Add(ctx context.Context, agentID, content string) (*ovMemory, error) {
var result ovMemory
if err := c.doJSON(ctx, http.MethodPost, "/memories", ovAddRequest{
AgentID: agentID,
Content: content,
}, &result); err != nil {
return nil, fmt.Errorf("openviking add: %w", err)
}
return &result, nil
}
func (c *openVikingClient) Search(ctx context.Context, agentID, query string, limit int) ([]ovMemory, error) {
var results []ovMemory
if err := c.doJSON(ctx, http.MethodPost, "/memories/search", ovSearchRequest{
Query: query,
AgentID: agentID,
Limit: limit,
}, &results); err != nil {
return nil, fmt.Errorf("openviking search: %w", err)
}
return results, nil
}
func (c *openVikingClient) GetAll(ctx context.Context, agentID string, limit int) ([]ovMemory, error) {
u := "/memories?agent_id=" + url.QueryEscape(agentID)
if limit > 0 {
u += fmt.Sprintf("&limit=%d", limit)
}
var results []ovMemory
if err := c.doJSON(ctx, http.MethodGet, u, nil, &results); err != nil {
return nil, fmt.Errorf("openviking get all: %w", err)
}
return results, nil
}
func (c *openVikingClient) Update(ctx context.Context, memoryID, content string) (*ovMemory, error) {
var result ovMemory
if err := c.doJSON(ctx, http.MethodPut, "/memories/"+memoryID, ovUpdateRequest{Content: content}, &result); err != nil {
return nil, fmt.Errorf("openviking update: %w", err)
}
return &result, nil
}
func (c *openVikingClient) Delete(ctx context.Context, memoryID string) error {
if err := c.doJSON(ctx, http.MethodDelete, "/memories/"+memoryID, nil, nil); err != nil {
return fmt.Errorf("openviking delete: %w", err)
}
return nil
}
func (c *openVikingClient) DeleteAll(ctx context.Context, agentID string) error {
u := "/memories?agent_id=" + url.QueryEscape(agentID)
if err := c.doJSON(ctx, http.MethodDelete, u, nil, nil); err != nil {
return fmt.Errorf("openviking delete all: %w", err)
}
return nil
}
func (c *openVikingClient) doJSON(ctx context.Context, method, urlPath string, body any, result any) error {
var bodyReader io.Reader
if body != nil {
data, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("marshal request: %w", err)
}
bodyReader = bytes.NewReader(data)
}
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+urlPath, bodyReader)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
if c.apiKey != "" {
req.Header.Set("Authorization", "Bearer "+c.apiKey)
}
resp, err := c.httpClient.Do(req) //nolint:gosec // URL is from admin-configured base_url
if err != nil {
return err
}
defer func() { _ = resp.Body.Close() }()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("read response: %w", err)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("openviking API error %d: %s", resp.StatusCode, truncateBody(respBody))
}
if result != nil && len(respBody) > 0 {
if err := json.Unmarshal(respBody, result); err != nil {
return fmt.Errorf("unmarshal response: %w", err)
}
}
return nil
}
func truncateBody(b []byte) string {
if len(b) > 300 {
return string(b[:300]) + "..."
}
return string(b)
}