@eucoder/rag 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +384 -0
- package/dist/ab-testing.d.ts +52 -0
- package/dist/ab-testing.d.ts.map +1 -0
- package/dist/ab-testing.js +144 -0
- package/dist/ab-testing.js.map +1 -0
- package/dist/ab-testing.test.d.ts +2 -0
- package/dist/ab-testing.test.d.ts.map +1 -0
- package/dist/ab-testing.test.js +147 -0
- package/dist/ab-testing.test.js.map +1 -0
- package/dist/agentic-rag.d.ts +23 -0
- package/dist/agentic-rag.d.ts.map +1 -0
- package/dist/agentic-rag.js +170 -0
- package/dist/agentic-rag.js.map +1 -0
- package/dist/agentic-rag.test.d.ts +2 -0
- package/dist/agentic-rag.test.d.ts.map +1 -0
- package/dist/agentic-rag.test.js +174 -0
- package/dist/agentic-rag.test.js.map +1 -0
- package/dist/corrective-rag.d.ts +16 -0
- package/dist/corrective-rag.d.ts.map +1 -0
- package/dist/corrective-rag.js +85 -0
- package/dist/corrective-rag.js.map +1 -0
- package/dist/corrective-rag.test.d.ts +2 -0
- package/dist/corrective-rag.test.d.ts.map +1 -0
- package/dist/corrective-rag.test.js +140 -0
- package/dist/corrective-rag.test.js.map +1 -0
- package/dist/feedback.d.ts +77 -0
- package/dist/feedback.d.ts.map +1 -0
- package/dist/feedback.js +44 -0
- package/dist/feedback.js.map +1 -0
- package/dist/feedback.test.d.ts +2 -0
- package/dist/feedback.test.d.ts.map +1 -0
- package/dist/feedback.test.js +202 -0
- package/dist/feedback.test.js.map +1 -0
- package/dist/hybrid-search.d.ts +14 -0
- package/dist/hybrid-search.d.ts.map +1 -0
- package/dist/hybrid-search.js +70 -0
- package/dist/hybrid-search.js.map +1 -0
- package/dist/hybrid-search.test.d.ts +2 -0
- package/dist/hybrid-search.test.d.ts.map +1 -0
- package/dist/hybrid-search.test.js +93 -0
- package/dist/hybrid-search.test.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/knowledge-graph.d.ts +24 -0
- package/dist/knowledge-graph.d.ts.map +1 -0
- package/dist/knowledge-graph.js +131 -0
- package/dist/knowledge-graph.js.map +1 -0
- package/dist/knowledge-graph.test.d.ts +2 -0
- package/dist/knowledge-graph.test.d.ts.map +1 -0
- package/dist/knowledge-graph.test.js +140 -0
- package/dist/knowledge-graph.test.js.map +1 -0
- package/dist/llm-grader.d.ts +19 -0
- package/dist/llm-grader.d.ts.map +1 -0
- package/dist/llm-grader.js +63 -0
- package/dist/llm-grader.js.map +1 -0
- package/dist/metrics.d.ts +26 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +100 -0
- package/dist/metrics.js.map +1 -0
- package/dist/optimizer.d.ts +52 -0
- package/dist/optimizer.d.ts.map +1 -0
- package/dist/optimizer.js +228 -0
- package/dist/optimizer.js.map +1 -0
- package/dist/optimizer.test.d.ts +2 -0
- package/dist/optimizer.test.d.ts.map +1 -0
- package/dist/optimizer.test.js +201 -0
- package/dist/optimizer.test.js.map +1 -0
- package/dist/self-improving.d.ts +85 -0
- package/dist/self-improving.d.ts.map +1 -0
- package/dist/self-improving.js +163 -0
- package/dist/self-improving.js.map +1 -0
- package/dist/self-improving.test.d.ts +2 -0
- package/dist/self-improving.test.d.ts.map +1 -0
- package/dist/self-improving.test.js +234 -0
- package/dist/self-improving.test.js.map +1 -0
- package/dist/types.d.ts +117 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +42 -0
- package/src/ab-testing.test.ts +239 -0
- package/src/ab-testing.ts +214 -0
- package/src/agentic-rag.test.ts +201 -0
- package/src/agentic-rag.ts +220 -0
- package/src/corrective-rag.test.ts +166 -0
- package/src/corrective-rag.ts +115 -0
- package/src/feedback.test.ts +227 -0
- package/src/feedback.ts +118 -0
- package/src/hybrid-search.test.ts +107 -0
- package/src/hybrid-search.ts +86 -0
- package/src/index.ts +57 -0
- package/src/knowledge-graph.test.ts +170 -0
- package/src/knowledge-graph.ts +182 -0
- package/src/llm-grader.ts +69 -0
- package/src/metrics.ts +121 -0
- package/src/optimizer.test.ts +232 -0
- package/src/optimizer.ts +307 -0
- package/src/self-improving.test.ts +341 -0
- package/src/self-improving.ts +239 -0
- package/src/types.ts +139 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import type { SearchHit } from "@eucode/indexer";
|
|
2
|
+
import type {
|
|
3
|
+
AgenticRagOptions,
|
|
4
|
+
AgenticRagResult,
|
|
5
|
+
ReasoningStep,
|
|
6
|
+
RagCitation,
|
|
7
|
+
} from "./types.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Agentic RAG con Reasoning e Self-Reflection
|
|
11
|
+
*
|
|
12
|
+
* Implementa un ciclo di ragionamento:
|
|
13
|
+
* 1. Thought: Analizza la query e pianifica l'approccio
|
|
14
|
+
* 2. Action: Esegue retrieval e grading
|
|
15
|
+
* 3. Observation: Valuta i risultati
|
|
16
|
+
* 4. Self-Reflection: Valuta la qualità della risposta
|
|
17
|
+
* 5. Itera se necessario
|
|
18
|
+
*/
|
|
19
|
+
export class AgenticRag {
|
|
20
|
+
private options: Required<AgenticRagOptions>;
|
|
21
|
+
|
|
22
|
+
constructor(options: AgenticRagOptions) {
|
|
23
|
+
this.options = {
|
|
24
|
+
maxIterations: options.maxIterations ?? 3,
|
|
25
|
+
enableSelfReflection: options.enableSelfReflection ?? true,
|
|
26
|
+
enableChainOfThought: options.enableChainOfThought ?? true,
|
|
27
|
+
...options,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async search(query: string): Promise<AgenticRagResult> {
|
|
32
|
+
const reasoningSteps: ReasoningStep[] = [];
|
|
33
|
+
let currentQuery = query;
|
|
34
|
+
let bestHits: SearchHit[] = [];
|
|
35
|
+
let bestScore = 0;
|
|
36
|
+
let iterations = 0;
|
|
37
|
+
|
|
38
|
+
// Ciclo di ragionamento
|
|
39
|
+
for (let i = 0; i < this.options.maxIterations; i++) {
|
|
40
|
+
iterations++;
|
|
41
|
+
|
|
42
|
+
// Step 1: Thought
|
|
43
|
+
const thought = this.generateThought(currentQuery, i);
|
|
44
|
+
|
|
45
|
+
// Step 2: Action - Retrieval
|
|
46
|
+
const hits = await this.options.retriever.search(currentQuery, 10);
|
|
47
|
+
|
|
48
|
+
// Step 3: Action - Grading
|
|
49
|
+
const gradedHits = await this.gradeHits(currentQuery, hits);
|
|
50
|
+
const avgScore = gradedHits.length > 0
|
|
51
|
+
? gradedHits.reduce((sum, h) => sum + h.score, 0) / gradedHits.length
|
|
52
|
+
: 0;
|
|
53
|
+
|
|
54
|
+
// Step 4: Observation
|
|
55
|
+
const observation = this.generateObservation(gradedHits, avgScore);
|
|
56
|
+
|
|
57
|
+
reasoningSteps.push({
|
|
58
|
+
thought,
|
|
59
|
+
action: `Retrieved ${hits.length} hits, graded ${gradedHits.length} as relevant`,
|
|
60
|
+
observation,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Aggiorna best hits se migliori
|
|
64
|
+
if (avgScore > bestScore) {
|
|
65
|
+
bestScore = avgScore;
|
|
66
|
+
bestHits = gradedHits;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Step 5: Self-Reflection (se abilitato)
|
|
70
|
+
if (this.options.enableSelfReflection && i < this.options.maxIterations - 1) {
|
|
71
|
+
const reflection = this.selfReflect(currentQuery, gradedHits, avgScore);
|
|
72
|
+
|
|
73
|
+
if (reflection.shouldRewrite) {
|
|
74
|
+
// Riscrivi la query
|
|
75
|
+
currentQuery = await this.options.rewriter.rewrite(
|
|
76
|
+
currentQuery,
|
|
77
|
+
gradedHits.map((h) => h.text).join("\n")
|
|
78
|
+
);
|
|
79
|
+
continue;
|
|
80
|
+
} else if (reflection.isSatisfied) {
|
|
81
|
+
break; // Risultati soddisfacenti
|
|
82
|
+
}
|
|
83
|
+
} else if (avgScore > 0.7) {
|
|
84
|
+
break; // Score足够高, termina
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Genera risposta finale
|
|
89
|
+
const answer = this.buildAnswer(query, bestHits, reasoningSteps);
|
|
90
|
+
const citations: RagCitation[] = bestHits.map((hit) => ({
|
|
91
|
+
path: hit.path,
|
|
92
|
+
startLine: hit.startLine,
|
|
93
|
+
endLine: hit.endLine,
|
|
94
|
+
text: hit.text,
|
|
95
|
+
}));
|
|
96
|
+
|
|
97
|
+
// Self-reflection finale
|
|
98
|
+
const selfReflection = this.options.enableSelfReflection
|
|
99
|
+
? this.finalReflection(query, bestHits, bestScore, iterations)
|
|
100
|
+
: undefined;
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
answer,
|
|
104
|
+
citations,
|
|
105
|
+
reasoningSteps,
|
|
106
|
+
selfReflection,
|
|
107
|
+
iterations,
|
|
108
|
+
confidence: bestScore,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private generateThought(query: string, iteration: number): string {
|
|
113
|
+
if (iteration === 0) {
|
|
114
|
+
return `Analizzo la query: "${query}". Cerco documenti rilevanti e valuto la loro pertinenza.`;
|
|
115
|
+
} else {
|
|
116
|
+
return `Iterazione ${iteration + 1}. I risultati precedenti non erano sufficientemente pertinenti. Riscrivo la query per migliorare i risultati.`;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private async gradeHits(query: string, hits: SearchHit[]): Promise<SearchHit[]> {
|
|
121
|
+
const graded = await Promise.all(
|
|
122
|
+
hits.map(async (hit) => {
|
|
123
|
+
const result = await this.options.grader.grade(query, hit);
|
|
124
|
+
return { ...hit, score: result.score };
|
|
125
|
+
})
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
return graded
|
|
129
|
+
.filter((h) => h.score > 0.3)
|
|
130
|
+
.sort((a, b) => b.score - a.score);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private generateObservation(hits: SearchHit[], avgScore: number): string {
|
|
134
|
+
if (hits.length === 0) {
|
|
135
|
+
return "Nessun documento rilevante trovato. Score medio: 0";
|
|
136
|
+
}
|
|
137
|
+
return `Trovati ${hits.length} documenti rilevanti. Score medio: ${avgScore.toFixed(2)}. Miglior documento: ${hits[0]?.path ?? "N/A"}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private selfReflect(
|
|
141
|
+
_query: string,
|
|
142
|
+
hits: SearchHit[],
|
|
143
|
+
avgScore: number
|
|
144
|
+
): { shouldRewrite: boolean; isSatisfied: boolean } {
|
|
145
|
+
// Se score è alto, siamo soddisfatti
|
|
146
|
+
if (avgScore > 0.7 && hits.length >= 3) {
|
|
147
|
+
return { shouldRewrite: false, isSatisfied: true };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Se score è molto basso, riscrivi
|
|
151
|
+
if (avgScore < 0.4 || hits.length < 2) {
|
|
152
|
+
return { shouldRewrite: true, isSatisfied: false };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Score medio, continua ma non riscrivere
|
|
156
|
+
return { shouldRewrite: false, isSatisfied: false };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private buildAnswer(
|
|
160
|
+
_query: string,
|
|
161
|
+
hits: SearchHit[],
|
|
162
|
+
reasoningSteps: ReasoningStep[]
|
|
163
|
+
): string {
|
|
164
|
+
const parts: string[] = [];
|
|
165
|
+
|
|
166
|
+
// Chain of Thought (se abilitato)
|
|
167
|
+
if (this.options.enableChainOfThought && reasoningSteps.length > 0) {
|
|
168
|
+
parts.push(`## Processo di Ragionamento\n`);
|
|
169
|
+
reasoningSteps.forEach((step, i) => {
|
|
170
|
+
parts.push(`### Step ${i + 1}`);
|
|
171
|
+
parts.push(`**Thought:** ${step.thought}`);
|
|
172
|
+
parts.push(`**Action:** ${step.action}`);
|
|
173
|
+
parts.push(`**Observation:** ${step.observation}\n`);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Risposta finale
|
|
178
|
+
parts.push(`## Risposta\n`);
|
|
179
|
+
|
|
180
|
+
if (hits.length === 0) {
|
|
181
|
+
parts.push(`Non sono stati trovati documenti rilevanti per la query fornita.`);
|
|
182
|
+
} else {
|
|
183
|
+
parts.push(`Basato su ${hits.length} documenti pertinenti:\n`);
|
|
184
|
+
|
|
185
|
+
hits.slice(0, 5).forEach((hit, i) => {
|
|
186
|
+
parts.push(`### Fonte ${i + 1}: ${hit.path}`);
|
|
187
|
+
parts.push(`Righe ${hit.startLine}-${hit.endLine} (score: ${hit.score.toFixed(2)})\n`);
|
|
188
|
+
parts.push(`\`\`\`\n${hit.text}\n\`\`\`\n`);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return parts.join("\n");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private finalReflection(
|
|
196
|
+
query: string,
|
|
197
|
+
hits: SearchHit[],
|
|
198
|
+
avgScore: number,
|
|
199
|
+
iterations: number
|
|
200
|
+
): string {
|
|
201
|
+
const parts: string[] = [];
|
|
202
|
+
|
|
203
|
+
parts.push(`**Valutazione Finale:**`);
|
|
204
|
+
parts.push(`- Query: "${query}"`);
|
|
205
|
+
parts.push(`- Iterazioni: ${iterations}`);
|
|
206
|
+
parts.push(`- Documenti trovati: ${hits.length}`);
|
|
207
|
+
parts.push(`- Score medio: ${avgScore.toFixed(2)}`);
|
|
208
|
+
parts.push(`- Confidenza: ${avgScore > 0.7 ? "Alta" : avgScore > 0.4 ? "Media" : "Bassa"}`);
|
|
209
|
+
|
|
210
|
+
if (avgScore > 0.7) {
|
|
211
|
+
parts.push(`\n✅ I risultati sono altamente pertinenti alla query.`);
|
|
212
|
+
} else if (avgScore > 0.4) {
|
|
213
|
+
parts.push(`\n⚠️ I risultati sono parzialmente pertinenti. Potrebbe essere necessario raffinare la query.`);
|
|
214
|
+
} else {
|
|
215
|
+
parts.push(`\n❌ I risultati non sono sufficientemente pertinenti. Considera di riformulare la query.`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return parts.join("\n");
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { CorrectiveRag } from "./corrective-rag.js";
|
|
3
|
+
import type { SearchHit } from "@eucode/indexer";
|
|
4
|
+
import type { Retriever, Grader, QueryRewriter } from "./types.js";
|
|
5
|
+
|
|
6
|
+
class MockRetriever implements Retriever {
|
|
7
|
+
constructor(private hits: SearchHit[]) {}
|
|
8
|
+
|
|
9
|
+
async search(_query: string, _topK: number): Promise<SearchHit[]> {
|
|
10
|
+
return this.hits;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class MockGrader implements Grader {
|
|
15
|
+
constructor(private relevantIds: Set<string>) {}
|
|
16
|
+
|
|
17
|
+
async grade(_query: string, hit: SearchHit) {
|
|
18
|
+
const relevant = this.relevantIds.has(hit.id);
|
|
19
|
+
return {
|
|
20
|
+
relevant,
|
|
21
|
+
score: relevant ? 0.9 : 0.1,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class MockRewriter implements QueryRewriter {
|
|
27
|
+
async rewrite(query: string, _context: string): Promise<string> {
|
|
28
|
+
return `${query} (rewritten)`;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function createHit(id: string, path: string, text: string): SearchHit {
|
|
33
|
+
return {
|
|
34
|
+
id,
|
|
35
|
+
path,
|
|
36
|
+
startLine: 1,
|
|
37
|
+
endLine: 10,
|
|
38
|
+
text,
|
|
39
|
+
score: 0.5,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe("CorrectiveRag", () => {
|
|
44
|
+
it("returns relevant hits without rewriting when enough good hits", async () => {
|
|
45
|
+
const hits = [
|
|
46
|
+
createHit("1", "src/auth.ts", "function login() { ... }"),
|
|
47
|
+
createHit("2", "src/auth.ts", "function logout() { ... }"),
|
|
48
|
+
createHit("3", "src/billing.ts", "function charge() { ... }"),
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const rag = new CorrectiveRag({
|
|
52
|
+
retriever: new MockRetriever(hits),
|
|
53
|
+
grader: new MockGrader(new Set(["1", "2"])),
|
|
54
|
+
rewriter: new MockRewriter(),
|
|
55
|
+
minGoodHits: 2,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const result = await rag.answer("authentication");
|
|
59
|
+
|
|
60
|
+
expect(result.citations).toHaveLength(2);
|
|
61
|
+
expect(result.rewrites).toBe(0);
|
|
62
|
+
expect(result.steps).toContain("Initial query: \"authentication\"");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("rewrites query when not enough relevant hits", async () => {
|
|
66
|
+
const hits = [
|
|
67
|
+
createHit("1", "src/auth.ts", "function login() { ... }"),
|
|
68
|
+
createHit("2", "src/billing.ts", "function charge() { ... }"),
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
const rag = new CorrectiveRag({
|
|
72
|
+
retriever: new MockRetriever(hits),
|
|
73
|
+
grader: new MockGrader(new Set(["1"])),
|
|
74
|
+
rewriter: new MockRewriter(),
|
|
75
|
+
minGoodHits: 2,
|
|
76
|
+
maxRewrites: 1,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const result = await rag.answer("authentication");
|
|
80
|
+
|
|
81
|
+
expect(result.rewrites).toBe(1);
|
|
82
|
+
expect(result.steps.some((s) => s.includes("Rewriting query"))).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("respects maxRewrites limit", async () => {
|
|
86
|
+
const hits = [
|
|
87
|
+
createHit("1", "src/billing.ts", "function charge() { ... }"),
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
const rag = new CorrectiveRag({
|
|
91
|
+
retriever: new MockRetriever(hits),
|
|
92
|
+
grader: new MockGrader(new Set()),
|
|
93
|
+
rewriter: new MockRewriter(),
|
|
94
|
+
minGoodHits: 2,
|
|
95
|
+
maxRewrites: 2,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const result = await rag.answer("authentication");
|
|
99
|
+
|
|
100
|
+
expect(result.rewrites).toBe(2);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("formats answer with citations", async () => {
|
|
104
|
+
const hits = [
|
|
105
|
+
createHit("1", "src/auth.ts", "function login() { return true; }"),
|
|
106
|
+
createHit("2", "src/auth.ts", "function logout() { return false; }"),
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
const rag = new CorrectiveRag({
|
|
110
|
+
retriever: new MockRetriever(hits),
|
|
111
|
+
grader: new MockGrader(new Set(["1", "2"])),
|
|
112
|
+
rewriter: new MockRewriter(),
|
|
113
|
+
minGoodHits: 2,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const result = await rag.answer("authentication");
|
|
117
|
+
|
|
118
|
+
expect(result.text).toContain("src/auth.ts:1-10");
|
|
119
|
+
expect(result.citations).toHaveLength(2);
|
|
120
|
+
expect(result.citations[0].path).toBe("src/auth.ts");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("returns empty answer when no relevant hits found", async () => {
|
|
124
|
+
const hits = [
|
|
125
|
+
createHit("1", "src/billing.ts", "function charge() { ... }"),
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
const rag = new CorrectiveRag({
|
|
129
|
+
retriever: new MockRetriever(hits),
|
|
130
|
+
grader: new MockGrader(new Set()),
|
|
131
|
+
rewriter: new MockRewriter(),
|
|
132
|
+
minGoodHits: 1,
|
|
133
|
+
maxRewrites: 0,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const result = await rag.answer("authentication");
|
|
137
|
+
|
|
138
|
+
expect(result.citations).toHaveLength(0);
|
|
139
|
+
expect(result.text).toContain("No relevant results found");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("uses web fallback when configured", async () => {
|
|
143
|
+
const hits = [
|
|
144
|
+
createHit("1", "src/billing.ts", "function charge() { ... }"),
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
const webHits = [
|
|
148
|
+
createHit("web1", "https://example.com", "Web result about auth"),
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
const rag = new CorrectiveRag({
|
|
152
|
+
retriever: new MockRetriever(hits),
|
|
153
|
+
grader: new MockGrader(new Set()),
|
|
154
|
+
rewriter: new MockRewriter(),
|
|
155
|
+
minGoodHits: 1,
|
|
156
|
+
maxRewrites: 0,
|
|
157
|
+
webFallback: async (_query: string) => webHits,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const result = await rag.answer("authentication");
|
|
161
|
+
|
|
162
|
+
expect(result.citations).toHaveLength(1);
|
|
163
|
+
expect(result.citations[0].path).toBe("https://example.com");
|
|
164
|
+
expect(result.steps.some((s) => s.includes("Falling back to web search"))).toBe(true);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CorrectiveRagOptions,
|
|
3
|
+
RagAnswer,
|
|
4
|
+
RagCitation,
|
|
5
|
+
Retriever,
|
|
6
|
+
Grader,
|
|
7
|
+
QueryRewriter,
|
|
8
|
+
} from "./types.js";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_MAX_REWRITES = 1;
|
|
11
|
+
const DEFAULT_MIN_GOOD_HITS = 2;
|
|
12
|
+
const DEFAULT_MIN_SCORE = 0.3;
|
|
13
|
+
const DEFAULT_TOP_K = 8;
|
|
14
|
+
|
|
15
|
+
export class CorrectiveRag {
|
|
16
|
+
private retriever: Retriever;
|
|
17
|
+
private grader: Grader;
|
|
18
|
+
private rewriter: QueryRewriter;
|
|
19
|
+
private maxRewrites: number;
|
|
20
|
+
private minGoodHits: number;
|
|
21
|
+
private minScore: number;
|
|
22
|
+
private webFallback?: (query: string) => Promise<any[]>;
|
|
23
|
+
|
|
24
|
+
constructor(options: CorrectiveRagOptions) {
|
|
25
|
+
this.retriever = options.retriever;
|
|
26
|
+
this.grader = options.grader;
|
|
27
|
+
this.rewriter = options.rewriter;
|
|
28
|
+
this.maxRewrites = options.maxRewrites ?? DEFAULT_MAX_REWRITES;
|
|
29
|
+
this.minGoodHits = options.minGoodHits ?? DEFAULT_MIN_GOOD_HITS;
|
|
30
|
+
this.minScore = options.minScore ?? DEFAULT_MIN_SCORE;
|
|
31
|
+
this.webFallback = options.webFallback;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async answer(query: string): Promise<RagAnswer> {
|
|
35
|
+
const steps: string[] = [];
|
|
36
|
+
let currentQuery = query;
|
|
37
|
+
let rewrites = 0;
|
|
38
|
+
|
|
39
|
+
steps.push(`Initial query: "${query}"`);
|
|
40
|
+
|
|
41
|
+
let hits = await this.retriever.search(currentQuery, DEFAULT_TOP_K);
|
|
42
|
+
steps.push(`Retrieved ${hits.length} candidates`);
|
|
43
|
+
|
|
44
|
+
let goodHits = await this.gradeHits(currentQuery, hits);
|
|
45
|
+
steps.push(`Graded: ${goodHits.length} relevant hits (min score: ${this.minScore})`);
|
|
46
|
+
|
|
47
|
+
while (goodHits.length < this.minGoodHits && rewrites < this.maxRewrites) {
|
|
48
|
+
rewrites++;
|
|
49
|
+
steps.push(`Rewriting query (attempt ${rewrites}/${this.maxRewrites})`);
|
|
50
|
+
|
|
51
|
+
const context = goodHits.map((h) => h.text).join("\n");
|
|
52
|
+
currentQuery = await this.rewriter.rewrite(currentQuery, context);
|
|
53
|
+
steps.push(`Rewritten query: "${currentQuery}"`);
|
|
54
|
+
|
|
55
|
+
hits = await this.retriever.search(currentQuery, DEFAULT_TOP_K);
|
|
56
|
+
steps.push(`Retrieved ${hits.length} candidates after rewrite`);
|
|
57
|
+
|
|
58
|
+
goodHits = await this.gradeHits(currentQuery, hits);
|
|
59
|
+
steps.push(`Graded: ${goodHits.length} relevant hits`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (goodHits.length < this.minGoodHits && this.webFallback) {
|
|
63
|
+
steps.push(`Falling back to web search`);
|
|
64
|
+
const webHits = await this.webFallback(currentQuery);
|
|
65
|
+
goodHits = [...goodHits, ...webHits];
|
|
66
|
+
steps.push(`Added ${webHits.length} web results`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const citations: RagCitation[] = goodHits.map((hit) => ({
|
|
70
|
+
path: hit.path,
|
|
71
|
+
startLine: hit.startLine,
|
|
72
|
+
endLine: hit.endLine,
|
|
73
|
+
text: hit.text,
|
|
74
|
+
}));
|
|
75
|
+
|
|
76
|
+
const text = this.formatAnswer(currentQuery, goodHits);
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
text,
|
|
80
|
+
citations,
|
|
81
|
+
steps,
|
|
82
|
+
rewrites,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private async gradeHits(query: string, hits: any[]): Promise<any[]> {
|
|
87
|
+
const graded = await Promise.all(
|
|
88
|
+
hits.map(async (hit) => {
|
|
89
|
+
const result = await this.grader.grade(query, hit);
|
|
90
|
+
return { hit, result };
|
|
91
|
+
})
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return graded
|
|
95
|
+
.filter(({ result }) => result.relevant && result.score >= this.minScore)
|
|
96
|
+
.map(({ hit }) => hit);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private formatAnswer(query: string, hits: any[]): string {
|
|
100
|
+
if (hits.length === 0) {
|
|
101
|
+
return `No relevant results found for: "${query}"`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const sections = hits.map((hit, i) => {
|
|
105
|
+
const location = `${hit.path}:${hit.startLine}-${hit.endLine}`;
|
|
106
|
+
return `## Result ${i + 1} (${location})\n\n${hit.text}`;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return sections.join("\n\n---\n\n");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function createCorrectiveRag(options: CorrectiveRagOptions): CorrectiveRag {
|
|
114
|
+
return new CorrectiveRag(options);
|
|
115
|
+
}
|