Merge the query-source badge and the search-signal measurement (#171)
This commit is contained in:
@@ -286,6 +286,15 @@ world questions, so she needs to read external sources. What replaces it:
|
||||
- **In the world, live search leads and the ZIMs are the fallback** (owner's call,
|
||||
2026-08-02). A self-hosted SearXNG (`search` block) answers first; the Kiwix ZIMs on
|
||||
homesrv answer when the search is empty, unreachable, or the line is down.
|
||||
`Response.Empty()` is the whole gate and there is no quality threshold in front of it:
|
||||
the three signals one could read were measured on 2026-08-05 and none of them separate a
|
||||
real question from an invented one. Token overlap would cost "столица Франции" its
|
||||
answer, because the answer is Париж and that word is not in the question. See
|
||||
`docs/evals/2026-08-05-search-quality-signals.md` (V-539). **Which query source claimed
|
||||
a turn is readable on `/chat`** as a badge beside the reply, carried on
|
||||
`ipc.ChatReply.Source` and noted by `noteQuerySource` in `cmd/mavend/querysource.go`. It
|
||||
rides the context, so `handleText` keeps the one string signature the mic, telegram and
|
||||
the web share.
|
||||
- **External search is allowed and off unless configured**, like the weather and telegram
|
||||
capabilities. The code default is still off. `deploy/mavend.json` now ships a `search`
|
||||
block (owner's call, 2026-08-02), so it is on for this box and deleting the block turns
|
||||
|
||||
@@ -161,8 +161,10 @@ func (h *reactiveHandler) actionQuery(ctx context.Context, dec router.Decision)
|
||||
// no query-source field, so a wrong answer could not be told from a
|
||||
// wrongly-ordered chain (Vikunja #474). Only the name is logged —
|
||||
// the utterance and the answer are already on the voice lines above
|
||||
// and below this one.
|
||||
// and below this one. The same name goes to the turn's sink when the
|
||||
// caller asked for one, so /chat can show it (V-539).
|
||||
log.Printf("voice: query claimed by source %q", src.name)
|
||||
noteQuerySource(ctx, src.name)
|
||||
return reply
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// The query source that claimed a turn was visible in the daemon log and
|
||||
// nowhere else (V-539). A QA step reading /chat could see a wrong answer but
|
||||
// not tell a wrong answer from a wrongly ordered chain: "почему небо голубое"
|
||||
// answered badly reads the same whether search claimed it, the ZIM did, or the
|
||||
// resident model answered from memory.
|
||||
//
|
||||
// It rides the context rather than a return value because handleText answers
|
||||
// every reach through one string, and threading a second value through the
|
||||
// whole action dispatch would change a signature the mic, telegram and the web
|
||||
// all share. The sink is per turn, created by the caller that wants to read it;
|
||||
// a turn with no sink notes nothing, which is what the mic path does.
|
||||
type querySourceKey struct{}
|
||||
|
||||
// querySourceSink holds the name of the source that claimed one turn. The mutex
|
||||
// is there because a query source may fan out to goroutines of its own, not
|
||||
// because two turns share a sink.
|
||||
type querySourceSink struct {
|
||||
mu sync.Mutex
|
||||
name string
|
||||
}
|
||||
|
||||
func (s *querySourceSink) note(name string) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.name = name
|
||||
}
|
||||
|
||||
// Name is the source that claimed, or empty when nothing did or the turn was
|
||||
// not a query at all.
|
||||
func (s *querySourceSink) Name() string {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.name
|
||||
}
|
||||
|
||||
// withQuerySourceSink returns a context that collects the claiming source, and
|
||||
// the sink to read after the turn has answered.
|
||||
func withQuerySourceSink(ctx context.Context) (context.Context, *querySourceSink) {
|
||||
sink := &querySourceSink{}
|
||||
return context.WithValue(ctx, querySourceKey{}, sink), sink
|
||||
}
|
||||
|
||||
// noteQuerySource records which source claimed the turn. It is a no-op when the
|
||||
// caller did not ask for one.
|
||||
func noteQuerySource(ctx context.Context, name string) {
|
||||
if sink, ok := ctx.Value(querySourceKey{}).(*querySourceSink); ok {
|
||||
sink.note(name)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestQuerySourceSinkCollectsTheClaimingName(t *testing.T) {
|
||||
ctx, sink := withQuerySourceSink(context.Background())
|
||||
if sink.Name() != "" {
|
||||
t.Fatalf("a fresh sink names a source: %q", sink.Name())
|
||||
}
|
||||
noteQuerySource(ctx, "kiwix")
|
||||
if got := sink.Name(); got != "kiwix" {
|
||||
t.Errorf("sink.Name() = %q, want kiwix", got)
|
||||
}
|
||||
}
|
||||
|
||||
// A turn with no sink must not panic. The mic path asks for no source, and a
|
||||
// query source calls noteQuerySource unconditionally.
|
||||
func TestNoteQuerySourceWithoutASinkIsSilent(t *testing.T) {
|
||||
noteQuerySource(context.Background(), "search")
|
||||
}
|
||||
|
||||
// The last source to claim wins, because only one does: actionQuery returns on
|
||||
// the first claim. This pins that the sink overwrites rather than appends, so a
|
||||
// second turn on the same context could not read a stale name.
|
||||
func TestQuerySourceSinkKeepsTheLastNote(t *testing.T) {
|
||||
ctx, sink := withQuerySourceSink(context.Background())
|
||||
noteQuerySource(ctx, "search")
|
||||
noteQuerySource(ctx, "kiwix")
|
||||
if got := sink.Name(); got != "kiwix" {
|
||||
t.Errorf("sink.Name() = %q, want kiwix", got)
|
||||
}
|
||||
}
|
||||
@@ -41,11 +41,16 @@ func (d *daemonAPI) RecentEvents(ctx context.Context, n int) ([]ipc.IntakeEvent,
|
||||
return d.getEvents(n), nil
|
||||
}
|
||||
|
||||
func (d *daemonAPI) Chat(ctx context.Context, conversation, text string) (string, error) {
|
||||
// Chat runs one text turn and reports which query source claimed it. The sink
|
||||
// rides the context so handleText keeps the one string signature the mic,
|
||||
// telegram and the web all call it through (V-539).
|
||||
func (d *daemonAPI) Chat(ctx context.Context, conversation, text string) (ipc.ChatReply, error) {
|
||||
if d.chatFn == nil {
|
||||
return "", errors.New("mavend: chat not available")
|
||||
return ipc.ChatReply{}, errors.New("mavend: chat not available")
|
||||
}
|
||||
return d.chatFn(ctx, conversation, text), nil
|
||||
ctx, sink := withQuerySourceSink(ctx)
|
||||
reply := d.chatFn(ctx, conversation, text)
|
||||
return ipc.ChatReply{Reply: reply, Source: sink.Name()}, nil
|
||||
}
|
||||
|
||||
// MCPServers — the configured MCP servers and their health (Vikunja #251).
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
# Does SearXNG claim a question it cannot answer? (V-539)
|
||||
|
||||
Measured 2026-08-05 against the configured instance, `http://127.0.0.1:9563`,
|
||||
`max_results: 4`, `language: auto`. Sixteen Russian questions: eight real, eight
|
||||
invented from non-words. The probe read SearXNG's JSON directly, so this measures
|
||||
the search, not the cascade around it.
|
||||
|
||||
## The premise no longer reproduces
|
||||
|
||||
V-539 was filed on the 2026-08-02 measurement, where SearXNG returned four
|
||||
results for every query including `зыркабулентный флогистон Мшанского`, and no
|
||||
`voice: kiwix:` line ever appeared. Today the same shape of query returns
|
||||
nothing:
|
||||
|
||||
| Query set | Zero results | Four results claimed |
|
||||
|---|---|---|
|
||||
| Eight real questions | 0 | 8 |
|
||||
| Eight invented questions | 7 | 1 |
|
||||
|
||||
`Response.Empty()` is already the gate. Seven of eight invented questions now
|
||||
pass the turn to the ZIM with no code change at all. What changed is upstream.
|
||||
Every real answer today comes from `google cse`. It answers a non-word with an
|
||||
empty result set, where the engine set of three days ago answered with
|
||||
something.
|
||||
|
||||
## The one that still claims
|
||||
|
||||
`трюмбальная нидроскопия` returned four results, all about a lumbar puncture:
|
||||
|
||||
```
|
||||
Люмбальная пункция - адреса и стоимость в больницах в СПб
|
||||
Пункция спинного мозга - Больница «Шиба
|
||||
Педиатрический фантом люмбальной пункции новорожденного
|
||||
```
|
||||
|
||||
The engine read the invented word as a misspelling of a real one and answered
|
||||
the real one. That is the whole remaining failure, and it is a near-miss
|
||||
spelling rather than a catch-all.
|
||||
|
||||
## The three candidate signals do not separate the sets
|
||||
|
||||
V-539 named three signals a quality gate could read. Each was recorded per
|
||||
query:
|
||||
|
||||
- **No result title shares a token with the query.** Useless. It is true of the
|
||||
one bad claim, and also true of `столица Франции`, whose four titles are
|
||||
`Париж`, `Франция`, `Париж — Путеводитель`, `Париж - Море Трэвел`. The right
|
||||
answer to a capital-city question is the city, which is not a word in the
|
||||
question. Two more real questions score 3 of 4 rather than 4.
|
||||
- **Every snippet is empty.** Never fired. Zero empty snippets across all
|
||||
sixteen queries, real or invented. `ParseResponse` already drops a hit with no
|
||||
text, so this signal cannot fire by construction.
|
||||
- **A spelling-suggestion or catch-all engine answered.** Never fired. SearXNG
|
||||
returned no `corrections` and no `suggestions` for any query, including the one
|
||||
that silently corrected the spelling itself.
|
||||
|
||||
## Decision: do not build the threshold
|
||||
|
||||
A gate on token overlap would cost `столица Франции` a correct answer to save
|
||||
one invented word, and the other two signals cannot fire. The task said a wrong
|
||||
threshold costs a real answer and needs measuring first. It was measured and it
|
||||
loses.
|
||||
|
||||
What ships instead is the second half of V-539. The claiming query source now
|
||||
crosses the IPC seam on `ipc.ChatReply.Source`. It renders as a badge beside the
|
||||
reply on `/chat`. The only evidence before it was a `voice:` log line, which is
|
||||
why this was hard to judge. The next occurrence is readable off the UI rather
|
||||
than off the box.
|
||||
|
||||
## Not measured here
|
||||
|
||||
- The cascade. This probe read SearXNG directly. It says nothing about how
|
||||
`querySearch` phrases what it gets, or whether the resident model turns four
|
||||
weak snippets into a confident wrong sentence.
|
||||
- Kiwix. It was healthy on 2026-08-02 and was not re-probed today.
|
||||
- English questions. The premise was about Russian, where the invented words are.
|
||||
- Whether the engine set is stable. The whole finding is that it moved in three
|
||||
days, so this table is a reading of one day.
|
||||
@@ -348,8 +348,8 @@ func TestGate_IpcServer_ChatAllowedForEnrolledCaller(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Chat: %v", err)
|
||||
}
|
||||
if reply != "echo: привет" {
|
||||
t.Fatalf("Chat reply = %q; want %q", reply, "echo: привет")
|
||||
if reply.Reply != "echo: привет" {
|
||||
t.Fatalf("Chat reply = %q; want %q", reply.Reply, "echo: привет")
|
||||
}
|
||||
if fake.chats != 1 {
|
||||
t.Fatalf("CoreAPI.Chat calls = %d; want 1", fake.chats)
|
||||
@@ -373,9 +373,9 @@ func (r *recordingAPI) WriteFact(_ context.Context, _ ipc.WriteFactReq) (int64,
|
||||
return int64(r.writes), nil
|
||||
}
|
||||
|
||||
func (r *recordingAPI) Chat(_ context.Context, _, text string) (string, error) {
|
||||
func (r *recordingAPI) Chat(_ context.Context, _, text string) (ipc.ChatReply, error) {
|
||||
r.chats++
|
||||
return "echo: " + text, nil
|
||||
return ipc.ChatReply{Reply: "echo: " + text}, nil
|
||||
}
|
||||
|
||||
// mustWriteFactParams — minimal WriteFactReq JSON with only the source field,
|
||||
|
||||
+17
-4
@@ -214,9 +214,9 @@ type SeedEventReq struct {
|
||||
// failure. Proposed is true only when this seed completed a pattern; the first
|
||||
// three seeds of a run return false with no routine.
|
||||
type SeedEventResp struct {
|
||||
FactID int64 `json:"fact_id"`
|
||||
EventID int64 `json:"event_id,omitempty"`
|
||||
Extracted bool `json:"extracted"`
|
||||
FactID int64 `json:"fact_id"`
|
||||
EventID int64 `json:"event_id,omitempty"`
|
||||
Extracted bool `json:"extracted"`
|
||||
Action string `json:"action,omitempty"`
|
||||
Object string `json:"object,omitempty"`
|
||||
Proposed bool `json:"proposed"`
|
||||
@@ -664,7 +664,20 @@ type chatReq struct {
|
||||
Conversation string `json:"conversation,omitempty"`
|
||||
}
|
||||
type chatResp struct {
|
||||
Reply string `json:"reply"`
|
||||
Reply string `json:"reply"`
|
||||
Source string `json:"source,omitempty"`
|
||||
}
|
||||
|
||||
// ChatReply — one text turn's answer plus which query source claimed it.
|
||||
//
|
||||
// Source is diagnostic and is empty unless the turn was a question a source
|
||||
// claimed: a fact write, an act or a chat turn names none. It exists because
|
||||
// the claiming source was readable only in the daemon log, so a QA step could
|
||||
// not tell a wrong answer from a wrongly ordered chain (V-539). It is not
|
||||
// authorization and nothing routes on it.
|
||||
type ChatReply struct {
|
||||
Reply string
|
||||
Source string
|
||||
}
|
||||
|
||||
type proposeToolReq struct {
|
||||
|
||||
@@ -640,12 +640,12 @@ func (c *Client) AcceptProposedRoutine(ctx context.Context, id int64) error {
|
||||
return c.call(ctx, MethodAcceptProposedRoutine, acceptProposedRoutineReq{ID: id}, nil)
|
||||
}
|
||||
|
||||
func (c *Client) Chat(ctx context.Context, conversation, text string) (string, error) {
|
||||
func (c *Client) Chat(ctx context.Context, conversation, text string) (ChatReply, error) {
|
||||
var r chatResp
|
||||
if err := c.call(ctx, MethodChat, chatReq{Text: text, Conversation: conversation}, &r); err != nil {
|
||||
return "", err
|
||||
return ChatReply{}, err
|
||||
}
|
||||
return r.Reply, nil
|
||||
return ChatReply{Reply: r.Reply, Source: r.Source}, nil
|
||||
}
|
||||
|
||||
func (c *Client) TickTrace(ctx context.Context) (TickTrace, error) {
|
||||
|
||||
@@ -176,7 +176,7 @@ type SystemAPI interface {
|
||||
// conversation, so an unanswered question on one reach cannot eat the next
|
||||
// utterance from another (Vikunja #466). Empty means the unattributed text
|
||||
// tap and is still one conversation of its own, separate from the mic.
|
||||
Chat(ctx context.Context, conversation, text string) (string, error)
|
||||
Chat(ctx context.Context, conversation, text string) (ChatReply, error)
|
||||
}
|
||||
|
||||
// CoreAPI — what core exposes to modules. One Go interface, satisfied by:
|
||||
|
||||
@@ -405,8 +405,17 @@ func TestChatViaClient(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Chat: %v", err)
|
||||
}
|
||||
if reply != "и тебе привет!" {
|
||||
t.Fatalf("Chat = %q, want %q", reply, "и тебе привет!")
|
||||
if reply.Reply != "и тебе привет!" {
|
||||
t.Fatalf("Chat = %q, want %q", reply.Reply, "и тебе привет!")
|
||||
}
|
||||
|
||||
// The claiming query source crosses the wire beside the reply (V-539).
|
||||
sourced, err := cli.Chat(context.Background(), "web", "почему небо голубое")
|
||||
if err != nil {
|
||||
t.Fatalf("Chat: %v", err)
|
||||
}
|
||||
if sourced.Source != "kiwix" {
|
||||
t.Fatalf("Chat source = %q, want kiwix", sourced.Source)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -417,11 +426,11 @@ type chatTestAPI struct {
|
||||
UnimplementedCoreAPI
|
||||
}
|
||||
|
||||
func (a *chatTestAPI) Chat(ctx context.Context, _, text string) (string, error) {
|
||||
func (a *chatTestAPI) Chat(ctx context.Context, _, text string) (ChatReply, error) {
|
||||
if text == "привет" {
|
||||
return "и тебе привет!", nil
|
||||
return ChatReply{Reply: "и тебе привет!"}, nil
|
||||
}
|
||||
return "поговорили.", nil
|
||||
return ChatReply{Reply: "поговорили.", Source: "kiwix"}, nil
|
||||
}
|
||||
|
||||
// TestDispatch_UnknownMethod — an unknown method over the wire comes back as
|
||||
|
||||
@@ -565,7 +565,7 @@ var methodTable = map[Method]handlerFunc{
|
||||
}),
|
||||
MethodChat: withParams(func(ctx context.Context, api CoreAPI, p chatReq) (chatResp, error) {
|
||||
reply, err := api.Chat(ctx, p.Conversation, p.Text)
|
||||
return chatResp{Reply: reply}, err
|
||||
return chatResp{Reply: reply.Reply, Source: reply.Source}, err
|
||||
}),
|
||||
MethodTickTrace: withoutParams(func(ctx context.Context, api CoreAPI) (TickTrace, error) {
|
||||
return api.TickTrace(ctx)
|
||||
|
||||
@@ -257,8 +257,8 @@ func (a *storeAPI) RevertFact(ctx context.Context, key string) (int64, error) {
|
||||
return newID, mapErr(err)
|
||||
}
|
||||
|
||||
func (a *storeAPI) Chat(ctx context.Context, conversation, text string) (string, error) {
|
||||
return "", errors.New("store: chat not available via direct store API")
|
||||
func (a *storeAPI) Chat(ctx context.Context, conversation, text string) (ChatReply, error) {
|
||||
return ChatReply{}, errors.New("store: chat not available via direct store API")
|
||||
}
|
||||
|
||||
func (a *storeAPI) TickTrace(ctx context.Context) (TickTrace, error) {
|
||||
|
||||
@@ -147,6 +147,6 @@ func (UnimplementedCoreAPI) MCPServers(ctx context.Context) ([]MCPServerStatus,
|
||||
func (UnimplementedCoreAPI) DayPlan(ctx context.Context) (DayPlan, error) {
|
||||
return DayPlan{}, ErrNotImplemented
|
||||
}
|
||||
func (UnimplementedCoreAPI) Chat(ctx context.Context, conversation, text string) (string, error) {
|
||||
return "", ErrNotImplemented
|
||||
func (UnimplementedCoreAPI) Chat(ctx context.Context, conversation, text string) (ChatReply, error) {
|
||||
return ChatReply{}, ErrNotImplemented
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user