@positronic/template-new-project 0.0.8 → 0.0.9
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/index.js +3 -3
- package/package.json +1 -1
- package/template/CLAUDE.md +1 -1
- package/template/brain.ts +20 -7
- package/template/docs/brain-dsl-guide.md +167 -16
- package/template/docs/brain-testing-guide.md +11 -3
- package/template/docs/positronic-guide.md +14 -7
- package/template/docs/tips-for-agents.md +44 -8
- package/template/tests/example.test.ts +1 -1
- package/template/tests/test-utils.ts +81 -0
package/index.js
CHANGED
|
@@ -53,9 +53,9 @@ module.exports = {
|
|
|
53
53
|
],
|
|
54
54
|
setup: async ctx => {
|
|
55
55
|
const devRootPath = process.env.POSITRONIC_LOCAL_PATH;
|
|
56
|
-
let coreVersion = '^0.0.
|
|
57
|
-
let cloudflareVersion = '^0.0.
|
|
58
|
-
let clientVercelVersion = '^0.0.
|
|
56
|
+
let coreVersion = '^0.0.9';
|
|
57
|
+
let cloudflareVersion = '^0.0.9';
|
|
58
|
+
let clientVercelVersion = '^0.0.9';
|
|
59
59
|
|
|
60
60
|
// Map backend selection to package names
|
|
61
61
|
const backendPackageMap = {
|
package/package.json
CHANGED
package/template/CLAUDE.md
CHANGED
|
@@ -26,7 +26,7 @@ This is a Positronic project - an AI-powered framework for building and running
|
|
|
26
26
|
|
|
27
27
|
### Testing & Building
|
|
28
28
|
|
|
29
|
-
- `npm test` - Run tests (uses Jest with
|
|
29
|
+
- `npm test` - Run tests (uses Jest with local test utilities)
|
|
30
30
|
- `npm run build` - Build the project
|
|
31
31
|
- `npm run dev` - Start development mode with hot reload
|
|
32
32
|
|
package/template/brain.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { brain as coreBrain, type
|
|
1
|
+
import { brain as coreBrain, type BrainFactory } from '@positronic/core';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Base brain factory for this project.
|
|
@@ -11,7 +11,7 @@ import { brain as coreBrain, type BrainFunction } from '@positronic/core';
|
|
|
11
11
|
* 2. Create service instances
|
|
12
12
|
* 3. Call .withServices() on the brain before returning it
|
|
13
13
|
*
|
|
14
|
-
* Example:
|
|
14
|
+
* Example with services:
|
|
15
15
|
* ```typescript
|
|
16
16
|
* interface ProjectServices {
|
|
17
17
|
* logger: {
|
|
@@ -23,7 +23,7 @@ import { brain as coreBrain, type BrainFunction } from '@positronic/core';
|
|
|
23
23
|
* };
|
|
24
24
|
* }
|
|
25
25
|
*
|
|
26
|
-
* export const brain:
|
|
26
|
+
* export const brain: BrainFactory = (brainConfig) => {
|
|
27
27
|
* return coreBrain(brainConfig)
|
|
28
28
|
* .withServices({
|
|
29
29
|
* logger: {
|
|
@@ -43,16 +43,29 @@ import { brain as coreBrain, type BrainFunction } from '@positronic/core';
|
|
|
43
43
|
* Then in your brain files (in the brains/ directory):
|
|
44
44
|
* ```typescript
|
|
45
45
|
* import { brain } from '../brain.js';
|
|
46
|
+
* import { z } from 'zod';
|
|
47
|
+
*
|
|
48
|
+
* const optionsSchema = z.object({
|
|
49
|
+
* environment: z.string().default('prod'),
|
|
50
|
+
* verbose: z.string().default('false')
|
|
51
|
+
* });
|
|
46
52
|
*
|
|
47
53
|
* export default brain('My Brain')
|
|
48
|
-
* .
|
|
49
|
-
*
|
|
50
|
-
*
|
|
54
|
+
* .withOptionsSchema(optionsSchema)
|
|
55
|
+
* .step('Use Services', async ({ state, options, logger, api }) => {
|
|
56
|
+
* if (options.verbose === 'true') {
|
|
57
|
+
* logger.info('Fetching data...');
|
|
58
|
+
* }
|
|
59
|
+
* const endpoint = options.environment === 'dev' ? '/users/test' : '/users';
|
|
60
|
+
* const data = await api.fetch(endpoint);
|
|
51
61
|
* return { users: data };
|
|
52
62
|
* });
|
|
53
63
|
* ```
|
|
64
|
+
*
|
|
65
|
+
* Run with custom options from CLI:
|
|
66
|
+
* px brain run my-brain -o environment=dev -o verbose=true
|
|
54
67
|
*/
|
|
55
|
-
export const brain:
|
|
68
|
+
export const brain: BrainFactory = (brainConfig) => {
|
|
56
69
|
// For now, just return the core brain without any services.
|
|
57
70
|
// Update this function to add your project-wide services.
|
|
58
71
|
return coreBrain(brainConfig);
|
|
@@ -8,6 +8,31 @@ The Brain DSL provides a fluent, type-safe API for building stateful AI workflow
|
|
|
8
8
|
|
|
9
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
10
|
|
|
11
|
+
### Type Safety and Options
|
|
12
|
+
|
|
13
|
+
The brain function provides full type safety through its fluent API. State types are automatically inferred as you build your brain, and options can be validated at runtime using schemas.
|
|
14
|
+
|
|
15
|
+
For runtime options validation, use the `withOptionsSchema` method with a Zod schema:
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { z } from 'zod';
|
|
19
|
+
|
|
20
|
+
const optionsSchema = z.object({
|
|
21
|
+
environment: z.enum(['dev', 'staging', 'prod']),
|
|
22
|
+
verbose: z.boolean().default(false)
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const myBrain = brain('My Brain')
|
|
26
|
+
.withOptionsSchema(optionsSchema)
|
|
27
|
+
.step('Process', ({ options }) => {
|
|
28
|
+
// options is fully typed based on the schema
|
|
29
|
+
if (options.verbose) {
|
|
30
|
+
console.log('Running in', options.environment);
|
|
31
|
+
}
|
|
32
|
+
return { status: 'complete' };
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
11
36
|
## Basic Brain Structure
|
|
12
37
|
|
|
13
38
|
```typescript
|
|
@@ -57,7 +82,7 @@ brain('AI Education Assistant')
|
|
|
57
82
|
}))
|
|
58
83
|
.prompt('Generate explanation', {
|
|
59
84
|
template: ({ topic, context }) =>
|
|
60
|
-
|
|
85
|
+
`<%= '${context}' %>. Please provide a brief, beginner-friendly explanation of <%= '${topic}' %>.`,
|
|
61
86
|
outputSchema: {
|
|
62
87
|
schema: z.object({
|
|
63
88
|
explanation: z.string().describe('A clear explanation of the topic'),
|
|
@@ -72,7 +97,7 @@ brain('AI Education Assistant')
|
|
|
72
97
|
formattedOutput: {
|
|
73
98
|
topic: state.topic,
|
|
74
99
|
explanation: state.topicExplanation.explanation || '',
|
|
75
|
-
summary:
|
|
100
|
+
summary: `This explanation covers <%= '${state.topicExplanation.keyPoints?.length || 0}' %> key points at a <%= '${state.topicExplanation.difficulty || \'unknown\'}' %> level.`,
|
|
76
101
|
points: state.topicExplanation.keyPoints || [],
|
|
77
102
|
},
|
|
78
103
|
}))
|
|
@@ -80,7 +105,9 @@ brain('AI Education Assistant')
|
|
|
80
105
|
'Generate follow-up questions',
|
|
81
106
|
{
|
|
82
107
|
template: ({ formattedOutput }) =>
|
|
83
|
-
|
|
108
|
+
`Based on this explanation about <%= '${formattedOutput.topic}' %>: "<%= '${formattedOutput.explanation}' %>"
|
|
109
|
+
|
|
110
|
+
Generate 3 thoughtful follow-up questions that a student might ask.`,
|
|
84
111
|
outputSchema: {
|
|
85
112
|
schema: z.object({
|
|
86
113
|
questions: z.array(z.string()).length(3).describe('Three follow-up questions'),
|
|
@@ -142,19 +169,144 @@ Each step receives these parameters:
|
|
|
142
169
|
|
|
143
170
|
## Configuration Methods
|
|
144
171
|
|
|
145
|
-
###
|
|
172
|
+
### Brain Options
|
|
173
|
+
|
|
174
|
+
Options provide runtime configuration for your brains, allowing different behavior without changing code. They're perfect for settings like API endpoints, feature flags, output preferences, or channel identifiers.
|
|
146
175
|
|
|
147
|
-
|
|
176
|
+
#### Typing Options
|
|
177
|
+
|
|
178
|
+
To use options in your brain, define a Zod schema with `withOptionsSchema`:
|
|
148
179
|
|
|
149
180
|
```typescript
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
181
|
+
import { z } from 'zod';
|
|
182
|
+
|
|
183
|
+
// Define your options schema
|
|
184
|
+
const notificationSchema = z.object({
|
|
185
|
+
slackChannel: z.string(),
|
|
186
|
+
priority: z.enum(['low', 'normal', 'high']),
|
|
187
|
+
includeTimestamp: z.boolean().default(true)
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Use withOptionsSchema to add runtime validation
|
|
191
|
+
const notificationBrain = brain('Notification Brain')
|
|
192
|
+
.withOptionsSchema(notificationSchema)
|
|
193
|
+
.step('Send Alert', async ({ state, options, slack }) => {
|
|
194
|
+
// TypeScript knows the exact shape of options from the schema
|
|
195
|
+
const message = options.includeTimestamp
|
|
196
|
+
? `[<%= '${new Date().toISOString()}' %>] <%= '${state.alert}' %>`
|
|
197
|
+
: state.alert;
|
|
198
|
+
|
|
199
|
+
await slack.post(options.slackChannel, {
|
|
200
|
+
text: message,
|
|
201
|
+
priority: options.priority // Type-safe: must be 'low' | 'normal' | 'high'
|
|
202
|
+
});
|
|
203
|
+
|
|
154
204
|
return state;
|
|
155
205
|
});
|
|
156
206
|
```
|
|
157
207
|
|
|
208
|
+
The schema approach provides:
|
|
209
|
+
- Runtime validation of options
|
|
210
|
+
- Automatic TypeScript type inference
|
|
211
|
+
- Clear error messages for invalid options
|
|
212
|
+
- Support for default values in the schema
|
|
213
|
+
|
|
214
|
+
#### Passing Options from Command Line
|
|
215
|
+
|
|
216
|
+
Override default options when running brains from the CLI using the `-o` or `--options` flag:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
# Single option
|
|
220
|
+
px brain run my-brain -o debug=true
|
|
221
|
+
|
|
222
|
+
# Multiple options
|
|
223
|
+
px brain run my-brain -o slackChannel=#alerts -o temperature=0.9 -o verbose=true
|
|
224
|
+
|
|
225
|
+
# Options with spaces or special characters (use quotes)
|
|
226
|
+
px brain run my-brain -o "webhook=https://example.com/api?key=value"
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Options are passed as simple key=value pairs and are available as strings in your brain.
|
|
230
|
+
|
|
231
|
+
#### Options vs Services vs Initial State
|
|
232
|
+
|
|
233
|
+
Understanding when to use each:
|
|
234
|
+
|
|
235
|
+
- **Options**: Runtime configuration (channels, endpoints, feature flags)
|
|
236
|
+
- Override from CLI with `-o key=value`
|
|
237
|
+
- Don't change during execution
|
|
238
|
+
- Examples: `slackChannel`, `apiEndpoint`, `debugMode`
|
|
239
|
+
|
|
240
|
+
- **Services**: External dependencies and side effects (clients, loggers, databases)
|
|
241
|
+
- Configure once with `.withServices()`
|
|
242
|
+
- Available in all steps
|
|
243
|
+
- Not serializable
|
|
244
|
+
- Examples: `slackClient`, `database`, `logger`
|
|
245
|
+
|
|
246
|
+
- **Initial State**: Starting data for a specific run
|
|
247
|
+
- Pass to `brain.run()` or set via CLI/API
|
|
248
|
+
- Changes throughout execution
|
|
249
|
+
- Must be serializable
|
|
250
|
+
- Examples: `userId`, `orderData`, `inputText`
|
|
251
|
+
|
|
252
|
+
#### Real-World Example
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
// Define a brain that uses options for configuration
|
|
256
|
+
const notificationSchema = z.object({
|
|
257
|
+
channel: z.string(),
|
|
258
|
+
priority: z.string().default('normal'),
|
|
259
|
+
includeDetails: z.string().default('false')
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const notificationBrain = brain('Smart Notifier')
|
|
263
|
+
.withOptionsSchema(notificationSchema)
|
|
264
|
+
.withServices({
|
|
265
|
+
slack: slackClient,
|
|
266
|
+
email: emailClient
|
|
267
|
+
})
|
|
268
|
+
.step('Process Alert', ({ state, options }) => ({
|
|
269
|
+
...state,
|
|
270
|
+
formattedMessage: options.includeDetails === 'true'
|
|
271
|
+
? `Alert: <%= '${state.message}' %> - Details: <%= '${state.details}' %>`
|
|
272
|
+
: `Alert: <%= '${state.message}' %>`,
|
|
273
|
+
isPriority: options.priority === 'high'
|
|
274
|
+
}))
|
|
275
|
+
.step('Send Notification', async ({ state, options, slack, email }) => {
|
|
276
|
+
// Use options to control behavior
|
|
277
|
+
if (state.isPriority) {
|
|
278
|
+
// High priority goes to email too
|
|
279
|
+
await email.send('admin@example.com', state.formattedMessage);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Always send to Slack channel from options
|
|
283
|
+
await slack.post(options.channel, state.formattedMessage);
|
|
284
|
+
|
|
285
|
+
return { ...state, notified: true };
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// Run with custom options from CLI:
|
|
289
|
+
// px brain run smart-notifier -o channel=#urgent -o priority=high -o includeDetails=true
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
#### Testing with Options
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
// In your tests
|
|
296
|
+
const result = await runBrainTest(notificationBrain, {
|
|
297
|
+
client: mockClient,
|
|
298
|
+
initialState: { message: 'System down', details: 'Database unreachable' },
|
|
299
|
+
options: {
|
|
300
|
+
channel: '#test-channel',
|
|
301
|
+
priority: 'high',
|
|
302
|
+
includeDetails: true
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
expect(mockSlack.post).toHaveBeenCalledWith('#test-channel', expect.any(String));
|
|
307
|
+
expect(mockEmail.send).toHaveBeenCalled(); // High priority triggers email
|
|
308
|
+
```
|
|
309
|
+
|
|
158
310
|
### Service Injection
|
|
159
311
|
|
|
160
312
|
The `withServices` method provides dependency injection for your brains, making external services available throughout the workflow while maintaining testability.
|
|
@@ -249,7 +401,7 @@ const analysisBrain = brain('Data Analysis')
|
|
|
249
401
|
return { ...state, data };
|
|
250
402
|
})
|
|
251
403
|
.prompt('Analyze Data', {
|
|
252
|
-
template: ({ data }) =>
|
|
404
|
+
template: ({ data }) => `Analyze this data: <%= '${JSON.stringify(data)}' %>`,
|
|
253
405
|
outputSchema: {
|
|
254
406
|
schema: z.object({
|
|
255
407
|
insights: z.array(z.string()),
|
|
@@ -283,7 +435,7 @@ Services make testing easier by allowing you to inject mocks:
|
|
|
283
435
|
|
|
284
436
|
```typescript
|
|
285
437
|
// In your test file
|
|
286
|
-
import { createMockClient, runBrainTest } from '
|
|
438
|
+
import { createMockClient, runBrainTest } from '../tests/test-utils.js';
|
|
287
439
|
|
|
288
440
|
const mockLogger = {
|
|
289
441
|
info: jest.fn(),
|
|
@@ -377,7 +529,7 @@ const typedBrain = brain('Typed Example')
|
|
|
377
529
|
name: 'Test', // TypeScript knows state has 'count'
|
|
378
530
|
}))
|
|
379
531
|
.step('Use Both', ({ state }) => ({
|
|
380
|
-
message:
|
|
532
|
+
message: `<%= '${state.name}' %>: <%= '${state.count}' %>`, // Both properties available
|
|
381
533
|
}));
|
|
382
534
|
```
|
|
383
535
|
|
|
@@ -482,7 +634,7 @@ export const aiFilterPrompt = {
|
|
|
482
634
|
|
|
483
635
|
// Build the prompt with state data
|
|
484
636
|
const articleList = state.articles
|
|
485
|
-
.map((a, i) =>
|
|
637
|
+
.map((a, i) => `<%= '${i + 1}' %>. <%= '${a.title}' %> (score: <%= '${a.score}' %>)`)
|
|
486
638
|
.join('\n');
|
|
487
639
|
|
|
488
640
|
return template
|
|
@@ -546,7 +698,6 @@ const completeBrain = brain({
|
|
|
546
698
|
title: 'Complete Example',
|
|
547
699
|
description: 'Demonstrates all Brain DSL features',
|
|
548
700
|
})
|
|
549
|
-
.withOptions({ temperature: 0.7 })
|
|
550
701
|
.withServices<Services>({
|
|
551
702
|
logger: console,
|
|
552
703
|
analytics: {
|
|
@@ -574,11 +725,11 @@ const completeBrain = brain({
|
|
|
574
725
|
},
|
|
575
726
|
// Services available in reduce function too
|
|
576
727
|
({ state, response, logger }) => {
|
|
577
|
-
logger.log(
|
|
728
|
+
logger.log(`Plan generated with <%= '${response.tasks.length}' %> tasks`);
|
|
578
729
|
return { ...state, plan: response };
|
|
579
730
|
})
|
|
580
731
|
.step('Process Plan', ({ state, logger, analytics }) => {
|
|
581
|
-
logger.log(
|
|
732
|
+
logger.log(`Processing <%= '${state.plan.tasks.length}' %> tasks`);
|
|
582
733
|
analytics.track('plan_processed', {
|
|
583
734
|
task_count: state.plan.tasks.length,
|
|
584
735
|
duration: state.plan.duration
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
# Brain Testing Guide
|
|
2
2
|
|
|
3
|
-
This guide explains how to test Positronic brains using the testing utilities
|
|
3
|
+
This guide explains how to test Positronic brains using the testing utilities in the `tests/test-utils.ts` file.
|
|
4
|
+
|
|
5
|
+
## Test Organization
|
|
6
|
+
|
|
7
|
+
All test files should be placed in the `tests/` directory at the root of your project. This keeps tests separate from your brain implementations and prevents them from being deployed with your application.
|
|
8
|
+
|
|
9
|
+
Test files should follow the naming convention `<brain-name>.test.ts`. For example:
|
|
10
|
+
- Brain file: `brains/customer-support.ts`
|
|
11
|
+
- Test file: `tests/customer-support.test.ts`
|
|
4
12
|
|
|
5
13
|
## Testing Philosophy
|
|
6
14
|
|
|
@@ -16,8 +24,8 @@ Testing brains is about verifying they produce the correct outputs given specifi
|
|
|
16
24
|
## Quick Start
|
|
17
25
|
|
|
18
26
|
```typescript
|
|
19
|
-
import { createMockClient, runBrainTest } from '
|
|
20
|
-
import yourBrain from '
|
|
27
|
+
import { createMockClient, runBrainTest } from '../tests/test-utils.js';
|
|
28
|
+
import yourBrain from '../brains/your-brain.js';
|
|
21
29
|
|
|
22
30
|
describe('your-brain', () => {
|
|
23
31
|
it('should process user data and generate a report', async () => {
|
|
@@ -60,13 +60,10 @@ interface ProjectServices {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
// Export the wrapped brain function
|
|
63
|
-
export function brain
|
|
64
|
-
TOptions extends object = object,
|
|
65
|
-
TState extends object = object
|
|
66
|
-
>(
|
|
63
|
+
export function brain(
|
|
67
64
|
brainConfig: string | { title: string; description?: string }
|
|
68
|
-
)
|
|
69
|
-
return coreBrain
|
|
65
|
+
) {
|
|
66
|
+
return coreBrain(brainConfig)
|
|
70
67
|
.withServices({
|
|
71
68
|
logger: {
|
|
72
69
|
info: (msg) => console.log(`[<%= '${new Date().toISOString()}' %>] INFO: <%= '${msg}' %>`),
|
|
@@ -127,7 +124,17 @@ See `/docs/brain-testing-guide.md` for detailed testing guidance.
|
|
|
127
124
|
|
|
128
125
|
1. **Start the development server**: `px server -d`
|
|
129
126
|
2. **Create or modify brains**: Always import from `./brain.js`
|
|
130
|
-
3. **Test locally**:
|
|
127
|
+
3. **Test locally**:
|
|
128
|
+
```bash
|
|
129
|
+
# Basic run
|
|
130
|
+
px brain run <brain-name>
|
|
131
|
+
|
|
132
|
+
# Run with options
|
|
133
|
+
px brain run <brain-name> -o channel=#dev -o debug=true
|
|
134
|
+
|
|
135
|
+
# Watch execution in real-time
|
|
136
|
+
px brain run <brain-name> --watch
|
|
137
|
+
```
|
|
131
138
|
4. **Run tests**: `npm test`
|
|
132
139
|
5. **Deploy**: Backend-specific commands (e.g., `px deploy` for Cloudflare)
|
|
133
140
|
|
|
@@ -202,10 +202,7 @@ import slack from './services/slack.js';
|
|
|
202
202
|
import database from './services/database.js';
|
|
203
203
|
import analytics from './services/analytics.js';
|
|
204
204
|
|
|
205
|
-
export function brain
|
|
206
|
-
TOptions extends object = object,
|
|
207
|
-
TState extends object = object
|
|
208
|
-
>(
|
|
205
|
+
export function brain(
|
|
209
206
|
brainConfig: string | { title: string; description?: string }
|
|
210
207
|
) {
|
|
211
208
|
return coreBrain(brainConfig)
|
|
@@ -220,6 +217,45 @@ export function brain<
|
|
|
220
217
|
|
|
221
218
|
This keeps your service implementations separate from your brain logic and makes them easier to test and maintain.
|
|
222
219
|
|
|
220
|
+
## Brain Options Usage
|
|
221
|
+
|
|
222
|
+
When creating brains that need runtime configuration, use the options schema pattern:
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
import { z } from 'zod';
|
|
226
|
+
|
|
227
|
+
// Good example - configurable brain with validated options
|
|
228
|
+
const alertSchema = z.object({
|
|
229
|
+
slackChannel: z.string(),
|
|
230
|
+
emailEnabled: z.string().default('false'),
|
|
231
|
+
alertThreshold: z.string().default('10')
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const alertBrain = brain('Alert System')
|
|
235
|
+
.withOptionsSchema(alertSchema)
|
|
236
|
+
.step('Check Threshold', ({ state, options }) => ({
|
|
237
|
+
...state,
|
|
238
|
+
shouldAlert: state.errorCount > parseInt(options.alertThreshold)
|
|
239
|
+
}))
|
|
240
|
+
.step('Send Alerts', async ({ state, options, slack }) => {
|
|
241
|
+
if (!state.shouldAlert) return state;
|
|
242
|
+
|
|
243
|
+
await slack.post(options.slackChannel, state.message);
|
|
244
|
+
|
|
245
|
+
if (options.emailEnabled === 'true') {
|
|
246
|
+
// Note: CLI options come as strings
|
|
247
|
+
await email.send('admin@example.com', state.message);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return { ...state, alerted: true };
|
|
251
|
+
});
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Remember:
|
|
255
|
+
- Options from CLI are always strings (even numbers and booleans)
|
|
256
|
+
- Options are for configuration, not data
|
|
257
|
+
- Document available options in comments above the brain
|
|
258
|
+
|
|
223
259
|
## Important: ESM Module Imports
|
|
224
260
|
|
|
225
261
|
This project uses ES modules (ESM). **Always include the `.js` extension in your imports**, even when importing TypeScript files:
|
|
@@ -255,8 +291,8 @@ Start by following the brain testing guide (`/docs/brain-testing-guide.md`) and
|
|
|
255
291
|
```typescript
|
|
256
292
|
// tests/my-new-brain.test.ts
|
|
257
293
|
import { describe, it, expect } from '@jest/globals';
|
|
258
|
-
import { createMockClient, runBrainTest } from '
|
|
259
|
-
import myNewBrain from '../brains/my-new-brain';
|
|
294
|
+
import { createMockClient, runBrainTest } from './test-utils.js';
|
|
295
|
+
import myNewBrain from '../brains/my-new-brain.js';
|
|
260
296
|
|
|
261
297
|
describe('MyNewBrain', () => {
|
|
262
298
|
it('should process data and return expected result', async () => {
|
|
@@ -381,7 +417,7 @@ export default feedbackBrain;
|
|
|
381
417
|
// Step 4: Add sentiment analysis step
|
|
382
418
|
.prompt('Analyze sentiment', {
|
|
383
419
|
template: ({ feedback }) =>
|
|
384
|
-
<%=
|
|
420
|
+
<%= '\`Analyze the sentiment of this feedback: "${feedback}"\`' %>,
|
|
385
421
|
outputSchema: {
|
|
386
422
|
schema: z.object({
|
|
387
423
|
sentiment: z.enum(['positive', 'neutral', 'negative']),
|
|
@@ -395,7 +431,7 @@ export default feedbackBrain;
|
|
|
395
431
|
// Step 6: Add response generation
|
|
396
432
|
.prompt('Generate response', {
|
|
397
433
|
template: ({ sentimentAnalysis, feedback }) =>
|
|
398
|
-
<%=
|
|
434
|
+
<%= '\`Generate a brief response to this ${sentimentAnalysis.sentiment} feedback: "${feedback}"\`' %>,
|
|
399
435
|
outputSchema: {
|
|
400
436
|
schema: z.object({
|
|
401
437
|
response: z.string()
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { ObjectGenerator } from '@positronic/core';
|
|
2
|
+
import type { BrainEvent } from '@positronic/core';
|
|
3
|
+
import { BRAIN_EVENTS, applyPatches } from '@positronic/core';
|
|
4
|
+
|
|
5
|
+
export interface MockClient extends ObjectGenerator {
|
|
6
|
+
mockResponses: (...responses: any[]) => void;
|
|
7
|
+
clearMocks: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function createMockClient(): MockClient {
|
|
11
|
+
const responses: any[] = [];
|
|
12
|
+
let responseIndex = 0;
|
|
13
|
+
|
|
14
|
+
const generateObject = jest.fn(async () => {
|
|
15
|
+
if (responseIndex >= responses.length) {
|
|
16
|
+
throw new Error('No more mock responses available');
|
|
17
|
+
}
|
|
18
|
+
return responses[responseIndex++];
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
generateObject,
|
|
23
|
+
mockResponses: (...newResponses: any[]) => {
|
|
24
|
+
responses.push(...newResponses);
|
|
25
|
+
},
|
|
26
|
+
clearMocks: () => {
|
|
27
|
+
responses.length = 0;
|
|
28
|
+
responseIndex = 0;
|
|
29
|
+
generateObject.mockClear();
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface BrainTestResult<TState> {
|
|
35
|
+
completed: boolean;
|
|
36
|
+
error: Error | null;
|
|
37
|
+
finalState: TState;
|
|
38
|
+
events: BrainEvent<any>[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function runBrainTest<TOptions extends object, TState extends object>(
|
|
42
|
+
brain: any,
|
|
43
|
+
options?: {
|
|
44
|
+
client?: ObjectGenerator;
|
|
45
|
+
initialState?: Partial<TState>;
|
|
46
|
+
resources?: any;
|
|
47
|
+
}
|
|
48
|
+
): Promise<BrainTestResult<TState>> {
|
|
49
|
+
const events: BrainEvent<any>[] = [];
|
|
50
|
+
let finalState: any = options?.initialState || {};
|
|
51
|
+
let error: Error | null = null;
|
|
52
|
+
let completed = false;
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const runOptions = {
|
|
56
|
+
...options,
|
|
57
|
+
state: options?.initialState,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
for await (const event of brain.run(runOptions)) {
|
|
61
|
+
events.push(event);
|
|
62
|
+
|
|
63
|
+
if (event.type === BRAIN_EVENTS.STEP_COMPLETE) {
|
|
64
|
+
finalState = applyPatches(finalState, [event.patch]);
|
|
65
|
+
} else if (event.type === BRAIN_EVENTS.ERROR) {
|
|
66
|
+
error = new Error(event.error.message);
|
|
67
|
+
} else if (event.type === BRAIN_EVENTS.COMPLETE) {
|
|
68
|
+
completed = true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} catch (err) {
|
|
72
|
+
error = err instanceof Error ? err : new Error(String(err));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
completed,
|
|
77
|
+
error,
|
|
78
|
+
finalState,
|
|
79
|
+
events,
|
|
80
|
+
};
|
|
81
|
+
}
|