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
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "human-in-the-loop",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Strongly-typed interface for human functions across multiple platforms",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"human",
|
|
17
|
+
"in-the-loop",
|
|
18
|
+
"feedback",
|
|
19
|
+
"slack",
|
|
20
|
+
"teams",
|
|
21
|
+
"react",
|
|
22
|
+
"email"
|
|
23
|
+
],
|
|
24
|
+
"author": "Drivly",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/drivly/primitives.org.ai.git"
|
|
29
|
+
},
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/drivly/primitives.org.ai/issues"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://primitives.org.ai",
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"zod": "^3.24.3",
|
|
36
|
+
"ai-providers": "0.2.0"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"react": "^19.0.0",
|
|
40
|
+
"react-dom": "^19.0.0",
|
|
41
|
+
"react-email": "^1.10.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/node": "^22.13.10",
|
|
45
|
+
"@types/react": "^19.0.0",
|
|
46
|
+
"@types/react-dom": "^19.0.0",
|
|
47
|
+
"eslint": "^9.17.0",
|
|
48
|
+
"prettier": "^3.4.2",
|
|
49
|
+
"tsup": "^8.0.1",
|
|
50
|
+
"typescript": "^5.8.2",
|
|
51
|
+
"vitest": "^3.0.9"
|
|
52
|
+
},
|
|
53
|
+
"type": "module",
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=18"
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
59
|
+
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
60
|
+
"test": "vitest run",
|
|
61
|
+
"test:watch": "vitest",
|
|
62
|
+
"lint": "eslint .",
|
|
63
|
+
"format": "prettier --write ."
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { createHumanFunction } from './factory'
|
|
3
|
+
import { SlackHumanFunction } from '../platforms/slack'
|
|
4
|
+
import { TeamsHumanFunction } from '../platforms/teams'
|
|
5
|
+
import { ReactHumanFunction } from '../platforms/react'
|
|
6
|
+
import { EmailHumanFunction } from '../platforms/email'
|
|
7
|
+
|
|
8
|
+
describe('createHumanFunction', () => {
|
|
9
|
+
it('should create a Slack human function', () => {
|
|
10
|
+
const humanFunction = createHumanFunction<{}, {}>({
|
|
11
|
+
platform: 'slack',
|
|
12
|
+
title: 'Test',
|
|
13
|
+
description: 'Test description'
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
expect(humanFunction).toBeInstanceOf(SlackHumanFunction)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('should create a Teams human function', () => {
|
|
20
|
+
const humanFunction = createHumanFunction<{}, {}>({
|
|
21
|
+
platform: 'teams',
|
|
22
|
+
title: 'Test',
|
|
23
|
+
description: 'Test description'
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
expect(humanFunction).toBeInstanceOf(TeamsHumanFunction)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('should create a React human function', () => {
|
|
30
|
+
const humanFunction = createHumanFunction<{}, {}>({
|
|
31
|
+
platform: 'react',
|
|
32
|
+
title: 'Test',
|
|
33
|
+
description: 'Test description'
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
expect(humanFunction).toBeInstanceOf(ReactHumanFunction)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('should create an Email human function', () => {
|
|
40
|
+
const humanFunction = createHumanFunction<{}, {}>({
|
|
41
|
+
platform: 'email',
|
|
42
|
+
title: 'Test',
|
|
43
|
+
description: 'Test description',
|
|
44
|
+
to: 'test@example.com'
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
expect(humanFunction).toBeInstanceOf(EmailHumanFunction)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('should throw an error for unsupported platforms', () => {
|
|
51
|
+
expect(() => {
|
|
52
|
+
createHumanFunction<{}, {}>({
|
|
53
|
+
platform: 'invalid' as any,
|
|
54
|
+
title: 'Test',
|
|
55
|
+
description: 'Test description'
|
|
56
|
+
})
|
|
57
|
+
}).toThrow('Unsupported platform: invalid')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('should throw an error for email platform without to field', () => {
|
|
61
|
+
expect(() => {
|
|
62
|
+
createHumanFunction<{}, {}>({
|
|
63
|
+
platform: 'email',
|
|
64
|
+
title: 'Test',
|
|
65
|
+
description: 'Test description'
|
|
66
|
+
})
|
|
67
|
+
}).toThrow('Email platform requires a "to" field in options')
|
|
68
|
+
})
|
|
69
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { CreateHumanFunctionOptions, HumanFunction } from './types'
|
|
2
|
+
import { SlackHumanFunction } from '../platforms/slack'
|
|
3
|
+
import { TeamsHumanFunction } from '../platforms/teams'
|
|
4
|
+
import { ReactHumanFunction } from '../platforms/react'
|
|
5
|
+
import { EmailHumanFunction } from '../platforms/email'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Create a strongly-typed human function
|
|
9
|
+
*/
|
|
10
|
+
export function createHumanFunction<TInput, TOutput>(
|
|
11
|
+
options: CreateHumanFunctionOptions
|
|
12
|
+
): HumanFunction<TInput, TOutput> {
|
|
13
|
+
const { platform } = options
|
|
14
|
+
|
|
15
|
+
switch (platform) {
|
|
16
|
+
case 'slack':
|
|
17
|
+
return new SlackHumanFunction<TInput, TOutput>(options)
|
|
18
|
+
case 'teams':
|
|
19
|
+
return new TeamsHumanFunction<TInput, TOutput>(options)
|
|
20
|
+
case 'react':
|
|
21
|
+
return new ReactHumanFunction<TInput, TOutput>(options)
|
|
22
|
+
case 'email':
|
|
23
|
+
if (!options.to) {
|
|
24
|
+
throw new Error('Email platform requires a "to" field in options')
|
|
25
|
+
}
|
|
26
|
+
return new EmailHumanFunction<TInput, TOutput>(options as any)
|
|
27
|
+
default:
|
|
28
|
+
throw new Error(`Unsupported platform: ${platform}`)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Supported platforms for human-in-the-loop interactions
|
|
5
|
+
*/
|
|
6
|
+
export type HumanPlatform = 'slack' | 'teams' | 'react' | 'email'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Status of a human task
|
|
10
|
+
*/
|
|
11
|
+
export type HumanTaskStatus = 'pending' | 'completed' | 'timeout'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Base configuration for Human Functions
|
|
15
|
+
*/
|
|
16
|
+
export interface HumanFunctionConfig {
|
|
17
|
+
/**
|
|
18
|
+
* Title of the request shown to humans
|
|
19
|
+
*/
|
|
20
|
+
title: string
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Description of the task for humans
|
|
24
|
+
*/
|
|
25
|
+
description: string
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Platform to use for human interaction
|
|
29
|
+
*/
|
|
30
|
+
platform: HumanPlatform
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Timeout in milliseconds after which the task is marked as timed out
|
|
34
|
+
*/
|
|
35
|
+
timeout?: number
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Additional platform-specific options
|
|
39
|
+
*/
|
|
40
|
+
[key: string]: any
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A request for human input
|
|
45
|
+
*/
|
|
46
|
+
export interface HumanTaskRequest {
|
|
47
|
+
/**
|
|
48
|
+
* Unique identifier for the task
|
|
49
|
+
*/
|
|
50
|
+
taskId: string
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Current status of the task
|
|
54
|
+
*/
|
|
55
|
+
status: HumanTaskStatus
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Platform-specific message ID
|
|
59
|
+
*/
|
|
60
|
+
messageId?: Record<HumanPlatform, string>
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Human Function interface with strongly-typed input and output
|
|
65
|
+
*/
|
|
66
|
+
export interface HumanFunction<TInput, TOutput> {
|
|
67
|
+
/**
|
|
68
|
+
* Request human input with the given input data
|
|
69
|
+
*/
|
|
70
|
+
request(input: TInput): Promise<HumanTaskRequest>
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get the human response for a given task
|
|
74
|
+
*/
|
|
75
|
+
getResponse(taskId: string): Promise<TOutput | null>
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Platform-specific configurations
|
|
80
|
+
*/
|
|
81
|
+
export interface PlatformConfigs {
|
|
82
|
+
slack?: SlackConfig
|
|
83
|
+
teams?: TeamsConfig
|
|
84
|
+
react?: ReactConfig
|
|
85
|
+
email?: EmailConfig
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Slack-specific configuration
|
|
90
|
+
*/
|
|
91
|
+
export interface SlackConfig {
|
|
92
|
+
/**
|
|
93
|
+
* Slack channel to send the message to
|
|
94
|
+
*/
|
|
95
|
+
channel?: string
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* User IDs to mention in the message
|
|
99
|
+
*/
|
|
100
|
+
mentions?: string[]
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Whether to use a modal dialog instead of a message
|
|
104
|
+
*/
|
|
105
|
+
modal?: boolean
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Custom Slack blocks
|
|
109
|
+
*/
|
|
110
|
+
blocks?: any[]
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Webhook URL for callbacks
|
|
114
|
+
*/
|
|
115
|
+
webhookUrl?: string
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Microsoft Teams specific configuration
|
|
120
|
+
*/
|
|
121
|
+
export interface TeamsConfig {
|
|
122
|
+
/**
|
|
123
|
+
* Teams webhook URL
|
|
124
|
+
*/
|
|
125
|
+
webhookUrl?: string
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Whether to use adaptive cards
|
|
129
|
+
*/
|
|
130
|
+
useAdaptiveCards?: boolean
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* React-specific configuration
|
|
135
|
+
*/
|
|
136
|
+
export interface ReactConfig {
|
|
137
|
+
/**
|
|
138
|
+
* Custom component styling
|
|
139
|
+
*/
|
|
140
|
+
styles?: Record<string, any>
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Theme configuration
|
|
144
|
+
*/
|
|
145
|
+
theme?: 'light' | 'dark' | 'system'
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Email-specific configuration
|
|
150
|
+
*/
|
|
151
|
+
export interface EmailConfig {
|
|
152
|
+
/**
|
|
153
|
+
* Recipients of the email
|
|
154
|
+
*/
|
|
155
|
+
to: string | string[]
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* CC recipients
|
|
159
|
+
*/
|
|
160
|
+
cc?: string | string[]
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* BCC recipients
|
|
164
|
+
*/
|
|
165
|
+
bcc?: string | string[]
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* From address
|
|
169
|
+
*/
|
|
170
|
+
from?: string
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Reply-to address
|
|
174
|
+
*/
|
|
175
|
+
replyTo?: string
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Callback URL for email responses
|
|
179
|
+
*/
|
|
180
|
+
callbackUrl?: string
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Options for creating human functions
|
|
185
|
+
*/
|
|
186
|
+
export interface CreateHumanFunctionOptions extends HumanFunctionConfig, PlatformConfigs {
|
|
187
|
+
/**
|
|
188
|
+
* Optional validation schema for the output
|
|
189
|
+
*/
|
|
190
|
+
outputSchema?: z.ZodType<any>
|
|
191
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import type { HumanFunction, HumanTaskRequest, EmailConfig } from '../../core/types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Basic Email Template component
|
|
6
|
+
*/
|
|
7
|
+
export function EmailTemplate({
|
|
8
|
+
taskId,
|
|
9
|
+
title,
|
|
10
|
+
description,
|
|
11
|
+
options,
|
|
12
|
+
callbackUrl
|
|
13
|
+
}: {
|
|
14
|
+
taskId: string
|
|
15
|
+
title: string
|
|
16
|
+
description: string
|
|
17
|
+
options?: string[] | Array<{ value: string; label: string }>
|
|
18
|
+
callbackUrl?: string
|
|
19
|
+
}) {
|
|
20
|
+
return (
|
|
21
|
+
<div>
|
|
22
|
+
<h1>{title}</h1>
|
|
23
|
+
<p>{description}</p>
|
|
24
|
+
|
|
25
|
+
{options && options.length > 0 && (
|
|
26
|
+
<div>
|
|
27
|
+
<p>Please select one of the following options:</p>
|
|
28
|
+
<ul>
|
|
29
|
+
{options.map((option, index) => {
|
|
30
|
+
const value = typeof option === 'string' ? option : option.value
|
|
31
|
+
const label = typeof option === 'string' ? option : option.label
|
|
32
|
+
const responseUrl = callbackUrl
|
|
33
|
+
? `${callbackUrl}?taskId=${taskId}&option=${encodeURIComponent(value)}`
|
|
34
|
+
: '#'
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<li key={index}>
|
|
38
|
+
<a href={responseUrl}>{label}</a>
|
|
39
|
+
</li>
|
|
40
|
+
)
|
|
41
|
+
})}
|
|
42
|
+
</ul>
|
|
43
|
+
</div>
|
|
44
|
+
)}
|
|
45
|
+
|
|
46
|
+
<p>
|
|
47
|
+
Or, you can reply to this email with your response.
|
|
48
|
+
</p>
|
|
49
|
+
</div>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Mock function to send an email
|
|
55
|
+
*/
|
|
56
|
+
export async function sendEmail(
|
|
57
|
+
config: EmailConfig & {
|
|
58
|
+
title: string
|
|
59
|
+
description: string
|
|
60
|
+
options?: string[] | Array<{ value: string; label: string }>
|
|
61
|
+
taskId: string
|
|
62
|
+
}
|
|
63
|
+
): Promise<{ messageId: string }> {
|
|
64
|
+
console.log(`Sending email to ${config.to}`)
|
|
65
|
+
console.log(`Title: ${config.title}`)
|
|
66
|
+
console.log(`Description: ${config.description}`)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
return { messageId: `email-${config.taskId}-${Date.now()}` }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get the response for an email task
|
|
74
|
+
*/
|
|
75
|
+
export async function getEmailResponse<TOutput>(taskId: string): Promise<TOutput | null> {
|
|
76
|
+
console.log(`Getting response for email task ${taskId}`)
|
|
77
|
+
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Implementation of HumanFunction for Email
|
|
83
|
+
*/
|
|
84
|
+
export class EmailHumanFunction<TInput, TOutput> implements HumanFunction<TInput, TOutput> {
|
|
85
|
+
private config: EmailConfig & {
|
|
86
|
+
title: string
|
|
87
|
+
description: string
|
|
88
|
+
options?: string[] | Array<{ value: string; label: string }>
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
constructor(config: EmailConfig & {
|
|
92
|
+
title: string
|
|
93
|
+
description: string
|
|
94
|
+
options?: string[] | Array<{ value: string; label: string }>
|
|
95
|
+
}) {
|
|
96
|
+
this.config = config
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async request(input: TInput): Promise<HumanTaskRequest> {
|
|
100
|
+
const taskId = `task-${Date.now()}`
|
|
101
|
+
|
|
102
|
+
const { messageId } = await sendEmail({
|
|
103
|
+
...this.config,
|
|
104
|
+
taskId
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
taskId,
|
|
109
|
+
status: 'pending',
|
|
110
|
+
messageId: {
|
|
111
|
+
slack: '',
|
|
112
|
+
teams: '',
|
|
113
|
+
react: '',
|
|
114
|
+
email: messageId
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async getResponse(taskId: string): Promise<TOutput | null> {
|
|
120
|
+
return getEmailResponse<TOutput>(taskId)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get a React component for this email template
|
|
125
|
+
*/
|
|
126
|
+
getEmailComponent(taskId: string): React.ReactNode {
|
|
127
|
+
return (
|
|
128
|
+
<EmailTemplate
|
|
129
|
+
taskId={taskId}
|
|
130
|
+
title={this.config.title}
|
|
131
|
+
description={this.config.description}
|
|
132
|
+
options={this.config.options}
|
|
133
|
+
callbackUrl={this.config.callbackUrl}
|
|
134
|
+
/>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
}
|