@loopstack/hitl 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -0
- package/package.json +5 -8
- package/src/documents/ask-user-confirm-document.ts +0 -20
- package/src/documents/ask-user-confirm-document.yaml +0 -6
- package/src/documents/ask-user-document.ts +0 -20
- package/src/documents/ask-user-document.yaml +0 -6
- package/src/documents/ask-user-options-document.ts +0 -24
- package/src/documents/ask-user-options-document.yaml +0 -6
- package/src/documents/confirm-user-document.ts +0 -18
- package/src/documents/confirm-user-document.yaml +0 -16
- package/src/documents/index.ts +0 -12
- package/src/hitl.module.ts +0 -11
- package/src/index.ts +0 -3
- package/src/workflows/ask-user/ask-user.ui.yaml +0 -6
- package/src/workflows/ask-user/ask-user.workflow.ts +0 -83
- package/src/workflows/confirm-user/confirm-user.ui.yaml +0 -5
- package/src/workflows/confirm-user/confirm-user.workflow.ts +0 -29
- package/src/workflows/index.ts +0 -2
package/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# @loopstack/hitl
|
|
2
|
+
|
|
3
|
+
> A module for the [Loopstack AI](https://loopstack.ai) automation framework.
|
|
4
|
+
|
|
5
|
+
Human-in-the-loop (HITL) building blocks for Loopstack workflows. Pause a running workflow, ask the user a question, and resume once they answer.
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
Most non-trivial workflows need a human decision at some point — "is this the right file?", "do you want to proceed?", "pick one of these options". This module provides two ready-to-use workflows that handle the wait-for-user pattern, along with the document types that render the prompts in the UI.
|
|
10
|
+
|
|
11
|
+
By using this module you'll get:
|
|
12
|
+
|
|
13
|
+
- **`AskUserWorkflow`** — asks a free-text question, a yes/no confirmation, or a multiple-choice question (mode is a runtime arg)
|
|
14
|
+
- **`ConfirmUserWorkflow`** — shows markdown content and waits for a confirm / deny response
|
|
15
|
+
- **4 document types** that render the prompts: `AskUserDocument`, `AskUserConfirmDocument`, `AskUserOptionsDocument`, `ConfirmUserDocument`
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
npm install @loopstack/hitl
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Register the module:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { HitlModule } from '@loopstack/hitl';
|
|
27
|
+
|
|
28
|
+
@Module({
|
|
29
|
+
imports: [HitlModule /* ... */],
|
|
30
|
+
})
|
|
31
|
+
export class AppModule {}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## How It Works
|
|
35
|
+
|
|
36
|
+
### Asking a text question as a sub-workflow
|
|
37
|
+
|
|
38
|
+
Use `@InjectWorkflow()` to launch `AskUserWorkflow` from a parent workflow. The call resolves with the user's answer once they respond in the UI:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { BaseWorkflow, InjectWorkflow, Transition, Workflow } from '@loopstack/common';
|
|
42
|
+
import { AskUserWorkflow } from '@loopstack/hitl';
|
|
43
|
+
|
|
44
|
+
@Workflow({ uiConfig: __dirname + '/my.ui.yaml' })
|
|
45
|
+
export class MyWorkflow extends BaseWorkflow {
|
|
46
|
+
@InjectWorkflow() askUser: AskUserWorkflow;
|
|
47
|
+
|
|
48
|
+
@Transition({ from: 'ready', to: 'done' })
|
|
49
|
+
async collectName() {
|
|
50
|
+
const { answer } = await this.askUser.run({ question: 'What is your name?' }, { alias: 'askName' });
|
|
51
|
+
// use `answer`
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Multiple-choice and confirmation modes
|
|
57
|
+
|
|
58
|
+
`AskUserWorkflow` takes an optional `mode`:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
await this.askUser.run({
|
|
62
|
+
question: 'Which environment?',
|
|
63
|
+
mode: 'options',
|
|
64
|
+
options: ['staging', 'production'],
|
|
65
|
+
allowCustomAnswer: false,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
await this.askUser.run({
|
|
69
|
+
question: 'Proceed with deletion?',
|
|
70
|
+
mode: 'confirm',
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Showing long-form content for confirmation
|
|
75
|
+
|
|
76
|
+
`ConfirmUserWorkflow` is for "review and confirm" flows where you want to render markdown (e.g. a summary) and get a confirm / deny response:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { ConfirmUserWorkflow } from '@loopstack/hitl';
|
|
80
|
+
|
|
81
|
+
const { confirmed } = await this.confirmUser.run({
|
|
82
|
+
markdown: '## About to commit\n\n- 3 files changed',
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Documents
|
|
87
|
+
|
|
88
|
+
Each workflow saves one of the document types on the workflow repository — the Studio UI picks up those documents and renders the corresponding input widget. You generally don't need to interact with the document classes directly, but they're exported if you want to query or post-process answers.
|
|
89
|
+
|
|
90
|
+
## Public API
|
|
91
|
+
|
|
92
|
+
- **Module:** `HitlModule`
|
|
93
|
+
- **Workflows:** `AskUserWorkflow`, `ConfirmUserWorkflow`
|
|
94
|
+
- **Documents:** `AskUserDocument`, `AskUserConfirmDocument`, `AskUserOptionsDocument`, `ConfirmUserDocument`
|
|
95
|
+
|
|
96
|
+
## Dependencies
|
|
97
|
+
|
|
98
|
+
- `@loopstack/common` — `BaseWorkflow`, decorators
|
|
99
|
+
- `@loopstack/core` — `LoopCoreModule`
|
|
100
|
+
|
|
101
|
+
## About
|
|
102
|
+
|
|
103
|
+
Author: [Jakob Klippel](https://www.linkedin.com/in/jakob-klippel/)
|
|
104
|
+
|
|
105
|
+
License: MIT
|
|
106
|
+
|
|
107
|
+
### Additional Resources
|
|
108
|
+
|
|
109
|
+
- [Loopstack Documentation](https://loopstack.ai/docs)
|
|
110
|
+
- Find more Loopstack modules in the [Loopstack Registry](https://loopstack.ai/registry)
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"workflow",
|
|
10
10
|
"user-input"
|
|
11
11
|
],
|
|
12
|
-
"version": "0.1.
|
|
12
|
+
"version": "0.1.2",
|
|
13
13
|
"license": "MIT",
|
|
14
14
|
"author": {
|
|
15
15
|
"name": "Jakob Klippel",
|
|
@@ -18,8 +18,7 @@
|
|
|
18
18
|
"main": "dist/index.js",
|
|
19
19
|
"types": "dist/index.d.ts",
|
|
20
20
|
"exports": {
|
|
21
|
-
".": "./dist/index.js"
|
|
22
|
-
"./src/*": "./src/*"
|
|
21
|
+
".": "./dist/index.js"
|
|
23
22
|
},
|
|
24
23
|
"scripts": {
|
|
25
24
|
"build": "nest build",
|
|
@@ -30,14 +29,13 @@
|
|
|
30
29
|
"watch": "nest build --watch"
|
|
31
30
|
},
|
|
32
31
|
"dependencies": {
|
|
33
|
-
"@loopstack/common": "^0.
|
|
34
|
-
"@loopstack/core": "^0.
|
|
32
|
+
"@loopstack/common": "^0.28.0",
|
|
33
|
+
"@loopstack/core": "^0.28.0",
|
|
35
34
|
"@nestjs/common": "^11.1.19",
|
|
36
35
|
"zod": "^4.3.6"
|
|
37
36
|
},
|
|
38
37
|
"files": [
|
|
39
|
-
"dist"
|
|
40
|
-
"src"
|
|
38
|
+
"dist"
|
|
41
39
|
],
|
|
42
40
|
"jest": {
|
|
43
41
|
"moduleFileExtensions": [
|
|
@@ -65,7 +63,6 @@
|
|
|
65
63
|
}
|
|
66
64
|
],
|
|
67
65
|
"installModes": [
|
|
68
|
-
"add",
|
|
69
66
|
"install"
|
|
70
67
|
]
|
|
71
68
|
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { Document } from '@loopstack/common';
|
|
3
|
-
|
|
4
|
-
export const AskUserConfirmDocumentSchema = z
|
|
5
|
-
.object({
|
|
6
|
-
question: z.string(),
|
|
7
|
-
answer: z.string().optional(),
|
|
8
|
-
})
|
|
9
|
-
.strict();
|
|
10
|
-
|
|
11
|
-
export type AskUserConfirmDocumentType = z.infer<typeof AskUserConfirmDocumentSchema>;
|
|
12
|
-
|
|
13
|
-
@Document({
|
|
14
|
-
uiConfig: __dirname + '/ask-user-confirm-document.yaml',
|
|
15
|
-
schema: AskUserConfirmDocumentSchema,
|
|
16
|
-
})
|
|
17
|
-
export class AskUserConfirmDocument {
|
|
18
|
-
question: string;
|
|
19
|
-
answer?: string;
|
|
20
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { Document } from '@loopstack/common';
|
|
3
|
-
|
|
4
|
-
export const AskUserDocumentSchema = z
|
|
5
|
-
.object({
|
|
6
|
-
question: z.string(),
|
|
7
|
-
answer: z.string().optional(),
|
|
8
|
-
})
|
|
9
|
-
.strict();
|
|
10
|
-
|
|
11
|
-
export type AskUserDocumentType = z.infer<typeof AskUserDocumentSchema>;
|
|
12
|
-
|
|
13
|
-
@Document({
|
|
14
|
-
uiConfig: __dirname + '/ask-user-document.yaml',
|
|
15
|
-
schema: AskUserDocumentSchema,
|
|
16
|
-
})
|
|
17
|
-
export class AskUserDocument {
|
|
18
|
-
question: string;
|
|
19
|
-
answer?: string;
|
|
20
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { Document } from '@loopstack/common';
|
|
3
|
-
|
|
4
|
-
export const AskUserOptionsDocumentSchema = z
|
|
5
|
-
.object({
|
|
6
|
-
question: z.string(),
|
|
7
|
-
options: z.array(z.string()),
|
|
8
|
-
allowCustomAnswer: z.boolean().optional(),
|
|
9
|
-
answer: z.string().optional(),
|
|
10
|
-
})
|
|
11
|
-
.strict();
|
|
12
|
-
|
|
13
|
-
export type AskUserOptionsDocumentType = z.infer<typeof AskUserOptionsDocumentSchema>;
|
|
14
|
-
|
|
15
|
-
@Document({
|
|
16
|
-
uiConfig: __dirname + '/ask-user-options-document.yaml',
|
|
17
|
-
schema: AskUserOptionsDocumentSchema,
|
|
18
|
-
})
|
|
19
|
-
export class AskUserOptionsDocument {
|
|
20
|
-
question: string;
|
|
21
|
-
options: string[];
|
|
22
|
-
allowCustomAnswer?: boolean;
|
|
23
|
-
answer?: string;
|
|
24
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { Document } from '@loopstack/common';
|
|
3
|
-
|
|
4
|
-
export const ConfirmUserDocumentSchema = z
|
|
5
|
-
.object({
|
|
6
|
-
markdown: z.string(),
|
|
7
|
-
})
|
|
8
|
-
.strict();
|
|
9
|
-
|
|
10
|
-
export type ConfirmUserDocumentType = z.infer<typeof ConfirmUserDocumentSchema>;
|
|
11
|
-
|
|
12
|
-
@Document({
|
|
13
|
-
uiConfig: __dirname + '/confirm-user-document.yaml',
|
|
14
|
-
schema: ConfirmUserDocumentSchema,
|
|
15
|
-
})
|
|
16
|
-
export class ConfirmUserDocument {
|
|
17
|
-
markdown: string;
|
|
18
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
type: document
|
|
2
|
-
ui:
|
|
3
|
-
widgets:
|
|
4
|
-
- widget: form
|
|
5
|
-
options:
|
|
6
|
-
properties:
|
|
7
|
-
markdown:
|
|
8
|
-
widget: markdown-view
|
|
9
|
-
actions:
|
|
10
|
-
- type: button
|
|
11
|
-
transition: userDenied
|
|
12
|
-
label: 'Deny'
|
|
13
|
-
variant: outline
|
|
14
|
-
- type: button
|
|
15
|
-
transition: userConfirmed
|
|
16
|
-
label: 'Confirm'
|
package/src/documents/index.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
export { AskUserDocument, AskUserDocumentSchema, AskUserDocumentType } from './ask-user-document';
|
|
2
|
-
export {
|
|
3
|
-
AskUserConfirmDocument,
|
|
4
|
-
AskUserConfirmDocumentSchema,
|
|
5
|
-
AskUserConfirmDocumentType,
|
|
6
|
-
} from './ask-user-confirm-document';
|
|
7
|
-
export {
|
|
8
|
-
AskUserOptionsDocument,
|
|
9
|
-
AskUserOptionsDocumentSchema,
|
|
10
|
-
AskUserOptionsDocumentType,
|
|
11
|
-
} from './ask-user-options-document';
|
|
12
|
-
export { ConfirmUserDocument, ConfirmUserDocumentSchema, ConfirmUserDocumentType } from './confirm-user-document';
|
package/src/hitl.module.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { Module } from '@nestjs/common';
|
|
2
|
-
import { LoopCoreModule } from '@loopstack/core';
|
|
3
|
-
import { AskUserWorkflow } from './workflows/ask-user/ask-user.workflow';
|
|
4
|
-
import { ConfirmUserWorkflow } from './workflows/confirm-user/confirm-user.workflow';
|
|
5
|
-
|
|
6
|
-
@Module({
|
|
7
|
-
imports: [LoopCoreModule],
|
|
8
|
-
providers: [AskUserWorkflow, ConfirmUserWorkflow],
|
|
9
|
-
exports: [AskUserWorkflow, ConfirmUserWorkflow],
|
|
10
|
-
})
|
|
11
|
-
export class HitlModule {}
|
package/src/index.ts
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
title: 'Ask User'
|
|
2
|
-
|
|
3
|
-
description: |
|
|
4
|
-
Generic sub-workflow that presents a question to the user and waits for their answer.
|
|
5
|
-
Used by async tool calls (e.g. askClarification) to interrupt an agent loop for user input.
|
|
6
|
-
Supports three modes: text (default), options (pick from a list), and confirm (yes/no).
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { BaseWorkflow, Final, Guard, Initial, Transition, Workflow } from '@loopstack/common';
|
|
3
|
-
import { AskUserConfirmDocument } from '../../documents/ask-user-confirm-document';
|
|
4
|
-
import { AskUserDocument } from '../../documents/ask-user-document';
|
|
5
|
-
import { AskUserOptionsDocument } from '../../documents/ask-user-options-document';
|
|
6
|
-
|
|
7
|
-
const AskUserAnswerSchema = z.object({
|
|
8
|
-
answer: z.string(),
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
@Workflow({
|
|
12
|
-
uiConfig: __dirname + '/ask-user.ui.yaml',
|
|
13
|
-
schema: z.object({
|
|
14
|
-
question: z.string(),
|
|
15
|
-
mode: z.enum(['text', 'options', 'confirm']).optional(),
|
|
16
|
-
options: z.array(z.string()).optional(),
|
|
17
|
-
allowCustomAnswer: z.boolean().optional(),
|
|
18
|
-
}),
|
|
19
|
-
})
|
|
20
|
-
export class AskUserWorkflow extends BaseWorkflow {
|
|
21
|
-
@Initial({ to: 'show_question' })
|
|
22
|
-
start() {}
|
|
23
|
-
|
|
24
|
-
@Transition({ from: 'show_question', to: 'waiting_for_user', priority: 10 })
|
|
25
|
-
@Guard('isOptionsMode')
|
|
26
|
-
async showQuestionOptions() {
|
|
27
|
-
const { question, options, allowCustomAnswer } = this.ctx.args as {
|
|
28
|
-
question: string;
|
|
29
|
-
options?: string[];
|
|
30
|
-
allowCustomAnswer?: boolean;
|
|
31
|
-
};
|
|
32
|
-
await this.repository.save(
|
|
33
|
-
AskUserOptionsDocument,
|
|
34
|
-
{ question, options: options ?? [], allowCustomAnswer },
|
|
35
|
-
{ id: 'question' },
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
@Transition({ from: 'show_question', to: 'waiting_for_user', priority: 10 })
|
|
40
|
-
@Guard('isConfirmMode')
|
|
41
|
-
async showQuestionConfirm() {
|
|
42
|
-
const { question } = this.ctx.args as { question: string };
|
|
43
|
-
await this.repository.save(AskUserConfirmDocument, { question }, { id: 'question' });
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
@Transition({ from: 'show_question', to: 'waiting_for_user' })
|
|
47
|
-
async showQuestionText() {
|
|
48
|
-
const { question } = this.ctx.args as { question: string };
|
|
49
|
-
await this.repository.save(AskUserDocument, { question }, { id: 'question' });
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
@Final({ from: 'waiting_for_user', wait: true, schema: AskUserAnswerSchema })
|
|
53
|
-
async userAnswered(payload: { answer: string }): Promise<{ answer: string }> {
|
|
54
|
-
const { question, mode, options, allowCustomAnswer } = this.ctx.args as {
|
|
55
|
-
question: string;
|
|
56
|
-
mode?: string;
|
|
57
|
-
options?: string[];
|
|
58
|
-
allowCustomAnswer?: boolean;
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
if (mode === 'options') {
|
|
62
|
-
await this.repository.save(
|
|
63
|
-
AskUserOptionsDocument,
|
|
64
|
-
{ question, options: options ?? [], allowCustomAnswer, answer: payload.answer },
|
|
65
|
-
{ id: 'question' },
|
|
66
|
-
);
|
|
67
|
-
} else if (mode === 'confirm') {
|
|
68
|
-
await this.repository.save(AskUserConfirmDocument, { question, answer: payload.answer }, { id: 'question' });
|
|
69
|
-
} else {
|
|
70
|
-
await this.repository.save(AskUserDocument, { question, answer: payload.answer }, { id: 'question' });
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return { answer: payload.answer };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
private isOptionsMode(): boolean {
|
|
77
|
-
return (this.ctx.args as { mode?: string })?.mode === 'options';
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
private isConfirmMode(): boolean {
|
|
81
|
-
return (this.ctx.args as { mode?: string })?.mode === 'confirm';
|
|
82
|
-
}
|
|
83
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { BaseWorkflow, Final, Initial, Workflow } from '@loopstack/common';
|
|
3
|
-
import { ConfirmUserDocument } from '../../documents/confirm-user-document';
|
|
4
|
-
|
|
5
|
-
@Workflow({
|
|
6
|
-
uiConfig: __dirname + '/confirm-user.ui.yaml',
|
|
7
|
-
schema: z.object({
|
|
8
|
-
markdown: z.string(),
|
|
9
|
-
}),
|
|
10
|
-
})
|
|
11
|
-
export class ConfirmUserWorkflow extends BaseWorkflow {
|
|
12
|
-
markdown?: string;
|
|
13
|
-
|
|
14
|
-
@Initial({ to: 'waiting_for_confirmation' })
|
|
15
|
-
async showContent(args: { markdown: string }) {
|
|
16
|
-
this.markdown = args.markdown;
|
|
17
|
-
await this.repository.save(ConfirmUserDocument, { markdown: args.markdown }, { id: 'content' });
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
@Final({ from: 'waiting_for_confirmation', wait: true })
|
|
21
|
-
async userConfirmed(): Promise<{ confirmed: boolean; markdown: string }> {
|
|
22
|
-
return Promise.resolve({ confirmed: true, markdown: this.markdown! });
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
@Final({ from: 'waiting_for_confirmation', wait: true })
|
|
26
|
-
async userDenied(): Promise<{ confirmed: boolean; markdown: string }> {
|
|
27
|
-
return Promise.resolve({ confirmed: false, markdown: this.markdown! });
|
|
28
|
-
}
|
|
29
|
-
}
|
package/src/workflows/index.ts
DELETED