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<string,
unknown> first instead of blindly casting to ChunkPayload.
This commit is contained in:
2026-03-01 18:29:10 +00:00
parent 94198d3897
commit d58fa405a2
2 changed files with 43 additions and 22 deletions

View File

@@ -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<typeof searchContextSchema>;
function formatResult(payload: Record<string, unknown>, 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<string> {
const vector = await embed(input.query);
const client = getClient();
@@ -38,15 +60,6 @@ export async function searchContext(input: SearchContextInput): Promise<string>
}
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<string, unknown>, i, r.score))
.join("\n\n---\n\n");
}

View File

@@ -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<typeof searchIssuesSchema>;
function formatIssueResult(payload: Record<string, unknown>, 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, unknown>): 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<string> {
const vector = await embed(input.query);
const client = getClient();
@@ -24,8 +38,8 @@ export async function searchIssues(input: SearchIssuesInput): Promise<string> {
// 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<string, unknown>);
return issuePatterns.test(text);
});
const final = filtered.slice(0, input.limit);
@@ -38,18 +52,12 @@ export async function searchIssues(input: SearchIssuesInput): Promise<string> {
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<string, unknown>, 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<string, unknown>, i, r.score))
.join("\n\n---\n\n");
}