@fastino-ai/pioneer-cli 0.1.0 → 0.2.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,371 @@
1
+ /**
2
+ * ModelTrainer - Fine-tune or train models based on feedback
3
+ */
4
+
5
+ import * as fs from "fs";
6
+ import * as path from "path";
7
+ import * as os from "os";
8
+ import { spawn } from "child_process";
9
+ import type { TrainingData } from "./types.js";
10
+
11
+ export interface ModelTrainerConfig {
12
+ provider: "openai" | "anthropic" | "modal" | "local";
13
+ baseModel: string;
14
+ outputDir?: string;
15
+ openaiApiKey?: string;
16
+ anthropicApiKey?: string;
17
+ modalTokenId?: string;
18
+ modalTokenSecret?: string;
19
+ }
20
+
21
+ export interface TrainingResult {
22
+ success: boolean;
23
+ modelId?: string;
24
+ modelPath?: string;
25
+ metrics?: {
26
+ loss?: number;
27
+ accuracy?: number;
28
+ epochs?: number;
29
+ };
30
+ error?: string;
31
+ }
32
+
33
+ export class ModelTrainer {
34
+ private config: ModelTrainerConfig;
35
+ private outputDir: string;
36
+
37
+ constructor(config: ModelTrainerConfig) {
38
+ this.config = config;
39
+ this.outputDir =
40
+ config.outputDir || path.join(os.homedir(), ".pioneer", "models");
41
+ this.ensureOutputDir();
42
+ }
43
+
44
+ private ensureOutputDir(): void {
45
+ if (!fs.existsSync(this.outputDir)) {
46
+ fs.mkdirSync(this.outputDir, { recursive: true });
47
+ }
48
+ }
49
+
50
+ async train(trainingData: TrainingData[]): Promise<TrainingResult> {
51
+ if (trainingData.length < 10) {
52
+ return {
53
+ success: false,
54
+ error: "Insufficient training data. Need at least 10 examples.",
55
+ };
56
+ }
57
+
58
+ switch (this.config.provider) {
59
+ case "openai":
60
+ return this.trainOpenAI(trainingData);
61
+ case "modal":
62
+ return this.trainModal(trainingData);
63
+ case "local":
64
+ return this.trainLocal(trainingData);
65
+ default:
66
+ return {
67
+ success: false,
68
+ error: `Unsupported provider: ${this.config.provider}`,
69
+ };
70
+ }
71
+ }
72
+
73
+ private async trainOpenAI(trainingData: TrainingData[]): Promise<TrainingResult> {
74
+ const apiKey = this.config.openaiApiKey || process.env.OPENAI_API_KEY;
75
+ if (!apiKey) {
76
+ return { success: false, error: "OpenAI API key not provided" };
77
+ }
78
+
79
+ // Prepare training file
80
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openai-train-"));
81
+ const trainingFile = path.join(tempDir, "training.jsonl");
82
+
83
+ const formattedData = trainingData.map((d) => ({
84
+ messages: d.messages,
85
+ }));
86
+
87
+ fs.writeFileSync(
88
+ trainingFile,
89
+ formattedData.map((d) => JSON.stringify(d)).join("\n")
90
+ );
91
+
92
+ try {
93
+ // Upload file
94
+ const uploadResponse = await fetch(
95
+ "https://api.openai.com/v1/files",
96
+ {
97
+ method: "POST",
98
+ headers: {
99
+ Authorization: `Bearer ${apiKey}`,
100
+ },
101
+ body: (() => {
102
+ const formData = new FormData();
103
+ formData.append("purpose", "fine-tune");
104
+ formData.append(
105
+ "file",
106
+ new Blob([fs.readFileSync(trainingFile)]),
107
+ "training.jsonl"
108
+ );
109
+ return formData;
110
+ })(),
111
+ }
112
+ );
113
+
114
+ if (!uploadResponse.ok) {
115
+ const error = await uploadResponse.text();
116
+ return { success: false, error: `File upload failed: ${error}` };
117
+ }
118
+
119
+ const uploadResult = await uploadResponse.json() as { id: string };
120
+
121
+ // Create fine-tuning job
122
+ const ftResponse = await fetch(
123
+ "https://api.openai.com/v1/fine_tuning/jobs",
124
+ {
125
+ method: "POST",
126
+ headers: {
127
+ Authorization: `Bearer ${apiKey}`,
128
+ "Content-Type": "application/json",
129
+ },
130
+ body: JSON.stringify({
131
+ training_file: uploadResult.id,
132
+ model: this.config.baseModel || "gpt-4o-mini-2024-07-18",
133
+ }),
134
+ }
135
+ );
136
+
137
+ if (!ftResponse.ok) {
138
+ const error = await ftResponse.text();
139
+ return { success: false, error: `Fine-tuning failed: ${error}` };
140
+ }
141
+
142
+ const ftResult = await ftResponse.json() as { id: string; fine_tuned_model?: string };
143
+
144
+ return {
145
+ success: true,
146
+ modelId: ftResult.id,
147
+ modelPath: ftResult.fine_tuned_model,
148
+ };
149
+ } finally {
150
+ fs.rmSync(tempDir, { recursive: true, force: true });
151
+ }
152
+ }
153
+
154
+ private async trainModal(trainingData: TrainingData[]): Promise<TrainingResult> {
155
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "modal-train-"));
156
+ const dataFile = path.join(tempDir, "training_data.jsonl");
157
+ const appFile = path.join(tempDir, "train_app.py");
158
+
159
+ // Save training data
160
+ fs.writeFileSync(
161
+ dataFile,
162
+ trainingData.map((d) => JSON.stringify(d)).join("\n")
163
+ );
164
+
165
+ // Create Modal training app
166
+ const modalCode = `
167
+ import modal
168
+ import json
169
+
170
+ app = modal.App("pioneer-finetune")
171
+
172
+ volume = modal.Volume.from_name("pioneer-models", create_if_missing=True)
173
+
174
+ image = modal.Image.debian_slim(python_version="3.11").pip_install([
175
+ "torch",
176
+ "transformers",
177
+ "datasets",
178
+ "accelerate",
179
+ "peft",
180
+ "bitsandbytes",
181
+ ])
182
+
183
+ @app.function(
184
+ image=image,
185
+ gpu="A10G",
186
+ timeout=7200,
187
+ volumes={"/models": volume},
188
+ )
189
+ def finetune(data_json: str, base_model: str, output_name: str):
190
+ import torch
191
+ from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments, Trainer
192
+ from datasets import Dataset
193
+ from peft import LoraConfig, get_peft_model
194
+
195
+ # Parse data
196
+ data = [json.loads(line) for line in data_json.strip().split("\\n")]
197
+
198
+ # Convert to dataset
199
+ texts = []
200
+ for item in data:
201
+ text = ""
202
+ for msg in item.get("messages", []):
203
+ text += f"{msg['role']}: {msg['content']}\\n"
204
+ texts.append(text)
205
+
206
+ dataset = Dataset.from_dict({"text": texts})
207
+
208
+ # Load model
209
+ model = AutoModelForCausalLM.from_pretrained(
210
+ base_model,
211
+ torch_dtype=torch.float16,
212
+ device_map="auto",
213
+ )
214
+ tokenizer = AutoTokenizer.from_pretrained(base_model)
215
+ tokenizer.pad_token = tokenizer.eos_token
216
+
217
+ # Apply LoRA
218
+ lora_config = LoraConfig(r=16, lora_alpha=32, target_modules=["q_proj", "v_proj"])
219
+ model = get_peft_model(model, lora_config)
220
+
221
+ # Tokenize
222
+ def tokenize(examples):
223
+ return tokenizer(examples["text"], truncation=True, max_length=512, padding="max_length")
224
+
225
+ tokenized = dataset.map(tokenize, batched=True)
226
+
227
+ # Train
228
+ training_args = TrainingArguments(
229
+ output_dir=f"/models/{output_name}",
230
+ num_train_epochs=3,
231
+ per_device_train_batch_size=4,
232
+ learning_rate=2e-5,
233
+ save_steps=500,
234
+ logging_steps=10,
235
+ )
236
+
237
+ trainer = Trainer(
238
+ model=model,
239
+ args=training_args,
240
+ train_dataset=tokenized,
241
+ )
242
+ trainer.train()
243
+
244
+ # Save
245
+ trainer.save_model(f"/models/{output_name}")
246
+ volume.commit()
247
+
248
+ return {"success": True, "model_path": f"/models/{output_name}"}
249
+
250
+ @app.local_entrypoint()
251
+ def main():
252
+ import sys
253
+ data_file = sys.argv[1] if len(sys.argv) > 1 else "training_data.jsonl"
254
+ base_model = "${this.config.baseModel || "meta-llama/Llama-2-7b-hf"}"
255
+ output_name = "pioneer-ft-" + str(int(__import__("time").time()))
256
+
257
+ with open(data_file) as f:
258
+ data_json = f.read()
259
+
260
+ result = finetune.remote(data_json, base_model, output_name)
261
+ print(json.dumps(result))
262
+ `;
263
+
264
+ fs.writeFileSync(appFile, modalCode);
265
+
266
+ try {
267
+ return new Promise((resolve) => {
268
+ let stdout = "";
269
+ let stderr = "";
270
+
271
+ const env: NodeJS.ProcessEnv = { ...process.env };
272
+ if (this.config.modalTokenId) {
273
+ env.MODAL_TOKEN_ID = this.config.modalTokenId;
274
+ }
275
+ if (this.config.modalTokenSecret) {
276
+ env.MODAL_TOKEN_SECRET = this.config.modalTokenSecret;
277
+ }
278
+
279
+ const proc = spawn("modal", ["run", appFile, "--", dataFile], {
280
+ stdio: ["pipe", "pipe", "pipe"],
281
+ env,
282
+ });
283
+
284
+ proc.stdout.on("data", (data: Buffer) => {
285
+ stdout += data.toString();
286
+ });
287
+
288
+ proc.stderr.on("data", (data: Buffer) => {
289
+ stderr += data.toString();
290
+ });
291
+
292
+ proc.on("close", (code) => {
293
+ if (code === 0) {
294
+ try {
295
+ const result = JSON.parse(stdout.trim().split("\n").pop() || "{}");
296
+ resolve({
297
+ success: true,
298
+ modelPath: result.model_path,
299
+ });
300
+ } catch {
301
+ resolve({ success: true, modelPath: stdout });
302
+ }
303
+ } else {
304
+ resolve({
305
+ success: false,
306
+ error: stderr || `Exit code: ${code}`,
307
+ });
308
+ }
309
+ });
310
+
311
+ proc.on("error", (err) => {
312
+ resolve({ success: false, error: err.message });
313
+ });
314
+ });
315
+ } finally {
316
+ fs.rmSync(tempDir, { recursive: true, force: true });
317
+ }
318
+ }
319
+
320
+ private async trainLocal(trainingData: TrainingData[]): Promise<TrainingResult> {
321
+ // For local training, we'll save the data and provide instructions
322
+ const outputPath = path.join(
323
+ this.outputDir,
324
+ `training_${Date.now()}.jsonl`
325
+ );
326
+
327
+ fs.writeFileSync(
328
+ outputPath,
329
+ trainingData.map((d) => JSON.stringify(d)).join("\n")
330
+ );
331
+
332
+ return {
333
+ success: true,
334
+ modelPath: outputPath,
335
+ metrics: {
336
+ epochs: 0,
337
+ },
338
+ };
339
+ }
340
+
341
+ // Prompt optimization (alternative to full fine-tuning)
342
+ async optimizePrompt(
343
+ currentPrompt: string,
344
+ feedback: { positive: string[]; negative: string[] }
345
+ ): Promise<string> {
346
+ // Use the agent itself to improve its system prompt based on feedback
347
+ const optimizationPrompt = `You are a prompt engineer. Given the current system prompt and feedback, create an improved version.
348
+
349
+ Current Prompt:
350
+ ${currentPrompt}
351
+
352
+ Positive Examples (what worked well):
353
+ ${feedback.positive.slice(0, 5).join("\n---\n")}
354
+
355
+ Negative Examples (what didn't work):
356
+ ${feedback.negative.slice(0, 5).join("\n---\n")}
357
+
358
+ Create an improved system prompt that:
359
+ 1. Keeps what's working well
360
+ 2. Addresses the issues in negative examples
361
+ 3. Maintains the core capabilities
362
+ 4. Is clear and actionable
363
+
364
+ Improved Prompt:`;
365
+
366
+ // For now, return the original prompt
367
+ // In a full implementation, this would call an LLM to generate the improved prompt
368
+ return currentPrompt;
369
+ }
370
+ }
371
+
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Evolution module exports
3
+ */
4
+
5
+ export { EvolutionEngine } from "./EvolutionEngine.js";
6
+ export type { EvolutionEngineConfig, EvolutionEvents } from "./EvolutionEngine.js";
7
+
8
+ export { FeedbackCollector } from "./FeedbackCollector.js";
9
+ export type { FeedbackCollectorConfig } from "./FeedbackCollector.js";
10
+
11
+ export { EvalRunner, DEFAULT_EVAL_CASES } from "./EvalRunner.js";
12
+ export type { EvalRunnerConfig } from "./EvalRunner.js";
13
+
14
+ export { ModelTrainer } from "./ModelTrainer.js";
15
+ export type { ModelTrainerConfig, TrainingResult } from "./ModelTrainer.js";
16
+
17
+ export * from "./types.js";
18
+
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Types for the self-evolution system
3
+ */
4
+
5
+ export interface Feedback {
6
+ id: string;
7
+ sessionId: string;
8
+ timestamp: Date;
9
+ userMessage: string;
10
+ agentResponse: string;
11
+ toolCalls: string[];
12
+ rating?: number; // 1-5
13
+ corrections?: string;
14
+ wasSuccessful: boolean;
15
+ metadata?: Record<string, unknown>;
16
+ }
17
+
18
+ export interface EvalCase {
19
+ id: string;
20
+ name: string;
21
+ description: string;
22
+ input: string;
23
+ expectedOutput?: string;
24
+ expectedToolCalls?: string[];
25
+ successCriteria: EvalCriteria[];
26
+ weight?: number;
27
+ }
28
+
29
+ export interface EvalCriteria {
30
+ type: "contains" | "not_contains" | "tool_called" | "tool_not_called" | "regex" | "custom";
31
+ value: string;
32
+ description?: string;
33
+ }
34
+
35
+ export interface EvalResult {
36
+ caseId: string;
37
+ passed: boolean;
38
+ score: number;
39
+ actualOutput: string;
40
+ toolsCalled: string[];
41
+ errors?: string[];
42
+ duration: number;
43
+ tokenUsage: number;
44
+ }
45
+
46
+ export interface EvalRunSummary {
47
+ runId: string;
48
+ timestamp: Date;
49
+ totalCases: number;
50
+ passedCases: number;
51
+ failedCases: number;
52
+ averageScore: number;
53
+ totalTokens: number;
54
+ totalDuration: number;
55
+ results: EvalResult[];
56
+ }
57
+
58
+ export interface EvolutionConfig {
59
+ evalCases: EvalCase[];
60
+ targetScore: number;
61
+ maxIterations: number;
62
+ budgetPerIteration: {
63
+ maxTokens?: number;
64
+ maxCost?: number;
65
+ maxTime?: number;
66
+ };
67
+ feedbackWindow: number; // Number of recent feedback items to consider
68
+ trainingConfig?: {
69
+ provider: "openai" | "anthropic" | "modal";
70
+ baseModel: string;
71
+ fineTuneMethod: "full" | "lora" | "prompt";
72
+ };
73
+ }
74
+
75
+ export interface EvolutionState {
76
+ iteration: number;
77
+ currentScore: number;
78
+ bestScore: number;
79
+ bestPrompt: string;
80
+ history: EvolutionHistory[];
81
+ totalTokensUsed: number;
82
+ totalCostUsed: number;
83
+ totalTimeUsed: number;
84
+ startTime: Date;
85
+ endTime?: Date;
86
+ status: "running" | "completed" | "failed" | "budget_exhausted";
87
+ }
88
+
89
+ export interface EvolutionHistory {
90
+ iteration: number;
91
+ prompt: string;
92
+ evalScore: number;
93
+ changes: string;
94
+ timestamp: Date;
95
+ }
96
+
97
+ export interface TrainingData {
98
+ id: string;
99
+ messages: Array<{
100
+ role: "user" | "assistant" | "system";
101
+ content: string;
102
+ }>;
103
+ toolCalls?: Array<{
104
+ name: string;
105
+ arguments: Record<string, unknown>;
106
+ result: string;
107
+ }>;
108
+ metadata?: Record<string, unknown>;
109
+ }
110
+
package/src/index.tsx CHANGED
@@ -14,8 +14,15 @@ import {
14
14
  getBaseUrl,
15
15
  saveConfig,
16
16
  clearApiKey,
17
+ getAgentConfig,
18
+ getBudgetConfig,
19
+ getSandboxConfig,
20
+ getMLConfig,
21
+ getSystemPrompt,
17
22
  } from "./config.js";
18
23
  import * as api from "./api.js";
24
+ import { ChatApp } from "./chat/ChatApp.js";
25
+ import type { AgentConfig } from "./agent/types.js";
19
26
 
20
27
  // ─────────────────────────────────────────────────────────────────────────────
21
28
  // ASCII Banner
@@ -252,6 +259,17 @@ const Help: React.FC = () => {
252
259
  <Text bold>Usage:</Text>
253
260
  <Text> pioneer {"<command>"} {"[options]"}</Text>
254
261
  <Text> </Text>
262
+ <Text bold>Chat Commands:</Text>
263
+ <Text> chat Start interactive chat agent</Text>
264
+ <Text> --provider {"<name>"} LLM provider (anthropic, openai)</Text>
265
+ <Text> --model {"<model>"} Model to use</Text>
266
+ <Text> --message {"<msg>"} Initial message to process</Text>
267
+ <Text> --max-tokens {"<n>"} Max tokens (default: 500000, 0=unlimited)</Text>
268
+ <Text> --max-cost {"<n>"} Max cost in USD (default: 5.0, 0=unlimited)</Text>
269
+ <Text> --max-time {"<n>"} Max time in seconds (default: 7200, 0=unlimited)</Text>
270
+ <Text> --max-tools {"<n>"} Max tool calls per turn (default: 50, 0=unlimited)</Text>
271
+ <Text> --no-limit Remove all limits</Text>
272
+ <Text> </Text>
255
273
  <Text bold>Auth Commands:</Text>
256
274
  <Text> auth login Login with API key</Text>
257
275
  <Text> auth logout Clear stored API key</Text>
@@ -284,12 +302,88 @@ const Help: React.FC = () => {
284
302
  <Text> --help Show this help</Text>
285
303
  <Text> </Text>
286
304
  <Text dimColor>Environment:</Text>
287
- <Text dimColor> PIONEER_API_URL API base URL (default: http://localhost:5001)</Text>
288
- <Text dimColor> PIONEER_API_KEY API key (overrides saved key)</Text>
305
+ <Text dimColor> PIONEER_API_URL API base URL (default: http://localhost:5001)</Text>
306
+ <Text dimColor> PIONEER_API_KEY API key (overrides saved key)</Text>
307
+ <Text dimColor> ANTHROPIC_API_KEY Anthropic API key for chat agent</Text>
308
+ <Text dimColor> OPENAI_API_KEY OpenAI API key for chat agent</Text>
289
309
  </Box>
290
310
  );
291
311
  };
292
312
 
313
+ // ─────────────────────────────────────────────────────────────────────────────
314
+ // Chat Wrapper Component
315
+ // ─────────────────────────────────────────────────────────────────────────────
316
+
317
+ interface ChatWrapperProps {
318
+ flags: Record<string, string>;
319
+ }
320
+
321
+ const ChatWrapper: React.FC<ChatWrapperProps> = ({ flags }) => {
322
+ const { exit } = useApp();
323
+
324
+ // Build agent config from flags and stored config
325
+ const storedConfig = getAgentConfig();
326
+ const budgetConfig = getBudgetConfig();
327
+ const systemPrompt = getSystemPrompt();
328
+
329
+ // Apply budget overrides from flags
330
+ const budget = { ...budgetConfig };
331
+ let maxToolCalls = 50; // Default
332
+
333
+ if (flags["no-limit"] === "true") {
334
+ // Remove all limits
335
+ budget.maxTokens = undefined;
336
+ budget.maxCost = undefined;
337
+ budget.maxTime = undefined;
338
+ budget.maxIterations = undefined;
339
+ maxToolCalls = 1000; // Effectively unlimited
340
+ } else {
341
+ // Apply individual overrides (0 = unlimited)
342
+ if (flags["max-tokens"]) {
343
+ const val = parseInt(flags["max-tokens"], 10);
344
+ budget.maxTokens = val === 0 ? undefined : val;
345
+ }
346
+ if (flags["max-cost"]) {
347
+ const val = parseFloat(flags["max-cost"]);
348
+ budget.maxCost = val === 0 ? undefined : val;
349
+ }
350
+ if (flags["max-time"]) {
351
+ const val = parseInt(flags["max-time"], 10);
352
+ budget.maxTime = val === 0 ? undefined : val;
353
+ }
354
+ if (flags["max-tools"]) {
355
+ const val = parseInt(flags["max-tools"], 10);
356
+ maxToolCalls = val === 0 ? 1000 : val;
357
+ }
358
+ }
359
+
360
+ const agentConfig: AgentConfig = {
361
+ provider: (flags.provider as "anthropic" | "openai") || storedConfig.provider,
362
+ model: flags.model || storedConfig.model,
363
+ apiKey: storedConfig.apiKey,
364
+ baseUrl: storedConfig.baseUrl,
365
+ budget,
366
+ systemPrompt,
367
+ maxToolCalls,
368
+ };
369
+
370
+ // Validate we have an API key
371
+ if (!agentConfig.apiKey) {
372
+ return (
373
+ <Box flexDirection="column">
374
+ <ErrorMessage error="No API key configured. Set ANTHROPIC_API_KEY or OPENAI_API_KEY environment variable." />
375
+ <Text dimColor>
376
+ Or run: export ANTHROPIC_API_KEY="your-key"
377
+ </Text>
378
+ </Box>
379
+ );
380
+ }
381
+
382
+ const initialMessage = flags.message;
383
+
384
+ return <ChatApp config={agentConfig} initialMessage={initialMessage} />;
385
+ };
386
+
293
387
  // ─────────────────────────────────────────────────────────────────────────────
294
388
  // Main Router
295
389
  // ─────────────────────────────────────────────────────────────────────────────
@@ -307,6 +401,11 @@ const App: React.FC<AppProps> = ({ command, flags }) => {
307
401
  return <Help />;
308
402
  }
309
403
 
404
+ // Chat command - Interactive agent
405
+ if (group === "chat") {
406
+ return <ChatWrapper flags={flags} />;
407
+ }
408
+
310
409
  // Auth commands
311
410
  if (group === "auth") {
312
411
  if (action === "login") return <AuthLogin />;