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
@@ -37,10 +37,12 @@ import (
|
||||
tracepkg "git.yeij.top/AskaEth/Cyrene_v2/services/ai-core/trace"
|
||||
perfpkg "git.yeij.top/AskaEth/Cyrene_v2/services/ai-core/perf"
|
||||
aiCoreCtx "git.yeij.top/AskaEth/Cyrene_v2/services/ai-core/context"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/services/ai-core/model"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/services/ai-core/orchestrator"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/services/ai-core/persona"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/services/ai-core/subsession"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/services/gateway/api"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/services/gateway/store"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/services/gateway/ws"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/services/memory"
|
||||
bridge "git.yeij.top/AskaEth/Cyrene_v2/services/platform-bridge"
|
||||
@@ -79,6 +81,7 @@ func main() {
|
||||
} else {
|
||||
defer database.Close()
|
||||
db.RunMigration(context.Background(), database, "memories", db.SplitSQL(memory.CreateTableSQL()))
|
||||
db.RunMigration(context.Background(), database, "chat", db.SplitSQL(store.CreateTableSQL()))
|
||||
}
|
||||
|
||||
// === 3. NATS (in-process) ===
|
||||
@@ -161,6 +164,14 @@ func main() {
|
||||
ctxStore := aiCoreCtx.NewStore(4000, 50)
|
||||
logger.Infof("[context] sliding window ready (4000 tokens, 50 turns)")
|
||||
|
||||
// === 9.5 Chat persistence (sessions + messages) ===
|
||||
var chatStore *store.ChatStore
|
||||
if database != nil {
|
||||
chatStore = store.NewChatStore(database)
|
||||
ctxStore.SetPersister(&chatPersisterAdapter{s: chatStore})
|
||||
logger.Infof("[chat] session/message persistence ready")
|
||||
}
|
||||
|
||||
// === 10. AI-Core: Sub-session Manager ===
|
||||
subMgr := subsession.NewManager(
|
||||
&subsession.GeneralProvider{},
|
||||
@@ -238,6 +249,9 @@ func main() {
|
||||
if memSvc != nil {
|
||||
gwDeps.MemorySvc = memory.NewAPIAdapter(memSvc)
|
||||
}
|
||||
if chatStore != nil {
|
||||
gwDeps.ChatStore = chatStore
|
||||
}
|
||||
gwDeps.VoiceSvc = voiceSvc
|
||||
|
||||
wsHub := ws.NewHub(orch)
|
||||
@@ -331,6 +345,31 @@ func (a *perfAdapter) History() []map[string]any {
|
||||
return result
|
||||
}
|
||||
|
||||
// chatPersisterAdapter adapts store.ChatStore to aiCoreCtx.Persister.
|
||||
type chatPersisterAdapter struct{ s *store.ChatStore }
|
||||
|
||||
func (a *chatPersisterAdapter) SaveMessage(sessionID, userID string, msg model.Message) {
|
||||
if userID == "" {
|
||||
userID = "unknown"
|
||||
}
|
||||
if err := a.s.AddMessage(context.Background(), sessionID, userID, msg.Role, msg.Content); err != nil {
|
||||
logger.Infof("[chat] persist message failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *chatPersisterAdapter) LoadRecent(sessionID string, limit int) []model.Message {
|
||||
msgs, err := a.s.ListMessages(context.Background(), sessionID, limit)
|
||||
if err != nil {
|
||||
logger.Infof("[chat] load history failed: %v", err)
|
||||
return nil
|
||||
}
|
||||
out := make([]model.Message, len(msgs))
|
||||
for i, m := range msgs {
|
||||
out[i] = model.Message{SessionID: m.SessionID, Role: m.Role, Content: m.Content, CreatedAt: m.CreatedAt}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// toolExecAdapter adapts the plugin registry to orchestrator.ToolExecutor.
|
||||
type toolExecAdapter struct{ reg *plugin.Registry }
|
||||
|
||||
@@ -389,7 +428,7 @@ func loadAdaptersFromConfig(router *bridge.Router, cfg *bridge.BridgeConfig, orc
|
||||
|
||||
router.SetMessageHandler(func(msg *bridge.UnifiedMessage) {
|
||||
sessionID := "bridge-" + msg.Source
|
||||
tokenCh, _, err := orch.ProcessInput(context.Background(), sessionID, msg.Content)
|
||||
tokenCh, _, err := orch.ProcessInput(context.Background(), sessionID, msg.SenderID, msg.Content)
|
||||
if err != nil { return }
|
||||
var fc string
|
||||
for t := range tokenCh { fc += t.Content }
|
||||
|
||||
Reference in new issue
Block a user