refactor: avoid code duplication

old-agentic-v1^2
Philipp Burckhardt 2023-06-13 16:32:58 -04:00 zatwierdzone przez Travis Fischer
rodzic 768a727597
commit 9c41ea5607
4 zmienionych plików z 216 dodań i 610 usunięć

Wyświetl plik

@ -4,7 +4,6 @@ import input from '@inquirer/input'
import select from '@inquirer/select' import select from '@inquirer/select'
import { Agentic } from '@/agentic' import { Agentic } from '@/agentic'
import { TaskResponseMetadata } from '@/types'
import { import {
HumanFeedbackMechanism, HumanFeedbackMechanism,
@ -13,22 +12,6 @@ import {
UserActions UserActions
} from './feedback' } from './feedback'
/**
* Prompt the user to select one of a list of options.
*/
async function askUser(
message: string,
choices: UserActions[]
): Promise<UserActions> {
return select({
message,
choices: choices.map((choice) => ({
name: UserActionMessages[choice],
value: choice
}))
})
}
export class HumanFeedbackMechanismCLI extends HumanFeedbackMechanism { export class HumanFeedbackMechanismCLI extends HumanFeedbackMechanism {
constructor({ constructor({
agentic, agentic,
@ -42,172 +25,49 @@ export class HumanFeedbackMechanismCLI extends HumanFeedbackMechanism {
this._options = options this._options = options
} }
protected async annotate( /**
response: any, * Prompt the user to select one of a list of options.
metadata: TaskResponseMetadata */
): Promise<void> { protected async askUser(
const annotation = await input({ message: string,
choices: UserActions[]
): Promise<UserActions> {
return select({
message,
choices: choices.map((choice) => ({
name: UserActionMessages[choice],
value: choice
}))
})
}
protected async edit(output: string): Promise<string> {
return editor({
message: 'Edit the output:',
default: output
})
}
protected async annotate(): Promise<string> {
return input({
message: message:
'Please leave an annotation (leave blank to skip; press enter to submit):' 'Please leave an annotation (leave blank to skip; press enter to submit):'
}) })
if (annotation) {
metadata.feedback.annotation = annotation
}
} }
protected async confirm( protected async selectOne(response: any[]): Promise<void> {
response: any, const choices = response.map((option) => ({
metadata: TaskResponseMetadata name: option,
): Promise<void> { value: option
const stringified = JSON.stringify(response, null, 2) }))
const msg = [ return select({ message: 'Pick one output:', choices })
'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 askUser(msg, choices)
metadata.feedback = {}
switch (feedback) {
case UserActions.Accept:
metadata.feedback.accepted = true
break
case UserActions.Edit: {
const editedOutput = await editor({
message: 'Edit the output:',
default: stringified
})
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}`)
}
} }
protected async selectOne( protected async selectN(response: any[]): Promise<any[]> {
response: any[], const choices = response.map((option) => ({
metadata: TaskResponseMetadata name: option,
): Promise<void> { value: option
const stringified = JSON.stringify(response, null, 2) }))
const msg = [ return checkbox({ message: 'Select outputs:', choices })
'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 = await askUser(msg, choices)
metadata.feedback = {}
switch (feedback) {
case UserActions.Edit: {
const editedOutput = await editor({
message: 'Edit the output:',
default: stringified
})
metadata.feedback.editedOutput = editedOutput
break
}
case UserActions.Select: {
const choices = response.map((option) => ({
name: option,
value: option
}))
const chosen = await select({ message: 'Pick one output:', choices })
metadata.feedback.chosen = chosen
break
}
case UserActions.Exit:
throw new Error('Exiting...')
default:
throw new Error(`Unexpected feedback: ${feedback}`)
}
}
protected 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 askUser(msg, choices)
metadata.feedback = {}
switch (feedback) {
case UserActions.Edit: {
const editedOutput = await editor({
message: 'Edit the output:',
default: stringified
})
metadata.feedback.editedOutput = editedOutput
break
}
case UserActions.Select: {
const choices = response.map((option) => ({
name: option,
value: option
}))
const chosen = await checkbox({ message: 'Select outputs:', choices })
metadata.feedback.selected = chosen
break
}
case UserActions.Exit:
throw new Error('Exiting...')
default:
throw new Error(`Unexpected feedback: ${feedback}`)
}
} }
} }

Wyświetl plik

@ -82,35 +82,92 @@ export abstract class HumanFeedbackMechanism {
this._options = options this._options = options
} }
protected abstract confirm( protected abstract selectOne(response: any): Promise<any>
response: any,
metadata: TaskResponseMetadata
): Promise<void>
protected abstract selectOne(
response: any,
metadata: TaskResponseMetadata
): Promise<void>
protected abstract selectN(
response: any,
metadata: TaskResponseMetadata
): Promise<void>
protected abstract annotate( protected abstract selectN(response: any): Promise<any>
response: any,
metadata: TaskResponseMetadata protected abstract annotate(): Promise<string>
): Promise<void>
protected abstract edit(output: string): Promise<string>
protected abstract askUser(
message: string,
choices: UserActions[]
): Promise<UserActions>
public async interact(response: any, metadata: TaskResponseMetadata) { public async interact(response: any, metadata: TaskResponseMetadata) {
if (this._options.type === 'selectN') { const stringified = JSON.stringify(response, null, 2)
await this.selectN(response, metadata) const msg = [
} else if (this._options.type === 'confirm') { 'The following output was generated:',
await this.confirm(response, metadata) '```',
} else if (this._options.type === 'selectOne') { stringified,
await this.selectOne(response, metadata) '```',
'What would you like to do?'
].join('\n')
const choices: UserActions[] = []
if (
this._options.type === 'selectN' ||
this._options.type === 'selectOne'
) {
choices.push(UserActions.Select)
} else {
// Case: confirm
choices.push(UserActions.Accept)
choices.push(UserActions.Decline)
}
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.Accept:
metadata.feedback.accepted = true
break
case UserActions.Edit: {
const editedOutput = await this.edit(stringified)
metadata.feedback.editedOutput = editedOutput
break
}
case UserActions.Decline:
metadata.feedback.accepted = false
break
case UserActions.Select:
if (this._options.type === 'selectN') {
metadata.feedback.selected = await this.selectN(response)
} else if (this._options.type === 'selectOne') {
metadata.feedback.chosen = await this.selectOne(response)
}
break
case UserActions.Exit:
throw new Error('Exiting...')
default:
throw new Error(`Unexpected feedback: ${feedback}`)
} }
if (this._options.annotations) { if (this._options.annotations) {
await this.annotate(response, metadata) const annotation = await this.annotate()
if (annotation) {
metadata.feedback.annotation = annotation
}
} }
} }
} }

Wyświetl plik

@ -1,6 +1,5 @@
import { Agentic } from '@/agentic' import { Agentic } from '@/agentic'
import { SlackClient } from '@/services/slack' import { SlackClient } from '@/services/slack'
import { TaskResponseMetadata } from '@/types'
import { import {
HumanFeedbackMechanism, HumanFeedbackMechanism,
@ -23,24 +22,28 @@ export class HumanFeedbackMechanismSlack extends HumanFeedbackMechanism {
this.slackClient = new SlackClient() this.slackClient = new SlackClient()
} }
protected async annotate( protected async annotate(): Promise<string> {
response: any,
metadata: TaskResponseMetadata
): Promise<void> {
try { try {
const annotation = await this.slackClient.sendAndWaitForReply({ const annotation = await this.slackClient.sendAndWaitForReply({
text: 'Please leave an annotation (optional):' text: 'Please leave an annotation (optional):'
}) })
return annotation.text
if (annotation) {
metadata.feedback.annotation = annotation.text
}
} catch (e) { } catch (e) {
// Deliberately swallow the error here as the user is not required to leave an annotation // Deliberately swallow the error here as the user is not required to leave an annotation
return ''
} }
} }
private async askUser( protected async edit(): Promise<string> {
let { text: editedOutput } = await this.slackClient.sendAndWaitForReply({
text: 'Copy and edit the output:'
})
editedOutput = editedOutput.replace(/```$/g, '')
editedOutput = editedOutput.replace(/^```/g, '')
return editedOutput
}
protected async askUser(
message: string, message: string,
choices: UserActions[] choices: UserActions[]
): Promise<UserActions> { ): Promise<UserActions> {
@ -60,202 +63,45 @@ export class HumanFeedbackMechanismSlack extends HumanFeedbackMechanism {
return choices[parseInt(response.text)] return choices[parseInt(response.text)]
} }
public async confirm( public async selectOne(response: any[]): Promise<any> {
response: any, const { text: selectedOutput } = await this.slackClient.sendAndWaitForReply(
metadata: TaskResponseMetadata {
): Promise<void> { text:
const stringified = JSON.stringify(response, null, 2) 'Pick one output:' +
const msg = [ response.map((r, idx) => `\n*${idx}* - ${r}`).join('') +
'The following output was generated:', '\n\nReply with the number of your choice.',
'```', validate: (slackMessage) => {
stringified, const choice = parseInt(slackMessage.text)
'```', return !isNaN(choice) && choice >= 0 && choice < response.length
'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: return response[parseInt(selectedOutput)]
metadata.feedback.accepted = false
break
case UserActions.Exit:
throw new Error('Exiting...')
default:
throw new Error(`Unexpected feedback: ${feedback}`)
}
} }
public async selectOne( public async selectN(response: any[]): Promise<any[]> {
response: any[], const { text: selectedOutput } = await this.slackClient.sendAndWaitForReply(
metadata: TaskResponseMetadata {
): Promise<void> { text:
const stringified = JSON.stringify(response, null, 2) 'Select outputs:' +
const msg = [ response.map((r, idx) => `\n*${idx}* - ${r}`).join('') +
'The following output was generated:', '\n\nReply with a comma-separated list of the output numbers of your choice.',
'```', validate: (slackMessage) => {
stringified, const choices = slackMessage.text.split(',')
'```', return choices.every((choice) => {
'What would you like to do?' const choiceInt = parseInt(choice)
].join('\n') return (
!isNaN(choiceInt) && choiceInt >= 0 && choiceInt < response.length
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: const chosenOutputs = selectedOutput
throw new Error('Exiting...') .split(',')
.map((choice) => parseInt(choice))
default: return response.filter((_, idx) => {
throw new Error(`Unexpected feedback: ${feedback}`) return chosenOutputs.includes(idx)
} })
}
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}`)
}
} }
} }

Wyświetl plik

@ -1,6 +1,5 @@
import { Agentic } from '@/agentic' import { Agentic } from '@/agentic'
import { TwilioConversationClient } from '@/services/twilio-conversation' import { TwilioConversationClient } from '@/services/twilio-conversation'
import { TaskResponseMetadata } from '@/types'
import { import {
HumanFeedbackMechanism, HumanFeedbackMechanism,
@ -23,25 +22,30 @@ export class HumanFeedbackMechanismTwilio extends HumanFeedbackMechanism {
this.twilioClient = new TwilioConversationClient() this.twilioClient = new TwilioConversationClient()
} }
protected async annotate( protected async annotate(): Promise<string> {
response: any,
metadata: TaskResponseMetadata
): Promise<void> {
try { try {
const annotation = await this.twilioClient.sendAndWaitForReply({ const annotation = await this.twilioClient.sendAndWaitForReply({
name: 'human-feedback-annotation', name: 'human-feedback-annotation',
text: 'Please leave an annotation (optional):' text: 'Please leave an annotation (optional):'
}) })
return annotation.body
if (annotation) {
metadata.feedback.annotation = annotation.body
}
} catch (e) { } catch (e) {
// Deliberately swallow the error here as the user is not required to leave an annotation // Deliberately swallow the error here as the user is not required to leave an annotation
return ''
} }
} }
private async askUser( protected async edit(): Promise<string> {
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, '')
return editedOutput
}
protected async askUser(
message: string, message: string,
choices: UserActions[] choices: UserActions[]
): Promise<UserActions> { ): Promise<UserActions> {
@ -62,206 +66,45 @@ export class HumanFeedbackMechanismTwilio extends HumanFeedbackMechanism {
return choices[parseInt(response.body)] return choices[parseInt(response.body)]
} }
public async confirm( public async selectOne(response: any[]): Promise<any> {
response: any, const { body: selectedOutput } =
metadata: TaskResponseMetadata await this.twilioClient.sendAndWaitForReply({
): Promise<void> { name: 'human-feedback-select',
const stringified = JSON.stringify(response, null, 2) text:
const msg = [ 'Pick one output:' +
'The following output was generated:', response.map((r, idx) => `\n${idx} - ${r}`).join('') +
'', '\n\nReply with the number of your choice.',
stringified, validate: (message) => {
'', const choice = parseInt(message.body)
'What would you like to do?' return !isNaN(choice) && choice >= 0 && choice < response.length
].join('\n') }
})
const choices: UserActions[] = [UserActions.Accept, UserActions.Decline] return response[parseInt(selectedOutput)]
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}`)
}
} }
public async selectOne( public async selectN(response: any[]): Promise<any[]> {
response: any[], const { body: selectedOutput } =
metadata: TaskResponseMetadata await this.twilioClient.sendAndWaitForReply({
): Promise<void> { name: 'human-feedback-select',
const stringified = JSON.stringify(response, null, 2) text:
const msg = [ 'Select outputs:' +
'The following output was generated:', response.map((r, idx) => `\n${idx} - ${r}`).join('') +
'', '\n\nReply with a comma-separated list of the output numbers of your choice.',
stringified, validate: (message) => {
'', const choices = message.body.split(',')
'What would you like to do?' return choices.every((choice) => {
].join('\n') const choiceInt = parseInt(choice)
return (
const choices: UserActions[] = [UserActions.Select] !isNaN(choiceInt) && choiceInt >= 0 && choiceInt < response.length
)
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 const chosenOutputs = selectedOutput
break .split(',')
} .map((choice) => parseInt(choice))
return response.filter((_, idx) => {
case UserActions.Select: { return chosenOutputs.includes(idx)
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}`)
}
}
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}`)
}
} }
} }