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.
@@ -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
+ }
@@ -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
+ })