@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,227 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { InMemoryFeedbackStorage } from "./feedback.js";
|
|
3
|
+
import { MetricsCalculator } from "./metrics.js";
|
|
4
|
+
import type { RagFeedback } from "./feedback.js";
|
|
5
|
+
|
|
6
|
+
describe("InMemoryFeedbackStorage", () => {
|
|
7
|
+
let storage: InMemoryFeedbackStorage;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
storage = new InMemoryFeedbackStorage();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("should save and retrieve feedback", async () => {
|
|
14
|
+
const feedback: RagFeedback = {
|
|
15
|
+
id: "test-1",
|
|
16
|
+
query: "test query",
|
|
17
|
+
answer: {
|
|
18
|
+
text: "test answer",
|
|
19
|
+
citations: [],
|
|
20
|
+
steps: [],
|
|
21
|
+
rewrites: 0,
|
|
22
|
+
},
|
|
23
|
+
rating: 4,
|
|
24
|
+
relevance: 0.8,
|
|
25
|
+
completeness: 0.7,
|
|
26
|
+
citationsQuality: 0.9,
|
|
27
|
+
timestamp: new Date(),
|
|
28
|
+
strategy: "strategy-a",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
await storage.saveFeedback(feedback);
|
|
32
|
+
const retrieved = await storage.getFeedback("strategy-a");
|
|
33
|
+
|
|
34
|
+
expect(retrieved).toHaveLength(1);
|
|
35
|
+
expect(retrieved[0]?.id).toBe("test-1");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should filter feedback by strategy", async () => {
|
|
39
|
+
const feedback1: RagFeedback = {
|
|
40
|
+
id: "test-1",
|
|
41
|
+
query: "query 1",
|
|
42
|
+
answer: { text: "answer 1", citations: [], steps: [], rewrites: 0 },
|
|
43
|
+
rating: 4,
|
|
44
|
+
relevance: 0.8,
|
|
45
|
+
completeness: 0.7,
|
|
46
|
+
citationsQuality: 0.9,
|
|
47
|
+
timestamp: new Date(),
|
|
48
|
+
strategy: "strategy-a",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const feedback2: RagFeedback = {
|
|
52
|
+
id: "test-2",
|
|
53
|
+
query: "query 2",
|
|
54
|
+
answer: { text: "answer 2", citations: [], steps: [], rewrites: 0 },
|
|
55
|
+
rating: 5,
|
|
56
|
+
relevance: 0.9,
|
|
57
|
+
completeness: 0.8,
|
|
58
|
+
citationsQuality: 0.95,
|
|
59
|
+
timestamp: new Date(),
|
|
60
|
+
strategy: "strategy-b",
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
await storage.saveFeedback(feedback1);
|
|
64
|
+
await storage.saveFeedback(feedback2);
|
|
65
|
+
|
|
66
|
+
const retrievedA = await storage.getFeedback("strategy-a");
|
|
67
|
+
const retrievedB = await storage.getFeedback("strategy-b");
|
|
68
|
+
|
|
69
|
+
expect(retrievedA).toHaveLength(1);
|
|
70
|
+
expect(retrievedB).toHaveLength(1);
|
|
71
|
+
expect(retrievedA[0]?.strategy).toBe("strategy-a");
|
|
72
|
+
expect(retrievedB[0]?.strategy).toBe("strategy-b");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("should limit feedback results", async () => {
|
|
76
|
+
for (let i = 0; i < 10; i++) {
|
|
77
|
+
await storage.saveFeedback({
|
|
78
|
+
id: `test-${i}`,
|
|
79
|
+
query: `query ${i}`,
|
|
80
|
+
answer: { text: `answer ${i}`, citations: [], steps: [], rewrites: 0 },
|
|
81
|
+
rating: 4,
|
|
82
|
+
relevance: 0.8,
|
|
83
|
+
completeness: 0.7,
|
|
84
|
+
citationsQuality: 0.9,
|
|
85
|
+
timestamp: new Date(),
|
|
86
|
+
strategy: "strategy-a",
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const retrieved = await storage.getFeedback("strategy-a", 5);
|
|
91
|
+
expect(retrieved).toHaveLength(5);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should clear feedback", async () => {
|
|
95
|
+
await storage.saveFeedback({
|
|
96
|
+
id: "test-1",
|
|
97
|
+
query: "query 1",
|
|
98
|
+
answer: { text: "answer 1", citations: [], steps: [], rewrites: 0 },
|
|
99
|
+
rating: 4,
|
|
100
|
+
relevance: 0.8,
|
|
101
|
+
completeness: 0.7,
|
|
102
|
+
citationsQuality: 0.9,
|
|
103
|
+
timestamp: new Date(),
|
|
104
|
+
strategy: "strategy-a",
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await storage.clearFeedback("strategy-a");
|
|
108
|
+
const retrieved = await storage.getFeedback("strategy-a");
|
|
109
|
+
expect(retrieved).toHaveLength(0);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("MetricsCalculator", () => {
|
|
114
|
+
it("should calculate metrics from feedback", () => {
|
|
115
|
+
const feedbacks: RagFeedback[] = [
|
|
116
|
+
{
|
|
117
|
+
id: "test-1",
|
|
118
|
+
query: "query 1",
|
|
119
|
+
answer: { text: "answer 1", citations: [], steps: [], rewrites: 0 },
|
|
120
|
+
rating: 4,
|
|
121
|
+
relevance: 0.8,
|
|
122
|
+
completeness: 0.7,
|
|
123
|
+
citationsQuality: 0.9,
|
|
124
|
+
timestamp: new Date(),
|
|
125
|
+
strategy: "strategy-a",
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: "test-2",
|
|
129
|
+
query: "query 2",
|
|
130
|
+
answer: { text: "answer 2", citations: [], steps: [], rewrites: 0 },
|
|
131
|
+
rating: 5,
|
|
132
|
+
relevance: 0.9,
|
|
133
|
+
completeness: 0.8,
|
|
134
|
+
citationsQuality: 0.95,
|
|
135
|
+
timestamp: new Date(),
|
|
136
|
+
strategy: "strategy-a",
|
|
137
|
+
},
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
const metrics = MetricsCalculator.calculateMetrics(feedbacks, "strategy-a");
|
|
141
|
+
|
|
142
|
+
expect(metrics.strategy).toBe("strategy-a");
|
|
143
|
+
expect(metrics.totalQueries).toBe(2);
|
|
144
|
+
expect(metrics.averageRating).toBe(4.5);
|
|
145
|
+
expect(metrics.averageRelevance).toBeCloseTo(0.85, 10);
|
|
146
|
+
expect(metrics.averageCompleteness).toBeCloseTo(0.75, 10);
|
|
147
|
+
expect(metrics.averageCitationsQuality).toBeCloseTo(0.925, 10);
|
|
148
|
+
expect(metrics.overallScore).toBeGreaterThan(0);
|
|
149
|
+
expect(metrics.confidence).toBeGreaterThan(0);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("should return zero metrics for empty feedback", () => {
|
|
153
|
+
const metrics = MetricsCalculator.calculateMetrics([], "strategy-a");
|
|
154
|
+
|
|
155
|
+
expect(metrics.totalQueries).toBe(0);
|
|
156
|
+
expect(metrics.averageRating).toBe(0);
|
|
157
|
+
expect(metrics.overallScore).toBe(0);
|
|
158
|
+
expect(metrics.confidence).toBe(0);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("should calculate standard deviation", () => {
|
|
162
|
+
const feedbacks: RagFeedback[] = [
|
|
163
|
+
{
|
|
164
|
+
id: "test-1",
|
|
165
|
+
query: "query 1",
|
|
166
|
+
answer: { text: "answer 1", citations: [], steps: [], rewrites: 0 },
|
|
167
|
+
rating: 3,
|
|
168
|
+
relevance: 0.6,
|
|
169
|
+
completeness: 0.5,
|
|
170
|
+
citationsQuality: 0.7,
|
|
171
|
+
timestamp: new Date(),
|
|
172
|
+
strategy: "strategy-a",
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
id: "test-2",
|
|
176
|
+
query: "query 2",
|
|
177
|
+
answer: { text: "answer 2", citations: [], steps: [], rewrites: 0 },
|
|
178
|
+
rating: 5,
|
|
179
|
+
relevance: 0.9,
|
|
180
|
+
completeness: 0.8,
|
|
181
|
+
citationsQuality: 0.95,
|
|
182
|
+
timestamp: new Date(),
|
|
183
|
+
strategy: "strategy-a",
|
|
184
|
+
},
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
const stdDev = MetricsCalculator.calculateStandardDeviation(feedbacks, "relevance");
|
|
188
|
+
expect(stdDev).toBeGreaterThan(0);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("should identify trend", () => {
|
|
192
|
+
const feedbacks: RagFeedback[] = [];
|
|
193
|
+
|
|
194
|
+
// Older feedback with lower relevance
|
|
195
|
+
for (let i = 0; i < 10; i++) {
|
|
196
|
+
feedbacks.push({
|
|
197
|
+
id: `old-${i}`,
|
|
198
|
+
query: `query ${i}`,
|
|
199
|
+
answer: { text: `answer ${i}`, citations: [], steps: [], rewrites: 0 },
|
|
200
|
+
rating: 3,
|
|
201
|
+
relevance: 0.5,
|
|
202
|
+
completeness: 0.5,
|
|
203
|
+
citationsQuality: 0.5,
|
|
204
|
+
timestamp: new Date(Date.now() - 1000000),
|
|
205
|
+
strategy: "strategy-a",
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Recent feedback with higher relevance
|
|
210
|
+
for (let i = 0; i < 10; i++) {
|
|
211
|
+
feedbacks.push({
|
|
212
|
+
id: `new-${i}`,
|
|
213
|
+
query: `query ${i}`,
|
|
214
|
+
answer: { text: `answer ${i}`, citations: [], steps: [], rewrites: 0 },
|
|
215
|
+
rating: 5,
|
|
216
|
+
relevance: 0.9,
|
|
217
|
+
completeness: 0.9,
|
|
218
|
+
citationsQuality: 0.9,
|
|
219
|
+
timestamp: new Date(),
|
|
220
|
+
strategy: "strategy-a",
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const trend = MetricsCalculator.identifyTrend(feedbacks, 10);
|
|
225
|
+
expect(trend).toBe("improving");
|
|
226
|
+
});
|
|
227
|
+
});
|
package/src/feedback.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { RagAnswer } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Feedback per una risposta RAG
|
|
5
|
+
*/
|
|
6
|
+
export interface RagFeedback {
|
|
7
|
+
id: string;
|
|
8
|
+
query: string;
|
|
9
|
+
answer: RagAnswer;
|
|
10
|
+
rating: number; // 1-5
|
|
11
|
+
relevance: number; // 0-1
|
|
12
|
+
completeness: number; // 0-1
|
|
13
|
+
citationsQuality: number; // 0-1
|
|
14
|
+
comments?: string;
|
|
15
|
+
timestamp: Date;
|
|
16
|
+
strategy?: string; // Nome della strategia usata (per A/B testing)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Metriche aggregate per una strategia RAG
|
|
21
|
+
*/
|
|
22
|
+
export interface RagMetrics {
|
|
23
|
+
strategy: string;
|
|
24
|
+
totalQueries: number;
|
|
25
|
+
averageRating: number;
|
|
26
|
+
averageRelevance: number;
|
|
27
|
+
averageCompleteness: number;
|
|
28
|
+
averageCitationsQuality: number;
|
|
29
|
+
overallScore: number; // Media pesata delle metriche
|
|
30
|
+
confidence: number; // 0-1, basato sul numero di sample
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Risultato A/B test
|
|
35
|
+
*/
|
|
36
|
+
export interface ABTestResult {
|
|
37
|
+
strategyA: string;
|
|
38
|
+
strategyB: string;
|
|
39
|
+
winner: "A" | "B" | "tie";
|
|
40
|
+
metricsA: RagMetrics;
|
|
41
|
+
metricsB: RagMetrics;
|
|
42
|
+
improvement: number; // Percentuale di miglioramento
|
|
43
|
+
statisticalSignificance: number; // 0-1
|
|
44
|
+
recommendation: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Suggerimento di ottimizzazione
|
|
49
|
+
*/
|
|
50
|
+
export interface OptimizationSuggestion {
|
|
51
|
+
type: "parameter" | "strategy" | "prompt";
|
|
52
|
+
target: string;
|
|
53
|
+
current: unknown;
|
|
54
|
+
suggested: unknown;
|
|
55
|
+
expectedImprovement: number; // 0-1
|
|
56
|
+
confidence: number; // 0-1
|
|
57
|
+
reasoning: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Storage per feedback e metriche
|
|
62
|
+
*/
|
|
63
|
+
export interface FeedbackStorage {
|
|
64
|
+
saveFeedback(feedback: RagFeedback): Promise<void>;
|
|
65
|
+
getFeedback(strategy?: string, limit?: number): Promise<RagFeedback[]>;
|
|
66
|
+
getMetrics(strategy: string): Promise<RagMetrics | null>;
|
|
67
|
+
saveMetrics(metrics: RagMetrics): Promise<void>;
|
|
68
|
+
clearFeedback(strategy?: string): Promise<void>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* In-memory implementation di FeedbackStorage
|
|
73
|
+
*/
|
|
74
|
+
export class InMemoryFeedbackStorage implements FeedbackStorage {
|
|
75
|
+
private feedbacks: Map<string, RagFeedback[]> = new Map();
|
|
76
|
+
private metrics: Map<string, RagMetrics> = new Map();
|
|
77
|
+
|
|
78
|
+
async saveFeedback(feedback: RagFeedback): Promise<void> {
|
|
79
|
+
const strategy = feedback.strategy || "default";
|
|
80
|
+
if (!this.feedbacks.has(strategy)) {
|
|
81
|
+
this.feedbacks.set(strategy, []);
|
|
82
|
+
}
|
|
83
|
+
this.feedbacks.get(strategy)!.push(feedback);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async getFeedback(strategy?: string, limit?: number): Promise<RagFeedback[]> {
|
|
87
|
+
if (strategy) {
|
|
88
|
+
const feedbacks = this.feedbacks.get(strategy) || [];
|
|
89
|
+
return limit ? feedbacks.slice(-limit) : feedbacks;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Tutti i feedback
|
|
93
|
+
const all: RagFeedback[] = [];
|
|
94
|
+
for (const feedbacks of this.feedbacks.values()) {
|
|
95
|
+
all.push(...feedbacks);
|
|
96
|
+
}
|
|
97
|
+
all.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
98
|
+
return limit ? all.slice(-limit) : all;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async getMetrics(strategy: string): Promise<RagMetrics | null> {
|
|
102
|
+
return this.metrics.get(strategy) || null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async saveMetrics(metrics: RagMetrics): Promise<void> {
|
|
106
|
+
this.metrics.set(metrics.strategy, metrics);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async clearFeedback(strategy?: string): Promise<void> {
|
|
110
|
+
if (strategy) {
|
|
111
|
+
this.feedbacks.delete(strategy);
|
|
112
|
+
this.metrics.delete(strategy);
|
|
113
|
+
} else {
|
|
114
|
+
this.feedbacks.clear();
|
|
115
|
+
this.metrics.clear();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { HybridSearchRag } from "./hybrid-search.js";
|
|
3
|
+
import type { SearchHit } from "@eucode/indexer";
|
|
4
|
+
import type { Retriever, KeywordRetriever } from "./types.js";
|
|
5
|
+
|
|
6
|
+
class MockSemanticRetriever implements Retriever {
|
|
7
|
+
async search(_query: string, topK: number): Promise<SearchHit[]> {
|
|
8
|
+
return [
|
|
9
|
+
{
|
|
10
|
+
id: "hit-1",
|
|
11
|
+
path: "src/auth.ts",
|
|
12
|
+
startLine: 10,
|
|
13
|
+
endLine: 20,
|
|
14
|
+
text: "Authentication logic",
|
|
15
|
+
score: 0.9,
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: "hit-2",
|
|
19
|
+
path: "src/login.ts",
|
|
20
|
+
startLine: 5,
|
|
21
|
+
endLine: 15,
|
|
22
|
+
text: "Login implementation",
|
|
23
|
+
score: 0.8,
|
|
24
|
+
},
|
|
25
|
+
].slice(0, topK);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class MockKeywordRetriever implements KeywordRetriever {
|
|
30
|
+
async search(_query: string, topK: number): Promise<SearchHit[]> {
|
|
31
|
+
return [
|
|
32
|
+
{
|
|
33
|
+
id: "hit-2",
|
|
34
|
+
path: "src/login.ts",
|
|
35
|
+
startLine: 5,
|
|
36
|
+
endLine: 15,
|
|
37
|
+
text: "Login implementation",
|
|
38
|
+
score: 0.85,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: "hit-3",
|
|
42
|
+
path: "src/session.ts",
|
|
43
|
+
startLine: 1,
|
|
44
|
+
endLine: 10,
|
|
45
|
+
text: "Session management",
|
|
46
|
+
score: 0.7,
|
|
47
|
+
},
|
|
48
|
+
].slice(0, topK);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe("HybridSearchRag", () => {
|
|
53
|
+
it("should combine semantic and keyword results using RRF", async () => {
|
|
54
|
+
const hybrid = new HybridSearchRag({
|
|
55
|
+
semanticRetriever: new MockSemanticRetriever(),
|
|
56
|
+
keywordRetriever: new MockKeywordRetriever(),
|
|
57
|
+
topK: 5,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const result = await hybrid.search("authentication");
|
|
61
|
+
|
|
62
|
+
expect(result.hits.length).toBeGreaterThan(0);
|
|
63
|
+
expect(result.fusionMethod).toBe("rrf");
|
|
64
|
+
expect(result.semanticCount).toBe(2);
|
|
65
|
+
expect(result.keywordCount).toBe(2);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should rank overlapping results higher", async () => {
|
|
69
|
+
const hybrid = new HybridSearchRag({
|
|
70
|
+
semanticRetriever: new MockSemanticRetriever(),
|
|
71
|
+
keywordRetriever: new MockKeywordRetriever(),
|
|
72
|
+
topK: 5,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const result = await hybrid.search("login");
|
|
76
|
+
|
|
77
|
+
// src/login.ts appears in both, should be ranked higher
|
|
78
|
+
const loginHit = result.hits.find((h) => h.path === "src/login.ts");
|
|
79
|
+
expect(loginHit).toBeDefined();
|
|
80
|
+
expect(result.hits[0]?.path).toBe("src/login.ts");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("should respect custom weights", async () => {
|
|
84
|
+
const hybridSemantic = new HybridSearchRag({
|
|
85
|
+
semanticRetriever: new MockSemanticRetriever(),
|
|
86
|
+
keywordRetriever: new MockKeywordRetriever(),
|
|
87
|
+
semanticWeight: 0.9,
|
|
88
|
+
keywordWeight: 0.1,
|
|
89
|
+
topK: 5,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const resultSemantic = await hybridSemantic.search("auth");
|
|
93
|
+
|
|
94
|
+
const hybridKeyword = new HybridSearchRag({
|
|
95
|
+
semanticRetriever: new MockSemanticRetriever(),
|
|
96
|
+
keywordRetriever: new MockKeywordRetriever(),
|
|
97
|
+
semanticWeight: 0.1,
|
|
98
|
+
keywordWeight: 0.9,
|
|
99
|
+
topK: 5,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const resultKeyword = await hybridKeyword.search("auth");
|
|
103
|
+
|
|
104
|
+
// Results should differ based on weights
|
|
105
|
+
expect(resultSemantic.hits[0]?.score).not.toBe(resultKeyword.hits[0]?.score);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { SearchHit } from "@eucode/indexer";
|
|
2
|
+
import type { HybridSearchOptions, HybridSearchResult } from "./types.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hybrid Search RAG - Combina semantic search e keyword search usando RRF (Reciprocal Rank Fusion)
|
|
6
|
+
*
|
|
7
|
+
* RRF Formula: score(d) = Σ 1/(k + rank_i(d))
|
|
8
|
+
* dove k è una costante (tipicamente 60) e rank_i è la posizione del documento nella lista i
|
|
9
|
+
*/
|
|
10
|
+
export class HybridSearchRag {
|
|
11
|
+
private options: Required<HybridSearchOptions>;
|
|
12
|
+
|
|
13
|
+
constructor(options: HybridSearchOptions) {
|
|
14
|
+
this.options = {
|
|
15
|
+
semanticWeight: options.semanticWeight ?? 0.5,
|
|
16
|
+
keywordWeight: options.keywordWeight ?? 0.5,
|
|
17
|
+
rrfK: options.rrfK ?? 60,
|
|
18
|
+
topK: options.topK ?? 10,
|
|
19
|
+
...options,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async search(query: string, topK?: number): Promise<HybridSearchResult> {
|
|
24
|
+
const limit = topK ?? this.options.topK;
|
|
25
|
+
|
|
26
|
+
// Esegui entrambe le ricerche in parallelo
|
|
27
|
+
const [semanticHits, keywordHits] = await Promise.all([
|
|
28
|
+
this.options.semanticRetriever.search(query, limit * 2),
|
|
29
|
+
this.options.keywordRetriever.search(query, limit * 2),
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
// Applica RRF fusion
|
|
33
|
+
const fusedHits = this.reciprocalRankFusion(semanticHits, keywordHits);
|
|
34
|
+
|
|
35
|
+
// Prendi i top K risultati
|
|
36
|
+
const finalHits = fusedHits.slice(0, limit);
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
hits: finalHits,
|
|
40
|
+
fusionMethod: "rrf",
|
|
41
|
+
semanticCount: semanticHits.length,
|
|
42
|
+
keywordCount: keywordHits.length,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private reciprocalRankFusion(
|
|
47
|
+
semanticHits: SearchHit[],
|
|
48
|
+
keywordHits: SearchHit[]
|
|
49
|
+
): SearchHit[] {
|
|
50
|
+
const scoreMap = new Map<string, { hit: SearchHit; score: number }>();
|
|
51
|
+
const k = this.options.rrfK;
|
|
52
|
+
|
|
53
|
+
// Calcola RRF score per semantic hits
|
|
54
|
+
semanticHits.forEach((hit, index) => {
|
|
55
|
+
const key = `${hit.path}:${hit.startLine}`;
|
|
56
|
+
const rrfScore = 1 / (k + index + 1);
|
|
57
|
+
|
|
58
|
+
const existing = scoreMap.get(key);
|
|
59
|
+
if (existing) {
|
|
60
|
+
existing.score += rrfScore * this.options.semanticWeight;
|
|
61
|
+
} else {
|
|
62
|
+
scoreMap.set(key, { hit, score: rrfScore * this.options.semanticWeight });
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Calcola RRF score per keyword hits
|
|
67
|
+
keywordHits.forEach((hit, index) => {
|
|
68
|
+
const key = `${hit.path}:${hit.startLine}`;
|
|
69
|
+
const rrfScore = 1 / (k + index + 1);
|
|
70
|
+
|
|
71
|
+
const existing = scoreMap.get(key);
|
|
72
|
+
if (existing) {
|
|
73
|
+
existing.score += rrfScore * this.options.keywordWeight;
|
|
74
|
+
} else {
|
|
75
|
+
scoreMap.set(key, { hit, score: rrfScore * this.options.keywordWeight });
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Ordina per score decrescente
|
|
80
|
+
const sorted = Array.from(scoreMap.values())
|
|
81
|
+
.sort((a, b) => b.score - a.score)
|
|
82
|
+
.map(({ hit, score }) => ({ ...hit, score }));
|
|
83
|
+
|
|
84
|
+
return sorted;
|
|
85
|
+
}
|
|
86
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export { CorrectiveRag, createCorrectiveRag } from "./corrective-rag.js";
|
|
2
|
+
export { LlmGrader, LlmQueryRewriter } from "./llm-grader.js";
|
|
3
|
+
export { HybridSearchRag } from "./hybrid-search.js";
|
|
4
|
+
export { KnowledgeGraphRag, InMemoryKnowledgeGraph } from "./knowledge-graph.js";
|
|
5
|
+
export { AgenticRag } from "./agentic-rag.js";
|
|
6
|
+
|
|
7
|
+
// Fase 4: Self-Improving
|
|
8
|
+
export { InMemoryFeedbackStorage } from "./feedback.js";
|
|
9
|
+
export { MetricsCalculator } from "./metrics.js";
|
|
10
|
+
export { ABTestFramework } from "./ab-testing.js";
|
|
11
|
+
export { ParameterOptimizer } from "./optimizer.js";
|
|
12
|
+
export { SelfImprovingRag } from "./self-improving.js";
|
|
13
|
+
|
|
14
|
+
export type { LlmProvider } from "./llm-grader.js";
|
|
15
|
+
export type {
|
|
16
|
+
Retriever,
|
|
17
|
+
KeywordRetriever,
|
|
18
|
+
Grader,
|
|
19
|
+
GraderResult,
|
|
20
|
+
QueryRewriter,
|
|
21
|
+
CorrectiveRagOptions,
|
|
22
|
+
RagAnswer,
|
|
23
|
+
RagCitation,
|
|
24
|
+
HybridSearchOptions,
|
|
25
|
+
HybridSearchResult,
|
|
26
|
+
Entity,
|
|
27
|
+
Relation,
|
|
28
|
+
KnowledgeGraph,
|
|
29
|
+
EntityExtractor,
|
|
30
|
+
RelationExtractor,
|
|
31
|
+
KnowledgeGraphRagOptions,
|
|
32
|
+
KnowledgeGraphRagResult,
|
|
33
|
+
ReasoningStep,
|
|
34
|
+
AgenticRagOptions,
|
|
35
|
+
AgenticRagResult,
|
|
36
|
+
} from "./types.js";
|
|
37
|
+
|
|
38
|
+
// Fase 4: Self-Improving Types
|
|
39
|
+
export type {
|
|
40
|
+
RagFeedback,
|
|
41
|
+
RagMetrics,
|
|
42
|
+
ABTestResult,
|
|
43
|
+
OptimizationSuggestion,
|
|
44
|
+
FeedbackStorage,
|
|
45
|
+
} from "./feedback.js";
|
|
46
|
+
|
|
47
|
+
export type {
|
|
48
|
+
RagStrategyConfig,
|
|
49
|
+
} from "./ab-testing.js";
|
|
50
|
+
|
|
51
|
+
export type {
|
|
52
|
+
OptimizableParams,
|
|
53
|
+
} from "./optimizer.js";
|
|
54
|
+
|
|
55
|
+
export type {
|
|
56
|
+
SelfImprovingConfig,
|
|
57
|
+
} from "./self-improving.js";
|