From d58fa405a27ba26175342086c58bf525f188a7c5 Mon Sep 17 00:00:00 2001 From: adam Date: Sun, 1 Mar 2026 18:29:10 +0000 Subject: [PATCH] Handle project points in search result formatters search-context.ts and search-issues.ts now check payload.type and branch formatting for project points (slug, name, status, summary) vs chunk points (source, section, text). Casts to Record first instead of blindly casting to ChunkPayload. --- src/tools/search-context.ts | 35 ++++++++++++++++++++++++----------- src/tools/search-issues.ts | 30 +++++++++++++++++++----------- 2 files changed, 43 insertions(+), 22 deletions(-) diff --git a/src/tools/search-context.ts b/src/tools/search-context.ts index 49e3bce..26077f4 100644 --- a/src/tools/search-context.ts +++ b/src/tools/search-context.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import { embed } from "../lib/embeddings.js"; -import { getClient, COLLECTION, type ChunkPayload } from "../lib/qdrant.js"; +import { getClient, COLLECTION } from "../lib/qdrant.js"; export const searchContextSchema = z.object({ query: z.string().describe("Natural language search query"), @@ -14,6 +14,28 @@ export const searchContextSchema = z.object({ export type SearchContextInput = z.infer; +function formatResult(payload: Record, index: number, score: number): string { + if (payload.type === "project") { + const tags = Array.isArray(payload.tags) && payload.tags.length > 0 ? (payload.tags as string[]).join(", ") : "none"; + return [ + `### Result ${index + 1} (score: ${score.toFixed(3)})`, + `**Project**: ${payload.name} (\`${payload.slug}\`) | **Status**: ${payload.status}`, + `**Tags**: ${tags} | **Updated**: ${payload.updated}`, + "", + payload.summary as string, + ].join("\n"); + } + + const tags = Array.isArray(payload.tags) && payload.tags.length > 0 ? (payload.tags as string[]).join(", ") : "none"; + return [ + `### Result ${index + 1} (score: ${score.toFixed(3)})`, + `**Source**: ${payload.source} | **Section**: ${payload.section} | **Type**: ${payload.type}${payload.host ? ` | **Host**: ${payload.host}` : ""}`, + `**Tags**: ${tags} | **Updated**: ${payload.updated}`, + "", + payload.text as string, + ].join("\n"); +} + export async function searchContext(input: SearchContextInput): Promise { const vector = await embed(input.query); const client = getClient(); @@ -38,15 +60,6 @@ export async function searchContext(input: SearchContextInput): Promise } return results - .map((r, i) => { - const p = r.payload as unknown as ChunkPayload; - return [ - `### Result ${i + 1} (score: ${r.score.toFixed(3)})`, - `**Source**: ${p.source} | **Section**: ${p.section} | **Type**: ${p.type}${p.host ? ` | **Host**: ${p.host}` : ""}`, - `**Tags**: ${p.tags.length > 0 ? p.tags.join(", ") : "none"} | **Updated**: ${p.updated}`, - "", - p.text, - ].join("\n"); - }) + .map((r, i) => formatResult(r.payload as Record, i, r.score)) .join("\n\n---\n\n"); } diff --git a/src/tools/search-issues.ts b/src/tools/search-issues.ts index 0ab9b76..d76f5d4 100644 --- a/src/tools/search-issues.ts +++ b/src/tools/search-issues.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import { embed } from "../lib/embeddings.js"; -import { getClient, COLLECTION, type ChunkPayload } from "../lib/qdrant.js"; +import { getClient, COLLECTION } from "../lib/qdrant.js"; export const searchIssuesSchema = z.object({ query: z.string().describe("Search query for operational notes and issues"), @@ -9,6 +9,20 @@ export const searchIssuesSchema = z.object({ export type SearchIssuesInput = z.infer; +function formatIssueResult(payload: Record, index: number, score: number): string { + if (payload.type === "project") { + return `### Result ${index + 1} (score: ${score.toFixed(3)})\n**Project**: ${payload.name} (\`${payload.slug}\`) | **Status**: ${payload.status}\n\n${payload.summary}`; + } + return `### Result ${index + 1} (score: ${score.toFixed(3)})\n**Source**: ${payload.source} | **Section**: ${payload.section}\n\n${payload.text}`; +} + +function getSearchableText(payload: Record): string { + if (payload.type === "project") { + return `${payload.name ?? ""} ${payload.summary ?? ""} ${payload.current_state ?? ""}`; + } + return `${payload.section ?? ""} ${payload.text ?? ""}`; +} + export async function searchIssues(input: SearchIssuesInput): Promise { const vector = await embed(input.query); const client = getClient(); @@ -24,8 +38,8 @@ export async function searchIssues(input: SearchIssuesInput): Promise { // Filter to operational/issue-related sections const issuePatterns = /operational|issue|troubleshoot|problem|fix|error|outage|rca|incident|quirk|gotcha|known/i; const filtered = results.filter((r) => { - const p = r.payload as unknown as ChunkPayload; - return issuePatterns.test(p.section) || issuePatterns.test(p.text); + const text = getSearchableText(r.payload as Record); + return issuePatterns.test(text); }); const final = filtered.slice(0, input.limit); @@ -38,18 +52,12 @@ export async function searchIssues(input: SearchIssuesInput): Promise { return ( "*No explicitly operational sections found. Showing best general matches:*\n\n" + fallback - .map((r, i) => { - const p = r.payload as unknown as ChunkPayload; - return `### Result ${i + 1} (score: ${r.score.toFixed(3)})\n**Source**: ${p.source} | **Section**: ${p.section}\n\n${p.text}`; - }) + .map((r, i) => formatIssueResult(r.payload as Record, i, r.score)) .join("\n\n---\n\n") ); } return final - .map((r, i) => { - const p = r.payload as unknown as ChunkPayload; - return `### Result ${i + 1} (score: ${r.score.toFixed(3)})\n**Source**: ${p.source} | **Section**: ${p.section}\n\n${p.text}`; - }) + .map((r, i) => formatIssueResult(r.payload as Record, i, r.score)) .join("\n\n---\n\n"); }