feat: a new chat is named by the model in two or three words, asked aside; the machine's name gets its room in the chat's header
This commit is contained in:
1 parent
6442c39360
commit
80ed7d4ffa
4 files changed
+90
-3
No files matched your search
@@ -10,6 +10,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -537,8 +538,13 @@ func (s *Server) handleAIChat(w http.ResponseWriter, r *http.Request) {
|
||||
in.Chat = s.aiNewChat(a.ID)
|
||||
}
|
||||
w.Header().Set("X-Chat-Id", strconv.FormatInt(in.Chat, 10))
|
||||
fresh := in.Message != "" && len(s.aiHistoryOf(a.ID, in.Chat, 1)) == 0
|
||||
s.aiAppendTo(a.ID, in.Chat, "result", in.Result)
|
||||
s.aiAppendTo(a.ID, in.Chat, "user", in.Message)
|
||||
if fresh {
|
||||
// A new chat gets a short name from the model, aside.
|
||||
go s.aiNameChat(a.ID, in.Chat, in.Message)
|
||||
}
|
||||
msgs := aiWindow(s.aiHistoryOf(a.ID, in.Chat, 60), 24000)
|
||||
if len(in.Context) > 30000 {
|
||||
in.Context = utf8Safe(in.Context[len(in.Context)-30000:])
|
||||
@@ -598,6 +604,45 @@ func (s *Server) handleAIChat(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// aiNameChat asks the model for a two-or-three-word name of the chat its
|
||||
// first question opened, and gives the chat that name.
|
||||
func (s *Server) aiNameChat(owner, chat int64, question string) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
|
||||
defer cancel()
|
||||
if r := []rune(question); len(r) > 600 {
|
||||
question = string(r[:600])
|
||||
}
|
||||
var b strings.Builder
|
||||
err := aiStream(ctx, s.aiConfig(), "Придумай короткое название чата по первому вопросу пользователя: 2-3 слова, по-русски, "+
|
||||
"без кавычек и точки в конце. Ответь только названием.", []aiMsg{{Role: "user", Content: question}}, func(t string) { b.WriteString(t) })
|
||||
if err != nil {
|
||||
log.Printf("ai: naming chat %d: %v", chat, err)
|
||||
return
|
||||
}
|
||||
if t := chatTitle(b.String()); t != "" {
|
||||
_, _ = s.db.sql.Exec(`UPDATE ai_chats SET title = ? WHERE id = ? AND admin_id = ?`, t, chat, owner)
|
||||
}
|
||||
}
|
||||
|
||||
// chatTitle tidies what the model gave for a name: the first line, no
|
||||
// thinking, quotes or end mark, four words at most.
|
||||
func chatTitle(s string) string {
|
||||
s = stripThink(s)
|
||||
if i := strings.IndexByte(strings.TrimSpace(s), '\n'); i >= 0 {
|
||||
s = strings.TrimSpace(s)[:i]
|
||||
}
|
||||
s = strings.Trim(strings.TrimSpace(s), "\"'«»„“”`*#.!:;")
|
||||
w := strings.Fields(s)
|
||||
if len(w) > 4 {
|
||||
w = w[:4]
|
||||
}
|
||||
s = strings.Join(w, " ")
|
||||
if r := []rune(s); len(r) > 40 {
|
||||
s = string(r[:40])
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// GET /api/panel/ai/chats - the user's chats, newest first.
|
||||
func (s *Server) handleAIChats(w http.ResponseWriter, r *http.Request) {
|
||||
a := s.currentAdmin(r)
|
||||
|
||||
@@ -150,7 +150,8 @@ func TestAIChatNeedsUserOn(t *testing.T) {
|
||||
}
|
||||
// The talk is kept: the question and the answer.
|
||||
cs := s.aiChats(a.ID)
|
||||
if len(cs) != 1 || cs[0].Title != "hi" || w.Header().Get("X-Chat-Id") != strconv.FormatInt(cs[0].ID, 10) {
|
||||
// Named by the question at once (the model may rename it a moment later).
|
||||
if len(cs) != 1 || cs[0].Title == "" || w.Header().Get("X-Chat-Id") != strconv.FormatInt(cs[0].ID, 10) {
|
||||
t.Fatalf("chats: %+v %q", cs, w.Header().Get("X-Chat-Id"))
|
||||
}
|
||||
if h := s.aiHistoryOf(a.ID, cs[0].ID, 10); len(h) != 2 || h[0].Content != "hi" || h[1].Content != "ok" {
|
||||
@@ -266,3 +267,37 @@ func TestAIThinkingKept(t *testing.T) {
|
||||
t.Fatalf("the model gets the thinking back: %+v", win)
|
||||
}
|
||||
}
|
||||
|
||||
// A new chat is named by the model: its first question in two or three
|
||||
// words, asked aside so the answer does not wait.
|
||||
func TestAIChatTitle(t *testing.T) {
|
||||
s, c := newTestServer(t)
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var in struct {
|
||||
Messages []aiMsg `json:"messages"`
|
||||
}
|
||||
_ = json.NewDecoder(r.Body).Decode(&in)
|
||||
reply := "ok"
|
||||
if strings.Contains(in.Messages[0].Content, "название") {
|
||||
reply = "«Выбор поисковика.»"
|
||||
}
|
||||
b, _ := json.Marshal(reply)
|
||||
fmt.Fprintf(w, "data: {\"choices\":[{\"delta\":{\"content\":%s}}]}\n\ndata: [DONE]\n\n", b)
|
||||
}))
|
||||
defer srv.Close()
|
||||
_ = s.db.SetConfig(cfgAIType, "openai")
|
||||
_ = s.db.SetConfig(cfgAIURL, srv.URL)
|
||||
a, _ := s.db.AdminByUsername("adm")
|
||||
_ = s.db.SetConfig(aiUserKey(a.ID), "1")
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/panel/ai/chat", strings.NewReader(`{"message":"какой поисковик ты используешь сейчас"}`))
|
||||
req.AddCookie(c)
|
||||
w := httptest.NewRecorder()
|
||||
s.Handler().ServeHTTP(w, req)
|
||||
for i := 0; i < 50; i++ {
|
||||
if cs := s.aiChats(a.ID); len(cs) == 1 && cs[0].Title == "Выбор поисковика" {
|
||||
return
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
t.Fatalf("title: %+v", s.aiChats(a.ID))
|
||||
}
|
||||
@@ -483,7 +483,11 @@
|
||||
});
|
||||
// A new chat gets its id (and its name) from the answer.
|
||||
const cid = Number(r.headers.get('X-Chat-Id')) || 0;
|
||||
if (cid && cid !== chatId) { setChat(cid); loadChats(); }
|
||||
if (cid && cid !== chatId) {
|
||||
setChat(cid); loadChats();
|
||||
// The model names the new chat a moment later.
|
||||
setTimeout(loadChats, 4000); setTimeout(loadChats, 12000);
|
||||
}
|
||||
if (r.redirected && new URL(r.url).pathname === '/login') { location.href = '/login'; return; }
|
||||
const contentType = r.headers.get('content-type') || '';
|
||||
if (!r.ok) throw new Error(responseError(r.status, contentType, await r.text()));
|
||||
|
||||
@@ -852,7 +852,10 @@ body.touch.term-active .ai-fab { bottom:104px; }
|
||||
.ai-fab:hover { transform:scale(1.06); box-shadow:0 10px 28px rgba(91,140,255,.45); }
|
||||
.ai-head { display:flex; align-items:center; gap:8px; padding:10px 12px; border-bottom:1px solid var(--border); }
|
||||
/* The machine the chat sees: its name on one line, cut with … */
|
||||
.ai-head .ai-where { flex:0 1 auto; min-width:0; max-width:30%; font-size:12px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
||||
/* The machine's name: room for 7-8 letters at least, the chat's name gives way. */
|
||||
.ai-head .ai-where { flex:0 0 auto; min-width:0; max-width:40%; font-size:12px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
||||
.ai-head .ai-chatsel { min-width:0; }
|
||||
.ai-head .ai-chatname { min-width:3ch; }
|
||||
.ai-head .ai-chatsel { flex:0 1 auto; }
|
||||
.ai-list { flex:1; overflow:auto; padding:12px; display:flex; flex-direction:column; gap:10px; }
|
||||
.ai-msg { max-width:92%; padding:8px 11px; border-radius:10px; font-size:14px; line-height:1.45; overflow-wrap:anywhere; }
|
||||
|
||||
Reference in new issue
Block a user