langtrain 0.1.10 → 0.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "langtrain",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "Unified JavaScript SDK for Langtrain Ecosystem",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
package/src/agent.ts CHANGED
@@ -43,6 +43,20 @@ export class AgentClient {
43
43
  return response.data.agents;
44
44
  }
45
45
 
46
+ async get(agentId: string): Promise<Agent> {
47
+ const response = await this.client.get<Agent>(`/agents/${agentId}`);
48
+ return response.data;
49
+ }
50
+
51
+ async create(agent: AgentCreate): Promise<Agent> {
52
+ const response = await this.client.post<Agent>('/agents/', agent);
53
+ return response.data;
54
+ }
55
+
56
+ async delete(agentId: string): Promise<void> {
57
+ await this.client.delete(`/agents/${agentId}`);
58
+ }
59
+
46
60
  async execute(agentId: string, input: any, messages: any[] = [], conversationId?: string): Promise<AgentRun> {
47
61
  const response = await this.client.post<AgentRun>(`/agents/${agentId}/execute`, {
48
62
  input,
@@ -52,3 +66,19 @@ export class AgentClient {
52
66
  return response.data;
53
67
  }
54
68
  }
69
+
70
+ export interface AgentConfig {
71
+ system_prompt?: string;
72
+ temperature?: number;
73
+ max_tokens?: number;
74
+ tools?: string[];
75
+ [key: string]: any;
76
+ }
77
+
78
+ export interface AgentCreate {
79
+ workspace_id: string; // UUID
80
+ name: string;
81
+ description?: string;
82
+ model_id?: string; // UUID
83
+ config?: AgentConfig;
84
+ }
package/src/cli.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { intro, outro, select, text, spinner, isCancel, cancel, password } from '@clack/prompts';
2
+ import {
3
+ intro, outro, text, select, confirm, spinner, isCancel, note, cancel, password
4
+ } from '@clack/prompts';
3
5
  import { bgCyan, black, red, green, yellow, gray } from 'kleur/colors';
4
6
  import { Command } from 'commander';
5
- import { Langvision, Langtune, AgentClient, Agent } from './index';
7
+ import { AgentClient, AgentCreate, FileClient, TrainingClient, SubscriptionClient, Langvision, Langtune } from './index';
6
8
  import fs from 'fs';
7
9
  import path from 'path';
8
10
  import os from 'os';
@@ -64,11 +66,14 @@ async function main() {
64
66
  // Operation Handlers Map (O(1) lookup)
65
67
  const handlers: Record<string, (clients?: any) => Promise<void>> = {
66
68
  'login': handleLogin,
69
+ 'status': handleSubscriptionStatus,
67
70
  'tune-finetune': (c) => handleTuneFinetune(c.tune),
68
71
  'tune-generate': (c) => handleTuneGenerate(c.tune),
69
72
  'vision-finetune': (c) => handleVisionFinetune(c.vision),
70
73
  'vision-generate': (c) => handleVisionGenerate(c.vision),
71
74
  'agent-list': (c) => handleAgentList(c.agent),
75
+ 'agent-create': (c) => handleAgentCreate(c.agent),
76
+ 'agent-delete': (c) => handleAgentDelete(c.agent),
72
77
  'exit': async () => { outro('Goodbye!'); process.exit(0); },
73
78
  };
74
79
 
@@ -78,6 +83,8 @@ async function main() {
78
83
  options: [
79
84
  { value: 'group-agents', label: '🤖 Agents (Server)', hint: 'Chat with custom agents' },
80
85
  { value: 'agent-list', label: ' ↳ List & Run Agents' },
86
+ { value: 'agent-create', label: ' ↳ Create New Agent' },
87
+ { value: 'agent-delete', label: ' ↳ Delete Agent' },
81
88
 
82
89
  { value: 'group-tune', label: '🧠 Langtune (LLM)', hint: 'Fine-tuning & Text Generation' },
83
90
  { value: 'tune-finetune', label: ' ↳ Fine-tune Text Model' },
@@ -140,6 +147,157 @@ async function handleLogin() {
140
147
  intro(green('API Key updated successfully!'));
141
148
  }
142
149
 
150
+ async function handleSubscriptionStatus() {
151
+ const config = getConfig();
152
+ if (!config.apiKey) {
153
+ intro(red('Not logged in. Run "login" first.'));
154
+ return;
155
+ }
156
+ const client = new SubscriptionClient({ apiKey: config.apiKey });
157
+ const s = spinner();
158
+ s.start('Fetching subscription status...');
159
+ try {
160
+ const info = await client.getStatus();
161
+ s.stop(green('Subscription Status:'));
162
+
163
+ console.log(gray('Plan: ') + (info.plan === 'pro' ? bgCyan(' PRO ') : info.plan.toUpperCase()));
164
+ console.log(gray('Active: ') + (info.is_active ? green('Yes') : red('No')));
165
+ if (info.expires_at) console.log(gray('Expires: ') + new Date(info.expires_at).toLocaleDateString());
166
+
167
+ console.log(gray('\nLimits:'));
168
+ console.log(` Models: ${info.limits.max_models === -1 ? 'Unlimited' : info.limits.max_models}`);
169
+ console.log(` Training Jobs: ${info.limits.max_training_jobs}`);
170
+
171
+ } catch (e: any) {
172
+ s.stop(red('Failed to fetch status.'));
173
+ console.error(e.message);
174
+ }
175
+ }
176
+
177
+ async function handleAgentCreate(client: AgentClient) {
178
+ const name = await text({
179
+ message: 'Agent Name:',
180
+ placeholder: 'e.g. Support Bot',
181
+ validate(value) {
182
+ if (!value || value.length === 0) return 'API Key is required';
183
+ },
184
+ });
185
+ if (isCancel(name)) return;
186
+
187
+ const description = await text({
188
+ message: 'Description:',
189
+ placeholder: 'e.g. A helpful support assistant',
190
+ });
191
+ if (isCancel(description)) return;
192
+
193
+ const systemPrompt = await text({
194
+ message: 'System Prompt:',
195
+ placeholder: 'e.g. You are a helpful assistant.',
196
+ initialValue: 'You are a helpful assistant.'
197
+ });
198
+ if (isCancel(systemPrompt)) return;
199
+
200
+ const s = spinner();
201
+ s.start('Creating agent...');
202
+
203
+ try {
204
+ // We need a workspace ID. server usually infers it from API key context if not provided?
205
+ // But the schema says workspace_id is required in AgentCreate.
206
+ // The server implementation of create_agent takes AgentCreate which has workspace_id.
207
+ // However, standard users might not know their exact workspace UUID.
208
+ // We might need to fetch it or rely on server to fill it if we made it optional in schema (which we didn't).
209
+ // EDIT: Let's fetch one agent to get the workspace_id or assume one?
210
+ // Better: List agents, get workspace_id from first one. Hacky but works for single-workspace users.
211
+ // Use 'default' or similar if server supports it?
212
+ // Checking agents.py: verify_api_key returns workspace_id.
213
+ // But create_agent payload requires it.
214
+ // I'll try to fetch list first to get workspace ID. If list empty, we are stuck?
215
+ // Wait, list_agents returns `AgentListResponse` which doesn't explicitly return workspace_id at top level, but agents have it.
216
+ // If no agents, we can't guess it.
217
+ // Maybe I should fetch user profile? No endpoint for that in CLI yet.
218
+ // I'll try to pass a placeholder and hope server ignores it if it uses context?
219
+ // Server code: `workspace_id=agent_in.workspace_id`. It uses payload.
220
+ // I might need to ask user for workspace ID or update server to be smarter.
221
+ // For now, I'll attempt to LIST agents to get a workspace ID.
222
+
223
+ const agents = await client.list();
224
+ let workspaceId = "";
225
+ if (agents.length > 0) {
226
+ workspaceId = agents[0].workspace_id;
227
+ } else {
228
+ // Fallback: Ask user or fail?
229
+ // Or maybe decoding JWT/API key client side? No.
230
+ // I'll prompt for it if not found, with a hint.
231
+ s.stop(yellow('Workspace ID needed (no existing agents found).'));
232
+ const wid = await text({
233
+ message: 'Enter Workspace ID (UUID):',
234
+ validate(value) {
235
+ if (!value || value.length === 0) return 'Required';
236
+ },
237
+ });
238
+ if (isCancel(wid)) return;
239
+ workspaceId = wid as string;
240
+ s.start('Creating agent...');
241
+ }
242
+
243
+ const agent = await client.create({
244
+ workspace_id: workspaceId,
245
+ name: name as string,
246
+ description: description as string,
247
+ config: {
248
+ system_prompt: systemPrompt as string,
249
+ model: 'gpt-4o' // Default or prompt? simplified
250
+ }
251
+ });
252
+ s.stop(green(`Agent "${agent.name}" created successfully! ID: ${agent.id}`));
253
+ } catch (e: any) {
254
+ s.stop(red('Failed to create agent.'));
255
+ throw e;
256
+ }
257
+ }
258
+
259
+ async function handleAgentDelete(client: AgentClient) {
260
+ const s = spinner();
261
+ s.start('Fetching agents...');
262
+ const agents = await client.list();
263
+ s.stop(`Found ${agents.length} agents`);
264
+
265
+ if (agents.length === 0) {
266
+ intro(yellow('No agents to delete.'));
267
+ return;
268
+ }
269
+
270
+ const agentId = await select({
271
+ message: 'Select an agent to DELETE:',
272
+ options: agents.map(a => ({ value: a.id, label: a.name, hint: a.description || 'No description' }))
273
+ });
274
+
275
+ if (isCancel(agentId)) return;
276
+
277
+ const confirm = await select({
278
+ message: `Are you sure you want to delete this agent?`,
279
+ options: [
280
+ { value: 'yes', label: 'Yes, delete it', hint: 'Cannot be undone' },
281
+ { value: 'no', label: 'No, keep it' }
282
+ ]
283
+ });
284
+
285
+ if (confirm !== 'yes') {
286
+ intro(gray('Deletion cancelled.'));
287
+ return;
288
+ }
289
+
290
+ const d = spinner();
291
+ d.start('Deleting agent...');
292
+ try {
293
+ await client.delete(agentId as string);
294
+ d.stop(green('Agent deleted successfully.'));
295
+ } catch (e: any) {
296
+ d.stop(red('Failed to delete agent.'));
297
+ throw e;
298
+ }
299
+ }
300
+
143
301
  async function handleAgentList(client: AgentClient) {
144
302
  const s = spinner();
145
303
  s.start('Fetching agents...');
@@ -225,10 +383,60 @@ async function handleTuneFinetune(tune: Langtune) {
225
383
  });
226
384
  if (isCancel(epochs)) cancel('Operation cancelled.');
227
385
 
386
+ const track = await select({
387
+ message: 'Track this job on Langtrain Cloud?',
388
+ options: [
389
+ { value: 'yes', label: 'Yes', hint: 'Upload dataset and log job' },
390
+ { value: 'no', label: 'No', hint: 'Local only' }
391
+ ]
392
+ });
393
+ if (isCancel(track)) cancel('Operation cancelled.');
394
+
395
+ if (track === 'yes') {
396
+ const s = spinner();
397
+ s.start('Connecting to Cloud...');
398
+ try {
399
+ const config = getConfig();
400
+ if (!config.apiKey) throw new Error('API Key required. Run "login" first.');
401
+
402
+ // Check Subscription
403
+ const subClient = new SubscriptionClient({ apiKey: config.apiKey });
404
+ const sub = await subClient.getStatus();
405
+ if (!sub.features.includes('cloud_finetuning')) {
406
+ s.stop(red('Feature "cloud_finetuning" is not available on your plan.'));
407
+ const upgrade = await confirm({ message: 'Upgrade to Pro for cloud tracking?' });
408
+ if (upgrade && !isCancel(upgrade)) {
409
+ console.log(bgCyan(black(' Visit https://langtrain.ai/dashboard/billing to upgrade. ')));
410
+ }
411
+ return;
412
+ }
413
+
414
+ const fileClient = new FileClient({ apiKey: config.apiKey });
415
+ const trainingClient = new TrainingClient({ apiKey: config.apiKey });
416
+
417
+ s.message('Uploading dataset...');
418
+ const fileResp = await fileClient.upload(trainFile as string);
419
+
420
+ s.message('Creating Job...');
421
+ const job = await trainingClient.createJob({
422
+ name: `cli-sft-${Date.now()}`,
423
+ base_model: model as string,
424
+ dataset_id: fileResp.id,
425
+ task: 'text',
426
+ hyperparameters: {
427
+ n_epochs: parseInt(epochs as string)
428
+ }
429
+ });
430
+ s.stop(green(`Job tracked: ${job.id}`));
431
+ } catch (e: any) {
432
+ s.stop(red(`Tracking failed: ${e.message}`));
433
+ const cont = await confirm({ message: 'Continue with local training anyway?' });
434
+ if (!cont || isCancel(cont)) return;
435
+ }
436
+ }
437
+
228
438
  const s = spinner();
229
- s.start('Connecting to Langtrain Cloud...');
230
- await new Promise(r => setTimeout(r, 800)); // Simulatoin
231
- s.message('Starting fine-tuning job...');
439
+ s.start('Starting local fine-tuning...');
232
440
 
233
441
  try {
234
442
  // Check if FinetuneConfig types match what's needed.
@@ -302,6 +510,60 @@ async function handleVisionFinetune(vision: Langvision) {
302
510
  placeholder: '3',
303
511
  initialValue: '3'
304
512
  });
513
+ if (isCancel(epochs)) cancel('Operation cancelled');
514
+
515
+ const track = await select({
516
+ message: 'Track this job on Langtrain Cloud?',
517
+ options: [
518
+ { value: 'yes', label: 'Yes', hint: 'Upload dataset and log job' },
519
+ { value: 'no', label: 'No', hint: 'Local only' }
520
+ ]
521
+ });
522
+ if (isCancel(track)) cancel('Operation cancelled');
523
+
524
+ if (track === 'yes') {
525
+ const s = spinner();
526
+ s.start('Connecting to Cloud...');
527
+ try {
528
+ const config = getConfig();
529
+ if (!config.apiKey) throw new Error('API Key required. Run "login" first.');
530
+
531
+ // Check Subscription
532
+ const subClient = new SubscriptionClient({ apiKey: config.apiKey });
533
+ const sub = await subClient.getStatus();
534
+ if (!sub.features.includes('cloud_finetuning')) {
535
+ s.stop(red('Feature "cloud_finetuning" is not available on your plan.'));
536
+ const upgrade = await confirm({ message: 'Upgrade to Pro for cloud tracking?' });
537
+ if (upgrade && !isCancel(upgrade)) {
538
+ console.log(bgCyan(black(' Visit https://langtrain.ai/dashboard/billing to upgrade. ')));
539
+ }
540
+ return;
541
+ }
542
+
543
+ const fileClient = new FileClient({ apiKey: config.apiKey });
544
+ const trainingClient = new TrainingClient({ apiKey: config.apiKey });
545
+
546
+ s.message('Uploading dataset...');
547
+ const fileResp = await fileClient.upload(dataset as string, undefined, 'fine-tune-vision');
548
+
549
+ s.message('Creating Job...');
550
+ const job = await trainingClient.createJob({
551
+ name: `cli-vision-${Date.now()}`,
552
+ base_model: model as string,
553
+ dataset_id: fileResp.id,
554
+ task: 'vision',
555
+ training_method: 'lora',
556
+ hyperparameters: {
557
+ n_epochs: parseInt(epochs as string)
558
+ }
559
+ });
560
+ s.stop(green(`Job tracked: ${job.id}`));
561
+ } catch (e: any) {
562
+ s.stop(red(`Tracking failed: ${e.message}`));
563
+ const cont = await confirm({ message: 'Continue with local training anyway?' });
564
+ if (!cont || isCancel(cont)) return;
565
+ }
566
+ }
305
567
 
306
568
  const s = spinner();
307
569
  s.start('Analyzing dataset structure...');
package/src/files.ts ADDED
@@ -0,0 +1,53 @@
1
+ export class FileClient {
2
+ private client: any; // AxiosInstance
3
+
4
+ constructor(config: { apiKey: string, baseUrl?: string }) {
5
+ const axios = require('axios');
6
+ this.client = axios.create({
7
+ baseURL: config.baseUrl || 'https://api.langtrain.ai/api/v1',
8
+ headers: {
9
+ 'X-API-Key': config.apiKey,
10
+ }
11
+ });
12
+ }
13
+
14
+ async upload(file: any, workspaceId?: string, purpose: string = 'fine-tune'): Promise<FileResponse> {
15
+ const FormData = require('form-data');
16
+ const fs = require('fs');
17
+
18
+ const form = new FormData();
19
+ // Check if file is a path or buffer. Assuming path for CLI
20
+ if (typeof file === 'string') {
21
+ if (!fs.existsSync(file)) throw new Error(`File not found: ${file}`);
22
+ form.append('file', fs.createReadStream(file));
23
+ } else {
24
+ // Handle buffer or other types if needed, but for now strict to path
25
+ throw new Error('File path required');
26
+ }
27
+
28
+ if (workspaceId) form.append('workspace_id', workspaceId);
29
+ form.append('purpose', purpose);
30
+
31
+ const response = await this.client.post('/files', form, {
32
+ headers: {
33
+ ...form.getHeaders()
34
+ }
35
+ });
36
+ return response.data;
37
+ }
38
+
39
+ async list(workspaceId: string, purpose?: string): Promise<FileResponse[]> {
40
+ const params: any = { workspace_id: workspaceId };
41
+ if (purpose) params.purpose = purpose;
42
+ const response = await this.client.get('/files', { params });
43
+ return response.data.data;
44
+ }
45
+ }
46
+
47
+ export interface FileResponse {
48
+ id: string;
49
+ filename: string;
50
+ purpose: string;
51
+ bytes: number;
52
+ created_at: string;
53
+ }
package/src/index.ts CHANGED
@@ -4,7 +4,10 @@ export { Langvision } from 'langvision';
4
4
  export { Langtune } from 'langtune';
5
5
 
6
6
  // Export Agent Client
7
- export { AgentClient, Agent, AgentRun } from './agent';
7
+ export { AgentClient, Agent, AgentRun, AgentCreate } from './agent';
8
+ export { FileClient, FileResponse } from './files';
9
+ export { TrainingClient, FineTuneJobCreate, FineTuneJobResponse } from './training';
10
+ export { SubscriptionClient, SubscriptionInfo, FeatureCheck } from './subscription';
8
11
 
9
12
  // Export Types with Namespaces to avoid collisions
10
13
  import * as Vision from 'langvision';
@@ -0,0 +1,45 @@
1
+ export class SubscriptionClient {
2
+ private client: any; // AxiosInstance
3
+
4
+ constructor(config: { apiKey: string, baseUrl?: string }) {
5
+ const axios = require('axios');
6
+ this.client = axios.create({
7
+ baseURL: config.baseUrl || 'https://api.langtrain.ai/api/v1',
8
+ headers: {
9
+ 'X-API-Key': config.apiKey,
10
+ 'Content-Type': 'application/json'
11
+ }
12
+ });
13
+ }
14
+
15
+ async getStatus(): Promise<SubscriptionInfo> {
16
+ const response = await this.client.get('/subscription/status');
17
+ return response.data;
18
+ }
19
+
20
+ async checkFeature(feature: string): Promise<FeatureCheck> {
21
+ const response = await this.client.get(`/subscription/check/${feature}`);
22
+ return response.data;
23
+ }
24
+
25
+ async getLimits(): Promise<any> {
26
+ const response = await this.client.get('/subscription/analytics');
27
+ return response.data;
28
+ }
29
+ }
30
+
31
+ export interface SubscriptionInfo {
32
+ is_active: boolean;
33
+ plan: string;
34
+ plan_name: string;
35
+ expires_at?: string;
36
+ features: string[];
37
+ limits: any;
38
+ }
39
+
40
+ export interface FeatureCheck {
41
+ feature: string;
42
+ allowed: boolean;
43
+ limit?: number;
44
+ used?: number;
45
+ }
@@ -0,0 +1,63 @@
1
+ export class TrainingClient {
2
+ private client: any; // AxiosInstance
3
+
4
+ constructor(config: { apiKey: string, baseUrl?: string }) {
5
+ const axios = require('axios');
6
+ this.client = axios.create({
7
+ baseURL: config.baseUrl || 'https://api.langtrain.ai/api/v1',
8
+ headers: {
9
+ 'X-API-Key': config.apiKey,
10
+ 'Content-Type': 'application/json'
11
+ }
12
+ });
13
+ }
14
+
15
+ async createJob(job: FineTuneJobCreate): Promise<FineTuneJobResponse> {
16
+ const response = await this.client.post('/finetune/jobs', job);
17
+ return response.data;
18
+ }
19
+
20
+ async listJobs(workspaceId: string, limit: number = 10): Promise<FineTuneJobList> {
21
+ const response = await this.client.get('/finetune/jobs', {
22
+ params: { workspace_id: workspaceId, limit }
23
+ });
24
+ return response.data;
25
+ }
26
+
27
+ async getJob(jobId: string): Promise<FineTuneJobResponse> {
28
+ const response = await this.client.get(`/finetune/jobs/${jobId}`);
29
+ return response.data;
30
+ }
31
+
32
+ async cancelJob(jobId: string): Promise<FineTuneJobResponse> {
33
+ const response = await this.client.post(`/finetune/jobs/${jobId}/cancel`);
34
+ return response.data;
35
+ }
36
+ }
37
+
38
+ export interface FineTuneJobCreate {
39
+ name?: string;
40
+ base_model: string;
41
+ model_id?: string;
42
+ dataset_id: string;
43
+ guardrail_id?: string;
44
+ task?: 'text' | 'vision';
45
+ training_method?: 'sft' | 'dpo' | 'rlhf' | 'lora' | 'qlora';
46
+ hyperparameters?: any;
47
+ [key: string]: any;
48
+ }
49
+
50
+ export interface FineTuneJobResponse {
51
+ id: string;
52
+ name: string;
53
+ status: string;
54
+ progress: number;
55
+ error_message?: string;
56
+ created_at: string;
57
+ [key: string]: any;
58
+ }
59
+
60
+ export interface FineTuneJobList {
61
+ data: FineTuneJobResponse[];
62
+ has_more: boolean;
63
+ }