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:
@@ -1,6 +1,6 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { embed } from "../lib/embeddings.js";
|
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({
|
export const searchContextSchema = z.object({
|
||||||
query: z.string().describe("Natural language search query"),
|
query: z.string().describe("Natural language search query"),
|
||||||
@@ -14,6 +14,28 @@ export const searchContextSchema = z.object({
|
|||||||
|
|
||||||
export type SearchContextInput = z.infer<typeof searchContextSchema>;
|
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> {
|
export async function searchContext(input: SearchContextInput): Promise<string> {
|
||||||
const vector = await embed(input.query);
|
const vector = await embed(input.query);
|
||||||
const client = getClient();
|
const client = getClient();
|
||||||
@@ -38,15 +60,6 @@ export async function searchContext(input: SearchContextInput): Promise<string>
|
|||||||
}
|
}
|
||||||
|
|
||||||
return results
|
return results
|
||||||
.map((r, i) => {
|
.map((r, i) => formatResult(r.payload as Record<string, unknown>, i, r.score))
|
||||||
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");
|
|
||||||
})
|
|
||||||
.join("\n\n---\n\n");
|
.join("\n\n---\n\n");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { embed } from "../lib/embeddings.js";
|
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({
|
export const searchIssuesSchema = z.object({
|
||||||
query: z.string().describe("Search query for operational notes and issues"),
|
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>;
|
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> {
|
export async function searchIssues(input: SearchIssuesInput): Promise<string> {
|
||||||
const vector = await embed(input.query);
|
const vector = await embed(input.query);
|
||||||
const client = getClient();
|
const client = getClient();
|
||||||
@@ -24,8 +38,8 @@ export async function searchIssues(input: SearchIssuesInput): Promise<string> {
|
|||||||
// Filter to operational/issue-related sections
|
// Filter to operational/issue-related sections
|
||||||
const issuePatterns = /operational|issue|troubleshoot|problem|fix|error|outage|rca|incident|quirk|gotcha|known/i;
|
const issuePatterns = /operational|issue|troubleshoot|problem|fix|error|outage|rca|incident|quirk|gotcha|known/i;
|
||||||
const filtered = results.filter((r) => {
|
const filtered = results.filter((r) => {
|
||||||
const p = r.payload as unknown as ChunkPayload;
|
const text = getSearchableText(r.payload as Record<string, unknown>);
|
||||||
return issuePatterns.test(p.section) || issuePatterns.test(p.text);
|
return issuePatterns.test(text);
|
||||||
});
|
});
|
||||||
|
|
||||||
const final = filtered.slice(0, input.limit);
|
const final = filtered.slice(0, input.limit);
|
||||||
@@ -38,18 +52,12 @@ export async function searchIssues(input: SearchIssuesInput): Promise<string> {
|
|||||||
return (
|
return (
|
||||||
"*No explicitly operational sections found. Showing best general matches:*\n\n" +
|
"*No explicitly operational sections found. Showing best general matches:*\n\n" +
|
||||||
fallback
|
fallback
|
||||||
.map((r, i) => {
|
.map((r, i) => formatIssueResult(r.payload as Record<string, unknown>, i, r.score))
|
||||||
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}`;
|
|
||||||
})
|
|
||||||
.join("\n\n---\n\n")
|
.join("\n\n---\n\n")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return final
|
return final
|
||||||
.map((r, i) => {
|
.map((r, i) => formatIssueResult(r.payload as Record<string, unknown>, i, r.score))
|
||||||
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}`;
|
|
||||||
})
|
|
||||||
.join("\n\n---\n\n");
|
.join("\n\n---\n\n");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user