interaqt 0.7.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,550 @@
1
+ ---
2
+ name: integration-implementation-handler
3
+ description: Guide for implementing integrations to connect reactive backend with imperative external systems
4
+ model: inherit
5
+ color: purple
6
+ ---
7
+
8
+ # Integration Implementation Guide
9
+
10
+ ## Overview
11
+
12
+ **Integrations** bridge the gap between the reactive backend framework and imperative external systems (APIs, services, databases, message queues, etc.). They allow external non-reactive systems to participate in the reactive data flow.
13
+
14
+ ## Integration Interface
15
+
16
+ Every integration must implement the `IIntegration` interface:
17
+
18
+ ```typescript
19
+ export type IIntegration = {
20
+ configure?:() => Promise<any>
21
+ setup?:(controller: Controller) => Promise<any>
22
+ createSideEffects:() => RecordMutationSideEffect[]
23
+ createAPIs?: () => APIs
24
+ }
25
+ ```
26
+
27
+ ### Constructor Arguments
28
+
29
+ ```typescript
30
+ export type IIntegrationConstructorArgs = {
31
+ entities: EntityInstance[],
32
+ relations: RelationInstance[],
33
+ activities: ActivityInstance[],
34
+ interactions: InteractionInstance[],
35
+ dict: DictionaryInstance[]
36
+ }
37
+
38
+ export type IIntegrationHandles = {
39
+ [k:string]: any // External handles like websocketServer, etc.
40
+ }
41
+
42
+ class MyIntegration implements IIntegration {
43
+ constructor(
44
+ public args: IIntegrationConstructorArgs,
45
+ public handles: IIntegrationHandles
46
+ ) {}
47
+ }
48
+ ```
49
+
50
+ ## Lifecycle Methods
51
+
52
+ ### 1. `configure()` - Schema Augmentation
53
+
54
+ **Purpose**: Modify the reactive schema before system initialization.
55
+
56
+ **Use Cases**:
57
+ - Inject new entities into the system
58
+ - Add computed properties to existing entities
59
+ - Configure state machines for reactive properties
60
+ - Inject computations into relations
61
+
62
+ **Execution Timing**: Before Controller initialization
63
+
64
+ **Example - Injecting Entity**:
65
+ ```typescript
66
+ async configure() {
67
+ // Create and inject a new entity for external events
68
+ const TaskEvent = Entity.create({
69
+ name: 'LLMPicGenAsyncTaskEvent',
70
+ properties: [
71
+ Property.create({ name: 'taskId', type: 'string' }),
72
+ Property.create({ name: 'status', type: 'string' }),
73
+ Property.create({ name: 'result', type: 'object' }),
74
+ ]
75
+ });
76
+
77
+ // Inject into entities array
78
+ this.args.entities.push(TaskEvent);
79
+ }
80
+ ```
81
+
82
+ **Example - Injecting Property Computation**:
83
+ ```typescript
84
+ async configure() {
85
+ // Find target entities
86
+ const streamEntities = Stream.instances;
87
+
88
+ for (const entity of streamEntities) {
89
+ // Find and inject computation into property
90
+ const urlProperty = entity.properties.find(p => p.name === 'url')!;
91
+
92
+ urlProperty.computation = Custom.create({
93
+ name: 'generateStreamUrl',
94
+ async getInitialValue(this: Controller, record?: any) {
95
+ // Call external API to generate URL
96
+ const timestamp = Math.floor(Date.now() / 1000) + 3600;
97
+ const authString = `/${appName}/${streamName}${key}${timestamp}`;
98
+ const sign = crypto.createHash('md5').update(authString).digest('hex');
99
+ return `rtmp://${domain}/${appName}/${streamName}?volcTime=${timestamp}&volcSecret=${sign}`;
100
+ },
101
+ incrementalCompute: async function(this: { controller: Controller, state: any }, lastValue: any, mutationEvent: any, record: any, dataDeps: any) {
102
+ // Skip recomputation if URL should remain constant
103
+ return ComputationResult.skip();
104
+ }
105
+ });
106
+ }
107
+ }
108
+ ```
109
+
110
+ **Example - Injecting State Machine**:
111
+ ```typescript
112
+ async configure() {
113
+ const taskEntities = AsyncTask.instances;
114
+
115
+ for (const taskEntity of taskEntities) {
116
+ const statusProperty = taskEntity.properties.find(p => p.name === 'status');
117
+
118
+ // Create state node
119
+ const statusState = StateNode.create({
120
+ name: 'status',
121
+ computeValue: (lastValue, mutationEvent) => {
122
+ return mutationEvent?.record?.status || lastValue || 'pending';
123
+ }
124
+ });
125
+
126
+ // Configure state machine
127
+ statusProperty.computation = StateMachine.create({
128
+ states: [statusState],
129
+ initialState: statusState,
130
+ transfers: [
131
+ StateTransfer.create({
132
+ trigger: {
133
+ recordName: 'TaskEvent', // Event entity name
134
+ type: 'create',
135
+ record: { taskType: taskEntity.name }
136
+ },
137
+ current: statusState,
138
+ next: statusState,
139
+ computeTarget: async function(this: Controller, mutationEvent: any) {
140
+ const event = mutationEvent.record;
141
+ if (event.status) {
142
+ return { id: event.taskId }; // Target record to update
143
+ }
144
+ return undefined;
145
+ }
146
+ })
147
+ ]
148
+ });
149
+ }
150
+ }
151
+ ```
152
+
153
+ ### 2. `setup()` - Runtime Initialization
154
+
155
+ **Purpose**: Initialize external connections and register runtime handlers.
156
+
157
+ **Use Cases**:
158
+ - Connect to external services (Redis, databases, message queues)
159
+ - Register event listeners on external handles
160
+ - Set up WebSocket connection handlers
161
+ - Initialize API clients
162
+
163
+ **Execution Timing**: After Controller initialization, before server starts
164
+
165
+ **Example - External Service Connection**:
166
+ ```typescript
167
+ async setup(controller: Controller) {
168
+ const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379';
169
+
170
+ this.subscriber = createClient({ url: redisUrl });
171
+ this.subscriber.on('error', (err) => console.error('Redis Error:', err));
172
+ await this.subscriber.connect();
173
+
174
+ this.publisher = createClient({ url: redisUrl });
175
+ await this.publisher.connect();
176
+ }
177
+ ```
178
+
179
+ **Example - WebSocket Handler Registration**:
180
+ ```typescript
181
+ async setup(controller: Controller) {
182
+ // Register handler for user connection
183
+ this.handles.websocketServer.on('connection-setup-completed', async (ws, request) => {
184
+ const user = ws.user;
185
+ if (!user) return;
186
+
187
+ // Query user's channels
188
+ const userChannels = await controller.system.storage.find(
189
+ 'UserChannelRelation',
190
+ MatchExp.atom({ key: 'source.id', value: ['=', user.id] }),
191
+ undefined,
192
+ ['id', ['target', { attributeQuery: ['id'] }]]
193
+ );
194
+
195
+ // Subscribe to Redis channels for this user
196
+ for (let relation of userChannels) {
197
+ this.subscriber?.subscribe(relation.target.id, (message) => {
198
+ ws.send(message);
199
+ });
200
+ }
201
+ });
202
+ }
203
+ ```
204
+
205
+ ### 3. `createSideEffects()` - Reactive Synchronization
206
+
207
+ **Purpose**: Define side effects that synchronize reactive data changes to external systems.
208
+
209
+ **Use Cases**:
210
+ - Push data mutations to external APIs
211
+ - Publish messages to message queues
212
+ - Update external database records
213
+ - Trigger external workflows
214
+
215
+ **Return Value**: Array of `RecordMutationSideEffect`
216
+
217
+ **Example - Publishing to Message Queue**:
218
+ ```typescript
219
+ createSideEffects(): RecordMutationSideEffect[] {
220
+ const dispatcher = this;
221
+
222
+ return [
223
+ RecordMutationSideEffect.create({
224
+ name: 'channel_message_publish_sideeffect',
225
+ record: {
226
+ name: 'ChannelMessageRelation', // Target relation/entity
227
+ },
228
+ async content(this: Controller, event: RecordMutationEvent) {
229
+ if (event.type === 'create') {
230
+ const channelId = event.record?.source.id;
231
+ const message = event.record?.target;
232
+
233
+ if (message && channelId) {
234
+ const messageData = {
235
+ type: 'message',
236
+ channelId,
237
+ message
238
+ };
239
+
240
+ // Publish to external system
241
+ await dispatcher.publisher?.publish(
242
+ channelId,
243
+ JSON.stringify(messageData)
244
+ );
245
+ console.log('Published to Redis:', channelId);
246
+ }
247
+ }
248
+ }
249
+ })
250
+ ];
251
+ }
252
+ ```
253
+
254
+ **Example - Multiple Side Effects**:
255
+ ```typescript
256
+ createSideEffects(): RecordMutationSideEffect[] {
257
+ return [
258
+ // Handle relation creation
259
+ ...UserChannelRelation.instances.map(instance =>
260
+ RecordMutationSideEffect.create({
261
+ name: `channel_user_relation_${instance.name}_create`,
262
+ record: { name: instance.name! },
263
+ async content(this: Controller, event: RecordMutationEvent) {
264
+ if (event.type === 'create') {
265
+ // Subscribe online users to new channels
266
+ const clients = Array.from(websocketServer.clients);
267
+ const wsClient = clients.find(c => c.user.id === event.record?.source.id);
268
+ if (wsClient) {
269
+ subscriber?.subscribe(event.record?.target.id, (msg) => {
270
+ wsClient.send(msg);
271
+ });
272
+ }
273
+ }
274
+ }
275
+ })
276
+ ),
277
+
278
+ // Handle relation deletion
279
+ ...UserChannelRelation.instances.map(instance =>
280
+ RecordMutationSideEffect.create({
281
+ name: `channel_user_relation_${instance.name}_delete`,
282
+ record: { name: instance.name! },
283
+ async content(this: Controller, event: RecordMutationEvent) {
284
+ if (event.type === 'delete') {
285
+ // Unsubscribe user from channel
286
+ // ... implementation
287
+ }
288
+ }
289
+ })
290
+ )
291
+ ];
292
+ }
293
+ ```
294
+
295
+ ### 4. `createAPIs()` - Custom Endpoints
296
+
297
+ **Purpose**: Create custom API endpoints for external system interactions.
298
+
299
+ **Use Cases**:
300
+ - Query external service status
301
+ - Trigger external operations
302
+ - Proxy requests to external APIs
303
+ - Implement custom business logic that doesn't fit interactions
304
+
305
+ **Return Value**: Object mapping API names to API definitions
306
+
307
+ **Example - Status Query API**:
308
+ ```typescript
309
+ createAPIs(): APIs {
310
+ const apis: APIs = {};
311
+
312
+ apis.queryTaskStatus = createAPI(
313
+ async function(this: Controller, context, params: { taskId: string, taskType: string }) {
314
+ const { taskId, taskType } = params;
315
+
316
+ // Query internal state
317
+ const task = await this.system.storage.findOne(
318
+ taskType,
319
+ MatchExp.atom({ key: 'id', value: ['=', taskId] }),
320
+ undefined,
321
+ ['id', 'executionId', 'status', 'result']
322
+ );
323
+
324
+ if (!task) {
325
+ return { error: 'Task not found' };
326
+ }
327
+
328
+ // Query external system
329
+ const externalStatus = await queryExternalAPI(task.executionId);
330
+
331
+ // Update internal state via event creation
332
+ if (externalStatus.status !== task.status) {
333
+ await this.system.storage.create('TaskEvent', {
334
+ taskId,
335
+ eventType: 'statusUpdated',
336
+ status: externalStatus.status,
337
+ result: externalStatus.result,
338
+ taskType
339
+ });
340
+ }
341
+
342
+ return {
343
+ success: true,
344
+ taskId,
345
+ status: externalStatus.status,
346
+ result: externalStatus.result
347
+ };
348
+ },
349
+ {
350
+ params: { taskId: 'string', taskType: 'string' },
351
+ useNamedParams: true,
352
+ allowAnonymous: false
353
+ }
354
+ );
355
+
356
+ return apis;
357
+ }
358
+ ```
359
+
360
+ ## Integration Patterns
361
+
362
+ ### Pattern 1: External Service Synchronization (Redis, MQ)
363
+
364
+ **Characteristics**:
365
+ - Bidirectional data flow
366
+ - Real-time synchronization
367
+ - Connection management
368
+
369
+ **Implementation**:
370
+ - `setup()`: Establish connections, register listeners
371
+ - `createSideEffects()`: Push internal changes to external system
372
+ - External events → Create records in reactive system
373
+
374
+ **Example**: RedisChannelIntegration
375
+
376
+ ### Pattern 2: External Resource Generation (URLs, Tokens)
377
+
378
+ **Characteristics**:
379
+ - One-way data flow (internal → external)
380
+ - Lazy evaluation
381
+ - Resource lifecycle management
382
+
383
+ **Implementation**:
384
+ - `configure()`: Inject Custom computation into properties
385
+ - `getInitialValue()`: Call external API to generate resource
386
+ - `incrementalCompute()`: Usually skip (resources are immutable)
387
+
388
+ **Example**: VolcStreamIntegration
389
+
390
+ ### Pattern 3: Async Task Management
391
+
392
+ **Characteristics**:
393
+ - Async operation lifecycle
394
+ - Status polling
395
+ - Result synchronization
396
+
397
+ **Implementation**:
398
+ - `configure()`: Inject event entity, configure state machines
399
+ - `createAPIs()`: Provide status query endpoint
400
+ - Event-driven state updates via injected entity
401
+
402
+ **Example**: VolcPicGenIntegration
403
+
404
+ ## Best Practices
405
+
406
+ ### 1. Separation of Concerns
407
+
408
+ - **configure()**: Schema modifications only
409
+ - **setup()**: Connection establishment only
410
+ - **createSideEffects()**: Data synchronization only
411
+ - **createAPIs()**: Query/command operations only
412
+
413
+ ### 2. Error Handling
414
+
415
+ ```typescript
416
+ async setup(controller: Controller) {
417
+ try {
418
+ this.client = await connectToService();
419
+ this.client.on('error', (err) => {
420
+ console.error('Service error:', err);
421
+ // Implement reconnection logic
422
+ });
423
+ } catch (error) {
424
+ console.error('Failed to connect:', error);
425
+ throw error; // Fail fast if critical
426
+ }
427
+ }
428
+ ```
429
+
430
+ ### 3. Event-Driven State Updates
431
+
432
+ **❌ BAD - Direct State Mutation**:
433
+ ```typescript
434
+ // Don't directly update entity properties
435
+ await this.system.storage.update(taskType, taskId, {
436
+ status: newStatus // This bypasses reactive system
437
+ });
438
+ ```
439
+
440
+ **✅ GOOD - Event-Driven Updates**:
441
+ ```typescript
442
+ // Create event records to trigger state machines
443
+ await this.system.storage.create('TaskEvent', {
444
+ taskId,
445
+ eventType: 'statusUpdated',
446
+ status: newStatus,
447
+ taskType
448
+ });
449
+ // State machine will reactively update the target entity
450
+ ```
451
+
452
+ ### 4. Resource Cleanup
453
+
454
+ ```typescript
455
+ async cleanup() {
456
+ if (this.subscriber?.isOpen) {
457
+ await this.subscriber.disconnect();
458
+ }
459
+ if (this.publisher?.isOpen) {
460
+ await this.publisher.disconnect();
461
+ }
462
+ }
463
+ ```
464
+
465
+ ### 5. Environment Configuration
466
+
467
+ ```typescript
468
+ async setup(controller: Controller) {
469
+ const apiKey = process.env.EXTERNAL_API_KEY;
470
+ if (!apiKey) {
471
+ throw new Error('EXTERNAL_API_KEY must be set');
472
+ }
473
+ // ... use apiKey
474
+ }
475
+ ```
476
+
477
+ ### 6. Type Safety with External APIs
478
+
479
+ ```typescript
480
+ // Define external API types
481
+ export type ExternalTaskStatus = 'pending' | 'processing' | 'success' | 'failed';
482
+
483
+ // Map to internal types
484
+ function mapExternalStatus(external: string): TaskStatus {
485
+ switch (external) {
486
+ case 'in_queue': return 'pending';
487
+ case 'generating': return 'processing';
488
+ case 'done': return 'success';
489
+ default: return 'failed';
490
+ }
491
+ }
492
+ ```
493
+
494
+ ## Integration Registration
495
+
496
+ After creating an integration, register it in `integrations/index.ts`:
497
+
498
+ ```typescript
499
+ import { MyIntegration } from './myintegration';
500
+
501
+ const AggregatedIntegrationClass = createAggregatedIntegration([
502
+ RedisChannelIntegration,
503
+ VolcStreamIntegration,
504
+ VolcPicGenIntegration,
505
+ MyIntegration // Add your integration
506
+ ]);
507
+
508
+ export default AggregatedIntegrationClass;
509
+ ```
510
+
511
+ ## Testing Integrations
512
+
513
+ 1. **Unit Tests**: Test integration logic in isolation
514
+ 2. **Integration Tests**: Test with mock external services
515
+ 3. **E2E Tests**: Test with real external services in staging
516
+
517
+ ```typescript
518
+ // Mock external service for testing
519
+ class MockExternalService {
520
+ async query(id: string) {
521
+ return { status: 'success', result: { data: 'test' } };
522
+ }
523
+ }
524
+
525
+ // Use in tests
526
+ const integration = new MyIntegration(args, {
527
+ externalService: new MockExternalService()
528
+ });
529
+ ```
530
+
531
+ ## Common Pitfalls
532
+
533
+ 1. **❌ Modifying schema in setup()**: Schema must be finalized before Controller initialization
534
+ 2. **❌ Blocking operations in configure()**: Avoid heavy I/O, keep it fast
535
+ 3. **❌ Forgetting error handlers**: Always handle connection errors
536
+ 4. **❌ Direct state mutations**: Use event-driven updates instead
537
+ 5. **❌ Ignoring cleanup**: Implement proper resource cleanup
538
+ 6. **❌ Hardcoding configuration**: Use environment variables
539
+
540
+ ## Summary
541
+
542
+ Integrations enable reactive systems to communicate with external imperative systems through four key mechanisms:
543
+
544
+ 1. **configure()**: Augment reactive schema
545
+ 2. **setup()**: Initialize external connections
546
+ 3. **createSideEffects()**: Synchronize data to external systems
547
+ 4. **createAPIs()**: Expose custom endpoints
548
+
549
+ Choose the appropriate pattern based on your integration needs, and always prioritize event-driven design for state synchronization.
550
+
@@ -0,0 +1,19 @@
1
+ 我们的项目所使用的框架是一个叫做 Interaqt 的后端响应式数据框架,它会自动根据应用中的数据变化及响应式数据的定义执行相应的数据变化。它只负责处理一般的业务逻辑中表达的数据逻辑,例如一般业务逻辑中会用到 平均/综合 等计算,还有常见的基于状态机等业务逻辑表达等。对于一些非一般的能力需求,例如 大模型生图、大模型生视频、tts、发送邮件、发送信息、完整支付系统等。它需要借助外部系统/api 来完成。
2
+ 我们设计了一个叫做 integration 的概念,专门用来对接当前系统和外部的api/系统。它通过 interaqt 框架提供的数据观察机制,来观察数据变化,根据数据变化来决定如何调用外部的 api。同时通过 webhook 等机制来接受外部的事件,将外部事件同步回系统中,触发响应式的业务逻辑。
3
+
4
+ 我们将 integration 需要集成的功能分成了三种类别:
5
+ 1. 调用外部的 api,为了获得一个具体的返回。例如 tts,大模型生图等。
6
+ 2. 执行某种副作用,例如发送邮件、发送 im 消息等。
7
+ 3. 对接其他有状态的系统,例如支付系统等。
8
+
9
+ 现在我们需要指导 claude code 的 sub agent 合理地识别需要的外部服务以及如何自己实现 integration。
10
+ 1. 指导 `.claude/agents/requirements-analysis-handler.md` 在需求分析阶段,正确分析出 integration 的类型。并在相应的输出的文档中,设计一个字段来表达 integration 的类型。
11
+ 2. 指导 `.claude/agents/implement-design-handler.md` 在设计数据的时候,根据如下原则进行设计:
12
+ 2.1. 不管是哪种类型,都会涉及到对外部 api 的调用,例如执行副作用,也会有副作用 api 的调用。所以我们应该对每一个 api 的调用都设计一个 `{xxx}APICall` 的 entity,它负责记录这次 api 调用的参数、状态、返回值、调用时间等。
13
+ 2.2. 同时设计一个相应的 integraion event entity,当我们通过 webhook 或者自己通过接口查询到 api 调用状态的变化时,在系统内创建相应的 api call result event 事件。并且将上一步创建的 `{xxx}APICall` entity 的 status 和 data 字段写成基于 integration event entity 的 computation,这样就完整符合了框架的响应式范式。也记录了所有应该记录的数据,增强了系统的健壮性。
14
+ 2.3. 如果当前场景是希望基于这个 integration 获得具体的返回值,那么意味着我们系统内的业务数据对这个 `{xxx}APICall` 的 entity 是有依赖的,应该写成基于 `{xxx}APICall` 的 computation。例如我们的有一个 `Greeting` entity,其中有个 `voiceUrl` property 是需要利用外部 tts 能力将文本转化为语音。那么 `Greeting.voiceUrl` 就应该表达为基于 `{ttsAPICall}` entity 的 computation。如果是纯副作用类型等的调用,就不需要了。注意,这种情况下,还需要建立相应的 entity 和 api call entity 之间的关系,才能查找到正确的数据。
15
+ 2.4. `.claude/agents/implement-design-handler.md` 在做 data-design 的时候,应该明确表达出来:1. 设计的哪些实体是 api call 类型的 entity,哪些实体是 api call result event 实体。2. 系统内的业务数据如果需要 api 的返回结果,那么应该依赖正确的 api call entity。
16
+ 3. 指导 `.claude/agents/code-generation-handler.md` 在实现阶段,在写测试用例时,完全可以通过创建正确的 api call result event 来模拟 api 的调用,完整验证系统的内部逻辑的正确性。不需要等到 integration 的真实实现。
17
+ 4. 指导 `.claude/agents/error-check-handler.md` 在合适的阶段对 integration 相关的设计做错误检查。
18
+
19
+ 你充分理解上面的所有思路,并且修改相应的 sub agent 文件来达成目标。
@@ -29,6 +29,6 @@
29
29
  6. 最终产出的 interactions-design.json 仍然使用原文档里的 interaction 数据结构,但以流程为组来组织。
30
30
 
31
31
  注意,在每一个步骤中,都要给出清晰的数据结构定义,用 json 来写。
32
- 整体用简洁的英语完成文档重写。注意原本文档中的在关键步骤 update STATUS.json 仍然按照原文档的方式写。
32
+ 整体用简洁的英语完成文档重写。注意原本文档中的在关键步骤 update docs/{module}.status.json 仍然按照原文档的方式写。
33
33
 
34
34