@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,170 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { KnowledgeGraphRag, InMemoryKnowledgeGraph } from "./knowledge-graph.js";
|
|
3
|
+
import type { SearchHit } from "@eucode/indexer";
|
|
4
|
+
import type {
|
|
5
|
+
Retriever,
|
|
6
|
+
EntityExtractor,
|
|
7
|
+
RelationExtractor,
|
|
8
|
+
Entity,
|
|
9
|
+
Relation,
|
|
10
|
+
} from "./types.js";
|
|
11
|
+
|
|
12
|
+
class MockRetriever implements Retriever {
|
|
13
|
+
async search(_query: string, topK: number): Promise<SearchHit[]> {
|
|
14
|
+
return [
|
|
15
|
+
{
|
|
16
|
+
id: "hit-1",
|
|
17
|
+
path: "src/user.ts",
|
|
18
|
+
startLine: 1,
|
|
19
|
+
endLine: 10,
|
|
20
|
+
text: "User class with authentication",
|
|
21
|
+
score: 0.9,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: "hit-2",
|
|
25
|
+
path: "src/auth.ts",
|
|
26
|
+
startLine: 5,
|
|
27
|
+
endLine: 15,
|
|
28
|
+
text: "Authentication service",
|
|
29
|
+
score: 0.85,
|
|
30
|
+
},
|
|
31
|
+
].slice(0, topK);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class MockEntityExtractor implements EntityExtractor {
|
|
36
|
+
async extract(_text: string): Promise<Entity[]> {
|
|
37
|
+
return [
|
|
38
|
+
{
|
|
39
|
+
id: "user",
|
|
40
|
+
name: "User",
|
|
41
|
+
type: "class",
|
|
42
|
+
properties: {},
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "auth",
|
|
46
|
+
name: "Authentication",
|
|
47
|
+
type: "service",
|
|
48
|
+
properties: {},
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
class MockRelationExtractor implements RelationExtractor {
|
|
55
|
+
async extract(_text: string, _entities: Entity[]): Promise<Relation[]> {
|
|
56
|
+
return [
|
|
57
|
+
{
|
|
58
|
+
id: "user-auth",
|
|
59
|
+
source: "user",
|
|
60
|
+
target: "auth",
|
|
61
|
+
type: "uses",
|
|
62
|
+
properties: {},
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe("KnowledgeGraphRag", () => {
|
|
69
|
+
it("should extract entities and relations from search results", async () => {
|
|
70
|
+
const graph = new InMemoryKnowledgeGraph();
|
|
71
|
+
const kgRag = new KnowledgeGraphRag({
|
|
72
|
+
retriever: new MockRetriever(),
|
|
73
|
+
entityExtractor: new MockEntityExtractor(),
|
|
74
|
+
relationExtractor: new MockRelationExtractor(),
|
|
75
|
+
graph,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const result = await kgRag.search("authentication");
|
|
79
|
+
|
|
80
|
+
expect(result.entities.length).toBe(2);
|
|
81
|
+
expect(result.relations.length).toBe(1);
|
|
82
|
+
expect(result.citations.length).toBe(2);
|
|
83
|
+
expect(result.answer).toContain("Knowledge Graph");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("should build graph context with connected entities", async () => {
|
|
87
|
+
const graph = new InMemoryKnowledgeGraph();
|
|
88
|
+
const kgRag = new KnowledgeGraphRag({
|
|
89
|
+
retriever: new MockRetriever(),
|
|
90
|
+
entityExtractor: new MockEntityExtractor(),
|
|
91
|
+
relationExtractor: new MockRelationExtractor(),
|
|
92
|
+
graph,
|
|
93
|
+
maxDepth: 2,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const result = await kgRag.search("user authentication");
|
|
97
|
+
|
|
98
|
+
expect(result.graphContext).toContain("User");
|
|
99
|
+
expect(result.graphContext).toContain("Authentication");
|
|
100
|
+
expect(result.graphContext).toContain("uses");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("should persist entities in the graph across searches", async () => {
|
|
104
|
+
const graph = new InMemoryKnowledgeGraph();
|
|
105
|
+
const kgRag = new KnowledgeGraphRag({
|
|
106
|
+
retriever: new MockRetriever(),
|
|
107
|
+
entityExtractor: new MockEntityExtractor(),
|
|
108
|
+
relationExtractor: new MockRelationExtractor(),
|
|
109
|
+
graph,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
await kgRag.search("first query");
|
|
113
|
+
await kgRag.search("second query");
|
|
114
|
+
|
|
115
|
+
expect(graph.entities.size).toBe(2);
|
|
116
|
+
expect(graph.relations.length).toBe(2);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("InMemoryKnowledgeGraph", () => {
|
|
121
|
+
it("should add and retrieve entities", () => {
|
|
122
|
+
const graph = new InMemoryKnowledgeGraph();
|
|
123
|
+
const entity: Entity = {
|
|
124
|
+
id: "test",
|
|
125
|
+
name: "Test",
|
|
126
|
+
type: "class",
|
|
127
|
+
properties: {},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
graph.addEntity(entity);
|
|
131
|
+
const retrieved = graph.getEntity("test");
|
|
132
|
+
|
|
133
|
+
expect(retrieved).toEqual(entity);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("should add and retrieve relations", () => {
|
|
137
|
+
const graph = new InMemoryKnowledgeGraph();
|
|
138
|
+
const relation: Relation = {
|
|
139
|
+
id: "rel1",
|
|
140
|
+
source: "a",
|
|
141
|
+
target: "b",
|
|
142
|
+
type: "uses",
|
|
143
|
+
properties: {},
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
graph.addRelation(relation);
|
|
147
|
+
const relations = graph.getRelations("a");
|
|
148
|
+
|
|
149
|
+
expect(relations.length).toBe(1);
|
|
150
|
+
expect(relations[0]).toEqual(relation);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("should find connected entities", () => {
|
|
154
|
+
const graph = new InMemoryKnowledgeGraph();
|
|
155
|
+
|
|
156
|
+
graph.addEntity({ id: "a", name: "A", type: "class", properties: {} });
|
|
157
|
+
graph.addEntity({ id: "b", name: "B", type: "class", properties: {} });
|
|
158
|
+
graph.addEntity({ id: "c", name: "C", type: "class", properties: {} });
|
|
159
|
+
|
|
160
|
+
graph.addRelation({ id: "r1", source: "a", target: "b", type: "uses", properties: {} });
|
|
161
|
+
graph.addRelation({ id: "r2", source: "b", target: "c", type: "uses", properties: {} });
|
|
162
|
+
|
|
163
|
+
const connected = graph.getConnectedEntities("a", 1);
|
|
164
|
+
expect(connected.length).toBe(1);
|
|
165
|
+
expect(connected[0]?.id).toBe("b");
|
|
166
|
+
|
|
167
|
+
const connectedDeep = graph.getConnectedEntities("a", 2);
|
|
168
|
+
expect(connectedDeep.length).toBe(2);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import type { SearchHit } from "@eucode/indexer";
|
|
2
|
+
import type {
|
|
3
|
+
Entity,
|
|
4
|
+
Relation,
|
|
5
|
+
KnowledgeGraph,
|
|
6
|
+
KnowledgeGraphRagOptions,
|
|
7
|
+
KnowledgeGraphRagResult,
|
|
8
|
+
RagCitation,
|
|
9
|
+
} from "./types.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Implementazione base di Knowledge Graph in memoria
|
|
13
|
+
*/
|
|
14
|
+
export class InMemoryKnowledgeGraph implements KnowledgeGraph {
|
|
15
|
+
entities: Map<string, Entity> = new Map();
|
|
16
|
+
relations: Relation[] = [];
|
|
17
|
+
|
|
18
|
+
addEntity(entity: Entity): void {
|
|
19
|
+
this.entities.set(entity.id, entity);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
addRelation(relation: Relation): void {
|
|
23
|
+
this.relations.push(relation);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
getEntity(id: string): Entity | undefined {
|
|
27
|
+
return this.entities.get(id);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
getRelations(entityId: string): Relation[] {
|
|
31
|
+
return this.relations.filter(
|
|
32
|
+
(r) => r.source === entityId || r.target === entityId
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
getConnectedEntities(entityId: string, depth: number = 1): Entity[] {
|
|
37
|
+
const visited = new Set<string>();
|
|
38
|
+
const result: Entity[] = [];
|
|
39
|
+
|
|
40
|
+
const traverse = (currentId: string, currentDepth: number) => {
|
|
41
|
+
if (currentDepth > depth || visited.has(currentId)) return;
|
|
42
|
+
visited.add(currentId);
|
|
43
|
+
|
|
44
|
+
const entity = this.getEntity(currentId);
|
|
45
|
+
if (entity && currentId !== entityId) {
|
|
46
|
+
result.push(entity);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (currentDepth < depth) {
|
|
50
|
+
const relations = this.getRelations(currentId);
|
|
51
|
+
for (const rel of relations) {
|
|
52
|
+
const nextId = rel.source === currentId ? rel.target : rel.source;
|
|
53
|
+
traverse(nextId, currentDepth + 1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
traverse(entityId, 0);
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Knowledge Graph RAG - Arricchisce i risultati con contesto da un grafo di conoscenza
|
|
65
|
+
*/
|
|
66
|
+
export class KnowledgeGraphRag {
|
|
67
|
+
private options: Required<KnowledgeGraphRagOptions>;
|
|
68
|
+
|
|
69
|
+
constructor(options: KnowledgeGraphRagOptions) {
|
|
70
|
+
this.options = {
|
|
71
|
+
topK: options.topK ?? 10,
|
|
72
|
+
maxDepth: options.maxDepth ?? 2,
|
|
73
|
+
...options,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async search(query: string): Promise<KnowledgeGraphRagResult> {
|
|
78
|
+
// 1. Recupera risultati dal retriever
|
|
79
|
+
const hits = await this.options.retriever.search(query, this.options.topK);
|
|
80
|
+
|
|
81
|
+
// 2. Estrai entità dai risultati
|
|
82
|
+
const allText = hits.map((h) => h.text).join("\n");
|
|
83
|
+
const entities = await this.options.entityExtractor.extract(allText);
|
|
84
|
+
|
|
85
|
+
// 3. Aggiungi entità al grafo
|
|
86
|
+
entities.forEach((e) => this.options.graph.addEntity(e));
|
|
87
|
+
|
|
88
|
+
// 4. Estrai relazioni
|
|
89
|
+
const relations = await this.options.relationExtractor.extract(allText, entities);
|
|
90
|
+
relations.forEach((r) => this.options.graph.addRelation(r));
|
|
91
|
+
|
|
92
|
+
// 5. Arricchisci con contesto del grafo
|
|
93
|
+
const graphContext = this.buildGraphContext(entities);
|
|
94
|
+
|
|
95
|
+
// 6. Genera citazioni
|
|
96
|
+
const citations: RagCitation[] = hits.map((hit) => ({
|
|
97
|
+
path: hit.path,
|
|
98
|
+
startLine: hit.startLine,
|
|
99
|
+
endLine: hit.endLine,
|
|
100
|
+
text: hit.text,
|
|
101
|
+
}));
|
|
102
|
+
|
|
103
|
+
// 7. Costruisci risposta
|
|
104
|
+
const answer = this.buildAnswer(query, hits, entities, relations, graphContext);
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
answer,
|
|
108
|
+
citations,
|
|
109
|
+
entities,
|
|
110
|
+
relations,
|
|
111
|
+
graphContext,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private buildGraphContext(entities: Entity[]): string {
|
|
116
|
+
const contextParts: string[] = [];
|
|
117
|
+
|
|
118
|
+
for (const entity of entities) {
|
|
119
|
+
const connected = this.options.graph.getConnectedEntities(
|
|
120
|
+
entity.id,
|
|
121
|
+
this.options.maxDepth
|
|
122
|
+
);
|
|
123
|
+
const relations = this.options.graph.getRelations(entity.id);
|
|
124
|
+
|
|
125
|
+
if (connected.length > 0 || relations.length > 0) {
|
|
126
|
+
let part = `**${entity.name}** (${entity.type})`;
|
|
127
|
+
|
|
128
|
+
if (relations.length > 0) {
|
|
129
|
+
const relDescs = relations.map((r) => {
|
|
130
|
+
const otherId = r.source === entity.id ? r.target : r.source;
|
|
131
|
+
const other = this.options.graph.getEntity(otherId);
|
|
132
|
+
return `${r.type} → ${other?.name ?? otherId}`;
|
|
133
|
+
});
|
|
134
|
+
part += `\n Relazioni: ${relDescs.join(", ")}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (connected.length > 0) {
|
|
138
|
+
const connNames = connected.map((e) => e.name).join(", ");
|
|
139
|
+
part += `\n Connesso a: ${connNames}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
contextParts.push(part);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return contextParts.join("\n\n");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private buildAnswer(
|
|
150
|
+
_query: string,
|
|
151
|
+
hits: SearchHit[],
|
|
152
|
+
entities: Entity[],
|
|
153
|
+
_relations: Relation[],
|
|
154
|
+
graphContext: string
|
|
155
|
+
): string {
|
|
156
|
+
const parts: string[] = [];
|
|
157
|
+
|
|
158
|
+
parts.push(`## Risposta\n`);
|
|
159
|
+
parts.push(`Basato sull'analisi di ${hits.length} documenti e ${entities.length} entità identificate:\n`);
|
|
160
|
+
|
|
161
|
+
if (graphContext) {
|
|
162
|
+
parts.push(`### Contesto dal Knowledge Graph\n${graphContext}\n`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (hits.length > 0) {
|
|
166
|
+
parts.push(`### Fonti Principali\n`);
|
|
167
|
+
hits.slice(0, 3).forEach((hit, i) => {
|
|
168
|
+
parts.push(`${i + 1}. **${hit.path}** (righe ${hit.startLine}-${hit.endLine})`);
|
|
169
|
+
parts.push(` ${hit.text.slice(0, 200)}...\n`);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (entities.length > 0) {
|
|
174
|
+
parts.push(`### Entità Identificate\n`);
|
|
175
|
+
entities.slice(0, 5).forEach((e) => {
|
|
176
|
+
parts.push(`- **${e.name}** (${e.type})`);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return parts.join("\n");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { SearchHit } from "@eucode/indexer";
|
|
2
|
+
import type { Grader, GraderResult, QueryRewriter } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export interface LlmProvider {
|
|
5
|
+
chat(messages: Array<{ role: string; content: string }>): Promise<string>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class LlmGrader implements Grader {
|
|
9
|
+
constructor(private llm: LlmProvider) {}
|
|
10
|
+
|
|
11
|
+
async grade(query: string, hit: SearchHit): Promise<GraderResult> {
|
|
12
|
+
const prompt = `Rate the relevance of this code snippet to the query.
|
|
13
|
+
|
|
14
|
+
Query: ${query}
|
|
15
|
+
|
|
16
|
+
Code snippet from ${hit.path}:${hit.startLine}-${hit.endLine}:
|
|
17
|
+
\`\`\`
|
|
18
|
+
${hit.text.slice(0, 500)}
|
|
19
|
+
\`\`\`
|
|
20
|
+
|
|
21
|
+
Respond with JSON only: {"relevant": true/false, "score": 0.0-1.0}`;
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const response = await this.llm.chat([
|
|
25
|
+
{ role: "system", content: "You are a code relevance grader. Respond only with valid JSON." },
|
|
26
|
+
{ role: "user", content: prompt },
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
const match = response.match(/\{[\s\S]*\}/);
|
|
30
|
+
if (!match) {
|
|
31
|
+
return { relevant: false, score: 0 };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const parsed = JSON.parse(match[0]);
|
|
35
|
+
return {
|
|
36
|
+
relevant: Boolean(parsed.relevant),
|
|
37
|
+
score: Number(parsed.score) || 0,
|
|
38
|
+
};
|
|
39
|
+
} catch {
|
|
40
|
+
return { relevant: false, score: 0 };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class LlmQueryRewriter implements QueryRewriter {
|
|
46
|
+
constructor(private llm: LlmProvider) {}
|
|
47
|
+
|
|
48
|
+
async rewrite(query: string, context: string): Promise<string> {
|
|
49
|
+
const prompt = `The original query didn't find enough relevant results. Rewrite it to be more specific and effective.
|
|
50
|
+
|
|
51
|
+
Original query: ${query}
|
|
52
|
+
|
|
53
|
+
Context from partial results:
|
|
54
|
+
${context.slice(0, 300)}
|
|
55
|
+
|
|
56
|
+
Provide a rewritten query that will find better results. Respond with the query only, no explanation.`;
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const response = await this.llm.chat([
|
|
60
|
+
{ role: "system", content: "You are a search query rewriter. Respond with the rewritten query only." },
|
|
61
|
+
{ role: "user", content: prompt },
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
return response.trim();
|
|
65
|
+
} catch {
|
|
66
|
+
return query;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/metrics.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { RagFeedback, RagMetrics } from "./feedback.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Calcola metriche aggregate da un array di feedback
|
|
5
|
+
*/
|
|
6
|
+
export class MetricsCalculator {
|
|
7
|
+
/**
|
|
8
|
+
* Calcola le metriche per una strategia specifica
|
|
9
|
+
*/
|
|
10
|
+
static calculateMetrics(feedbacks: RagFeedback[], strategy: string): RagMetrics {
|
|
11
|
+
if (feedbacks.length === 0) {
|
|
12
|
+
return {
|
|
13
|
+
strategy,
|
|
14
|
+
totalQueries: 0,
|
|
15
|
+
averageRating: 0,
|
|
16
|
+
averageRelevance: 0,
|
|
17
|
+
averageCompleteness: 0,
|
|
18
|
+
averageCitationsQuality: 0,
|
|
19
|
+
overallScore: 0,
|
|
20
|
+
confidence: 0,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const totalQueries = feedbacks.length;
|
|
25
|
+
|
|
26
|
+
// Calcola medie
|
|
27
|
+
const sumRating = feedbacks.reduce((sum, f) => sum + f.rating, 0);
|
|
28
|
+
const sumRelevance = feedbacks.reduce((sum, f) => sum + f.relevance, 0);
|
|
29
|
+
const sumCompleteness = feedbacks.reduce((sum, f) => sum + f.completeness, 0);
|
|
30
|
+
const sumCitationsQuality = feedbacks.reduce((sum, f) => sum + f.citationsQuality, 0);
|
|
31
|
+
|
|
32
|
+
const averageRating = sumRating / totalQueries;
|
|
33
|
+
const averageRelevance = sumRelevance / totalQueries;
|
|
34
|
+
const averageCompleteness = sumCompleteness / totalQueries;
|
|
35
|
+
const averageCitationsQuality = sumCitationsQuality / totalQueries;
|
|
36
|
+
|
|
37
|
+
// Overall score: media pesata
|
|
38
|
+
// Rating (30%), Relevance (30%), Completeness (25%), Citations (15%)
|
|
39
|
+
const normalizedRating = averageRating / 5; // Normalizza a 0-1
|
|
40
|
+
const overallScore =
|
|
41
|
+
normalizedRating * 0.30 +
|
|
42
|
+
averageRelevance * 0.30 +
|
|
43
|
+
averageCompleteness * 0.25 +
|
|
44
|
+
averageCitationsQuality * 0.15;
|
|
45
|
+
|
|
46
|
+
// Confidence: basato sul numero di sample
|
|
47
|
+
// Minimo 10 sample per avere confidence > 0.5
|
|
48
|
+
// Massimo confidence a 50+ sample
|
|
49
|
+
const confidence = Math.min(1, totalQueries / 50);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
strategy,
|
|
53
|
+
totalQueries,
|
|
54
|
+
averageRating,
|
|
55
|
+
averageRelevance,
|
|
56
|
+
averageCompleteness,
|
|
57
|
+
averageCitationsQuality,
|
|
58
|
+
overallScore,
|
|
59
|
+
confidence,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Calcola la deviazione standard per una metrica specifica
|
|
65
|
+
*/
|
|
66
|
+
static calculateStandardDeviation(
|
|
67
|
+
feedbacks: RagFeedback[],
|
|
68
|
+
metric: "rating" | "relevance" | "completeness" | "citationsQuality"
|
|
69
|
+
): number {
|
|
70
|
+
if (feedbacks.length < 2) return 0;
|
|
71
|
+
|
|
72
|
+
const values = feedbacks.map((f) => f[metric]);
|
|
73
|
+
const mean = values.reduce((sum, v) => sum + v, 0) / values.length;
|
|
74
|
+
const squaredDiffs = values.map((v) => Math.pow(v - mean, 2));
|
|
75
|
+
const variance = squaredDiffs.reduce((sum, v) => sum + v, 0) / (values.length - 1);
|
|
76
|
+
|
|
77
|
+
return Math.sqrt(variance);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Calcola l'intervallo di confidenza al 95%
|
|
82
|
+
*/
|
|
83
|
+
static calculateConfidenceInterval(
|
|
84
|
+
feedbacks: RagFeedback[],
|
|
85
|
+
metric: "rating" | "relevance" | "completeness" | "citationsQuality"
|
|
86
|
+
): { lower: number; upper: number } {
|
|
87
|
+
if (feedbacks.length < 2) {
|
|
88
|
+
const value = feedbacks.length === 1 ? feedbacks[0][metric] : 0;
|
|
89
|
+
return { lower: value, upper: value };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const values = feedbacks.map((f) => f[metric]);
|
|
93
|
+
const mean = values.reduce((sum, v) => sum + v, 0) / values.length;
|
|
94
|
+
const stdDev = this.calculateStandardDeviation(feedbacks, metric);
|
|
95
|
+
const marginOfError = 1.96 * (stdDev / Math.sqrt(values.length)); // 95% confidence
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
lower: Math.max(0, mean - marginOfError),
|
|
99
|
+
upper: Math.min(metric === "rating" ? 5 : 1, mean + marginOfError),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Identifica trend nei feedback (miglioramento o peggioramento)
|
|
105
|
+
*/
|
|
106
|
+
static identifyTrend(feedbacks: RagFeedback[], windowSize: number = 10): "improving" | "declining" | "stable" {
|
|
107
|
+
if (feedbacks.length < windowSize * 2) return "stable";
|
|
108
|
+
|
|
109
|
+
const recent = feedbacks.slice(-windowSize);
|
|
110
|
+
const older = feedbacks.slice(-windowSize * 2, -windowSize);
|
|
111
|
+
|
|
112
|
+
const recentAvg = recent.reduce((sum, f) => sum + f.relevance, 0) / recent.length;
|
|
113
|
+
const olderAvg = older.reduce((sum, f) => sum + f.relevance, 0) / older.length;
|
|
114
|
+
|
|
115
|
+
const diff = recentAvg - olderAvg;
|
|
116
|
+
|
|
117
|
+
if (diff > 0.05) return "improving";
|
|
118
|
+
if (diff < -0.05) return "declining";
|
|
119
|
+
return "stable";
|
|
120
|
+
}
|
|
121
|
+
}
|