rippletide 1.0.13 → 1.0.15

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/bin/rippletide CHANGED
@@ -13,12 +13,6 @@ async function loadErrorHandler() {
13
13
  }
14
14
 
15
15
  async function main() {
16
- if (!cmd || cmd === 'eval') {
17
- process.argv = ['node', 'dist/index.js', ...args.slice(cmd === 'eval' ? 1 : 0)];
18
- await import('../dist/index.js');
19
- return;
20
- }
21
-
22
16
  if (cmd === 'list-templates' || cmd === 'templates') {
23
17
  process.argv = ['node', 'dist/index.js', ...args];
24
18
  await import('../dist/index.js');
@@ -26,29 +20,13 @@ async function main() {
26
20
  }
27
21
 
28
22
  if (cmd === '--help' || cmd === '-h') {
29
- console.log(`
30
- Rippletide CLI
31
-
32
- Usage:
33
- rippletide eval [options] Run the Rippletide evaluation UI
34
-
35
- Options:
36
- -b, --backend-url <url> Backend API URL (default: https://rippletide-backend.azurewebsites.net)
37
- -d, --dashboard-url <url> Dashboard URL (default: https://eval.rippletide.com)
38
- --debug Show detailed error information and stack traces
39
- -h, --help Show this help message
40
-
41
- Examples:
42
- rippletide eval
43
- rippletide eval -b http://localhost:3001 -d http://localhost:5173
44
- rippletide eval --debug
45
- `);
23
+ process.argv = ['node', 'dist/index.js', '--help'];
24
+ await import('../dist/index.js');
46
25
  return;
47
26
  }
48
27
 
49
- console.error(`Unknown command: ${cmd}`);
50
- console.log(`Run "rippletide --help" for usage.`);
51
- process.exit(1);
28
+ process.argv = ['node', 'dist/index.js', ...args.slice(cmd === 'eval' ? 1 : 0)];
29
+ await import('../dist/index.js');
52
30
  }
53
31
 
54
32
  main().catch(async (err) => {
package/dist/App.d.ts CHANGED
@@ -13,6 +13,7 @@ interface AppProps {
13
13
  customBodyTemplate?: string;
14
14
  customResponseField?: string;
15
15
  templatePath?: string;
16
+ isRemoteTemplate?: boolean;
16
17
  }
17
18
  export declare const App: React.FC<AppProps>;
18
19
  export {};
package/dist/App.js CHANGED
@@ -22,7 +22,7 @@ const knowledgeSources = [
22
22
  { label: 'GitHub Repository', value: 'github', description: 'Import from GitHub repo', disabled: true },
23
23
  { label: 'Skip (No Knowledge)', value: 'skip', description: 'Run tests without knowledge base', disabled: true },
24
24
  ];
25
- export const App = ({ backendUrl, dashboardUrl, nonInteractive, agentEndpoint: initialAgentEndpoint, knowledgeSource: initialKnowledgeSource, pineconeUrl: initialPineconeUrl, pineconeApiKey: initialPineconeApiKey, postgresqlConnection: initialPostgresqlConnection, pdfPath: initialPdfPath, customHeaders, customBodyTemplate, customResponseField, templatePath }) => {
25
+ export const App = ({ backendUrl, dashboardUrl, nonInteractive, agentEndpoint: initialAgentEndpoint, knowledgeSource: initialKnowledgeSource, pineconeUrl: initialPineconeUrl, pineconeApiKey: initialPineconeApiKey, postgresqlConnection: initialPostgresqlConnection, pdfPath: initialPdfPath, customHeaders, customBodyTemplate, customResponseField, templatePath, isRemoteTemplate }) => {
26
26
  const { exit } = useApp();
27
27
  const initialStep = nonInteractive && initialAgentEndpoint ? 'testing-connection' : 'agent-endpoint';
28
28
  const [step, setStep] = useState(initialStep);
@@ -299,25 +299,82 @@ export const App = ({ backendUrl, dashboardUrl, nonInteractive, agentEndpoint: i
299
299
  setCurrentAgentId(agentId);
300
300
  }
301
301
  setEvaluationProgress(30);
302
+ if (knowledgeSource === 'files') {
303
+ let knowledgeData = null;
304
+ if (templatePath) {
305
+ try {
306
+ if (isRemoteTemplate) {
307
+ const axios = await import('axios');
308
+ const qandaUrl = `${templatePath}/qanda.json`;
309
+ const response = await axios.default.get(qandaUrl, { timeout: 5000 });
310
+ knowledgeData = response.data;
311
+ }
312
+ else {
313
+ const fs = await import('fs');
314
+ const path = await import('path');
315
+ const qandaPath = path.join(templatePath, 'qanda.json');
316
+ if (fs.existsSync(qandaPath)) {
317
+ knowledgeData = JSON.parse(fs.readFileSync(qandaPath, 'utf-8'));
318
+ }
319
+ }
320
+ }
321
+ catch (error) {
322
+ logger.debug('Error loading knowledge from template:', error);
323
+ }
324
+ }
325
+ else {
326
+ const knowledgeResult = await api.checkKnowledge();
327
+ if (knowledgeResult.found && knowledgeResult.path) {
328
+ try {
329
+ const fs = await import('fs');
330
+ knowledgeData = JSON.parse(fs.readFileSync(knowledgeResult.path, 'utf-8'));
331
+ }
332
+ catch (error) {
333
+ logger.debug('Error loading knowledge:', error);
334
+ }
335
+ }
336
+ }
337
+ if (knowledgeData) {
338
+ setEvaluationProgress(35);
339
+ try {
340
+ const importResult = await api.importKnowledge(agentId, knowledgeData);
341
+ logger.debug('Knowledge import result:', importResult);
342
+ await new Promise(resolve => setTimeout(resolve, 1000));
343
+ }
344
+ catch (error) {
345
+ logger.error('Failed to import knowledge:', error?.message || error);
346
+ logger.debug('Import error details:', error?.response?.data);
347
+ }
348
+ }
349
+ }
302
350
  setEvaluationProgress(40);
303
351
  let testPrompts = [];
304
352
  if (knowledgeSource === 'files') {
305
353
  if (templatePath) {
306
354
  try {
307
- const fs = await import('fs');
308
- const path = await import('path');
309
- const qandaPath = path.join(templatePath, 'qanda.json');
310
- if (fs.existsSync(qandaPath)) {
311
- const knowledgeData = JSON.parse(fs.readFileSync(qandaPath, 'utf-8'));
312
- if (Array.isArray(knowledgeData)) {
313
- testPrompts = knowledgeData.map((item) => ({
314
- question: item.question || item.prompt || item.input || 'Test question',
315
- answer: item.answer || item.response || item.expectedAnswer
316
- }));
317
- }
355
+ let knowledgeData = null;
356
+ if (isRemoteTemplate) {
357
+ const axios = await import('axios');
358
+ const qandaUrl = `${templatePath}/qanda.json`;
359
+ const response = await axios.default.get(qandaUrl, { timeout: 5000 });
360
+ knowledgeData = response.data;
318
361
  }
319
362
  else {
320
- logger.debug('No qanda.json found in template directory:', templatePath);
363
+ const fs = await import('fs');
364
+ const path = await import('path');
365
+ const qandaPath = path.join(templatePath, 'qanda.json');
366
+ if (fs.existsSync(qandaPath)) {
367
+ knowledgeData = JSON.parse(fs.readFileSync(qandaPath, 'utf-8'));
368
+ }
369
+ else {
370
+ logger.debug('No qanda.json found in template directory:', templatePath);
371
+ }
372
+ }
373
+ if (knowledgeData && Array.isArray(knowledgeData)) {
374
+ testPrompts = knowledgeData.map((item) => ({
375
+ question: item.question || item.prompt || item.input || 'Test question',
376
+ answer: item.answer || item.response || item.expectedAnswer
377
+ }));
321
378
  }
322
379
  }
323
380
  catch (error) {
@@ -365,7 +422,6 @@ export const App = ({ backendUrl, dashboardUrl, nonInteractive, agentEndpoint: i
365
422
  }
366
423
  const createdPrompts = await api.addTestPrompts(agentId, testPrompts);
367
424
  setEvaluationProgress(50);
368
- // Ensure customConfig has the body template for evaluation
369
425
  const evalConfig = {
370
426
  ...customConfig,
371
427
  bodyTemplate: customConfig.bodyTemplate || '{"message": "[eval-question]"}'
@@ -7,7 +7,10 @@ export declare function checkKnowledge(folderPath?: string): Promise<{
7
7
  found: boolean;
8
8
  path?: undefined;
9
9
  }>;
10
- export declare function importKnowledge(agentId: string, knowledgeData: any): Promise<any>;
10
+ export declare function importKnowledge(agentId: string, knowledgeData: any): Promise<{
11
+ imported: number;
12
+ total: number;
13
+ } | null>;
11
14
  export declare function getTestResults(agentId: string): Promise<any>;
12
15
  export interface EvaluationConfig {
13
16
  agentEndpoint: string;
@@ -41,14 +41,44 @@ export async function checkKnowledge(folderPath = '.') {
41
41
  }
42
42
  export async function importKnowledge(agentId, knowledgeData) {
43
43
  try {
44
- const response = await client.post(`/api/agents/${agentId}/knowledge/import`, {
45
- data: knowledgeData,
46
- });
47
- return response.data;
44
+ logger.debug('Importing knowledge for agent:', agentId);
45
+ logger.debug('Knowledge data type:', Array.isArray(knowledgeData) ? 'array' : typeof knowledgeData);
46
+ logger.debug('Knowledge data length:', Array.isArray(knowledgeData) ? knowledgeData.length : 'N/A');
47
+ if (!Array.isArray(knowledgeData)) {
48
+ logger.warn('Knowledge data is not an array, skipping import');
49
+ return null;
50
+ }
51
+ let importedCount = 0;
52
+ for (const item of knowledgeData) {
53
+ try {
54
+ const question = item.question || item.prompt || item.input;
55
+ const answer = item.answer || item.response || item.expectedAnswer;
56
+ if (!question || !answer) {
57
+ logger.debug('Skipping item without question or answer:', item);
58
+ continue;
59
+ }
60
+ const response = await client.post(`/api/agents/${agentId}/config`, {
61
+ label: question,
62
+ description: answer,
63
+ type: 'knowledge'
64
+ });
65
+ importedCount++;
66
+ logger.debug(`Imported Q&A ${importedCount}/${knowledgeData.length}: ${question.substring(0, 50)}...`);
67
+ }
68
+ catch (error) {
69
+ logger.warn(`Failed to import Q&A pair: ${error?.message || error}`);
70
+ logger.debug('Q&A import error details:', error?.response?.data);
71
+ logger.debug('Q&A import error status:', error?.response?.status);
72
+ }
73
+ }
74
+ logger.info(`Knowledge imported successfully: ${importedCount}/${knowledgeData.length} Q&A pairs`);
75
+ return { imported: importedCount, total: knowledgeData.length };
48
76
  }
49
77
  catch (error) {
50
- logger.error('Error importing knowledge:', error);
51
- return null;
78
+ logger.error('Error importing knowledge:', error?.message || error);
79
+ logger.debug('Error details:', error?.response?.data);
80
+ logger.debug('Error status:', error?.response?.status);
81
+ throw error;
52
82
  }
53
83
  }
54
84
  export async function getTestResults(agentId) {
package/dist/index.js CHANGED
@@ -6,13 +6,13 @@ import { ErrorHandler } from './errors/handler.js';
6
6
  import { ValidationError } from './errors/types.js';
7
7
  import { listTemplates, loadTemplate, getTemplateOptions } from './utils/templates.js';
8
8
  import { analytics } from './utils/analytics.js';
9
- const parseArgs = () => {
9
+ const parseArgs = async () => {
10
10
  const args = process.argv.slice(2);
11
11
  if (args[0] === 'list-templates' || args[0] === 'templates') {
12
- const templates = listTemplates();
12
+ const templates = await listTemplates();
13
13
  console.log('\nAvailable Templates:\n');
14
14
  if (templates.length === 0) {
15
- console.log('No templates found in ./templates directory');
15
+ console.log('No templates found');
16
16
  }
17
17
  else {
18
18
  templates.forEach(template => {
@@ -37,15 +37,15 @@ const parseArgs = () => {
37
37
  };
38
38
  for (let i = 0; i < args.length; i++) {
39
39
  if ((args[i] === '--template' || args[i] === '-t') && args[i + 1]) {
40
- const template = loadTemplate(args[i + 1]);
40
+ const template = await loadTemplate(args[i + 1]);
41
41
  if (!template) {
42
42
  console.error(`Template '${args[i + 1]}' not found.`);
43
43
  console.log('\nAvailable templates:');
44
- const templates = listTemplates();
44
+ const templates = await listTemplates();
45
45
  templates.forEach(t => console.log(` - ${t.name}`));
46
46
  process.exit(1);
47
47
  }
48
- const templateOptions = getTemplateOptions(template);
48
+ const templateOptions = await getTemplateOptions(template);
49
49
  Object.assign(options, templateOptions);
50
50
  i++;
51
51
  }
@@ -184,14 +184,12 @@ Examples:
184
184
  };
185
185
  async function run() {
186
186
  try {
187
- const options = parseArgs();
188
- // Check if this is a new user
187
+ const options = await parseArgs();
189
188
  const fs = await import('fs');
190
189
  const path = await import('path');
191
190
  const os = await import('os');
192
191
  const configPath = path.join(os.homedir(), '.rippletide', 'config.json');
193
192
  const isNewUser = !fs.existsSync(configPath);
194
- // Track start event and wait for it to be sent
195
193
  analytics.track('cli_started', {
196
194
  command: process.argv[2] || 'eval',
197
195
  has_template: !!options.templatePath,
@@ -199,19 +197,16 @@ async function run() {
199
197
  is_non_interactive: options.nonInteractive,
200
198
  is_new_user: isNewUser,
201
199
  });
202
- // Track new user activation separately
203
200
  if (isNewUser) {
204
201
  analytics.track('new_user_activated', {
205
202
  source: process.argv[2] || 'eval',
206
203
  activation_date: new Date().toISOString(),
207
204
  });
208
205
  }
209
- // Give PostHog a moment to send the event
210
206
  await new Promise(resolve => setTimeout(resolve, 100));
211
207
  process.stdout.write('\x1Bc');
212
- 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 }));
208
+ 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, isRemoteTemplate: options.isRemoteTemplate, pdfPath: options.pdfPath }));
213
209
  await waitUntilExit();
214
- // Ensure all events are sent before exiting
215
210
  await analytics.shutdown();
216
211
  await new Promise(resolve => setTimeout(resolve, 500));
217
212
  }
@@ -33,7 +33,6 @@ class Analytics {
33
33
  if (process.env.DEBUG_ANALYTICS) {
34
34
  console.log('[Analytics] Client initialized with ID:', this.distinctId);
35
35
  }
36
- // Force flush on various exit events
37
36
  const cleanup = () => {
38
37
  if (this.client) {
39
38
  this.client.shutdown();
@@ -149,13 +148,11 @@ class Analytics {
149
148
  const config = this.getConfig();
150
149
  const firstSeen = config.first_seen || new Date().toISOString();
151
150
  const totalLaunches = (config.total_launches || 0) + 1;
152
- // Save first seen and increment launch count
153
151
  this.saveConfig({
154
152
  ...config,
155
153
  first_seen: firstSeen,
156
154
  total_launches: totalLaunches
157
155
  });
158
- // Identify the user with persistent properties
159
156
  this.client.identify({
160
157
  distinctId: this.distinctId,
161
158
  properties: {
@@ -168,7 +165,6 @@ class Analytics {
168
165
  hostname_hash: createHash('sha256').update(os.hostname()).digest('hex').substring(0, 8),
169
166
  },
170
167
  });
171
- // Also set these as super properties for all events
172
168
  this.client.capture({
173
169
  distinctId: this.distinctId,
174
170
  event: '$set',
@@ -200,7 +196,6 @@ class Analytics {
200
196
  });
201
197
  }
202
198
  catch (error) {
203
- // Silently fail
204
199
  }
205
200
  }
206
201
  async shutdown() {
@@ -15,7 +15,9 @@ export interface Template {
15
15
  name: string;
16
16
  path: string;
17
17
  config: TemplateConfig;
18
+ isRemote?: boolean;
18
19
  }
19
- export declare const listTemplates: () => Template[];
20
- export declare const loadTemplate: (templateName: string) => Template | null;
21
- export declare const getTemplateOptions: (template: Template) => any;
20
+ export declare const listTemplates: () => Promise<Template[]>;
21
+ export declare const loadTemplate: (templateName: string) => Promise<Template | null>;
22
+ export declare const getTemplateOptions: (template: Template) => Promise<any>;
23
+ export declare const loadRemoteQAndA: (templateName: string) => Promise<any>;
@@ -1,25 +1,43 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
+ import axios from 'axios';
4
+ const GITHUB_BASE_URL = 'https://raw.githubusercontent.com/rippletideco/starter/refs/heads/main/cli/templates';
5
+ const REMOTE_TEMPLATES = [
6
+ 'banking_analyst',
7
+ 'blog_to_linkedin',
8
+ 'customer_service',
9
+ 'local_dev',
10
+ 'luxe_concierge',
11
+ 'openai_compatible',
12
+ 'project_manager'
13
+ ];
3
14
  const getTemplatesDir = () => {
4
15
  const currentDir = process.cwd();
5
16
  const cliDir = path.resolve(currentDir);
6
17
  const templatesDir = path.join(cliDir, 'templates');
7
- if (!fs.existsSync(templatesDir)) {
8
- const altTemplatesDir = path.join(path.dirname(cliDir), 'cli', 'templates');
9
- if (fs.existsSync(altTemplatesDir)) {
10
- return altTemplatesDir;
18
+ if (fs.existsSync(templatesDir)) {
19
+ const configFiles = fs.readdirSync(templatesDir, { withFileTypes: true })
20
+ .filter(dirent => dirent.isDirectory())
21
+ .some(dirent => fs.existsSync(path.join(templatesDir, dirent.name, 'config.json')));
22
+ if (configFiles) {
23
+ return templatesDir;
11
24
  }
12
- const globalTemplatesDir = path.join(__dirname, '..', '..', 'templates');
13
- if (fs.existsSync(globalTemplatesDir)) {
14
- return globalTemplatesDir;
25
+ }
26
+ const altTemplatesDir = path.join(path.dirname(cliDir), 'cli', 'templates');
27
+ if (fs.existsSync(altTemplatesDir)) {
28
+ const configFiles = fs.readdirSync(altTemplatesDir, { withFileTypes: true })
29
+ .filter(dirent => dirent.isDirectory())
30
+ .some(dirent => fs.existsSync(path.join(altTemplatesDir, dirent.name, 'config.json')));
31
+ if (configFiles) {
32
+ return altTemplatesDir;
15
33
  }
16
34
  }
17
- return templatesDir;
35
+ return null;
18
36
  };
19
- export const listTemplates = () => {
37
+ const loadLocalTemplates = () => {
20
38
  const templates = [];
21
39
  const templatesDir = getTemplatesDir();
22
- if (!fs.existsSync(templatesDir)) {
40
+ if (!templatesDir || !fs.existsSync(templatesDir)) {
23
41
  return templates;
24
42
  }
25
43
  const dirs = fs.readdirSync(templatesDir, { withFileTypes: true })
@@ -38,24 +56,65 @@ export const listTemplates = () => {
38
56
  ...config,
39
57
  name: config.name || dir,
40
58
  description: config.description || `Template: ${dir}`
41
- }
59
+ },
60
+ isRemote: false
42
61
  });
43
62
  }
44
63
  catch (error) {
45
- console.error(`Error loading template ${dir}:`, error);
46
64
  }
47
65
  }
48
66
  }
49
67
  return templates;
50
68
  };
51
- export const loadTemplate = (templateName) => {
52
- const templates = listTemplates();
53
- const template = templates.find(t => t.name.toLowerCase() === templateName.toLowerCase() ||
69
+ const loadRemoteTemplate = async (templateName) => {
70
+ try {
71
+ const configUrl = `${GITHUB_BASE_URL}/${templateName}/config.json`;
72
+ const response = await axios.get(configUrl, { timeout: 5000 });
73
+ const config = response.data;
74
+ return {
75
+ name: templateName,
76
+ path: `${GITHUB_BASE_URL}/${templateName}`,
77
+ config: {
78
+ ...config,
79
+ name: config.name || templateName,
80
+ description: config.description || `Template: ${templateName}`
81
+ },
82
+ isRemote: true
83
+ };
84
+ }
85
+ catch (error) {
86
+ return null;
87
+ }
88
+ };
89
+ export const listTemplates = async () => {
90
+ const localTemplates = loadLocalTemplates();
91
+ if (localTemplates.length > 0) {
92
+ return localTemplates;
93
+ }
94
+ const remoteTemplates = [];
95
+ for (const templateName of REMOTE_TEMPLATES) {
96
+ const template = await loadRemoteTemplate(templateName);
97
+ if (template) {
98
+ remoteTemplates.push(template);
99
+ }
100
+ }
101
+ return remoteTemplates;
102
+ };
103
+ export const loadTemplate = async (templateName) => {
104
+ const localTemplates = loadLocalTemplates();
105
+ const localTemplate = localTemplates.find(t => t.name.toLowerCase() === templateName.toLowerCase() ||
54
106
  t.config.name?.toLowerCase() === templateName.toLowerCase());
55
- return template || null;
107
+ if (localTemplate) {
108
+ return localTemplate;
109
+ }
110
+ return await loadRemoteTemplate(templateName);
56
111
  };
57
- export const getTemplateOptions = (template) => {
112
+ export const getTemplateOptions = async (template) => {
58
113
  const config = template.config;
114
+ let templatePath = template.path;
115
+ if (template.isRemote) {
116
+ templatePath = `${template.path}`;
117
+ }
59
118
  return {
60
119
  agentEndpoint: config.endpoint_url,
61
120
  knowledgeSource: config.knowledge_source || 'files',
@@ -66,6 +125,17 @@ export const getTemplateOptions = (template) => {
66
125
  bodyTemplate: config.body_template,
67
126
  responseField: config.response_field,
68
127
  nonInteractive: true,
69
- templatePath: template.path
128
+ templatePath,
129
+ isRemoteTemplate: template.isRemote
70
130
  };
71
131
  };
132
+ export const loadRemoteQAndA = async (templateName) => {
133
+ try {
134
+ const qandaUrl = `${GITHUB_BASE_URL}/${templateName}/qanda.json`;
135
+ const response = await axios.get(qandaUrl, { timeout: 5000 });
136
+ return response.data;
137
+ }
138
+ catch (error) {
139
+ return null;
140
+ }
141
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rippletide",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "description": "Rippletide Evaluation CLI",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
package/src/App.tsx CHANGED
@@ -58,6 +58,7 @@ interface AppProps {
58
58
  customBodyTemplate?: string;
59
59
  customResponseField?: string;
60
60
  templatePath?: string;
61
+ isRemoteTemplate?: boolean;
61
62
  }
62
63
 
63
64
  export const App: React.FC<AppProps> = ({
@@ -73,7 +74,8 @@ export const App: React.FC<AppProps> = ({
73
74
  customHeaders,
74
75
  customBodyTemplate,
75
76
  customResponseField,
76
- templatePath
77
+ templatePath,
78
+ isRemoteTemplate
77
79
  }) => {
78
80
  const { exit } = useApp();
79
81
  const initialStep = nonInteractive && initialAgentEndpoint ? 'testing-connection' : 'agent-endpoint';
@@ -368,24 +370,78 @@ export const App: React.FC<AppProps> = ({
368
370
 
369
371
  setEvaluationProgress(30);
370
372
 
373
+ if (knowledgeSource === 'files') {
374
+ let knowledgeData: any = null;
375
+ if (templatePath) {
376
+ try {
377
+ if (isRemoteTemplate) {
378
+ const axios = await import('axios');
379
+ const qandaUrl = `${templatePath}/qanda.json`;
380
+ const response = await axios.default.get(qandaUrl, { timeout: 5000 });
381
+ knowledgeData = response.data;
382
+ } else {
383
+ const fs = await import('fs');
384
+ const path = await import('path');
385
+ const qandaPath = path.join(templatePath, 'qanda.json');
386
+ if (fs.existsSync(qandaPath)) {
387
+ knowledgeData = JSON.parse(fs.readFileSync(qandaPath, 'utf-8'));
388
+ }
389
+ }
390
+ } catch (error) {
391
+ logger.debug('Error loading knowledge from template:', error);
392
+ }
393
+ } else {
394
+ const knowledgeResult = await api.checkKnowledge();
395
+ if (knowledgeResult.found && knowledgeResult.path) {
396
+ try {
397
+ const fs = await import('fs');
398
+ knowledgeData = JSON.parse(fs.readFileSync(knowledgeResult.path, 'utf-8'));
399
+ } catch (error) {
400
+ logger.debug('Error loading knowledge:', error);
401
+ }
402
+ }
403
+ }
404
+
405
+ if (knowledgeData) {
406
+ setEvaluationProgress(35);
407
+ try {
408
+ const importResult = await api.importKnowledge(agentId, knowledgeData);
409
+ logger.debug('Knowledge import result:', importResult);
410
+ await new Promise(resolve => setTimeout(resolve, 1000));
411
+ } catch (error: any) {
412
+ logger.error('Failed to import knowledge:', error?.message || error);
413
+ logger.debug('Import error details:', error?.response?.data);
414
+ }
415
+ }
416
+ }
417
+
371
418
  setEvaluationProgress(40);
372
419
  let testPrompts: Array<{question: string, answer?: string}> | string[] = [];
373
420
  if (knowledgeSource === 'files') {
374
421
  if (templatePath) {
375
422
  try {
376
- const fs = await import('fs');
377
- const path = await import('path');
378
- const qandaPath = path.join(templatePath, 'qanda.json');
379
- if (fs.existsSync(qandaPath)) {
380
- const knowledgeData = JSON.parse(fs.readFileSync(qandaPath, 'utf-8'));
381
- if (Array.isArray(knowledgeData)) {
382
- testPrompts = knowledgeData.map((item: any) => ({
383
- question: item.question || item.prompt || item.input || 'Test question',
384
- answer: item.answer || item.response || item.expectedAnswer
385
- }));
386
- }
423
+ let knowledgeData: any = null;
424
+ if (isRemoteTemplate) {
425
+ const axios = await import('axios');
426
+ const qandaUrl = `${templatePath}/qanda.json`;
427
+ const response = await axios.default.get(qandaUrl, { timeout: 5000 });
428
+ knowledgeData = response.data;
387
429
  } else {
388
- logger.debug('No qanda.json found in template directory:', templatePath);
430
+ const fs = await import('fs');
431
+ const path = await import('path');
432
+ const qandaPath = path.join(templatePath, 'qanda.json');
433
+ if (fs.existsSync(qandaPath)) {
434
+ knowledgeData = JSON.parse(fs.readFileSync(qandaPath, 'utf-8'));
435
+ } else {
436
+ logger.debug('No qanda.json found in template directory:', templatePath);
437
+ }
438
+ }
439
+
440
+ if (knowledgeData && Array.isArray(knowledgeData)) {
441
+ testPrompts = knowledgeData.map((item: any) => ({
442
+ question: item.question || item.prompt || item.input || 'Test question',
443
+ answer: item.answer || item.response || item.expectedAnswer
444
+ }));
389
445
  }
390
446
  } catch (error) {
391
447
  logger.debug('Error loading prompts from template:', error);
@@ -48,14 +48,49 @@ export async function checkKnowledge(folderPath: string = '.') {
48
48
 
49
49
  export async function importKnowledge(agentId: string, knowledgeData: any) {
50
50
  try {
51
- const response = await client.post(`/api/agents/${agentId}/knowledge/import`, {
52
- data: knowledgeData,
53
- });
51
+ logger.debug('Importing knowledge for agent:', agentId);
52
+ logger.debug('Knowledge data type:', Array.isArray(knowledgeData) ? 'array' : typeof knowledgeData);
53
+ logger.debug('Knowledge data length:', Array.isArray(knowledgeData) ? knowledgeData.length : 'N/A');
54
54
 
55
- return response.data;
56
- } catch (error) {
57
- logger.error('Error importing knowledge:', error);
58
- return null;
55
+ if (!Array.isArray(knowledgeData)) {
56
+ logger.warn('Knowledge data is not an array, skipping import');
57
+ return null;
58
+ }
59
+
60
+ let importedCount = 0;
61
+ for (const item of knowledgeData) {
62
+ try {
63
+ const question = item.question || item.prompt || item.input;
64
+ const answer = item.answer || item.response || item.expectedAnswer;
65
+
66
+ if (!question || !answer) {
67
+ logger.debug('Skipping item without question or answer:', item);
68
+ continue;
69
+ }
70
+
71
+ const response = await client.post(`/api/agents/${agentId}/config`, {
72
+ label: question,
73
+ description: answer,
74
+ type: 'knowledge'
75
+ });
76
+
77
+ importedCount++;
78
+ logger.debug(`Imported Q&A ${importedCount}/${knowledgeData.length}: ${question.substring(0, 50)}...`);
79
+ } catch (error: any) {
80
+ logger.warn(`Failed to import Q&A pair: ${error?.message || error}`);
81
+ logger.debug('Q&A import error details:', error?.response?.data);
82
+ logger.debug('Q&A import error status:', error?.response?.status);
83
+ }
84
+ }
85
+
86
+ logger.info(`Knowledge imported successfully: ${importedCount}/${knowledgeData.length} Q&A pairs`);
87
+
88
+ return { imported: importedCount, total: knowledgeData.length };
89
+ } catch (error: any) {
90
+ logger.error('Error importing knowledge:', error?.message || error);
91
+ logger.debug('Error details:', error?.response?.data);
92
+ logger.debug('Error status:', error?.response?.status);
93
+ throw error;
59
94
  }
60
95
  }
61
96
 
package/src/index.tsx CHANGED
@@ -7,14 +7,14 @@ import { ValidationError } from './errors/types.js';
7
7
  import { listTemplates, loadTemplate, getTemplateOptions } from './utils/templates.js';
8
8
  import { analytics } from './utils/analytics.js';
9
9
 
10
- const parseArgs = () => {
10
+ const parseArgs = async () => {
11
11
  const args = process.argv.slice(2);
12
12
 
13
13
  if (args[0] === 'list-templates' || args[0] === 'templates') {
14
- const templates = listTemplates();
14
+ const templates = await listTemplates();
15
15
  console.log('\nAvailable Templates:\n');
16
16
  if (templates.length === 0) {
17
- console.log('No templates found in ./templates directory');
17
+ console.log('No templates found');
18
18
  } else {
19
19
  templates.forEach(template => {
20
20
  console.log(` ${template.name}`);
@@ -40,15 +40,15 @@ const parseArgs = () => {
40
40
 
41
41
  for (let i = 0; i < args.length; i++) {
42
42
  if ((args[i] === '--template' || args[i] === '-t') && args[i + 1]) {
43
- const template = loadTemplate(args[i + 1]);
43
+ const template = await loadTemplate(args[i + 1]);
44
44
  if (!template) {
45
45
  console.error(`Template '${args[i + 1]}' not found.`);
46
46
  console.log('\nAvailable templates:');
47
- const templates = listTemplates();
47
+ const templates = await listTemplates();
48
48
  templates.forEach(t => console.log(` - ${t.name}`));
49
49
  process.exit(1);
50
50
  }
51
- const templateOptions = getTemplateOptions(template);
51
+ const templateOptions = await getTemplateOptions(template);
52
52
  Object.assign(options, templateOptions);
53
53
  i++;
54
54
  } else if ((args[i] === '--backend-url' || args[i] === '-b') && args[i + 1]) {
@@ -177,7 +177,7 @@ Examples:
177
177
 
178
178
  async function run() {
179
179
  try {
180
- const options = parseArgs();
180
+ const options = await parseArgs();
181
181
 
182
182
  const fs = await import('fs');
183
183
  const path = await import('path');
@@ -218,6 +218,7 @@ async function run() {
218
218
  customBodyTemplate={options.bodyTemplate}
219
219
  customResponseField={options.responseField}
220
220
  templatePath={options.templatePath}
221
+ isRemoteTemplate={options.isRemoteTemplate}
221
222
  pdfPath={options.pdfPath}
222
223
  />
223
224
  );
@@ -1,5 +1,7 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import axios from 'axios';
3
5
 
4
6
  export interface TemplateConfig {
5
7
  endpoint_url: string;
@@ -19,33 +21,53 @@ export interface Template {
19
21
  name: string;
20
22
  path: string;
21
23
  config: TemplateConfig;
24
+ isRemote?: boolean;
22
25
  }
23
26
 
24
- const getTemplatesDir = (): string => {
27
+ const GITHUB_BASE_URL = 'https://raw.githubusercontent.com/rippletideco/starter/refs/heads/main/cli/templates';
28
+
29
+ const REMOTE_TEMPLATES = [
30
+ 'banking_analyst',
31
+ 'blog_to_linkedin',
32
+ 'customer_service',
33
+ 'local_dev',
34
+ 'luxe_concierge',
35
+ 'openai_compatible',
36
+ 'project_manager'
37
+ ];
38
+
39
+ const getTemplatesDir = (): string | null => {
25
40
  const currentDir = process.cwd();
26
41
  const cliDir = path.resolve(currentDir);
27
42
  const templatesDir = path.join(cliDir, 'templates');
28
43
 
29
- if (!fs.existsSync(templatesDir)) {
30
- const altTemplatesDir = path.join(path.dirname(cliDir), 'cli', 'templates');
31
- if (fs.existsSync(altTemplatesDir)) {
32
- return altTemplatesDir;
44
+ if (fs.existsSync(templatesDir)) {
45
+ const configFiles = fs.readdirSync(templatesDir, { withFileTypes: true })
46
+ .filter(dirent => dirent.isDirectory())
47
+ .some(dirent => fs.existsSync(path.join(templatesDir, dirent.name, 'config.json')));
48
+ if (configFiles) {
49
+ return templatesDir;
33
50
  }
34
-
35
- const globalTemplatesDir = path.join(__dirname, '..', '..', 'templates');
36
- if (fs.existsSync(globalTemplatesDir)) {
37
- return globalTemplatesDir;
51
+ }
52
+
53
+ const altTemplatesDir = path.join(path.dirname(cliDir), 'cli', 'templates');
54
+ if (fs.existsSync(altTemplatesDir)) {
55
+ const configFiles = fs.readdirSync(altTemplatesDir, { withFileTypes: true })
56
+ .filter(dirent => dirent.isDirectory())
57
+ .some(dirent => fs.existsSync(path.join(altTemplatesDir, dirent.name, 'config.json')));
58
+ if (configFiles) {
59
+ return altTemplatesDir;
38
60
  }
39
61
  }
40
62
 
41
- return templatesDir;
63
+ return null;
42
64
  };
43
65
 
44
- export const listTemplates = (): Template[] => {
66
+ const loadLocalTemplates = (): Template[] => {
45
67
  const templates: Template[] = [];
46
68
  const templatesDir = getTemplatesDir();
47
69
 
48
- if (!fs.existsSync(templatesDir)) {
70
+ if (!templatesDir || !fs.existsSync(templatesDir)) {
49
71
  return templates;
50
72
  }
51
73
 
@@ -66,10 +88,10 @@ export const listTemplates = (): Template[] => {
66
88
  ...config,
67
89
  name: config.name || dir,
68
90
  description: config.description || `Template: ${dir}`
69
- }
91
+ },
92
+ isRemote: false
70
93
  });
71
94
  } catch (error) {
72
- console.error(`Error loading template ${dir}:`, error);
73
95
  }
74
96
  }
75
97
  }
@@ -77,18 +99,67 @@ export const listTemplates = (): Template[] => {
77
99
  return templates;
78
100
  };
79
101
 
80
- export const loadTemplate = (templateName: string): Template | null => {
81
- const templates = listTemplates();
82
- const template = templates.find(t =>
102
+ const loadRemoteTemplate = async (templateName: string): Promise<Template | null> => {
103
+ try {
104
+ const configUrl = `${GITHUB_BASE_URL}/${templateName}/config.json`;
105
+ const response = await axios.get(configUrl, { timeout: 5000 });
106
+ const config = response.data as TemplateConfig;
107
+
108
+ return {
109
+ name: templateName,
110
+ path: `${GITHUB_BASE_URL}/${templateName}`,
111
+ config: {
112
+ ...config,
113
+ name: config.name || templateName,
114
+ description: config.description || `Template: ${templateName}`
115
+ },
116
+ isRemote: true
117
+ };
118
+ } catch (error) {
119
+ return null;
120
+ }
121
+ };
122
+
123
+ export const listTemplates = async (): Promise<Template[]> => {
124
+ const localTemplates = loadLocalTemplates();
125
+
126
+ if (localTemplates.length > 0) {
127
+ return localTemplates;
128
+ }
129
+
130
+ const remoteTemplates: Template[] = [];
131
+ for (const templateName of REMOTE_TEMPLATES) {
132
+ const template = await loadRemoteTemplate(templateName);
133
+ if (template) {
134
+ remoteTemplates.push(template);
135
+ }
136
+ }
137
+
138
+ return remoteTemplates;
139
+ };
140
+
141
+ export const loadTemplate = async (templateName: string): Promise<Template | null> => {
142
+ const localTemplates = loadLocalTemplates();
143
+ const localTemplate = localTemplates.find(t =>
83
144
  t.name.toLowerCase() === templateName.toLowerCase() ||
84
145
  t.config.name?.toLowerCase() === templateName.toLowerCase()
85
146
  );
86
147
 
87
- return template || null;
148
+ if (localTemplate) {
149
+ return localTemplate;
150
+ }
151
+
152
+ return await loadRemoteTemplate(templateName);
88
153
  };
89
154
 
90
- export const getTemplateOptions = (template: Template): any => {
155
+ export const getTemplateOptions = async (template: Template): Promise<any> => {
91
156
  const config = template.config;
157
+
158
+ let templatePath = template.path;
159
+ if (template.isRemote) {
160
+ templatePath = `${template.path}`;
161
+ }
162
+
92
163
  return {
93
164
  agentEndpoint: config.endpoint_url,
94
165
  knowledgeSource: config.knowledge_source || 'files',
@@ -99,6 +170,17 @@ export const getTemplateOptions = (template: Template): any => {
99
170
  bodyTemplate: config.body_template,
100
171
  responseField: config.response_field,
101
172
  nonInteractive: true,
102
- templatePath: template.path
173
+ templatePath,
174
+ isRemoteTemplate: template.isRemote
103
175
  };
104
176
  };
177
+
178
+ export const loadRemoteQAndA = async (templateName: string): Promise<any> => {
179
+ try {
180
+ const qandaUrl = `${GITHUB_BASE_URL}/${templateName}/qanda.json`;
181
+ const response = await axios.get(qandaUrl, { timeout: 5000 });
182
+ return response.data;
183
+ } catch (error) {
184
+ return null;
185
+ }
186
+ };
@@ -41,7 +41,7 @@
41
41
  },
42
42
  {
43
43
  "question": "What shopping outputs can you produce?",
44
- "answer": "Shortlist, "best overall / best value / best statement," sizing guide, care guide, authenticity checklist, buying plan by budget."
44
+ "answer": "Shortlist, \"best overall / best value / best statement,\" sizing guide, care guide, authenticity checklist, buying plan by budget."
45
45
  },
46
46
  {
47
47
  "question": "How do you handle gifts?",