fix: Думать - the model is reminded in the question itself, the label no longer lands in the arrow, and the thinking with its time is kept to show again later

This commit is contained in:
redtea committed 2026-10-16 10:29:44 +03:00
1 parent 3b7a6271ac
commit fa62f2c5b6
5 files changed
+78 -15

No files matched your search

+34 -5
View File
@@ -41,6 +41,8 @@ type aiConfig struct{ Type, URL, Key, Model string }
type aiMsg struct {
Role string `json:"role"`
Content string `json:"content"`
// ThinkMS: how long the model thought before this answer (kept to show it).
ThinkMS int64 `json:"think_ms,omitempty"`
}
func (s *Server) aiConfig() aiConfig {
@@ -440,6 +442,11 @@ func (s *Server) aiDeleteChat(owner, chat int64) {
// aiAppendTo keeps a message of owner's chat; the first question names it.
func (s *Server) aiAppendTo(owner, chat int64, role, content string) {
s.aiKeepMsg(owner, chat, aiMsg{Role: role, Content: content})
}
func (s *Server) aiKeepMsg(owner, chat int64, m aiMsg) {
role, content := m.Role, m.Content
if content == "" || !s.aiOwns(owner, chat) {
return
}
@@ -447,7 +454,7 @@ func (s *Server) aiAppendTo(owner, chat int64, role, content string) {
content = utf8Safe(content[:64<<10])
}
now := time.Now().Unix()
_, _ = s.db.sql.Exec(`INSERT INTO ai_messages (admin_id, chat_id, role, content, at) VALUES (?, ?, ?, ?, ?)`, owner, chat, role, content, now)
_, _ = s.db.sql.Exec(`INSERT INTO ai_messages (admin_id, chat_id, role, content, at, think_ms) VALUES (?, ?, ?, ?, ?, ?)`, owner, chat, role, content, now, m.ThinkMS)
_, _ = s.db.sql.Exec(`DELETE FROM ai_messages WHERE chat_id = ? AND id <= (SELECT id FROM ai_messages WHERE chat_id = ? ORDER BY id DESC LIMIT 1 OFFSET ?)`, chat, chat, aiKeep)
_, _ = s.db.sql.Exec(`UPDATE ai_chats SET updated = ? WHERE id = ?`, now, chat)
if role == "user" {
@@ -461,7 +468,7 @@ func (s *Server) aiAppendTo(owner, chat int64, role, content string) {
// aiHistoryOf is a chat's talk, oldest first, the last limit messages.
func (s *Server) aiHistoryOf(owner, chat int64, limit int) []aiMsg {
rows, err := s.db.sql.Query(`SELECT role, content FROM ai_messages WHERE admin_id = ? AND chat_id = ? ORDER BY id DESC LIMIT ?`, owner, chat, limit)
rows, err := s.db.sql.Query(`SELECT role, content, think_ms FROM ai_messages WHERE admin_id = ? AND chat_id = ? ORDER BY id DESC LIMIT ?`, owner, chat, limit)
out := []aiMsg{}
if err != nil {
return out
@@ -469,7 +476,7 @@ func (s *Server) aiHistoryOf(owner, chat int64, limit int) []aiMsg {
defer rows.Close()
for rows.Next() {
var m aiMsg
if rows.Scan(&m.Role, &m.Content) == nil {
if rows.Scan(&m.Role, &m.Content, &m.ThinkMS) == nil {
out = append(out, m)
}
}
@@ -541,24 +548,44 @@ func (s *Server) handleAIChat(w http.ResponseWriter, r *http.Request) {
}
if in.Think {
system += aiThink
// Models heed the last words most: the reminder goes there too (not
// kept with the talk).
if n := len(msgs); n > 0 && msgs[n-1].Role == "user" {
msgs[n-1].Content += "\n\n(Сначала подумай: первая строка ответа - ::думаю::, дальше рассуждение, потом строка ::ответ:: и сам ответ.)"
}
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("X-Accel-Buffering", "no")
fl, _ := w.(http.Flusher)
var answer strings.Builder
var thinkFrom, thinkTo time.Time // when the thinking began and ended
emit := func(t string) {
if t == "" {
return
}
answer.WriteString(t)
if a := answer.String(); thinkFrom.IsZero() && (strings.Contains(a, "::думаю::") || strings.Contains(a, "<think>")) {
thinkFrom = time.Now()
} else if !thinkFrom.IsZero() && thinkTo.IsZero() && (strings.Contains(a, "::ответ::") || strings.Contains(a, "</think>")) {
thinkTo = time.Now()
}
_, _ = io.WriteString(w, t)
if fl != nil {
fl.Flush()
}
}
err := aiStream(r.Context(), s.aiConfig(), system+"\n\n--- Панель сейчас ---\n"+in.Context, msgs, emit)
s.aiAppendTo(a.ID, in.Chat, "assistant", stripThink(answer.String()))
// The answer is kept whole, thinking too (to show it again); the model
// gets it without (aiWindow).
ans := aiMsg{Role: "assistant", Content: answer.String()}
if !thinkFrom.IsZero() {
if thinkTo.IsZero() {
thinkTo = time.Now()
}
ans.ThinkMS = thinkTo.Sub(thinkFrom).Milliseconds()
}
s.aiKeepMsg(a.ID, in.Chat, ans)
if err != nil && r.Context().Err() == nil {
if answer.Len() == 0 {
w.WriteHeader(http.StatusBadGateway)
@@ -679,7 +706,9 @@ func aiWindow(rows []aiMsg, budget int) []aiMsg {
}
m = aiMsg{Role: "user", Content: "Результат действия:\n" + cut(m.Content, max)}
case m.Role == "assistant" && i < len(rows)-2:
m.Content = cut(m.Content, 3000)
m.Content = cut(stripThink(m.Content), 3000)
case m.Role == "assistant":
m.Content = stripThink(m.Content)
case m.Role != "assistant":
m.Role = "user"
}