From 925fc12799e970578dc9342240ad986666238021 Mon Sep 17 00:00:00 2001 From: Isaac Clements Date: Wed, 19 Nov 2025 11:57:55 +0000 Subject: [PATCH] FEAT: Add ability to see which posts matched the search --- src/test/tools.test.ts | 37 +++++++++++++++++++++++++++++++++++++ src/tools/builtin/search.ts | 23 ++++++++++++++++++++--- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/src/test/tools.test.ts b/src/test/tools.test.ts index 0a723dd..52b8632 100644 --- a/src/test/tools.test.ts +++ b/src/test/tools.test.ts @@ -201,3 +201,40 @@ test('default-search prefix is applied to queries', async () => { globalThis.fetch = originalFetch as any; } }); + +test('search tool also returns post ids', async () => { + const logger = new Logger('silent'); + const siteState = new SiteState({ logger, timeoutMs: 5000, defaultAuth: { type: 'none' } }); + const tools: Record = {}; + const fakeServer: any = { + registerTool(name: string, _meta: any, handler: Function) { + tools[name] = { handler }; + }, + }; + + // Mock fetch to capture the search URL + const originalFetch = globalThis.fetch; + globalThis.fetch = (async (input: any, _init?: any) => { + const url = typeof input === 'string' ? input : input.toString(); + if (url.endsWith('/about.json')) { + return new Response(JSON.stringify({ about: { title: 'Example Discourse' } }), { status: 200, headers: { 'Content-Type': 'application/json' } }); + } + if (url.includes('/search.json')) { + return new Response(JSON.stringify({ topics: [{ id: 123, title: 'Hello World', slug: 'hello-world' }], posts: [{ id: 456, topic_id: 123 }, { id: 789, topic_id: 123 }] }), { status: 200, headers: { 'Content-Type': 'application/json' } }); + } + return new Response('not found', { status: 404 }); + }) as any; + + try { + const { base, client } = siteState.buildClientForSite('https://example.com'); + await client.get('/about.json'); + siteState.selectSite(base); + await registerAllTools(fakeServer, siteState, logger, { allowWrites: false, toolsMode: 'discourse_api_only', defaultSearchPrefix: 'tag:ai order:latest-post' } as any); + const searchRes = await tools['discourse_search'].handler({ query: 'hello world' }, {}); + const text = String(searchRes?.content?.[0]?.text || ''); + assert.match(text, /456/); + assert.match(text, /789/); + } finally { + globalThis.fetch = originalFetch as any; + } +}); \ No newline at end of file diff --git a/src/tools/builtin/search.ts b/src/tools/builtin/search.ts index 8b8a7e4..c060801 100644 --- a/src/tools/builtin/search.ts +++ b/src/tools/builtin/search.ts @@ -1,6 +1,14 @@ import { z } from "zod"; import type { RegisterFn } from "../types.js"; +type Topic = { + type: "topic"; + id: number; + title: string; + slug: string, + posts: Array<{ type: "post", id: number }> +} + export const registerSearch: RegisterFn = (server, ctx) => { const schema = z.object({ query: z.string().min(1).describe("Search query"), @@ -15,7 +23,7 @@ export const registerSearch: RegisterFn = (server, ctx) => { description: "Search site content.", inputSchema: schema.shape, }, - async (args, _extra: any) => { + async (args: any, _extra: any) => { const { query, with_private = false, max_results = 10 } = args; const { base, client } = ctx.siteState.ensureSelectedSite(); const q = new URLSearchParams(); @@ -32,7 +40,11 @@ export const registerSearch: RegisterFn = (server, ctx) => { id: t.id, title: t.title, slug: t.slug, - })) as Array<{ type: "topic"; id: number; title: string; slug: string }>).slice(0, max_results); + posts: posts.filter((post) => post.topic_id == t.id).map((post) => ({ + type: "post" as const, + id: post.id, + })) + })) as Array).slice(0, max_results); const lines: string[] = []; lines.push(`Top results for "${query}":`); @@ -44,7 +56,12 @@ export const registerSearch: RegisterFn = (server, ctx) => { } const jsonFooter = { - results: items.map((it) => ({ id: it.id, url: `${base}/t/${it.slug}/${it.id}`, title: it.title })), + results: items.map((it) => ({ + id: it.id, + url: `${base}/t/${it.slug}/${it.id}`, + title: it.title, + posts: it.posts.map(({id}) => ({ id })) + })), }; const text = lines.join("\n") + "\n\n```json\n" + JSON.stringify(jsonFooter) + "\n```\n"; return { content: [{ type: "text", text }] };