rippletide 1.0.10 → 1.0.12
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/dist/App.d.ts +1 -0
- package/dist/App.js +88 -5
- package/dist/api/endpoint.js +11 -5
- package/dist/api/evaluation.d.ts +1 -1
- package/dist/api/evaluation.js +19 -1
- package/dist/api/knowledge.d.ts +12 -0
- package/dist/api/knowledge.js +65 -1
- package/dist/errors/types.d.ts +8 -0
- package/dist/errors/types.js +23 -0
- package/dist/index.js +12 -2
- package/dist/utils/pdf.d.ts +28 -0
- package/dist/utils/pdf.js +100 -0
- package/package.json +4 -1
- package/src/App.tsx +111 -4
- package/src/api/endpoint.ts +12 -6
- package/src/api/evaluation.ts +23 -1
- package/src/api/knowledge.ts +105 -1
- package/src/errors/types.ts +48 -0
- package/src/index.tsx +11 -1
- package/src/utils/pdf.ts +184 -0
- package/tarifs-hellobank.pdf +0 -0
- package/tarifs-hellobank_removed.pdf +0 -0
package/dist/App.d.ts
CHANGED
package/dist/App.js
CHANGED
|
@@ -13,6 +13,7 @@ import { BaseError, ValidationError } from './errors/types.js';
|
|
|
13
13
|
import { logger } from './utils/logger.js';
|
|
14
14
|
const knowledgeSources = [
|
|
15
15
|
{ label: 'Local Files (qanda.json)', value: 'files', description: 'Use qanda.json from current directory' },
|
|
16
|
+
{ label: 'PDF Document', value: 'pdf', description: 'Upload and extract knowledge from a PDF file' },
|
|
16
17
|
{ label: 'Pinecone', value: 'pinecone', description: 'Fetch Q&A from Pinecone database' },
|
|
17
18
|
{ label: 'PostgreSQL Database', value: 'postgresql', description: 'Connect to PostgreSQL database' },
|
|
18
19
|
{ label: 'Current Repository', value: 'repo', description: 'Scan current git repository', disabled: true },
|
|
@@ -20,7 +21,7 @@ const knowledgeSources = [
|
|
|
20
21
|
{ label: 'GitHub Repository', value: 'github', description: 'Import from GitHub repo', disabled: true },
|
|
21
22
|
{ label: 'Skip (No Knowledge)', value: 'skip', description: 'Run tests without knowledge base', disabled: true },
|
|
22
23
|
];
|
|
23
|
-
export const App = ({ backendUrl, dashboardUrl, nonInteractive, agentEndpoint: initialAgentEndpoint, knowledgeSource: initialKnowledgeSource, pineconeUrl: initialPineconeUrl, pineconeApiKey: initialPineconeApiKey, postgresqlConnection: initialPostgresqlConnection, customHeaders, customBodyTemplate, customResponseField, templatePath }) => {
|
|
24
|
+
export const App = ({ backendUrl, dashboardUrl, nonInteractive, agentEndpoint: initialAgentEndpoint, knowledgeSource: initialKnowledgeSource, pineconeUrl: initialPineconeUrl, pineconeApiKey: initialPineconeApiKey, postgresqlConnection: initialPostgresqlConnection, pdfPath: initialPdfPath, customHeaders, customBodyTemplate, customResponseField, templatePath }) => {
|
|
24
25
|
const { exit } = useApp();
|
|
25
26
|
const initialStep = nonInteractive && initialAgentEndpoint ? 'testing-connection' : 'agent-endpoint';
|
|
26
27
|
const [step, setStep] = useState(initialStep);
|
|
@@ -40,6 +41,10 @@ export const App = ({ backendUrl, dashboardUrl, nonInteractive, agentEndpoint: i
|
|
|
40
41
|
const [postgresqlConnectionString, setPostgresqlConnectionString] = useState(initialPostgresqlConnection || '');
|
|
41
42
|
const [postgresqlQAndA, setPostgresqlQAndA] = useState([]);
|
|
42
43
|
const [postgresqlProgress, setPostgresqlProgress] = useState('');
|
|
44
|
+
const [pdfPath, setPdfPath] = useState(initialPdfPath || '');
|
|
45
|
+
const [pdfQAndA, setPdfQAndA] = useState([]);
|
|
46
|
+
const [pdfProgress, setPdfProgress] = useState('');
|
|
47
|
+
const [currentAgentId, setCurrentAgentId] = useState('');
|
|
43
48
|
const [evaluationProgress, setEvaluationProgress] = useState(0);
|
|
44
49
|
const [evaluationResult, setEvaluationResult] = useState(null);
|
|
45
50
|
const [currentQuestion, setCurrentQuestion] = useState('');
|
|
@@ -141,6 +146,15 @@ export const App = ({ backendUrl, dashboardUrl, nonInteractive, agentEndpoint: i
|
|
|
141
146
|
process.exit(1);
|
|
142
147
|
}
|
|
143
148
|
}
|
|
149
|
+
else if (knowledgeSource === 'pdf') {
|
|
150
|
+
if (pdfPath) {
|
|
151
|
+
setStep('uploading-pdf');
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
console.error('PDF path required for PDF source');
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
144
158
|
else {
|
|
145
159
|
setStep('running-evaluation');
|
|
146
160
|
}
|
|
@@ -220,6 +234,39 @@ export const App = ({ backendUrl, dashboardUrl, nonInteractive, agentEndpoint: i
|
|
|
220
234
|
})();
|
|
221
235
|
}
|
|
222
236
|
}, [step, postgresqlConnectionString, backendUrl]);
|
|
237
|
+
useEffect(() => {
|
|
238
|
+
if (step === 'uploading-pdf') {
|
|
239
|
+
(async () => {
|
|
240
|
+
try {
|
|
241
|
+
const startTime = Date.now();
|
|
242
|
+
setPdfProgress('Generating API key...');
|
|
243
|
+
await api.generateApiKey('CLI Evaluation');
|
|
244
|
+
setPdfProgress('Creating agent...');
|
|
245
|
+
const agent = await api.createAgent(agentEndpoint);
|
|
246
|
+
const agentId = agent.id;
|
|
247
|
+
setCurrentAgentId(agentId);
|
|
248
|
+
setPdfProgress('Uploading PDF file...');
|
|
249
|
+
const { uploadPdfToAgent } = await import('./api/knowledge.js');
|
|
250
|
+
const { qaPairs } = await uploadPdfToAgent(agentId, pdfPath, (message) => setPdfProgress(message));
|
|
251
|
+
setPdfQAndA(qaPairs);
|
|
252
|
+
setStep('running-evaluation');
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
logger.error('Error uploading PDF:', error);
|
|
256
|
+
const errorMessage = error instanceof BaseError ? error.userMessage : error.message;
|
|
257
|
+
setEvaluationResult({
|
|
258
|
+
totalTests: 0,
|
|
259
|
+
passed: 0,
|
|
260
|
+
failed: 0,
|
|
261
|
+
duration: 'Failed',
|
|
262
|
+
evaluationUrl: dashboardUrl || 'https://eval.rippletide.com',
|
|
263
|
+
error: errorMessage,
|
|
264
|
+
});
|
|
265
|
+
setStep('complete');
|
|
266
|
+
}
|
|
267
|
+
})();
|
|
268
|
+
}
|
|
269
|
+
}, [step, pdfPath, agentEndpoint, dashboardUrl]);
|
|
223
270
|
useEffect(() => {
|
|
224
271
|
if (step === 'running-evaluation') {
|
|
225
272
|
(async () => {
|
|
@@ -229,8 +276,14 @@ export const App = ({ backendUrl, dashboardUrl, nonInteractive, agentEndpoint: i
|
|
|
229
276
|
setEvaluationProgress(5);
|
|
230
277
|
await api.generateApiKey('CLI Evaluation');
|
|
231
278
|
setEvaluationProgress(10);
|
|
232
|
-
|
|
233
|
-
|
|
279
|
+
let agentId = currentAgentId;
|
|
280
|
+
if (!agentId) {
|
|
281
|
+
// Ensure we have a body template for the evaluation
|
|
282
|
+
const effectiveBodyTemplate = customConfig.bodyTemplate || '{"message": "[eval-question]"}';
|
|
283
|
+
const agent = await api.createAgent(agentEndpoint, effectiveBodyTemplate);
|
|
284
|
+
agentId = agent.id;
|
|
285
|
+
setCurrentAgentId(agentId);
|
|
286
|
+
}
|
|
234
287
|
setEvaluationProgress(30);
|
|
235
288
|
setEvaluationProgress(40);
|
|
236
289
|
let testPrompts = [];
|
|
@@ -290,8 +343,19 @@ export const App = ({ backendUrl, dashboardUrl, nonInteractive, agentEndpoint: i
|
|
|
290
343
|
answer: item.answer
|
|
291
344
|
}));
|
|
292
345
|
}
|
|
346
|
+
else if (knowledgeSource === 'pdf' && pdfQAndA.length > 0) {
|
|
347
|
+
testPrompts = pdfQAndA.slice(0, 5).map((item) => ({
|
|
348
|
+
question: item.question,
|
|
349
|
+
answer: item.answer
|
|
350
|
+
}));
|
|
351
|
+
}
|
|
293
352
|
const createdPrompts = await api.addTestPrompts(agentId, testPrompts);
|
|
294
353
|
setEvaluationProgress(50);
|
|
354
|
+
// Ensure customConfig has the body template for evaluation
|
|
355
|
+
const evalConfig = {
|
|
356
|
+
...customConfig,
|
|
357
|
+
bodyTemplate: customConfig.bodyTemplate || '{"message": "[eval-question]"}'
|
|
358
|
+
};
|
|
295
359
|
const evaluationResults = await api.runAllPromptEvaluations(agentId, createdPrompts, agentEndpoint, (current, total, question, llmResponse) => {
|
|
296
360
|
const progress = 50 + Math.round((current / total) * 40);
|
|
297
361
|
setEvaluationProgress(progress);
|
|
@@ -303,7 +367,7 @@ export const App = ({ backendUrl, dashboardUrl, nonInteractive, agentEndpoint: i
|
|
|
303
367
|
logs.push({ question: question || '', response: llmResponse });
|
|
304
368
|
setEvaluationLogs([...logs]);
|
|
305
369
|
}
|
|
306
|
-
},
|
|
370
|
+
}, evalConfig);
|
|
307
371
|
setEvaluationProgress(100);
|
|
308
372
|
let passed = 0;
|
|
309
373
|
let failed = 0;
|
|
@@ -345,7 +409,7 @@ export const App = ({ backendUrl, dashboardUrl, nonInteractive, agentEndpoint: i
|
|
|
345
409
|
}
|
|
346
410
|
})();
|
|
347
411
|
}
|
|
348
|
-
}, [step, agentEndpoint, knowledgeSource, pineconeQAndA, postgresqlQAndA]);
|
|
412
|
+
}, [step, agentEndpoint, knowledgeSource, pineconeQAndA, postgresqlQAndA, pdfQAndA, currentAgentId]);
|
|
349
413
|
const handleAgentEndpointSubmit = (value) => {
|
|
350
414
|
const trimmedValue = value.trim();
|
|
351
415
|
if (!trimmedValue) {
|
|
@@ -362,6 +426,9 @@ export const App = ({ backendUrl, dashboardUrl, nonInteractive, agentEndpoint: i
|
|
|
362
426
|
else if (value === 'postgresql') {
|
|
363
427
|
setStep('postgresql-config');
|
|
364
428
|
}
|
|
429
|
+
else if (value === 'pdf') {
|
|
430
|
+
setStep('pdf-path-input');
|
|
431
|
+
}
|
|
365
432
|
else {
|
|
366
433
|
setStep('running-evaluation');
|
|
367
434
|
}
|
|
@@ -378,6 +445,10 @@ export const App = ({ backendUrl, dashboardUrl, nonInteractive, agentEndpoint: i
|
|
|
378
445
|
setPostgresqlConnectionString(value);
|
|
379
446
|
setStep('fetching-postgresql');
|
|
380
447
|
};
|
|
448
|
+
const handlePdfPathSubmit = (value) => {
|
|
449
|
+
setPdfPath(value);
|
|
450
|
+
setStep('uploading-pdf');
|
|
451
|
+
};
|
|
381
452
|
return (React.createElement(Box, { flexDirection: "column", padding: 1 },
|
|
382
453
|
React.createElement(Header, null),
|
|
383
454
|
step === 'agent-endpoint' && (React.createElement(Box, { flexDirection: "column" },
|
|
@@ -542,6 +613,18 @@ export const App = ({ backendUrl, dashboardUrl, nonInteractive, agentEndpoint: i
|
|
|
542
613
|
React.createElement(TextInput, { label: "PostgreSQL connection", placeholder: "postgresql://postgres:password@localhost:5432/mydb", onSubmit: handlePostgresqlConnectionSubmit }))),
|
|
543
614
|
step === 'fetching-postgresql' && (React.createElement(Box, { flexDirection: "column" },
|
|
544
615
|
React.createElement(Spinner, { label: postgresqlProgress || "Analyzing PostgreSQL database..." }))),
|
|
616
|
+
step === 'pdf-path-input' && (React.createElement(Box, { flexDirection: "column" },
|
|
617
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
618
|
+
React.createElement(Text, { color: "#eba1b5" }, "Enter the path to your PDF file")),
|
|
619
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
620
|
+
React.createElement(Text, { dimColor: true }, "Examples:"),
|
|
621
|
+
React.createElement(Box, { paddingLeft: 2, flexDirection: "column" },
|
|
622
|
+
React.createElement(Text, { dimColor: true }, "- ./docs/manual.pdf"),
|
|
623
|
+
React.createElement(Text, { dimColor: true }, "- /home/user/documents/guide.pdf"),
|
|
624
|
+
React.createElement(Text, { dimColor: true }, "- ../knowledge/faq.pdf"))),
|
|
625
|
+
React.createElement(TextInput, { label: "PDF path", placeholder: "./document.pdf", onSubmit: handlePdfPathSubmit }))),
|
|
626
|
+
step === 'uploading-pdf' && (React.createElement(Box, { flexDirection: "column" },
|
|
627
|
+
React.createElement(Spinner, { label: pdfProgress || "Uploading PDF file..." }))),
|
|
545
628
|
step === 'running-evaluation' && (React.createElement(Box, { flexDirection: "column" },
|
|
546
629
|
React.createElement(Box, { marginBottom: 2 },
|
|
547
630
|
React.createElement(Spinner, { label: "Running evaluation" })),
|
package/dist/api/endpoint.js
CHANGED
|
@@ -25,6 +25,9 @@ export function normalizeEndpoint(endpoint) {
|
|
|
25
25
|
}
|
|
26
26
|
export function generatePayloadVariants(question) {
|
|
27
27
|
const variants = [];
|
|
28
|
+
variants.push({
|
|
29
|
+
messages: [{ role: 'user', content: question }]
|
|
30
|
+
});
|
|
28
31
|
variants.push({ message: question });
|
|
29
32
|
variants.push({ inputs: question });
|
|
30
33
|
variants.push({
|
|
@@ -39,9 +42,6 @@ export function generatePayloadVariants(question) {
|
|
|
39
42
|
variants.push({ input: question });
|
|
40
43
|
variants.push({ text: question });
|
|
41
44
|
variants.push({ user_message: question });
|
|
42
|
-
variants.push({
|
|
43
|
-
messages: [{ role: 'user', content: question }]
|
|
44
|
-
});
|
|
45
45
|
variants.push({ data: question });
|
|
46
46
|
variants.push({ content: question });
|
|
47
47
|
variants.push(question);
|
|
@@ -49,11 +49,17 @@ export function generatePayloadVariants(question) {
|
|
|
49
49
|
}
|
|
50
50
|
export function buildCustomPayload(template, question) {
|
|
51
51
|
try {
|
|
52
|
-
|
|
52
|
+
let replaced = template
|
|
53
|
+
.replace(/\[eval-question\]/g, question)
|
|
54
|
+
.replace(/\{\{question\}\}/g, question)
|
|
55
|
+
.replace(/\{question\}/g, question);
|
|
53
56
|
return JSON.parse(replaced);
|
|
54
57
|
}
|
|
55
58
|
catch (e) {
|
|
56
|
-
return template
|
|
59
|
+
return template
|
|
60
|
+
.replace(/\[eval-question\]/g, question)
|
|
61
|
+
.replace(/\{\{question\}\}/g, question)
|
|
62
|
+
.replace(/\{question\}/g, question);
|
|
57
63
|
}
|
|
58
64
|
}
|
|
59
65
|
export function createAxiosClient(customConfig) {
|
package/dist/api/evaluation.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export interface PromptEvaluationResult {
|
|
|
17
17
|
}
|
|
18
18
|
export declare function setBackendUrl(url: string): void;
|
|
19
19
|
export declare function generateApiKey(name?: string): Promise<any>;
|
|
20
|
-
export declare function createAgent(publicUrl: string): Promise<any>;
|
|
20
|
+
export declare function createAgent(publicUrl: string, customPayloadTemplate?: string): Promise<any>;
|
|
21
21
|
export declare function addTestPrompts(agentId: string, prompts?: string[] | Array<{
|
|
22
22
|
question: string;
|
|
23
23
|
answer?: string;
|
package/dist/api/evaluation.js
CHANGED
|
@@ -35,6 +35,8 @@ export async function generateApiKey(name) {
|
|
|
35
35
|
API_KEY = response.data.apiKey;
|
|
36
36
|
logger.info('API key generated successfully');
|
|
37
37
|
logger.debug('API Key:', API_KEY?.substring(0, 12) + '...');
|
|
38
|
+
const { setApiClient } = await import('./knowledge.js');
|
|
39
|
+
setApiClient(client, API_KEY);
|
|
38
40
|
return response.data;
|
|
39
41
|
}
|
|
40
42
|
catch (error) {
|
|
@@ -42,7 +44,7 @@ export async function generateApiKey(name) {
|
|
|
42
44
|
throw error;
|
|
43
45
|
}
|
|
44
46
|
}
|
|
45
|
-
export async function createAgent(publicUrl) {
|
|
47
|
+
export async function createAgent(publicUrl, customPayloadTemplate) {
|
|
46
48
|
try {
|
|
47
49
|
const response = await client.post('/api/agents', {
|
|
48
50
|
name: `Agent Eval ${Date.now()}`,
|
|
@@ -51,6 +53,22 @@ export async function createAgent(publicUrl) {
|
|
|
51
53
|
publicUrl: publicUrl,
|
|
52
54
|
label: 'eval',
|
|
53
55
|
});
|
|
56
|
+
const agentId = response.data.id;
|
|
57
|
+
const payloadTemplate = customPayloadTemplate || '{"message": "[eval-question]"}';
|
|
58
|
+
logger.debug(`Configuring advanced payload for agent ${agentId}`);
|
|
59
|
+
logger.debug(`Payload template: ${payloadTemplate}`);
|
|
60
|
+
try {
|
|
61
|
+
await client.patch(`/api/agents/${agentId}`, {
|
|
62
|
+
advancedPayload: {
|
|
63
|
+
payload: payloadTemplate
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
logger.debug('Advanced payload configured successfully');
|
|
67
|
+
}
|
|
68
|
+
catch (patchError) {
|
|
69
|
+
logger.warn('Could not set advanced payload:', patchError?.message);
|
|
70
|
+
logger.debug('Will use default payload format');
|
|
71
|
+
}
|
|
54
72
|
return response.data;
|
|
55
73
|
}
|
|
56
74
|
catch (error) {
|
package/dist/api/knowledge.d.ts
CHANGED
|
@@ -12,6 +12,12 @@ export declare function getTestResults(agentId: string): Promise<any>;
|
|
|
12
12
|
export interface EvaluationConfig {
|
|
13
13
|
agentEndpoint: string;
|
|
14
14
|
knowledgeSource?: string;
|
|
15
|
+
customConfig?: {
|
|
16
|
+
headers?: Record<string, string>;
|
|
17
|
+
bodyTemplate?: string;
|
|
18
|
+
responseField?: string;
|
|
19
|
+
method?: string;
|
|
20
|
+
};
|
|
15
21
|
}
|
|
16
22
|
export interface EvaluationResult {
|
|
17
23
|
totalTests: number;
|
|
@@ -21,6 +27,12 @@ export interface EvaluationResult {
|
|
|
21
27
|
evaluationUrl: string;
|
|
22
28
|
agentId?: string;
|
|
23
29
|
}
|
|
30
|
+
export declare function uploadPdfToAgent(agentId: string, pdfPath: string, onProgress?: (message: string) => void): Promise<{
|
|
31
|
+
qaPairs: Array<{
|
|
32
|
+
question: string;
|
|
33
|
+
answer: string;
|
|
34
|
+
}>;
|
|
35
|
+
}>;
|
|
24
36
|
export declare function runEvaluation(config: EvaluationConfig, onProgress?: (progress: number) => void): Promise<{
|
|
25
37
|
totalTests: any;
|
|
26
38
|
passed: number;
|
package/dist/api/knowledge.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import axios from 'axios';
|
|
4
|
+
import FormData from 'form-data';
|
|
4
5
|
import { logger } from '../utils/logger.js';
|
|
6
|
+
import { PdfUploadError, PdfValidationError } from '../errors/types.js';
|
|
5
7
|
let client = axios.create({
|
|
6
8
|
baseURL: 'https://rippletide-backend.azurewebsites.net',
|
|
7
9
|
headers: {
|
|
@@ -59,13 +61,75 @@ export async function getTestResults(agentId) {
|
|
|
59
61
|
return [];
|
|
60
62
|
}
|
|
61
63
|
}
|
|
64
|
+
export async function uploadPdfToAgent(agentId, pdfPath, onProgress) {
|
|
65
|
+
try {
|
|
66
|
+
if (!fs.existsSync(pdfPath)) {
|
|
67
|
+
throw new PdfValidationError(pdfPath, 'not_found');
|
|
68
|
+
}
|
|
69
|
+
const stats = fs.statSync(pdfPath);
|
|
70
|
+
const MAX_SIZE = 10 * 1024 * 1024;
|
|
71
|
+
if (stats.size > MAX_SIZE) {
|
|
72
|
+
throw new PdfValidationError(pdfPath, 'too_large', {
|
|
73
|
+
fileSize: stats.size,
|
|
74
|
+
maxSize: MAX_SIZE
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
const fileName = path.basename(pdfPath);
|
|
78
|
+
const fileBuffer = fs.readFileSync(pdfPath);
|
|
79
|
+
const fileSizeKB = Math.round(fileBuffer.length / 1024);
|
|
80
|
+
if (onProgress) {
|
|
81
|
+
onProgress(`Uploading ${fileName} (${fileSizeKB}KB) - this may take a few minutes...`);
|
|
82
|
+
}
|
|
83
|
+
const formData = new FormData();
|
|
84
|
+
formData.append('file', fileBuffer, {
|
|
85
|
+
filename: fileName,
|
|
86
|
+
contentType: 'application/pdf'
|
|
87
|
+
});
|
|
88
|
+
const response = await client.post(`/api/agents/${agentId}/upload-pdf`, formData, {
|
|
89
|
+
headers: {
|
|
90
|
+
...formData.getHeaders(),
|
|
91
|
+
...(API_KEY ? { 'x-api-key': API_KEY } : {})
|
|
92
|
+
},
|
|
93
|
+
maxBodyLength: Infinity,
|
|
94
|
+
maxContentLength: Infinity,
|
|
95
|
+
timeout: 300000 // 5 minutes timeout for PDF processing
|
|
96
|
+
});
|
|
97
|
+
if (!response.data.success) {
|
|
98
|
+
throw new PdfUploadError('upload', response.data.message || 'Upload failed', pdfPath);
|
|
99
|
+
}
|
|
100
|
+
if (onProgress) {
|
|
101
|
+
onProgress('Processing PDF and generating Q&A pairs...');
|
|
102
|
+
}
|
|
103
|
+
const qaPairs = response.data.processed?.qaPairs || [];
|
|
104
|
+
if (qaPairs.length === 0) {
|
|
105
|
+
throw new PdfUploadError('extract', 'No Q&A pairs generated from PDF', pdfPath);
|
|
106
|
+
}
|
|
107
|
+
if (onProgress) {
|
|
108
|
+
onProgress(`Generated ${qaPairs.length} Q&A pairs`);
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
qaPairs: qaPairs.map((pair) => ({
|
|
112
|
+
question: pair.question,
|
|
113
|
+
answer: pair.answer
|
|
114
|
+
}))
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (error instanceof PdfValidationError || error instanceof PdfUploadError) {
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
logger.error('Error uploading PDF:', error);
|
|
122
|
+
throw new PdfUploadError('upload', error.message || 'Unknown error', pdfPath, error);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
62
125
|
export async function runEvaluation(config, onProgress) {
|
|
63
126
|
const { generateApiKey, createAgent, addTestPrompts, runAllPromptEvaluations } = await import('./evaluation.js');
|
|
64
127
|
try {
|
|
65
128
|
const startTime = Date.now();
|
|
66
129
|
if (onProgress)
|
|
67
130
|
onProgress(10);
|
|
68
|
-
const
|
|
131
|
+
const payloadTemplate = config.customConfig?.bodyTemplate || '{"message": "[eval-question]"}';
|
|
132
|
+
const agent = await createAgent(config.agentEndpoint, payloadTemplate);
|
|
69
133
|
const agentId = agent.id;
|
|
70
134
|
if (onProgress)
|
|
71
135
|
onProgress(30);
|
package/dist/errors/types.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export declare enum ErrorCode {
|
|
|
13
13
|
DATABASE_ERROR = "DATABASE_ERROR",
|
|
14
14
|
PINECONE_ERROR = "PINECONE_ERROR",
|
|
15
15
|
POSTGRESQL_ERROR = "POSTGRESQL_ERROR",
|
|
16
|
+
PDF_UPLOAD_ERROR = "PDF_UPLOAD_ERROR",
|
|
17
|
+
PDF_VALIDATION_ERROR = "PDF_VALIDATION_ERROR",
|
|
16
18
|
EVALUATION_ERROR = "EVALUATION_ERROR",
|
|
17
19
|
UNKNOWN_ERROR = "UNKNOWN_ERROR"
|
|
18
20
|
}
|
|
@@ -39,6 +41,12 @@ export declare class FileError extends BaseError {
|
|
|
39
41
|
export declare class DatabaseError extends BaseError {
|
|
40
42
|
constructor(dbType: 'pinecone' | 'postgresql', operation: string, originalError?: any);
|
|
41
43
|
}
|
|
44
|
+
export declare class PdfUploadError extends BaseError {
|
|
45
|
+
constructor(operation: 'upload' | 'process' | 'extract', reason: string, filePath?: string, originalError?: any);
|
|
46
|
+
}
|
|
47
|
+
export declare class PdfValidationError extends BaseError {
|
|
48
|
+
constructor(filePath: string, reason: 'not_found' | 'too_large' | 'invalid_format' | 'no_permission', details?: any);
|
|
49
|
+
}
|
|
42
50
|
export declare class EvaluationError extends BaseError {
|
|
43
51
|
constructor(stage: string, reason: string, details?: any);
|
|
44
52
|
}
|
package/dist/errors/types.js
CHANGED
|
@@ -14,6 +14,8 @@ export var ErrorCode;
|
|
|
14
14
|
ErrorCode["DATABASE_ERROR"] = "DATABASE_ERROR";
|
|
15
15
|
ErrorCode["PINECONE_ERROR"] = "PINECONE_ERROR";
|
|
16
16
|
ErrorCode["POSTGRESQL_ERROR"] = "POSTGRESQL_ERROR";
|
|
17
|
+
ErrorCode["PDF_UPLOAD_ERROR"] = "PDF_UPLOAD_ERROR";
|
|
18
|
+
ErrorCode["PDF_VALIDATION_ERROR"] = "PDF_VALIDATION_ERROR";
|
|
17
19
|
ErrorCode["EVALUATION_ERROR"] = "EVALUATION_ERROR";
|
|
18
20
|
ErrorCode["UNKNOWN_ERROR"] = "UNKNOWN_ERROR";
|
|
19
21
|
})(ErrorCode || (ErrorCode = {}));
|
|
@@ -96,6 +98,27 @@ export class DatabaseError extends BaseError {
|
|
|
96
98
|
super(code, `${dbName} ${operation} failed: ${originalError?.message || 'Unknown error'}`, `Failed to ${operation} with ${dbName}. Please check your connection details and try again.`, true, { dbType, operation, originalError });
|
|
97
99
|
}
|
|
98
100
|
}
|
|
101
|
+
export class PdfUploadError extends BaseError {
|
|
102
|
+
constructor(operation, reason, filePath, originalError) {
|
|
103
|
+
const userMessages = {
|
|
104
|
+
upload: `Failed to upload PDF: ${reason}`,
|
|
105
|
+
process: `Failed to process PDF: ${reason}`,
|
|
106
|
+
extract: `Failed to extract Q&A from PDF: ${reason}`
|
|
107
|
+
};
|
|
108
|
+
super(ErrorCode.PDF_UPLOAD_ERROR, `PDF ${operation} error: ${reason}`, userMessages[operation], operation === 'upload', { filePath, operation, originalError });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
export class PdfValidationError extends BaseError {
|
|
112
|
+
constructor(filePath, reason, details) {
|
|
113
|
+
const userMessages = {
|
|
114
|
+
not_found: `PDF file not found: ${filePath}`,
|
|
115
|
+
too_large: `PDF file is too large (max 10MB): ${filePath}`,
|
|
116
|
+
invalid_format: `File is not a valid PDF: ${filePath}`,
|
|
117
|
+
no_permission: `No permission to read PDF file: ${filePath}`
|
|
118
|
+
};
|
|
119
|
+
super(ErrorCode.PDF_VALIDATION_ERROR, `PDF validation error: ${reason} - ${filePath}`, userMessages[reason], false, { filePath, reason, ...details });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
99
122
|
export class EvaluationError extends BaseError {
|
|
100
123
|
constructor(stage, reason, details) {
|
|
101
124
|
super(ErrorCode.EVALUATION_ERROR, `Evaluation failed at ${stage}: ${reason}`, `Evaluation could not complete: ${reason}`, false, { stage, ...details });
|
package/dist/index.js
CHANGED
|
@@ -65,6 +65,10 @@ const parseArgs = () => {
|
|
|
65
65
|
options.knowledgeSource = args[i + 1];
|
|
66
66
|
i++;
|
|
67
67
|
}
|
|
68
|
+
else if ((args[i] === '--pdf-path' || args[i] === '-pp') && args[i + 1]) {
|
|
69
|
+
options.pdfPath = args[i + 1];
|
|
70
|
+
i++;
|
|
71
|
+
}
|
|
68
72
|
else if ((args[i] === '--pinecone-url' || args[i] === '-pu') && args[i + 1]) {
|
|
69
73
|
options.pineconeUrl = args[i + 1];
|
|
70
74
|
i++;
|
|
@@ -116,7 +120,7 @@ Commands:
|
|
|
116
120
|
Options:
|
|
117
121
|
-t, --template <name> Use a pre-configured template
|
|
118
122
|
-a, --agent <url> Agent endpoint URL (e.g., localhost:8000)
|
|
119
|
-
-k, --knowledge <source> Knowledge source: files, pinecone, or
|
|
123
|
+
-k, --knowledge <source> Knowledge source: files, pinecone, postgresql, or pdf (default: files)
|
|
120
124
|
-b, --backend-url <url> Backend API URL (default: https://rippletide-backend.azurewebsites.net)
|
|
121
125
|
-d, --dashboard-url <url> Dashboard URL (default: https://eval.rippletide.com)
|
|
122
126
|
|
|
@@ -127,6 +131,9 @@ Options:
|
|
|
127
131
|
PostgreSQL options:
|
|
128
132
|
-pg, --postgresql <conn> PostgreSQL connection string or comma-separated values
|
|
129
133
|
|
|
134
|
+
PDF options:
|
|
135
|
+
-pp, --pdf-path <path> Path to PDF file for knowledge extraction
|
|
136
|
+
|
|
130
137
|
Custom endpoint options:
|
|
131
138
|
-H, --headers <headers> Custom headers (e.g., "Authorization: Bearer token, X-API-Key: key")
|
|
132
139
|
-B, --body <template> Custom body template (use {question} as placeholder)
|
|
@@ -155,6 +162,9 @@ Examples:
|
|
|
155
162
|
# Direct evaluation with PostgreSQL
|
|
156
163
|
rippletide eval -a localhost:8000 -k postgresql -pg "postgresql://user:pass@localhost:5432/db"
|
|
157
164
|
|
|
165
|
+
# Direct evaluation with PDF
|
|
166
|
+
rippletide eval -a localhost:8000 -k pdf -pp ./docs/manual.pdf
|
|
167
|
+
|
|
158
168
|
# With custom headers and body
|
|
159
169
|
rippletide eval -a localhost:8000 -H "Authorization: Bearer token" -B '{"prompt": "{question}"}'
|
|
160
170
|
`);
|
|
@@ -170,7 +180,7 @@ async function run() {
|
|
|
170
180
|
try {
|
|
171
181
|
const options = parseArgs();
|
|
172
182
|
process.stdout.write('\x1Bc');
|
|
173
|
-
const { waitUntilExit } = render(React.createElement(App, { backendUrl: options.backendUrl, dashboardUrl: options.dashboardUrl, nonInteractive: options.nonInteractive, agentEndpoint: options.agentEndpoint, knowledgeSource: options.knowledgeSource, pineconeUrl: options.pineconeUrl, pineconeApiKey: options.pineconeApiKey, postgresqlConnection: options.postgresqlConnection, customHeaders: options.headers, customBodyTemplate: options.bodyTemplate, customResponseField: options.responseField, templatePath: options.templatePath }));
|
|
183
|
+
const { waitUntilExit } = render(React.createElement(App, { backendUrl: options.backendUrl, dashboardUrl: options.dashboardUrl, nonInteractive: options.nonInteractive, agentEndpoint: options.agentEndpoint, knowledgeSource: options.knowledgeSource, pineconeUrl: options.pineconeUrl, pineconeApiKey: options.pineconeApiKey, postgresqlConnection: options.postgresqlConnection, customHeaders: options.headers, customBodyTemplate: options.bodyTemplate, customResponseField: options.responseField, templatePath: options.templatePath, pdfPath: options.pdfPath }));
|
|
174
184
|
await waitUntilExit();
|
|
175
185
|
}
|
|
176
186
|
catch (error) {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface QAPair {
|
|
2
|
+
question: string;
|
|
3
|
+
answer: string;
|
|
4
|
+
}
|
|
5
|
+
export interface PdfUploadResponse {
|
|
6
|
+
success: boolean;
|
|
7
|
+
message: string;
|
|
8
|
+
file?: {
|
|
9
|
+
originalName: string;
|
|
10
|
+
size: number;
|
|
11
|
+
mimetype: string;
|
|
12
|
+
};
|
|
13
|
+
processed?: {
|
|
14
|
+
totalChunks: number;
|
|
15
|
+
chunks: Array<{
|
|
16
|
+
text: string;
|
|
17
|
+
type: string;
|
|
18
|
+
}>;
|
|
19
|
+
qaPairs: Array<{
|
|
20
|
+
question: string;
|
|
21
|
+
answer: string;
|
|
22
|
+
sourceChunk?: string;
|
|
23
|
+
chunkType?: string;
|
|
24
|
+
}>;
|
|
25
|
+
totalQAPairs: number;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export declare function getPdfQAndA(pdfPath: string, agentId: string, backendUrl: string, onProgress?: (message: string) => void): Promise<QAPair[]>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import FormData from 'form-data';
|
|
4
|
+
import axios from 'axios';
|
|
5
|
+
import { PdfUploadError, PdfValidationError } from '../errors/types.js';
|
|
6
|
+
import { logger } from './logger.js';
|
|
7
|
+
const MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
8
|
+
function validatePdfFile(filePath) {
|
|
9
|
+
if (!fs.existsSync(filePath)) {
|
|
10
|
+
throw new PdfValidationError(filePath, 'not_found');
|
|
11
|
+
}
|
|
12
|
+
const stats = fs.statSync(filePath);
|
|
13
|
+
if (!stats.isFile()) {
|
|
14
|
+
throw new PdfValidationError(filePath, 'invalid_format', { reason: 'Path is not a file' });
|
|
15
|
+
}
|
|
16
|
+
if (stats.size > MAX_FILE_SIZE) {
|
|
17
|
+
throw new PdfValidationError(filePath, 'too_large', {
|
|
18
|
+
fileSize: stats.size,
|
|
19
|
+
maxSize: MAX_FILE_SIZE
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
fs.accessSync(filePath, fs.constants.R_OK);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
throw new PdfValidationError(filePath, 'no_permission');
|
|
27
|
+
}
|
|
28
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
29
|
+
if (ext !== '.pdf') {
|
|
30
|
+
throw new PdfValidationError(filePath, 'invalid_format', {
|
|
31
|
+
extension: ext,
|
|
32
|
+
expected: '.pdf'
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export async function getPdfQAndA(pdfPath, agentId, backendUrl, onProgress) {
|
|
37
|
+
try {
|
|
38
|
+
if (onProgress)
|
|
39
|
+
onProgress('Validating PDF file...');
|
|
40
|
+
validatePdfFile(pdfPath);
|
|
41
|
+
const fileName = path.basename(pdfPath);
|
|
42
|
+
const fileBuffer = fs.readFileSync(pdfPath);
|
|
43
|
+
if (onProgress)
|
|
44
|
+
onProgress(`Uploading ${fileName} (${Math.round(fileBuffer.length / 1024)}KB)...`);
|
|
45
|
+
const formData = new FormData();
|
|
46
|
+
formData.append('file', fileBuffer, {
|
|
47
|
+
filename: fileName,
|
|
48
|
+
contentType: 'application/pdf'
|
|
49
|
+
});
|
|
50
|
+
const uploadUrl = `${backendUrl}/api/agents/${agentId}/upload-pdf`;
|
|
51
|
+
logger.debug(`Uploading PDF to: ${uploadUrl}`);
|
|
52
|
+
logger.debug(`File: ${fileName}, Size: ${fileBuffer.length} bytes`);
|
|
53
|
+
const response = await axios.post(uploadUrl, formData, {
|
|
54
|
+
headers: {
|
|
55
|
+
...formData.getHeaders()
|
|
56
|
+
},
|
|
57
|
+
maxBodyLength: Infinity,
|
|
58
|
+
maxContentLength: Infinity,
|
|
59
|
+
timeout: 300000 // 5 minutes timeout for PDF processing
|
|
60
|
+
});
|
|
61
|
+
if (!response.data.success) {
|
|
62
|
+
throw new PdfUploadError('upload', response.data.message || 'Upload failed', pdfPath);
|
|
63
|
+
}
|
|
64
|
+
if (onProgress)
|
|
65
|
+
onProgress('Processing PDF content...');
|
|
66
|
+
if (!response.data.processed || !response.data.processed.qaPairs) {
|
|
67
|
+
throw new PdfUploadError('extract', 'No Q&A pairs generated from PDF', pdfPath);
|
|
68
|
+
}
|
|
69
|
+
const qaPairs = response.data.processed.qaPairs;
|
|
70
|
+
if (onProgress) {
|
|
71
|
+
onProgress(`Generated ${qaPairs.length} Q&A pairs from ${response.data.processed.totalChunks} chunks`);
|
|
72
|
+
}
|
|
73
|
+
return qaPairs.map(pair => ({
|
|
74
|
+
question: pair.question,
|
|
75
|
+
answer: pair.answer
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
if (error instanceof PdfValidationError || error instanceof PdfUploadError) {
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
if (error.response) {
|
|
83
|
+
const status = error.response.status;
|
|
84
|
+
const message = error.response.data?.message || error.response.statusText;
|
|
85
|
+
if (status === 413) {
|
|
86
|
+
throw new PdfValidationError(pdfPath, 'too_large', {
|
|
87
|
+
serverMessage: message
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
throw new PdfUploadError('upload', `Server error (${status}): ${message}`, pdfPath, error);
|
|
91
|
+
}
|
|
92
|
+
if (error.code === 'ECONNREFUSED') {
|
|
93
|
+
throw new PdfUploadError('upload', 'Cannot connect to backend server', pdfPath, error);
|
|
94
|
+
}
|
|
95
|
+
if (error.code === 'ETIMEDOUT') {
|
|
96
|
+
throw new PdfUploadError('upload', 'Upload timed out - file may be too large', pdfPath, error);
|
|
97
|
+
}
|
|
98
|
+
throw new PdfUploadError('upload', error.message || 'Unknown error', pdfPath, error);
|
|
99
|
+
}
|
|
100
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rippletide",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.12",
|
|
4
4
|
"description": "Rippletide Evaluation CLI",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -33,8 +33,10 @@
|
|
|
33
33
|
"axios": "^1.13.2",
|
|
34
34
|
"chalk": "^5.3.0",
|
|
35
35
|
"drizzle-orm": "^0.38.3",
|
|
36
|
+
"form-data": "^4.0.0",
|
|
36
37
|
"ink": "^5.0.1",
|
|
37
38
|
"ink-text-input": "^6.0.0",
|
|
39
|
+
"mime-types": "^2.1.35",
|
|
38
40
|
"node-fetch": "^3.3.2",
|
|
39
41
|
"pg": "^8.13.1",
|
|
40
42
|
"react": "^18.3.1",
|
|
@@ -48,6 +50,7 @@
|
|
|
48
50
|
"@types/node-fetch": "^2.6.13",
|
|
49
51
|
"@types/pg": "^8.16.0",
|
|
50
52
|
"@types/react": "^18.3.18",
|
|
53
|
+
"@types/mime-types": "^2.1.4",
|
|
51
54
|
"tsx": "^4.19.2",
|
|
52
55
|
"typescript": "^5.7.2"
|
|
53
56
|
}
|