human-in-the-loop 0.1.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/README.md +222 -0
- package/TODO.md +53 -0
- package/dist/index.cjs +899 -0
- package/dist/index.d.cts +344 -0
- package/dist/index.d.ts +344 -0
- package/dist/index.js +793 -0
- package/package.json +65 -0
- package/src/core/factory.test.ts +69 -0
- package/src/core/factory.ts +30 -0
- package/src/core/types.ts +191 -0
- package/src/index.ts +7 -0
- package/src/platforms/email/index.tsx +137 -0
- package/src/platforms/react/index.tsx +218 -0
- package/src/platforms/slack/index.ts +84 -0
- package/src/platforms/teams/index.ts +84 -0
- package/tsconfig.json +14 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
import type { HumanFunction, HumanTaskRequest, ReactConfig } from '../../core/types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Props for the HumanFeedback component
|
|
6
|
+
*/
|
|
7
|
+
export interface HumanFeedbackProps<TInput, TOutput> {
|
|
8
|
+
taskId: string
|
|
9
|
+
title: string
|
|
10
|
+
description: string
|
|
11
|
+
input: TInput
|
|
12
|
+
options?: string[] | Array<{ value: string; label: string }>
|
|
13
|
+
freeText?: boolean
|
|
14
|
+
onSubmit: (response: Partial<TOutput>) => void
|
|
15
|
+
config?: ReactConfig
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Simple React component for human feedback
|
|
20
|
+
*/
|
|
21
|
+
export function HumanFeedback<TInput, TOutput>({
|
|
22
|
+
taskId,
|
|
23
|
+
title,
|
|
24
|
+
description,
|
|
25
|
+
input,
|
|
26
|
+
options,
|
|
27
|
+
freeText,
|
|
28
|
+
onSubmit,
|
|
29
|
+
config
|
|
30
|
+
}: HumanFeedbackProps<TInput, TOutput>) {
|
|
31
|
+
const [response, setResponse] = useState<string>('')
|
|
32
|
+
const [selectedOption, setSelectedOption] = useState<string>('')
|
|
33
|
+
|
|
34
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
35
|
+
e.preventDefault()
|
|
36
|
+
|
|
37
|
+
const responseObj: Record<string, any> = {}
|
|
38
|
+
|
|
39
|
+
if (selectedOption) {
|
|
40
|
+
responseObj.selectedOption = selectedOption
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (response) {
|
|
44
|
+
responseObj.freeText = response
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
onSubmit(responseObj as Partial<TOutput>)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const styles = {
|
|
51
|
+
container: {
|
|
52
|
+
maxWidth: '500px',
|
|
53
|
+
margin: '0 auto',
|
|
54
|
+
padding: '20px',
|
|
55
|
+
borderRadius: '8px',
|
|
56
|
+
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
|
|
57
|
+
backgroundColor: config?.theme === 'dark' ? '#1a1a1a' : '#fff',
|
|
58
|
+
color: config?.theme === 'dark' ? '#fff' : '#333',
|
|
59
|
+
...config?.styles?.container
|
|
60
|
+
},
|
|
61
|
+
title: {
|
|
62
|
+
fontSize: '20px',
|
|
63
|
+
fontWeight: 'bold',
|
|
64
|
+
marginBottom: '10px',
|
|
65
|
+
...config?.styles?.title
|
|
66
|
+
},
|
|
67
|
+
description: {
|
|
68
|
+
marginBottom: '20px',
|
|
69
|
+
...config?.styles?.description
|
|
70
|
+
},
|
|
71
|
+
optionsContainer: {
|
|
72
|
+
marginBottom: '20px',
|
|
73
|
+
...config?.styles?.optionsContainer
|
|
74
|
+
},
|
|
75
|
+
option: {
|
|
76
|
+
display: 'block',
|
|
77
|
+
margin: '8px 0',
|
|
78
|
+
...config?.styles?.option
|
|
79
|
+
},
|
|
80
|
+
textarea: {
|
|
81
|
+
width: '100%',
|
|
82
|
+
padding: '10px',
|
|
83
|
+
borderRadius: '4px',
|
|
84
|
+
border: '1px solid #ccc',
|
|
85
|
+
minHeight: '100px',
|
|
86
|
+
marginBottom: '20px',
|
|
87
|
+
backgroundColor: config?.theme === 'dark' ? '#333' : '#fff',
|
|
88
|
+
color: config?.theme === 'dark' ? '#fff' : '#333',
|
|
89
|
+
...config?.styles?.textarea
|
|
90
|
+
},
|
|
91
|
+
button: {
|
|
92
|
+
padding: '10px 20px',
|
|
93
|
+
backgroundColor: '#0070f3',
|
|
94
|
+
color: 'white',
|
|
95
|
+
border: 'none',
|
|
96
|
+
borderRadius: '4px',
|
|
97
|
+
cursor: 'pointer',
|
|
98
|
+
...config?.styles?.button
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div style={styles.container}>
|
|
104
|
+
<h2 style={styles.title}>{title}</h2>
|
|
105
|
+
<p style={styles.description}>{description}</p>
|
|
106
|
+
|
|
107
|
+
<form onSubmit={handleSubmit}>
|
|
108
|
+
{options && options.length > 0 && (
|
|
109
|
+
<div style={styles.optionsContainer}>
|
|
110
|
+
{options.map((option, index) => {
|
|
111
|
+
const value = typeof option === 'string' ? option : option.value
|
|
112
|
+
const label = typeof option === 'string' ? option : option.label
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<label key={index} style={styles.option}>
|
|
116
|
+
<input
|
|
117
|
+
type="radio"
|
|
118
|
+
name="option"
|
|
119
|
+
value={value}
|
|
120
|
+
checked={selectedOption === value}
|
|
121
|
+
onChange={() => setSelectedOption(value)}
|
|
122
|
+
/>
|
|
123
|
+
{' '}{label}
|
|
124
|
+
</label>
|
|
125
|
+
)
|
|
126
|
+
})}
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
129
|
+
|
|
130
|
+
{freeText && (
|
|
131
|
+
<textarea
|
|
132
|
+
style={styles.textarea}
|
|
133
|
+
value={response}
|
|
134
|
+
onChange={(e) => setResponse(e.target.value)}
|
|
135
|
+
placeholder="Enter your response..."
|
|
136
|
+
/>
|
|
137
|
+
)}
|
|
138
|
+
|
|
139
|
+
<button type="submit" style={styles.button}>Submit</button>
|
|
140
|
+
</form>
|
|
141
|
+
</div>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Storage for in-memory responses (in a real implementation, this would be a database)
|
|
147
|
+
*/
|
|
148
|
+
const responses = new Map<string, any>()
|
|
149
|
+
const tasks = new Map<string, { status: 'pending' | 'completed' | 'timeout' }>()
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Implementation of HumanFunction for React
|
|
153
|
+
*/
|
|
154
|
+
export class ReactHumanFunction<TInput, TOutput> implements HumanFunction<TInput, TOutput> {
|
|
155
|
+
private config: ReactConfig & {
|
|
156
|
+
title: string,
|
|
157
|
+
description: string,
|
|
158
|
+
options?: string[] | Array<{ value: string; label: string }>,
|
|
159
|
+
freeText?: boolean
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
constructor(config: ReactConfig & {
|
|
163
|
+
title: string,
|
|
164
|
+
description: string,
|
|
165
|
+
options?: string[] | Array<{ value: string; label: string }>,
|
|
166
|
+
freeText?: boolean
|
|
167
|
+
}) {
|
|
168
|
+
this.config = config
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async request(input: TInput): Promise<HumanTaskRequest> {
|
|
172
|
+
const taskId = `task-${Date.now()}`
|
|
173
|
+
|
|
174
|
+
tasks.set(taskId, { status: 'pending' })
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
taskId,
|
|
178
|
+
status: 'pending',
|
|
179
|
+
messageId: {
|
|
180
|
+
slack: '',
|
|
181
|
+
teams: '',
|
|
182
|
+
react: taskId,
|
|
183
|
+
email: ''
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async getResponse(taskId: string): Promise<TOutput | null> {
|
|
189
|
+
const response = responses.get(taskId)
|
|
190
|
+
return response || null
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Handle a response submission
|
|
195
|
+
*/
|
|
196
|
+
handleResponse(taskId: string, response: TOutput): void {
|
|
197
|
+
responses.set(taskId, response)
|
|
198
|
+
tasks.set(taskId, { status: 'completed' })
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Get a React component for this human function
|
|
203
|
+
*/
|
|
204
|
+
getComponent(taskId: string, input: TInput): React.ReactNode {
|
|
205
|
+
return (
|
|
206
|
+
<HumanFeedback<TInput, TOutput>
|
|
207
|
+
taskId={taskId}
|
|
208
|
+
title={this.config.title}
|
|
209
|
+
description={this.config.description}
|
|
210
|
+
input={input}
|
|
211
|
+
options={this.config.options}
|
|
212
|
+
freeText={this.config.freeText}
|
|
213
|
+
config={this.config}
|
|
214
|
+
onSubmit={(response) => this.handleResponse(taskId, response as TOutput)}
|
|
215
|
+
/>
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { HumanFunction, HumanTaskRequest, SlackConfig } from '../../core/types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Slack-specific response option
|
|
5
|
+
*/
|
|
6
|
+
export interface SlackResponseOption {
|
|
7
|
+
value: string
|
|
8
|
+
label: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create a Slack message with the given options
|
|
13
|
+
*/
|
|
14
|
+
export async function createSlackMessage<TInput, TOutput>(
|
|
15
|
+
taskId: string,
|
|
16
|
+
input: TInput,
|
|
17
|
+
config: SlackConfig & {
|
|
18
|
+
title: string,
|
|
19
|
+
description: string,
|
|
20
|
+
options?: string[] | SlackResponseOption[],
|
|
21
|
+
freeText?: boolean
|
|
22
|
+
}
|
|
23
|
+
): Promise<{ messageId: string }> {
|
|
24
|
+
console.log(`Creating Slack message for task ${taskId}`)
|
|
25
|
+
console.log(`Title: ${config.title}`)
|
|
26
|
+
console.log(`Description: ${config.description}`)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
return { messageId: `slack-${taskId}-${Date.now()}` }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get the response for a Slack task
|
|
34
|
+
*/
|
|
35
|
+
export async function getSlackResponse<TOutput>(taskId: string): Promise<TOutput | null> {
|
|
36
|
+
console.log(`Getting response for Slack task ${taskId}`)
|
|
37
|
+
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Implementation of HumanFunction for Slack
|
|
43
|
+
*/
|
|
44
|
+
export class SlackHumanFunction<TInput, TOutput> implements HumanFunction<TInput, TOutput> {
|
|
45
|
+
private config: SlackConfig & {
|
|
46
|
+
title: string,
|
|
47
|
+
description: string,
|
|
48
|
+
options?: string[] | SlackResponseOption[],
|
|
49
|
+
freeText?: boolean
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
constructor(config: SlackConfig & {
|
|
53
|
+
title: string,
|
|
54
|
+
description: string,
|
|
55
|
+
options?: string[] | SlackResponseOption[],
|
|
56
|
+
freeText?: boolean
|
|
57
|
+
}) {
|
|
58
|
+
this.config = config
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async request(input: TInput): Promise<HumanTaskRequest> {
|
|
62
|
+
const taskId = `task-${Date.now()}`
|
|
63
|
+
const { messageId } = await createSlackMessage<TInput, TOutput>(
|
|
64
|
+
taskId,
|
|
65
|
+
input,
|
|
66
|
+
this.config
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
taskId,
|
|
71
|
+
status: 'pending',
|
|
72
|
+
messageId: {
|
|
73
|
+
slack: messageId,
|
|
74
|
+
teams: '',
|
|
75
|
+
react: '',
|
|
76
|
+
email: ''
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async getResponse(taskId: string): Promise<TOutput | null> {
|
|
82
|
+
return getSlackResponse<TOutput>(taskId)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { HumanFunction, HumanTaskRequest, TeamsConfig } from '../../core/types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Teams-specific response option
|
|
5
|
+
*/
|
|
6
|
+
export interface TeamsResponseOption {
|
|
7
|
+
value: string
|
|
8
|
+
label: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create a Teams message with the given options
|
|
13
|
+
*/
|
|
14
|
+
export async function createTeamsMessage<TInput, TOutput>(
|
|
15
|
+
taskId: string,
|
|
16
|
+
input: TInput,
|
|
17
|
+
config: TeamsConfig & {
|
|
18
|
+
title: string,
|
|
19
|
+
description: string,
|
|
20
|
+
options?: string[] | TeamsResponseOption[],
|
|
21
|
+
freeText?: boolean
|
|
22
|
+
}
|
|
23
|
+
): Promise<{ messageId: string }> {
|
|
24
|
+
console.log(`Creating Teams message for task ${taskId}`)
|
|
25
|
+
console.log(`Title: ${config.title}`)
|
|
26
|
+
console.log(`Description: ${config.description}`)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
return { messageId: `teams-${taskId}-${Date.now()}` }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get the response for a Teams task
|
|
34
|
+
*/
|
|
35
|
+
export async function getTeamsResponse<TOutput>(taskId: string): Promise<TOutput | null> {
|
|
36
|
+
console.log(`Getting response for Teams task ${taskId}`)
|
|
37
|
+
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Implementation of HumanFunction for Microsoft Teams
|
|
43
|
+
*/
|
|
44
|
+
export class TeamsHumanFunction<TInput, TOutput> implements HumanFunction<TInput, TOutput> {
|
|
45
|
+
private config: TeamsConfig & {
|
|
46
|
+
title: string,
|
|
47
|
+
description: string,
|
|
48
|
+
options?: string[] | TeamsResponseOption[],
|
|
49
|
+
freeText?: boolean
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
constructor(config: TeamsConfig & {
|
|
53
|
+
title: string,
|
|
54
|
+
description: string,
|
|
55
|
+
options?: string[] | TeamsResponseOption[],
|
|
56
|
+
freeText?: boolean
|
|
57
|
+
}) {
|
|
58
|
+
this.config = config
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async request(input: TInput): Promise<HumanTaskRequest> {
|
|
62
|
+
const taskId = `task-${Date.now()}`
|
|
63
|
+
const { messageId } = await createTeamsMessage<TInput, TOutput>(
|
|
64
|
+
taskId,
|
|
65
|
+
input,
|
|
66
|
+
this.config
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
taskId,
|
|
71
|
+
status: 'pending',
|
|
72
|
+
messageId: {
|
|
73
|
+
slack: '',
|
|
74
|
+
teams: messageId,
|
|
75
|
+
react: '',
|
|
76
|
+
email: ''
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async getResponse(taskId: string): Promise<TOutput | null> {
|
|
82
|
+
return getTeamsResponse<TOutput>(taskId)
|
|
83
|
+
}
|
|
84
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"include": ["src"],
|
|
4
|
+
"exclude": ["node_modules", "dist"],
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"incremental": false,
|
|
10
|
+
"paths": {
|
|
11
|
+
"@/*": ["./src/*"]
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config'
|
|
2
|
+
import { resolve } from 'path'
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
test: {
|
|
6
|
+
environment: 'node',
|
|
7
|
+
include: ['**/*.test.ts', '**/*.test.tsx'],
|
|
8
|
+
globals: true
|
|
9
|
+
},
|
|
10
|
+
resolve: {
|
|
11
|
+
alias: {
|
|
12
|
+
'@': resolve(__dirname, './src')
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
})
|