the query source that claimed a turn is readable on /chat (V-539)
V-539 said SearXNG claims every world question, including invented terms, so Kiwix is never reached. Measured today against the configured instance: seven of eight invented Russian questions now return zero results, and Response.Empty() already passes those to the ZIM. The premise moved with the upstream engine set in three days. The three quality signals the task named were recorded per query and none separate the sets. Token overlap is zero for the one bad claim and also zero for "столица Франции", whose answer is Париж. Empty snippets never fire, because ParseResponse already drops a hit with no text. SearXNG returned no corrections or suggestions even for the query it silently respelled. So no threshold is built: it would cost a real answer to save one invented word. What ships is the second half. The claiming query source crosses the IPC seam on ipc.ChatReply.Source and renders as a badge beside the reply on /chat. It rides the context rather than a return value, because handleText answers every reach through one string and the mic, telegram and the web all share it. Chat now returns ChatReply instead of a bare string. Full -race suite green.
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
{{if .Error}}<div class="msg msg-err">{{.Error}}</div>{{end}}
|
||||
<div class="scroll chat-scroll" id=chatHistory>
|
||||
{{range .Messages}}
|
||||
<div class="chat-msg {{.Role}}"><strong>{{if eq .Role "user"}}you{{else}}maven{{end}}:</strong> {{.Text}}</div>
|
||||
<div class="chat-msg {{.Role}}"><strong>{{if eq .Role "user"}}you{{else}}maven{{end}}:</strong> {{.Text}}{{if .Source}} <span class="badge badge-accent" title="the query source that claimed this turn">{{.Source}}</span>{{end}}</div>
|
||||
{{else}}
|
||||
<div class=empty>
|
||||
<svg class=icon width="20" height="20"><use href="/ethos-icons.svg#i-message"/></svg>
|
||||
|
||||
@@ -67,8 +67,9 @@ type fakeCore struct {
|
||||
traceErr error
|
||||
|
||||
// for handleChatAPI tests
|
||||
chatText string
|
||||
chatErr error
|
||||
chatText string
|
||||
chatSource string
|
||||
chatErr error
|
||||
|
||||
// for the MCP section of /tools
|
||||
mcpServers []ipc.MCPServerStatus
|
||||
@@ -79,12 +80,12 @@ func (f *fakeCore) MCPServers(context.Context) ([]ipc.MCPServerStatus, error) {
|
||||
return f.mcpServers, f.mcpErr
|
||||
}
|
||||
|
||||
func (f *fakeCore) Chat(_ context.Context, _, text string) (string, error) {
|
||||
func (f *fakeCore) Chat(_ context.Context, _, text string) (ipc.ChatReply, error) {
|
||||
f.chatText = text
|
||||
if f.chatErr != nil {
|
||||
return "", f.chatErr
|
||||
return ipc.ChatReply{}, f.chatErr
|
||||
}
|
||||
return "поняла", nil
|
||||
return ipc.ChatReply{Reply: "поняла", Source: f.chatSource}, nil
|
||||
}
|
||||
|
||||
func (f *fakeCore) EnableTool(_ context.Context, name string, cmd []string, destructive bool, scope string, _ time.Time) error {
|
||||
@@ -1285,3 +1286,37 @@ func TestHandleNotifications_ShowsTheOutbox(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- the query source badge (V-539) ---
|
||||
//
|
||||
// Which query source claimed a turn was readable in the daemon log and nowhere
|
||||
// else, so a QA step could not tell a wrong answer from a wrongly ordered
|
||||
// chain. It now rides the redirect and renders beside the reply.
|
||||
|
||||
func TestHandleChatAPI_CarriesTheClaimingSource(t *testing.T) {
|
||||
core := &fakeCore{chatSource: "kiwix"}
|
||||
rr := httptest.NewRecorder()
|
||||
handleChatAPI(rr, postChat("почему небо голубое"), core, nil, false)
|
||||
loc := rr.Header().Get("Location")
|
||||
if !strings.Contains(loc, "s=kiwix") {
|
||||
t.Errorf("redirect = %q; want the claiming source in it", loc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleChatAPI_OmitsTheSourceWhenNothingClaimed(t *testing.T) {
|
||||
core := &fakeCore{}
|
||||
rr := httptest.NewRecorder()
|
||||
handleChatAPI(rr, postChat("запиши что я пил воду"), core, nil, false)
|
||||
if loc := rr.Header().Get("Location"); strings.Contains(loc, "s=") {
|
||||
t.Errorf("redirect = %q; a turn no source claimed carries no badge", loc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatPageRendersTheSourceBadge(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/chat?q=%D1%82%D0%B5%D1%81%D1%82&r=%D0%BE%D1%82%D0%B2%D0%B5%D1%82&s=search", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
handleChatPage(rr, req, &fakeCore{})
|
||||
if body := rr.Body.String(); !strings.Contains(body, ">search</span>") {
|
||||
t.Errorf("chat page does not render the source badge; body=%s", body)
|
||||
}
|
||||
}
|
||||
|
||||
+12
-3
@@ -1627,8 +1627,8 @@ func handleChatPage(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) {
|
||||
if q := r.URL.Query().Get("q"); q != "" {
|
||||
msgs = append(msgs, chatMsg{Role: "user", Text: q})
|
||||
}
|
||||
if r := r.URL.Query().Get("r"); r != "" {
|
||||
msgs = append(msgs, chatMsg{Role: "assistant", Text: r})
|
||||
if reply := r.URL.Query().Get("r"); reply != "" {
|
||||
msgs = append(msgs, chatMsg{Role: "assistant", Text: reply, Source: r.URL.Query().Get("s")})
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
if err := chatTmpl.Execute(w, struct {
|
||||
@@ -1677,13 +1677,22 @@ func handleChatAPI(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI, ses
|
||||
http.Redirect(w, r, "/chat", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/chat?q="+url.QueryEscape(text)+"&r="+url.QueryEscape(reply), http.StatusSeeOther)
|
||||
// The claiming query source rides back on the redirect so the page can show
|
||||
// it. Empty for a turn no source claimed, which is most of them.
|
||||
dest := "/chat?q=" + url.QueryEscape(text) + "&r=" + url.QueryEscape(reply.Reply)
|
||||
if reply.Source != "" {
|
||||
dest += "&s=" + url.QueryEscape(reply.Source)
|
||||
}
|
||||
http.Redirect(w, r, dest, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// chatMsg — one message in the conversation history.
|
||||
type chatMsg struct {
|
||||
Role string // "user" | "assistant"
|
||||
Text string
|
||||
// Source — the query source that claimed the turn, shown as a badge beside
|
||||
// the reply. Empty for a turn no source claimed (V-539).
|
||||
Source string
|
||||
}
|
||||
|
||||
func mustMarshal(v any) json.RawMessage {
|
||||
|
||||
Reference in New Issue
Block a user