feat(gateway): wire real services into Gateway API handlers

- Router accepts full Dependencies (memory/voice/orchestrator/WShub)
- Memory API: CRUD + search endpoints with PostgreSQL persistence
- Voice API: STT/TTS endpoints
- Admin dashboard: real uptime + service status
- WebSocket chat: routes to AI-Core orchestrator pipeline
- APIAdapter bridges memory domain model to REST interface
- Smoke test: memory CRUD + session creation verified
This commit is contained in:
Aska committed 2026-08-12 12:04:31 +08:00
1 parent 4a71965686
commit 6811480786
5 files changed
+448 -111

No files matched your search

+15 -2
View File
@@ -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())
+243 -50
View File
@@ -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
+5 -5
View File
@@ -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()
+99 -54
View File
@@ -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
+86
View File
@@ -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