feat(gateway): persist chat sessions and messages in PostgreSQL
- store: new chat_sessions/chat_messages tables with CRUD, ILIKE message search, and auto-titling from the first user message - context: optional Persister on the sliding-window store — messages are saved asynchronously on Add, sessions not in memory are restored from the database on Get - orchestrator: ProcessInput/FinishRound now carry userID so persisted messages have an owner - api: real session handlers (list/create/delete), message history and clear endpoints, working message search and three-format export (markdown/txt/json), fixing the short-ID panic in export - main: migration, store wiring, ctxStore persister adapter Tests: chat store handler lifecycle (fake store), context persister save/restore, stub auth login flow updated.
This commit is contained in:
1 parent
f44bf26050
commit
f8196c111c
11 files changed
+787
-27
No files matched your search
@@ -12,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/services/ai-core/model"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/services/gateway/store"
|
||||
)
|
||||
|
||||
// --- Interfaces for injected services ---
|
||||
@@ -51,7 +52,7 @@ type VoiceService interface {
|
||||
|
||||
// OrchestratorPipeline handles the main dialogue flow.
|
||||
type OrchestratorPipeline interface {
|
||||
ProcessInput(ctx context.Context, sessionID, userMessage string) (<-chan model.StreamToken, *model.Intent, error)
|
||||
ProcessInput(ctx context.Context, sessionID, userID, userMessage string) (<-chan model.StreamToken, *model.Intent, error)
|
||||
}
|
||||
|
||||
// WebSocketHub manages WS connections.
|
||||
@@ -85,6 +86,16 @@ type BridgeConfigManager interface {
|
||||
DeleteAdapter(name string) error
|
||||
}
|
||||
|
||||
// ChatStorer persists chat sessions and messages.
|
||||
type ChatStorer interface {
|
||||
CreateSession(ctx context.Context, id, userID, title string) (*store.Session, error)
|
||||
ListSessions(ctx context.Context, userID string) ([]store.Session, error)
|
||||
DeleteSession(ctx context.Context, id, userID string) error
|
||||
ListMessages(ctx context.Context, sessionID string, limit int) ([]store.Message, error)
|
||||
ClearMessages(ctx context.Context, sessionID string) error
|
||||
SearchMessages(ctx context.Context, userID, query string, limit int) ([]store.Message, error)
|
||||
}
|
||||
|
||||
// Dependencies holds all injectable services.
|
||||
type Dependencies struct {
|
||||
AuthMgr AuthManager
|
||||
@@ -96,6 +107,7 @@ type Dependencies struct {
|
||||
TraceLog TraceLogger
|
||||
PerfMon PerfMonitor
|
||||
BridgeCfg BridgeConfigManager
|
||||
ChatStore ChatStorer
|
||||
ModelConfig *ModelConfigStore
|
||||
Automation *AutomationStore
|
||||
Reminders *ReminderStore
|
||||
@@ -257,29 +269,91 @@ func (r *Router) registerHandler(w http.ResponseWriter, req *http.Request) {
|
||||
// --- Sessions ---
|
||||
|
||||
func (r *Router) sessionsHandler(w http.ResponseWriter, req *http.Request) {
|
||||
if r.deps.ChatStore == nil {
|
||||
writeJSON(w, 503, M{"error": "chat store unavailable"})
|
||||
return
|
||||
}
|
||||
uid := r.userID(req)
|
||||
switch req.Method {
|
||||
case "GET":
|
||||
writeJSON(w, 200, M{"sessions": []any{}})
|
||||
sessions, err := r.deps.ChatStore.ListSessions(req.Context(), uid)
|
||||
if err != nil {
|
||||
writeJSON(w, 500, M{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, 200, M{"sessions": sessions})
|
||||
case "POST":
|
||||
writeJSON(w, 201, M{"session_id": randID()})
|
||||
var body struct {
|
||||
Title string `json:"title"`
|
||||
}
|
||||
json.NewDecoder(req.Body).Decode(&body)
|
||||
sess, err := r.deps.ChatStore.CreateSession(req.Context(), randID(), uid, body.Title)
|
||||
if err != nil {
|
||||
writeJSON(w, 500, M{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, 201, M{"session_id": sess.ID, "title": sess.Title})
|
||||
default:
|
||||
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/")
|
||||
// Handle export
|
||||
if strings.HasSuffix(id, "/export") {
|
||||
id = strings.TrimSuffix(id, "/export")
|
||||
if r.deps.ChatStore == nil {
|
||||
writeJSON(w, 503, M{"error": "chat store unavailable"})
|
||||
return
|
||||
}
|
||||
rest := strings.TrimPrefix(req.URL.Path, "/api/v1/sessions/")
|
||||
|
||||
// Sub-resources: /sessions/{id}/export and /sessions/{id}/messages
|
||||
if strings.HasSuffix(rest, "/export") {
|
||||
r.sessionExportHandler(w, req)
|
||||
return
|
||||
}
|
||||
if strings.HasSuffix(rest, "/messages") {
|
||||
r.sessionMessagesHandler(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
id := rest
|
||||
switch req.Method {
|
||||
case "DELETE":
|
||||
if err := r.deps.ChatStore.DeleteSession(req.Context(), id, r.userID(req)); err != nil {
|
||||
writeJSON(w, 404, M{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, 204, nil)
|
||||
case "GET":
|
||||
writeJSON(w, 200, M{"session_id": id})
|
||||
messages, err := r.deps.ChatStore.ListMessages(req.Context(), id, 100)
|
||||
if err != nil {
|
||||
writeJSON(w, 500, M{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, 200, M{"session_id": id, "messages": messages})
|
||||
default:
|
||||
writeJSON(w, 405, M{"error": "method not allowed"})
|
||||
}
|
||||
}
|
||||
|
||||
// sessionMessagesHandler serves GET (recent history) and DELETE (clear).
|
||||
func (r *Router) sessionMessagesHandler(w http.ResponseWriter, req *http.Request) {
|
||||
id := strings.TrimSuffix(strings.TrimPrefix(req.URL.Path, "/api/v1/sessions/"), "/messages")
|
||||
switch req.Method {
|
||||
case "GET":
|
||||
limit := 0
|
||||
fmt.Sscanf(req.URL.Query().Get("limit"), "%d", &limit)
|
||||
messages, err := r.deps.ChatStore.ListMessages(req.Context(), id, limit)
|
||||
if err != nil {
|
||||
writeJSON(w, 500, M{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, 200, M{"messages": messages})
|
||||
case "DELETE":
|
||||
if err := r.deps.ChatStore.ClearMessages(req.Context(), id); err != nil {
|
||||
writeJSON(w, 500, M{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, 204, nil)
|
||||
default:
|
||||
writeJSON(w, 405, M{"error": "method not allowed"})
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user