mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-06 04:23:15 -05:00
feat(studio): filter the chat list by origin
GET /api/chats takes an optional origin parameter: person (chats a person started), automated (every other chat) or one origin kind such as trigger or connector. The limit counts the chats kept, so a flood of chats that a connector or a trigger started cannot push the chats of a person off a page, and the other way round. Without the parameter the list is unchanged. Assisted-by: Claude Code:claude-sonnet-5-5 Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
4 files changed
+103
-2
No files matched your search
@@ -159,6 +159,23 @@ func failFor(c echo.Context, err error) error {
|
||||
})
|
||||
}
|
||||
|
||||
// originFilter turns the origin parameter of the chat list into a test on a
|
||||
// chat. person keeps the chats a person started (no origin), automated keeps
|
||||
// every other chat, and any other word keeps the chats of that origin kind. An
|
||||
// empty value is no filter (nil), so a client that does not send it gets all.
|
||||
func originFilter(origin string) func(session.ChatRecord) bool {
|
||||
switch origin {
|
||||
case "":
|
||||
return nil
|
||||
case "person":
|
||||
return func(r session.ChatRecord) bool { return r.Origin == "" }
|
||||
case "automated":
|
||||
return func(r session.ChatRecord) bool { return r.Origin != "" }
|
||||
default:
|
||||
return func(r session.ChatRecord) bool { return r.Origin == origin }
|
||||
}
|
||||
}
|
||||
|
||||
func conversation(rec session.ChatRecord, info store.TurnInfo) schema.ChatsConversation {
|
||||
return schema.ChatsConversation{
|
||||
ID: rec.ID, Title: rec.Title, Model: rec.Model, Kind: rec.Kind, ParentID: rec.ParentID,
|
||||
@@ -247,6 +264,7 @@ func (h *Handlers) Create(c echo.Context) error {
|
||||
// @Tags chats
|
||||
// @Param limit query int false "most conversations to return (default 50, at most 200)"
|
||||
// @Param kind query string false "with chat, sub-agent runs are left out and the limit counts chats"
|
||||
// @Param origin query string false "only chats of one origin, with sub-agent runs left out and the limit counting the chats kept: person (a person started it), automated (anything else) or one origin kind (trigger, connector, job, page). Without it, all chats"
|
||||
// @Success 200 {object} schema.ChatsConversationList "Conversations, including sub-agent runs unless kind=chat"
|
||||
// @Router /api/chats [get]
|
||||
func (h *Handlers) List(c echo.Context) error {
|
||||
@@ -256,6 +274,12 @@ func (h *Handlers) List(c echo.Context) error {
|
||||
if c.QueryParam("kind") == "chat" {
|
||||
list = h.svc.ListChats
|
||||
}
|
||||
// An origin filter reads chats only: a sub-agent run has no origin of its own.
|
||||
if keep := originFilter(c.QueryParam("origin")); keep != nil {
|
||||
list = func(ctx context.Context, user string, limit int) ([]session.ChatRecord, error) {
|
||||
return h.svc.ListChatsWhere(ctx, user, limit, keep)
|
||||
}
|
||||
}
|
||||
recs, err := list(c.Request().Context(), user, limit)
|
||||
if err != nil {
|
||||
return failFor(c, err)
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
//go:build auth
|
||||
|
||||
package routes_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/mudler/LocalAI/core/services/studio/session"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Chats list origin filter", func() {
|
||||
var s *studioServer
|
||||
seed := func(user, id, origin, originID string, age int) {
|
||||
now := time.Now().Add(time.Duration(age) * time.Minute)
|
||||
Expect(s.store.CreateChat(context.Background(), session.ChatRecord{
|
||||
ID: id, Principal: user, Kind: session.KindChat, Title: id, Origin: origin, OriginID: originID, Created: now, Updated: now,
|
||||
})).To(Succeed())
|
||||
}
|
||||
ids := func(user, query string) []string {
|
||||
rec := s.do(user, http.MethodGet, "/api/chats?"+query, nil)
|
||||
Expect(rec.Code).To(Equal(http.StatusOK), rec.Body.String())
|
||||
var out []string
|
||||
for _, c := range decodeJSON(rec)["conversations"].([]any) {
|
||||
out = append(out, c.(map[string]any)["id"].(string))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
BeforeEach(func() {
|
||||
s = newStudioServer()
|
||||
// Oldest first: the person's chats are older than the bot's.
|
||||
seed("alice", "mine-1", "", "", 1)
|
||||
seed("alice", "mine-2", "", "", 2)
|
||||
for i := range 5 {
|
||||
seed("alice", fmt.Sprintf("bot-%d", i), session.OriginConnector, "binding-1", 10+i)
|
||||
}
|
||||
seed("alice", "cron-1", session.OriginTrigger, "trigger-1", 20)
|
||||
seed("bob", "bob-bot", session.OriginConnector, "binding-2", 30)
|
||||
})
|
||||
|
||||
It("returns every chat when origin is absent, as old clients expect", func() {
|
||||
Expect(ids("alice", "kind=chat")).To(HaveLen(8))
|
||||
})
|
||||
|
||||
It("person leaves out chats that started by themselves, and the limit counts the chats kept", func() {
|
||||
// Seven newer automated chats must not push the two person chats off a page of 2.
|
||||
Expect(ids("alice", "kind=chat&origin=person&limit=2")).To(Equal([]string{"mine-2", "mine-1"}))
|
||||
})
|
||||
|
||||
It("automated returns the others and a kind returns only that kind", func() {
|
||||
Expect(ids("alice", "origin=automated")).To(HaveLen(6))
|
||||
Expect(ids("alice", "origin=trigger")).To(Equal([]string{"cron-1"}))
|
||||
Expect(ids("alice", "origin=connector&limit=3")).To(HaveLen(3))
|
||||
Expect(ids("alice", "origin=job")).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("never shows another user's chats", func() {
|
||||
Expect(ids("alice", "origin=automated")).NotTo(ContainElement("bob-bot"))
|
||||
})
|
||||
})
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/mudler/LocalAI/core/services/studio/loops"
|
||||
"github.com/mudler/LocalAI/core/services/studio/notify"
|
||||
"github.com/mudler/LocalAI/core/services/studio/provider"
|
||||
"github.com/mudler/LocalAI/core/services/studio/session"
|
||||
"github.com/mudler/LocalAI/core/services/studio/store"
|
||||
"github.com/mudler/LocalAI/core/services/studio/studiotest"
|
||||
"github.com/mudler/LocalAI/core/services/studio/triggers"
|
||||
@@ -45,6 +46,8 @@ type studioServer struct {
|
||||
notify *notify.Service
|
||||
host *studio.Triggers
|
||||
svc *studio.Service
|
||||
// store is the chat store behind svc, for specs that seed chats a person did not start.
|
||||
store session.Store
|
||||
}
|
||||
|
||||
// studioTriggerOptions lets a spec set the clock or the limits of the trigger
|
||||
@@ -133,7 +136,7 @@ func newStudioServerWith(f *files.Service, mod func(*studio.Deps)) *studioServer
|
||||
DeferCleanup(conns.Close)
|
||||
DeferCleanup(host.Close)
|
||||
|
||||
s := &studioServer{e: echo.New(), harness: h, sessions: map[string]string{}, notify: nsvc, host: host, svc: svc}
|
||||
s := &studioServer{e: echo.New(), harness: h, sessions: map[string]string{}, notify: nsvc, host: host, svc: svc, store: st}
|
||||
for _, id := range []string{"alice", "bob", "carol", "dave", "root"} {
|
||||
role := auth.RoleUser
|
||||
if id == "root" {
|
||||
|
||||
@@ -520,6 +520,14 @@ func (s *Service) List(ctx context.Context, user string, limit int) ([]session.C
|
||||
// limit applies to chats, so a burst of newer runs cannot push older chats out
|
||||
// of the page: the store is read page by page until enough chats are found.
|
||||
func (s *Service) ListChats(ctx context.Context, user string, limit int) ([]session.ChatRecord, error) {
|
||||
return s.ListChatsWhere(ctx, user, limit, nil)
|
||||
}
|
||||
|
||||
// ListChatsWhere is ListChats for the chats that keep returns true for (nil
|
||||
// keeps all). The limit counts the chats kept, so a flood of chats that a
|
||||
// connector or a trigger started cannot push a person's chats out of a page of
|
||||
// person-started chats, and the other way round.
|
||||
func (s *Service) ListChatsWhere(ctx context.Context, user string, limit int, keep func(session.ChatRecord) bool) ([]session.ChatRecord, error) {
|
||||
var out []session.ChatRecord
|
||||
var cur session.ChatCursor
|
||||
for {
|
||||
@@ -528,7 +536,7 @@ func (s *Service) ListChats(ctx context.Context, user string, limit int) ([]sess
|
||||
return nil, err
|
||||
}
|
||||
for _, r := range page {
|
||||
if r.Kind == session.KindSubAgent {
|
||||
if r.Kind == session.KindSubAgent || (keep != nil && !keep(r)) {
|
||||
continue
|
||||
}
|
||||
out = append(out, r)
|
||||
|
||||
Reference in new issue
Block a user