rippletide 1.0.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.
@@ -0,0 +1,403 @@
1
+ import axios from 'axios';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { logger } from '../utils/logger.js';
5
+
6
+ const BASE_URL = 'http://localhost:3001';
7
+
8
+ let API_KEY: string | null = null;
9
+
10
+ const client = axios.create({
11
+ baseURL: BASE_URL,
12
+ headers: {
13
+ 'Content-Type': 'application/json',
14
+ },
15
+ });
16
+
17
+ client.interceptors.request.use((config) => {
18
+ if (API_KEY) {
19
+ config.headers['x-api-key'] = API_KEY;
20
+ }
21
+ return config;
22
+ });
23
+
24
+ export interface EvaluationConfig {
25
+ agentEndpoint: string;
26
+ knowledgeSource?: string;
27
+ }
28
+
29
+ export interface EvaluationResult {
30
+ totalTests: number;
31
+ passed: number;
32
+ failed: number;
33
+ duration: string;
34
+ evaluationUrl: string;
35
+ agentId?: string;
36
+ }
37
+
38
+ export interface HallucinationCheckResult {
39
+ question: string;
40
+ llmResponse: string;
41
+ summary: string;
42
+ facts: string[];
43
+ status: 'passed' | 'failed' | 'ambiguous';
44
+ hallucinationLabel: string;
45
+ hallucinationFindings: any[];
46
+ }
47
+
48
+ export interface PromptEvaluationResult {
49
+ success: boolean;
50
+ question: string;
51
+ llmResponse?: string;
52
+ hallucinationResult?: HallucinationCheckResult;
53
+ error?: any;
54
+ }
55
+
56
+ export const api = {
57
+ async generateApiKey(name?: string) {
58
+ try {
59
+ const response = await client.post('/api/api-keys/generate-cli', {
60
+ name: name || 'CLI Evaluation Key'
61
+ });
62
+
63
+ API_KEY = response.data.apiKey;
64
+ logger.info('API key generated successfully');
65
+ logger.debug('API Key:', API_KEY?.substring(0, 12) + '...');
66
+
67
+ return response.data;
68
+ } catch (error) {
69
+ logger.error('Error generating API key:', error);
70
+ throw error;
71
+ }
72
+ },
73
+
74
+ async healthCheck() {
75
+ const response = await client.get('/health');
76
+ return response.data;
77
+ },
78
+
79
+ async checkKnowledge(folderPath: string = '.') {
80
+ try {
81
+ const knowledgeFiles = [
82
+ 'knowledge-base/qanda.json',
83
+ 'qanda.json',
84
+ 'knowledge.json',
85
+ ];
86
+
87
+ for (const file of knowledgeFiles) {
88
+ const filePath = path.join(folderPath, file);
89
+ if (fs.existsSync(filePath)) {
90
+ return { found: true, path: filePath };
91
+ }
92
+ }
93
+
94
+ return { found: false };
95
+ } catch (error) {
96
+ logger.error('Error checking knowledge:', error);
97
+ return { found: false };
98
+ }
99
+ },
100
+
101
+ async createAgent(publicUrl: string) {
102
+ try {
103
+ const response = await client.post('/api/agents', {
104
+ name: `Agent Eval ${Date.now()}`,
105
+ seed: Math.floor(Math.random() * 1000),
106
+ numNodes: 100,
107
+ publicUrl: publicUrl,
108
+ label: 'eval',
109
+ });
110
+
111
+ return response.data;
112
+ } catch (error) {
113
+ logger.error('Error creating agent:', error);
114
+ throw error;
115
+ }
116
+ },
117
+
118
+ async importKnowledge(agentId: string, knowledgeData: any) {
119
+ try {
120
+ const response = await client.post(`/api/agents/${agentId}/knowledge/import`, {
121
+ data: knowledgeData,
122
+ });
123
+
124
+ return response.data;
125
+ } catch (error) {
126
+ logger.error('Error importing knowledge:', error);
127
+ return null;
128
+ }
129
+ },
130
+
131
+ async addTestPrompts(agentId: string, prompts?: string[] | Array<{question: string, answer?: string}>) {
132
+ try {
133
+ const defaultPrompts = [
134
+ 'What can you help me with?',
135
+ 'Tell me about your capabilities',
136
+ 'How do I get started?',
137
+ 'What features do you support?',
138
+ 'Can you explain your main functionality?',
139
+ ];
140
+
141
+ let promptsArray: Array<{prompt: string, expectedAnswer: string | null}>;
142
+
143
+ if (!prompts || prompts.length === 0) {
144
+ promptsArray = defaultPrompts.map(p => ({ prompt: p, expectedAnswer: null }));
145
+ } else if (typeof prompts[0] === 'string') {
146
+ promptsArray = (prompts as string[]).map(p => ({ prompt: p, expectedAnswer: null }));
147
+ } else {
148
+ promptsArray = (prompts as Array<{question: string, answer?: string}>).map(p => ({
149
+ prompt: p.question,
150
+ expectedAnswer: p.answer || null,
151
+ }));
152
+ }
153
+
154
+ const response = await client.post(`/api/agents/${agentId}/test-prompts`, {
155
+ prompts: promptsArray,
156
+ });
157
+
158
+ return response.data;
159
+ } catch (error: any) {
160
+ logger.error('Error adding test prompts:', error);
161
+ if (error.response) {
162
+ logger.debug('Response data:', error.response.data);
163
+ logger.debug('Response status:', error.response.status);
164
+ }
165
+ throw error;
166
+ }
167
+ },
168
+
169
+ async checkHallucination(agentId: string, question: string, llmResponse: string, expectedAnswer?: string): Promise<HallucinationCheckResult> {
170
+ const response = await client.post(`/api/agents/${agentId}/hallucination`, {
171
+ question,
172
+ llmResponse,
173
+ expectedAnswer
174
+ });
175
+ return response.data;
176
+ },
177
+
178
+ async callLLMEndpoint(agentEndpoint: string, question: string): Promise<string> {
179
+ try {
180
+ const llmClient = axios.create({
181
+ timeout: 60000,
182
+ });
183
+
184
+ const response = await llmClient.post(agentEndpoint, {
185
+ message: question,
186
+ query: question,
187
+ question: question,
188
+ prompt: question,
189
+ });
190
+
191
+ let llmResponse = '';
192
+ if (typeof response.data === 'string') {
193
+ llmResponse = response.data;
194
+ } else if (response.data.response) {
195
+ llmResponse = response.data.response;
196
+ } else if (response.data.message) {
197
+ llmResponse = response.data.message;
198
+ } else if (response.data.answer) {
199
+ llmResponse = response.data.answer;
200
+ } else if (response.data.text) {
201
+ llmResponse = response.data.text;
202
+ } else {
203
+ llmResponse = JSON.stringify(response.data);
204
+ }
205
+
206
+ return llmResponse;
207
+ } catch (error: any) {
208
+ logger.error('Error calling LLM endpoint:', error?.message || error);
209
+ throw new Error(`Failed to call LLM endpoint: ${error?.message || 'Unknown error'}`);
210
+ }
211
+ },
212
+
213
+ async runPromptEvaluation(
214
+ agentId: string,
215
+ promptId: number,
216
+ promptText: string,
217
+ agentEndpoint: string,
218
+ expectedAnswer?: string,
219
+ onLLMResponse?: (response: string) => void
220
+ ): Promise<PromptEvaluationResult> {
221
+ try {
222
+ logger.info(`Calling LLM for question: ${promptText}`);
223
+ const llmResponse = await api.callLLMEndpoint(agentEndpoint, promptText);
224
+
225
+ if (onLLMResponse) {
226
+ onLLMResponse(llmResponse);
227
+ }
228
+
229
+ logger.info(`LLM Response: ${llmResponse.substring(0, 100)}...`);
230
+
231
+ const hallucinationResult = await api.checkHallucination(
232
+ agentId,
233
+ promptText,
234
+ llmResponse,
235
+ expectedAnswer
236
+ );
237
+
238
+ const status = hallucinationResult.status === 'passed' ? 'passed' : 'failed';
239
+ await client.post(`/api/agents/${agentId}/test-results/${promptId}`, {
240
+ status,
241
+ response: llmResponse,
242
+ hallucinationLabel: hallucinationResult.hallucinationLabel,
243
+ hallucinationFindings: hallucinationResult.hallucinationFindings
244
+ });
245
+
246
+ return {
247
+ success: status === 'passed',
248
+ question: promptText,
249
+ llmResponse,
250
+ hallucinationResult
251
+ };
252
+ } catch (error: any) {
253
+ logger.debug(`Error running prompt ${promptId}:`, error?.response?.data || error.message);
254
+
255
+ try {
256
+ await client.post(`/api/agents/${agentId}/test-results/${promptId}`, {
257
+ status: 'failed'
258
+ });
259
+ } catch (e) {
260
+ logger.debug('Failed to store failed result:', e);
261
+ }
262
+
263
+ return {
264
+ success: false,
265
+ question: promptText,
266
+ error
267
+ };
268
+ }
269
+ },
270
+
271
+ async runAllPromptEvaluations(
272
+ agentId: string,
273
+ prompts: any[],
274
+ agentEndpoint: string,
275
+ onProgress?: (current: number, total: number, question?: string, llmResponse?: string) => void
276
+ ) {
277
+ const results: PromptEvaluationResult[] = [];
278
+ try {
279
+ for (let i = 0; i < prompts.length; i++) {
280
+ const prompt = prompts[i];
281
+
282
+ if (onProgress) {
283
+ onProgress(i + 1, prompts.length, prompt.prompt);
284
+ }
285
+
286
+ const result = await api.runPromptEvaluation(
287
+ agentId,
288
+ prompt.id,
289
+ prompt.prompt,
290
+ agentEndpoint,
291
+ prompt.expectedAnswer,
292
+ (llmResponse) => {
293
+ if (onProgress) {
294
+ onProgress(i + 1, prompts.length, prompt.prompt, llmResponse);
295
+ }
296
+ }
297
+ );
298
+
299
+ results.push(result);
300
+
301
+ await new Promise(resolve => setTimeout(resolve, 500));
302
+ }
303
+ } catch (error) {
304
+ logger.error('Error running evaluations:', error);
305
+ }
306
+ return results;
307
+ },
308
+
309
+ async getTestResults(agentId: string) {
310
+ try {
311
+ const response = await client.get(`/api/agents/${agentId}/test-results`);
312
+ return response.data;
313
+ } catch (error) {
314
+ console.error('Error getting test results:', error);
315
+ return [];
316
+ }
317
+ },
318
+
319
+ async runEvaluation(config: EvaluationConfig, onProgress?: (progress: number) => void) {
320
+ try {
321
+ const startTime = Date.now();
322
+
323
+ if (onProgress) onProgress(10);
324
+ const agent = await api.createAgent(config.agentEndpoint);
325
+ const agentId = agent.id;
326
+
327
+ if (onProgress) onProgress(30);
328
+ if (config.knowledgeSource === 'files') {
329
+ const knowledgeResult = await api.checkKnowledge();
330
+ if (knowledgeResult.found && knowledgeResult.path) {
331
+ const knowledgeData = JSON.parse(fs.readFileSync(knowledgeResult.path, 'utf-8'));
332
+ await api.importKnowledge(agentId, knowledgeData);
333
+ }
334
+ }
335
+
336
+ if (onProgress) onProgress(40);
337
+
338
+ let testPrompts: string[] = [];
339
+ if (config.knowledgeSource === 'files') {
340
+ const knowledgeResult = await api.checkKnowledge();
341
+ if (knowledgeResult.found && knowledgeResult.path) {
342
+ try {
343
+ const knowledgeData = JSON.parse(fs.readFileSync(knowledgeResult.path, 'utf-8'));
344
+ if (Array.isArray(knowledgeData)) {
345
+ testPrompts = knowledgeData.slice(0, 5).map((item: any) =>
346
+ item.question || item.prompt || item.input || 'Test question'
347
+ );
348
+ } else if (knowledgeData.questions) {
349
+ testPrompts = knowledgeData.questions.slice(0, 5);
350
+ }
351
+ } catch (error) {
352
+ logger.error('Error loading prompts from knowledge:', error);
353
+ }
354
+ }
355
+ }
356
+
357
+ const createdPrompts = await api.addTestPrompts(agentId, testPrompts);
358
+ const promptIds = createdPrompts.map((p: any) => p.id);
359
+ const promptCount = promptIds.length;
360
+
361
+ if (onProgress) onProgress(50);
362
+ const evaluationResults = await api.runAllPromptEvaluations(
363
+ agentId,
364
+ createdPrompts,
365
+ config.agentEndpoint,
366
+ (current, total) => {
367
+ const progress = 50 + Math.round((current / total) * 40);
368
+ if (onProgress) onProgress(progress);
369
+ }
370
+ );
371
+
372
+ if (onProgress) onProgress(100);
373
+
374
+ let passed = 0;
375
+ let failed = 0;
376
+
377
+ evaluationResults.forEach((result: PromptEvaluationResult) => {
378
+ if (result.success) {
379
+ passed++;
380
+ } else {
381
+ failed++;
382
+ }
383
+ });
384
+
385
+ const duration = Math.round((Date.now() - startTime) / 1000);
386
+ const durationStr = duration > 60
387
+ ? `${Math.floor(duration / 60)}m ${duration % 60}s`
388
+ : `${duration}s`;
389
+
390
+ return {
391
+ totalTests: promptCount || 5,
392
+ passed,
393
+ failed,
394
+ duration: durationStr,
395
+ evaluationUrl: `http://localhost:5173/eval/${agentId}`,
396
+ agentId,
397
+ };
398
+ } catch (error) {
399
+ logger.error('Evaluation error:', error);
400
+ throw error;
401
+ }
402
+ },
403
+ };
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+
4
+ export const Header: React.FC = () => {
5
+ return (
6
+ <Box flexDirection="column" marginBottom={2}>
7
+ <Text bold color="#eba1b5">Rippletide Evaluation</Text>
8
+ <Text color="gray">━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</Text>
9
+ </Box>
10
+ );
11
+ };
@@ -0,0 +1,29 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+
4
+ interface ProgressBarProps {
5
+ progress: number; // 0-100
6
+ label?: string;
7
+ }
8
+
9
+ export const ProgressBar: React.FC<ProgressBarProps> = ({ progress, label }) => {
10
+ const width = 40;
11
+ const filled = Math.round((progress / 100) * width);
12
+ const empty = width - filled;
13
+
14
+ return (
15
+ <Box flexDirection="column" marginY={1}>
16
+ {label && (
17
+ <Box marginBottom={1}>
18
+ <Text dimColor>{label}</Text>
19
+ </Box>
20
+ )}
21
+ <Box>
22
+ <Text color="#eba1b5">{'='.repeat(filled)}</Text>
23
+ <Text color="gray">{'-'.repeat(empty)}</Text>
24
+ <Text> </Text>
25
+ <Text color="#eba1b5">{progress.toFixed(0)}%</Text>
26
+ </Box>
27
+ </Box>
28
+ );
29
+ };
@@ -0,0 +1,81 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+
4
+ interface SelectOption {
5
+ label: string;
6
+ value: string;
7
+ description?: string;
8
+ disabled?: boolean;
9
+ }
10
+
11
+ interface SelectMenuProps {
12
+ title: string;
13
+ options: SelectOption[];
14
+ onSelect: (value: string) => void;
15
+ }
16
+
17
+ export const SelectMenu: React.FC<SelectMenuProps> = ({ title, options, onSelect }) => {
18
+ const [selectedIndex, setSelectedIndex] = useState(0);
19
+
20
+ const getNextEnabledIndex = (start: number, direction: 1 | -1) => {
21
+ for (let i = 1; i <= options.length; i++) {
22
+ const idx = (start + direction * i + options.length) % options.length;
23
+ if (!options[idx].disabled) {
24
+ return idx;
25
+ }
26
+ }
27
+ return start;
28
+ };
29
+
30
+ useInput((input, key) => {
31
+ if (key.upArrow) {
32
+ setSelectedIndex((prev) => getNextEnabledIndex(prev, -1));
33
+ } else if (key.downArrow) {
34
+ setSelectedIndex((prev) => getNextEnabledIndex(prev, 1));
35
+ } else if (key.return) {
36
+ const option = options[selectedIndex];
37
+ if (!option.disabled) {
38
+ onSelect(option.value);
39
+ }
40
+ }
41
+ });
42
+
43
+ return (
44
+ <Box flexDirection="column">
45
+ <Box marginBottom={1}>
46
+ <Text color="white" dimColor>{title}</Text>
47
+ </Box>
48
+ {options.map((option, index) => (
49
+ <Box key={option.value} flexDirection="column" marginBottom={0}>
50
+ <Box>
51
+ <Text color={option.disabled ? '#d0c0cf' : 'white'}>
52
+ {index === selectedIndex ? '> ' : ' '}
53
+ </Text>
54
+ <Text
55
+ color={
56
+ option.disabled
57
+ ? '#d0c0cf'
58
+ : index === selectedIndex
59
+ ? '#eba1b5'
60
+ : 'white'
61
+ }
62
+ bold={index === selectedIndex && !option.disabled}
63
+ >
64
+ {option.label}
65
+ </Text>
66
+ </Box>
67
+ {option.description && (
68
+ <Box paddingLeft={2}>
69
+ <Text color={option.disabled ? '#d0c0cf' : 'white'} dimColor={option.disabled}> {option.description}</Text>
70
+ </Box>
71
+ )}
72
+ </Box>
73
+ ))}
74
+ <Box marginTop={1}>
75
+ <Text color="white" dimColor>
76
+ Up/Down to navigate - Enter to select (gray = coming soon)
77
+ </Text>
78
+ </Box>
79
+ </Box>
80
+ );
81
+ };
@@ -0,0 +1,29 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Box, Text } from 'ink';
3
+
4
+ interface SpinnerProps {
5
+ label: string;
6
+ }
7
+
8
+ const spinnerFrames = ['|', '/', '-', '\\'];
9
+
10
+ export const Spinner: React.FC<SpinnerProps> = ({ label }) => {
11
+ const [frame, setFrame] = useState(0);
12
+
13
+ useEffect(() => {
14
+ const timer = setInterval(() => {
15
+ setFrame((prevFrame) => (prevFrame + 1) % spinnerFrames.length);
16
+ }, 100);
17
+
18
+ return () => {
19
+ clearInterval(timer);
20
+ };
21
+ }, []);
22
+
23
+ return (
24
+ <Box>
25
+ <Text color="#eba1b5">{spinnerFrames[frame]} </Text>
26
+ <Text dimColor>{label}</Text>
27
+ </Box>
28
+ );
29
+ };
@@ -0,0 +1,70 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+
4
+ interface SummaryProps {
5
+ totalTests: number;
6
+ passed: number;
7
+ failed: number;
8
+ duration: string;
9
+ evaluationUrl: string;
10
+ }
11
+
12
+ export const Summary: React.FC<SummaryProps> = ({
13
+ totalTests,
14
+ passed,
15
+ failed,
16
+ duration,
17
+ evaluationUrl
18
+ }) => {
19
+ const passRate = ((passed / totalTests) * 100).toFixed(1);
20
+
21
+ return (
22
+ <Box flexDirection="column">
23
+ <Box marginBottom={2}>
24
+ <Text bold color="#eba1b5">Evaluation Complete</Text>
25
+ </Box>
26
+
27
+ <Box flexDirection="column" marginBottom={2}>
28
+ <Box>
29
+ <Box width={12}>
30
+ <Text dimColor>Tests:</Text>
31
+ </Box>
32
+ <Text>{totalTests}</Text>
33
+ </Box>
34
+
35
+ <Box>
36
+ <Box width={12}>
37
+ <Text dimColor>Passed:</Text>
38
+ </Box>
39
+ <Text>{passed}</Text>
40
+ </Box>
41
+
42
+ <Box>
43
+ <Box width={12}>
44
+ <Text dimColor>Failed:</Text>
45
+ </Box>
46
+ <Text>{failed}</Text>
47
+ </Box>
48
+
49
+ <Box>
50
+ <Box width={12}>
51
+ <Text dimColor>Success:</Text>
52
+ </Box>
53
+ <Text>{passRate}%</Text>
54
+ </Box>
55
+
56
+ <Box>
57
+ <Box width={12}>
58
+ <Text dimColor>Duration:</Text>
59
+ </Box>
60
+ <Text>{duration}</Text>
61
+ </Box>
62
+ </Box>
63
+
64
+ <Box flexDirection="column">
65
+ <Text dimColor>Full results:</Text>
66
+ <Text color="#eba1b5">{evaluationUrl}</Text>
67
+ </Box>
68
+ </Box>
69
+ );
70
+ };
@@ -0,0 +1,30 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import InkTextInput from 'ink-text-input';
4
+
5
+ interface TextInputProps {
6
+ label: string;
7
+ onSubmit: (value: string) => void;
8
+ placeholder?: string;
9
+ }
10
+
11
+ export const TextInput: React.FC<TextInputProps> = ({ label, onSubmit, placeholder }) => {
12
+ const [value, setValue] = useState('');
13
+
14
+ return (
15
+ <Box flexDirection="column">
16
+ <Box marginBottom={1}>
17
+ <Text dimColor>{label}</Text>
18
+ </Box>
19
+ <Box>
20
+ <Text color="#eba1b5">&gt; </Text>
21
+ <InkTextInput
22
+ value={value}
23
+ onChange={setValue}
24
+ onSubmit={() => onSubmit(value)}
25
+ placeholder={placeholder}
26
+ />
27
+ </Box>
28
+ </Box>
29
+ );
30
+ };
package/src/index.tsx ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import React from 'react';
3
+ import { render } from 'ink';
4
+ import { App } from './App.js';
5
+
6
+ process.stdout.write('\x1Bc');
7
+
8
+ render(<App />);
@@ -0,0 +1,21 @@
1
+ const isDebugMode = process.argv.includes('--debug');
2
+
3
+ export const logger = {
4
+ debug: (...args: any[]) => {
5
+ if (isDebugMode) {
6
+ console.log('[DEBUG]', ...args);
7
+ }
8
+ },
9
+
10
+ error: (...args: any[]) => {
11
+ if (isDebugMode) {
12
+ console.error('[ERROR]', ...args);
13
+ }
14
+ },
15
+
16
+ info: (...args: any[]) => {
17
+ if (isDebugMode) {
18
+ console.log(...args);
19
+ }
20
+ },
21
+ };