@loopstack/meeting-notes-example-workflow 0.15.0 → 0.18.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.
- package/.turbo/turbo-build.log +4 -0
- package/CHANGELOG.md +28 -0
- package/README.md +240 -1
- package/dist/documents/meeting-notes-document.d.ts +1 -5
- package/dist/documents/meeting-notes-document.js +5 -5
- package/dist/documents/meeting-notes-document.js.map +1 -1
- package/dist/documents/meeting-notes-document.yaml +1 -1
- package/dist/documents/optimized-notes-document.d.ts +4 -16
- package/dist/documents/optimized-notes-document.js +5 -5
- package/dist/documents/optimized-notes-document.js.map +1 -1
- package/dist/documents/optimized-notes-document.yaml +1 -1
- package/dist/meeting-notes-example.module.js +4 -10
- package/dist/meeting-notes-example.module.js.map +1 -1
- package/dist/meeting-notes.workflow.d.ts +2 -2
- package/dist/meeting-notes.workflow.js +7 -5
- package/dist/meeting-notes.workflow.js.map +1 -1
- package/dist/meeting-notes.workflow.yaml +4 -4
- package/nest-cli.json +10 -0
- package/package.json +19 -46
- package/src/__tests__/meeting-notes.workflow.spec.ts +260 -0
- package/src/documents/meeting-notes-document.ts +15 -0
- package/src/documents/meeting-notes-document.yaml +13 -0
- package/src/documents/optimized-notes-document.ts +19 -0
- package/src/documents/optimized-notes-document.yaml +36 -0
- package/src/index.ts +2 -0
- package/src/meeting-notes-example.module.ts +14 -0
- package/src/meeting-notes.workflow.ts +32 -0
- package/src/meeting-notes.workflow.yaml +79 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.json +21 -0
- package/LICENSE +0 -19
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# @loopstack/meeting-notes-example-workflow
|
|
2
|
+
|
|
3
|
+
## 0.18.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#13](https://github.com/loopstack-ai/loopstack/pull/13) [`13ab1a7`](https://github.com/loopstack-ai/loopstack/commit/13ab1a792a12bc46e0a21cf1ac038a7e69c566df) Thanks [@jakobklippel](https://github.com/jakobklippel)! - Added from external repository
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [[`eea1b9f`](https://github.com/loopstack-ai/loopstack/commit/eea1b9ff10eaf8eda2a1bd4c2042148f964d5a39), [`e556176`](https://github.com/loopstack-ai/loopstack/commit/e5561769b365218f1ffdc890b887e7b607d06101), [`4ee0af1`](https://github.com/loopstack-ai/loopstack/commit/4ee0af1536e7802eb9d69a788c10184e3c5a7a11), [`3fd1db5`](https://github.com/loopstack-ai/loopstack/commit/3fd1db5d0de8ad26e3e22348f7f1593024a74273)]:
|
|
12
|
+
- @loopstack/ai-module@0.18.0
|
|
13
|
+
- @loopstack/core-ui-module@0.18.0
|
|
14
|
+
- @loopstack/core@0.18.0
|
|
15
|
+
- @loopstack/common@0.18.0
|
|
16
|
+
|
|
17
|
+
## 0.18.0-rc.0
|
|
18
|
+
|
|
19
|
+
### Minor Changes
|
|
20
|
+
|
|
21
|
+
- [#13](https://github.com/loopstack-ai/loopstack/pull/13) [`13ab1a7`](https://github.com/loopstack-ai/loopstack/commit/13ab1a792a12bc46e0a21cf1ac038a7e69c566df) Thanks [@jakobklippel](https://github.com/jakobklippel)! - Added from external repository
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- Updated dependencies [[`eea1b9f`](https://github.com/loopstack-ai/loopstack/commit/eea1b9ff10eaf8eda2a1bd4c2042148f964d5a39)]:
|
|
26
|
+
- @loopstack/ai-module@0.18.0-rc.0
|
|
27
|
+
- @loopstack/common@0.18.0-rc.1
|
|
28
|
+
- @loopstack/core@0.18.0-rc.1
|
package/README.md
CHANGED
|
@@ -1,2 +1,241 @@
|
|
|
1
|
+
# @loopstack/meeting-notes-example-workflow
|
|
1
2
|
|
|
2
|
-
|
|
3
|
+
> A module for the [Loopstack AI](https://loopstack.ai) automation framework.
|
|
4
|
+
|
|
5
|
+
This module provides an example workflow demonstrating how to build human-in-the-loop AI workflows with manual triggers and interactive documents.
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
The Meeting Notes Example Workflow shows how to create workflows that pause for user input and allow users to review and edit AI-generated content. It takes unstructured meeting notes and uses AI to extract structured information like date, participants, decisions, and action items.
|
|
10
|
+
|
|
11
|
+
By using this workflow as a reference, you'll learn how to:
|
|
12
|
+
|
|
13
|
+
- Use manual triggers to pause workflows for user input
|
|
14
|
+
- Create interactive documents with action buttons
|
|
15
|
+
- Handle transition payloads from user interactions
|
|
16
|
+
- Transform unstructured text into structured data with AI
|
|
17
|
+
- Build review-and-confirm patterns for AI outputs
|
|
18
|
+
|
|
19
|
+
This example is essential for developers building workflows that require human oversight or approval steps.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
### Prerequisites
|
|
24
|
+
|
|
25
|
+
Create a new Loopstack project if you haven't already:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx create-loopstack-app my-project
|
|
29
|
+
cd my-project
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Start Environment
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
cd my-project
|
|
36
|
+
docker compose up -d
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Add the Module
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
loopstack add @loopstack/meeting-notes-example-workflow
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
This copies the source files into your `src` directory.
|
|
46
|
+
|
|
47
|
+
> Using the `loopstack add` command is a great way to explore the code to learn new concepts or add own customizations.
|
|
48
|
+
|
|
49
|
+
## Setup
|
|
50
|
+
|
|
51
|
+
### 1. Import the Module
|
|
52
|
+
|
|
53
|
+
Add `MeetingNotesExampleModule` to your `default.module.ts` (included in the skeleton app) or to your own module:
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { Module } from '@nestjs/common';
|
|
57
|
+
import { AiModule } from '@loopstack/ai-module';
|
|
58
|
+
import { LoopCoreModule } from '@loopstack/core';
|
|
59
|
+
import { CoreUiModule } from '@loopstack/core-ui-module';
|
|
60
|
+
import { MeetingNotesExampleModule } from './@loopstack/meeting-notes-example-workflow';
|
|
61
|
+
import { DefaultWorkspace } from './default.workspace';
|
|
62
|
+
|
|
63
|
+
@Module({
|
|
64
|
+
imports: [LoopCoreModule, MeetingNotesExampleModule],
|
|
65
|
+
providers: [DefaultWorkspace],
|
|
66
|
+
})
|
|
67
|
+
export class DefaultModule {}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 2. Register in Your Workspace
|
|
71
|
+
|
|
72
|
+
Add the workflow to your workspace class using the `@Workflow()` decorator:
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
import { Injectable } from '@nestjs/common';
|
|
76
|
+
import { BlockConfig, Workflow } from '@loopstack/common';
|
|
77
|
+
import { WorkspaceBase } from '@loopstack/core';
|
|
78
|
+
import { MeetingNotesWorkflow } from './@loopstack/meeting-notes-example-workflow';
|
|
79
|
+
|
|
80
|
+
@Injectable()
|
|
81
|
+
@BlockConfig({
|
|
82
|
+
config: {
|
|
83
|
+
title: 'My Workspace',
|
|
84
|
+
description: 'A workspace with the meeting notes example workflow',
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
export class MyWorkspace extends WorkspaceBase {
|
|
88
|
+
@Workflow() meetingNotesWorkflow: MeetingNotesWorkflow;
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 3. Configure API Key
|
|
93
|
+
|
|
94
|
+
Set your OpenAI API key as an environment variable:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
OPENAI_API_KEY=sk-...
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## How It Works
|
|
101
|
+
|
|
102
|
+
### Workflow Flow
|
|
103
|
+
|
|
104
|
+
1. **Start** - User provides unstructured meeting notes
|
|
105
|
+
2. **Wait for Input** - User can edit the notes, then clicks "Optimize Notes"
|
|
106
|
+
3. **AI Processing** - LLM extracts structured information into a formatted document
|
|
107
|
+
4. **Review** - User reviews and can edit the structured output
|
|
108
|
+
5. **Confirm** - User clicks "Confirm" to finalize
|
|
109
|
+
|
|
110
|
+
### Key Concepts
|
|
111
|
+
|
|
112
|
+
#### 1. Manual Triggers
|
|
113
|
+
|
|
114
|
+
Use `trigger: manual` to pause the workflow and wait for user interaction:
|
|
115
|
+
|
|
116
|
+
```yaml
|
|
117
|
+
- id: user_response
|
|
118
|
+
from: waiting_for_response
|
|
119
|
+
to: response_received
|
|
120
|
+
trigger: manual
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The workflow pauses at `waiting_for_response` until the user triggers the transition.
|
|
124
|
+
|
|
125
|
+
#### 2. Document Actions with Buttons
|
|
126
|
+
|
|
127
|
+
Add action buttons to documents that trigger transitions:
|
|
128
|
+
|
|
129
|
+
```yaml
|
|
130
|
+
ui:
|
|
131
|
+
form:
|
|
132
|
+
properties:
|
|
133
|
+
text:
|
|
134
|
+
title: Text
|
|
135
|
+
widget: textarea
|
|
136
|
+
actions:
|
|
137
|
+
- type: button
|
|
138
|
+
widget: button
|
|
139
|
+
transition: user_response
|
|
140
|
+
options:
|
|
141
|
+
label: 'Optimize Notes'
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
When clicked, the button triggers the `user_response` transition with the current document content.
|
|
145
|
+
|
|
146
|
+
#### 3. Transition Payloads
|
|
147
|
+
|
|
148
|
+
Access user input from the transition payload:
|
|
149
|
+
|
|
150
|
+
```yaml
|
|
151
|
+
- id: user_response
|
|
152
|
+
trigger: manual
|
|
153
|
+
call:
|
|
154
|
+
- tool: createDocument
|
|
155
|
+
args:
|
|
156
|
+
update:
|
|
157
|
+
content: ${ transition.payload }
|
|
158
|
+
assign:
|
|
159
|
+
meetingNotes: ${ result.data.content }
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The `transition.payload` contains the document content when the user clicked the button.
|
|
163
|
+
|
|
164
|
+
#### 4. Structured Output Documents
|
|
165
|
+
|
|
166
|
+
Define complex document schemas for structured AI output:
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
export const OptimizedMeetingNotesDocumentSchema = z.object({
|
|
170
|
+
date: z.string(),
|
|
171
|
+
summary: z.string(),
|
|
172
|
+
participants: z.array(z.string()),
|
|
173
|
+
decisions: z.array(z.string()),
|
|
174
|
+
actionItems: z.array(z.string()),
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
#### 5. Array Fields with Collapsible UI
|
|
179
|
+
|
|
180
|
+
Configure array fields with custom item titles and collapsed display:
|
|
181
|
+
|
|
182
|
+
```yaml
|
|
183
|
+
ui:
|
|
184
|
+
form:
|
|
185
|
+
properties:
|
|
186
|
+
participants:
|
|
187
|
+
title: Participants
|
|
188
|
+
collapsed: true
|
|
189
|
+
items:
|
|
190
|
+
title: Participant
|
|
191
|
+
actionItems:
|
|
192
|
+
title: Action Items
|
|
193
|
+
collapsed: true
|
|
194
|
+
items:
|
|
195
|
+
title: Action Item
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
#### 6. AI Document Generation
|
|
199
|
+
|
|
200
|
+
Use `aiGenerateDocument` to populate a structured document:
|
|
201
|
+
|
|
202
|
+
```yaml
|
|
203
|
+
- tool: aiGenerateDocument
|
|
204
|
+
args:
|
|
205
|
+
llm:
|
|
206
|
+
provider: openai
|
|
207
|
+
model: gpt-4o
|
|
208
|
+
response:
|
|
209
|
+
id: final
|
|
210
|
+
document: optimizedNotesDocument
|
|
211
|
+
prompt: |
|
|
212
|
+
Extract all information from the provided meeting notes into the structured document.
|
|
213
|
+
|
|
214
|
+
<Meeting Notes>
|
|
215
|
+
{{ meetingNotes.text }}
|
|
216
|
+
</Meeting Notes>
|
|
217
|
+
assign:
|
|
218
|
+
optimizedNotes: ${ result.data.content }
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
The LLM automatically fills in the document fields based on the schema.
|
|
222
|
+
|
|
223
|
+
## Dependencies
|
|
224
|
+
|
|
225
|
+
This workflow uses the following Loopstack modules:
|
|
226
|
+
|
|
227
|
+
- `@loopstack/core` - Core framework functionality
|
|
228
|
+
- `@loopstack/core-ui-module` - Provides `CreateDocument` tool
|
|
229
|
+
- `@loopstack/ai-module` - Provides `AiGenerateDocument` tool
|
|
230
|
+
|
|
231
|
+
## About
|
|
232
|
+
|
|
233
|
+
Author: [Jakob Klippel](https://www.linkedin.com/in/jakob-klippel/)
|
|
234
|
+
|
|
235
|
+
License: Apache-2.0
|
|
236
|
+
|
|
237
|
+
### Additional Resources
|
|
238
|
+
|
|
239
|
+
- [Loopstack Documentation](https://loopstack.ai/docs)
|
|
240
|
+
- [Getting Started with Loopstack](https://loopstack.ai/docs/getting-started)
|
|
241
|
+
- Find more Loopstack examples in the [Loopstack Registry](https://loopstack.ai/registry)
|
|
@@ -2,10 +2,6 @@ import { z } from 'zod';
|
|
|
2
2
|
import { DocumentBase } from '@loopstack/core';
|
|
3
3
|
export declare const MeetingNotesDocumentSchema: z.ZodObject<{
|
|
4
4
|
text: z.ZodString;
|
|
5
|
-
},
|
|
6
|
-
text?: string;
|
|
7
|
-
}, {
|
|
8
|
-
text?: string;
|
|
9
|
-
}>;
|
|
5
|
+
}, z.core.$strip>;
|
|
10
6
|
export declare class MeetingNotesDocument extends DocumentBase {
|
|
11
7
|
}
|
|
@@ -7,10 +7,10 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
};
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
9
|
exports.MeetingNotesDocument = exports.MeetingNotesDocumentSchema = void 0;
|
|
10
|
-
const common_1 = require("@
|
|
10
|
+
const common_1 = require("@nestjs/common");
|
|
11
11
|
const zod_1 = require("zod");
|
|
12
|
+
const common_2 = require("@loopstack/common");
|
|
12
13
|
const core_1 = require("@loopstack/core");
|
|
13
|
-
const common_2 = require("@nestjs/common");
|
|
14
14
|
exports.MeetingNotesDocumentSchema = zod_1.z.object({
|
|
15
15
|
text: zod_1.z.string(),
|
|
16
16
|
});
|
|
@@ -18,10 +18,10 @@ let MeetingNotesDocument = class MeetingNotesDocument extends core_1.DocumentBas
|
|
|
18
18
|
};
|
|
19
19
|
exports.MeetingNotesDocument = MeetingNotesDocument;
|
|
20
20
|
exports.MeetingNotesDocument = MeetingNotesDocument = __decorate([
|
|
21
|
-
(0,
|
|
22
|
-
(0,
|
|
21
|
+
(0, common_1.Injectable)(),
|
|
22
|
+
(0, common_2.BlockConfig)({
|
|
23
23
|
configFile: __dirname + '/meeting-notes-document.yaml',
|
|
24
24
|
}),
|
|
25
|
-
(0,
|
|
25
|
+
(0, common_2.WithArguments)(exports.MeetingNotesDocumentSchema)
|
|
26
26
|
], MeetingNotesDocument);
|
|
27
27
|
//# sourceMappingURL=meeting-notes-document.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"meeting-notes-document.js","sourceRoot":"","sources":["../../src/documents/meeting-notes-document.ts"],"names":[],"mappings":";;;;;;;;;AAAA,8CAA+D;AAC/D,
|
|
1
|
+
{"version":3,"file":"meeting-notes-document.js","sourceRoot":"","sources":["../../src/documents/meeting-notes-document.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAA4C;AAC5C,6BAAwB;AACxB,8CAA+D;AAC/D,0CAA+C;AAElC,QAAA,0BAA0B,GAAG,OAAC,CAAC,MAAM,CAAC;IACjD,IAAI,EAAE,OAAC,CAAC,MAAM,EAAE;CACjB,CAAC,CAAC;AAOI,IAAM,oBAAoB,GAA1B,MAAM,oBAAqB,SAAQ,mBAAY;CAAG,CAAA;AAA5C,oDAAoB;+BAApB,oBAAoB;IALhC,IAAA,mBAAU,GAAE;IACZ,IAAA,oBAAW,EAAC;QACX,UAAU,EAAE,SAAS,GAAG,8BAA8B;KACvD,CAAC;IACD,IAAA,sBAAa,EAAC,kCAA0B,CAAC;GAC7B,oBAAoB,CAAwB"}
|
|
@@ -3,21 +3,9 @@ import { DocumentBase } from '@loopstack/core';
|
|
|
3
3
|
export declare const OptimizedMeetingNotesDocumentSchema: z.ZodObject<{
|
|
4
4
|
date: z.ZodString;
|
|
5
5
|
summary: z.ZodString;
|
|
6
|
-
participants: z.ZodArray<z.ZodString
|
|
7
|
-
decisions: z.ZodArray<z.ZodString
|
|
8
|
-
actionItems: z.ZodArray<z.ZodString
|
|
9
|
-
},
|
|
10
|
-
date?: string;
|
|
11
|
-
summary?: string;
|
|
12
|
-
participants?: string[];
|
|
13
|
-
decisions?: string[];
|
|
14
|
-
actionItems?: string[];
|
|
15
|
-
}, {
|
|
16
|
-
date?: string;
|
|
17
|
-
summary?: string;
|
|
18
|
-
participants?: string[];
|
|
19
|
-
decisions?: string[];
|
|
20
|
-
actionItems?: string[];
|
|
21
|
-
}>;
|
|
6
|
+
participants: z.ZodArray<z.ZodString>;
|
|
7
|
+
decisions: z.ZodArray<z.ZodString>;
|
|
8
|
+
actionItems: z.ZodArray<z.ZodString>;
|
|
9
|
+
}, z.core.$strip>;
|
|
22
10
|
export declare class OptimizedNotesDocument extends DocumentBase {
|
|
23
11
|
}
|
|
@@ -7,10 +7,10 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
};
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
9
|
exports.OptimizedNotesDocument = exports.OptimizedMeetingNotesDocumentSchema = void 0;
|
|
10
|
-
const common_1 = require("@
|
|
10
|
+
const common_1 = require("@nestjs/common");
|
|
11
11
|
const zod_1 = require("zod");
|
|
12
|
+
const common_2 = require("@loopstack/common");
|
|
12
13
|
const core_1 = require("@loopstack/core");
|
|
13
|
-
const common_2 = require("@nestjs/common");
|
|
14
14
|
exports.OptimizedMeetingNotesDocumentSchema = zod_1.z.object({
|
|
15
15
|
date: zod_1.z.string(),
|
|
16
16
|
summary: zod_1.z.string(),
|
|
@@ -22,10 +22,10 @@ let OptimizedNotesDocument = class OptimizedNotesDocument extends core_1.Documen
|
|
|
22
22
|
};
|
|
23
23
|
exports.OptimizedNotesDocument = OptimizedNotesDocument;
|
|
24
24
|
exports.OptimizedNotesDocument = OptimizedNotesDocument = __decorate([
|
|
25
|
-
(0,
|
|
26
|
-
(0,
|
|
25
|
+
(0, common_1.Injectable)(),
|
|
26
|
+
(0, common_2.BlockConfig)({
|
|
27
27
|
configFile: __dirname + '/optimized-notes-document.yaml',
|
|
28
28
|
}),
|
|
29
|
-
(0,
|
|
29
|
+
(0, common_2.WithArguments)(exports.OptimizedMeetingNotesDocumentSchema)
|
|
30
30
|
], OptimizedNotesDocument);
|
|
31
31
|
//# sourceMappingURL=optimized-notes-document.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"optimized-notes-document.js","sourceRoot":"","sources":["../../src/documents/optimized-notes-document.ts"],"names":[],"mappings":";;;;;;;;;AAAA,8CAA+D;AAC/D,
|
|
1
|
+
{"version":3,"file":"optimized-notes-document.js","sourceRoot":"","sources":["../../src/documents/optimized-notes-document.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAA4C;AAC5C,6BAAwB;AACxB,8CAA+D;AAC/D,0CAA+C;AAElC,QAAA,mCAAmC,GAAG,OAAC,CAAC,MAAM,CAAC;IAC1D,IAAI,EAAE,OAAC,CAAC,MAAM,EAAE;IAChB,OAAO,EAAE,OAAC,CAAC,MAAM,EAAE;IACnB,YAAY,EAAE,OAAC,CAAC,KAAK,CAAC,OAAC,CAAC,MAAM,EAAE,CAAC;IACjC,SAAS,EAAE,OAAC,CAAC,KAAK,CAAC,OAAC,CAAC,MAAM,EAAE,CAAC;IAC9B,WAAW,EAAE,OAAC,CAAC,KAAK,CAAC,OAAC,CAAC,MAAM,EAAE,CAAC;CACjC,CAAC,CAAC;AAOI,IAAM,sBAAsB,GAA5B,MAAM,sBAAuB,SAAQ,mBAAY;CAAG,CAAA;AAA9C,wDAAsB;iCAAtB,sBAAsB;IALlC,IAAA,mBAAU,GAAE;IACZ,IAAA,oBAAW,EAAC;QACX,UAAU,EAAE,SAAS,GAAG,gCAAgC;KACzD,CAAC;IACD,IAAA,sBAAa,EAAC,2CAAmC,CAAC;GACtC,sBAAsB,CAAwB"}
|
|
@@ -8,26 +8,20 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
9
|
exports.MeetingNotesExampleModule = void 0;
|
|
10
10
|
const common_1 = require("@nestjs/common");
|
|
11
|
+
const ai_module_1 = require("@loopstack/ai-module");
|
|
11
12
|
const core_1 = require("@loopstack/core");
|
|
12
13
|
const core_ui_module_1 = require("@loopstack/core-ui-module");
|
|
13
|
-
const ai_module_1 = require("@loopstack/ai-module");
|
|
14
|
-
const meeting_notes_workflow_1 = require("./meeting-notes.workflow");
|
|
15
14
|
const meeting_notes_document_1 = require("./documents/meeting-notes-document");
|
|
16
15
|
const optimized_notes_document_1 = require("./documents/optimized-notes-document");
|
|
16
|
+
const meeting_notes_workflow_1 = require("./meeting-notes.workflow");
|
|
17
17
|
let MeetingNotesExampleModule = class MeetingNotesExampleModule {
|
|
18
18
|
};
|
|
19
19
|
exports.MeetingNotesExampleModule = MeetingNotesExampleModule;
|
|
20
20
|
exports.MeetingNotesExampleModule = MeetingNotesExampleModule = __decorate([
|
|
21
21
|
(0, common_1.Module)({
|
|
22
22
|
imports: [core_1.LoopCoreModule, core_ui_module_1.CoreUiModule, ai_module_1.AiModule],
|
|
23
|
-
providers: [
|
|
24
|
-
|
|
25
|
-
meeting_notes_document_1.MeetingNotesDocument,
|
|
26
|
-
optimized_notes_document_1.OptimizedNotesDocument,
|
|
27
|
-
],
|
|
28
|
-
exports: [
|
|
29
|
-
meeting_notes_workflow_1.MeetingNotesWorkflow,
|
|
30
|
-
]
|
|
23
|
+
providers: [meeting_notes_workflow_1.MeetingNotesWorkflow, meeting_notes_document_1.MeetingNotesDocument, optimized_notes_document_1.OptimizedNotesDocument],
|
|
24
|
+
exports: [meeting_notes_workflow_1.MeetingNotesWorkflow],
|
|
31
25
|
})
|
|
32
26
|
], MeetingNotesExampleModule);
|
|
33
27
|
//# sourceMappingURL=meeting-notes-example.module.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"meeting-notes-example.module.js","sourceRoot":"","sources":["../src/meeting-notes-example.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,0CAAiD;AACjD,8DAAyD;AACzD
|
|
1
|
+
{"version":3,"file":"meeting-notes-example.module.js","sourceRoot":"","sources":["../src/meeting-notes-example.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,oDAAgD;AAChD,0CAAiD;AACjD,8DAAyD;AACzD,+EAA0E;AAC1E,mFAA8E;AAC9E,qEAAgE;AAOzD,IAAM,yBAAyB,GAA/B,MAAM,yBAAyB;CAAG,CAAA;AAA5B,8DAAyB;oCAAzB,yBAAyB;IALrC,IAAA,eAAM,EAAC;QACN,OAAO,EAAE,CAAC,qBAAc,EAAE,6BAAY,EAAE,oBAAQ,CAAC;QACjD,SAAS,EAAE,CAAC,6CAAoB,EAAE,6CAAoB,EAAE,iDAAsB,CAAC;QAC/E,OAAO,EAAE,CAAC,6CAAoB,CAAC;KAChC,CAAC;GACW,yBAAyB,CAAG"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { AiGenerateDocument } from '@loopstack/ai-module';
|
|
1
2
|
import { WorkflowBase } from '@loopstack/core';
|
|
3
|
+
import { CreateDocument } from '@loopstack/core-ui-module';
|
|
2
4
|
import { MeetingNotesDocument } from './documents/meeting-notes-document';
|
|
3
5
|
import { OptimizedNotesDocument } from './documents/optimized-notes-document';
|
|
4
|
-
import { AiGenerateDocument } from '@loopstack/ai-module';
|
|
5
|
-
import { CreateDocument } from '@loopstack/core-ui-module';
|
|
6
6
|
export declare class MeetingNotesWorkflow extends WorkflowBase {
|
|
7
7
|
aiGenerateDocument: AiGenerateDocument;
|
|
8
8
|
createDocument: CreateDocument;
|
|
@@ -10,13 +10,13 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.MeetingNotesWorkflow = void 0;
|
|
13
|
-
const core_1 = require("@loopstack/core");
|
|
14
|
-
const common_1 = require("@loopstack/common");
|
|
15
|
-
const meeting_notes_document_1 = require("./documents/meeting-notes-document");
|
|
16
|
-
const optimized_notes_document_1 = require("./documents/optimized-notes-document");
|
|
17
13
|
const zod_1 = require("zod");
|
|
18
14
|
const ai_module_1 = require("@loopstack/ai-module");
|
|
15
|
+
const common_1 = require("@loopstack/common");
|
|
16
|
+
const core_1 = require("@loopstack/core");
|
|
19
17
|
const core_ui_module_1 = require("@loopstack/core-ui-module");
|
|
18
|
+
const meeting_notes_document_1 = require("./documents/meeting-notes-document");
|
|
19
|
+
const optimized_notes_document_1 = require("./documents/optimized-notes-document");
|
|
20
20
|
let MeetingNotesWorkflow = class MeetingNotesWorkflow extends core_1.WorkflowBase {
|
|
21
21
|
aiGenerateDocument;
|
|
22
22
|
createDocument;
|
|
@@ -45,7 +45,9 @@ exports.MeetingNotesWorkflow = MeetingNotesWorkflow = __decorate([
|
|
|
45
45
|
configFile: __dirname + '/meeting-notes.workflow.yaml',
|
|
46
46
|
}),
|
|
47
47
|
(0, common_1.WithArguments)(zod_1.z.object({
|
|
48
|
-
inputText: zod_1.z
|
|
48
|
+
inputText: zod_1.z
|
|
49
|
+
.string()
|
|
50
|
+
.default('- meeting 1.1.2025\n- budget: need 2 cut costs sarah said\n- hire new person?? --> marketing\n- vendor pricing - follow up needed by anna'),
|
|
49
51
|
})),
|
|
50
52
|
(0, common_1.WithState)(zod_1.z.object({
|
|
51
53
|
meetingNotes: meeting_notes_document_1.MeetingNotesDocumentSchema.optional(),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"meeting-notes.workflow.js","sourceRoot":"","sources":["../src/meeting-notes.workflow.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,0CAA+C;AAC/C,
|
|
1
|
+
{"version":3,"file":"meeting-notes.workflow.js","sourceRoot":"","sources":["../src/meeting-notes.workflow.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,6BAAwB;AACxB,oDAA0D;AAC1D,8CAA0F;AAC1F,0CAA+C;AAC/C,8DAA2D;AAC3D,+EAAsG;AACtG,mFAAmH;AAoB5G,IAAM,oBAAoB,GAA1B,MAAM,oBAAqB,SAAQ,mBAAY;IAC5C,kBAAkB,CAAqB;IACvC,cAAc,CAAiB;IAC3B,oBAAoB,CAAuB;IAC3C,sBAAsB,CAAyB;CAC5D,CAAA;AALY,oDAAoB;AACvB;IAAP,IAAA,aAAI,GAAE;8BAAqB,8BAAkB;gEAAC;AACvC;IAAP,IAAA,aAAI,GAAE;8BAAiB,+BAAc;4DAAC;AAC3B;IAAX,IAAA,iBAAQ,GAAE;8BAAuB,6CAAoB;kEAAC;AAC3C;IAAX,IAAA,iBAAQ,GAAE;8BAAyB,iDAAsB;oEAAC;+BAJhD,oBAAoB;IAlBhC,IAAA,oBAAW,EAAC;QACX,UAAU,EAAE,SAAS,GAAG,8BAA8B;KACvD,CAAC;IACD,IAAA,sBAAa,EACZ,OAAC,CAAC,MAAM,CAAC;QACP,SAAS,EAAE,OAAC;aACT,MAAM,EAAE;aACR,OAAO,CACN,2IAA2I,CAC5I;KACJ,CAAC,CACH;IACA,IAAA,kBAAS,EACR,OAAC,CAAC,MAAM,CAAC;QACP,YAAY,EAAE,mDAA0B,CAAC,QAAQ,EAAE;QACnD,cAAc,EAAE,8DAAmC,CAAC,QAAQ,EAAE;KAC/D,CAAC,CACH;GACY,oBAAoB,CAKhC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
title:
|
|
1
|
+
title: 'Human-in-the-loop Demo (Meeting Notes Optimizer)'
|
|
2
2
|
|
|
3
|
-
description:
|
|
3
|
+
description: 'A demo workflow to demonstrate how to use AI to structure meeting notes.'
|
|
4
4
|
|
|
5
5
|
ui:
|
|
6
6
|
form:
|
|
@@ -23,7 +23,7 @@ transitions:
|
|
|
23
23
|
content:
|
|
24
24
|
text: |
|
|
25
25
|
Unstructured Notes:
|
|
26
|
-
|
|
26
|
+
|
|
27
27
|
{{ args.inputText }}
|
|
28
28
|
|
|
29
29
|
- id: user_response
|
|
@@ -76,4 +76,4 @@ transitions:
|
|
|
76
76
|
update:
|
|
77
77
|
content: ${ transition.payload }
|
|
78
78
|
assign:
|
|
79
|
-
optimizedNotes: ${ result.data.content }
|
|
79
|
+
optimizedNotes: ${ result.data.content }
|
package/nest-cli.json
ADDED
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@loopstack/meeting-notes-example-workflow",
|
|
3
3
|
"displayName": "Loopstack Meeting Notes Example Worflow",
|
|
4
4
|
"description": "A complete workflow demonstrating how to create a workflow in loopstack based on a meeting notes summary use case.",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.18.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Jakob Klippel",
|
|
8
8
|
"url": "https://www.linkedin.com/in/jakob-klippel/"
|
|
@@ -17,62 +17,35 @@
|
|
|
17
17
|
"license": "Apache-2.0",
|
|
18
18
|
"main": "dist/index.js",
|
|
19
19
|
"types": "dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": "./dist/index.js",
|
|
22
|
+
"./src/*": "./src/*"
|
|
23
|
+
},
|
|
20
24
|
"scripts": {
|
|
21
25
|
"build": "nest build",
|
|
22
|
-
"watch": "nest build --watch",
|
|
23
26
|
"compile": "tsc --noEmit",
|
|
24
|
-
"format": "prettier --write
|
|
25
|
-
"lint": "eslint
|
|
26
|
-
"test": "jest",
|
|
27
|
-
"
|
|
28
|
-
"test:cov": "jest --coverage",
|
|
29
|
-
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
|
30
|
-
"test:e2e": "jest --config ./test/jest-e2e.json"
|
|
27
|
+
"format": "prettier --write .",
|
|
28
|
+
"lint": "eslint .",
|
|
29
|
+
"test": "jest --passWithNoTests",
|
|
30
|
+
"watch": "nest build --watch"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@loopstack/ai-module": "
|
|
34
|
-
"@loopstack/common": "^0.
|
|
35
|
-
"@loopstack/core": "^0.
|
|
36
|
-
"@loopstack/core-ui-module": "^0.
|
|
37
|
-
"@nestjs/common": "^11.
|
|
38
|
-
"zod": "^3.
|
|
39
|
-
},
|
|
40
|
-
"devDependencies": {
|
|
41
|
-
"@eslint/eslintrc": "^3.3.0",
|
|
42
|
-
"@eslint/js": "^9.22.0",
|
|
43
|
-
"@nestjs/cli": "^11.0.5",
|
|
44
|
-
"@nestjs/testing": "^11.0.12",
|
|
45
|
-
"@swc/cli": "^0.6.0",
|
|
46
|
-
"@swc/core": "^1.11.11",
|
|
47
|
-
"@types/jest": "^29.5.14",
|
|
48
|
-
"@types/node": "^22.13.10",
|
|
49
|
-
"eslint": "^9.22.0",
|
|
50
|
-
"eslint-config-prettier": "^10.1.1",
|
|
51
|
-
"eslint-plugin-prettier": "^5.2.3",
|
|
52
|
-
"globals": "^16.0.0",
|
|
53
|
-
"jest": "^29.7.0",
|
|
54
|
-
"prettier": "^3.5.3",
|
|
55
|
-
"ts-jest": "^29.2.6",
|
|
56
|
-
"tsconfig-paths": "^4.2.0",
|
|
57
|
-
"typescript": "^5.8.3",
|
|
58
|
-
"typescript-eslint": "^8.38.0"
|
|
33
|
+
"@loopstack/ai-module": "^0.18.0",
|
|
34
|
+
"@loopstack/common": "^0.18.0",
|
|
35
|
+
"@loopstack/core": "^0.18.0",
|
|
36
|
+
"@loopstack/core-ui-module": "^0.18.0",
|
|
37
|
+
"@nestjs/common": "^11.1.12",
|
|
38
|
+
"zod": "^4.3.5"
|
|
59
39
|
},
|
|
60
40
|
"jest": {
|
|
61
|
-
"
|
|
62
|
-
"js",
|
|
63
|
-
"json",
|
|
64
|
-
"ts"
|
|
65
|
-
],
|
|
41
|
+
"testEnvironment": "node",
|
|
66
42
|
"rootDir": "src",
|
|
67
43
|
"testRegex": ".*\\.spec\\.ts$",
|
|
68
44
|
"transform": {
|
|
69
45
|
"^.+\\.ts$": "ts-jest"
|
|
70
46
|
},
|
|
71
|
-
"
|
|
72
|
-
|
|
73
|
-
],
|
|
74
|
-
"coverageDirectory": "../coverage",
|
|
75
|
-
"testEnvironment": "node",
|
|
47
|
+
"testTimeout": 10000,
|
|
48
|
+
"forceExit": true,
|
|
76
49
|
"maxWorkers": 1
|
|
77
50
|
}
|
|
78
|
-
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { TestingModule } from '@nestjs/testing';
|
|
2
|
+
import { AiGenerateDocument, AiModule } from '@loopstack/ai-module';
|
|
3
|
+
import { generateObjectFingerprint } from '@loopstack/common';
|
|
4
|
+
import { BlockExecutionContextDto, LoopCoreModule, WorkflowProcessorService } from '@loopstack/core';
|
|
5
|
+
import { CoreUiModule, CreateDocument } from '@loopstack/core-ui-module';
|
|
6
|
+
import { ToolMock, createWorkflowTest } from '@loopstack/testing';
|
|
7
|
+
import { MeetingNotesDocument } from '../documents/meeting-notes-document';
|
|
8
|
+
import { OptimizedNotesDocument } from '../documents/optimized-notes-document';
|
|
9
|
+
import { MeetingNotesWorkflow } from '../meeting-notes.workflow';
|
|
10
|
+
|
|
11
|
+
describe('MeetingNotesWorkflow', () => {
|
|
12
|
+
let module: TestingModule;
|
|
13
|
+
let workflow: MeetingNotesWorkflow;
|
|
14
|
+
let processor: WorkflowProcessorService;
|
|
15
|
+
|
|
16
|
+
let mockCreateDocument: ToolMock;
|
|
17
|
+
|
|
18
|
+
const mockInitialNotes = {
|
|
19
|
+
text: `
|
|
20
|
+
- meeting 1.1.2025
|
|
21
|
+
- budget: need 2 cut costs sarah said
|
|
22
|
+
- hire new person?? --> marketing
|
|
23
|
+
- vendor pricing - follow up needed by anna`,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
beforeEach(async () => {
|
|
27
|
+
module = await createWorkflowTest()
|
|
28
|
+
.forWorkflow(MeetingNotesWorkflow)
|
|
29
|
+
.withImports(LoopCoreModule, CoreUiModule, AiModule)
|
|
30
|
+
.withProvider(MeetingNotesDocument)
|
|
31
|
+
.withProvider(OptimizedNotesDocument)
|
|
32
|
+
.withToolOverride(CreateDocument)
|
|
33
|
+
.withToolOverride(AiGenerateDocument)
|
|
34
|
+
.compile();
|
|
35
|
+
|
|
36
|
+
workflow = module.get(MeetingNotesWorkflow);
|
|
37
|
+
processor = module.get(WorkflowProcessorService);
|
|
38
|
+
|
|
39
|
+
mockCreateDocument = module.get(CreateDocument);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(async () => {
|
|
43
|
+
await module.close();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('initialization', () => {
|
|
47
|
+
it('should be defined with correct tools', () => {
|
|
48
|
+
expect(workflow).toBeDefined();
|
|
49
|
+
expect(workflow.tools).toContain('createDocument');
|
|
50
|
+
expect(workflow.tools).toContain('aiGenerateDocument');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should apply default argument value', () => {
|
|
54
|
+
const result = workflow.validate({});
|
|
55
|
+
expect(result.inputText).toContain('meeting 1.1.2025');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('initial step', () => {
|
|
60
|
+
const context = new BlockExecutionContextDto({});
|
|
61
|
+
|
|
62
|
+
it('should execute initial step and stop at waiting_for_response', async () => {
|
|
63
|
+
mockCreateDocument.execute.mockResolvedValue({
|
|
64
|
+
data: { content: mockInitialNotes },
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const result = await processor.process(workflow, {}, context);
|
|
68
|
+
|
|
69
|
+
// Should execute without errors and stop at waiting_for_response (manual step)
|
|
70
|
+
expect(result.runtime.error).toBe(false);
|
|
71
|
+
expect(result.runtime.stop).toBe(true);
|
|
72
|
+
|
|
73
|
+
// Should call CreateDocument once for the initial form
|
|
74
|
+
expect(mockCreateDocument.execute).toHaveBeenCalledTimes(1);
|
|
75
|
+
expect(mockCreateDocument.execute).toHaveBeenCalledWith(
|
|
76
|
+
expect.objectContaining({
|
|
77
|
+
id: 'input',
|
|
78
|
+
update: {
|
|
79
|
+
content: {
|
|
80
|
+
text: expect.stringContaining('1.1.2025'),
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
}),
|
|
84
|
+
expect.anything(),
|
|
85
|
+
expect.anything(),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// Verify history contains expected places
|
|
89
|
+
const history = result.state.caretaker.getHistory();
|
|
90
|
+
const places = history.map((h) => h.metadata?.place);
|
|
91
|
+
expect(places).toContain('waiting_for_response');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('user response step', () => {
|
|
96
|
+
it('should process user response and generate optimized notes', async () => {
|
|
97
|
+
const mockUserEditedNotes = {
|
|
98
|
+
text: `Meeting Notes - January 1, 2025
|
|
99
|
+
- Budget discussion: need to cut costs (Sarah's input)
|
|
100
|
+
- Hiring: new person needed for marketing
|
|
101
|
+
- Vendor pricing: follow up needed by Anna`,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const mockOptimizedNotes = {
|
|
105
|
+
date: '2025-01-01',
|
|
106
|
+
summary: 'Budget and hiring discussion',
|
|
107
|
+
participants: ['Sarah', 'Anna'],
|
|
108
|
+
decisions: ['Cut costs', 'Hire for marketing'],
|
|
109
|
+
actionItems: ['Follow up on vendor pricing'],
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const args = { inputText: mockInitialNotes.text };
|
|
113
|
+
|
|
114
|
+
// Create module with existing workflow state
|
|
115
|
+
const moduleWithState = await createWorkflowTest()
|
|
116
|
+
.forWorkflow(MeetingNotesWorkflow)
|
|
117
|
+
.withImports(LoopCoreModule, CoreUiModule, AiModule)
|
|
118
|
+
.withProvider(MeetingNotesDocument)
|
|
119
|
+
.withProvider(OptimizedNotesDocument)
|
|
120
|
+
.withToolOverride(CreateDocument)
|
|
121
|
+
.withToolOverride(AiGenerateDocument)
|
|
122
|
+
.withExistingWorkflow({
|
|
123
|
+
id: '123',
|
|
124
|
+
place: 'waiting_for_response',
|
|
125
|
+
hashRecord: {
|
|
126
|
+
options: generateObjectFingerprint(args), // previously run with same arguments
|
|
127
|
+
},
|
|
128
|
+
})
|
|
129
|
+
.compile();
|
|
130
|
+
|
|
131
|
+
const workflowWithState = moduleWithState.get(MeetingNotesWorkflow);
|
|
132
|
+
const processorWithState = moduleWithState.get(WorkflowProcessorService);
|
|
133
|
+
|
|
134
|
+
const mockCreateDocumentWithState: ToolMock = moduleWithState.get(CreateDocument);
|
|
135
|
+
const mockAiGenerateDocumentWithState: ToolMock = moduleWithState.get(AiGenerateDocument);
|
|
136
|
+
|
|
137
|
+
mockCreateDocumentWithState.execute.mockResolvedValue({
|
|
138
|
+
data: { content: mockUserEditedNotes },
|
|
139
|
+
});
|
|
140
|
+
mockAiGenerateDocumentWithState.execute.mockResolvedValue({
|
|
141
|
+
data: { content: mockOptimizedNotes },
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Context with user payload for manual transition
|
|
145
|
+
const contextWithPayload = new BlockExecutionContextDto({
|
|
146
|
+
payload: {
|
|
147
|
+
transition: {
|
|
148
|
+
id: 'user_response',
|
|
149
|
+
workflowId: '123',
|
|
150
|
+
payload: mockUserEditedNotes,
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const result = await processorWithState.process(workflowWithState, args, contextWithPayload);
|
|
156
|
+
|
|
157
|
+
// Should execute and stop at notes_optimized (next manual step)
|
|
158
|
+
expect(result.runtime.error).toBe(false);
|
|
159
|
+
expect(result.runtime.stop).toBe(true);
|
|
160
|
+
|
|
161
|
+
// Should call CreateDocument once for user response
|
|
162
|
+
expect(mockCreateDocumentWithState.execute).toHaveBeenCalledTimes(1);
|
|
163
|
+
expect(mockCreateDocumentWithState.execute).toHaveBeenCalledWith(
|
|
164
|
+
expect.objectContaining({
|
|
165
|
+
id: 'input',
|
|
166
|
+
}),
|
|
167
|
+
expect.anything(),
|
|
168
|
+
expect.anything(),
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
// Should call AiGenerateDocument once
|
|
172
|
+
expect(mockAiGenerateDocumentWithState.execute).toHaveBeenCalledTimes(1);
|
|
173
|
+
expect(mockAiGenerateDocumentWithState.execute).toHaveBeenCalledWith(
|
|
174
|
+
expect.objectContaining({
|
|
175
|
+
llm: {
|
|
176
|
+
provider: 'openai',
|
|
177
|
+
model: 'gpt-4o',
|
|
178
|
+
},
|
|
179
|
+
}),
|
|
180
|
+
expect.anything(),
|
|
181
|
+
expect.anything(),
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
// Verify history contains expected places
|
|
185
|
+
const history = result.state.caretaker.getHistory();
|
|
186
|
+
const places = history.map((h) => h.metadata?.place);
|
|
187
|
+
expect(places).toContain('response_received');
|
|
188
|
+
expect(places).toContain('notes_optimized');
|
|
189
|
+
|
|
190
|
+
await moduleWithState.close();
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe('confirm step', () => {
|
|
195
|
+
it('should complete workflow when user confirms optimized notes', async () => {
|
|
196
|
+
const mockFinalNotes = {
|
|
197
|
+
date: '2025-01-01',
|
|
198
|
+
summary: 'Budget discussion with updates',
|
|
199
|
+
participants: ['Sarah', 'Anna', 'Bob'],
|
|
200
|
+
decisions: ['Cut costs by 15%'],
|
|
201
|
+
actionItems: ['Follow up on vendor pricing by Friday'],
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const args = { inputText: 'any text' };
|
|
205
|
+
|
|
206
|
+
// Create module with existing workflow state after AI optimization
|
|
207
|
+
const moduleWithState = await createWorkflowTest()
|
|
208
|
+
.forWorkflow(MeetingNotesWorkflow)
|
|
209
|
+
.withImports(LoopCoreModule, CoreUiModule, AiModule)
|
|
210
|
+
.withProvider(MeetingNotesDocument)
|
|
211
|
+
.withProvider(OptimizedNotesDocument)
|
|
212
|
+
.withToolOverride(CreateDocument)
|
|
213
|
+
.withToolOverride(AiGenerateDocument)
|
|
214
|
+
.withExistingWorkflow({
|
|
215
|
+
id: '123',
|
|
216
|
+
place: 'notes_optimized',
|
|
217
|
+
hashRecord: {
|
|
218
|
+
options: generateObjectFingerprint(args), // previously run with same arguments
|
|
219
|
+
},
|
|
220
|
+
})
|
|
221
|
+
.compile();
|
|
222
|
+
|
|
223
|
+
const workflowWithState = moduleWithState.get(MeetingNotesWorkflow);
|
|
224
|
+
const processorWithState = moduleWithState.get(WorkflowProcessorService);
|
|
225
|
+
|
|
226
|
+
const mockCreateDocumentWithState: ToolMock = moduleWithState.get(CreateDocument);
|
|
227
|
+
|
|
228
|
+
mockCreateDocumentWithState.execute.mockResolvedValue({
|
|
229
|
+
data: { content: mockFinalNotes },
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Context with user confirmation for manual transition
|
|
233
|
+
const contextWithPayload = new BlockExecutionContextDto({
|
|
234
|
+
payload: {
|
|
235
|
+
transition: {
|
|
236
|
+
id: 'confirm',
|
|
237
|
+
workflowId: '123',
|
|
238
|
+
payload: mockFinalNotes,
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const result = await processorWithState.process(workflowWithState, args, contextWithPayload);
|
|
244
|
+
|
|
245
|
+
// Should complete and reach end state
|
|
246
|
+
expect(result.runtime.error).toBe(false);
|
|
247
|
+
expect(result.runtime.stop).toBe(false);
|
|
248
|
+
|
|
249
|
+
// Should call CreateDocument once for final confirmation
|
|
250
|
+
expect(mockCreateDocumentWithState.execute).toHaveBeenCalledTimes(1);
|
|
251
|
+
|
|
252
|
+
// Verify history contains expected places including end
|
|
253
|
+
const history = result.state.caretaker.getHistory();
|
|
254
|
+
const places = history.map((h) => h.metadata?.place);
|
|
255
|
+
expect(places).toContain('end');
|
|
256
|
+
|
|
257
|
+
await moduleWithState.close();
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { BlockConfig, WithArguments } from '@loopstack/common';
|
|
4
|
+
import { DocumentBase } from '@loopstack/core';
|
|
5
|
+
|
|
6
|
+
export const MeetingNotesDocumentSchema = z.object({
|
|
7
|
+
text: z.string(),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
@Injectable()
|
|
11
|
+
@BlockConfig({
|
|
12
|
+
configFile: __dirname + '/meeting-notes-document.yaml',
|
|
13
|
+
})
|
|
14
|
+
@WithArguments(MeetingNotesDocumentSchema)
|
|
15
|
+
export class MeetingNotesDocument extends DocumentBase {}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { BlockConfig, WithArguments } from '@loopstack/common';
|
|
4
|
+
import { DocumentBase } from '@loopstack/core';
|
|
5
|
+
|
|
6
|
+
export const OptimizedMeetingNotesDocumentSchema = z.object({
|
|
7
|
+
date: z.string(),
|
|
8
|
+
summary: z.string(),
|
|
9
|
+
participants: z.array(z.string()),
|
|
10
|
+
decisions: z.array(z.string()),
|
|
11
|
+
actionItems: z.array(z.string()),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
@Injectable()
|
|
15
|
+
@BlockConfig({
|
|
16
|
+
configFile: __dirname + '/optimized-notes-document.yaml',
|
|
17
|
+
})
|
|
18
|
+
@WithArguments(OptimizedMeetingNotesDocumentSchema)
|
|
19
|
+
export class OptimizedNotesDocument extends DocumentBase {}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
type: document
|
|
2
|
+
ui:
|
|
3
|
+
form:
|
|
4
|
+
order:
|
|
5
|
+
- date
|
|
6
|
+
- summary
|
|
7
|
+
- participants
|
|
8
|
+
- decisions
|
|
9
|
+
- actionItems
|
|
10
|
+
properties:
|
|
11
|
+
date:
|
|
12
|
+
title: Date
|
|
13
|
+
summary:
|
|
14
|
+
title: Summary
|
|
15
|
+
widget: textarea
|
|
16
|
+
participants:
|
|
17
|
+
title: Participants
|
|
18
|
+
collapsed: true
|
|
19
|
+
items:
|
|
20
|
+
title: Participant
|
|
21
|
+
decisions:
|
|
22
|
+
title: Decisions
|
|
23
|
+
collapsed: true
|
|
24
|
+
items:
|
|
25
|
+
title: Decision
|
|
26
|
+
actionItems:
|
|
27
|
+
title: Action Items
|
|
28
|
+
collapsed: true
|
|
29
|
+
items:
|
|
30
|
+
title: Action Item
|
|
31
|
+
actions:
|
|
32
|
+
- type: button
|
|
33
|
+
widget: button
|
|
34
|
+
transition: confirm
|
|
35
|
+
options:
|
|
36
|
+
label: 'Confirm'
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common';
|
|
2
|
+
import { AiModule } from '@loopstack/ai-module';
|
|
3
|
+
import { LoopCoreModule } from '@loopstack/core';
|
|
4
|
+
import { CoreUiModule } from '@loopstack/core-ui-module';
|
|
5
|
+
import { MeetingNotesDocument } from './documents/meeting-notes-document';
|
|
6
|
+
import { OptimizedNotesDocument } from './documents/optimized-notes-document';
|
|
7
|
+
import { MeetingNotesWorkflow } from './meeting-notes.workflow';
|
|
8
|
+
|
|
9
|
+
@Module({
|
|
10
|
+
imports: [LoopCoreModule, CoreUiModule, AiModule],
|
|
11
|
+
providers: [MeetingNotesWorkflow, MeetingNotesDocument, OptimizedNotesDocument],
|
|
12
|
+
exports: [MeetingNotesWorkflow],
|
|
13
|
+
})
|
|
14
|
+
export class MeetingNotesExampleModule {}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { AiGenerateDocument } from '@loopstack/ai-module';
|
|
3
|
+
import { BlockConfig, Document, Tool, WithArguments, WithState } from '@loopstack/common';
|
|
4
|
+
import { WorkflowBase } from '@loopstack/core';
|
|
5
|
+
import { CreateDocument } from '@loopstack/core-ui-module';
|
|
6
|
+
import { MeetingNotesDocument, MeetingNotesDocumentSchema } from './documents/meeting-notes-document';
|
|
7
|
+
import { OptimizedMeetingNotesDocumentSchema, OptimizedNotesDocument } from './documents/optimized-notes-document';
|
|
8
|
+
|
|
9
|
+
@BlockConfig({
|
|
10
|
+
configFile: __dirname + '/meeting-notes.workflow.yaml',
|
|
11
|
+
})
|
|
12
|
+
@WithArguments(
|
|
13
|
+
z.object({
|
|
14
|
+
inputText: z
|
|
15
|
+
.string()
|
|
16
|
+
.default(
|
|
17
|
+
'- meeting 1.1.2025\n- budget: need 2 cut costs sarah said\n- hire new person?? --> marketing\n- vendor pricing - follow up needed by anna',
|
|
18
|
+
),
|
|
19
|
+
}),
|
|
20
|
+
)
|
|
21
|
+
@WithState(
|
|
22
|
+
z.object({
|
|
23
|
+
meetingNotes: MeetingNotesDocumentSchema.optional(),
|
|
24
|
+
optimizedNotes: OptimizedMeetingNotesDocumentSchema.optional(),
|
|
25
|
+
}),
|
|
26
|
+
)
|
|
27
|
+
export class MeetingNotesWorkflow extends WorkflowBase {
|
|
28
|
+
@Tool() aiGenerateDocument: AiGenerateDocument;
|
|
29
|
+
@Tool() createDocument: CreateDocument;
|
|
30
|
+
@Document() meetingNotesDocument: MeetingNotesDocument;
|
|
31
|
+
@Document() optimizedNotesDocument: OptimizedNotesDocument;
|
|
32
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
title: 'Human-in-the-loop Demo (Meeting Notes Optimizer)'
|
|
2
|
+
|
|
3
|
+
description: 'A demo workflow to demonstrate how to use AI to structure meeting notes.'
|
|
4
|
+
|
|
5
|
+
ui:
|
|
6
|
+
form:
|
|
7
|
+
properties:
|
|
8
|
+
inputText:
|
|
9
|
+
title: 'Text'
|
|
10
|
+
widget: 'textarea'
|
|
11
|
+
|
|
12
|
+
transitions:
|
|
13
|
+
- id: create_form
|
|
14
|
+
from: start
|
|
15
|
+
to: waiting_for_response
|
|
16
|
+
call:
|
|
17
|
+
- id: form
|
|
18
|
+
tool: createDocument
|
|
19
|
+
args:
|
|
20
|
+
id: input
|
|
21
|
+
document: meetingNotesDocument
|
|
22
|
+
update:
|
|
23
|
+
content:
|
|
24
|
+
text: |
|
|
25
|
+
Unstructured Notes:
|
|
26
|
+
|
|
27
|
+
{{ args.inputText }}
|
|
28
|
+
|
|
29
|
+
- id: user_response
|
|
30
|
+
from: waiting_for_response
|
|
31
|
+
to: response_received
|
|
32
|
+
trigger: manual
|
|
33
|
+
call:
|
|
34
|
+
- id: create_response
|
|
35
|
+
tool: createDocument
|
|
36
|
+
args:
|
|
37
|
+
id: input
|
|
38
|
+
document: meetingNotesDocument
|
|
39
|
+
update:
|
|
40
|
+
content: ${ transition.payload }
|
|
41
|
+
assign:
|
|
42
|
+
meetingNotes: ${ result.data.content }
|
|
43
|
+
|
|
44
|
+
- id: optimize_notes
|
|
45
|
+
from: response_received
|
|
46
|
+
to: notes_optimized
|
|
47
|
+
call:
|
|
48
|
+
- id: prompt
|
|
49
|
+
tool: aiGenerateDocument
|
|
50
|
+
args:
|
|
51
|
+
llm:
|
|
52
|
+
provider: openai
|
|
53
|
+
model: gpt-4o
|
|
54
|
+
response:
|
|
55
|
+
id: final
|
|
56
|
+
document: optimizedNotesDocument
|
|
57
|
+
prompt: |
|
|
58
|
+
Extract all information from the provided meeting notes into the structured document.
|
|
59
|
+
|
|
60
|
+
<Meeting Notes>
|
|
61
|
+
{{ meetingNotes.text }}
|
|
62
|
+
</Meeting Notes>
|
|
63
|
+
assign:
|
|
64
|
+
optimizedNotes: ${ result.data.content }
|
|
65
|
+
|
|
66
|
+
- id: confirm
|
|
67
|
+
from: notes_optimized
|
|
68
|
+
to: end
|
|
69
|
+
trigger: manual
|
|
70
|
+
call:
|
|
71
|
+
- id: create_response
|
|
72
|
+
tool: createDocument
|
|
73
|
+
args:
|
|
74
|
+
id: final
|
|
75
|
+
document: optimizedNotesDocument
|
|
76
|
+
update:
|
|
77
|
+
content: ${ transition.payload }
|
|
78
|
+
assign:
|
|
79
|
+
optimizedNotes: ${ result.data.content }
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2023",
|
|
4
|
+
"module": "CommonJS",
|
|
5
|
+
"declaration": true,
|
|
6
|
+
"removeComments": true,
|
|
7
|
+
"emitDecoratorMetadata": true,
|
|
8
|
+
"experimentalDecorators": true,
|
|
9
|
+
"allowSyntheticDefaultImports": true,
|
|
10
|
+
"sourceMap": true,
|
|
11
|
+
"outDir": "./dist",
|
|
12
|
+
"rootDir": "./src",
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"strictNullChecks": true,
|
|
15
|
+
"forceConsistentCasingInFileNames": true,
|
|
16
|
+
"noImplicitAny": false,
|
|
17
|
+
"strictBindCallApply": false,
|
|
18
|
+
"noFallthroughCasesInSwitch": false
|
|
19
|
+
},
|
|
20
|
+
"include": ["./src"]
|
|
21
|
+
}
|
package/LICENSE
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
Apache License
|
|
2
|
-
Version 2.0, January 2004
|
|
3
|
-
http://www.apache.org/licenses/
|
|
4
|
-
|
|
5
|
-
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
-
|
|
7
|
-
Copyright 2025 Loopstack GmbH
|
|
8
|
-
|
|
9
|
-
Licensed under the Apache License, Version 2.0 (the "License");
|
|
10
|
-
you may not use this skeleton application except in compliance with the License.
|
|
11
|
-
You may obtain a copy of the License at
|
|
12
|
-
|
|
13
|
-
http://www.apache.org/licenses/LICENSE-2.0
|
|
14
|
-
|
|
15
|
-
Unless required by applicable law or agreed to in writing, software
|
|
16
|
-
distributed under the License is distributed on an "AS IS" BASIS,
|
|
17
|
-
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
18
|
-
See the License for the specific language governing permissions and
|
|
19
|
-
limitations under the License.
|