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 CHANGED
@@ -8,6 +8,7 @@ interface AppProps {
8
8
  pineconeUrl?: string;
9
9
  pineconeApiKey?: string;
10
10
  postgresqlConnection?: string;
11
+ pdfPath?: string;
11
12
  customHeaders?: Record<string, string>;
12
13
  customBodyTemplate?: string;
13
14
  customResponseField?: string;
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
- const agent = await api.createAgent(agentEndpoint);
233
- const agentId = agent.id;
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
- }, customConfig);
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" })),
@@ -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
- const replaced = template.replace(/{question}/g, question);
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.replace(/{question}/g, question);
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) {
@@ -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;
@@ -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) {
@@ -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;
@@ -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 agent = await createAgent(config.agentEndpoint);
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);
@@ -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
  }
@@ -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 postgresql (default: files)
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.10",
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
  }