sampling-rerank.mjs
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd
feat(calendar): enforce agent context tiers in retrieval AP…
Human
minor
⚠ breaking
1 day ago
| 1 | /** |
| 2 | * Issue #1 Phase F4 — sampling-based search result reranking. |
| 3 | * After vector search returns candidates, use the client LLM to rerank by semantic relevance. |
| 4 | */ |
| 5 | |
| 6 | import { trySampling } from '../sampling.mjs'; |
| 7 | |
| 8 | const RERANK_SYSTEM = `You are a search result reranker. Given a query and numbered search results, return ONLY a JSON array of the result numbers (1-based) sorted by relevance to the query, most relevant first. Example: [3, 1, 5, 2, 4] |
| 9 | Do not include results that are clearly irrelevant. Return at most the number requested.`; |
| 10 | |
| 11 | const MAX_RESULTS_FOR_RERANK = 20; |
| 12 | |
| 13 | /** |
| 14 | * Attempt to rerank search results using the client LLM via sampling. |
| 15 | * Falls back to the original order when sampling is unavailable or fails. |
| 16 | * |
| 17 | * @param {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} mcpServer |
| 18 | * @param {string} query |
| 19 | * @param {{ path: string, snippet?: string, score?: number }[]} results |
| 20 | * @param {number} [limit] |
| 21 | * @returns {Promise<{ path: string, snippet?: string, score?: number }[]>} |
| 22 | */ |
| 23 | export async function rerankWithSampling(mcpServer, query, results, limit) { |
| 24 | if (!results || results.length <= 1) return results; |
| 25 | |
| 26 | const candidates = results.slice(0, MAX_RESULTS_FOR_RERANK); |
| 27 | const numbered = candidates |
| 28 | .map((r, i) => `${i + 1}. [${r.path}] ${(r.snippet || '').slice(0, 200)}`) |
| 29 | .join('\n'); |
| 30 | |
| 31 | const user = `Query: "${query}"\nReturn up to ${limit || candidates.length} results.\n\nResults:\n${numbered}`; |
| 32 | |
| 33 | const raw = await trySampling(mcpServer, { |
| 34 | system: RERANK_SYSTEM, |
| 35 | user, |
| 36 | maxTokens: 256, |
| 37 | }); |
| 38 | |
| 39 | if (!raw) return results; |
| 40 | |
| 41 | const ranked = parseRerankResponse(raw, candidates.length); |
| 42 | if (!ranked || ranked.length === 0) return results; |
| 43 | |
| 44 | const reordered = []; |
| 45 | const used = new Set(); |
| 46 | for (const idx of ranked) { |
| 47 | if (idx >= 0 && idx < candidates.length && !used.has(idx)) { |
| 48 | reordered.push(candidates[idx]); |
| 49 | used.add(idx); |
| 50 | } |
| 51 | } |
| 52 | for (let i = 0; i < candidates.length; i++) { |
| 53 | if (!used.has(i)) reordered.push(candidates[i]); |
| 54 | } |
| 55 | if (results.length > MAX_RESULTS_FOR_RERANK) { |
| 56 | reordered.push(...results.slice(MAX_RESULTS_FOR_RERANK)); |
| 57 | } |
| 58 | |
| 59 | return limit ? reordered.slice(0, limit) : reordered; |
| 60 | } |
| 61 | |
| 62 | /** |
| 63 | * Parse the LLM rerank response into an array of 0-based indices. |
| 64 | * @param {string} raw |
| 65 | * @param {number} maxIdx |
| 66 | * @returns {number[] | null} |
| 67 | */ |
| 68 | export function parseRerankResponse(raw, maxIdx) { |
| 69 | if (!raw) return null; |
| 70 | try { |
| 71 | const cleaned = raw.replace(/^```(?:json)?\s*/i, '').replace(/```\s*$/, '').trim(); |
| 72 | const arr = JSON.parse(cleaned); |
| 73 | if (!Array.isArray(arr)) return null; |
| 74 | return arr |
| 75 | .filter((n) => typeof n === 'number' && Number.isFinite(n)) |
| 76 | .map((n) => Math.round(n) - 1) |
| 77 | .filter((i) => i >= 0 && i < maxIdx); |
| 78 | } catch (_) { |
| 79 | const matches = raw.match(/\d+/g); |
| 80 | if (!matches) return null; |
| 81 | return matches |
| 82 | .map((s) => parseInt(s, 10) - 1) |
| 83 | .filter((i) => Number.isFinite(i) && i >= 0 && i < maxIdx); |
| 84 | } |
| 85 | } |
File History
2 commits
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd
feat(calendar): enforce agent context tiers in retrieval AP…
Human
minor
⚠
1 day ago
sha256:9103f98c89257ed2b01c237cea895dabb3e85ea337dccb1161c175e4422355b6
docs: accept Calendar Events v0 spec with Phase 0 security …
Human
1 day ago