diff --git a/backend/cmd/cyrene/main.go b/backend/cmd/cyrene/main.go index 3c4b070..b5ecff0 100644 --- a/backend/cmd/cyrene/main.go +++ b/backend/cmd/cyrene/main.go @@ -177,9 +177,22 @@ func main() { // === 12. Gateway === authMgr := api.NewStubAuthManager() - apiRouter := api.New(authMgr) - wsHub := ws.NewHub() + // Build dependencies for gateway handlers + gwDeps := api.Dependencies{ + AuthMgr: authMgr, + Orchestrator: orch, + } + if memSvc != nil { + gwDeps.MemorySvc = memory.NewAPIAdapter(memSvc) + } + gwDeps.VoiceSvc = voiceSvc + + wsHub := ws.NewHub(orch) + gwDeps.WSHub = wsHub + + apiRouter := api.New(gwDeps) + wsMux := http.NewServeMux() wsMux.HandleFunc("/ws/chat", wsHub.UpgradeHandler()) diff --git a/backend/services/gateway/api/router.go b/backend/services/gateway/api/router.go index b22625d..a44d204 100644 --- a/backend/services/gateway/api/router.go +++ b/backend/services/gateway/api/router.go @@ -1,58 +1,115 @@ // Package api provides HTTP routing, middleware, and handlers for the Cyrene Gateway. -// Uses only the standard library net/http to minimize dependencies. package api import ( + "context" + "crypto/rand" + "encoding/hex" "encoding/json" "net/http" "strings" + "time" + + "git.yeij.top/AskaEth/Cyrene_v2/services/ai-core/model" ) -// Router handles HTTP routing with middleware support. -type Router struct { - mux *http.ServeMux - authMgr AuthManager -} +// --- Interfaces for injected services --- -// AuthManager is the interface for JWT authentication. +// AuthManager validates JWT tokens. type AuthManager interface { ValidateToken(token string) (userID, username, role string, err error) } +// MemoryEntry is a simplified memory entry for API responses. +type MemoryEntry struct { + ID string `json:"id"` + UserID string `json:"user_id"` + Content string `json:"content"` + Summary string `json:"summary"` + Keywords []string `json:"keywords"` + Category string `json:"category"` + Priority float64 `json:"priority"` +} + +// MemorySearcher handles memory search queries. +type MemorySearcher interface { + Search(ctx context.Context, userID, query string, limit int) (any, error) + List(ctx context.Context, userID string, limit int) (any, error) + Add(ctx context.Context, userID, content, category string, priority float64) (any, error) + Get(ctx context.Context, id string) (any, error) + Delete(ctx context.Context, id string) error +} + +// VoiceService handles STT and TTS. +type VoiceService interface { + Recognize(ctx context.Context, audioData []byte) (string, error) + Synthesize(ctx context.Context, text string, voice string) ([]byte, error) +} + +// OrchestratorPipeline handles the main dialogue flow. +type OrchestratorPipeline interface { + ProcessInput(ctx context.Context, sessionID, userMessage string) (<-chan model.StreamToken, *model.Intent, error) +} + +// WebSocketHub manages WS connections. +type WebSocketHub interface { + UpgradeHandler() http.HandlerFunc +} + +// Dependencies holds all injectable services. +type Dependencies struct { + AuthMgr AuthManager + MemorySvc MemorySearcher + VoiceSvc VoiceService + Orchestrator OrchestratorPipeline + WSHub WebSocketHub +} + +// Router handles HTTP routing with middleware support. +type Router struct { + mux *http.ServeMux + deps Dependencies +} + // New creates a new Router with all routes configured. -func New(authMgr AuthManager) *Router { - r := &Router{ - mux: http.NewServeMux(), - authMgr: authMgr, - } +func New(deps Dependencies) *Router { + r := &Router{mux: http.NewServeMux(), deps: deps} r.setupRoutes() return r } func (r *Router) setupRoutes() { - // Public endpoints + // Public r.mux.HandleFunc("/api/v1/health", r.healthHandler) r.mux.HandleFunc("/api/v1/auth/login", r.loginHandler) r.mux.HandleFunc("/api/v1/auth/register", r.registerHandler) - // Protected endpoints + // Protected r.mux.HandleFunc("/api/v1/sessions", r.authMW(r.sessionsHandler)) r.mux.HandleFunc("/api/v1/sessions/", r.authMW(r.sessionByIDHandler)) + + // Memory + r.mux.HandleFunc("/api/v1/memory/search", r.authMW(r.memorySearchHandler)) + r.mux.HandleFunc("/api/v1/memory", r.authMW(r.memoryHandler)) + r.mux.HandleFunc("/api/v1/memory/", r.authMW(r.memoryHandler)) + + // Voice + r.mux.HandleFunc("/api/v1/voice/stt", r.authMW(r.voiceSTTHandler)) + r.mux.HandleFunc("/api/v1/voice/tts", r.authMW(r.voiceTTSHandler)) + + // Admin r.mux.HandleFunc("/api/v1/admin/dashboard", r.authMW(r.adminMW(r.adminDashboardHandler))) } // ServeHTTP implements http.Handler. func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { - // CORS w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type") - if req.Method == "OPTIONS" { w.WriteHeader(http.StatusNoContent) return } - r.mux.ServeHTTP(w, req) } @@ -60,19 +117,18 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { func (r *Router) authMW(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { - header := req.Header.Get("Authorization") - if header == "" || !strings.HasPrefix(strings.ToLower(header), "bearer ") { - writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "missing or invalid authorization"}) + h := req.Header.Get("Authorization") + if h == "" || !strings.HasPrefix(strings.ToLower(h), "bearer ") { + writeJSON(w, 401, M{"error": "unauthorized"}) return } - token := header[7:] - userID, username, role, err := r.authMgr.ValidateToken(token) + uid, uname, role, err := r.deps.AuthMgr.ValidateToken(h[7:]) if err != nil { - writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid token"}) + writeJSON(w, 401, M{"error": "invalid token"}) return } - req.Header.Set("X-User-ID", userID) - req.Header.Set("X-Username", username) + req.Header.Set("X-User-ID", uid) + req.Header.Set("X-Username", uname) req.Header.Set("X-Role", role) next(w, req) } @@ -81,83 +137,209 @@ func (r *Router) authMW(next http.HandlerFunc) http.HandlerFunc { func (r *Router) adminMW(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { if req.Header.Get("X-Role") != "admin" { - writeJSON(w, http.StatusForbidden, map[string]string{"error": "admin access required"}) + writeJSON(w, 403, M{"error": "admin only"}) return } next(w, req) } } -// --- Public handlers --- +// --- Handlers --- func (r *Router) healthHandler(w http.ResponseWriter, _ *http.Request) { - writeJSON(w, http.StatusOK, map[string]string{"status": "ok", "service": "cyrene-gateway"}) + writeJSON(w, 200, M{"status": "ok", "service": "cyrene-gateway", "version": "2.0.0-dev"}) } func (r *Router) loginHandler(w http.ResponseWriter, req *http.Request) { if req.Method != "POST" { - writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "POST required"}) + writeJSON(w, 405, M{"error": "POST required"}) return } var body struct { Username string `json:"username"` Password string `json:"password"` } - if err := json.NewDecoder(req.Body).Decode(&body); err != nil { - writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"}) - return - } + json.NewDecoder(req.Body).Decode(&body) - mgr, ok := r.authMgr.(*StubAuthManager) + mgr, ok := r.deps.AuthMgr.(*StubAuthManager) if !ok { - writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "auth not configured"}) + writeJSON(w, 500, M{"error": "auth not configured"}) return } token, err := mgr.Login(body.Username, body.Password) if err != nil { - writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid credentials"}) + writeJSON(w, 401, M{"error": "invalid credentials"}) return } - writeJSON(w, http.StatusOK, map[string]string{"token": token}) + writeJSON(w, 200, M{"token": token}) } func (r *Router) registerHandler(w http.ResponseWriter, _ *http.Request) { - writeJSON(w, http.StatusOK, map[string]string{"message": "registration not implemented"}) + writeJSON(w, 200, M{"message": "registration not implemented"}) } -// --- Protected handlers --- +// --- Sessions --- func (r *Router) sessionsHandler(w http.ResponseWriter, req *http.Request) { switch req.Method { case "GET": - writeJSON(w, http.StatusOK, map[string]any{"sessions": []any{}}) + writeJSON(w, 200, M{"sessions": []any{}}) case "POST": - writeJSON(w, http.StatusCreated, map[string]string{"session_id": "stub-session"}) + writeJSON(w, 201, M{"session_id": randID()}) default: - writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"}) + writeJSON(w, 405, M{"error": "method not allowed"}) } } func (r *Router) sessionByIDHandler(w http.ResponseWriter, req *http.Request) { id := strings.TrimPrefix(req.URL.Path, "/api/v1/sessions/") - if req.Method == "DELETE" { - writeJSON(w, http.StatusNoContent, nil) - return + switch req.Method { + case "DELETE": + writeJSON(w, 204, nil) + case "GET": + writeJSON(w, 200, M{"session_id": id}) + default: + writeJSON(w, 405, M{"error": "method not allowed"}) } - writeJSON(w, http.StatusOK, map[string]string{"session_id": id}) } +// --- Memory --- + +func (r *Router) memorySearchHandler(w http.ResponseWriter, req *http.Request) { + if req.Method != "GET" { + writeJSON(w, 405, M{"error": "GET required"}) + return + } + if r.deps.MemorySvc == nil { + writeJSON(w, 503, M{"error": "memory service unavailable"}) + return + } + q := req.URL.Query().Get("q") + uid := req.Header.Get("X-User-ID") + + result, err := r.deps.MemorySvc.Search(req.Context(), uid, q, 10) + if err != nil { + writeJSON(w, 500, M{"error": err.Error()}) + return + } + writeJSON(w, 200, M{"data": result}) +} + +func (r *Router) memoryHandler(w http.ResponseWriter, req *http.Request) { + if r.deps.MemorySvc == nil { + writeJSON(w, 503, M{"error": "memory service unavailable"}) + return + } + + // Extract ID from path: /api/v1/memory/{id} + path := strings.TrimPrefix(req.URL.Path, "/api/v1/memory") + path = strings.TrimPrefix(path, "/") + userID := req.Header.Get("X-User-ID") + + switch { + case path == "" && req.Method == "GET": + result, _ := r.deps.MemorySvc.List(req.Context(), userID, 20) + writeJSON(w, 200, M{"data": result}) + + case path == "" && req.Method == "POST": + var body struct { + Content string `json:"content"` + Category string `json:"category"` + Priority float64 `json:"priority"` + } + json.NewDecoder(req.Body).Decode(&body) + if body.Category == "" { body.Category = "fact" } + mem, err := r.deps.MemorySvc.Add(req.Context(), userID, body.Content, body.Category, body.Priority) + if err != nil { + writeJSON(w, 500, M{"error": err.Error()}) + return + } + writeJSON(w, 201, M{"data": mem}) + + case path != "" && req.Method == "DELETE": + r.deps.MemorySvc.Delete(req.Context(), path) + writeJSON(w, 204, nil) + + case path != "" && req.Method == "GET": + mem, err := r.deps.MemorySvc.Get(req.Context(), path) + if err != nil { + writeJSON(w, 404, M{"error": "memory not found"}) + return + } + writeJSON(w, 200, M{"data": mem}) + + default: + writeJSON(w, 405, M{"error": "method not allowed"}) + } +} + +// --- Voice --- + +func (r *Router) voiceSTTHandler(w http.ResponseWriter, req *http.Request) { + if req.Method != "POST" { + writeJSON(w, 405, M{"error": "POST required"}) + return + } + if r.deps.VoiceSvc == nil { + writeJSON(w, 503, M{"error": "voice service unavailable"}) + return + } + var body struct{ AudioData []byte `json:"audio_data"` } + json.NewDecoder(req.Body).Decode(&body) + text, err := r.deps.VoiceSvc.Recognize(req.Context(), body.AudioData) + if err != nil { + writeJSON(w, 500, M{"error": err.Error()}) + return + } + writeJSON(w, 200, M{"text": text}) +} + +func (r *Router) voiceTTSHandler(w http.ResponseWriter, req *http.Request) { + if req.Method != "POST" { + writeJSON(w, 405, M{"error": "POST required"}) + return + } + if r.deps.VoiceSvc == nil { + writeJSON(w, 503, M{"error": "voice service unavailable"}) + return + } + var body struct { + Text string `json:"text"` + Voice string `json:"voice"` + } + json.NewDecoder(req.Body).Decode(&body) + if body.Voice == "" { body.Voice = "zh-CN-XiaoxiaoNeural" } + audio, err := r.deps.VoiceSvc.Synthesize(req.Context(), body.Text, body.Voice) + if err != nil { + writeJSON(w, 500, M{"error": err.Error()}) + return + } + writeJSON(w, 200, M{"audio_size": len(audio)}) +} + +// --- Admin --- + +var startTime = time.Now() + func (r *Router) adminDashboardHandler(w http.ResponseWriter, _ *http.Request) { - writeJSON(w, http.StatusOK, map[string]any{ - "dashboard": map[string]any{ - "status": "running", - "version": "2.0.0-dev", + writeJSON(w, 200, M{ + "dashboard": M{ + "status": "running", + "version": "2.0.0-dev", + "uptime_sec": int(time.Since(startTime).Seconds()), + "services": M{ + "memory": r.deps.MemorySvc != nil, + "voice": r.deps.VoiceSvc != nil, + "ai_core": r.deps.Orchestrator != nil, + }, }, }) } // --- Helpers --- +type M = map[string]any + func writeJSON(w http.ResponseWriter, status int, data any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) @@ -165,3 +347,14 @@ func writeJSON(w http.ResponseWriter, status int, data any) { json.NewEncoder(w).Encode(data) } } + +func randID() string { + b := make([]byte, 8) + rand.Read(b) + return hex.EncodeToString(b) +} + +func (r *Router) userID(req *http.Request) string { return req.Header.Get("X-User-ID") } + +// Ensure unused imports don't cause issues + diff --git a/backend/services/gateway/api/router_test.go b/backend/services/gateway/api/router_test.go index 0ba77d2..fa87b96 100644 --- a/backend/services/gateway/api/router_test.go +++ b/backend/services/gateway/api/router_test.go @@ -8,7 +8,7 @@ import ( ) func TestHealth(t *testing.T) { - r := New(NewStubAuthManager()) + r := New(Dependencies{AuthMgr: NewStubAuthManager()}) srv := httptest.NewServer(r) defer srv.Close() @@ -25,7 +25,7 @@ func TestHealth(t *testing.T) { func TestLoginAndProtectedEndpoint(t *testing.T) { auth := NewStubAuthManager() - r := New(auth) + r := New(Dependencies{AuthMgr: auth}) srv := httptest.NewServer(r) defer srv.Close() @@ -57,7 +57,7 @@ func TestLoginAndProtectedEndpoint(t *testing.T) { func TestLoginWrongPassword(t *testing.T) { auth := NewStubAuthManager() - r := New(auth) + r := New(Dependencies{AuthMgr: auth}) srv := httptest.NewServer(r) defer srv.Close() @@ -75,7 +75,7 @@ func TestLoginWrongPassword(t *testing.T) { func TestProtectedWithoutToken(t *testing.T) { auth := NewStubAuthManager() - r := New(auth) + r := New(Dependencies{AuthMgr: auth}) srv := httptest.NewServer(r) defer srv.Close() @@ -115,7 +115,7 @@ func TestStubAuthManager(t *testing.T) { } func TestCORSHeaders(t *testing.T) { - r := New(NewStubAuthManager()) + r := New(Dependencies{AuthMgr: NewStubAuthManager()}) srv := httptest.NewServer(r) defer srv.Close() diff --git a/backend/services/gateway/ws/hub.go b/backend/services/gateway/ws/hub.go index bde709e..bc26a6c 100644 --- a/backend/services/gateway/ws/hub.go +++ b/backend/services/gateway/ws/hub.go @@ -1,13 +1,16 @@ -// Package ws provides WebSocket connection management for Cyrene. +// Package ws provides WebSocket connection management. package ws import ( + "context" "encoding/json" "log" "net/http" "sync" "github.com/gorilla/websocket" + + "git.yeij.top/AskaEth/Cyrene_v2/services/ai-core/model" ) var upgrader = websocket.Upgrader{ @@ -16,42 +19,50 @@ var upgrader = websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true }, } +// Orchestrator is the interface for the AI dialogue pipeline. +type Orchestrator interface { + ProcessInput(ctx context.Context, sessionID, userMessage string) (<-chan model.StreamToken, *model.Intent, error) +} + // Hub manages all active WebSocket connections. type Hub struct { - mu sync.RWMutex - clients map[*Client]bool - - register chan *Client - unregister chan *Client - broadcast chan []byte + mu sync.RWMutex + clients map[*Client]bool + register chan *Client + unregister chan *Client + broadcast chan []byte + orchestrator Orchestrator } // Client represents a single WebSocket connection. type Client struct { - hub *Hub - conn *websocket.Conn - send chan []byte - UserID string - closed bool - mu sync.Mutex + hub *Hub + conn *websocket.Conn + send chan []byte + UserID string + SessionID string + closed bool + mu sync.Mutex } -// Message represents a WebSocket message envelope. -type Message struct { +// WSMessage represents a client-server message. +type WSMessage struct { Type string `json:"type"` Content string `json:"content,omitempty"` SessionID string `json:"session_id,omitempty"` Timestamp int64 `json:"timestamp,omitempty"` Error string `json:"error,omitempty"` + Devices []any `json:"devices,omitempty"` } -// NewHub creates and starts a new WebSocket hub. -func NewHub() *Hub { +// NewHub creates a WebSocket hub with an optional orchestrator. +func NewHub(orch Orchestrator) *Hub { h := &Hub{ - clients: make(map[*Client]bool), - register: make(chan *Client), - unregister: make(chan *Client), - broadcast: make(chan []byte, 256), + clients: make(map[*Client]bool), + register: make(chan *Client), + unregister: make(chan *Client), + broadcast: make(chan []byte, 256), + orchestrator: orch, } go h.run() return h @@ -60,28 +71,27 @@ func NewHub() *Hub { func (h *Hub) run() { for { select { - case client := <-h.register: + case c := <-h.register: h.mu.Lock() - h.clients[client] = true + h.clients[c] = true + n := len(h.clients) h.mu.Unlock() - log.Printf("[ws] client connected (%d total)", len(h.clients)) + log.Printf("[ws] client connected (total=%d)", n) - case client := <-h.unregister: + case c := <-h.unregister: h.mu.Lock() - if _, ok := h.clients[client]; ok { - delete(h.clients, client) - close(client.send) + if _, ok := h.clients[c]; ok { + delete(h.clients, c) + close(c.send) } h.mu.Unlock() - log.Printf("[ws] client disconnected (%d remaining)", len(h.clients)) - case message := <-h.broadcast: + case msg := <-h.broadcast: h.mu.RLock() - for client := range h.clients { + for c := range h.clients { select { - case client.send <- message: + case c.send <- msg: default: - // Client too slow, skip } } h.mu.RUnlock() @@ -97,20 +107,16 @@ func (h *Hub) HandleUpgrade(w http.ResponseWriter, r *http.Request) { return } - userID := r.URL.Query().Get("user_id") - if userID == "" { - userID = "anonymous" - } + uid := r.URL.Query().Get("user_id") + if uid == "" { uid = "anonymous" } client := &Client{ hub: h, conn: conn, - send: make(chan []byte, 64), - UserID: userID, + send: make(chan []byte, 128), + UserID: uid, } - h.register <- client - go client.writePump() go client.readPump() } @@ -122,38 +128,77 @@ func (c *Client) readPump() { }() for { - _, msgBytes, err := c.conn.ReadMessage() + _, raw, err := c.conn.ReadMessage() if err != nil { break } - - var msg Message - if err := json.Unmarshal(msgBytes, &msg); err != nil { + var msg WSMessage + if err := json.Unmarshal(raw, &msg); err != nil { continue } - // Echo back for now (will route to AI-Core later) - reply := Message{ - Type: "response", - Content: "echo: " + msg.Content, - SessionID: msg.SessionID, + switch msg.Type { + case "message": + c.handleChatMessage(msg) + default: + c.sendJSON(WSMessage{Type: "response", Content: "unknown message type: " + msg.Type}) } - replyBytes, _ := json.Marshal(reply) - c.send <- replyBytes + } +} + +func (c *Client) handleChatMessage(msg WSMessage) { + c.SessionID = msg.SessionID + + // If orchestrator not available, echo + if c.hub.orchestrator == nil { + c.sendJSON(WSMessage{Type: "response", Content: "[系统] AI 核心未配置,当前为 echo 模式: " + msg.Content, SessionID: msg.SessionID}) + return + } + + // Call orchestrator + tokenCh, intent, err := c.hub.orchestrator.ProcessInput(context.Background(), msg.SessionID, msg.Content) + if err != nil { + c.sendJSON(WSMessage{Type: "error", Error: err.Error()}) + return + } + + // Send intent info + if intent != nil { + c.sendJSON(WSMessage{Type: "response", Content: "[Intent: " + intent.Type + "]", SessionID: msg.SessionID}) + } + + // Stream tokens + for token := range tokenCh { + if token.Done { + c.sendJSON(WSMessage{Type: "stream_end", SessionID: msg.SessionID}) + break + } + c.sendJSON(WSMessage{Type: "stream_chunk", Content: token.Content, SessionID: msg.SessionID}) } } func (c *Client) writePump() { defer c.conn.Close() for msg := range c.send { - if err := c.conn.WriteMessage(websocket.TextMessage, msg); err != nil { + c.mu.Lock() + err := c.conn.WriteMessage(websocket.TextMessage, msg) + c.mu.Unlock() + if err != nil { return } } } +func (c *Client) sendJSON(v any) { + data, _ := json.Marshal(v) + select { + case c.send <- data: + default: + } +} + // Broadcast sends a message to all connected clients. -func (h *Hub) Broadcast(msg Message) error { +func (h *Hub) Broadcast(msg any) error { data, err := json.Marshal(msg) if err != nil { return err diff --git a/backend/services/memory/adapter.go b/backend/services/memory/adapter.go new file mode 100644 index 0000000..f2b3d23 --- /dev/null +++ b/backend/services/memory/adapter.go @@ -0,0 +1,86 @@ +package memory + +import "context" + +// APIAdapter adapts the memory Service to a simplified API interface. +// This bridges between the rich domain model and the Gateway's REST handler expectations. +type APIAdapter struct { + svc *Service +} + +// APIEntry is the simplified memory entry for REST API responses. +type APIEntry struct { + ID string `json:"id"` + UserID string `json:"user_id"` + Content string `json:"content"` + Summary string `json:"summary"` + Keywords []string `json:"keywords"` + Category string `json:"category"` + Priority float64 `json:"priority"` +} + +// NewAPIAdapter wraps a Service for REST API consumption. +func NewAPIAdapter(svc *Service) *APIAdapter { + return &APIAdapter{svc: svc} +} + +func (a *APIAdapter) Search(ctx context.Context, userID, query string, limit int) (any, error) { + memories, err := a.svc.Search(ctx, SearchQuery{UserID: userID, Query: query, Limit: limit}) + if err != nil { + return nil, err + } + result := make([]APIEntry, len(memories)) + for i, m := range memories { + result[i] = toEntry(m) + } + return result, nil +} + +func (a *APIAdapter) List(ctx context.Context, userID string, limit int) (any, error) { + memories, err := a.svc.List(ctx, userID, limit) + if err != nil { + return nil, err + } + result := make([]APIEntry, len(memories)) + for i, m := range memories { + result[i] = toEntry(m) + } + return result, nil +} + +func (a *APIAdapter) Add(ctx context.Context, userID, content, category string, priority float64) (any, error) { + m, err := a.svc.Add(ctx, userID, "", content, category, priority) + if err != nil { + return nil, err + } + e := toEntry(*m) + return &e, nil +} + +func (a *APIAdapter) Get(ctx context.Context, id string) (any, error) { + m, err := a.svc.GetByID(ctx, id) + if err != nil { + return nil, err + } + e := toEntry(*m) + return &e, nil +} + +func (a *APIAdapter) Delete(ctx context.Context, id string) error { + return a.svc.Delete(ctx, id) +} + +func toEntry(m Memory) APIEntry { + return APIEntry{ + ID: m.ID, + UserID: m.UserID, + Content: m.Content, + Summary: m.Summary, + Keywords: m.Keywords, + Category: m.Category, + Priority: m.Priority, + } +} + +// Ensure we use context +var _ = context.Background