@amitdeshmukh/ax-crew 7.0.0 → 8.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.
- package/CHANGELOG.md +17 -0
- package/README.md +104 -0
- package/dist/agents/ace.d.ts +134 -0
- package/dist/agents/ace.js +477 -0
- package/dist/agents/agentConfig.d.ts +1 -0
- package/dist/agents/agentConfig.js +1 -0
- package/dist/agents/index.d.ts +83 -1
- package/dist/agents/index.js +359 -4
- package/dist/index.d.ts +3 -3
- package/dist/types.d.ts +39 -1
- package/examples/README.md +46 -8
- package/examples/ace-customer-support.ts +480 -0
- package/examples/ace-flight-finder.ts +329 -0
- package/examples/telemetry-demo.ts +0 -1
- package/package.json +1 -1
- package/plan.md +255 -0
- package/playbooks/customer-support.json +32 -0
- package/playbooks/flight-assistant.json +23 -0
- package/src/agents/ace.ts +594 -0
- package/src/agents/agentConfig.ts +1 -0
- package/src/agents/index.ts +408 -6
- package/src/index.ts +14 -2
- package/src/types.ts +52 -1
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ACE (Agentic Context Engineering) integration for AxCrew
|
|
3
|
+
*
|
|
4
|
+
* This module provides helpers to build and manage AxACE optimizers for agents,
|
|
5
|
+
* enabling offline compilation and online learning from feedback.
|
|
6
|
+
*
|
|
7
|
+
* Reference: https://axllm.dev/ace/
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { AxACE, ai as buildAI, type AxMetricFn, AxSignature, AxGen } from "@ax-llm/ax";
|
|
11
|
+
import type { AxAI } from "@ax-llm/ax";
|
|
12
|
+
import type {
|
|
13
|
+
ACEConfig,
|
|
14
|
+
ACEPersistenceConfig,
|
|
15
|
+
ACEMetricConfig,
|
|
16
|
+
ACETeacherConfig,
|
|
17
|
+
FunctionRegistryType
|
|
18
|
+
} from "../types.js";
|
|
19
|
+
|
|
20
|
+
// Re-export types for convenience
|
|
21
|
+
export type { AxACE, AxMetricFn };
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create an empty playbook structure
|
|
25
|
+
*/
|
|
26
|
+
export const createEmptyPlaybook = (): ACEPlaybook => {
|
|
27
|
+
const now = new Date().toISOString();
|
|
28
|
+
return {
|
|
29
|
+
version: 1,
|
|
30
|
+
sections: {},
|
|
31
|
+
stats: {
|
|
32
|
+
bulletCount: 0,
|
|
33
|
+
helpfulCount: 0,
|
|
34
|
+
harmfulCount: 0,
|
|
35
|
+
tokenEstimate: 0,
|
|
36
|
+
},
|
|
37
|
+
updatedAt: now,
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Playbook types (mirroring AxACEPlaybook structure)
|
|
43
|
+
*/
|
|
44
|
+
export interface ACEBullet {
|
|
45
|
+
id: string;
|
|
46
|
+
section: string;
|
|
47
|
+
content: string;
|
|
48
|
+
helpfulCount: number;
|
|
49
|
+
harmfulCount: number;
|
|
50
|
+
createdAt: string;
|
|
51
|
+
updatedAt: string;
|
|
52
|
+
metadata?: Record<string, unknown>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ACEPlaybook {
|
|
56
|
+
version: number;
|
|
57
|
+
sections: Record<string, ACEBullet[]>;
|
|
58
|
+
stats: {
|
|
59
|
+
bulletCount: number;
|
|
60
|
+
helpfulCount: number;
|
|
61
|
+
harmfulCount: number;
|
|
62
|
+
tokenEstimate: number;
|
|
63
|
+
};
|
|
64
|
+
updatedAt: string;
|
|
65
|
+
description?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Render a playbook into markdown instruction block for injection into prompts.
|
|
70
|
+
* Mirrors the AxACE renderPlaybook function.
|
|
71
|
+
*/
|
|
72
|
+
export const renderPlaybook = (playbook: Readonly<ACEPlaybook>): string => {
|
|
73
|
+
if (!playbook) return '';
|
|
74
|
+
|
|
75
|
+
const sectionsObj = playbook.sections || {};
|
|
76
|
+
const header = playbook.description
|
|
77
|
+
? `## Context Playbook\n${playbook.description.trim()}\n`
|
|
78
|
+
: '## Context Playbook\n';
|
|
79
|
+
|
|
80
|
+
const sectionEntries = Object.entries(sectionsObj);
|
|
81
|
+
if (sectionEntries.length === 0) return '';
|
|
82
|
+
|
|
83
|
+
const sections = sectionEntries
|
|
84
|
+
.map(([sectionName, bullets]) => {
|
|
85
|
+
const body = bullets
|
|
86
|
+
.map((bullet) => `- [${bullet.id}] ${bullet.content}`)
|
|
87
|
+
.join('\n');
|
|
88
|
+
return body
|
|
89
|
+
? `### ${sectionName}\n${body}`
|
|
90
|
+
: `### ${sectionName}\n_(empty)_`;
|
|
91
|
+
})
|
|
92
|
+
.join('\n\n');
|
|
93
|
+
|
|
94
|
+
return `${header}\n${sections}`.trim();
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Check if running in Node.js environment (for file operations)
|
|
99
|
+
*/
|
|
100
|
+
const isNodeLike = (): boolean => {
|
|
101
|
+
try {
|
|
102
|
+
return typeof process !== "undefined" && !!process.versions?.node;
|
|
103
|
+
} catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Read JSON file (Node.js only)
|
|
110
|
+
*/
|
|
111
|
+
const readFileJSON = async (path: string): Promise<any | undefined> => {
|
|
112
|
+
if (!isNodeLike()) return undefined;
|
|
113
|
+
try {
|
|
114
|
+
const { readFile } = await import("fs/promises");
|
|
115
|
+
const buf = await readFile(path, "utf-8");
|
|
116
|
+
return JSON.parse(buf);
|
|
117
|
+
} catch {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Write JSON file (Node.js only)
|
|
124
|
+
*/
|
|
125
|
+
const writeFileJSON = async (path: string, data: any): Promise<void> => {
|
|
126
|
+
if (!isNodeLike()) return;
|
|
127
|
+
try {
|
|
128
|
+
const { mkdir, writeFile } = await import("fs/promises");
|
|
129
|
+
const { dirname } = await import("path");
|
|
130
|
+
await mkdir(dirname(path), { recursive: true });
|
|
131
|
+
await writeFile(path, JSON.stringify(data ?? {}, null, 2), "utf-8");
|
|
132
|
+
} catch {
|
|
133
|
+
// Swallow persistence errors by default
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Resolve environment variable
|
|
139
|
+
*/
|
|
140
|
+
const resolveEnv = (name: string): string | undefined => {
|
|
141
|
+
try {
|
|
142
|
+
if (typeof process !== "undefined" && process?.env) {
|
|
143
|
+
return process.env[name];
|
|
144
|
+
}
|
|
145
|
+
return (globalThis as any)?.[name];
|
|
146
|
+
} catch {
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Build teacher AI instance from config, falling back to student AI
|
|
153
|
+
*/
|
|
154
|
+
const buildTeacherAI = (teacherCfg: ACETeacherConfig | undefined, fallback: AxAI): AxAI => {
|
|
155
|
+
if (!teacherCfg) return fallback;
|
|
156
|
+
|
|
157
|
+
const { provider, providerKeyName, apiURL, ai: aiConfig, providerArgs } = teacherCfg;
|
|
158
|
+
if (!provider || !providerKeyName || !aiConfig) return fallback;
|
|
159
|
+
|
|
160
|
+
const apiKey = resolveEnv(providerKeyName) || "";
|
|
161
|
+
if (!apiKey) return fallback;
|
|
162
|
+
|
|
163
|
+
const args: any = {
|
|
164
|
+
name: provider,
|
|
165
|
+
apiKey,
|
|
166
|
+
config: aiConfig,
|
|
167
|
+
options: {}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
if (apiURL) args.apiURL = apiURL;
|
|
171
|
+
if (providerArgs && typeof providerArgs === "object") {
|
|
172
|
+
Object.assign(args, providerArgs);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
return buildAI(args);
|
|
177
|
+
} catch {
|
|
178
|
+
return fallback;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Build an AxACE optimizer for an agent
|
|
184
|
+
*
|
|
185
|
+
* @param studentAI - The agent's AI instance (used as student)
|
|
186
|
+
* @param cfg - ACE configuration
|
|
187
|
+
* @returns Configured AxACE optimizer
|
|
188
|
+
*/
|
|
189
|
+
export const buildACEOptimizer = (studentAI: AxAI, cfg: ACEConfig): AxACE => {
|
|
190
|
+
const teacherAI = buildTeacherAI(cfg.teacher, studentAI);
|
|
191
|
+
|
|
192
|
+
// Build optimizer options, only include initialPlaybook if it has the right structure
|
|
193
|
+
const optimizerOptions: any = {
|
|
194
|
+
maxEpochs: cfg.options?.maxEpochs,
|
|
195
|
+
allowDynamicSections: cfg.options?.allowDynamicSections,
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// Only pass initialPlaybook if it looks like a valid playbook structure
|
|
199
|
+
if (cfg.persistence?.initialPlaybook &&
|
|
200
|
+
typeof cfg.persistence.initialPlaybook === 'object' &&
|
|
201
|
+
'sections' in cfg.persistence.initialPlaybook) {
|
|
202
|
+
optimizerOptions.initialPlaybook = cfg.persistence.initialPlaybook;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return new AxACE(
|
|
206
|
+
{
|
|
207
|
+
studentAI,
|
|
208
|
+
teacherAI,
|
|
209
|
+
verbose: !!cfg.options?.maxEpochs
|
|
210
|
+
},
|
|
211
|
+
optimizerOptions
|
|
212
|
+
);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Load initial playbook from file, callback, or inline config
|
|
217
|
+
*
|
|
218
|
+
* @param cfg - Persistence configuration
|
|
219
|
+
* @returns Loaded playbook or undefined
|
|
220
|
+
*/
|
|
221
|
+
export const loadInitialPlaybook = async (cfg?: ACEPersistenceConfig): Promise<any | undefined> => {
|
|
222
|
+
if (!cfg) return undefined;
|
|
223
|
+
|
|
224
|
+
// Try callback first
|
|
225
|
+
if (typeof cfg.onLoad === "function") {
|
|
226
|
+
try {
|
|
227
|
+
return await cfg.onLoad();
|
|
228
|
+
} catch {
|
|
229
|
+
// Fall through to other methods
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Try inline playbook
|
|
234
|
+
if (cfg.initialPlaybook) {
|
|
235
|
+
return cfg.initialPlaybook;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Try file path
|
|
239
|
+
if (cfg.playbookPath) {
|
|
240
|
+
return await readFileJSON(cfg.playbookPath);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return undefined;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Persist playbook to file or via callback
|
|
248
|
+
*
|
|
249
|
+
* @param pb - Playbook to persist
|
|
250
|
+
* @param cfg - Persistence configuration
|
|
251
|
+
*/
|
|
252
|
+
export const persistPlaybook = async (pb: any, cfg?: ACEPersistenceConfig): Promise<void> => {
|
|
253
|
+
if (!cfg || !pb) return;
|
|
254
|
+
|
|
255
|
+
// Call persist callback if provided
|
|
256
|
+
if (typeof cfg.onPersist === "function") {
|
|
257
|
+
try {
|
|
258
|
+
await cfg.onPersist(pb);
|
|
259
|
+
} catch {
|
|
260
|
+
// Ignore callback errors
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Write to file if auto-persist enabled
|
|
265
|
+
if (cfg.autoPersist && cfg.playbookPath) {
|
|
266
|
+
await writeFileJSON(cfg.playbookPath, pb);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Resolve metric function from registry or create equality-based metric
|
|
272
|
+
*
|
|
273
|
+
* @param cfg - Metric configuration
|
|
274
|
+
* @param registry - Function registry to search
|
|
275
|
+
* @returns Metric function or undefined
|
|
276
|
+
*/
|
|
277
|
+
export const resolveMetric = (
|
|
278
|
+
cfg: ACEMetricConfig | undefined,
|
|
279
|
+
registry: FunctionRegistryType
|
|
280
|
+
): AxMetricFn | undefined => {
|
|
281
|
+
if (!cfg) return undefined;
|
|
282
|
+
|
|
283
|
+
const { metricFnName, primaryOutputField } = cfg;
|
|
284
|
+
|
|
285
|
+
// Try to find a function by name in the registry
|
|
286
|
+
if (metricFnName) {
|
|
287
|
+
const candidate = (registry as any)[metricFnName];
|
|
288
|
+
if (typeof candidate === "function") {
|
|
289
|
+
return candidate as AxMetricFn;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Create simple equality-based metric if primary output field specified
|
|
294
|
+
if (primaryOutputField) {
|
|
295
|
+
const field = primaryOutputField;
|
|
296
|
+
return ({ prediction, example }: { prediction: any; example: any }) => {
|
|
297
|
+
try {
|
|
298
|
+
return prediction?.[field] === example?.[field] ? 1 : 0;
|
|
299
|
+
} catch {
|
|
300
|
+
return 0;
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return undefined;
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Run offline ACE compilation
|
|
310
|
+
*
|
|
311
|
+
* @param args - Compilation arguments
|
|
312
|
+
* @returns Compilation result with optimized program
|
|
313
|
+
*/
|
|
314
|
+
export const runOfflineCompile = async (args: {
|
|
315
|
+
program: any;
|
|
316
|
+
optimizer: AxACE;
|
|
317
|
+
metric: AxMetricFn;
|
|
318
|
+
examples: any[];
|
|
319
|
+
persistence?: ACEPersistenceConfig;
|
|
320
|
+
}): Promise<any> => {
|
|
321
|
+
const { program, optimizer, metric, examples = [], persistence } = args;
|
|
322
|
+
|
|
323
|
+
if (!optimizer || !metric || examples.length === 0) {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
try {
|
|
328
|
+
// Run compilation
|
|
329
|
+
const result = await optimizer.compile(program, examples, metric);
|
|
330
|
+
|
|
331
|
+
// Extract and persist playbook
|
|
332
|
+
const playbook = result?.artifact?.playbook;
|
|
333
|
+
if (playbook && persistence) {
|
|
334
|
+
await persistPlaybook(playbook, persistence);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return result;
|
|
338
|
+
} catch (error) {
|
|
339
|
+
console.warn("ACE offline compile failed:", error);
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Apply online update with feedback
|
|
346
|
+
*
|
|
347
|
+
* @param args - Update arguments
|
|
348
|
+
* @returns Curator delta (operations applied)
|
|
349
|
+
*/
|
|
350
|
+
export const runOnlineUpdate = async (args: {
|
|
351
|
+
optimizer: AxACE;
|
|
352
|
+
example: any;
|
|
353
|
+
prediction: any;
|
|
354
|
+
feedback?: string;
|
|
355
|
+
persistence?: ACEPersistenceConfig;
|
|
356
|
+
tokenBudget?: number; // Reserved for future use
|
|
357
|
+
debug?: boolean;
|
|
358
|
+
}): Promise<any> => {
|
|
359
|
+
const { optimizer, example, prediction, feedback, persistence, debug } = args;
|
|
360
|
+
|
|
361
|
+
if (!optimizer) return null;
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
// Apply online update (per ACE API: example, prediction, feedback)
|
|
365
|
+
const curatorDelta = await optimizer.applyOnlineUpdate({
|
|
366
|
+
example,
|
|
367
|
+
prediction,
|
|
368
|
+
feedback
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// Access the optimizer's private playbook property
|
|
372
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
373
|
+
const playbook = (optimizer as any).playbook;
|
|
374
|
+
|
|
375
|
+
// Persist updated playbook if we have one and persistence is configured
|
|
376
|
+
if (playbook && persistence?.autoPersist) {
|
|
377
|
+
await persistPlaybook(playbook, persistence);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return curatorDelta;
|
|
381
|
+
} catch (error) {
|
|
382
|
+
// AxACE's reflector sometimes returns bulletTags in non-array format, causing iteration errors.
|
|
383
|
+
// This is a known issue - we fall back to direct playbook updates via addFeedbackToPlaybook.
|
|
384
|
+
if (debug) {
|
|
385
|
+
console.warn("[ACE Debug] AxACE applyOnlineUpdate failed (falling back to direct update):", error);
|
|
386
|
+
}
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Generate a unique bullet ID (mirrors AxACE's generateBulletId)
|
|
393
|
+
*/
|
|
394
|
+
const generateBulletId = (section: string): string => {
|
|
395
|
+
const normalized = section
|
|
396
|
+
.toLowerCase()
|
|
397
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
398
|
+
.replace(/^-+|-+$/g, '')
|
|
399
|
+
.slice(0, 6);
|
|
400
|
+
const randomHex = Math.random().toString(16).slice(2, 10);
|
|
401
|
+
return `${normalized || 'ctx'}-${randomHex}`;
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Recompute playbook stats after modifications
|
|
406
|
+
*/
|
|
407
|
+
const recomputePlaybookStats = (playbook: ACEPlaybook): void => {
|
|
408
|
+
let bulletCount = 0;
|
|
409
|
+
let helpfulCount = 0;
|
|
410
|
+
let harmfulCount = 0;
|
|
411
|
+
let tokenEstimate = 0;
|
|
412
|
+
|
|
413
|
+
const sections = playbook.sections || {};
|
|
414
|
+
for (const bullets of Object.values(sections)) {
|
|
415
|
+
for (const bullet of bullets) {
|
|
416
|
+
bulletCount += 1;
|
|
417
|
+
helpfulCount += bullet.helpfulCount;
|
|
418
|
+
harmfulCount += bullet.harmfulCount;
|
|
419
|
+
tokenEstimate += Math.ceil(bullet.content.length / 4);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
playbook.stats = { bulletCount, helpfulCount, harmfulCount, tokenEstimate };
|
|
424
|
+
playbook.updatedAt = new Date().toISOString();
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Apply curator operations to playbook (mirrors AxACE's applyCuratorOperations)
|
|
429
|
+
*/
|
|
430
|
+
const applyCuratorOperations = (
|
|
431
|
+
playbook: ACEPlaybook,
|
|
432
|
+
operations: Array<{ type: 'ADD' | 'UPDATE' | 'REMOVE'; section: string; content?: string; bulletId?: string }>
|
|
433
|
+
): void => {
|
|
434
|
+
// Ensure playbook has sections initialized
|
|
435
|
+
if (!playbook.sections) {
|
|
436
|
+
playbook.sections = {};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const now = new Date().toISOString();
|
|
440
|
+
|
|
441
|
+
for (const op of operations) {
|
|
442
|
+
if (!op.section) continue;
|
|
443
|
+
|
|
444
|
+
// Initialize section if needed
|
|
445
|
+
if (!playbook.sections[op.section]) {
|
|
446
|
+
playbook.sections[op.section] = [];
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const section = playbook.sections[op.section]!;
|
|
450
|
+
|
|
451
|
+
switch (op.type) {
|
|
452
|
+
case 'ADD': {
|
|
453
|
+
if (!op.content?.trim()) continue;
|
|
454
|
+
|
|
455
|
+
// Check for duplicates
|
|
456
|
+
const isDuplicate = section.some(
|
|
457
|
+
b => b.content.toLowerCase() === op.content!.toLowerCase()
|
|
458
|
+
);
|
|
459
|
+
if (isDuplicate) continue;
|
|
460
|
+
|
|
461
|
+
const bullet: ACEBullet = {
|
|
462
|
+
id: op.bulletId || generateBulletId(op.section),
|
|
463
|
+
section: op.section,
|
|
464
|
+
content: op.content.trim(),
|
|
465
|
+
helpfulCount: 1,
|
|
466
|
+
harmfulCount: 0,
|
|
467
|
+
createdAt: now,
|
|
468
|
+
updatedAt: now,
|
|
469
|
+
};
|
|
470
|
+
section.push(bullet);
|
|
471
|
+
break;
|
|
472
|
+
}
|
|
473
|
+
case 'UPDATE': {
|
|
474
|
+
if (!op.bulletId) continue;
|
|
475
|
+
const bullet = section.find(b => b.id === op.bulletId);
|
|
476
|
+
if (bullet && op.content) {
|
|
477
|
+
bullet.content = op.content.trim();
|
|
478
|
+
bullet.updatedAt = now;
|
|
479
|
+
}
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
case 'REMOVE': {
|
|
483
|
+
if (!op.bulletId) continue;
|
|
484
|
+
const idx = section.findIndex(b => b.id === op.bulletId);
|
|
485
|
+
if (idx >= 0) section.splice(idx, 1);
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
recomputePlaybookStats(playbook);
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
// Cached feedback analyzer program (created lazily)
|
|
495
|
+
let feedbackAnalyzerProgram: AxGen<any, any> | null = null;
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Get or create the feedback analyzer program.
|
|
499
|
+
* Uses AxGen with a proper signature, just like AxACE's reflector/curator.
|
|
500
|
+
*
|
|
501
|
+
* Uses `class` type for section to get type-safe enums and better token efficiency.
|
|
502
|
+
* See: https://axllm.dev/signatures/
|
|
503
|
+
*/
|
|
504
|
+
const getOrCreateFeedbackAnalyzer = (): AxGen<any, any> => {
|
|
505
|
+
if (!feedbackAnalyzerProgram) {
|
|
506
|
+
const signature = new AxSignature(
|
|
507
|
+
`feedback:string "User feedback to analyze"
|
|
508
|
+
->
|
|
509
|
+
section:class "Guidelines, Response Strategies, Common Pitfalls, Root Cause Notes" "Playbook section category",
|
|
510
|
+
content:string "The specific instruction to add to the playbook - keep all concrete details"`
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
signature.setDescription(
|
|
514
|
+
`Convert user feedback into a playbook instruction. Keep ALL specific details from the feedback (times, names, numbers, constraints).`
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
feedbackAnalyzerProgram = new AxGen(signature);
|
|
518
|
+
}
|
|
519
|
+
return feedbackAnalyzerProgram;
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Use LLM to analyze feedback and generate playbook operations.
|
|
524
|
+
*
|
|
525
|
+
* This leverages AxGen with a proper signature (like AxACE's reflector/curator)
|
|
526
|
+
* to properly categorize feedback and extract actionable insights.
|
|
527
|
+
*
|
|
528
|
+
* IMPORTANT: The prompt explicitly tells the LLM to preserve specificity.
|
|
529
|
+
*
|
|
530
|
+
* @param ai - The AI instance to use for analysis
|
|
531
|
+
* @param feedback - User feedback string
|
|
532
|
+
* @param debug - Whether to log debug info
|
|
533
|
+
* @returns Promise of curator operations
|
|
534
|
+
*/
|
|
535
|
+
export const analyzeAndCategorizeFeedback = async (
|
|
536
|
+
ai: AxAI,
|
|
537
|
+
feedback: string,
|
|
538
|
+
debug = false
|
|
539
|
+
): Promise<Array<{ type: 'ADD' | 'UPDATE' | 'REMOVE'; section: string; content: string }>> => {
|
|
540
|
+
if (!feedback?.trim()) return [];
|
|
541
|
+
|
|
542
|
+
try {
|
|
543
|
+
const analyzer = getOrCreateFeedbackAnalyzer();
|
|
544
|
+
|
|
545
|
+
const result = await analyzer.forward(ai, {
|
|
546
|
+
feedback: feedback.trim(),
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
if (debug) {
|
|
550
|
+
console.log('[ACE Debug] Feedback analysis result:', result);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Section is guaranteed to be valid by the class type constraint
|
|
554
|
+
const section = result.section || 'Guidelines';
|
|
555
|
+
// Use the LLM's content, but fall back to raw feedback if empty
|
|
556
|
+
const content = result.content?.trim() || feedback.trim();
|
|
557
|
+
|
|
558
|
+
return [{ type: 'ADD', section, content }];
|
|
559
|
+
} catch (error) {
|
|
560
|
+
if (debug) {
|
|
561
|
+
console.warn('[ACE Debug] Feedback analysis failed, using raw feedback:', error);
|
|
562
|
+
}
|
|
563
|
+
// Fallback: use the raw feedback as-is
|
|
564
|
+
return [{ type: 'ADD', section: 'Guidelines', content: feedback.trim() }];
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Add feedback to playbook using LLM analysis.
|
|
570
|
+
*
|
|
571
|
+
* Uses the AI to properly understand and categorize the feedback,
|
|
572
|
+
* then applies it as a curator operation.
|
|
573
|
+
*
|
|
574
|
+
* @param playbook - The playbook to update (mutated in place)
|
|
575
|
+
* @param feedback - User feedback string to add
|
|
576
|
+
* @param ai - AI instance for smart categorization
|
|
577
|
+
* @param debug - Whether to log debug info
|
|
578
|
+
*/
|
|
579
|
+
export const addFeedbackToPlaybook = async (
|
|
580
|
+
playbook: ACEPlaybook,
|
|
581
|
+
feedback: string,
|
|
582
|
+
ai: AxAI,
|
|
583
|
+
debug = false
|
|
584
|
+
): Promise<void> => {
|
|
585
|
+
if (!playbook || !feedback?.trim()) return;
|
|
586
|
+
|
|
587
|
+
// Use LLM to categorize feedback while preserving specificity
|
|
588
|
+
const operations = await analyzeAndCategorizeFeedback(ai, feedback, debug);
|
|
589
|
+
|
|
590
|
+
if (operations.length > 0) {
|
|
591
|
+
applyCuratorOperations(playbook, operations);
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
|
|
@@ -210,6 +210,7 @@ const parseAgentConfig = async (
|
|
|
210
210
|
subAgentNames: agentConfigData.agents || [],
|
|
211
211
|
examples: agentConfigData.examples || [],
|
|
212
212
|
tracker: costTracker,
|
|
213
|
+
debug: (agentConfigData as any).options?.debug ?? (agentConfigData as any).debug ?? false,
|
|
213
214
|
};
|
|
214
215
|
} catch (error) {
|
|
215
216
|
if (error instanceof Error) {
|