mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
df1e1fc917
- Use Nowledge Mem Spaces for per-bot memory isolation (space name: memoh:{botID})
- Auto-ensure space on first use with sync.Map cache
- Add platform/conversation context header to stored text: (Telegram 群组「开发讨论」)
- Replace [我] with bot's actual display name [小助手]
- Thread ConversationType, ConversationName, Platform, BotName through AfterChatRequest
- Add resolveBotDisplayName to resolver for DB lookup
238 lines
6.2 KiB
Go
238 lines
6.2 KiB
Go
package nowledgemem
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
adapters "github.com/memohai/memoh/internal/memory/adapters"
|
|
)
|
|
|
|
const (
|
|
defaultBaseURL = "http://127.0.0.1:14242"
|
|
clientTimeout = 30 * time.Second
|
|
)
|
|
|
|
type nmemClient struct {
|
|
baseURL string
|
|
httpClient *http.Client
|
|
}
|
|
|
|
func newNmemClient(config map[string]any) (*nmemClient, error) {
|
|
baseURL := adapters.StringFromConfig(config, "base_url")
|
|
if baseURL == "" {
|
|
baseURL = defaultBaseURL
|
|
}
|
|
baseURL = strings.TrimRight(baseURL, "/")
|
|
return &nmemClient{
|
|
baseURL: baseURL,
|
|
httpClient: &http.Client{
|
|
Timeout: clientTimeout,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// --- Request/Response types ---
|
|
|
|
type nmemMemoryCreateRequest struct {
|
|
Content string `json:"content"`
|
|
Source string `json:"source,omitempty"`
|
|
SpaceID string `json:"space_id,omitempty"`
|
|
}
|
|
|
|
type nmemMemoryUpdateRequest struct {
|
|
Content string `json:"content"`
|
|
}
|
|
|
|
type nmemSearchRequest struct {
|
|
Query string `json:"query"`
|
|
Limit int `json:"limit,omitempty"`
|
|
IncludeEntities bool `json:"include_entities,omitempty"`
|
|
SpaceID string `json:"space_id,omitempty"`
|
|
}
|
|
|
|
type nmemMemory struct {
|
|
ID string `json:"id"`
|
|
Title string `json:"title,omitempty"`
|
|
Content string `json:"content"`
|
|
Source string `json:"source,omitempty"`
|
|
Time string `json:"time,omitempty"`
|
|
Confidence *float64 `json:"confidence,omitempty"`
|
|
Metadata map[string]any `json:"metadata,omitempty"`
|
|
}
|
|
|
|
type nmemSearchResult struct {
|
|
Memory nmemMemory `json:"memory"`
|
|
SimilarityScore float64 `json:"similarity_score"`
|
|
RelevanceReason string `json:"relevance_reason,omitempty"`
|
|
}
|
|
|
|
type nmemSearchResponse struct {
|
|
Results []nmemSearchResult `json:"results"`
|
|
}
|
|
|
|
type nmemDeleteResponse struct {
|
|
Message string `json:"message,omitempty"`
|
|
DeletedRelationships int `json:"deleted_relationships,omitempty"`
|
|
DeletedEntities int `json:"deleted_entities,omitempty"`
|
|
}
|
|
|
|
// --- Space types ---
|
|
|
|
type nmemSpaceCreateRequest struct {
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
type nmemSpace struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
type nmemSpacesResponse struct {
|
|
Enabled bool `json:"enabled"`
|
|
Spaces []nmemSpace `json:"spaces"`
|
|
}
|
|
|
|
// --- Client methods ---
|
|
|
|
func (c *nmemClient) addMemory(ctx context.Context, content, source, spaceID string) (*nmemMemory, error) {
|
|
var result nmemMemory
|
|
if err := c.doJSON(ctx, http.MethodPost, "/memories", nmemMemoryCreateRequest{
|
|
Content: content,
|
|
Source: source,
|
|
SpaceID: spaceID,
|
|
}, &result); err != nil {
|
|
return nil, fmt.Errorf("nowledgemem add: %w", err)
|
|
}
|
|
return &result, nil
|
|
}
|
|
|
|
func (c *nmemClient) searchMemories(ctx context.Context, query string, limit int, spaceID string) ([]nmemSearchResult, error) {
|
|
var result nmemSearchResponse
|
|
if err := c.doJSON(ctx, http.MethodPost, "/memories/search", nmemSearchRequest{
|
|
Query: query,
|
|
Limit: limit,
|
|
IncludeEntities: true,
|
|
SpaceID: spaceID,
|
|
}, &result); err != nil {
|
|
return nil, fmt.Errorf("nowledgemem search: %w", err)
|
|
}
|
|
return result.Results, nil
|
|
}
|
|
|
|
func (c *nmemClient) updateMemory(ctx context.Context, id, content string) (*nmemMemory, error) {
|
|
var result nmemMemory
|
|
if err := c.doJSON(ctx, http.MethodPatch, "/memories/"+id, nmemMemoryUpdateRequest{
|
|
Content: content,
|
|
}, &result); err != nil {
|
|
return nil, fmt.Errorf("nowledgemem update: %w", err)
|
|
}
|
|
return &result, nil
|
|
}
|
|
|
|
func (c *nmemClient) deleteMemory(ctx context.Context, id string) error {
|
|
var result nmemDeleteResponse
|
|
if err := c.doJSON(ctx, http.MethodDelete, "/memories/"+id, nil, &result); err != nil {
|
|
return fmt.Errorf("nowledgemem delete: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *nmemClient) listSpaces(ctx context.Context) ([]nmemSpace, error) {
|
|
var result nmemSpacesResponse
|
|
if err := c.doJSON(ctx, http.MethodGet, "/spaces", nil, &result); err != nil {
|
|
return nil, fmt.Errorf("nowledgemem list spaces: %w", err)
|
|
}
|
|
return result.Spaces, nil
|
|
}
|
|
|
|
func (c *nmemClient) createSpace(ctx context.Context, name string) (*nmemSpace, error) {
|
|
var result nmemSpacesResponse
|
|
if err := c.doJSON(ctx, http.MethodPost, "/spaces", nmemSpaceCreateRequest{
|
|
Name: name,
|
|
}, &result); err != nil {
|
|
return nil, fmt.Errorf("nowledgemem create space: %w", err)
|
|
}
|
|
for _, s := range result.Spaces {
|
|
if s.Name == name {
|
|
return &s, nil
|
|
}
|
|
}
|
|
if len(result.Spaces) > 0 {
|
|
last := result.Spaces[len(result.Spaces)-1]
|
|
return &last, nil
|
|
}
|
|
return nil, fmt.Errorf("nowledgemem create space: space %q not found in response", name)
|
|
}
|
|
|
|
// ensureSpace finds an existing space by name or creates it. Returns the space ID.
|
|
func (c *nmemClient) ensureSpace(ctx context.Context, name string) (string, error) {
|
|
spaces, err := c.listSpaces(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
for _, s := range spaces {
|
|
if s.Name == name {
|
|
return s.ID, nil
|
|
}
|
|
}
|
|
created, err := c.createSpace(ctx, name)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return created.ID, nil
|
|
}
|
|
|
|
// --- HTTP helper ---
|
|
|
|
func (c *nmemClient) 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")
|
|
req.Header.Set("Accept", "application/json")
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
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("nowledgemem 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 {
|
|
const maxLen = 300
|
|
s := strings.TrimSpace(string(b))
|
|
if len(s) <= maxLen {
|
|
return s
|
|
}
|
|
return s[:maxLen] + "..."
|
|
}
|