This shows you the differences between two versions of the page.
| Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
| wiki:ai:advanced_rag_implementation_and_example_code [2025/09/03 16:23] – bgourley | wiki:ai:advanced_rag_implementation_and_example_code [2025/09/03 16:34] (current) – [Embed Your Dataset in Azure AI Search] bgourley | ||
|---|---|---|---|
| Line 15: | Line 15: | ||
| ===== Embed Your Dataset in Azure AI Search ===== | ===== Embed Your Dataset in Azure AI Search ===== | ||
| - | == 1) Configure environment == | + | === 1) Configure environment |
| Create/ | Create/ | ||
| Line 67: | Line 67: | ||
| MAX_TOKENS_ANSWER=800 | MAX_TOKENS_ANSWER=800 | ||
| - | == 2) Install deps == | + | === 2) Install deps === |
| npm install | npm install | ||
| - | == 3) Create/ | + | === 3) Create/ |
| * Our ingest.js creates docs with fields like: | * Our ingest.js creates docs with fields like: | ||
| Line 89: | Line 89: | ||
| * **Invalid keys** – we sanitized IDs (allowUnsafeKeys not needed). | * **Invalid keys** – we sanitized IDs (allowUnsafeKeys not needed). | ||
| - | == 4) Put your documents in the ingest folder == | + | === 4) Put your documents in the ingest folder |
| ingest/ | ingest/ | ||
| Line 101: | Line 101: | ||
| Etc. | Etc. | ||
| - | == 5) Run the ingest == | + | === 5) Run the ingest |
| node ingest/ | node ingest/ | ||
| Line 296: | Line 296: | ||
| < | < | ||
| require(' | require(' | ||
| - | |||
| const os = require(' | const os = require(' | ||
| - | |||
| const { answerQuestion } = require(' | const { answerQuestion } = require(' | ||
| - | + | ||
| - | + | ||
| function arg(name, def) { | function arg(name, def) { | ||
| - | + | | |
| - | const prefix = `--${name}=`; | + | const a = process.argv.find(x => x.startsWith(prefix)); |
| - | + | return a ? a.slice(prefix.length) : def; | |
| - | const a = process.argv.find(x => x.startsWith(prefix)); | + | |
| - | + | ||
| - | return a ? a.slice(prefix.length) : def; | + | |
| } | } | ||
| - | + | ||
| - | + | ||
| const q = arg(' | const q = arg(' | ||
| - | |||
| const n = Number(arg(' | const n = Number(arg(' | ||
| - | |||
| const conc = Number(arg(' | const conc = Number(arg(' | ||
| - | + | ||
| - | + | ||
| function sleep(ms) { return new Promise(r => setTimeout(r, | function sleep(ms) { return new Promise(r => setTimeout(r, | ||
| - | + | ||
| - | + | ||
| async function worker(id, jobs, results) { | async function worker(id, jobs, results) { | ||
| - | + | | |
| - | while (true) { | + | const i = jobs.next(); |
| - | + | if (i.done) break; | |
| - | const i = jobs.next(); | + | const t0 = Date.now(); |
| - | + | try { | |
| - | if (i.done) break; | + | const r = await answerQuestion(q); |
| - | + | const t = Date.now() - t0; | |
| - | const t0 = Date.now(); | + | results.push({ ok: true, latencyMs: t, usage: r.metrics.usage }); |
| - | + | process.stdout.write(' | |
| - | try { | + | } catch (e) { |
| - | + | const t = Date.now() - t0; | |
| - | const r = await answerQuestion(q); | + | results.push({ ok: false, latencyMs: t, error: String(e) }); |
| - | + | process.stdout.write(' | |
| - | const t = Date.now() - t0; | + | } |
| - | + | await sleep(50); | |
| - | results.push({ ok: true, latencyMs: t, usage: r.metrics.usage }); | + | } |
| - | + | ||
| - | process.stdout.write(' | + | |
| - | + | ||
| - | } catch (e) { | + | |
| - | + | ||
| - | const t = Date.now() - t0; | + | |
| - | + | ||
| - | results.push({ ok: false, latencyMs: t, error: String(e) }); | + | |
| - | + | ||
| - | process.stdout.write(' | + | |
| } | } | ||
| - | + | ||
| - | await sleep(50); | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | + | ||
| async function main() { | async function main() { | ||
| - | + | | |
| - | const jobs = (function*(){ for (let i=0; | + | const results = []; |
| - | + | const ps = []; | |
| - | const results = []; | + | for (let i=0; |
| - | + | await Promise.all(ps); | |
| - | const ps = []; | + | console.log(' |
| - | + | ||
| - | for (let i=0; | + | const lat = results.map(r => r.latencyMs).sort((a, |
| - | + | const p = (x) => lat[Math.floor((lat.length-1)*x)]; | |
| - | await Promise.all(ps); | + | const summary = { |
| - | + | q, n, concurrency: | |
| - | console.log(' | + | p50: p(0.50), |
| - | + | p90: p(0.90), | |
| - | + | p95: p(0.95), | |
| - | + | p99: p(0.99), | |
| - | const lat = results.map(r => r.latencyMs).sort((a, | + | errors: results.filter(r => !r.ok).length, |
| - | + | node: process.version, | |
| - | const p = (x) => lat[Math.floor((lat.length-1)*x)]; | + | cpu: os.cpus()[0]? |
| - | + | }; | |
| - | const summary = { | + | console.log(JSON.stringify(summary, |
| - | + | ||
| - | q, n, concurrency: | + | |
| - | + | ||
| - | p50: p(0.50), | + | |
| - | + | ||
| - | p90: p(0.90), | + | |
| - | + | ||
| - | p95: p(0.95), | + | |
| - | + | ||
| - | p99: p(0.99), | + | |
| - | + | ||
| - | errors: results.filter(r => !r.ok).length, | + | |
| - | + | ||
| - | node: process.version, | + | |
| - | + | ||
| - | cpu: os.cpus()[0]? | + | |
| - | + | ||
| - | }; | + | |
| - | + | ||
| - | console.log(JSON.stringify(summary, | + | |
| } | } | ||
| - | + | ||
| - | + | ||
| if (require.main === module) main().catch(console.error); | if (require.main === module) main().catch(console.error); | ||
| - | </code? | + | </code> |
| Line 418: | Line 360: | ||
| < | < | ||
| const { ActivityHandler, | const { ActivityHandler, | ||
| - | |||
| const { answerQuestion } = require(' | const { answerQuestion } = require(' | ||
| - | + | ||
| - | + | ||
| function renderSources(citations) { | function renderSources(citations) { | ||
| - | + | | |
| - | if (!citations || !citations.length) return ''; | + | const lines = citations.map(c => { |
| - | + | const title = c.title || c.id || ' | |
| - | const lines = citations.map(c => { | + | const url = c.url ? ` (${c.url})` : ''; |
| - | + | return `- ${c.key} **${title}**${url}`; | |
| - | const title = c.title || c.id || ' | + | }); |
| - | + | return `\n\n**Sources**\n${lines.join(' | |
| - | const url = c.url ? ` (${c.url})` : ''; | + | |
| - | + | ||
| - | return `- ${c.key} | + | |
| - | + | ||
| - | }); | + | |
| - | + | ||
| - | return `\n\n%%**%%Sources%%**%%\n${lines.join(' | + | |
| } | } | ||
| - | + | ||
| - | + | ||
| class EchoBot extends ActivityHandler { | class EchoBot extends ActivityHandler { | ||
| - | + | | |
| - | constructor() { | + | super(); |
| - | + | ||
| - | super(); | + | this.onMessage(async (context, next) => { |
| - | + | const userInput = (context.activity.text || '' | |
| - | + | if (!userInput) { | |
| - | + | await context.sendActivity(' | |
| - | this.onMessage(async (context, next) => { | + | return; |
| - | + | } | |
| - | const userInput = (context.activity.text || '' | + | |
| - | + | try { | |
| - | if (!userInput) { | + | const result = await answerQuestion(userInput); |
| - | + | const text = `${result.answer}${renderSources(result.citations)}`; | |
| - | await context.sendActivity(' | + | await context.sendActivity(MessageFactory.text(text, |
| - | + | } catch (err) { | |
| - | return; | + | console.error(' |
| + | await context.sendActivity(' | ||
| + | } | ||
| + | |||
| + | await next(); | ||
| + | }); | ||
| + | |||
| + | this.onMembersAdded(async (context, next) => { | ||
| + | const welcomeText = 'Hello and welcome! Ask me any question about our documents.'; | ||
| + | for (const member of context.activity.membersAdded) { | ||
| + | if (member.id !== context.activity.recipient.id) { | ||
| + | await context.sendActivity(MessageFactory.text(welcomeText, | ||
| + | } | ||
| + | } | ||
| + | await next(); | ||
| + | }); | ||
| + | } | ||
| } | } | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | try { | + | |
| - | + | ||
| - | const result = await answerQuestion(userInput); | + | |
| - | + | ||
| - | const text = `${result.answer}${renderSources(result.citations)}`; | + | |
| - | + | ||
| - | await context.sendActivity(MessageFactory.text(text, | + | |
| - | + | ||
| - | } catch (err) { | + | |
| - | + | ||
| - | console.error(' | + | |
| - | + | ||
| - | await context.sendActivity(' | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | + | ||
| - | + | ||
| - | await next(); | + | |
| - | + | ||
| - | }); | + | |
| - | + | ||
| - | + | ||
| - | + | ||
| - | this.onMembersAdded(async (context, next) => { | + | |
| - | + | ||
| - | const welcomeText = 'Hello and welcome! Ask me any question about our documents.'; | + | |
| - | + | ||
| - | for (const member of context.activity.membersAdded) { | + | |
| - | + | ||
| - | if (member.id !== context.activity.recipient.id) { | + | |
| - | + | ||
| - | await context.sendActivity(MessageFactory.text(welcomeText, | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | await next(); | + | |
| - | + | ||
| - | }); | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | + | ||
| module.exports.EchoBot = EchoBot; | module.exports.EchoBot = EchoBot; | ||
| </ | </ | ||
| Line 520: | Line 414: | ||
| < | < | ||
| require(' | require(' | ||
| - | |||
| const fs = require(' | const fs = require(' | ||
| - | |||
| const path = require(' | const path = require(' | ||
| - | |||
| const { answerQuestion, | const { answerQuestion, | ||
| - | + | ||
| - | + | // ---------------- CLI args ---------------- | |
| - | + | ||
| - | %%//%% ---------------- CLI args ---------------- | + | |
| function arg(name, def) { | function arg(name, def) { | ||
| - | + | | |
| - | const p = `--${name}=`; | + | const a = process.argv.find(x => x.startsWith(p)); |
| - | + | if (!a) return def; | |
| - | const a = process.argv.find(x => x.startsWith(p)); | + | const v = a.slice(p.length); |
| - | + | if (v === ' | |
| - | if (!a) return def; | + | if (v === ' |
| - | + | const n = Number(v); | |
| - | const v = a.slice(p.length); | + | return Number.isFinite(n) ? n : v; |
| - | + | ||
| - | if (v === ' | + | |
| - | + | ||
| - | if (v === ' | + | |
| - | + | ||
| - | const n = Number(v); | + | |
| - | + | ||
| - | return Number.isFinite(n) ? n : v; | + | |
| } | } | ||
| - | + | ||
| - | + | ||
| const K = arg(' | const K = arg(' | ||
| - | + | const LIMIT = arg(' | |
| - | const LIMIT = arg(' | + | |
| const USE_JUDGE = arg(' | const USE_JUDGE = arg(' | ||
| - | + | ||
| - | + | // ---------------- Helpers ---------------- | |
| - | + | ||
| - | %%//%% ---------------- Helpers ---------------- | + | |
| function loadJsonl(p) { | function loadJsonl(p) { | ||
| - | + | | |
| - | const lines = fs.readFileSync(p, | + | return lines.map((l, |
| - | + | try { return JSON.parse(l); | |
| - | return lines.map((l, | + | catch { |
| - | + | return { id: `row${i+1}`, | |
| - | try { return JSON.parse(l); | + | } |
| - | + | }); | |
| - | catch { | + | |
| - | + | ||
| - | return { id: `row${i+1}`, | + | |
| } | } | ||
| - | + | ||
| - | }); | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | + | ||
| function recallAtK(retrieved, | function recallAtK(retrieved, | ||
| - | + | | |
| - | if (!Array.isArray(refs) || !refs.length) return null; | + | const ids = new Set(retrieved.slice(0, |
| - | + | const hits = refs.filter(r => ids.has(r)).length; | |
| - | const ids = new Set(retrieved.slice(0, | + | return { hits, total: refs.length, |
| - | + | ||
| - | const hits = refs.filter(r => ids.has(r)).length; | + | |
| - | + | ||
| - | return { hits, total: refs.length, | + | |
| } | } | ||
| - | + | ||
| - | + | ||
| function parentRecallAtK(retrieved, | function parentRecallAtK(retrieved, | ||
| - | + | | |
| - | if (!Array.isArray(parentRefs) || !parentRefs.length) return null; | + | const pids = new Set(retrieved.slice(0, |
| - | + | const hits = parentRefs.filter(r => pids.has(r)).length; | |
| - | const pids = new Set(retrieved.slice(0, | + | return { hits, total: parentRefs.length, |
| - | + | ||
| - | const hits = parentRefs.filter(r => pids.has(r)).length; | + | |
| - | + | ||
| - | return { hits, total: parentRefs.length, | + | |
| } | } | ||
| - | + | ||
| - | + | ||
| function mustMentionCoverage(answer, | function mustMentionCoverage(answer, | ||
| - | + | | |
| - | if (!Array.isArray(must) || !must.length) return null; | + | const a = (answer || '' |
| - | + | const checks = must.map(m => ({ term: m, present: a.includes(String(m).toLowerCase()) })); | |
| - | const a = (answer || '' | + | const pct = checks.reduce((s, |
| - | + | return { coverage: pct, checks }; | |
| - | const checks = must.map(m => ({ term: m, present: a.includes(String(m).toLowerCase()) })); | + | |
| - | + | ||
| - | const pct = checks.reduce((s, | + | |
| - | + | ||
| - | return { coverage: pct, checks }; | + | |
| } | } | ||
| - | + | ||
| - | + | ||
| async function llmJudge(row, | async function llmJudge(row, | ||
| - | + | | |
| - | %%//%% Optional LLM-as-a-judge: | + | // Uses your chat deployment via openaiService._internals.chat |
| - | + | const { chat } = _internals; | |
| - | %%//%% Uses your chat deployment via openaiService._internals.chat | + | const sys = { role: ' |
| - | + | 'You are scoring an answer for a Retrieval-Augmented Generation (RAG) system.', | |
| - | const { chat } = _internals; | + | ' |
| - | + | ' | |
| - | const sys = { role: ' | + | '- groundedness: |
| - | + | '- relevance: The answer addresses the user question (on-topic).', | |
| - | 'You are scoring an answer for a Retrieval-Augmented Generation (RAG) system.', | + | '- completeness: |
| - | + | ].join(' | |
| - | ' | + | |
| - | + | const payload = { | |
| - | ' | + | role: ' |
| - | + | content: [ | |
| - | '- groundedness: | + | `Question: ${row.question}`, |
| - | + | `Answer: ${result.answer}`, | |
| - | '- relevance: The answer addresses the user question (on-topic).', | + | `Citations: ${JSON.stringify(result.citations, |
| - | + | `Top Passages: ${JSON.stringify(result.retrieved.slice(0, | |
| - | '- completeness: | + | ' |
| - | + | ].join(' | |
| - | ].join(' | + | }; |
| - | + | ||
| - | + | try { | |
| - | + | const data = await chat([sys, payload], { temperature: | |
| - | const payload = { | + | const txt = data.choices? |
| - | + | const obj = JSON.parse(txt); | |
| - | role: ' | + | const g = Math.max(0, Math.min(5, Number(obj.groundedness)||0)); |
| - | + | const r = Math.max(0, Math.min(5, Number(obj.relevance)||0)); | |
| - | content: [ | + | const c = Math.max(0, Math.min(5, Number(obj.completeness)||0)); |
| - | + | return { groundedness: | |
| - | `Question: ${row.question}`, | + | } catch (e) { |
| - | + | return { groundedness: | |
| - | `Answer: ${result.answer}`, | + | } |
| - | + | ||
| - | `Citations: ${JSON.stringify(result.citations, | + | |
| - | + | ||
| - | `Top Passages: ${JSON.stringify(result.retrieved.slice(0, | + | |
| - | + | ||
| - | ' | + | |
| - | + | ||
| - | ].join(' | + | |
| - | + | ||
| - | }; | + | |
| - | + | ||
| - | + | ||
| - | + | ||
| - | try { | + | |
| - | + | ||
| - | const data = await chat([sys, payload], { temperature: | + | |
| - | + | ||
| - | const txt = data.choices? | + | |
| - | + | ||
| - | const obj = JSON.parse(txt); | + | |
| - | + | ||
| - | const g = Math.max(0, Math.min(5, Number(obj.groundedness)||0)); | + | |
| - | + | ||
| - | const r = Math.max(0, Math.min(5, Number(obj.relevance)||0)); | + | |
| - | + | ||
| - | const c = Math.max(0, Math.min(5, Number(obj.completeness)||0)); | + | |
| - | + | ||
| - | return { groundedness: | + | |
| - | + | ||
| - | } catch (e) { | + | |
| - | + | ||
| - | return { groundedness: | + | |
| } | } | ||
| - | + | ||
| - | } | + | |
| - | + | ||
| - | + | ||
| function mean(arr) { | function mean(arr) { | ||
| - | + | | |
| - | const xs = arr.filter(x => typeof x === ' | + | if (!xs.length) return null; |
| - | + | return xs.reduce((a, | |
| - | if (!xs.length) return null; | + | |
| - | + | ||
| - | return xs.reduce((a, | + | |
| } | } | ||
| - | + | ||
| - | + | // ---------------- Main ---------------- | |
| - | + | ||
| - | %%//%% ---------------- Main ---------------- | + | |
| async function main() { | async function main() { | ||
| - | + | | |
| - | const datasetPath = path.join(%%__%%dirname, ' | + | if (!fs.existsSync(datasetPath)) { |
| - | + | console.error(' | |
| - | if (!fs.existsSync(datasetPath)) { | + | process.exit(1); |
| - | + | } | |
| - | console.error(' | + | |
| - | + | const rowsAll = loadJsonl(datasetPath); | |
| - | process.exit(1); | + | const rows = LIMIT ? rowsAll.slice(0, |
| + | const results = []; | ||
| + | |||
| + | for (const row of rows) { | ||
| + | const options = {}; | ||
| + | if (row.filter) options.filter = row.filter; // OData filter passthrough | ||
| + | |||
| + | const res = await answerQuestion(row.question, | ||
| + | |||
| + | const rDoc = recallAtK(res.retrieved, | ||
| + | const rPar = parentRecallAtK(res.retrieved, | ||
| + | const mention = mustMentionCoverage(res.answer, | ||
| + | let judge = null; | ||
| + | if (USE_JUDGE) judge = await llmJudge(row, | ||
| + | |||
| + | results.push({ | ||
| + | id: row.id, | ||
| + | question: row.question, | ||
| + | references: row.references || null, | ||
| + | parent_refs: | ||
| + | must_mention: | ||
| + | filter: row.filter || null, | ||
| + | answer: res.answer, | ||
| + | citations: res.citations, | ||
| + | metrics: { | ||
| + | retrievalLatencyMs: | ||
| + | generationLatencyMs: | ||
| + | usage: res.metrics.usage || null, | ||
| + | expansions: res.metrics.expansions || null, | ||
| + | recallAtK: rDoc, | ||
| + | parentRecallAtK: | ||
| + | mustMention: | ||
| + | judge | ||
| + | } | ||
| + | }); | ||
| + | |||
| + | const recStr = rDoc ? rDoc.recall.toFixed(2) : ' | ||
| + | const lat = res.metrics.generationLatencyMs; | ||
| + | console.log(`✓ ${row.id} | ||
| + | } | ||
| + | |||
| + | // Summary | ||
| + | const p = path.join(__dirname, | ||
| + | fs.writeFileSync(p, | ||
| + | console.log(' | ||
| + | |||
| + | const rVals = results.map(r => r.metrics.recallAtK? | ||
| + | const prVals = results.map(r => r.metrics.parentRecallAtK? | ||
| + | const retLat = results.map(r => r.metrics.retrievalLatencyMs).filter(Number.isFinite); | ||
| + | const genLat = results.map(r => r.metrics.generationLatencyMs).filter(Number.isFinite); | ||
| + | |||
| + | const gScores = results.map(r => r.metrics.judge? | ||
| + | const relScores = results.map(r => r.metrics.judge? | ||
| + | const compScores = results.map(r => r.metrics.judge? | ||
| + | |||
| + | const summary = { | ||
| + | count: results.length, | ||
| + | k: K, | ||
| + | avgRecallAtK: | ||
| + | avgParentRecallAtK: | ||
| + | avgRetrievalLatencyMs: | ||
| + | avgGenerationLatencyMs: | ||
| + | judgeAverages: | ||
| + | groundedness: | ||
| + | relevance: mean(relScores), | ||
| + | completeness: | ||
| + | } : null | ||
| + | }; | ||
| + | |||
| + | console.log(' | ||
| } | } | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | const rowsAll = loadJsonl(datasetPath); | + | |
| - | + | ||
| - | const rows = LIMIT ? rowsAll.slice(0, | + | |
| - | + | ||
| - | const results = []; | + | |
| - | + | ||
| - | + | ||
| - | + | ||
| - | for (const row of rows) { | + | |
| - | + | ||
| - | const options = {}; | + | |
| - | + | ||
| - | if (row.filter) options.filter = row.filter; %%//%% OData filter passthrough | + | |
| - | + | ||
| - | + | ||
| - | + | ||
| - | const res = await answerQuestion(row.question, | + | |
| - | + | ||
| - | + | ||
| - | + | ||
| - | const rDoc = recallAtK(res.retrieved, | + | |
| - | + | ||
| - | const rPar = parentRecallAtK(res.retrieved, | + | |
| - | + | ||
| - | const mention = mustMentionCoverage(res.answer, | + | |
| - | + | ||
| - | let judge = null; | + | |
| - | + | ||
| - | if (USE_JUDGE) judge = await llmJudge(row, | + | |
| - | + | ||
| - | + | ||
| - | + | ||
| - | results.push({ | + | |
| - | + | ||
| - | id: row.id, | + | |
| - | + | ||
| - | question: row.question, | + | |
| - | + | ||
| - | references: row.references || null, | + | |
| - | + | ||
| - | parent_refs: | + | |
| - | + | ||
| - | must_mention: | + | |
| - | + | ||
| - | filter: row.filter || null, | + | |
| - | + | ||
| - | answer: res.answer, | + | |
| - | + | ||
| - | citations: res.citations, | + | |
| - | + | ||
| - | metrics: { | + | |
| - | + | ||
| - | retrievalLatencyMs: | + | |
| - | + | ||
| - | generationLatencyMs: | + | |
| - | + | ||
| - | usage: res.metrics.usage || null, | + | |
| - | + | ||
| - | expansions: res.metrics.expansions || null, | + | |
| - | + | ||
| - | recallAtK: rDoc, | + | |
| - | + | ||
| - | parentRecallAtK: | + | |
| - | + | ||
| - | mustMention: | + | |
| - | + | ||
| - | judge | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | }); | + | |
| - | + | ||
| - | + | ||
| - | + | ||
| - | const recStr = rDoc ? rDoc.recall.toFixed(2) : ' | + | |
| - | + | ||
| - | const lat = res.metrics.generationLatencyMs; | + | |
| - | + | ||
| - | console.log(`✓ ${row.id} recall@${K}=${recStr} gen=${lat}ms`); | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | + | ||
| - | + | ||
| - | %%//%% Summary | + | |
| - | + | ||
| - | const p = path.join(%%__%%dirname, | + | |
| - | + | ||
| - | fs.writeFileSync(p, | + | |
| - | + | ||
| - | console.log(' | + | |
| - | + | ||
| - | + | ||
| - | + | ||
| - | const rVals = results.map(r => r.metrics.recallAtK? | + | |
| - | + | ||
| - | const prVals = results.map(r => r.metrics.parentRecallAtK? | + | |
| - | + | ||
| - | const retLat = results.map(r => r.metrics.retrievalLatencyMs).filter(Number.isFinite); | + | |
| - | + | ||
| - | const genLat = results.map(r => r.metrics.generationLatencyMs).filter(Number.isFinite); | + | |
| - | + | ||
| - | + | ||
| - | + | ||
| - | const gScores = results.map(r => r.metrics.judge? | + | |
| - | + | ||
| - | const relScores = results.map(r => r.metrics.judge? | + | |
| - | + | ||
| - | const compScores = results.map(r => r.metrics.judge? | + | |
| - | + | ||
| - | + | ||
| - | + | ||
| - | const summary = { | + | |
| - | + | ||
| - | count: results.length, | + | |
| - | + | ||
| - | k: K, | + | |
| - | + | ||
| - | avgRecallAtK: | + | |
| - | + | ||
| - | avgParentRecallAtK: | + | |
| - | + | ||
| - | avgRetrievalLatencyMs: | + | |
| - | + | ||
| - | avgGenerationLatencyMs: | + | |
| - | + | ||
| - | judgeAverages: | + | |
| - | + | ||
| - | groundedness: | + | |
| - | + | ||
| - | relevance: mean(relScores), | + | |
| - | + | ||
| - | completeness: | + | |
| - | + | ||
| - | } : null | + | |
| - | + | ||
| - | }; | + | |
| - | + | ||
| - | + | ||
| - | + | ||
| - | console.log(' | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | + | ||
| if (require.main === module) { | if (require.main === module) { | ||
| - | + | | |
| - | main().catch(err => { console.error(err); | + | |
| } | } | ||
| </ | </ | ||
| Line 886: | Line 600: | ||
| < | < | ||
| const path = require(' | const path = require(' | ||
| - | + | require(' | |
| - | require(' | + | |
| - | + | ||
| - | + | ||
| const restify = require(' | const restify = require(' | ||
| - | |||
| const { | const { | ||
| - | + | | |
| - | CloudAdapter, | + | ConfigurationServiceClientCredentialFactory, |
| - | + | createBotFrameworkAuthenticationFromConfiguration | |
| - | ConfigurationServiceClientCredentialFactory, | + | |
| - | + | ||
| - | createBotFrameworkAuthenticationFromConfiguration | + | |
| } = require(' | } = require(' | ||
| - | + | ||
| - | + | // (Optional) Application Insights for runtime telemetry | |
| - | + | ||
| - | %%//%% (Optional) Application Insights for runtime telemetry | + | |
| try { | try { | ||
| - | + | | |
| - | if (process.env.APPINSIGHTS_CONNECTION_STRING) { | + | require(' |
| - | + | console.log(' | |
| - | require(' | + | } |
| - | + | ||
| - | console.log(' | + | |
| - | + | ||
| - | } | + | |
| } catch (e) { | } catch (e) { | ||
| - | + | | |
| - | console.warn(' | + | |
| } | } | ||
| - | + | ||
| - | + | // Import your bot implementation | |
| - | + | ||
| - | %%//%% Import your bot implementation | + | |
| const { EchoBot } = require(' | const { EchoBot } = require(' | ||
| - | + | ||
| - | + | // --- Server setup --- | |
| - | + | ||
| - | %%//%% --- Server setup --- | + | |
| const server = restify.createServer(); | const server = restify.createServer(); | ||
| - | |||
| server.use(restify.plugins.bodyParser()); | server.use(restify.plugins.bodyParser()); | ||
| - | |||
| server.use(restify.plugins.queryParser()); | server.use(restify.plugins.queryParser()); | ||
| - | + | ||
| - | + | // Prefer Azure' | |
| - | + | ||
| - | %%//%% Prefer Azure' | + | |
| const rawPort = process.env.PORT || process.env.port || 8080; | const rawPort = process.env.PORT || process.env.port || 8080; | ||
| - | |||
| const port = Number(rawPort); | const port = Number(rawPort); | ||
| - | |||
| console.log(' | console.log(' | ||
| - | + | ||
| - | + | ||
| server.listen(port, | server.listen(port, | ||
| - | + | | |
| - | console.log(`${server.name} listening on ${server.url}`); | + | console.log(`Node: |
| - | + | console.log(' | |
| - | console.log(`Node: | + | console.log(' |
| - | + | ||
| - | console.log(' | + | |
| - | + | ||
| - | console.log(' | + | |
| }); | }); | ||
| - | + | ||
| - | + | // Health endpoint for App Service | |
| - | + | ||
| - | %%//%% Health endpoint for App Service | + | |
| server.get('/ | server.get('/ | ||
| - | + | | |
| - | res.send(200, | + | ok: true, |
| - | + | uptimeSec: Math.round(process.uptime()), | |
| - | ok: true, | + | node: process.version |
| - | + | }); | |
| - | uptimeSec: Math.round(process.uptime()), | + | return next(); |
| - | + | ||
| - | node: process.version | + | |
| }); | }); | ||
| - | + | // 1) What is Search returning right now? | |
| - | return next(); | + | |
| - | + | ||
| - | }); | + | |
| - | + | ||
| - | %%//%% 1) What is Search returning right now? | + | |
| const { _internals } = require(' | const { _internals } = require(' | ||
| - | + | ||
| - | + | ||
| server.get('/ | server.get('/ | ||
| - | + | | |
| - | try { | + | const { _internals } = require(' |
| - | + | const q = req.query.q || ''; | |
| - | const { _internals } = require(' | + | const items = await _internals.searchOnce({ query: q }); |
| - | + | res.send(200, | |
| - | const q = req.query.q || ''; | + | ok: true, |
| - | + | q, | |
| - | const items = await _internals.searchOnce({ query: q }); | + | returned: items.length, |
| - | + | items: items.slice(0, | |
| - | res.send(200, | + | id: x.id, |
| - | + | title: x.title, | |
| - | ok: true, | + | contentPreview: |
| - | + | })) | |
| - | q, | + | }); |
| - | + | } catch (e) { | |
| - | returned: items.length, | + | |
| - | + | } | |
| - | items: items.slice(0, | + | |
| - | + | ||
| - | id: x.id, | + | |
| - | + | ||
| - | title: x.title, | + | |
| - | + | ||
| - | contentPreview: | + | |
| - | + | ||
| - | })) | + | |
| }); | }); | ||
| - | + | ||
| - | } catch (e) { | + | // 2) End-to-end answer (to see retrieved count + citations) |
| - | + | ||
| - | res.send(503, | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | }); | + | |
| - | + | ||
| - | + | ||
| - | + | ||
| - | %%//%% 2) End-to-end answer (to see retrieved count + citations) | + | |
| const { answerQuestion } = require(' | const { answerQuestion } = require(' | ||
| - | |||
| server.get('/ | server.get('/ | ||
| - | + | | |
| - | try { | + | const { answerQuestion } = require(' |
| - | + | const q = req.query.q || ''; | |
| - | const { answerQuestion } = require(' | + | const out = await answerQuestion(q); |
| - | + | res.send(200, | |
| - | const q = req.query.q || ''; | + | ok: true, |
| - | + | query: q, | |
| - | const out = await answerQuestion(q); | + | retrievedCount: |
| - | + | citationsCount: | |
| - | res.send(200, | + | answer: out.answer, |
| - | + | sampleRetrieved: | |
| - | ok: true, | + | id: r.id, title: r.title, preview: (r.content||'' |
| - | + | })) || [] | |
| - | query: q, | + | }); |
| - | + | } catch (e) { | |
| - | retrievedCount: | + | res.send(503, |
| - | + | } | |
| - | citationsCount: | + | |
| - | + | ||
| - | answer: out.answer, | + | |
| - | + | ||
| - | sampleRetrieved: | + | |
| - | + | ||
| - | id: r.id, title: r.title, preview: (r.content||'' | + | |
| - | + | ||
| - | })) || [] | + | |
| }); | }); | ||
| - | + | // How many docs are in the index? | |
| - | } catch (e) { | + | |
| - | + | ||
| - | res.send(503, | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | }); | + | |
| - | + | ||
| - | %%//%% How many docs are in the index? | + | |
| server.get('/ | server.get('/ | ||
| - | + | | |
| - | try { | + | const { SearchClient, |
| - | + | const endpoint = process.env.AZURE_SEARCH_ENDPOINT.trim(); | |
| - | const { SearchClient, | + | const index = process.env.AZURE_SEARCH_INDEX_NAME.trim(); |
| - | + | const key = (process.env.AZURE_SEARCH_QUERY_KEY || process.env.AZURE_SEARCH_API_KEY).trim(); | |
| - | const endpoint = process.env.AZURE_SEARCH_ENDPOINT.trim(); | + | const client = new SearchClient(endpoint, |
| - | + | const count = await client.getDocumentsCount(); | |
| - | const index = process.env.AZURE_SEARCH_INDEX_NAME.trim(); | + | res.send(200, |
| - | + | } catch (e) { | |
| - | const key = (process.env.AZURE_SEARCH_QUERY_KEY || process.env.AZURE_SEARCH_API_KEY).trim(); | + | res.send(503, |
| - | + | } | |
| - | const client = new SearchClient(endpoint, | + | |
| - | + | ||
| - | const count = await client.getDocumentsCount(); | + | |
| - | + | ||
| - | res.send(200, | + | |
| - | + | ||
| - | } catch (e) { | + | |
| - | + | ||
| - | res.send(503, | + | |
| - | + | ||
| - | } | + | |
| }); | }); | ||
| - | + | ||
| - | + | // Vector diag: embed the query and run a pure vector search | |
| - | + | ||
| - | %%//%% Vector diag: embed the query and run a pure vector search | + | |
| server.get('/ | server.get('/ | ||
| - | + | | |
| - | try { | + | const q = (req.query && req.query.q) ? String(req.query.q) : ''; |
| - | + | if (!q) return res.send(400, | |
| - | const q = (req.query && req.query.q) ? String(req.query.q) : ''; | + | |
| - | + | const { _internals } = require(' | |
| - | if (!q) return res.send(400, | + | const vec = await _internals.embedMemo(q); |
| - | + | const items = await _internals.searchOnce({ query: '', | |
| - | + | ||
| - | + | res.send(200, | |
| - | const { _internals } = require(' | + | ok: true, |
| - | + | q, | |
| - | const vec = await _internals.embedMemo(q); | + | returned: items.length, |
| - | + | items: items.slice(0, | |
| - | const items = await _internals.searchOnce({ query: '', | + | id: x.id, |
| - | + | title: x.title, | |
| - | + | contentPreview: | |
| - | + | })) | |
| - | res.send(200, | + | }); |
| - | + | } catch (e) { | |
| - | ok: true, | + | |
| - | + | } | |
| - | q, | + | |
| - | + | ||
| - | returned: items.length, | + | |
| - | + | ||
| - | items: items.slice(0, | + | |
| - | + | ||
| - | id: x.id, | + | |
| - | + | ||
| - | title: x.title, | + | |
| - | + | ||
| - | contentPreview: | + | |
| - | + | ||
| - | })) | + | |
| }); | }); | ||
| - | + | ||
| - | } catch (e) { | + | |
| - | + | ||
| - | res.send(503, | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | }); | + | |
| - | + | ||
| - | + | ||
| const axios = require(' | const axios = require(' | ||
| - | + | ||
| - | + | // Ultra-raw: call Azure Search REST API directly (bypasses SDK iterator) | |
| - | + | ||
| - | %%//%% Ultra-raw: call Azure Search REST API directly (bypasses SDK iterator) | + | |
| server.get('/ | server.get('/ | ||
| - | + | | |
| - | try { | + | const endpoint = String(process.env.AZURE_SEARCH_ENDPOINT || '' |
| - | + | const index = String(process.env.AZURE_SEARCH_INDEX_NAME || '' | |
| - | const endpoint = String(process.env.AZURE_SEARCH_ENDPOINT || '' | + | const apiKey = String(process.env.AZURE_SEARCH_QUERY_KEY || process.env.AZURE_SEARCH_API_KEY || '' |
| - | + | ||
| - | const index = String(process.env.AZURE_SEARCH_INDEX_NAME || '' | + | if (!endpoint || !index || !apiKey) { |
| - | + | return res.send(400, | |
| - | const apiKey = String(process.env.AZURE_SEARCH_QUERY_KEY || process.env.AZURE_SEARCH_API_KEY || '' | + | } |
| - | + | ||
| - | + | const q = (req.query && typeof req.query.q === ' | |
| - | + | const url = `${endpoint}/ | |
| - | if (!endpoint || !index || !apiKey) { | + | |
| - | + | // IMPORTANT: set select conservatively; | |
| - | return res.send(400, | + | // If your text field is `contents`, keep " |
| - | + | const body = { | |
| - | } | + | search: q, // '' |
| - | + | top: 5, | |
| - | + | select: ' | |
| - | + | }; | |
| - | const q = (req.query && typeof req.query.q === ' | + | |
| - | + | const { data } = await axios.post(url, | |
| - | const url = `${endpoint}/ | + | headers: { |
| - | + | ' | |
| - | + | ' | |
| - | + | ' | |
| - | %%//%% IMPORTANT: set select conservatively; | + | }, |
| - | + | timeout: 10000 | |
| - | %%//%% If your text field is `contents`, keep " | + | }); |
| - | + | ||
| - | const body = { | + | const items = Array.isArray(data.value) ? data.value.map(d => ({ |
| - | + | id: d.id, | |
| - | search: q, %%//%% '' | + | title: d.title || null, |
| - | + | contentPreview: | |
| - | top: 5, | + | url: d.url || null |
| - | + | })) : []; | |
| - | select: ' | + | |
| - | + | res.send(200, | |
| - | }; | + | } catch (e) { |
| - | + | res.send(503, | |
| - | + | } | |
| - | + | ||
| - | const { data } = await axios.post(url, | + | |
| - | + | ||
| - | headers: { | + | |
| - | + | ||
| - | ' | + | |
| - | + | ||
| - | ' | + | |
| - | + | ||
| - | ' | + | |
| - | + | ||
| - | }, | + | |
| - | + | ||
| - | timeout: 10000 | + | |
| }); | }); | ||
| - | + | ||
| - | + | // Simple root info (optional) | |
| - | + | ||
| - | const items = Array.isArray(data.value) ? data.value.map(d => ({ | + | |
| - | + | ||
| - | id: d.id, | + | |
| - | + | ||
| - | title: d.title || null, | + | |
| - | + | ||
| - | contentPreview: | + | |
| - | + | ||
| - | url: d.url || null | + | |
| - | + | ||
| - | })) : []; | + | |
| - | + | ||
| - | + | ||
| - | + | ||
| - | res.send(200, | + | |
| - | + | ||
| - | } catch (e) { | + | |
| - | + | ||
| - | res.send(503, | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | }); | + | |
| - | + | ||
| - | + | ||
| - | + | ||
| - | %%//%% Simple root info (optional) | + | |
| server.get('/', | server.get('/', | ||
| - | + | | |
| - | res.send(200, | + | return next(); |
| - | + | ||
| - | return next(); | + | |
| }); | }); | ||
| - | + | ||
| - | + | // --- Bot Framework adapter (credentials from env) --- | |
| - | + | ||
| - | %%//%% --- Bot Framework adapter (credentials from env) --- | + | |
| const credentialsFactory = new ConfigurationServiceClientCredentialFactory({ | const credentialsFactory = new ConfigurationServiceClientCredentialFactory({ | ||
| - | + | | |
| - | MicrosoftAppId: | + | MicrosoftAppPassword: |
| - | + | MicrosoftAppType: | |
| - | MicrosoftAppPassword: | + | MicrosoftAppTenantId: |
| - | + | ||
| - | MicrosoftAppType: | + | |
| - | + | ||
| - | MicrosoftAppTenantId: | + | |
| }); | }); | ||
| - | + | ||
| - | + | ||
| const botFrameworkAuthentication = | const botFrameworkAuthentication = | ||
| - | + | | |
| - | createBotFrameworkAuthenticationFromConfiguration(null, | + | |
| - | + | ||
| - | + | ||
| const adapter = new CloudAdapter(botFrameworkAuthentication); | const adapter = new CloudAdapter(botFrameworkAuthentication); | ||
| - | + | ||
| - | + | // Catch-all for errors | |
| - | + | ||
| - | %%//%% Catch-all for errors | + | |
| adapter.onTurnError = async (context, error) => { | adapter.onTurnError = async (context, error) => { | ||
| - | + | | |
| - | console.error(' | + | try { |
| - | + | await context.sendTraceActivity( | |
| - | try { | + | ' |
| - | + | `${error}`, | |
| - | await context.sendTraceActivity( | + | ' |
| - | + | ' | |
| - | ' | + | ); |
| - | + | await context.sendActivity(' | |
| - | `${error}`, | + | } catch (_) { |
| - | + | // ignore secondary failures | |
| - | ' | + | } |
| - | + | ||
| - | ' | + | |
| - | + | ||
| - | ); | + | |
| - | + | ||
| - | await context.sendActivity(' | + | |
| - | + | ||
| - | } catch (_) { | + | |
| - | + | ||
| - | %%//%% ignore secondary failures | + | |
| - | + | ||
| - | } | + | |
| }; | }; | ||
| - | + | ||
| - | + | // Create the bot instance | |
| - | + | ||
| - | %%//%% Create the bot instance | + | |
| const bot = new EchoBot(); | const bot = new EchoBot(); | ||
| - | + | ||
| - | + | // Bot messages endpoint (required by Azure Bot Service) | |
| - | + | ||
| - | %%//%% Bot messages endpoint (required by Azure Bot Service) | + | |
| server.post('/ | server.post('/ | ||
| - | + | | |
| - | await adapter.process(req, | + | |
| }); | }); | ||
| - | + | ||
| - | + | // Streaming (Teams / Skills) | |
| - | + | ||
| - | %%//%% Streaming (Teams / Skills) | + | |
| server.on(' | server.on(' | ||
| - | + | | |
| - | const streamingAdapter = new CloudAdapter(botFrameworkAuthentication); | + | streamingAdapter.onTurnError = adapter.onTurnError; |
| - | + | await streamingAdapter.process(req, | |
| - | streamingAdapter.onTurnError = adapter.onTurnError; | + | |
| - | + | ||
| - | await streamingAdapter.process(req, | + | |
| }); | }); | ||
| + | </ | ||
| - | ingest.js | + | === ingest.js |
| + | < | ||
| require(' | require(' | ||
| - | |||
| const mammoth = require(' | const mammoth = require(' | ||
| - | |||
| const fs = require(' | const fs = require(' | ||
| - | |||
| const path = require(' | const path = require(' | ||
| - | |||
| const axios = require(' | const axios = require(' | ||
| - | |||
| const { SearchClient, | const { SearchClient, | ||
| - | + | ||
| - | + | ||
| const SEARCH_ENDPOINT = process.env.AZURE_SEARCH_ENDPOINT; | const SEARCH_ENDPOINT = process.env.AZURE_SEARCH_ENDPOINT; | ||
| - | + | const SEARCH_INDEX = process.env.AZURE_SEARCH_AZURE_SEARCH_INDEX_NAME || process.env.AZURE_SEARCH_INDEX_NAME; | |
| - | const SEARCH_INDEX = process.env.AZURE_SEARCH_AZURE_SEARCH_INDEX_NAME || process.env.AZURE_SEARCH_INDEX_NAME; | + | |
| const SEARCH_KEY = process.env.AZURE_SEARCH_API_KEY; | const SEARCH_KEY = process.env.AZURE_SEARCH_API_KEY; | ||
| - | + | ||
| - | + | ||
| const OPENAI_ENDPOINT = process.env.OPENAI_ENDPOINT; | const OPENAI_ENDPOINT = process.env.OPENAI_ENDPOINT; | ||
| - | |||
| const OPENAI_API_KEY = process.env.OPENAI_API_KEY; | const OPENAI_API_KEY = process.env.OPENAI_API_KEY; | ||
| - | |||
| const OPENAI_API_VERSION = process.env.OPENAI_API_VERSION || ' | const OPENAI_API_VERSION = process.env.OPENAI_API_VERSION || ' | ||
| - | |||
| const OPENAI_EMBEDDING_DEPLOYMENT = process.env.OPENAI_EMBEDDING_DEPLOYMENT || ' | const OPENAI_EMBEDDING_DEPLOYMENT = process.env.OPENAI_EMBEDDING_DEPLOYMENT || ' | ||
| - | + | ||
| - | + | ||
| const VECTOR_FIELD = process.env.VECTOR_FIELD || ' | const VECTOR_FIELD = process.env.VECTOR_FIELD || ' | ||
| - | |||
| const CONTENT_FIELD = process.env.CONTENT_FIELD || ' | const CONTENT_FIELD = process.env.CONTENT_FIELD || ' | ||
| - | |||
| const TITLE_FIELD = process.env.TITLE_FIELD || ' | const TITLE_FIELD = process.env.TITLE_FIELD || ' | ||
| - | |||
| const URL_FIELD = process.env.URL_FIELD || ' | const URL_FIELD = process.env.URL_FIELD || ' | ||
| - | |||
| const METADATA_FIELD = process.env.METADATA_FIELD || ' | const METADATA_FIELD = process.env.METADATA_FIELD || ' | ||
| - | |||
| const PARENT_ID_FIELD = process.env.PARENT_ID_FIELD || ' | const PARENT_ID_FIELD = process.env.PARENT_ID_FIELD || ' | ||
| - | + | ||
| - | + | ||
| function arg(name, def){ const p=`--${name}=`; | function arg(name, def){ const p=`--${name}=`; | ||
| - | + | const dataDir = require(' | |
| - | const dataDir = require(' | + | |
| - | + | ||
| - | + | ||
| const indexClient = new SearchIndexClient(SEARCH_ENDPOINT, | const indexClient = new SearchIndexClient(SEARCH_ENDPOINT, | ||
| - | |||
| const searchClient = new SearchClient(SEARCH_ENDPOINT, | const searchClient = new SearchClient(SEARCH_ENDPOINT, | ||
| - | + | ||
| - | + | ||
| function approxTokenLen(s) { return Math.ceil((s || '' | function approxTokenLen(s) { return Math.ceil((s || '' | ||
| - | + | ||
| - | + | ||
| function chunkText(text, | function chunkText(text, | ||
| - | + | | |
| - | const tokens = approxTokenLen(text); | + | const estCharsPerTok = (text.length || 1) / Math.max(tokens, |
| - | + | const chunkChars = Math.floor(chunkTokens * estCharsPerTok); | |
| - | const estCharsPerTok = (text.length || 1) / Math.max(tokens, | + | const overlapChars = Math.floor(overlapTokens * estCharsPerTok); |
| - | + | const chunks = []; | |
| - | const chunkChars = Math.floor(chunkTokens * estCharsPerTok); | + | let start = 0; |
| - | + | while (start < text.length) { | |
| - | const overlapChars = Math.floor(overlapTokens * estCharsPerTok); | + | const end = Math.min(start + chunkChars, text.length); |
| - | + | chunks.push(text.slice(start, | |
| - | const chunks = []; | + | start = end - overlapChars; |
| - | + | if (start < 0) start = 0; | |
| - | let start = 0; | + | if (end === text.length) break; |
| - | + | } | |
| - | while (start < text.length) { | + | return chunks; |
| - | + | ||
| - | const end = Math.min(start + chunkChars, text.length); | + | |
| - | + | ||
| - | chunks.push(text.slice(start, | + | |
| - | + | ||
| - | start = end - overlapChars; | + | |
| - | + | ||
| - | if (start < 0) start = 0; | + | |
| - | + | ||
| - | if (end === text.length) break; | + | |
| } | } | ||
| - | + | ||
| - | return chunks; | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | + | ||
| async function embed(text) { | async function embed(text) { | ||
| - | + | | |
| - | const url = `${OPENAI_ENDPOINT}/ | + | const { data } = await axios.post(url, |
| - | + | headers: { ' | |
| - | const { data } = await axios.post(url, | + | }); |
| - | + | return data.data[0].embedding; | |
| - | headers: { ' | + | |
| - | + | ||
| - | }); | + | |
| - | + | ||
| - | return data.data[0].embedding; | + | |
| } | } | ||
| - | + | ||
| - | + | ||
| async function ensureIndex() { | async function ensureIndex() { | ||
| - | + | | |
| - | const definition = { | + | name: SEARCH_INDEX, |
| - | + | fields: [ | |
| - | name: SEARCH_INDEX, | + | { name: ' |
| - | + | { name: TITLE_FIELD, | |
| - | fields: [ | + | { name: URL_FIELD, type: ' |
| - | + | { name: CONTENT_FIELD, | |
| - | { name: ' | + | { name: METADATA_FIELD, |
| - | + | { name: ' | |
| - | { name: TITLE_FIELD, | + | { name: ' |
| - | + | { name: ' | |
| - | { name: URL_FIELD, type: ' | + | ]}, |
| - | + | { name: PARENT_ID_FIELD, | |
| - | { name: CONTENT_FIELD, | + | { |
| - | + | name: VECTOR_FIELD, | |
| - | { name: METADATA_FIELD, | + | searchable: true, filterable: false, sortable: false, facetable: false, |
| - | + | vectorSearchDimensions: | |
| - | { name: ' | + | vectorSearchProfileName: |
| - | + | } | |
| - | { name: ' | + | ], |
| - | + | semanticSearch: | |
| - | { name: ' | + | defaultConfigurationName: |
| - | + | configurations: | |
| - | ]}, | + | { |
| - | + | name: (process.env.SEMANTIC_CONFIGURATION || ' | |
| - | { name: PARENT_ID_FIELD, | + | prioritizedFields: |
| - | + | titleField: { name: TITLE_FIELD }, | |
| - | { | + | prioritizedContentFields: |
| - | + | } | |
| - | name: VECTOR_FIELD, | + | } |
| - | + | ] | |
| - | searchable: true, filterable: false, sortable: false, facetable: false, | + | }, |
| - | + | vectorSearch: | |
| - | vectorSearchDimensions: | + | algorithms: [{ name: ' |
| - | + | profiles: [{ name: ' | |
| - | vectorSearchProfileName: | + | } |
| + | }; | ||
| + | |||
| + | try { | ||
| + | await indexClient.getIndex(SEARCH_INDEX); | ||
| + | console.log(' | ||
| + | await indexClient.createOrUpdateIndex(definition); | ||
| + | } catch { | ||
| + | console.log(' | ||
| + | await indexClient.createIndex(definition); | ||
| + | } | ||
| } | } | ||
| - | + | ||
| - | ], | + | |
| - | + | ||
| - | semanticSearch: | + | |
| - | + | ||
| - | defaultConfigurationName: | + | |
| - | + | ||
| - | configurations: | + | |
| - | + | ||
| - | { | + | |
| - | + | ||
| - | name: (process.env.SEMANTIC_CONFIGURATION || ' | + | |
| - | + | ||
| - | prioritizedFields: | + | |
| - | + | ||
| - | titleField: { name: TITLE_FIELD }, | + | |
| - | + | ||
| - | prioritizedContentFields: | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | ] | + | |
| - | + | ||
| - | }, | + | |
| - | + | ||
| - | vectorSearch: | + | |
| - | + | ||
| - | algorithms: [{ name: ' | + | |
| - | + | ||
| - | profiles: [{ name: ' | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | }; | + | |
| - | + | ||
| - | + | ||
| - | + | ||
| - | try { | + | |
| - | + | ||
| - | await indexClient.getIndex(SEARCH_INDEX); | + | |
| - | + | ||
| - | console.log(' | + | |
| - | + | ||
| - | await indexClient.createOrUpdateIndex(definition); | + | |
| - | + | ||
| - | } catch { | + | |
| - | + | ||
| - | console.log(' | + | |
| - | + | ||
| - | await indexClient.createIndex(definition); | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | + | ||
| async function readDocs(dir) { | async function readDocs(dir) { | ||
| - | + | | |
| - | if (!fs.existsSync(dir)) return []; | + | const files = fs.readdirSync(dir); |
| - | + | const docs = []; | |
| - | const files = fs.readdirSync(dir); | + | |
| - | + | for (const f of files) { | |
| - | const docs = []; | + | const full = path.join(dir, |
| - | + | const ext = path.extname(f).toLowerCase(); | |
| - | + | ||
| - | + | let text = null; | |
| - | for (const f of files) { | + | |
| - | + | if (ext === ' | |
| - | const full = path.join(dir, | + | // Convert Word (DOCX) → plain text |
| - | + | const buffer = fs.readFileSync(full); | |
| - | const ext = path.extname(f).toLowerCase(); | + | const { value } = await mammoth.extractRawText({ buffer }); |
| - | + | text = (value || '' | |
| - | + | } else if (/ | |
| - | + | text = fs.readFileSync(full, | |
| - | let text = null; | + | } else if (ext === ' |
| - | + | // Accept JSON too—either stringify or use a specific field if you prefer | |
| - | + | const raw = fs.readFileSync(full, | |
| - | + | try { | |
| - | if (ext === ' | + | const obj = JSON.parse(raw); |
| - | + | text = typeof obj === ' | |
| - | %%//%% Convert Word (DOCX) → plain text | + | } catch { |
| - | + | text = raw; | |
| - | const buffer = fs.readFileSync(full); | + | } |
| - | + | } else { | |
| - | const { value } = await mammoth.extractRawText({ buffer }); | + | // Unsupported type — skip |
| - | + | continue; | |
| - | text = (value || '' | + | } |
| - | + | ||
| - | } else if (/ | + | if (!text) continue; |
| - | + | ||
| - | text = fs.readFileSync(full, | + | const title = path.parse(f).name; |
| - | + | docs.push({ | |
| - | } else if (ext === ' | + | id: title, |
| - | + | title, | |
| - | %%//%% Accept JSON too—either stringify or use a specific field if you prefer | + | url: null, |
| - | + | content: text, | |
| - | const raw = fs.readFileSync(full, | + | metadata: { source: f } |
| - | + | }); | |
| - | try { | + | } |
| - | + | ||
| - | const obj = JSON.parse(raw); | + | return docs; |
| - | + | ||
| - | text = typeof obj === ' | + | |
| - | + | ||
| - | } catch { | + | |
| - | + | ||
| - | text = raw; | + | |
| } | } | ||
| - | + | ||
| - | } else { | + | |
| - | + | ||
| - | %%//%% Unsupported type — skip | + | |
| - | + | ||
| - | continue; | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | + | ||
| - | + | ||
| - | if (!text) continue; | + | |
| - | + | ||
| - | + | ||
| - | + | ||
| - | const title = path.parse(f).name; | + | |
| - | + | ||
| - | docs.push({ | + | |
| - | + | ||
| - | id: title, | + | |
| - | + | ||
| - | title, | + | |
| - | + | ||
| - | url: null, | + | |
| - | + | ||
| - | content: text, | + | |
| - | + | ||
| - | metadata: { source: f } | + | |
| - | + | ||
| - | }); | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | + | ||
| - | + | ||
| - | return docs; | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | + | ||
| async function run() { | async function run() { | ||
| - | + | | |
| - | await ensureIndex(); | + | const docs = await readDocs(dataDir); |
| - | + | console.log(`Found ${docs.length} docs`); | |
| - | const docs = await readDocs(dataDir); | + | |
| - | + | const actions = []; | |
| - | console.log(`Found ${docs.length} docs`); | + | for (const doc of docs) { |
| - | + | const chunks = chunkText(doc.content); | |
| - | + | ||
| - | + | // Make the parent id safe for Azure Search keys: letters/ | |
| - | const actions = []; | + | const parentKey = String(doc.id || '' |
| - | + | ||
| - | for (const doc of docs) { | + | for (let i = 0; i < chunks.length; |
| - | + | const chunk = chunks[i]; | |
| - | const chunks = chunkText(doc.content); | + | |
| - | + | // Chunk key is parentKey + ' | |
| - | + | const id = `${parentKey}-${i}`; | |
| - | + | ||
| - | %%//%% Make the parent id safe for Azure Search keys: letters/ | + | const vec = await embed(chunk); |
| - | + | actions.push({ | |
| - | const parentKey = String(doc.id || '' | + | ' |
| - | + | id, | |
| - | + | [TITLE_FIELD]: | |
| - | + | [URL_FIELD]: | |
| - | for (let i = 0; i < chunks.length; | + | [CONTENT_FIELD]: |
| - | + | [METADATA_FIELD]: | |
| - | const chunk = chunks[i]; | + | [PARENT_ID_FIELD]: |
| - | + | [VECTOR_FIELD]: | |
| - | + | }); | |
| - | + | } | |
| - | %%//%% Chunk key is parentKey + ' | + | } |
| - | + | ||
| - | const id = `${parentKey}-${i}`; | + | console.log(`Uploading ${actions.length} chunks...`); |
| - | + | for (let i = 0; i < actions.length; | |
| - | + | const batch = actions.slice(i, | |
| - | + | await searchClient.mergeOrUploadDocuments(batch); | |
| - | const vec = await embed(chunk); | + | console.log(`Uploaded ${Math.min(i + batch.length, |
| - | + | } | |
| - | actions.push({ | + | console.log(' |
| - | + | ||
| - | ' | + | |
| - | + | ||
| - | id, | + | |
| - | + | ||
| - | [TITLE_FIELD]: | + | |
| - | + | ||
| - | [URL_FIELD]: | + | |
| - | + | ||
| - | [CONTENT_FIELD]: | + | |
| - | + | ||
| - | [METADATA_FIELD]: | + | |
| - | + | ||
| - | [PARENT_ID_FIELD]: | + | |
| - | + | ||
| - | [VECTOR_FIELD]: | + | |
| - | + | ||
| - | }); | + | |
| } | } | ||
| - | + | ||
| - | } | + | |
| - | + | ||
| - | + | ||
| - | + | ||
| - | console.log(`Uploading ${actions.length} chunks...`); | + | |
| - | + | ||
| - | for (let i = 0; i < actions.length; | + | |
| - | + | ||
| - | const batch = actions.slice(i, | + | |
| - | + | ||
| - | await searchClient.mergeOrUploadDocuments(batch); | + | |
| - | + | ||
| - | console.log(`Uploaded ${Math.min(i + batch.length, | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | console.log(' | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | + | ||
| if (require.main === module) { | if (require.main === module) { | ||
| - | + | | |
| - | run().catch(err => { console.error(err); | + | |
| } | } | ||
| </ | </ | ||
| Line 1742: | Line 1034: | ||
| < | < | ||
| require(' | require(' | ||
| - | |||
| const axios = require(' | const axios = require(' | ||
| - | |||
| const { SearchClient, | const { SearchClient, | ||
| - | + | ||
| - | + | // ---------- Helpers / Config ---------- | |
| - | + | ||
| - | %%//%% ---------- Helpers / Config ---------- | + | |
| const cleanName = (v, def) => (v ?? def ?? '' | const cleanName = (v, def) => (v ?? def ?? '' | ||
| - | |||
| const cleanBool = (v, def = ' | const cleanBool = (v, def = ' | ||
| - | |||
| const cleanNum = (v, def) => { | const cleanNum = (v, def) => { | ||
| - | + | | |
| - | const n = Number(String(v ?? '' | + | return Number.isFinite(n) ? n : def; |
| - | + | ||
| - | return Number.isFinite(n) ? n : def; | + | |
| }; | }; | ||
| - | |||
| const sanitizeUrl = s => String(s || '' | const sanitizeUrl = s => String(s || '' | ||
| - | + | ||
| - | + | ||
| const SEARCH_ENDPOINT = sanitizeUrl(process.env.AZURE_SEARCH_ENDPOINT); | const SEARCH_ENDPOINT = sanitizeUrl(process.env.AZURE_SEARCH_ENDPOINT); | ||
| - | |||
| const SEARCH_INDEX = cleanName(process.env.AZURE_SEARCH_INDEX_NAME); | const SEARCH_INDEX = cleanName(process.env.AZURE_SEARCH_INDEX_NAME); | ||
| - | |||
| const SEARCH_KEY = cleanName(process.env.AZURE_SEARCH_API_KEY); | const SEARCH_KEY = cleanName(process.env.AZURE_SEARCH_API_KEY); | ||
| - | + | ||
| - | + | ||
| const OPENAI_ENDPOINT = sanitizeUrl(process.env.OPENAI_ENDPOINT); | const OPENAI_ENDPOINT = sanitizeUrl(process.env.OPENAI_ENDPOINT); | ||
| - | |||
| const OPENAI_API_KEY = cleanName(process.env.OPENAI_API_KEY); | const OPENAI_API_KEY = cleanName(process.env.OPENAI_API_KEY); | ||
| - | |||
| const OPENAI_API_VERSION = cleanName(process.env.OPENAI_API_VERSION, | const OPENAI_API_VERSION = cleanName(process.env.OPENAI_API_VERSION, | ||
| - | |||
| const OPENAI_DEPLOYMENT = cleanName(process.env.OPENAI_DEPLOYMENT, | const OPENAI_DEPLOYMENT = cleanName(process.env.OPENAI_DEPLOYMENT, | ||
| - | |||
| const OPENAI_EMBEDDING_DEPLOYMENT = cleanName(process.env.OPENAI_EMBEDDING_DEPLOYMENT, | const OPENAI_EMBEDDING_DEPLOYMENT = cleanName(process.env.OPENAI_EMBEDDING_DEPLOYMENT, | ||
| - | + | ||
| - | + | ||
| const VECTOR_FIELD = cleanName(process.env.VECTOR_FIELD, | const VECTOR_FIELD = cleanName(process.env.VECTOR_FIELD, | ||
| - | + | const CONTENT_FIELD = cleanName(process.env.CONTENT_FIELD, | |
| - | const CONTENT_FIELD = cleanName(process.env.CONTENT_FIELD, | + | |
| const TITLE_FIELD = cleanName(process.env.TITLE_FIELD, | const TITLE_FIELD = cleanName(process.env.TITLE_FIELD, | ||
| - | |||
| const URL_FIELD = cleanName(process.env.URL_FIELD, | const URL_FIELD = cleanName(process.env.URL_FIELD, | ||
| - | |||
| const METADATA_FIELD = cleanName(process.env.METADATA_FIELD, | const METADATA_FIELD = cleanName(process.env.METADATA_FIELD, | ||
| - | |||
| const PARENT_ID_FIELD = cleanName(process.env.PARENT_ID_FIELD, | const PARENT_ID_FIELD = cleanName(process.env.PARENT_ID_FIELD, | ||
| - | + | ||
| - | + | ||
| const SEMANTIC_CONFIGURATION = cleanName(process.env.SEMANTIC_CONFIGURATION, | const SEMANTIC_CONFIGURATION = cleanName(process.env.SEMANTIC_CONFIGURATION, | ||
| - | |||
| const USE_SEMANTIC_RANKER = cleanBool(process.env.USE_SEMANTIC_RANKER, | const USE_SEMANTIC_RANKER = cleanBool(process.env.USE_SEMANTIC_RANKER, | ||
| - | |||
| const SEMANTIC_USE_CAPTIONS = cleanBool(process.env.SEMANTIC_USE_CAPTIONS, | const SEMANTIC_USE_CAPTIONS = cleanBool(process.env.SEMANTIC_USE_CAPTIONS, | ||
| - | + | const SEMANTIC_USE_ANSWERS | |
| - | const SEMANTIC_USE_ANSWERS = cleanBool(process.env.SEMANTIC_USE_ANSWERS, | + | |
| const LLM_RERANK = cleanBool(process.env.LLM_RERANK, | const LLM_RERANK = cleanBool(process.env.LLM_RERANK, | ||
| - | + | ||
| - | + | ||
| const TOP_K = cleanNum(process.env.TOP_K, | const TOP_K = cleanNum(process.env.TOP_K, | ||
| - | |||
| const TEMPERATURE = cleanNum(process.env.TEMPERATURE, | const TEMPERATURE = cleanNum(process.env.TEMPERATURE, | ||
| - | |||
| const MAX_TOKENS_ANSWER = cleanNum(process.env.MAX_TOKENS_ANSWER, | const MAX_TOKENS_ANSWER = cleanNum(process.env.MAX_TOKENS_ANSWER, | ||
| - | + | ||
| - | + | // Retrieval feature toggles | |
| - | + | ||
| - | %%//%% Retrieval feature toggles | + | |
| const RETRIEVER_MULTIQUERY_N = cleanNum(process.env.RETRIEVER_MULTIQUERY_N, | const RETRIEVER_MULTIQUERY_N = cleanNum(process.env.RETRIEVER_MULTIQUERY_N, | ||
| - | |||
| const RETRIEVER_USE_HYDE = cleanBool(process.env.RETRIEVER_USE_HYDE, | const RETRIEVER_USE_HYDE = cleanBool(process.env.RETRIEVER_USE_HYDE, | ||
| - | + | ||
| - | + | // Clients | |
| - | + | ||
| - | %%//%% Clients | + | |
| const searchClient = new SearchClient(SEARCH_ENDPOINT, | const searchClient = new SearchClient(SEARCH_ENDPOINT, | ||
| - | + | ||
| - | + | // Timing helpers | |
| - | + | ||
| - | %%//%% Timing helpers | + | |
| function nowMs() { return Date.now(); } | function nowMs() { return Date.now(); } | ||
| - | + | ||
| - | + | ||
| async function searchOnceRest({ query, top = TOP_K }) { | async function searchOnceRest({ query, top = TOP_K }) { | ||
| - | + | | |
| - | const endpoint = String(process.env.AZURE_SEARCH_ENDPOINT || '' | + | const index = String(process.env.AZURE_SEARCH_INDEX_NAME || '' |
| - | + | const apiKey = String(process.env.AZURE_SEARCH_QUERY_KEY || process.env.AZURE_SEARCH_API_KEY || '' | |
| - | const index = String(process.env.AZURE_SEARCH_INDEX_NAME || '' | + | const url = `${endpoint}/ |
| - | + | ||
| - | const apiKey = String(process.env.AZURE_SEARCH_QUERY_KEY || process.env.AZURE_SEARCH_API_KEY || '' | + | const body = { search: query ?? '', |
| - | + | const { data } = await axios.post(url, | |
| - | const url = `${endpoint}/ | + | headers: { |
| - | + | ' | |
| - | + | ' | |
| - | + | ' | |
| - | const body = { search: query ?? '', | + | }, |
| - | + | timeout: 10000 | |
| - | const { data } = await axios.post(url, | + | }); |
| - | + | ||
| - | headers: { | + | const arr = Array.isArray(data.value) ? data.value : []; |
| - | + | return arr.map(d => ({ | |
| - | ' | + | id: d.id, |
| - | + | content: String(d[CONTENT_FIELD] ?? d.contents ?? d.content ?? '' | |
| - | ' | + | title: d[TITLE_FIELD] ?? d.title ?? null, |
| - | + | url: d[URL_FIELD] ?? d.url ?? null, | |
| - | ' | + | metadata: d[METADATA_FIELD] ?? d.metadata ?? {}, |
| - | + | parentId: PARENT_ID_FIELD ? (d[PARENT_ID_FIELD] ?? null) : null, | |
| - | }, | + | score: d[' |
| - | + | rerankScore: | |
| - | timeout: 10000 | + | })); |
| - | + | ||
| - | }); | + | |
| - | + | ||
| - | + | ||
| - | + | ||
| - | const arr = Array.isArray(data.value) ? data.value : []; | + | |
| - | + | ||
| - | return arr.map(d => ({ | + | |
| - | + | ||
| - | id: d.id, | + | |
| - | + | ||
| - | content: String(d[CONTENT_FIELD] ?? d.contents ?? d.content ?? '' | + | |
| - | + | ||
| - | title: d[TITLE_FIELD] ?? d.title ?? null, | + | |
| - | + | ||
| - | url: d[URL_FIELD] ?? d.url ?? null, | + | |
| - | + | ||
| - | metadata: d[METADATA_FIELD] ?? d.metadata ?? {}, | + | |
| - | + | ||
| - | parentId: PARENT_ID_FIELD ? (d[PARENT_ID_FIELD] ?? null) : null, | + | |
| - | + | ||
| - | score: d[' | + | |
| - | + | ||
| - | rerankScore: | + | |
| - | + | ||
| - | })); | + | |
| } | } | ||
| - | + | ||
| - | + | // ---------- Reliability: | |
| - | + | ||
| - | %%//%% ---------- Reliability: | + | |
| const sleep = (ms) => new Promise(r => setTimeout(r, | const sleep = (ms) => new Promise(r => setTimeout(r, | ||
| - | |||
| async function withRetry(fn, | async function withRetry(fn, | ||
| - | + | | |
| - | let lastErr; | + | for (let i = 0; i < tries; i++) { |
| - | + | try { return await fn(); } catch (e) { | |
| - | for (let i = 0; i < tries; i++) { | + | const status = e? |
| - | + | const retryable = status === 206 || status === 408 || status === 429 || (status >= 500 && status <= 599); | |
| - | try { return await fn(); } catch (e) { | + | if (!retryable || i === tries - 1) { lastErr = e; break; } |
| - | + | const wait = base * Math.pow(2, i) + Math.floor(Math.random() * 100); | |
| - | const status = e? | + | console.warn(`${desc} failed (status=${status}), |
| - | + | await sleep(wait); | |
| - | const retryable = status === 206 || status === 408 || status === 429 || (status >= 500 && status <= 599); | + | } |
| - | + | } | |
| - | if (!retryable || i === tries - 1) { lastErr = e; break; } | + | throw lastErr; |
| - | + | ||
| - | const wait = base * Math.pow(2, i) + Math.floor(Math.random() * 100); | + | |
| - | + | ||
| - | console.warn(`${desc} failed (status=${status}), | + | |
| - | + | ||
| - | await sleep(wait); | + | |
| } | } | ||
| - | + | ||
| - | } | + | |
| - | + | ||
| - | throw lastErr; | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | + | ||
| const _memo = new Map(); | const _memo = new Map(); | ||
| - | |||
| const _get = (k) => _memo.get(k); | const _get = (k) => _memo.get(k); | ||
| - | |||
| const _set = (k, v) => { if (_memo.size > 500) _memo.clear(); | const _set = (k, v) => { if (_memo.size > 500) _memo.clear(); | ||
| - | + | ||
| - | + | // ---------- Azure OpenAI calls ---------- | |
| - | + | ||
| - | %%//%% ---------- Azure OpenAI calls ---------- | + | |
| async function embed(text) { | async function embed(text) { | ||
| - | + | | |
| - | const url = `${OPENAI_ENDPOINT}/ | + | const { data } = await withRetry( |
| - | + | () => axios.post(url, | |
| - | const { data } = await withRetry( | + | ' |
| - | + | ); | |
| - | () => axios.post(url, | + | return data.data[0].embedding; |
| - | + | ||
| - | ' | + | |
| - | + | ||
| - | ); | + | |
| - | + | ||
| - | return data.data[0].embedding; | + | |
| } | } | ||
| - | |||
| async function embedMemo(text) { | async function embedMemo(text) { | ||
| - | + | | |
| - | const k = `emb: | + | const v = await embed(text); |
| - | + | ||
| - | const v = await embed(text); | + | |
| } | } | ||
| - | + | ||
| - | + | ||
| async function chat(messages, | async function chat(messages, | ||
| - | + | | |
| - | const url = `${OPENAI_ENDPOINT}/ | + | const payload = { messages, temperature: |
| - | + | const { data } = await withRetry( | |
| - | const payload = { messages, temperature: | + | () => axios.post(url, |
| - | + | ' | |
| - | const { data } = await withRetry( | + | ); |
| - | + | return data; | |
| - | () => axios.post(url, | + | |
| - | + | ||
| - | ' | + | |
| - | + | ||
| - | ); | + | |
| - | + | ||
| - | return data; | + | |
| } | } | ||
| - | + | ||
| - | + | // ---------- Query Expansion ---------- | |
| - | + | ||
| - | %%//%% ---------- Query Expansion ---------- | + | |
| async function multiQueryExpand(userQuery, | async function multiQueryExpand(userQuery, | ||
| - | + | | |
| - | const k = `mq: | + | const sys = { role: ' |
| - | + | const usr = { role: ' | |
| - | const sys = { role: ' | + | const res = await chat([sys, usr], { temperature: |
| - | + | let arr = []; | |
| - | const usr = { role: ' | + | try { arr = JSON.parse(res.choices[0].message.content.trim()); |
| - | + | const out = Array.isArray(arr) ? arr.filter(s => typeof s === ' | |
| - | const res = await chat([sys, usr], { temperature: | + | _set(k, out); |
| - | + | return out; | |
| - | let arr = []; | + | |
| - | + | ||
| - | try { arr = JSON.parse(res.choices[0].message.content.trim()); | + | |
| - | + | ||
| - | const out = Array.isArray(arr) ? arr.filter(s => typeof s === ' | + | |
| - | + | ||
| - | _set(k, out); | + | |
| - | + | ||
| - | return out; | + | |
| } | } | ||
| - | + | ||
| - | + | ||
| async function hydeDoc(userQuery) { | async function hydeDoc(userQuery) { | ||
| - | + | | |
| - | const k = `hyde: | + | const sys = { role: ' |
| - | + | const usr = { role: ' | |
| - | const sys = { role: ' | + | const res = await chat([sys, usr], { temperature: |
| - | + | const out = (res.choices[0].message.content || '' | |
| - | const usr = { role: ' | + | _set(k, out); |
| - | + | return out; | |
| - | const res = await chat([sys, usr], { temperature: | + | |
| - | + | ||
| - | const out = (res.choices[0].message.content || '' | + | |
| - | + | ||
| - | _set(k, out); | + | |
| - | + | ||
| - | return out; | + | |
| } | } | ||
| - | + | ||
| - | + | // ---------- Retrieval Strategies ---------- | |
| - | + | ||
| - | %%//%% ---------- Retrieval Strategies ---------- | + | |
| function buildCommonOptions({ filter, select } = {}) { | function buildCommonOptions({ filter, select } = {}) { | ||
| - | + | | |
| - | %%//%% Only select what we know exists from env; no alternates here. | + | const want = select || [ |
| - | + | CONTENT_FIELD, | |
| - | const want = select || [ | + | TITLE_FIELD, |
| - | + | URL_FIELD, | |
| - | CONTENT_FIELD, | + | METADATA_FIELD, |
| - | + | ' | |
| - | TITLE_FIELD, | + | PARENT_ID_FIELD |
| - | + | ].filter(Boolean); | |
| - | URL_FIELD, | + | |
| - | + | const base = { | |
| - | METADATA_FIELD, | + | top: TOP_K, |
| - | + | includeTotalCount: | |
| - | ' | + | // select: want |
| - | + | }; | |
| - | PARENT_ID_FIELD | + | if (filter) base.filter = filter; |
| - | + | ||
| - | ].filter(Boolean); | + | if (USE_SEMANTIC_RANKER) { |
| - | + | base.queryType = ' | |
| - | + | const sc = (SEMANTIC_CONFIGURATION || '' | |
| - | + | if (sc) base.semanticConfiguration = sc; | |
| - | const base = { | + | base.queryLanguage = ' |
| - | + | if (SEMANTIC_USE_CAPTIONS) base.captions = ' | |
| - | top: TOP_K, | + | if (SEMANTIC_USE_ANSWERS) |
| - | + | } | |
| - | includeTotalCount: | + | return base; |
| - | + | ||
| - | %%//%% select: want | + | |
| - | + | ||
| - | }; | + | |
| - | + | ||
| - | if (filter) base.filter = filter; | + | |
| - | + | ||
| - | + | ||
| - | + | ||
| - | if (USE_SEMANTIC_RANKER) { | + | |
| - | + | ||
| - | base.queryType = ' | + | |
| - | + | ||
| - | const sc = (SEMANTIC_CONFIGURATION || '' | + | |
| - | + | ||
| - | if (sc) base.semanticConfiguration = sc; | + | |
| - | + | ||
| - | base.queryLanguage = ' | + | |
| - | + | ||
| - | if (SEMANTIC_USE_CAPTIONS) base.captions = ' | + | |
| - | + | ||
| - | if (SEMANTIC_USE_ANSWERS) base.answers = ' | + | |
| } | } | ||
| - | + | ||
| - | return base; | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | + | ||
| function mapSearchResult(r) { | function mapSearchResult(r) { | ||
| - | + | | |
| - | const d = r.document || r; | + | |
| - | + | // HARDENED: never return empty content if either field is present | |
| - | + | const content = | |
| - | + | d[CONTENT_FIELD] ?? | |
| - | %%//%% HARDENED: never return empty content if either field is present | + | d.contents ?? |
| - | + | d.content ?? | |
| - | const content = | + | ''; |
| - | + | ||
| - | d[CONTENT_FIELD] ?? | + | return { |
| - | + | id: d.id ?? d[' | |
| - | d.contents ?? | + | content, |
| - | + | title: d[TITLE_FIELD] ?? d.title ?? null, | |
| - | d.content ?? | + | url: d[URL_FIELD] ?? d.url ?? null, |
| - | + | metadata: d[METADATA_FIELD] ?? d.metadata ?? {}, | |
| - | ''; | + | parentId: PARENT_ID_FIELD ? (d[PARENT_ID_FIELD] ?? null) : null, |
| - | + | score: r[' | |
| - | + | rerankScore: | |
| - | + | }; | |
| - | return { | + | |
| - | + | ||
| - | id: d.id ?? d[' | + | |
| - | + | ||
| - | content, | + | |
| - | + | ||
| - | title: d[TITLE_FIELD] ?? d.title ?? null, | + | |
| - | + | ||
| - | url: d[URL_FIELD] ?? d.url ?? null, | + | |
| - | + | ||
| - | metadata: d[METADATA_FIELD] ?? d.metadata ?? {}, | + | |
| - | + | ||
| - | parentId: PARENT_ID_FIELD ? (d[PARENT_ID_FIELD] ?? null) : null, | + | |
| - | + | ||
| - | score: r[' | + | |
| - | + | ||
| - | rerankScore: | + | |
| - | + | ||
| - | }; | + | |
| } | } | ||
| - | + | ||
| - | + | // RRF helper | |
| - | + | ||
| - | %%//%% RRF helper | + | |
| function rrfFuse(lists, | function rrfFuse(lists, | ||
| - | + | | |
| - | const scores = new Map(); | + | for (const list of lists) { |
| - | + | list.forEach((item, | |
| - | for (const list of lists) { | + | const prev = scores.get(item.id) || 0; |
| - | + | scores.set(item.id, | |
| - | list.forEach((item, | + | }); |
| - | + | } | |
| - | const prev = scores.get(item.id) || 0; | + | const itemById = new Map(); |
| - | + | for (const list of lists) for (const it of list) if (!itemById.has(it.id)) itemById.set(it.id, | |
| - | scores.set(item.id, | + | return Array.from(scores.entries()).sort((a, |
| - | + | ||
| - | }); | + | |
| } | } | ||
| - | + | ||
| - | const itemById = new Map(); | + | |
| - | + | ||
| - | for (const list of lists) for (const it of list) if (!itemById.has(it.id)) itemById.set(it.id, | + | |
| - | + | ||
| - | return Array.from(scores.entries()).sort((a, | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | + | ||
| async function searchOnce({ query, vector, fields, filter }) { | async function searchOnce({ query, vector, fields, filter }) { | ||
| - | + | | |
| - | const opts = buildCommonOptions({ filter }); | + | |
| - | + | if (vector) { | |
| - | + | const fieldList = Array.isArray(fields) | |
| - | + | ? fields | |
| - | if (vector) { | + | : [ (fields || VECTOR_FIELD) ].map(s => String(s || '' |
| - | + | opts.vectorSearchOptions = { | |
| - | const fieldList = Array.isArray(fields) | + | queries: [{ |
| - | + | kind: ' | |
| - | ? fields | + | vector, |
| - | + | kNearestNeighborsCount: | |
| - | : [ (fields || VECTOR_FIELD) ].map(s => String(s || '' | + | fields: fieldList, |
| - | + | }] | |
| - | opts.vectorSearchOptions = { | + | }; |
| - | + | } else { | |
| - | queries: [{ | + | // BM25 path: explicitly tell Search which fields to match on |
| - | + | opts.searchFields = [CONTENT_FIELD, | |
| - | kind: ' | + | } |
| - | + | ||
| - | vector, | + | const results = []; |
| - | + | const isAsyncIterable = (x) => x && typeof x[Symbol.asyncIterator] === ' | |
| - | kNearestNeighborsCount: | + | |
| - | + | const run = async (o) => { | |
| - | fields: fieldList, | + | const iter = searchClient.search(query || '', |
| - | + | if (isAsyncIterable(iter)) { | |
| - | }] | + | for await (const r of iter) results.push(mapSearchResult(r)); |
| - | + | } else if (iter? | |
| - | }; | + | for await (const r of iter.results) results.push(mapSearchResult(r)); |
| - | + | } else if (Array.isArray(iter? | |
| - | } else { | + | for (const r of iter.results) results.push(mapSearchResult(r)); |
| - | + | } | |
| - | %%//%% BM25 path: explicitly tell Search which fields to match on | + | }; |
| - | + | ||
| - | opts.searchFields = [CONTENT_FIELD, | + | try { |
| + | await run(opts); | ||
| + | } catch (e) { | ||
| + | const msg = String(e? | ||
| + | const status = e? | ||
| + | |||
| + | const selectProblem = | ||
| + | (status === 400 && /Parameter name: | ||
| + | /Could not find a property named .* on type ' | ||
| + | |||
| + | const overload = | ||
| + | / | ||
| + | status === 206 || status === 503; | ||
| + | |||
| + | if (selectProblem) { | ||
| + | console.warn(' | ||
| + | const fallback = { ...opts }; | ||
| + | delete fallback.select; | ||
| + | await run(fallback); | ||
| + | } else if (USE_SEMANTIC_RANKER && overload) { | ||
| + | console.warn(' | ||
| + | const fallback = { ...opts }; | ||
| + | delete fallback.queryType; | ||
| + | delete fallback.semanticConfiguration; | ||
| + | delete fallback.queryLanguage; | ||
| + | delete fallback.captions; | ||
| + | delete fallback.answers; | ||
| + | await run(fallback); | ||
| + | } else { | ||
| + | throw e; | ||
| + | } | ||
| + | } | ||
| + | |||
| + | // 👉 If BM25 (non-vector) still returned 0, fallback to REST (we know it works) | ||
| + | if (!vector && results.length === 0) { | ||
| + | try { | ||
| + | const restHits = await searchOnceRest({ query, top: TOP_K }); | ||
| + | if (restHits.length) return restHits; | ||
| + | } catch (e) { | ||
| + | console.warn(' | ||
| + | } | ||
| + | } | ||
| + | |||
| + | return results; | ||
| } | } | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | const results = []; | + | |
| - | + | ||
| - | const isAsyncIterable = (x) => x && typeof x[Symbol.asyncIterator] === ' | + | |
| - | + | ||
| - | + | ||
| - | + | ||
| - | const run = async (o) => { | + | |
| - | + | ||
| - | const iter = searchClient.search(query || '', | + | |
| - | + | ||
| - | if (isAsyncIterable(iter)) { | + | |
| - | + | ||
| - | for await (const r of iter) results.push(mapSearchResult(r)); | + | |
| - | + | ||
| - | } else if (iter? | + | |
| - | + | ||
| - | for await (const r of iter.results) results.push(mapSearchResult(r)); | + | |
| - | + | ||
| - | } else if (Array.isArray(iter? | + | |
| - | + | ||
| - | for (const r of iter.results) results.push(mapSearchResult(r)); | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | }; | + | |
| - | + | ||
| - | + | ||
| - | + | ||
| - | try { | + | |
| - | + | ||
| - | await run(opts); | + | |
| - | + | ||
| - | } catch (e) { | + | |
| - | + | ||
| - | const msg = String(e? | + | |
| - | + | ||
| - | const status = e? | + | |
| - | + | ||
| - | + | ||
| - | + | ||
| - | const selectProblem = | + | |
| - | + | ||
| - | (status === 400 && /Parameter name: | + | |
| - | + | ||
| - | /Could not find a property named .* on type ' | + | |
| - | + | ||
| - | + | ||
| - | + | ||
| - | const overload = | + | |
| - | + | ||
| - | / | + | |
| - | + | ||
| - | status === 206 || status === 503; | + | |
| - | + | ||
| - | + | ||
| - | + | ||
| - | if (selectProblem) { | + | |
| - | + | ||
| - | console.warn(' | + | |
| - | + | ||
| - | const fallback = { ...opts }; | + | |
| - | + | ||
| - | delete fallback.select; | + | |
| - | + | ||
| - | await run(fallback); | + | |
| - | + | ||
| - | } else if (USE_SEMANTIC_RANKER && overload) { | + | |
| - | + | ||
| - | console.warn(' | + | |
| - | + | ||
| - | const fallback = { ...opts }; | + | |
| - | + | ||
| - | delete fallback.queryType; | + | |
| - | + | ||
| - | delete fallback.semanticConfiguration; | + | |
| - | + | ||
| - | delete fallback.queryLanguage; | + | |
| - | + | ||
| - | delete fallback.captions; | + | |
| - | + | ||
| - | delete fallback.answers; | + | |
| - | + | ||
| - | await run(fallback); | + | |
| - | + | ||
| - | } else { | + | |
| - | + | ||
| - | throw e; | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | + | ||
| - | + | ||
| - | %%//%% 👉 If BM25 (non-vector) still returned 0, fallback to REST (we know it works) | + | |
| - | + | ||
| - | if (!vector && results.length === 0) { | + | |
| - | + | ||
| - | try { | + | |
| - | + | ||
| - | const restHits = await searchOnceRest({ query, top: TOP_K }); | + | |
| - | + | ||
| - | if (restHits.length) return restHits; | + | |
| - | + | ||
| - | } catch (e) { | + | |
| - | + | ||
| - | console.warn(' | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | + | ||
| - | + | ||
| - | return results; | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | + | ||
| function collapseByParent(items, | function collapseByParent(items, | ||
| - | + | | |
| - | if (!PARENT_ID_FIELD) return items; | + | const groups = new Map(); |
| - | + | for (const it of items) { | |
| - | const groups = new Map(); | + | const key = it.parentId || it.id; |
| - | + | if (!groups.has(key)) groups.set(key, | |
| - | for (const it of items) { | + | groups.get(key).push(it); |
| - | + | } | |
| - | const key = it.parentId || it.id; | + | const collapsed = []; |
| - | + | for (const arr of groups.values()) { | |
| - | if (!groups.has(key)) groups.set(key, | + | arr.sort((a, |
| - | + | collapsed.push(...arr.slice(0, | |
| - | groups.get(key).push(it); | + | } |
| + | return collapsed.slice(0, | ||
| } | } | ||
| - | + | ||
| - | const collapsed = []; | + | |
| - | + | ||
| - | for (const arr of groups.values()) { | + | |
| - | + | ||
| - | arr.sort((a, | + | |
| - | + | ||
| - | collapsed.push(...arr.slice(0, | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | return collapsed.slice(0, | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | + | ||
| async function retrieve(userQuery, | async function retrieve(userQuery, | ||
| - | + | | |
| - | const t0 = nowMs(); | + | const lists = []; |
| - | + | ||
| - | const lists = []; | + | // Core keyword/ |
| - | + | lists.push(await searchOnce({ query: userQuery, filter })); | |
| - | + | ||
| - | + | // Vector on the raw query (memoized) | |
| - | %%//%% Core keyword/ | + | try { |
| - | + | const qvec = await embedMemo(userQuery); | |
| - | lists.push(await searchOnce({ query: userQuery, filter })); | + | lists.push(await searchOnce({ query: '', |
| - | + | } catch (e) { | |
| - | + | console.warn(' | |
| - | + | } | |
| - | %%//%% Vector on the raw query (memoized) | + | |
| - | + | // Multi-query expansion (togglable) | |
| - | try { | + | let expansions = []; |
| - | + | if (RETRIEVER_MULTIQUERY_N > 0) { | |
| - | const qvec = await embedMemo(userQuery); | + | try { |
| - | + | expansions = await multiQueryExpand(userQuery, | |
| - | lists.push(await searchOnce({ query: '', | + | for (const q of expansions) { |
| - | + | lists.push(await searchOnce({ query: q, filter })); | |
| - | } catch (e) { | + | } |
| - | + | } catch (e) { | |
| - | console.warn(' | + | console.warn(' |
| + | } | ||
| + | } | ||
| + | |||
| + | // HyDE (togglable) | ||
| + | if (RETRIEVER_USE_HYDE) { | ||
| + | try { | ||
| + | const pseudo = await hydeDoc(userQuery); | ||
| + | const pvec = await embedMemo(pseudo); | ||
| + | lists.push(await searchOnce({ query: '', | ||
| + | } catch (e) { | ||
| + | console.warn(' | ||
| + | } | ||
| + | } | ||
| + | |||
| + | // Fuse & collapse | ||
| + | const fused = collapseByParent(rrfFuse(lists)); | ||
| + | const latencyMs = nowMs() - t0; | ||
| + | return { items: fused.slice(0, | ||
| } | } | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | %%//%% Multi-query expansion (togglable) | + | |
| - | + | ||
| - | let expansions = []; | + | |
| - | + | ||
| - | if (RETRIEVER_MULTIQUERY_N > 0) { | + | |
| - | + | ||
| - | try { | + | |
| - | + | ||
| - | expansions = await multiQueryExpand(userQuery, | + | |
| - | + | ||
| - | for (const q of expansions) { | + | |
| - | + | ||
| - | lists.push(await searchOnce({ query: q, filter })); | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | } catch (e) { | + | |
| - | + | ||
| - | console.warn(' | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | + | ||
| - | + | ||
| - | %%//%% HyDE (togglable) | + | |
| - | + | ||
| - | if (RETRIEVER_USE_HYDE) { | + | |
| - | + | ||
| - | try { | + | |
| - | + | ||
| - | const pseudo = await hydeDoc(userQuery); | + | |
| - | + | ||
| - | const pvec = await embedMemo(pseudo); | + | |
| - | + | ||
| - | lists.push(await searchOnce({ query: '', | + | |
| - | + | ||
| - | } catch (e) { | + | |
| - | + | ||
| - | console.warn(' | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | + | ||
| - | + | ||
| - | %%//%% Fuse & collapse | + | |
| - | + | ||
| - | const fused = collapseByParent(rrfFuse(lists)); | + | |
| - | + | ||
| - | const latencyMs = nowMs() - t0; | + | |
| - | + | ||
| - | return { items: fused.slice(0, | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | + | ||
| async function rerankWithLLM(query, | async function rerankWithLLM(query, | ||
| - | + | | |
| - | if (!LLM_RERANK || !items.length) return items; | + | const sys = { role: ' |
| - | + | const passages = items.map(p => ({ id: p.id, title: p.title || '', | |
| - | const sys = { role: ' | + | const usr = { role: ' |
| - | + | const res = await chat([sys, usr], { temperature: | |
| - | const passages = items.map(p => ({ id: p.id, title: p.title || '', | + | let scores = []; |
| - | + | try { scores = JSON.parse(res.choices[0].message.content); | |
| - | const usr = { role: ' | + | const byId = new Map(scores.map(s => [String(s.id), |
| - | + | for (const it of items) it.rerankScore = byId.get(String(it.id)) ?? null; | |
| - | const res = await chat([sys, usr], { temperature: | + | items.sort((a, |
| - | + | return items; | |
| - | let scores = []; | + | |
| - | + | ||
| - | try { scores = JSON.parse(res.choices[0].message.content); | + | |
| - | + | ||
| - | const byId = new Map(scores.map(s => [String(s.id), | + | |
| - | + | ||
| - | for (const it of items) it.rerankScore = byId.get(String(it.id)) ?? null; | + | |
| - | + | ||
| - | items.sort((a, | + | |
| - | + | ||
| - | return items; | + | |
| } | } | ||
| - | + | ||
| - | + | ||
| function toCitableChunks(items) { | function toCitableChunks(items) { | ||
| - | + | | |
| - | return items.map((it, | + | key: `[${idx + 1}]`, |
| - | + | id: it.id, | |
| - | key: `[${idx + 1}]`, | + | title: it.title || `Doc ${idx + 1}`, |
| - | + | url: it.url || null, | |
| - | id: it.id, | + | content: (it.content || '' |
| - | + | })); | |
| - | title: it.title || `Doc ${idx + 1}`, | + | |
| - | + | ||
| - | url: it.url || null, | + | |
| - | + | ||
| - | content: (it.content || '' | + | |
| - | + | ||
| - | })); | + | |
| } | } | ||
| - | + | ||
| - | + | ||
| async function synthesizeAnswer(userQuery, | async function synthesizeAnswer(userQuery, | ||
| - | + | | |
| - | const sys = { role: ' | + | `You answer using only the provided sources. Cite like [1], [2] inline. |
| - | + | ||
| - | `You answer using only the provided sources. Cite like [1], [2] inline. | + | |
| If unsure or missing info, say you don't know. | If unsure or missing info, say you don't know. | ||
| - | |||
| Return a JSON object: { " | Return a JSON object: { " | ||
| - | + | | |
| - | const usr = { role: ' | + | const data = await chat([sys, usr], { temperature: |
| - | + | const raw = data.choices[0].message.content; | |
| - | const data = await chat([sys, usr], { temperature: | + | let parsed; |
| - | + | try { parsed = JSON.parse(raw); | |
| - | const raw = data.choices[0].message.content; | + | parsed = { answer: raw, citations: chunks.map(c => ({ key: c.key, id: c.id, title: c.title, url: c.url })) }; |
| - | + | } | |
| - | let parsed; | + | parsed.citations = parsed.citations || chunks.map(c => ({ key: c.key, id: c.id, title: c.title, url: c.url })); |
| - | + | return { ...parsed, usage: data.usage || null }; | |
| - | try { parsed = JSON.parse(raw); | + | |
| - | + | ||
| - | parsed = { answer: raw, citations: chunks.map(c => ({ key: c.key, id: c.id, title: c.title, url: c.url })) }; | + | |
| } | } | ||
| - | + | ||
| - | parsed.citations = parsed.citations || chunks.map(c => ({ key: c.key, id: c.id, title: c.title, url: c.url })); | + | |
| - | + | ||
| - | return { ...parsed, usage: data.usage || null }; | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | + | ||
| async function answerQuestion(userQuery, | async function answerQuestion(userQuery, | ||
| - | + | | |
| - | const retrieval = await retrieve(userQuery, | + | let items = retrieval.items; |
| - | + | items = await rerankWithLLM(userQuery, | |
| - | let items = retrieval.items; | + | |
| - | + | const topChunks = toCitableChunks(items.slice(0, | |
| - | items = await rerankWithLLM(userQuery, | + | const t0 = nowMs(); |
| - | + | const synthesis = await synthesizeAnswer(userQuery, | |
| - | + | const genLatencyMs = nowMs() - t0; | |
| - | + | ||
| - | const topChunks = toCitableChunks(items.slice(0, | + | return { |
| - | + | answer: synthesis.answer, | |
| - | const t0 = nowMs(); | + | citations: synthesis.citations, |
| - | + | retrieved: items, | |
| - | const synthesis = await synthesizeAnswer(userQuery, | + | metrics: { |
| - | + | retrievalLatencyMs: | |
| - | const genLatencyMs = nowMs() - t0; | + | generationLatencyMs: |
| - | + | expansions: retrieval.expansions, | |
| - | + | usage: synthesis.usage || null | |
| - | + | } | |
| - | return { | + | }; |
| - | + | ||
| - | answer: synthesis.answer, | + | |
| - | + | ||
| - | citations: synthesis.citations, | + | |
| - | + | ||
| - | retrieved: items, | + | |
| - | + | ||
| - | metrics: { | + | |
| - | + | ||
| - | retrievalLatencyMs: | + | |
| - | + | ||
| - | generationLatencyMs: | + | |
| - | + | ||
| - | expansions: retrieval.expansions, | + | |
| - | + | ||
| - | usage: synthesis.usage || null | + | |
| } | } | ||
| - | + | ||
| - | }; | + | |
| - | + | ||
| - | } | + | |
| - | + | ||
| - | + | ||
| module.exports = { | module.exports = { | ||
| - | + | | |
| - | answerQuestion, | + | _internals: { retrieve, rerankWithLLM, |
| - | + | ||
| - | _internals: { retrieve, rerankWithLLM, | + | |
| }; | }; | ||
| </ | </ | ||
| Line 2588: | Line 1460: | ||
| < | < | ||
| { | { | ||
| - | + | | |
| - | " | + | " |
| - | + | " | |
| - | " | + | " |
| - | + | " | |
| - | " | + | " |
| - | + | " | |
| - | " | + | " |
| - | + | " | |
| - | " | + | " |
| - | + | " | |
| - | " | + | }, |
| - | + | " | |
| - | " | + | " |
| - | + | " | |
| - | " | + | }, |
| - | + | " | |
| - | " | + | " |
| - | + | " | |
| - | " | + | " |
| - | + | " | |
| - | " | + | " |
| - | + | " | |
| - | }, | + | }, |
| - | + | " | |
| - | " | + | " |
| - | + | " | |
| - | " | + | " |
| - | + | " | |
| - | " | + | " |
| - | + | " | |
| - | }, | + | " |
| - | + | }, | |
| - | " | + | " |
| - | + | ||
| - | " | + | |
| - | + | ||
| - | " | + | |
| - | + | ||
| - | " | + | |
| - | + | ||
| - | " | + | |
| - | + | ||
| - | " | + | |
| - | + | ||
| - | " | + | |
| - | + | ||
| - | }, | + | |
| - | + | ||
| - | " | + | |
| - | + | ||
| - | " | + | |
| - | + | ||
| - | " | + | |
| - | + | ||
| - | " | + | |
| - | + | ||
| - | " | + | |
| - | + | ||
| - | " | + | |
| - | + | ||
| - | " | + | |
| - | + | ||
| - | " | + | |
| - | + | ||
| - | }, | + | |
| - | + | ||
| - | " | + | |
| } | } | ||
| </ | </ | ||