@positronic/template-new-project 0.0.53 → 0.0.55
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 +4 -4
- package/package.json +1 -1
- package/template/{components → .positronic}/bundle.ts +3 -3
- package/template/CLAUDE.md +5 -4
- package/template/_env +4 -0
- package/template/_gitignore +2 -3
- package/template/brain.ts +13 -10
- package/template/brains/hello.ts +33 -0
- package/template/docs/brain-dsl-guide.md +305 -3
- package/template/docs/brain-testing-guide.md +56 -49
- package/template/docs/positronic-guide.md +23 -26
- package/template/docs/tips-for-agents.md +10 -12
- package/template/esbuild.config.mjs +3 -3
- package/template/package.json +3 -2
- package/template/runner.ts +2 -2
package/index.js
CHANGED
|
@@ -53,10 +53,10 @@ 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.
|
|
59
|
-
let genUIComponentsVersion = '^0.0.
|
|
56
|
+
let coreVersion = '^0.0.55';
|
|
57
|
+
let cloudflareVersion = '^0.0.55';
|
|
58
|
+
let clientVercelVersion = '^0.0.55';
|
|
59
|
+
let genUIComponentsVersion = '^0.0.55';
|
|
60
60
|
|
|
61
61
|
// Map backend selection to package names
|
|
62
62
|
const backendPackageMap = {
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Bundle entry point for client-side rendering.
|
|
3
3
|
*
|
|
4
|
-
* This file is bundled by esbuild into dist/components.js which exposes
|
|
4
|
+
* This file is bundled by esbuild into .positronic/dist/components.js which exposes
|
|
5
5
|
* React components to window.PositronicComponents for use by generated pages.
|
|
6
6
|
*
|
|
7
|
-
* When you add custom components to
|
|
7
|
+
* When you add custom components to components/index.ts, they will automatically
|
|
8
8
|
* be included in the bundle.
|
|
9
9
|
*/
|
|
10
|
-
import { components } from '
|
|
10
|
+
import { components } from '../components/index.js';
|
|
11
11
|
|
|
12
12
|
// Extract the React component from each UIComponent and expose to window
|
|
13
13
|
const PositronicComponents: Record<string, React.ComponentType<any>> = {};
|
package/template/CLAUDE.md
CHANGED
|
@@ -38,7 +38,8 @@ For testing guidance, see `/docs/brain-testing-guide.md`
|
|
|
38
38
|
The Brain DSL provides a fluent API for defining AI workflows:
|
|
39
39
|
|
|
40
40
|
```typescript
|
|
41
|
-
|
|
41
|
+
// Import from the project brain wrapper (see positronic-guide.md)
|
|
42
|
+
import { brain } from '../brain.js';
|
|
42
43
|
|
|
43
44
|
const myBrain = brain('my-brain')
|
|
44
45
|
.step('Initialize', ({ state }) => ({
|
|
@@ -46,12 +47,12 @@ const myBrain = brain('my-brain')
|
|
|
46
47
|
initialized: true
|
|
47
48
|
}))
|
|
48
49
|
.step('Process', async ({ state, resources }) => {
|
|
49
|
-
// Access resources
|
|
50
|
-
const
|
|
50
|
+
// Access resources with type-safe API
|
|
51
|
+
const content = await resources.example.loadText();
|
|
51
52
|
return {
|
|
52
53
|
...state,
|
|
53
54
|
processed: true,
|
|
54
|
-
content
|
|
55
|
+
content
|
|
55
56
|
};
|
|
56
57
|
});
|
|
57
58
|
|
package/template/_env
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# Development Environment
|
|
2
2
|
NODE_ENV=development
|
|
3
3
|
|
|
4
|
+
# Google AI API Key (required for running brains)
|
|
5
|
+
# Get your API key at https://aistudio.google.com/apikey
|
|
6
|
+
GOOGLE_GENERATIVE_AI_API_KEY=
|
|
7
|
+
|
|
4
8
|
# Cloudflare R2 Configuration
|
|
5
9
|
R2_BUCKET_NAME=<%= projectName %>
|
|
6
10
|
|
package/template/_gitignore
CHANGED
package/template/brain.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { createBrain } from '@positronic/core';
|
|
1
|
+
import { createBrain, defaultTools } from '@positronic/core';
|
|
2
2
|
import { components } from './components/index.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Project-level brain function with pre-configured components.
|
|
5
|
+
* Project-level brain function with pre-configured components and tools.
|
|
6
6
|
*
|
|
7
7
|
* All brains in your project should import from this file:
|
|
8
8
|
*
|
|
@@ -13,10 +13,15 @@ import { components } from './components/index.js';
|
|
|
13
13
|
* .step('Do something', ({ state }) => ({ ...state, done: true }));
|
|
14
14
|
* ```
|
|
15
15
|
*
|
|
16
|
+
* Default tools available in agent steps:
|
|
17
|
+
* - generateUI: Generate interactive UI components
|
|
18
|
+
* - consoleLog: Log messages for debugging
|
|
19
|
+
* - done: Complete the agent and return a result
|
|
20
|
+
*
|
|
16
21
|
* To add services (e.g., Slack, Gmail, database clients):
|
|
17
22
|
*
|
|
18
23
|
* ```typescript
|
|
19
|
-
* import { createBrain } from '@positronic/core';
|
|
24
|
+
* import { createBrain, defaultTools } from '@positronic/core';
|
|
20
25
|
* import { components } from './components/index.js';
|
|
21
26
|
* import slack from './services/slack.js';
|
|
22
27
|
* import gmail from './services/gmail.js';
|
|
@@ -24,6 +29,7 @@ import { components } from './components/index.js';
|
|
|
24
29
|
* export const brain = createBrain({
|
|
25
30
|
* services: { slack, gmail },
|
|
26
31
|
* components,
|
|
32
|
+
* defaultTools,
|
|
27
33
|
* });
|
|
28
34
|
* ```
|
|
29
35
|
*
|
|
@@ -37,27 +43,24 @@ import { components } from './components/index.js';
|
|
|
37
43
|
* });
|
|
38
44
|
* ```
|
|
39
45
|
*
|
|
40
|
-
* You can also create agents directly:
|
|
46
|
+
* You can also create agents directly with access to default tools:
|
|
41
47
|
*
|
|
42
48
|
* ```typescript
|
|
43
|
-
* export default brain('my-agent', ({ slack,
|
|
49
|
+
* export default brain('my-agent', ({ slack, tools }) => ({
|
|
44
50
|
* system: 'You are a helpful assistant',
|
|
45
51
|
* prompt: 'Help the user with their request',
|
|
46
52
|
* tools: {
|
|
53
|
+
* ...tools, // includes generateUI, consoleLog, done
|
|
47
54
|
* notify: {
|
|
48
55
|
* description: 'Send a Slack notification',
|
|
49
56
|
* inputSchema: z.object({ message: z.string() }),
|
|
50
57
|
* execute: ({ message }) => slack.postMessage('#general', message),
|
|
51
58
|
* },
|
|
52
|
-
* done: {
|
|
53
|
-
* description: 'Complete the task',
|
|
54
|
-
* inputSchema: z.object({ result: z.string() }),
|
|
55
|
-
* terminal: true,
|
|
56
|
-
* },
|
|
57
59
|
* },
|
|
58
60
|
* }));
|
|
59
61
|
* ```
|
|
60
62
|
*/
|
|
61
63
|
export const brain = createBrain({
|
|
62
64
|
components,
|
|
65
|
+
defaultTools,
|
|
63
66
|
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { brain } from '../brain.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A simple agent brain that demonstrates the default tools.
|
|
5
|
+
*
|
|
6
|
+
* This brain uses only a system prompt and tools - no explicit user prompt needed.
|
|
7
|
+
* When prompt is omitted, the agent automatically starts with "Begin."
|
|
8
|
+
*
|
|
9
|
+
* This brain:
|
|
10
|
+
* 1. Uses generateUI to create a form asking for the user's name
|
|
11
|
+
* 2. Waits for the user to submit the form
|
|
12
|
+
* 3. Uses consoleLog to log a personalized greeting
|
|
13
|
+
* 4. Uses done to complete with a welcome message
|
|
14
|
+
*
|
|
15
|
+
* Run with: px brain run hello
|
|
16
|
+
*/
|
|
17
|
+
export default brain('hello', ({ tools }) => ({
|
|
18
|
+
system: `You are a friendly greeter for the Positronic framework.
|
|
19
|
+
Your job is to welcome new users and make them feel excited about building AI workflows.
|
|
20
|
+
|
|
21
|
+
When you start:
|
|
22
|
+
1. Use generateUI to create a simple, welcoming form that asks for the user's name
|
|
23
|
+
(use a friendly title and a single text input)
|
|
24
|
+
2. After receiving their name, use consoleLog to log a warm, personalized greeting
|
|
25
|
+
3. Use done to complete with an encouraging message about what they can build
|
|
26
|
+
|
|
27
|
+
You have access to these tools:
|
|
28
|
+
- generateUI: Create a form to collect the user's name
|
|
29
|
+
- consoleLog: Log messages to the console
|
|
30
|
+
- done: Complete the greeting with a final message`,
|
|
31
|
+
|
|
32
|
+
tools,
|
|
33
|
+
}));
|
|
@@ -165,7 +165,11 @@ Each step receives these parameters:
|
|
|
165
165
|
- `client` - AI client for generating structured objects
|
|
166
166
|
- `resources` - Loaded resources (files, documents, etc.)
|
|
167
167
|
- `options` - Runtime options passed to the brain
|
|
168
|
-
-
|
|
168
|
+
- `response` - Webhook response data (available after `waitFor` completes)
|
|
169
|
+
- `page` - Generated page object (available after `.ui()` step)
|
|
170
|
+
- `pages` - Pages service for HTML page management
|
|
171
|
+
- `env` - Runtime environment containing `origin` (base URL) and `secrets` (typed secrets object)
|
|
172
|
+
- Custom services (if configured with `.withServices()` or `createBrain()`)
|
|
169
173
|
|
|
170
174
|
## Configuration Methods
|
|
171
175
|
|
|
@@ -473,15 +477,141 @@ expect(result.finalState.data).toEqual({ id: '123', name: 'Test' });
|
|
|
473
477
|
- Services are not serialized - they're for side effects and external interactions
|
|
474
478
|
- Each brain instance maintains its own service references
|
|
475
479
|
|
|
480
|
+
### Tool Configuration with `withTools()`
|
|
481
|
+
|
|
482
|
+
The `withTools()` method registers tools that can be used by agent steps:
|
|
483
|
+
|
|
484
|
+
```typescript
|
|
485
|
+
import { z } from 'zod';
|
|
486
|
+
|
|
487
|
+
const brainWithTools = brain('Tool Brain')
|
|
488
|
+
.withTools({
|
|
489
|
+
fetchData: {
|
|
490
|
+
description: 'Fetch data from an external API',
|
|
491
|
+
inputSchema: z.object({
|
|
492
|
+
endpoint: z.string(),
|
|
493
|
+
params: z.record(z.string()).optional()
|
|
494
|
+
}),
|
|
495
|
+
execute: async ({ endpoint, params }) => {
|
|
496
|
+
const url = new URL(endpoint);
|
|
497
|
+
if (params) {
|
|
498
|
+
Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
|
|
499
|
+
}
|
|
500
|
+
const response = await fetch(url);
|
|
501
|
+
return response.json();
|
|
502
|
+
}
|
|
503
|
+
},
|
|
504
|
+
saveToDatabase: {
|
|
505
|
+
description: 'Save data to the database',
|
|
506
|
+
inputSchema: z.object({
|
|
507
|
+
table: z.string(),
|
|
508
|
+
data: z.any()
|
|
509
|
+
}),
|
|
510
|
+
execute: async ({ table, data }) => {
|
|
511
|
+
// Database save logic
|
|
512
|
+
return { success: true, id: 'generated-id' };
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
})
|
|
516
|
+
.brain('Data Agent', {
|
|
517
|
+
system: 'You can fetch and save data.',
|
|
518
|
+
prompt: 'Fetch user data and save the summary.'
|
|
519
|
+
// Tools defined with withTools() are automatically available
|
|
520
|
+
});
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
### Component Configuration with `withComponents()`
|
|
524
|
+
|
|
525
|
+
The `withComponents()` method registers custom UI components for use in `.ui()` steps:
|
|
526
|
+
|
|
527
|
+
```typescript
|
|
528
|
+
const brainWithComponents = brain('Custom UI Brain')
|
|
529
|
+
.withComponents({
|
|
530
|
+
CustomCard: {
|
|
531
|
+
description: 'A styled card component for displaying content',
|
|
532
|
+
props: z.object({
|
|
533
|
+
title: z.string(),
|
|
534
|
+
content: z.string(),
|
|
535
|
+
variant: z.enum(['default', 'highlighted', 'warning']).default('default')
|
|
536
|
+
}),
|
|
537
|
+
render: (props) => `
|
|
538
|
+
<div class="card card-<%= '${props.variant}' %>">
|
|
539
|
+
<h3><%= '${props.title}' %></h3>
|
|
540
|
+
<p><%= '${props.content}' %></p>
|
|
541
|
+
</div>
|
|
542
|
+
`
|
|
543
|
+
},
|
|
544
|
+
DataTable: {
|
|
545
|
+
description: 'A table for displaying structured data',
|
|
546
|
+
props: z.object({
|
|
547
|
+
headers: z.array(z.string()),
|
|
548
|
+
rows: z.array(z.array(z.string()))
|
|
549
|
+
}),
|
|
550
|
+
render: (props) => {
|
|
551
|
+
// Build table HTML from headers and rows
|
|
552
|
+
const headerRow = props.headers.map(h => '<th>' + h + '</th>').join('');
|
|
553
|
+
const bodyRows = props.rows.map(row =>
|
|
554
|
+
'<tr>' + row.map(cell => '<td>' + cell + '</td>').join('') + '</tr>'
|
|
555
|
+
).join('');
|
|
556
|
+
return '<table><thead><tr>' + headerRow + '</tr></thead><tbody>' + bodyRows + '</tbody></table>';
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
})
|
|
560
|
+
.ui('Dashboard', {
|
|
561
|
+
template: (state) => `
|
|
562
|
+
Create a dashboard using CustomCard components to display:
|
|
563
|
+
- User name: <%= '${state.userName}' %>
|
|
564
|
+
- Account status: <%= '${state.status}' %>
|
|
565
|
+
Use DataTable to show recent activity.
|
|
566
|
+
`,
|
|
567
|
+
responseSchema: z.object({
|
|
568
|
+
acknowledged: z.boolean()
|
|
569
|
+
})
|
|
570
|
+
});
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
### Using `createBrain()` for Project Configuration
|
|
574
|
+
|
|
575
|
+
For project-wide configuration, use `createBrain()` in your `brain.ts` file:
|
|
576
|
+
|
|
577
|
+
```typescript
|
|
578
|
+
// brain.ts
|
|
579
|
+
import { createBrain } from '@positronic/core';
|
|
580
|
+
import { z } from 'zod';
|
|
581
|
+
|
|
582
|
+
export const brain = createBrain({
|
|
583
|
+
services: {
|
|
584
|
+
logger: console,
|
|
585
|
+
api: apiClient
|
|
586
|
+
},
|
|
587
|
+
tools: {
|
|
588
|
+
search: {
|
|
589
|
+
description: 'Search the web',
|
|
590
|
+
inputSchema: z.object({ query: z.string() }),
|
|
591
|
+
execute: async ({ query }) => searchWeb(query)
|
|
592
|
+
}
|
|
593
|
+
},
|
|
594
|
+
components: {
|
|
595
|
+
Alert: {
|
|
596
|
+
description: 'Alert banner',
|
|
597
|
+
props: z.object({ message: z.string(), type: z.enum(['info', 'warning', 'error']) }),
|
|
598
|
+
render: (props) => `<div class="alert alert-<%= '${props.type}' %>"><%= '${props.message}' %></div>`
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
All brains created with this factory will have access to the configured services, tools, and components.
|
|
605
|
+
|
|
476
606
|
## Running Brains
|
|
477
607
|
|
|
478
608
|
### Basic Execution
|
|
479
609
|
|
|
480
610
|
```typescript
|
|
481
|
-
const
|
|
611
|
+
const myBrain = brain('Simple').step('Process', () => ({ result: 'done' }));
|
|
482
612
|
|
|
483
613
|
// Run and collect events
|
|
484
|
-
for await (const event of
|
|
614
|
+
for await (const event of myBrain.run({ client: aiClient })) {
|
|
485
615
|
console.log(event.type); // START, STEP_START, STEP_COMPLETE, etc.
|
|
486
616
|
}
|
|
487
617
|
```
|
|
@@ -678,6 +808,178 @@ Extract prompts to separate files when:
|
|
|
678
808
|
- The prompt might be reused in other brains
|
|
679
809
|
- You want to test the prompt logic separately
|
|
680
810
|
|
|
811
|
+
## Batch Prompt Mode
|
|
812
|
+
|
|
813
|
+
When you need to run the same prompt over multiple items, use batch mode with the `over` option:
|
|
814
|
+
|
|
815
|
+
```typescript
|
|
816
|
+
brain('Batch Processor')
|
|
817
|
+
.step('Initialize', () => ({
|
|
818
|
+
items: [
|
|
819
|
+
{ id: 1, title: 'First item' },
|
|
820
|
+
{ id: 2, title: 'Second item' },
|
|
821
|
+
{ id: 3, title: 'Third item' }
|
|
822
|
+
]
|
|
823
|
+
}))
|
|
824
|
+
.prompt('Summarize Items', {
|
|
825
|
+
template: (item) => `Summarize this item: <%= '${item.title}' %>`,
|
|
826
|
+
outputSchema: {
|
|
827
|
+
schema: z.object({ summary: z.string() }),
|
|
828
|
+
name: 'summaries' as const
|
|
829
|
+
}
|
|
830
|
+
}, {
|
|
831
|
+
over: (state) => state.items, // Array to iterate over
|
|
832
|
+
concurrency: 10, // Parallel requests (default: 10)
|
|
833
|
+
stagger: 100, // Delay between requests in ms
|
|
834
|
+
retry: {
|
|
835
|
+
maxRetries: 3,
|
|
836
|
+
backoff: 'exponential',
|
|
837
|
+
initialDelay: 1000,
|
|
838
|
+
maxDelay: 30000,
|
|
839
|
+
},
|
|
840
|
+
error: (item, error) => ({ summary: 'Failed to summarize' }) // Fallback on error
|
|
841
|
+
})
|
|
842
|
+
.step('Process Results', ({ state }) => ({
|
|
843
|
+
...state,
|
|
844
|
+
// summaries is [item, response][] - array of tuples
|
|
845
|
+
processedSummaries: state.summaries.map(([item, response]) => ({
|
|
846
|
+
id: item.id,
|
|
847
|
+
summary: response.summary
|
|
848
|
+
}))
|
|
849
|
+
}));
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
### Batch Options
|
|
853
|
+
|
|
854
|
+
- `over: (state) => T[]` - Function returning the array to iterate over
|
|
855
|
+
- `concurrency: number` - Maximum parallel requests (default: 10)
|
|
856
|
+
- `stagger: number` - Milliseconds to wait between starting requests
|
|
857
|
+
- `retry: RetryConfig` - Retry configuration for failed requests
|
|
858
|
+
- `error: (item, error) => Response` - Fallback function when a request fails
|
|
859
|
+
|
|
860
|
+
### Result Format
|
|
861
|
+
|
|
862
|
+
The result is stored as an array of `[item, response]` tuples, preserving the relationship between each input item and its generated response.
|
|
863
|
+
|
|
864
|
+
## Agent Steps
|
|
865
|
+
|
|
866
|
+
For complex AI workflows that require tool use, use the `.brain()` method with an agent configuration:
|
|
867
|
+
|
|
868
|
+
```typescript
|
|
869
|
+
brain('Research Assistant')
|
|
870
|
+
.step('Initialize', () => ({
|
|
871
|
+
query: 'What are the latest developments in AI?'
|
|
872
|
+
}))
|
|
873
|
+
.brain('Research Agent', {
|
|
874
|
+
system: 'You are a helpful research assistant with access to search tools.',
|
|
875
|
+
prompt: ({ query }) => `Research this topic: <%= '${query}' %>`,
|
|
876
|
+
tools: {
|
|
877
|
+
search: {
|
|
878
|
+
description: 'Search the web for information',
|
|
879
|
+
inputSchema: z.object({
|
|
880
|
+
query: z.string().describe('The search query')
|
|
881
|
+
}),
|
|
882
|
+
execute: async ({ query }) => {
|
|
883
|
+
// Implement search logic
|
|
884
|
+
const results = await searchWeb(query);
|
|
885
|
+
return { results };
|
|
886
|
+
}
|
|
887
|
+
},
|
|
888
|
+
summarize: {
|
|
889
|
+
description: 'Summarize a piece of text',
|
|
890
|
+
inputSchema: z.object({
|
|
891
|
+
text: z.string().describe('Text to summarize')
|
|
892
|
+
}),
|
|
893
|
+
execute: async ({ text }) => {
|
|
894
|
+
return { summary: text.slice(0, 100) + '...' };
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
},
|
|
898
|
+
maxTokens: 10000,
|
|
899
|
+
})
|
|
900
|
+
.step('Format Results', ({ state, brainState }) => ({
|
|
901
|
+
...state,
|
|
902
|
+
researchResults: brainState.response
|
|
903
|
+
}));
|
|
904
|
+
```
|
|
905
|
+
|
|
906
|
+
### Agent Configuration Options
|
|
907
|
+
|
|
908
|
+
- `system: string` - System prompt for the agent
|
|
909
|
+
- `prompt: string | ((state) => string)` - User prompt (can be a function)
|
|
910
|
+
- `tools: Record<string, ToolDefinition>` - Tools available to the agent
|
|
911
|
+
- `maxTokens: number` - Maximum tokens for the agent response
|
|
912
|
+
|
|
913
|
+
### Tool Definition
|
|
914
|
+
|
|
915
|
+
Each tool requires:
|
|
916
|
+
- `description: string` - What the tool does
|
|
917
|
+
- `inputSchema: ZodSchema` - Zod schema for the tool's input
|
|
918
|
+
- `execute: (input) => Promise<any>` - Function to execute when the tool is called
|
|
919
|
+
|
|
920
|
+
## Environment and Pages Service
|
|
921
|
+
|
|
922
|
+
### The `env` Parameter
|
|
923
|
+
|
|
924
|
+
Steps have access to the runtime environment via the `env` parameter:
|
|
925
|
+
|
|
926
|
+
```typescript
|
|
927
|
+
brain('Environment Example')
|
|
928
|
+
.step('Use Environment', ({ state, env }) => {
|
|
929
|
+
// env.origin - Base URL of the deployment
|
|
930
|
+
console.log('Running at:', env.origin);
|
|
931
|
+
|
|
932
|
+
// env.secrets - Type-augmented secrets object
|
|
933
|
+
const apiKey = env.secrets.EXTERNAL_API_KEY;
|
|
934
|
+
|
|
935
|
+
return {
|
|
936
|
+
...state,
|
|
937
|
+
baseUrl: env.origin,
|
|
938
|
+
configured: true
|
|
939
|
+
};
|
|
940
|
+
});
|
|
941
|
+
```
|
|
942
|
+
|
|
943
|
+
### The `pages` Service
|
|
944
|
+
|
|
945
|
+
The `pages` service allows you to create and manage HTML pages programmatically:
|
|
946
|
+
|
|
947
|
+
```typescript
|
|
948
|
+
brain('Page Creator')
|
|
949
|
+
.step('Create Custom Page', async ({ state, pages, env }) => {
|
|
950
|
+
// Create a page with HTML content
|
|
951
|
+
const page = await pages.create(
|
|
952
|
+
`<html>
|
|
953
|
+
<body>
|
|
954
|
+
<h1>Hello, <%= '${state.userName}' %>!</h1>
|
|
955
|
+
<p>Your dashboard is ready.</p>
|
|
956
|
+
</body>
|
|
957
|
+
</html>`,
|
|
958
|
+
{ persist: true } // Keep the page after brain completes
|
|
959
|
+
);
|
|
960
|
+
|
|
961
|
+
return {
|
|
962
|
+
...state,
|
|
963
|
+
dashboardUrl: page.url, // URL where users can view the page
|
|
964
|
+
pageWebhook: page.webhook // Webhook for form submissions (if any)
|
|
965
|
+
};
|
|
966
|
+
})
|
|
967
|
+
.step('Notify User', async ({ state, slack }) => {
|
|
968
|
+
await slack.post('#general', `Your dashboard: <%= '${state.dashboardUrl}' %>`);
|
|
969
|
+
return state;
|
|
970
|
+
});
|
|
971
|
+
```
|
|
972
|
+
|
|
973
|
+
### Page Options
|
|
974
|
+
|
|
975
|
+
- `persist: boolean` - If true, the page remains accessible after the brain completes
|
|
976
|
+
|
|
977
|
+
### Page Object
|
|
978
|
+
|
|
979
|
+
The created page object contains:
|
|
980
|
+
- `url: string` - Public URL to access the page
|
|
981
|
+
- `webhook: WebhookConfig` - Webhook configuration for handling form submissions
|
|
982
|
+
|
|
681
983
|
## UI Steps
|
|
682
984
|
|
|
683
985
|
UI steps allow brains to generate dynamic user interfaces using AI. The `.ui()` step generates a page and provides a `page` object to the next step. You then notify users and use `waitFor` to pause until the form is submitted.
|
|
@@ -56,7 +56,7 @@ const result = await runBrainTest(brain, {
|
|
|
56
56
|
client: mockClient, // Optional: defaults to createMockClient()
|
|
57
57
|
initialState: { count: 0 }, // Optional: initial state
|
|
58
58
|
resources: resourceLoader, // Optional: resources
|
|
59
|
-
|
|
59
|
+
options: { mode: 'test' } // Optional: brain-specific options
|
|
60
60
|
});
|
|
61
61
|
```
|
|
62
62
|
|
|
@@ -64,9 +64,9 @@ const result = await runBrainTest(brain, {
|
|
|
64
64
|
- `completed: boolean` - Whether the brain completed successfully
|
|
65
65
|
- `error: Error | null` - Any error that occurred
|
|
66
66
|
- `finalState: State` - The final state after all steps
|
|
67
|
-
- `
|
|
67
|
+
- `events: BrainEvent[]` - All emitted events during execution
|
|
68
68
|
|
|
69
|
-
##
|
|
69
|
+
## MockClient API
|
|
70
70
|
|
|
71
71
|
### Creating a Mock Client
|
|
72
72
|
|
|
@@ -76,44 +76,42 @@ const mockClient = createMockClient();
|
|
|
76
76
|
|
|
77
77
|
### Mocking Responses
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
```typescript
|
|
81
|
-
mockClient.mockNextResponse({
|
|
82
|
-
answer: 'The capital of France is Paris',
|
|
83
|
-
confidence: 0.95
|
|
84
|
-
});
|
|
85
|
-
```
|
|
79
|
+
Queue responses that will be consumed in order by `generateObject` calls:
|
|
86
80
|
|
|
87
|
-
#### Multiple Responses
|
|
88
81
|
```typescript
|
|
82
|
+
// Queue one or more responses
|
|
89
83
|
mockClient.mockResponses(
|
|
90
84
|
{ step1: 'completed' },
|
|
91
85
|
{ step2: 'processed' },
|
|
92
86
|
{ finalResult: 'success' }
|
|
93
87
|
);
|
|
94
|
-
```
|
|
95
88
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
mockClient.mockNextError('API rate limit exceeded');
|
|
99
|
-
// or
|
|
100
|
-
mockClient.mockNextError(new Error('Connection timeout'));
|
|
89
|
+
// Clear all mocked responses
|
|
90
|
+
mockClient.clearMocks();
|
|
101
91
|
```
|
|
102
92
|
|
|
103
93
|
### Assertions
|
|
104
94
|
|
|
95
|
+
Use standard Jest assertions on the mock:
|
|
96
|
+
|
|
105
97
|
```typescript
|
|
106
98
|
// Check call count
|
|
107
|
-
mockClient.
|
|
99
|
+
expect(mockClient.generateObject).toHaveBeenCalledTimes(3);
|
|
108
100
|
|
|
109
101
|
// Check call parameters
|
|
110
|
-
mockClient.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
})
|
|
102
|
+
expect(mockClient.generateObject).toHaveBeenCalledWith(
|
|
103
|
+
expect.objectContaining({
|
|
104
|
+
prompt: expect.stringContaining('Generate a summary')
|
|
105
|
+
})
|
|
106
|
+
);
|
|
114
107
|
|
|
115
|
-
//
|
|
116
|
-
|
|
108
|
+
// Check specific call (0-indexed)
|
|
109
|
+
expect(mockClient.generateObject).toHaveBeenNthCalledWith(
|
|
110
|
+
1,
|
|
111
|
+
expect.objectContaining({
|
|
112
|
+
prompt: expect.stringContaining('first prompt')
|
|
113
|
+
})
|
|
114
|
+
);
|
|
117
115
|
```
|
|
118
116
|
|
|
119
117
|
## Testing Patterns
|
|
@@ -145,13 +143,19 @@ it('should generate personalized recommendations', async () => {
|
|
|
145
143
|
|
|
146
144
|
### Testing Error Cases
|
|
147
145
|
|
|
146
|
+
To test error handling, create a mock client that throws:
|
|
147
|
+
|
|
148
148
|
```typescript
|
|
149
149
|
it('should handle API failures gracefully', async () => {
|
|
150
|
-
// Arrange
|
|
151
|
-
|
|
150
|
+
// Arrange - create a mock that throws on the first call
|
|
151
|
+
const errorClient = {
|
|
152
|
+
generateObject: jest.fn().mockRejectedValue(
|
|
153
|
+
new Error('Service temporarily unavailable')
|
|
154
|
+
)
|
|
155
|
+
};
|
|
152
156
|
|
|
153
157
|
// Act
|
|
154
|
-
const result = await runBrainTest(processingBrain, { client:
|
|
158
|
+
const result = await runBrainTest(processingBrain, { client: errorClient });
|
|
155
159
|
|
|
156
160
|
// Assert
|
|
157
161
|
expect(result.completed).toBe(false);
|
|
@@ -167,6 +171,7 @@ Verify that data flows correctly through your brain:
|
|
|
167
171
|
it('should use customer data to generate personalized content', async () => {
|
|
168
172
|
// Arrange
|
|
169
173
|
const customerName = 'Alice';
|
|
174
|
+
const mockClient = createMockClient();
|
|
170
175
|
mockClient.mockResponses(
|
|
171
176
|
{ greeting: 'Hello Alice!', tone: 'friendly' },
|
|
172
177
|
{ email: 'Personalized email content...' }
|
|
@@ -179,8 +184,11 @@ it('should use customer data to generate personalized content', async () => {
|
|
|
179
184
|
});
|
|
180
185
|
|
|
181
186
|
// Assert that the AI used the customer data
|
|
182
|
-
|
|
183
|
-
|
|
187
|
+
expect(mockClient.generateObject).toHaveBeenCalledWith(
|
|
188
|
+
expect.objectContaining({
|
|
189
|
+
prompt: expect.stringContaining(customerName)
|
|
190
|
+
})
|
|
191
|
+
);
|
|
184
192
|
expect(result.finalState.email).toContain('Personalized email content');
|
|
185
193
|
});
|
|
186
194
|
```
|
|
@@ -227,28 +235,23 @@ Following testing best practices, avoid testing:
|
|
|
227
235
|
## Complete Example
|
|
228
236
|
|
|
229
237
|
```typescript
|
|
230
|
-
import { createMockClient, runBrainTest } from '
|
|
231
|
-
import analysisBrain from '
|
|
238
|
+
import { createMockClient, runBrainTest } from './test-utils.js';
|
|
239
|
+
import analysisBrain from '../brains/analysis-brain.js';
|
|
232
240
|
|
|
233
241
|
describe('analysis-brain', () => {
|
|
234
|
-
let mockClient;
|
|
235
|
-
|
|
236
|
-
beforeEach(() => {
|
|
237
|
-
mockClient = createMockClient();
|
|
238
|
-
});
|
|
239
|
-
|
|
240
242
|
it('should analyze customer feedback and generate insights', async () => {
|
|
241
243
|
// Arrange: Set up AI to return analysis
|
|
242
|
-
mockClient
|
|
244
|
+
const mockClient = createMockClient();
|
|
245
|
+
mockClient.mockResponses({
|
|
243
246
|
sentiment: 'positive',
|
|
244
247
|
keywords: ['innovation', 'quality', 'service'],
|
|
245
248
|
summary: 'Customers appreciate product quality and innovation'
|
|
246
249
|
});
|
|
247
250
|
|
|
248
251
|
// Act: Run analysis on customer feedback
|
|
249
|
-
const result = await runBrainTest(analysisBrain, {
|
|
252
|
+
const result = await runBrainTest(analysisBrain, {
|
|
250
253
|
client: mockClient,
|
|
251
|
-
initialState: {
|
|
254
|
+
initialState: {
|
|
252
255
|
feedback: 'Your product is innovative and high quality...'
|
|
253
256
|
}
|
|
254
257
|
});
|
|
@@ -265,14 +268,18 @@ describe('analysis-brain', () => {
|
|
|
265
268
|
|
|
266
269
|
it('should handle analysis service outages', async () => {
|
|
267
270
|
// Arrange: Simulate service failure
|
|
268
|
-
|
|
271
|
+
const errorClient = {
|
|
272
|
+
generateObject: jest.fn().mockRejectedValue(
|
|
273
|
+
new Error('Analysis service unavailable')
|
|
274
|
+
)
|
|
275
|
+
};
|
|
269
276
|
|
|
270
277
|
// Act: Attempt analysis
|
|
271
|
-
const result = await runBrainTest(analysisBrain, {
|
|
272
|
-
client:
|
|
278
|
+
const result = await runBrainTest(analysisBrain, {
|
|
279
|
+
client: errorClient,
|
|
273
280
|
initialState: { feedback: 'Some feedback...' }
|
|
274
281
|
});
|
|
275
|
-
|
|
282
|
+
|
|
276
283
|
// Assert: Verify graceful failure
|
|
277
284
|
expect(result.completed).toBe(false);
|
|
278
285
|
expect(result.error?.message).toBe('Analysis service unavailable');
|
|
@@ -300,12 +307,12 @@ expect(result.finalState.myProperty).toBe('value');
|
|
|
300
307
|
### Debugging Tips
|
|
301
308
|
|
|
302
309
|
```typescript
|
|
303
|
-
// See what prompts
|
|
304
|
-
const calls = mockClient.
|
|
305
|
-
console.log('AI prompts:', calls.map(c => c.
|
|
310
|
+
// See what prompts were sent to AI
|
|
311
|
+
const calls = mockClient.generateObject.mock.calls;
|
|
312
|
+
console.log('AI prompts:', calls.map(c => c[0].prompt));
|
|
306
313
|
|
|
307
|
-
// Check execution
|
|
308
|
-
console.log('
|
|
314
|
+
// Check events that occurred during execution
|
|
315
|
+
console.log('Events:', result.events.map(e => e.type));
|
|
309
316
|
```
|
|
310
317
|
|
|
311
318
|
## Next Steps
|
|
@@ -42,10 +42,10 @@ import { brain } from '@positronic/core';
|
|
|
42
42
|
|
|
43
43
|
### Configuring Services
|
|
44
44
|
|
|
45
|
-
To add project-wide services, modify the `brain.ts` file in the root directory
|
|
45
|
+
To add project-wide services, modify the `brain.ts` file in the root directory using `createBrain()`:
|
|
46
46
|
|
|
47
47
|
```typescript
|
|
48
|
-
import {
|
|
48
|
+
import { createBrain } from '@positronic/core';
|
|
49
49
|
|
|
50
50
|
// Define your services
|
|
51
51
|
interface ProjectServices {
|
|
@@ -59,28 +59,25 @@ interface ProjectServices {
|
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
// Export the
|
|
63
|
-
export
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
62
|
+
// Export the project brain factory
|
|
63
|
+
export const brain = createBrain({
|
|
64
|
+
services: {
|
|
65
|
+
logger: {
|
|
66
|
+
info: (msg) => console.log(`[<%= '${new Date().toISOString()}' %>] INFO: <%= '${msg}' %>`),
|
|
67
|
+
error: (msg) => console.error(`[<%= '${new Date().toISOString()}' %>] ERROR: <%= '${msg}' %>`)
|
|
68
|
+
},
|
|
69
|
+
database: {
|
|
70
|
+
get: async (key) => {
|
|
71
|
+
// Your database implementation
|
|
72
|
+
return localStorage.getItem(key);
|
|
71
73
|
},
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return localStorage.getItem(key);
|
|
76
|
-
},
|
|
77
|
-
set: async (key, value) => {
|
|
78
|
-
// Your database implementation
|
|
79
|
-
localStorage.setItem(key, JSON.stringify(value));
|
|
80
|
-
}
|
|
74
|
+
set: async (key, value) => {
|
|
75
|
+
// Your database implementation
|
|
76
|
+
localStorage.setItem(key, JSON.stringify(value));
|
|
81
77
|
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
84
81
|
```
|
|
85
82
|
|
|
86
83
|
Now all brains automatically have access to these services:
|
|
@@ -158,9 +155,9 @@ Use `.env` files for configuration:
|
|
|
158
155
|
ANTHROPIC_API_KEY=your-key-here
|
|
159
156
|
OPENAI_API_KEY=your-key-here
|
|
160
157
|
|
|
161
|
-
# Backend-specific
|
|
162
|
-
|
|
163
|
-
CLOUDFLARE_API_TOKEN=your-api-token
|
|
158
|
+
# Backend-specific (Cloudflare example)
|
|
159
|
+
CLOUDFLARE_ACCOUNT_ID=your-account-id
|
|
160
|
+
CLOUDFLARE_API_TOKEN=your-api-token
|
|
164
161
|
```
|
|
165
162
|
|
|
166
163
|
## Best Practices
|
|
@@ -224,7 +221,7 @@ const api = {
|
|
|
224
221
|
post: async (path: string, data: any) => {
|
|
225
222
|
const response = await fetch(`https://api.example.com<%= '${path}' %>`, {
|
|
226
223
|
method: 'POST',
|
|
227
|
-
headers: {
|
|
224
|
+
headers: {
|
|
228
225
|
'Authorization': `Bearer <%= '${process.env.API_KEY}' %>`,
|
|
229
226
|
'Content-Type': 'application/json'
|
|
230
227
|
},
|
|
@@ -247,22 +247,20 @@ services/
|
|
|
247
247
|
Then in your `brain.ts` (at the project root):
|
|
248
248
|
|
|
249
249
|
```typescript
|
|
250
|
+
import { createBrain } from '@positronic/core';
|
|
250
251
|
import gmail from './services/gmail.js';
|
|
251
252
|
import slack from './services/slack.js';
|
|
252
253
|
import database from './services/database.js';
|
|
253
254
|
import analytics from './services/analytics.js';
|
|
254
255
|
|
|
255
|
-
export
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
analytics
|
|
264
|
-
});
|
|
265
|
-
}
|
|
256
|
+
export const brain = createBrain({
|
|
257
|
+
services: {
|
|
258
|
+
gmail,
|
|
259
|
+
slack,
|
|
260
|
+
database,
|
|
261
|
+
analytics
|
|
262
|
+
}
|
|
263
|
+
});
|
|
266
264
|
```
|
|
267
265
|
|
|
268
266
|
This keeps your service implementations separate from your brain logic and makes them easier to test and maintain.
|
|
@@ -452,7 +450,7 @@ describe('FeedbackProcessor', () => {
|
|
|
452
450
|
});
|
|
453
451
|
|
|
454
452
|
// Step 2: Create minimal brain implementation
|
|
455
|
-
import { brain } from '
|
|
453
|
+
import { brain } from '../brain.js';
|
|
456
454
|
import { z } from 'zod';
|
|
457
455
|
|
|
458
456
|
const feedbackBrain = brain('feedback-processor')
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* esbuild configuration for bundling UI components.
|
|
3
3
|
*
|
|
4
|
-
* This bundles the components from
|
|
4
|
+
* This bundles the components from .positronic/bundle.ts into a single
|
|
5
5
|
* JavaScript file that can be served to the browser.
|
|
6
6
|
*
|
|
7
7
|
* Run: npm run build:components
|
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
import * as esbuild from 'esbuild';
|
|
11
11
|
|
|
12
12
|
await esbuild.build({
|
|
13
|
-
entryPoints: ['
|
|
13
|
+
entryPoints: ['.positronic/bundle.ts'],
|
|
14
14
|
bundle: true,
|
|
15
15
|
external: ['react', 'react-dom'],
|
|
16
16
|
format: 'iife',
|
|
17
|
-
outfile: 'dist/components.js',
|
|
17
|
+
outfile: '.positronic/dist/components.js',
|
|
18
18
|
jsx: 'transform',
|
|
19
19
|
jsxFactory: 'React.createElement',
|
|
20
20
|
jsxFragment: 'React.Fragment',
|
package/template/package.json
CHANGED
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"zod": "^3.24.1",
|
|
17
17
|
"@positronic/client-vercel": "<%= positronicClientVercelVersion %>",
|
|
18
|
-
"@ai-sdk/
|
|
18
|
+
"@ai-sdk/google": "^3.0.13",
|
|
19
|
+
"ai": "^6.0.48",
|
|
19
20
|
"@positronic/core": "<%= positronicCoreVersion %>",
|
|
20
21
|
"@positronic/gen-ui-components": "<%= positronicGenUIComponentsVersion %>"<% if (backend === 'cloudflare') { %>,
|
|
21
22
|
"@positronic/cloudflare": "<%= positronicCloudflareVersion %>"<% } %>
|
|
@@ -27,7 +28,7 @@
|
|
|
27
28
|
"@jest/globals": "^30.0.4",
|
|
28
29
|
"ts-jest": "^29.2.6",
|
|
29
30
|
"@types/jest": "^30.0.0",
|
|
30
|
-
"esbuild": "^0.
|
|
31
|
+
"esbuild": "^0.27.2",
|
|
31
32
|
"@types/react": "^18.2.0"
|
|
32
33
|
}
|
|
33
34
|
}
|
package/template/runner.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { BrainRunner } from '@positronic/core';
|
|
2
2
|
import { VercelClient } from '@positronic/client-vercel';
|
|
3
|
-
import {
|
|
3
|
+
import { google } from '@ai-sdk/google';
|
|
4
4
|
|
|
5
5
|
export const runner = new BrainRunner({
|
|
6
6
|
adapters: [],
|
|
7
|
-
client: new VercelClient(
|
|
7
|
+
client: new VercelClient(google('gemini-3-pro-preview')),
|
|
8
8
|
resources: {},
|
|
9
9
|
});
|