@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.
@@ -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
+ ```