kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
feat: add initial Slack and Twilio human feedback classes
rodzic
25d285b290
commit
721f34f3f1
|
@ -6,28 +6,12 @@ import select from '@inquirer/select'
|
||||||
import { Agentic } from '@/agentic'
|
import { Agentic } from '@/agentic'
|
||||||
import { TaskResponseMetadata } from '@/types'
|
import { TaskResponseMetadata } from '@/types'
|
||||||
|
|
||||||
import { HumanFeedbackMechanism, HumanFeedbackOptions } from './index'
|
import {
|
||||||
|
HumanFeedbackMechanism,
|
||||||
/**
|
HumanFeedbackOptions,
|
||||||
* Actions the user can take in the feedback selection prompt.
|
UserActionMessages,
|
||||||
*/
|
UserActions
|
||||||
export const UserActions = {
|
} from './feedback'
|
||||||
Accept: 'accept',
|
|
||||||
Edit: 'edit',
|
|
||||||
Decline: 'decline',
|
|
||||||
Select: 'select',
|
|
||||||
Exit: 'exit'
|
|
||||||
} as const
|
|
||||||
|
|
||||||
export type UserActions = (typeof UserActions)[keyof typeof UserActions]
|
|
||||||
|
|
||||||
const UserActionMessages: Record<UserActions, string> = {
|
|
||||||
[UserActions.Accept]: 'Accept the output',
|
|
||||||
[UserActions.Edit]: 'Edit the output (open in editor)',
|
|
||||||
[UserActions.Decline]: 'Decline the output',
|
|
||||||
[UserActions.Select]: 'Select outputs to keep',
|
|
||||||
[UserActions.Exit]: 'Exit'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prompt the user to select one of a list of options.
|
* Prompt the user to select one of a list of options.
|
||||||
|
|
|
@ -0,0 +1,156 @@
|
||||||
|
import { Agentic } from '@/agentic'
|
||||||
|
import { BaseTask } from '@/task'
|
||||||
|
import { TaskResponseMetadata } from '@/types'
|
||||||
|
|
||||||
|
import { HumanFeedbackMechanismCLI } from './cli'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actions the user can take in the feedback selection prompt.
|
||||||
|
*/
|
||||||
|
export const UserActions = {
|
||||||
|
Accept: 'accept',
|
||||||
|
Edit: 'edit',
|
||||||
|
Decline: 'decline',
|
||||||
|
Select: 'select',
|
||||||
|
Exit: 'exit'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type UserActions = (typeof UserActions)[keyof typeof UserActions]
|
||||||
|
|
||||||
|
export const UserActionMessages: Record<UserActions, string> = {
|
||||||
|
[UserActions.Accept]: 'Accept the output',
|
||||||
|
[UserActions.Edit]: 'Edit the output',
|
||||||
|
[UserActions.Decline]: 'Decline the output',
|
||||||
|
[UserActions.Select]: 'Select outputs to keep',
|
||||||
|
[UserActions.Exit]: 'Exit'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Available types of human feedback.
|
||||||
|
*/
|
||||||
|
export type HumanFeedbackType = 'confirm' | 'selectOne' | 'selectN'
|
||||||
|
|
||||||
|
type HumanFeedbackMechanismConstructor<T extends HumanFeedbackMechanism> = new (
|
||||||
|
...args: any[]
|
||||||
|
) => T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for human feedback.
|
||||||
|
*/
|
||||||
|
export type HumanFeedbackOptions<
|
||||||
|
T extends HumanFeedbackMechanism = HumanFeedbackMechanism
|
||||||
|
> = {
|
||||||
|
/**
|
||||||
|
* What type of feedback to request.
|
||||||
|
*/
|
||||||
|
type?: HumanFeedbackType
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the user can bail out of the feedback loop.
|
||||||
|
*/
|
||||||
|
bail?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the user can edit the output.
|
||||||
|
*/
|
||||||
|
editing?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the user can add free-form text annotations.
|
||||||
|
*/
|
||||||
|
annotations?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The human feedback mechanism to use for this task.
|
||||||
|
*/
|
||||||
|
mechanism?: HumanFeedbackMechanismConstructor<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class HumanFeedbackMechanism {
|
||||||
|
protected _agentic: Agentic
|
||||||
|
|
||||||
|
protected _options: HumanFeedbackOptions
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
agentic,
|
||||||
|
options
|
||||||
|
}: {
|
||||||
|
agentic: Agentic
|
||||||
|
options: HumanFeedbackOptions
|
||||||
|
}) {
|
||||||
|
this._agentic = agentic
|
||||||
|
this._options = options
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract confirm(
|
||||||
|
response: any,
|
||||||
|
metadata: TaskResponseMetadata
|
||||||
|
): Promise<void>
|
||||||
|
public abstract selectOne(
|
||||||
|
response: any,
|
||||||
|
metadata: TaskResponseMetadata
|
||||||
|
): Promise<void>
|
||||||
|
public abstract selectN(
|
||||||
|
response: any,
|
||||||
|
metadata: TaskResponseMetadata
|
||||||
|
): Promise<void>
|
||||||
|
|
||||||
|
public async interact(response: any, metadata: TaskResponseMetadata) {
|
||||||
|
if (this._options.type === 'selectN') {
|
||||||
|
await this.selectN(response, metadata)
|
||||||
|
} else if (this._options.type === 'confirm') {
|
||||||
|
await this.confirm(response, metadata)
|
||||||
|
} else if (this._options.type === 'selectOne') {
|
||||||
|
await this.selectOne(response, metadata)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withHumanFeedback<T, U>(
|
||||||
|
task: BaseTask<T, U>,
|
||||||
|
options: HumanFeedbackOptions = {}
|
||||||
|
) {
|
||||||
|
task = task.clone()
|
||||||
|
|
||||||
|
// Default options defined at the instance level
|
||||||
|
const instanceDefaults = task.agentic.humanFeedbackDefaults
|
||||||
|
|
||||||
|
// Use Object.assign to merge the options, instance defaults, and hard-coded defaults
|
||||||
|
const finalOptions: HumanFeedbackOptions = Object.assign(
|
||||||
|
{
|
||||||
|
type: 'confirm',
|
||||||
|
bail: false,
|
||||||
|
editing: false,
|
||||||
|
annotations: false,
|
||||||
|
mechanism: HumanFeedbackMechanismCLI
|
||||||
|
},
|
||||||
|
// Defaults from the instance:
|
||||||
|
instanceDefaults,
|
||||||
|
// User-provided options (override instance defaults):
|
||||||
|
options
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!finalOptions.mechanism) {
|
||||||
|
throw new Error(
|
||||||
|
'No feedback mechanism provided. Please provide a feedback mechanism to use.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const feedbackMechanism = new finalOptions.mechanism({
|
||||||
|
agentic: task.agentic,
|
||||||
|
options: finalOptions
|
||||||
|
})
|
||||||
|
|
||||||
|
const originalCall = task.callWithMetadata.bind(task)
|
||||||
|
|
||||||
|
task.callWithMetadata = async function (input?: T) {
|
||||||
|
const response = await originalCall(input)
|
||||||
|
|
||||||
|
// Process the response and add feedback to metadata
|
||||||
|
await feedbackMechanism.interact(response.result, response.metadata)
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
return task
|
||||||
|
}
|
|
@ -1,135 +1,4 @@
|
||||||
import { Agentic } from '@/agentic'
|
export * from './cli'
|
||||||
import { BaseTask } from '@/task'
|
export * from './feedback'
|
||||||
import { TaskResponseMetadata } from '@/types'
|
export * from './slack'
|
||||||
|
export * from './twilio'
|
||||||
import { HumanFeedbackMechanismCLI } from './cli'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Available types of human feedback.
|
|
||||||
*/
|
|
||||||
export type HumanFeedbackType = 'confirm' | 'selectOne' | 'selectN'
|
|
||||||
|
|
||||||
type HumanFeedbackMechanismConstructor<T extends HumanFeedbackMechanism> = new (
|
|
||||||
...args: any[]
|
|
||||||
) => T
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Options for human feedback.
|
|
||||||
*/
|
|
||||||
export type HumanFeedbackOptions<
|
|
||||||
T extends HumanFeedbackMechanism = HumanFeedbackMechanism
|
|
||||||
> = {
|
|
||||||
/**
|
|
||||||
* What type of feedback to request.
|
|
||||||
*/
|
|
||||||
type?: HumanFeedbackType
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the user can bail out of the feedback loop.
|
|
||||||
*/
|
|
||||||
bail?: boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the user can edit the output.
|
|
||||||
*/
|
|
||||||
editing?: boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the user can add free-form text annotations.
|
|
||||||
*/
|
|
||||||
annotations?: boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The human feedback mechanism to use for this task.
|
|
||||||
*/
|
|
||||||
mechanism?: HumanFeedbackMechanismConstructor<T>
|
|
||||||
}
|
|
||||||
|
|
||||||
export abstract class HumanFeedbackMechanism {
|
|
||||||
protected _agentic: Agentic
|
|
||||||
|
|
||||||
protected _options: HumanFeedbackOptions
|
|
||||||
|
|
||||||
constructor({
|
|
||||||
agentic,
|
|
||||||
options
|
|
||||||
}: {
|
|
||||||
agentic: Agentic
|
|
||||||
options: HumanFeedbackOptions
|
|
||||||
}) {
|
|
||||||
this._agentic = agentic
|
|
||||||
this._options = options
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract confirm(
|
|
||||||
response: any,
|
|
||||||
metadata: TaskResponseMetadata
|
|
||||||
): Promise<void>
|
|
||||||
public abstract selectOne(
|
|
||||||
response: any,
|
|
||||||
metadata: TaskResponseMetadata
|
|
||||||
): Promise<void>
|
|
||||||
public abstract selectN(
|
|
||||||
response: any,
|
|
||||||
metadata: TaskResponseMetadata
|
|
||||||
): Promise<void>
|
|
||||||
|
|
||||||
public async interact(response: any, metadata: TaskResponseMetadata) {
|
|
||||||
if (this._options.type === 'selectN') {
|
|
||||||
await this.selectN(response, metadata)
|
|
||||||
} else if (this._options.type === 'confirm') {
|
|
||||||
await this.confirm(response, metadata)
|
|
||||||
} else if (this._options.type === 'selectOne') {
|
|
||||||
await this.selectOne(response, metadata)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function withHumanFeedback<T, U>(
|
|
||||||
task: BaseTask<T, U>,
|
|
||||||
options: HumanFeedbackOptions = {}
|
|
||||||
) {
|
|
||||||
task = task.clone()
|
|
||||||
|
|
||||||
// Default options defined at the instance level
|
|
||||||
const instanceDefaults = task.agentic.humanFeedbackDefaults
|
|
||||||
|
|
||||||
// Use Object.assign to merge the options, instance defaults, and hard-coded defaults
|
|
||||||
const finalOptions: HumanFeedbackOptions = Object.assign(
|
|
||||||
{
|
|
||||||
type: 'confirm',
|
|
||||||
bail: false,
|
|
||||||
editing: false,
|
|
||||||
annotations: false,
|
|
||||||
mechanism: HumanFeedbackMechanismCLI
|
|
||||||
},
|
|
||||||
// Defaults from the instance:
|
|
||||||
instanceDefaults,
|
|
||||||
// User-provided options (override instance defaults):
|
|
||||||
options
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!finalOptions.mechanism) {
|
|
||||||
throw new Error(
|
|
||||||
'No feedback mechanism provided. Please provide a feedback mechanism to use.'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const feedbackMechanism = new finalOptions.mechanism({
|
|
||||||
agentic: task.agentic,
|
|
||||||
options: finalOptions
|
|
||||||
})
|
|
||||||
|
|
||||||
const originalCall = task.callWithMetadata.bind(task)
|
|
||||||
|
|
||||||
task.callWithMetadata = async function (input?: T) {
|
|
||||||
const response = await originalCall(input)
|
|
||||||
|
|
||||||
// Process the response and add feedback to metadata
|
|
||||||
await feedbackMechanism.interact(response.result, response.metadata)
|
|
||||||
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
return task
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,286 @@
|
||||||
|
import { Agentic } from '@/agentic'
|
||||||
|
import { SlackClient } from '@/services/slack'
|
||||||
|
import { TaskResponseMetadata } from '@/types'
|
||||||
|
|
||||||
|
import {
|
||||||
|
HumanFeedbackMechanism,
|
||||||
|
HumanFeedbackOptions,
|
||||||
|
UserActionMessages,
|
||||||
|
UserActions
|
||||||
|
} from './feedback'
|
||||||
|
|
||||||
|
export class HumanFeedbackMechanismSlack extends HumanFeedbackMechanism {
|
||||||
|
private slackClient: SlackClient
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
agentic,
|
||||||
|
options
|
||||||
|
}: {
|
||||||
|
agentic: Agentic
|
||||||
|
options: HumanFeedbackOptions
|
||||||
|
}) {
|
||||||
|
super({ agentic, options })
|
||||||
|
this.slackClient = new SlackClient()
|
||||||
|
}
|
||||||
|
|
||||||
|
private async askUser(
|
||||||
|
message: string,
|
||||||
|
choices: UserActions[]
|
||||||
|
): Promise<UserActions> {
|
||||||
|
message += '\n\n'
|
||||||
|
message += choices
|
||||||
|
.map((choice, idx) => `*${idx}* - ${UserActionMessages[choice]}`)
|
||||||
|
.join('\n')
|
||||||
|
message += '\n\n'
|
||||||
|
message += 'Reply with the number of your choice.'
|
||||||
|
const response = await this.slackClient.sendAndWaitForReply({
|
||||||
|
text: message,
|
||||||
|
validate: (slackMessage) => {
|
||||||
|
const choice = parseInt(slackMessage.text)
|
||||||
|
return !isNaN(choice) && choice >= 0 && choice < choices.length
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return choices[parseInt(response.text)]
|
||||||
|
}
|
||||||
|
|
||||||
|
public async confirm(
|
||||||
|
response: any,
|
||||||
|
metadata: TaskResponseMetadata
|
||||||
|
): Promise<void> {
|
||||||
|
const stringified = JSON.stringify(response, null, 2)
|
||||||
|
const msg = [
|
||||||
|
'The following output was generated:',
|
||||||
|
'```',
|
||||||
|
stringified,
|
||||||
|
'```',
|
||||||
|
'What would you like to do?'
|
||||||
|
].join('\n')
|
||||||
|
|
||||||
|
const choices: UserActions[] = [UserActions.Accept, UserActions.Decline]
|
||||||
|
|
||||||
|
if (this._options.editing) {
|
||||||
|
choices.push(UserActions.Edit)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._options.bail) {
|
||||||
|
choices.push(UserActions.Exit)
|
||||||
|
}
|
||||||
|
|
||||||
|
const feedback = await this.askUser(msg, choices)
|
||||||
|
|
||||||
|
metadata.feedback = {}
|
||||||
|
|
||||||
|
switch (feedback) {
|
||||||
|
case UserActions.Accept:
|
||||||
|
metadata.feedback.accepted = true
|
||||||
|
break
|
||||||
|
|
||||||
|
case UserActions.Edit: {
|
||||||
|
let { text: editedOutput } = await this.slackClient.sendAndWaitForReply(
|
||||||
|
{
|
||||||
|
text: 'Copy and edit the output:'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
editedOutput = editedOutput.replace(/```$/g, '')
|
||||||
|
editedOutput = editedOutput.replace(/^```/g, '')
|
||||||
|
metadata.feedback.editedOutput = editedOutput
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case UserActions.Decline:
|
||||||
|
metadata.feedback.accepted = false
|
||||||
|
break
|
||||||
|
|
||||||
|
case UserActions.Exit:
|
||||||
|
throw new Error('Exiting...')
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unexpected feedback: ${feedback}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._options.annotations) {
|
||||||
|
try {
|
||||||
|
const annotation = await this.slackClient.sendAndWaitForReply({
|
||||||
|
text: 'Please leave an annotation (optional):'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (annotation) {
|
||||||
|
metadata.feedback.annotation = annotation.text
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Deliberately swallow the error here as the user is not required to leave an annotation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async selectOne(
|
||||||
|
response: any[],
|
||||||
|
metadata: TaskResponseMetadata
|
||||||
|
): Promise<void> {
|
||||||
|
const stringified = JSON.stringify(response, null, 2)
|
||||||
|
const msg = [
|
||||||
|
'The following output was generated:',
|
||||||
|
'```',
|
||||||
|
stringified,
|
||||||
|
'```',
|
||||||
|
'What would you like to do?'
|
||||||
|
].join('\n')
|
||||||
|
|
||||||
|
const choices: UserActions[] = [UserActions.Select]
|
||||||
|
|
||||||
|
if (this._options.editing) {
|
||||||
|
choices.push(UserActions.Edit)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._options.bail) {
|
||||||
|
choices.push(UserActions.Exit)
|
||||||
|
}
|
||||||
|
|
||||||
|
const feedback =
|
||||||
|
choices.length === 1
|
||||||
|
? UserActions.Select
|
||||||
|
: await this.askUser(msg, choices)
|
||||||
|
|
||||||
|
metadata.feedback = {}
|
||||||
|
|
||||||
|
switch (feedback) {
|
||||||
|
case UserActions.Edit: {
|
||||||
|
let { text: editedOutput } = await this.slackClient.sendAndWaitForReply(
|
||||||
|
{
|
||||||
|
text: 'Copy and edit the output:'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
editedOutput = editedOutput.replace(/```$/g, '')
|
||||||
|
editedOutput = editedOutput.replace(/^```/g, '')
|
||||||
|
metadata.feedback.editedOutput = editedOutput
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case UserActions.Select: {
|
||||||
|
const { text: selectedOutput } =
|
||||||
|
await this.slackClient.sendAndWaitForReply({
|
||||||
|
text:
|
||||||
|
'Pick one output:' +
|
||||||
|
response.map((r, idx) => `\n*${idx}* - ${r}`).join('') +
|
||||||
|
'\n\nReply with the number of your choice.',
|
||||||
|
validate: (slackMessage) => {
|
||||||
|
const choice = parseInt(slackMessage.text)
|
||||||
|
return !isNaN(choice) && choice >= 0 && choice < response.length
|
||||||
|
}
|
||||||
|
})
|
||||||
|
metadata.feedback.chosen = response[parseInt(selectedOutput)]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case UserActions.Exit:
|
||||||
|
throw new Error('Exiting...')
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unexpected feedback: ${feedback}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._options.annotations) {
|
||||||
|
try {
|
||||||
|
const annotation = await this.slackClient.sendAndWaitForReply({
|
||||||
|
text: 'Please leave an annotation (optional):'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (annotation) {
|
||||||
|
metadata.feedback.annotation = annotation.text
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Deliberately swallow the error here as the user is not required to leave an annotation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async selectN(
|
||||||
|
response: any[],
|
||||||
|
metadata: TaskResponseMetadata
|
||||||
|
): Promise<void> {
|
||||||
|
const stringified = JSON.stringify(response, null, 2)
|
||||||
|
const msg = [
|
||||||
|
'The following output was generated:',
|
||||||
|
stringified,
|
||||||
|
'What would you like to do?'
|
||||||
|
].join('\n')
|
||||||
|
const choices: UserActions[] = [UserActions.Select]
|
||||||
|
|
||||||
|
if (this._options.editing) {
|
||||||
|
choices.push(UserActions.Edit)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._options.bail) {
|
||||||
|
choices.push(UserActions.Exit)
|
||||||
|
}
|
||||||
|
|
||||||
|
const feedback =
|
||||||
|
choices.length === 1
|
||||||
|
? UserActions.Select
|
||||||
|
: await this.askUser(msg, choices)
|
||||||
|
|
||||||
|
metadata.feedback = {}
|
||||||
|
|
||||||
|
switch (feedback) {
|
||||||
|
case UserActions.Edit: {
|
||||||
|
let { text: editedOutput } = await this.slackClient.sendAndWaitForReply(
|
||||||
|
{
|
||||||
|
text: 'Copy and edit the output:'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
editedOutput = editedOutput.replace(/```$/g, '')
|
||||||
|
editedOutput = editedOutput.replace(/^```/g, '')
|
||||||
|
metadata.feedback.editedOutput = editedOutput
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case UserActions.Select: {
|
||||||
|
const { text: selectedOutput } =
|
||||||
|
await this.slackClient.sendAndWaitForReply({
|
||||||
|
text:
|
||||||
|
'Select outputs:' +
|
||||||
|
response.map((r, idx) => `\n*${idx}* - ${r}`).join('') +
|
||||||
|
'\n\nReply with a comma-separated list of the output numbers of your choice.',
|
||||||
|
validate: (slackMessage) => {
|
||||||
|
const choices = slackMessage.text.split(',')
|
||||||
|
return choices.every((choice) => {
|
||||||
|
const choiceInt = parseInt(choice)
|
||||||
|
return (
|
||||||
|
!isNaN(choiceInt) &&
|
||||||
|
choiceInt >= 0 &&
|
||||||
|
choiceInt < response.length
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const chosenOutputs = selectedOutput
|
||||||
|
.split(',')
|
||||||
|
.map((choice) => parseInt(choice))
|
||||||
|
metadata.feedback.selected = response.filter((_, idx) => {
|
||||||
|
return chosenOutputs.includes(idx)
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case UserActions.Exit:
|
||||||
|
throw new Error('Exiting...')
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unexpected feedback: ${feedback}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._options.annotations) {
|
||||||
|
try {
|
||||||
|
const annotation = await this.slackClient.sendAndWaitForReply({
|
||||||
|
text: 'Please leave an annotation (optional):'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (annotation) {
|
||||||
|
metadata.feedback.annotation = annotation.text
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Deliberately swallow the error here as the user is not required to leave an annotation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,294 @@
|
||||||
|
import { Agentic } from '@/agentic'
|
||||||
|
import { TwilioConversationClient } from '@/services/twilio-conversation'
|
||||||
|
import { TaskResponseMetadata } from '@/types'
|
||||||
|
|
||||||
|
import {
|
||||||
|
HumanFeedbackMechanism,
|
||||||
|
HumanFeedbackOptions,
|
||||||
|
UserActionMessages,
|
||||||
|
UserActions
|
||||||
|
} from './feedback'
|
||||||
|
|
||||||
|
export class HumanFeedbackMechanismTwilio extends HumanFeedbackMechanism {
|
||||||
|
private twilioClient: TwilioConversationClient
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
agentic,
|
||||||
|
options
|
||||||
|
}: {
|
||||||
|
agentic: Agentic
|
||||||
|
options: HumanFeedbackOptions
|
||||||
|
}) {
|
||||||
|
super({ agentic, options })
|
||||||
|
this.twilioClient = new TwilioConversationClient()
|
||||||
|
}
|
||||||
|
|
||||||
|
private async askUser(
|
||||||
|
message: string,
|
||||||
|
choices: UserActions[]
|
||||||
|
): Promise<UserActions> {
|
||||||
|
message += '\n\n'
|
||||||
|
message += choices
|
||||||
|
.map((choice, idx) => `*${idx}* - ${UserActionMessages[choice]}`)
|
||||||
|
.join('\n')
|
||||||
|
message += '\n\n'
|
||||||
|
message += 'Reply with the number of your choice.'
|
||||||
|
const response = await this.twilioClient.sendAndWaitForReply({
|
||||||
|
name: 'human-feedback-ask',
|
||||||
|
text: message,
|
||||||
|
validate: (message) => {
|
||||||
|
const choice = parseInt(message.body)
|
||||||
|
return !isNaN(choice) && choice >= 0 && choice < choices.length
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return choices[parseInt(response.body)]
|
||||||
|
}
|
||||||
|
|
||||||
|
public async confirm(
|
||||||
|
response: any,
|
||||||
|
metadata: TaskResponseMetadata
|
||||||
|
): Promise<void> {
|
||||||
|
const stringified = JSON.stringify(response, null, 2)
|
||||||
|
const msg = [
|
||||||
|
'The following output was generated:',
|
||||||
|
'',
|
||||||
|
stringified,
|
||||||
|
'',
|
||||||
|
'What would you like to do?'
|
||||||
|
].join('\n')
|
||||||
|
|
||||||
|
const choices: UserActions[] = [UserActions.Accept, UserActions.Decline]
|
||||||
|
|
||||||
|
if (this._options.editing) {
|
||||||
|
choices.push(UserActions.Edit)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._options.bail) {
|
||||||
|
choices.push(UserActions.Exit)
|
||||||
|
}
|
||||||
|
|
||||||
|
const feedback = await this.askUser(msg, choices)
|
||||||
|
|
||||||
|
metadata.feedback = {}
|
||||||
|
|
||||||
|
switch (feedback) {
|
||||||
|
case UserActions.Accept:
|
||||||
|
metadata.feedback.accepted = true
|
||||||
|
break
|
||||||
|
|
||||||
|
case UserActions.Edit: {
|
||||||
|
let { body: editedOutput } =
|
||||||
|
await this.twilioClient.sendAndWaitForReply({
|
||||||
|
name: 'human-feedback-edit',
|
||||||
|
text: 'Copy and edit the output:'
|
||||||
|
})
|
||||||
|
editedOutput = editedOutput.replace(/```$/g, '')
|
||||||
|
editedOutput = editedOutput.replace(/^```/g, '')
|
||||||
|
metadata.feedback.editedOutput = editedOutput
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case UserActions.Decline:
|
||||||
|
metadata.feedback.accepted = false
|
||||||
|
break
|
||||||
|
|
||||||
|
case UserActions.Exit:
|
||||||
|
throw new Error('Exiting...')
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unexpected feedback: ${feedback}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._options.annotations) {
|
||||||
|
try {
|
||||||
|
const annotation = await this.twilioClient.sendAndWaitForReply({
|
||||||
|
name: 'human-feedback-annotation',
|
||||||
|
text: 'Please leave an annotation (optional):'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (annotation) {
|
||||||
|
metadata.feedback.annotation = annotation.body
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Deliberately swallow the error here as the user is not required to leave an annotation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async selectOne(
|
||||||
|
response: any[],
|
||||||
|
metadata: TaskResponseMetadata
|
||||||
|
): Promise<void> {
|
||||||
|
const stringified = JSON.stringify(response, null, 2)
|
||||||
|
const msg = [
|
||||||
|
'The following output was generated:',
|
||||||
|
'',
|
||||||
|
stringified,
|
||||||
|
'',
|
||||||
|
'What would you like to do?'
|
||||||
|
].join('\n')
|
||||||
|
|
||||||
|
const choices: UserActions[] = [UserActions.Select]
|
||||||
|
|
||||||
|
if (this._options.editing) {
|
||||||
|
choices.push(UserActions.Edit)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._options.bail) {
|
||||||
|
choices.push(UserActions.Exit)
|
||||||
|
}
|
||||||
|
|
||||||
|
const feedback =
|
||||||
|
choices.length === 1
|
||||||
|
? UserActions.Select
|
||||||
|
: await this.askUser(msg, choices)
|
||||||
|
|
||||||
|
metadata.feedback = {}
|
||||||
|
|
||||||
|
switch (feedback) {
|
||||||
|
case UserActions.Edit: {
|
||||||
|
let { body: editedOutput } =
|
||||||
|
await this.twilioClient.sendAndWaitForReply({
|
||||||
|
text: 'Copy and edit the output:',
|
||||||
|
name: 'human-feedback-edit'
|
||||||
|
})
|
||||||
|
editedOutput = editedOutput.replace(/```$/g, '')
|
||||||
|
editedOutput = editedOutput.replace(/^```/g, '')
|
||||||
|
metadata.feedback.editedOutput = editedOutput
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case UserActions.Select: {
|
||||||
|
const { body: selectedOutput } =
|
||||||
|
await this.twilioClient.sendAndWaitForReply({
|
||||||
|
name: 'human-feedback-select',
|
||||||
|
text:
|
||||||
|
'Pick one output:' +
|
||||||
|
response.map((r, idx) => `\n${idx} - ${r}`).join('') +
|
||||||
|
'\n\nReply with the number of your choice.',
|
||||||
|
validate: (message) => {
|
||||||
|
const choice = parseInt(message.body)
|
||||||
|
return !isNaN(choice) && choice >= 0 && choice < response.length
|
||||||
|
}
|
||||||
|
})
|
||||||
|
metadata.feedback.chosen = response[parseInt(selectedOutput)]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case UserActions.Exit:
|
||||||
|
throw new Error('Exiting...')
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unexpected feedback: ${feedback}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._options.annotations) {
|
||||||
|
try {
|
||||||
|
const annotation = await this.twilioClient.sendAndWaitForReply({
|
||||||
|
text: 'Please leave an annotation (optional):',
|
||||||
|
name: 'human-feedback-annotation'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (annotation) {
|
||||||
|
metadata.feedback.annotation = annotation.body
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Deliberately swallow the error here as the user is not required to leave an annotation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async selectN(
|
||||||
|
response: any[],
|
||||||
|
metadata: TaskResponseMetadata
|
||||||
|
): Promise<void> {
|
||||||
|
const stringified = JSON.stringify(response, null, 2)
|
||||||
|
const msg = [
|
||||||
|
'The following output was generated:',
|
||||||
|
'',
|
||||||
|
stringified,
|
||||||
|
'',
|
||||||
|
'What would you like to do?'
|
||||||
|
].join('\n')
|
||||||
|
const choices: UserActions[] = [UserActions.Select]
|
||||||
|
|
||||||
|
if (this._options.editing) {
|
||||||
|
choices.push(UserActions.Edit)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._options.bail) {
|
||||||
|
choices.push(UserActions.Exit)
|
||||||
|
}
|
||||||
|
|
||||||
|
const feedback =
|
||||||
|
choices.length === 1
|
||||||
|
? UserActions.Select
|
||||||
|
: await this.askUser(msg, choices)
|
||||||
|
|
||||||
|
metadata.feedback = {}
|
||||||
|
|
||||||
|
switch (feedback) {
|
||||||
|
case UserActions.Edit: {
|
||||||
|
let { body: editedOutput } =
|
||||||
|
await this.twilioClient.sendAndWaitForReply({
|
||||||
|
text: 'Copy and edit the output:',
|
||||||
|
name: 'human-feedback-edit'
|
||||||
|
})
|
||||||
|
editedOutput = editedOutput.replace(/```$/g, '')
|
||||||
|
editedOutput = editedOutput.replace(/^```/g, '')
|
||||||
|
metadata.feedback.editedOutput = editedOutput
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case UserActions.Select: {
|
||||||
|
const { body: selectedOutput } =
|
||||||
|
await this.twilioClient.sendAndWaitForReply({
|
||||||
|
name: 'human-feedback-select',
|
||||||
|
text:
|
||||||
|
'Select outputs:' +
|
||||||
|
response.map((r, idx) => `\n${idx} - ${r}`).join('') +
|
||||||
|
'\n\nReply with a comma-separated list of the output numbers of your choice.',
|
||||||
|
validate: (message) => {
|
||||||
|
const choices = message.body.split(',')
|
||||||
|
return choices.every((choice) => {
|
||||||
|
const choiceInt = parseInt(choice)
|
||||||
|
return (
|
||||||
|
!isNaN(choiceInt) &&
|
||||||
|
choiceInt >= 0 &&
|
||||||
|
choiceInt < response.length
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const chosenOutputs = selectedOutput
|
||||||
|
.split(',')
|
||||||
|
.map((choice) => parseInt(choice))
|
||||||
|
metadata.feedback.selected = response.filter((_, idx) => {
|
||||||
|
return chosenOutputs.includes(idx)
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case UserActions.Exit:
|
||||||
|
throw new Error('Exiting...')
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unexpected feedback: ${feedback}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._options.annotations) {
|
||||||
|
try {
|
||||||
|
const annotation = await this.twilioClient.sendAndWaitForReply({
|
||||||
|
text: 'Please leave an annotation (optional):',
|
||||||
|
name: 'human-feedback-annotation'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (annotation) {
|
||||||
|
metadata.feedback.annotation = annotation.body
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Deliberately swallow the error here as the user is not required to leave an annotation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Ładowanie…
Reference in New Issue