mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
627b673a5c
* 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
164 lines
4.5 KiB
Go
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)
|
|
}
|