@positronic/template-new-project 0.0.2
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/.claude/settings.local.json +8 -0
- package/CLAUDE.md +215 -0
- package/index.js +180 -0
- package/package.json +18 -0
- package/template/.positronic/package.json +17 -0
- package/template/.positronic/src/index.ts +35 -0
- package/template/.positronic/tsconfig.json +19 -0
- package/template/.positronic/wrangler.jsonc +53 -0
- package/template/CLAUDE.md +120 -0
- package/template/_env +32 -0
- package/template/_gitignore +24 -0
- package/template/brain.ts +59 -0
- package/template/brains/example.ts +13 -0
- package/template/docs/brain-dsl-guide.md +601 -0
- package/template/docs/brain-testing-guide.md +307 -0
- package/template/docs/positronic-guide.md +236 -0
- package/template/docs/tips-for-agents.md +425 -0
- package/template/jest.config.js +24 -0
- package/template/package.json +29 -0
- package/template/positronic.config.json +8 -0
- package/template/resources/example.md +0 -0
- package/template/runner.ts +9 -0
- package/template/tests/example.test.ts +20 -0
- package/template/tsconfig.json +16 -0
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
# Brain DSL User Guide
|
|
2
|
+
|
|
3
|
+
This guide explains how to use the Positronic Brain DSL to create AI-powered workflows.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The Brain DSL provides a fluent, type-safe API for building stateful AI workflows. Brains are composed of steps that transform state, with full TypeScript type inference throughout the chain.
|
|
8
|
+
|
|
9
|
+
**Note**: This project uses a custom brain function. Always import `brain` from `../brain.js`, not from `@positronic/core`. See positronic-guide.md for details.
|
|
10
|
+
|
|
11
|
+
## Basic Brain Structure
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { brain } from '../brain.js';
|
|
15
|
+
import { z } from 'zod';
|
|
16
|
+
|
|
17
|
+
const myBrain = brain('My First Brain')
|
|
18
|
+
.step('Initialize', ({ state }) => ({
|
|
19
|
+
count: 0,
|
|
20
|
+
message: 'Starting...',
|
|
21
|
+
}))
|
|
22
|
+
.step('Process', ({ state }) => ({
|
|
23
|
+
...state,
|
|
24
|
+
count: state.count + 1,
|
|
25
|
+
processed: true,
|
|
26
|
+
}));
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Step Types
|
|
30
|
+
|
|
31
|
+
### 1. Basic Steps
|
|
32
|
+
|
|
33
|
+
Transform state with synchronous or asynchronous functions:
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
brain('Example')
|
|
37
|
+
.step('Sync Step', ({ state }) => ({
|
|
38
|
+
...state,
|
|
39
|
+
updated: true,
|
|
40
|
+
}))
|
|
41
|
+
.step('Async Step', async ({ state, client }) => {
|
|
42
|
+
const data = await fetchSomeData();
|
|
43
|
+
return { ...state, data };
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2. Prompt Steps
|
|
48
|
+
|
|
49
|
+
Generate structured output from AI models. Here's a complete example that shows how to chain prompts:
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
brain('AI Education Assistant')
|
|
53
|
+
.step('Initialize', ({ state }) => ({
|
|
54
|
+
...state,
|
|
55
|
+
topic: 'artificial intelligence',
|
|
56
|
+
context: 'We are creating an educational example',
|
|
57
|
+
}))
|
|
58
|
+
.prompt('Generate explanation', {
|
|
59
|
+
template: ({ topic, context }) =>
|
|
60
|
+
<%= "`${context}. Please provide a brief, beginner-friendly explanation of ${topic}.`" %>,
|
|
61
|
+
outputSchema: {
|
|
62
|
+
schema: z.object({
|
|
63
|
+
explanation: z.string().describe('A clear explanation of the topic'),
|
|
64
|
+
keyPoints: z.array(z.string()).describe('3-5 key points about the topic'),
|
|
65
|
+
difficulty: z.enum(['beginner', 'intermediate', 'advanced']).describe('The difficulty level'),
|
|
66
|
+
}),
|
|
67
|
+
name: 'topicExplanation' as const,
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
.step('Format output', ({ state }) => ({
|
|
71
|
+
...state,
|
|
72
|
+
formattedOutput: {
|
|
73
|
+
topic: state.topic,
|
|
74
|
+
explanation: state.topicExplanation.explanation || '',
|
|
75
|
+
summary: <%= "`This explanation covers ${state.topicExplanation.keyPoints?.length || 0} key points at a ${state.topicExplanation.difficulty || 'unknown'} level.`" %>,
|
|
76
|
+
points: state.topicExplanation.keyPoints || [],
|
|
77
|
+
},
|
|
78
|
+
}))
|
|
79
|
+
.prompt(
|
|
80
|
+
'Generate follow-up questions',
|
|
81
|
+
{
|
|
82
|
+
template: ({ formattedOutput }) =>
|
|
83
|
+
<%= "`Based on this explanation about ${formattedOutput.topic}: \"${formattedOutput.explanation}\"\n \n Generate 3 thoughtful follow-up questions that a student might ask.`" %>,
|
|
84
|
+
outputSchema: {
|
|
85
|
+
schema: z.object({
|
|
86
|
+
questions: z.array(z.string()).length(3).describe('Three follow-up questions'),
|
|
87
|
+
}),
|
|
88
|
+
name: 'followUpQuestions' as const,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
// Optional: Transform the response before merging with state
|
|
92
|
+
({ state, response }) => ({
|
|
93
|
+
...state,
|
|
94
|
+
followUpQuestions: response.questions,
|
|
95
|
+
finalOutput: {
|
|
96
|
+
...state.formattedOutput,
|
|
97
|
+
questions: response.questions,
|
|
98
|
+
},
|
|
99
|
+
})
|
|
100
|
+
);
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Key points about prompt steps:
|
|
104
|
+
- The `template` function receives the current state and resources, returning the prompt string
|
|
105
|
+
- Templates can be async to load resources: `async (state, resources) => { ... }`
|
|
106
|
+
- `outputSchema` defines the structure using Zod schemas
|
|
107
|
+
- The `name` property determines where the response is stored in state
|
|
108
|
+
- You can optionally provide a transform function as the third parameter
|
|
109
|
+
- Type inference works throughout - TypeScript knows about your schema types
|
|
110
|
+
|
|
111
|
+
### 3. Nested Brains
|
|
112
|
+
|
|
113
|
+
Compose complex workflows from smaller brains:
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
const subBrain = brain('Sub Process').step('Transform', ({ state }) => ({
|
|
117
|
+
result: state.input * 2,
|
|
118
|
+
}));
|
|
119
|
+
|
|
120
|
+
const mainBrain = brain('Main Process')
|
|
121
|
+
.step('Prepare', () => ({ value: 10 }))
|
|
122
|
+
.brain(
|
|
123
|
+
'Run Sub Process',
|
|
124
|
+
subBrain,
|
|
125
|
+
({ state, brainState }) => ({
|
|
126
|
+
...state,
|
|
127
|
+
processed: brainState.result,
|
|
128
|
+
}),
|
|
129
|
+
(state) => ({ input: state.value }) // Initial state for sub-brain
|
|
130
|
+
);
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Step Parameters
|
|
134
|
+
|
|
135
|
+
Each step receives these parameters:
|
|
136
|
+
|
|
137
|
+
- `state` - Current state (type-inferred from previous steps)
|
|
138
|
+
- `client` - AI client for generating structured objects
|
|
139
|
+
- `resources` - Loaded resources (files, documents, etc.)
|
|
140
|
+
- `options` - Runtime options passed to the brain
|
|
141
|
+
- Custom services (if configured with `.withServices()`)
|
|
142
|
+
|
|
143
|
+
## Configuration Methods
|
|
144
|
+
|
|
145
|
+
### Default Options
|
|
146
|
+
|
|
147
|
+
Set default options for all brain runs:
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
const configuredBrain = brain('My Brain')
|
|
151
|
+
.withOptions({ debug: true, temperature: 0.7 })
|
|
152
|
+
.step('Process', ({ state, options }) => {
|
|
153
|
+
if (options.debug) console.log('Processing...');
|
|
154
|
+
return state;
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Service Injection
|
|
159
|
+
|
|
160
|
+
The `withServices` method provides dependency injection for your brains, making external services available throughout the workflow while maintaining testability.
|
|
161
|
+
|
|
162
|
+
#### Basic Usage
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
interface MyServices {
|
|
166
|
+
logger: Logger;
|
|
167
|
+
database: Database;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const brainWithServices = brain('Service Brain')
|
|
171
|
+
.withServices<MyServices>({ logger, database })
|
|
172
|
+
.step('Log and Save', async ({ state, logger, database }) => {
|
|
173
|
+
logger.info('Processing state');
|
|
174
|
+
await database.save(state);
|
|
175
|
+
return state;
|
|
176
|
+
});
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
#### Where Services Are Available
|
|
180
|
+
|
|
181
|
+
Services are destructured alongside other parameters in:
|
|
182
|
+
|
|
183
|
+
1. **Step Actions**:
|
|
184
|
+
```typescript
|
|
185
|
+
.step('Process', ({ state, logger, database }) => {
|
|
186
|
+
logger.info('Step executing');
|
|
187
|
+
return state;
|
|
188
|
+
})
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
2. **Prompt Reduce Functions**:
|
|
192
|
+
```typescript
|
|
193
|
+
.prompt('Generate', {
|
|
194
|
+
template: (state) => 'Generate something',
|
|
195
|
+
outputSchema: { schema, name: 'result' as const }
|
|
196
|
+
}, async ({ state, response, logger, database }) => {
|
|
197
|
+
logger.info('Saving AI response');
|
|
198
|
+
await database.save({ ...state, result: response });
|
|
199
|
+
return state;
|
|
200
|
+
})
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
3. **Nested Brain Reducers**:
|
|
204
|
+
```typescript
|
|
205
|
+
.brain('Run Sub-Brain', subBrain, ({ state, brainState, logger }) => {
|
|
206
|
+
logger.info('Sub-brain completed');
|
|
207
|
+
return { ...state, subResult: brainState };
|
|
208
|
+
})
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
#### Real-World Example
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
// Define service interfaces
|
|
215
|
+
interface Services {
|
|
216
|
+
api: {
|
|
217
|
+
fetchData: (id: string) => Promise<Data>;
|
|
218
|
+
submitResult: (result: any) => Promise<void>;
|
|
219
|
+
};
|
|
220
|
+
cache: {
|
|
221
|
+
get: (key: string) => Promise<any>;
|
|
222
|
+
set: (key: string, value: any) => Promise<void>;
|
|
223
|
+
};
|
|
224
|
+
metrics: {
|
|
225
|
+
track: (event: string, properties?: any) => void;
|
|
226
|
+
time: (label: string) => () => void;
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Create a brain with multiple services
|
|
231
|
+
const analysisBrain = brain('Data Analysis')
|
|
232
|
+
.withServices<Services>({
|
|
233
|
+
api: apiClient,
|
|
234
|
+
cache: redisClient,
|
|
235
|
+
metrics: analyticsClient
|
|
236
|
+
})
|
|
237
|
+
.step('Start Timing', ({ metrics }) => {
|
|
238
|
+
const endTimer = metrics.time('analysis_duration');
|
|
239
|
+
return { startTime: Date.now(), endTimer };
|
|
240
|
+
})
|
|
241
|
+
.step('Check Cache', async ({ state, cache, metrics }) => {
|
|
242
|
+
const cached = await cache.get('analysis_result');
|
|
243
|
+
metrics.track('cache_check', { hit: !!cached });
|
|
244
|
+
return { ...state, cached, fromCache: !!cached };
|
|
245
|
+
})
|
|
246
|
+
.step('Fetch If Needed', async ({ state, api }) => {
|
|
247
|
+
if (state.fromCache) return state;
|
|
248
|
+
const data = await api.fetchData('latest');
|
|
249
|
+
return { ...state, data };
|
|
250
|
+
})
|
|
251
|
+
.prompt('Analyze Data', {
|
|
252
|
+
template: ({ data }) => <%= "`Analyze this data: ${JSON.stringify(data)}`" %>,
|
|
253
|
+
outputSchema: {
|
|
254
|
+
schema: z.object({
|
|
255
|
+
insights: z.array(z.string()),
|
|
256
|
+
confidence: z.number()
|
|
257
|
+
}),
|
|
258
|
+
name: 'analysis' as const
|
|
259
|
+
}
|
|
260
|
+
})
|
|
261
|
+
.step('Save Results', async ({ state, api, cache, metrics }) => {
|
|
262
|
+
// Save to cache for next time
|
|
263
|
+
await cache.set('analysis_result', state.analysis);
|
|
264
|
+
|
|
265
|
+
// Submit to API
|
|
266
|
+
await api.submitResult(state.analysis);
|
|
267
|
+
|
|
268
|
+
// Track completion
|
|
269
|
+
state.endTimer(); // End the timer
|
|
270
|
+
metrics.track('analysis_complete', {
|
|
271
|
+
insights_count: state.analysis.insights.length,
|
|
272
|
+
confidence: state.analysis.confidence,
|
|
273
|
+
from_cache: state.fromCache
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
return state;
|
|
277
|
+
});
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
#### Testing with Services
|
|
281
|
+
|
|
282
|
+
Services make testing easier by allowing you to inject mocks:
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
// In your test file
|
|
286
|
+
import { createMockClient, runBrainTest } from '@positronic/core/testing';
|
|
287
|
+
|
|
288
|
+
const mockLogger = {
|
|
289
|
+
info: jest.fn(),
|
|
290
|
+
error: jest.fn()
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const mockDatabase = {
|
|
294
|
+
save: jest.fn().mockResolvedValue(undefined),
|
|
295
|
+
find: jest.fn().mockResolvedValue({ id: '123', name: 'Test' })
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const testBrain = brain('Test Brain')
|
|
299
|
+
.withServices({ logger: mockLogger, database: mockDatabase })
|
|
300
|
+
.step('Do Something', async ({ logger, database }) => {
|
|
301
|
+
logger.info('Fetching data');
|
|
302
|
+
const data = await database.find('123');
|
|
303
|
+
return { data };
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// Run test
|
|
307
|
+
const result = await runBrainTest(testBrain, {
|
|
308
|
+
client: createMockClient()
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// Verify service calls
|
|
312
|
+
expect(mockLogger.info).toHaveBeenCalledWith('Fetching data');
|
|
313
|
+
expect(mockDatabase.find).toHaveBeenCalledWith('123');
|
|
314
|
+
expect(result.finalState.data).toEqual({ id: '123', name: 'Test' });
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
#### Important Notes
|
|
318
|
+
|
|
319
|
+
- Call `withServices` before defining any steps
|
|
320
|
+
- Services are typed - TypeScript knows exactly which services are available
|
|
321
|
+
- Services are not serialized - they're for side effects and external interactions
|
|
322
|
+
- Each brain instance maintains its own service references
|
|
323
|
+
|
|
324
|
+
## Running Brains
|
|
325
|
+
|
|
326
|
+
### Basic Execution
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
const brain = brain('Simple').step('Process', () => ({ result: 'done' }));
|
|
330
|
+
|
|
331
|
+
// Run and collect events
|
|
332
|
+
for await (const event of brain.run({ client: aiClient })) {
|
|
333
|
+
console.log(event.type); // START, STEP_START, STEP_COMPLETE, etc.
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### With Initial State
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
const result = brain.run({
|
|
341
|
+
client: aiClient,
|
|
342
|
+
initialState: { count: 5 },
|
|
343
|
+
resources: myResources,
|
|
344
|
+
options: { verbose: true },
|
|
345
|
+
});
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### Using BrainRunner
|
|
349
|
+
|
|
350
|
+
For production use with adapters and state management:
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
import { BrainRunner } from '@positronic/core';
|
|
354
|
+
|
|
355
|
+
const runner = new BrainRunner({
|
|
356
|
+
client: aiClient,
|
|
357
|
+
adapters: [loggingAdapter],
|
|
358
|
+
resources: resourceLoader
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// Get final state directly
|
|
362
|
+
const finalState = await runner.run(myBrain, {
|
|
363
|
+
initialState: { count: 0 },
|
|
364
|
+
options: { debug: true }
|
|
365
|
+
});
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
## Type Safety
|
|
369
|
+
|
|
370
|
+
The Brain DSL provides complete type inference:
|
|
371
|
+
|
|
372
|
+
```typescript
|
|
373
|
+
const typedBrain = brain('Typed Example')
|
|
374
|
+
.step('Init', () => ({ count: 0 }))
|
|
375
|
+
.step('Add Name', ({ state }) => ({
|
|
376
|
+
...state,
|
|
377
|
+
name: 'Test', // TypeScript knows state has 'count'
|
|
378
|
+
}))
|
|
379
|
+
.step('Use Both', ({ state }) => ({
|
|
380
|
+
message: <%= "`${state.name}: ${state.count}`" %>, // Both properties available
|
|
381
|
+
}));
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
## Events
|
|
385
|
+
|
|
386
|
+
Brains emit events during execution:
|
|
387
|
+
|
|
388
|
+
- `START`/`RESTART` - Brain begins execution
|
|
389
|
+
- `STEP_START` - Step begins
|
|
390
|
+
- `STEP_COMPLETE` - Step completes with state patch
|
|
391
|
+
- `STEP_STATUS` - Status update for all steps
|
|
392
|
+
- `COMPLETE` - Brain finishes successfully
|
|
393
|
+
- `ERROR` - Error occurred
|
|
394
|
+
|
|
395
|
+
## Error Handling
|
|
396
|
+
|
|
397
|
+
Errors in steps emit ERROR events but don't throw:
|
|
398
|
+
|
|
399
|
+
```typescript
|
|
400
|
+
brain('Error Example').step('May Fail', ({ state }) => {
|
|
401
|
+
if (Math.random() > 0.5) {
|
|
402
|
+
throw new Error('Random failure');
|
|
403
|
+
}
|
|
404
|
+
return state;
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
// Handle in event stream
|
|
408
|
+
for await (const event of brain.run({ client })) {
|
|
409
|
+
if (event.type === BRAIN_EVENTS.ERROR) {
|
|
410
|
+
console.error('Step failed:', event.error);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
## Resources
|
|
416
|
+
|
|
417
|
+
Access loaded resources with type-safe API:
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
brain('Resource Example').step('Load Data', async ({ resources }) => {
|
|
421
|
+
const config = await resources.config.loadText();
|
|
422
|
+
const data = await resources.data.records.loadText();
|
|
423
|
+
return { config: JSON.parse(config), data };
|
|
424
|
+
});
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
Resources are also available in prompt templates:
|
|
428
|
+
|
|
429
|
+
```typescript
|
|
430
|
+
brain('Template Example').prompt('Generate Content', {
|
|
431
|
+
template: async (state, resources) => {
|
|
432
|
+
const template = await resources.prompts.customerSupport.loadText();
|
|
433
|
+
return template.replace('{{issue}}', state.issue);
|
|
434
|
+
},
|
|
435
|
+
outputSchema: {
|
|
436
|
+
schema: z.object({ response: z.string() }),
|
|
437
|
+
name: 'supportResponse' as const,
|
|
438
|
+
},
|
|
439
|
+
});
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
## Organizing Complex Prompts
|
|
443
|
+
|
|
444
|
+
When prompts become more than a sentence or two, extract them into separate files for better maintainability:
|
|
445
|
+
|
|
446
|
+
### File Structure
|
|
447
|
+
|
|
448
|
+
For complex brains, organize your code into folders:
|
|
449
|
+
|
|
450
|
+
```
|
|
451
|
+
brains/
|
|
452
|
+
├── hn-bot/
|
|
453
|
+
│ ├── brain.ts # Main brain definition
|
|
454
|
+
│ └── ai-filter-prompt.ts # Complex prompt configuration
|
|
455
|
+
└── simple-bot.ts # Simple brains can stay as single files
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
### Extracting Prompts
|
|
459
|
+
|
|
460
|
+
When you extract a prompt to a separate file, you'll need to explicitly specify the state type:
|
|
461
|
+
|
|
462
|
+
```typescript
|
|
463
|
+
// brains/hn-bot/ai-filter-prompt.ts
|
|
464
|
+
import { z } from 'zod';
|
|
465
|
+
import type { Resources } from '@positronic/core';
|
|
466
|
+
|
|
467
|
+
// Define the state type that this prompt expects, only what the prompt needs
|
|
468
|
+
interface FilterPromptState {
|
|
469
|
+
articles: Array<{
|
|
470
|
+
title: string;
|
|
471
|
+
url: string;
|
|
472
|
+
score: number;
|
|
473
|
+
}>;
|
|
474
|
+
userPreferences?: string;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Export the prompt configuration
|
|
478
|
+
export const aiFilterPrompt = {
|
|
479
|
+
template: async (state: FilterPromptState, resources: Resources) => {
|
|
480
|
+
// Load a prompt template from resources
|
|
481
|
+
const template = await resources.prompts.hnFilter.loadText();
|
|
482
|
+
|
|
483
|
+
// Build the prompt with state data
|
|
484
|
+
const articleList = state.articles
|
|
485
|
+
.map((a, i) => <%= "`${i + 1}. ${a.title} (score: ${a.score})`" %>)
|
|
486
|
+
.join('\n');
|
|
487
|
+
|
|
488
|
+
return template
|
|
489
|
+
.replace('{{articleList}}', articleList)
|
|
490
|
+
.replace('{{preferences}}', state.userPreferences || 'No specific preferences');
|
|
491
|
+
},
|
|
492
|
+
outputSchema: {
|
|
493
|
+
schema: z.object({
|
|
494
|
+
selectedArticles: z.array(z.number()).describe('Indices of selected articles'),
|
|
495
|
+
reasoning: z.string().describe('Brief explanation of selections'),
|
|
496
|
+
}),
|
|
497
|
+
name: 'filterResults' as const,
|
|
498
|
+
},
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
// brains/hn-bot/brain.ts
|
|
502
|
+
import { brain } from '../brain.js';
|
|
503
|
+
import { aiFilterPrompt } from './ai-filter-prompt.js';
|
|
504
|
+
|
|
505
|
+
export default brain('HN Article Filter')
|
|
506
|
+
.step('Fetch Articles', async ({ state }) => {
|
|
507
|
+
// Fetch Hacker News articles
|
|
508
|
+
const articles = await fetchHNArticles();
|
|
509
|
+
return { articles };
|
|
510
|
+
})
|
|
511
|
+
.prompt('Filter Articles', aiFilterPrompt)
|
|
512
|
+
.step('Format Results', ({ state }) => ({
|
|
513
|
+
selectedArticles: state.filterResults.selectedArticles.map(
|
|
514
|
+
i => state.articles[i]
|
|
515
|
+
),
|
|
516
|
+
reasoning: state.filterResults.reasoning,
|
|
517
|
+
}));
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
### When to Extract Prompts
|
|
521
|
+
|
|
522
|
+
Extract prompts to separate files when:
|
|
523
|
+
- The template is more than 2-3 lines
|
|
524
|
+
- The prompt uses complex logic or formatting
|
|
525
|
+
- You need to load resources or templates
|
|
526
|
+
- The prompt might be reused in other brains
|
|
527
|
+
- You want to test the prompt logic separately
|
|
528
|
+
|
|
529
|
+
## Complete Example
|
|
530
|
+
|
|
531
|
+
```typescript
|
|
532
|
+
import { brain } from '../brain.js';
|
|
533
|
+
import { BrainRunner } from '@positronic/core';
|
|
534
|
+
import { z } from 'zod';
|
|
535
|
+
|
|
536
|
+
// Define services
|
|
537
|
+
interface Services {
|
|
538
|
+
logger: Logger;
|
|
539
|
+
analytics: {
|
|
540
|
+
track: (event: string, properties?: any) => void;
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Create brain with all features
|
|
545
|
+
const completeBrain = brain({
|
|
546
|
+
title: 'Complete Example',
|
|
547
|
+
description: 'Demonstrates all Brain DSL features',
|
|
548
|
+
})
|
|
549
|
+
.withOptions({ temperature: 0.7 })
|
|
550
|
+
.withServices<Services>({
|
|
551
|
+
logger: console,
|
|
552
|
+
analytics: {
|
|
553
|
+
track: (event, props) => console.log('Track:', event, props)
|
|
554
|
+
}
|
|
555
|
+
})
|
|
556
|
+
.step('Initialize', ({ logger, analytics }) => {
|
|
557
|
+
logger.log('Starting workflow');
|
|
558
|
+
analytics.track('brain_started');
|
|
559
|
+
return { startTime: Date.now() };
|
|
560
|
+
})
|
|
561
|
+
.prompt('Generate Plan', {
|
|
562
|
+
template: async (state, resources) => {
|
|
563
|
+
// Load a template from resources
|
|
564
|
+
const template = await resources.templates.projectPlan.loadText();
|
|
565
|
+
return template.replace('{{context}}', 'software project');
|
|
566
|
+
},
|
|
567
|
+
outputSchema: {
|
|
568
|
+
schema: z.object({
|
|
569
|
+
tasks: z.array(z.string()),
|
|
570
|
+
duration: z.number(),
|
|
571
|
+
}),
|
|
572
|
+
name: 'plan' as const,
|
|
573
|
+
},
|
|
574
|
+
},
|
|
575
|
+
// Services available in reduce function too
|
|
576
|
+
({ state, response, logger }) => {
|
|
577
|
+
logger.log(<%= "`Plan generated with ${response.tasks.length} tasks`" %>);
|
|
578
|
+
return { ...state, plan: response };
|
|
579
|
+
})
|
|
580
|
+
.step('Process Plan', ({ state, logger, analytics }) => {
|
|
581
|
+
logger.log(<%= "`Processing ${state.plan.tasks.length} tasks`" %>);
|
|
582
|
+
analytics.track('plan_processed', {
|
|
583
|
+
task_count: state.plan.tasks.length,
|
|
584
|
+
duration: state.plan.duration
|
|
585
|
+
});
|
|
586
|
+
return {
|
|
587
|
+
...state,
|
|
588
|
+
taskCount: state.plan.tasks.length,
|
|
589
|
+
endTime: Date.now(),
|
|
590
|
+
};
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
// Run with BrainRunner
|
|
594
|
+
const runner = new BrainRunner({
|
|
595
|
+
client: aiClient,
|
|
596
|
+
adapters: [persistenceAdapter],
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
const finalState = await runner.run(completeBrain);
|
|
600
|
+
console.log('Completed:', finalState);
|
|
601
|
+
```
|