kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
feat: add event emitting + terminal task output #34
commit
3c71bc7e31
|
@ -0,0 +1,22 @@
|
|||
import 'dotenv/config'
|
||||
import { OpenAIClient } from 'openai-fetch'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { Agentic, TaskStatus } from '@/index'
|
||||
|
||||
async function main() {
|
||||
const openai = new OpenAIClient({ apiKey: process.env.OPENAI_API_KEY! })
|
||||
const $ = new Agentic({ openai })
|
||||
|
||||
const ai = $.gpt4(`generate fake data`).output(
|
||||
z.object({ foo: z.string(), bar: z.number() })
|
||||
)
|
||||
|
||||
ai.eventEmitter.on(TaskStatus.COMPLETED, (event) => {
|
||||
console.log('Task completed successfully:', event)
|
||||
})
|
||||
|
||||
ai.call()
|
||||
}
|
||||
|
||||
main()
|
|
@ -45,6 +45,7 @@
|
|||
"@types/json-schema": "^7.0.12",
|
||||
"colorette": "^2.0.20",
|
||||
"debug": "^4.3.4",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"expr-eval": "^2.0.2",
|
||||
"handlebars": "^4.7.7",
|
||||
"is-relative-url": "^4.0.0",
|
||||
|
@ -62,6 +63,7 @@
|
|||
"p-timeout": "^6.1.2",
|
||||
"quick-lru": "^6.1.1",
|
||||
"replicate": "^0.12.3",
|
||||
"tree-model": "^1.0.7",
|
||||
"ts-dedent": "^2.2.0",
|
||||
"type-fest": "^3.12.0",
|
||||
"zod": "^3.21.4",
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
import { EventEmitter } from 'eventemitter3'
|
||||
import defaultKy from 'ky'
|
||||
import { SetOptional } from 'type-fest'
|
||||
|
||||
import * as types from './types'
|
||||
import { DEFAULT_OPENAI_MODEL } from './constants'
|
||||
import { TerminalTaskTracker } from './events'
|
||||
import { HumanFeedbackOptions, HumanFeedbackType } from './human-feedback'
|
||||
import { HumanFeedbackMechanismCLI } from './human-feedback/cli'
|
||||
import { OpenAIChatCompletion } from './llms/openai'
|
||||
import { defaultLogger } from './logger'
|
||||
import { defaultIDGeneratorFn, isFunction, isString } from './utils'
|
||||
|
||||
export class Agentic {
|
||||
export class Agentic extends EventEmitter {
|
||||
protected _ky: types.KyInstance
|
||||
protected _logger: types.Logger
|
||||
protected _taskTracker: TerminalTaskTracker
|
||||
|
||||
protected _openai?: types.openai.OpenAIClient
|
||||
protected _anthropic?: types.anthropic.Client
|
||||
|
@ -35,7 +38,10 @@ export class Agentic {
|
|||
idGeneratorFn?: types.IDGeneratorFunction
|
||||
logger?: types.Logger
|
||||
ky?: types.KyInstance
|
||||
taskTracker?: TerminalTaskTracker
|
||||
}) {
|
||||
super()
|
||||
|
||||
// TODO: This is a bit hacky, but we're doing it to have a slightly nicer API
|
||||
// for the end developer when creating subclasses of `BaseTask` to use as
|
||||
// tools.
|
||||
|
@ -48,6 +54,7 @@ export class Agentic {
|
|||
|
||||
this._ky = opts.ky ?? defaultKy
|
||||
this._logger = opts.logger ?? defaultLogger
|
||||
this._taskTracker = opts.taskTracker ?? new TerminalTaskTracker()
|
||||
|
||||
this._openaiModelDefaults = {
|
||||
provider: 'openai',
|
||||
|
@ -98,6 +105,10 @@ export class Agentic {
|
|||
return this._humanFeedbackDefaults
|
||||
}
|
||||
|
||||
public get taskTracker(): TerminalTaskTracker {
|
||||
return this._taskTracker
|
||||
}
|
||||
|
||||
public get idGeneratorFn(): types.IDGeneratorFunction {
|
||||
return this._idGeneratorFn
|
||||
}
|
||||
|
|
|
@ -2,3 +2,4 @@ export const DEFAULT_OPENAI_MODEL = 'gpt-3.5-turbo'
|
|||
export const DEFAULT_ANTHROPIC_MODEL = 'claude-instant-v1'
|
||||
export const DEFAULT_BOT_NAME = 'Agentic Bot'
|
||||
export const SKIP_HOOKS = Symbol('SKIP_HOOKS')
|
||||
export const SPACE = ' '
|
||||
|
|
|
@ -0,0 +1,158 @@
|
|||
import { defaultIDGeneratorFn } from '@/utils'
|
||||
|
||||
/**
|
||||
* Payload of an event.
|
||||
*/
|
||||
export interface EventPayload {
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
/**
|
||||
* Data required to create a new Event object.
|
||||
*/
|
||||
export interface EventData<T extends EventPayload> {
|
||||
/**
|
||||
* Event identifier
|
||||
*/
|
||||
id?: string
|
||||
|
||||
/**
|
||||
* Event timestamp
|
||||
*/
|
||||
timestamp?: Date
|
||||
|
||||
/**
|
||||
* Key-value pairs holding event data.
|
||||
*/
|
||||
payload?: T
|
||||
|
||||
/**
|
||||
* Version of the event.
|
||||
*/
|
||||
version?: number
|
||||
|
||||
/**
|
||||
* Event type.
|
||||
*/
|
||||
type?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Events that occur within the library (should be treated as immutable).
|
||||
*/
|
||||
export class Event<T extends EventPayload> {
|
||||
public readonly id: string
|
||||
public readonly timestamp: Date
|
||||
public readonly payload?: T
|
||||
public readonly version: number
|
||||
|
||||
constructor(data: EventData<T> = {}) {
|
||||
this.id = defaultIDGeneratorFn()
|
||||
this.timestamp = data.timestamp ?? new Date()
|
||||
this.payload = data.payload
|
||||
? JSON.parse(JSON.stringify(data.payload))
|
||||
: undefined
|
||||
this.version = data.version ?? 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a JSON string representation of an event back into an Event object.
|
||||
*/
|
||||
static fromJSON<T extends EventPayload>(json: string): Event<T> {
|
||||
const data = JSON.parse(json)
|
||||
data.timestamp = new Date(data.timestamp)
|
||||
let Type
|
||||
switch (data.type) {
|
||||
case 'TaskEvent':
|
||||
Type = TaskEvent<any, any>
|
||||
break
|
||||
case 'Event':
|
||||
Type = Event
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unknown event type: ${data.type}`)
|
||||
}
|
||||
|
||||
return new Type(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes an event into a JSON string.
|
||||
*/
|
||||
toJSON(): string {
|
||||
return JSON.stringify({
|
||||
id: this.id,
|
||||
timestamp: this.timestamp.toISOString(),
|
||||
payload: this.payload,
|
||||
version: this.version,
|
||||
type: this.constructor.name
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a human-readable string representation of an event.
|
||||
*/
|
||||
toString(): string {
|
||||
return `Event { id: ${
|
||||
this.id
|
||||
}, timestamp: ${this.timestamp.toISOString()}, payload: ${JSON.stringify(
|
||||
this.payload
|
||||
)} }`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Payload of a task event.
|
||||
*/
|
||||
export interface TaskEventPayload<TInput, TOutput> extends EventPayload {
|
||||
taskName: string
|
||||
taskId: string
|
||||
taskStatus: TaskStatus
|
||||
taskInputs?: TInput
|
||||
taskOutput?: TOutput
|
||||
parentTaskId?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Status of a task.
|
||||
*/
|
||||
export enum TaskStatus {
|
||||
COMPLETED = 'COMPLETED',
|
||||
FAILED = 'FAILED',
|
||||
PENDING = 'PENDING',
|
||||
RETRYING = 'RETRYING',
|
||||
SKIPPED = 'SKIPPED',
|
||||
RUNNING = 'RUNNING',
|
||||
CANCELLED = 'CANCELLED'
|
||||
}
|
||||
|
||||
/**
|
||||
* Events that occur within the library related to tasks.
|
||||
*/
|
||||
export class TaskEvent<TInput, TOutput> extends Event<
|
||||
TaskEventPayload<TInput, TOutput>
|
||||
> {
|
||||
get name(): string {
|
||||
return this.payload?.taskName ?? ''
|
||||
}
|
||||
|
||||
get taskId(): string {
|
||||
return this.payload?.taskId ?? ''
|
||||
}
|
||||
|
||||
get status(): TaskStatus {
|
||||
return this.payload?.taskStatus ?? TaskStatus.RUNNING
|
||||
}
|
||||
|
||||
get inputs(): any {
|
||||
return this.payload?.taskInputs ?? ''
|
||||
}
|
||||
|
||||
get output(): any {
|
||||
return this.payload?.taskOutput ?? ''
|
||||
}
|
||||
|
||||
get parentTaskId(): string {
|
||||
return this.payload?.parentTaskId ?? 'root'
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export * from './event'
|
||||
export * from './tracker'
|
|
@ -0,0 +1,32 @@
|
|||
import isUnicodeSupported from 'is-unicode-supported'
|
||||
|
||||
const UNICODE_SYMBOLS = {
|
||||
ARROW_RIGHT: '→',
|
||||
CIRCLE: '●',
|
||||
WARNING: '▲',
|
||||
CROSS: '⨯',
|
||||
SQUARE_SMALL_FILLED: '◼',
|
||||
SPINNER: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
|
||||
BAR_START: '┌',
|
||||
BAR: '│',
|
||||
BAR_END: '└',
|
||||
ACTIVE: '◆',
|
||||
LEFT_ARROW: '←',
|
||||
RIGHT_ARROW: '→'
|
||||
}
|
||||
const ASCII_SYMBOLS = {
|
||||
ARROW_RIGHT: '→',
|
||||
CIRCLE: '•',
|
||||
WARNING: '‼',
|
||||
CROSS: '×',
|
||||
SQUARE_SMALL_FILLED: '■',
|
||||
SPINNER: ['-', '\\', '|', '/'],
|
||||
BAR_START: 'T',
|
||||
BAR: '|',
|
||||
BAR_END: '—',
|
||||
ACTIVE: '*',
|
||||
LEFT_ARROW: '<',
|
||||
RIGHT_ARROW: '>'
|
||||
}
|
||||
|
||||
export const SYMBOLS = isUnicodeSupported() ? UNICODE_SYMBOLS : ASCII_SYMBOLS
|
|
@ -0,0 +1,422 @@
|
|||
import process from 'node:process'
|
||||
import readline from 'node:readline'
|
||||
|
||||
import { bgWhite, black, bold, cyan, gray, green, red, yellow } from 'colorette'
|
||||
import TreeModel from 'tree-model'
|
||||
|
||||
import { SPACE } from '@/constants'
|
||||
import { capitalize } from '@/utils'
|
||||
|
||||
import { TaskEvent, TaskStatus } from './event'
|
||||
import { SYMBOLS } from './symbols'
|
||||
|
||||
export const MAGIC_STRING = '__INSIDE_TRACKER__' // a unique "magic" string that used to identify the output of the tracker
|
||||
|
||||
const TWO_SPACES = `${SPACE}${SPACE}`
|
||||
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const RE_ANSI_ESCAPES = /^(\x1b\[[0-9;]*[ABCDHJK]|[\r\n])+$/ // cursor movement, screen clearing, etc.
|
||||
|
||||
const originalStdoutWrite = process.stdout.write
|
||||
const originalStderrWrite = process.stderr.write
|
||||
|
||||
export interface TerminalTaskTrackerOptions {
|
||||
spinnerInterval?: number
|
||||
inactivityInterval?: number
|
||||
}
|
||||
|
||||
export class TerminalTaskTracker {
|
||||
protected _tree = new TreeModel()
|
||||
protected _root = this._tree.parse({ id: 'root' })
|
||||
protected _interval: NodeJS.Timeout | null = null
|
||||
protected _inactivityTimeout: NodeJS.Timeout | null = null
|
||||
protected _truncateOutput = false
|
||||
protected _viewMode = 'tasks'
|
||||
protected _outputs: Array<string | Uint8Array> = []
|
||||
protected _renderingPaused = false
|
||||
protected _isClosed = false
|
||||
|
||||
protected _spinnerInterval: number
|
||||
protected _inactivityInterval: number
|
||||
|
||||
private _stdoutBuffer: string[] = []
|
||||
private _stderrBuffer: string[] = []
|
||||
|
||||
constructor({
|
||||
spinnerInterval = 100,
|
||||
inactivityInterval = 2_000
|
||||
}: TerminalTaskTrackerOptions = {}) {
|
||||
this._spinnerInterval = spinnerInterval
|
||||
this._inactivityInterval = inactivityInterval
|
||||
|
||||
if (!process.stderr.isTTY) {
|
||||
// If stderr is not a TTY, don't render any dynamic output...
|
||||
return
|
||||
}
|
||||
|
||||
process.stdout.write = (buffer: string | Uint8Array) => {
|
||||
if (buffer instanceof Uint8Array) {
|
||||
buffer = Buffer.from(buffer).toString('utf-8')
|
||||
}
|
||||
|
||||
if (!this._renderingPaused) {
|
||||
this._stdoutBuffer.push(buffer)
|
||||
}
|
||||
|
||||
return originalStdoutWrite.call(process.stdout, buffer)
|
||||
}
|
||||
|
||||
process.stderr.write = (buffer: string | Uint8Array) => {
|
||||
if (buffer instanceof Uint8Array) {
|
||||
buffer = Buffer.from(buffer).toString('utf-8')
|
||||
}
|
||||
|
||||
if (typeof buffer === 'string' && buffer.startsWith(MAGIC_STRING)) {
|
||||
// This write is from inside the tracker, remove the magic string and write to stderr:
|
||||
return originalStderrWrite.call(
|
||||
process.stderr,
|
||||
buffer.replace(MAGIC_STRING, '')
|
||||
)
|
||||
} else {
|
||||
if (!this._renderingPaused && !RE_ANSI_ESCAPES.test(buffer)) {
|
||||
// If an ANSI escape sequence is written to stderr, it will mess up the output, so we need to write it to stdout instead:
|
||||
// This write is from outside the tracker, add it to stderrBuffer and write to stderr:
|
||||
this._stderrBuffer.push(buffer)
|
||||
}
|
||||
|
||||
return originalStderrWrite.call(process.stderr, buffer)
|
||||
}
|
||||
}
|
||||
|
||||
this.start()
|
||||
}
|
||||
|
||||
handleKeyPress = (str, key) => {
|
||||
if (key.ctrl && key.name === 'c') {
|
||||
process.exit()
|
||||
}
|
||||
|
||||
if (key.ctrl && key.name === 'e') {
|
||||
this.toggleOutputTruncation()
|
||||
}
|
||||
|
||||
if (key.ctrl && key.name === 'right') {
|
||||
this.toggleView('next')
|
||||
}
|
||||
|
||||
if (key.ctrl && key.name === 'left') {
|
||||
this.toggleView('prev')
|
||||
}
|
||||
}
|
||||
|
||||
start() {
|
||||
this._interval = setInterval(() => {
|
||||
this.render()
|
||||
}, this._spinnerInterval)
|
||||
|
||||
readline.emitKeypressEvents(process.stdin)
|
||||
|
||||
process.stdin.setRawMode(true)
|
||||
|
||||
process.stdin.on('keypress', this.handleKeyPress)
|
||||
|
||||
this.startInactivityTimeout()
|
||||
|
||||
this._isClosed = false
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this._isClosed) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this._interval) {
|
||||
clearInterval(this._interval)
|
||||
}
|
||||
|
||||
if (this._inactivityTimeout) {
|
||||
clearTimeout(this._inactivityTimeout)
|
||||
}
|
||||
|
||||
process.stdin.setRawMode(false)
|
||||
|
||||
// Remove the keypress listener:
|
||||
process.stdin.off('keypress', this.handleKeyPress)
|
||||
|
||||
// Restore the original `process.stdout.write()` and `process.stderr.write()` functions:
|
||||
process.stdout.write = originalStdoutWrite
|
||||
process.stderr.write = originalStderrWrite
|
||||
|
||||
const finalLines = [
|
||||
'',
|
||||
'',
|
||||
bgWhite(black(' Completed all tasks. ')),
|
||||
'',
|
||||
'',
|
||||
bgWhite(black(' stderr: ')),
|
||||
'',
|
||||
this._stderrBuffer.join(''),
|
||||
'',
|
||||
bgWhite(black(' stdout: ')),
|
||||
'',
|
||||
this._stdoutBuffer.join(''),
|
||||
''
|
||||
]
|
||||
|
||||
process.stderr.write(finalLines.join('\n'))
|
||||
|
||||
// Pause the reading of stdin so that the Node.js process will exit once done:
|
||||
process.stdin.pause()
|
||||
|
||||
this._isClosed = true
|
||||
}
|
||||
|
||||
pause() {
|
||||
this.clearAndSetCursorPosition()
|
||||
this._renderingPaused = true
|
||||
}
|
||||
|
||||
resume() {
|
||||
this._renderingPaused = false
|
||||
this.render()
|
||||
}
|
||||
|
||||
stringify(value: any): string {
|
||||
if (this._truncateOutput) {
|
||||
const json = JSON.stringify(value)
|
||||
if (json.length < 40) {
|
||||
return json
|
||||
}
|
||||
|
||||
return json.slice(0, 20) + '...' + json.slice(-20)
|
||||
}
|
||||
|
||||
return JSON.stringify(value)
|
||||
}
|
||||
|
||||
toggleOutputTruncation() {
|
||||
this._truncateOutput = !this._truncateOutput
|
||||
}
|
||||
|
||||
startInactivityTimeout() {
|
||||
this._inactivityTimeout = setTimeout(() => {
|
||||
const unfinishedTasks = this._root.all((node) => {
|
||||
return (
|
||||
node.model.status == TaskStatus.RUNNING ||
|
||||
node.model.status == TaskStatus.RETRYING ||
|
||||
node.model.status == TaskStatus.PENDING
|
||||
)
|
||||
})
|
||||
|
||||
if (unfinishedTasks.length === 0) {
|
||||
this.close()
|
||||
} else {
|
||||
this.startInactivityTimeout()
|
||||
}
|
||||
}, this._inactivityInterval)
|
||||
}
|
||||
|
||||
addEvent<TInput, TOutput>(event: TaskEvent<TInput, TOutput>) {
|
||||
const {
|
||||
parentTaskId = 'root',
|
||||
taskId: id,
|
||||
name,
|
||||
status,
|
||||
inputs,
|
||||
output
|
||||
} = event
|
||||
const parentNode = this._root.first(
|
||||
(node) => node.model.id === parentTaskId
|
||||
)
|
||||
|
||||
const existingEventNode = parentNode
|
||||
? parentNode.first((node) => node.model.id === id)
|
||||
: null
|
||||
|
||||
if (existingEventNode) {
|
||||
existingEventNode.model.status = status
|
||||
existingEventNode.model.output = output
|
||||
} else {
|
||||
const node = this._tree.parse({ id, name, status, inputs, output: null })
|
||||
if (parentNode) {
|
||||
parentNode.addChild(node)
|
||||
} else {
|
||||
this._root.addChild(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getStatusSymbolColor(
|
||||
status: TaskStatus
|
||||
): [string, (text: string) => string] {
|
||||
switch (status) {
|
||||
case TaskStatus.COMPLETED:
|
||||
return [SYMBOLS.CIRCLE, green]
|
||||
case TaskStatus.FAILED:
|
||||
return [SYMBOLS.CROSS, red]
|
||||
case TaskStatus.RETRYING:
|
||||
return [this.getSpinnerSymbol(), yellow]
|
||||
case TaskStatus.RUNNING:
|
||||
default:
|
||||
return [this.getSpinnerSymbol(), cyan]
|
||||
}
|
||||
}
|
||||
|
||||
renderTree(id?: string, level = 0): string[] {
|
||||
const indent = SPACE.repeat(level * 2)
|
||||
let lines: string[] = []
|
||||
|
||||
const root = id
|
||||
? this._root.first((node) => node.model.id === id)
|
||||
: this._root
|
||||
|
||||
if (root?.children) {
|
||||
root.children.forEach(
|
||||
({ model: { id, name, status, output, inputs } }) => {
|
||||
const [statusSymbol, color] = this.getStatusSymbolColor(status)
|
||||
|
||||
lines.push(
|
||||
indent +
|
||||
color(statusSymbol) +
|
||||
SPACE +
|
||||
bold(name) +
|
||||
gray('(' + this.stringify(inputs) + ')')
|
||||
)
|
||||
|
||||
const hasChildren = root.hasChildren()
|
||||
|
||||
if (hasChildren) {
|
||||
lines = lines.concat(
|
||||
this.renderTree(id, level + 1).map((line, index, arr) => {
|
||||
if (index === arr.length - 1) {
|
||||
return indent + gray(SYMBOLS.BAR) + line
|
||||
}
|
||||
|
||||
return indent + gray(SYMBOLS.BAR) + line
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
let line = ''
|
||||
if (hasChildren) {
|
||||
line = indent + gray(SYMBOLS.BAR_END)
|
||||
}
|
||||
|
||||
const formattedOutput = this.stringify(output || '')
|
||||
if (status === TaskStatus.COMPLETED) {
|
||||
line +=
|
||||
indent +
|
||||
' ' +
|
||||
gray(SYMBOLS.RIGHT_ARROW + SPACE + formattedOutput)
|
||||
} else if (status === TaskStatus.FAILED) {
|
||||
line +=
|
||||
indent +
|
||||
' ' +
|
||||
gray(SYMBOLS.RIGHT_ARROW) +
|
||||
SPACE +
|
||||
red(formattedOutput)
|
||||
} else if (status === TaskStatus.RETRYING) {
|
||||
line +=
|
||||
indent +
|
||||
' ' +
|
||||
yellow(SYMBOLS.WARNING) +
|
||||
SPACE +
|
||||
gray(formattedOutput)
|
||||
}
|
||||
|
||||
lines.push(line)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
clearAndSetCursorPosition() {
|
||||
process.stderr.cursorTo(0, 0)
|
||||
process.stderr.clearScreenDown()
|
||||
}
|
||||
|
||||
clearPreviousRender(linesCount: number) {
|
||||
for (let i = 0; i < linesCount; i++) {
|
||||
process.stderr.moveCursor(0, -1)
|
||||
process.stderr.clearLine(1)
|
||||
}
|
||||
}
|
||||
|
||||
private writeWithMagicString(content: string | string[]) {
|
||||
let output
|
||||
|
||||
if (Array.isArray(content)) {
|
||||
if (content.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
output = content.join('\n')
|
||||
} else {
|
||||
output = content
|
||||
}
|
||||
|
||||
process.stderr.write(MAGIC_STRING + output)
|
||||
}
|
||||
|
||||
toggleView(direction: string) {
|
||||
const viewModes = ['tasks', 'stdout', 'stderr']
|
||||
const currentIdx = viewModes.indexOf(this._viewMode)
|
||||
|
||||
if (direction === 'next') {
|
||||
this._viewMode = viewModes[(currentIdx + 1) % viewModes.length]
|
||||
} else if (direction === 'prev') {
|
||||
this._viewMode =
|
||||
viewModes[(currentIdx - 1 + viewModes.length) % viewModes.length]
|
||||
}
|
||||
|
||||
this.render()
|
||||
}
|
||||
|
||||
getSpinnerSymbol(): string {
|
||||
return SYMBOLS.SPINNER[
|
||||
Math.floor(Date.now() / this._spinnerInterval) % SYMBOLS.SPINNER.length
|
||||
]
|
||||
}
|
||||
|
||||
renderHeader() {
|
||||
const commands = [
|
||||
'ctrl+c: exit',
|
||||
`ctrl+e: ${
|
||||
this._truncateOutput ? TWO_SPACES + 'expand' : 'truncate'
|
||||
} output`,
|
||||
'ctrl+left/right: switch view'
|
||||
].join(' | ')
|
||||
|
||||
const header = [
|
||||
` Agentic - ${capitalize(this._viewMode)} View`,
|
||||
' ' + commands + ' ',
|
||||
'',
|
||||
''
|
||||
].join('\n')
|
||||
this.writeWithMagicString(bgWhite(black(header)))
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this._renderingPaused) {
|
||||
return // Do not render if paused
|
||||
}
|
||||
|
||||
this.clearAndSetCursorPosition()
|
||||
if (this._viewMode === 'tasks') {
|
||||
const lines = this.renderTree('root')
|
||||
this.clearPreviousRender(lines.length)
|
||||
this.renderHeader()
|
||||
this.writeWithMagicString(lines)
|
||||
} else if (this._viewMode === 'stdout') {
|
||||
this.clearPreviousRender(this._stdoutBuffer.length)
|
||||
this.renderHeader()
|
||||
this.writeWithMagicString(this._stdoutBuffer)
|
||||
} else if (this._viewMode === 'stderr') {
|
||||
this.clearPreviousRender(this._stderrBuffer.length)
|
||||
this.renderHeader()
|
||||
this.writeWithMagicString(this._stderrBuffer)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +1,10 @@
|
|||
export * from './agentic'
|
||||
export * from './task'
|
||||
export * from './constants'
|
||||
export * from './errors'
|
||||
export * from './tokenizer'
|
||||
export * from './events'
|
||||
export * from './human-feedback'
|
||||
export * from './task'
|
||||
export * from './tokenizer'
|
||||
|
||||
export * from './llms'
|
||||
export * from './services'
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { cyan, green, magenta, red, yellow } from 'colorette'
|
||||
import logger from 'debug'
|
||||
|
||||
import { SPACE } from '@/constants'
|
||||
import { identity } from '@/utils'
|
||||
|
||||
import { getEnv } from './env'
|
||||
|
@ -57,7 +58,6 @@ if (LOG_LEVEL === undefined) {
|
|||
|
||||
const debug = logger('agentic')
|
||||
|
||||
const SPACE = ' '
|
||||
const INDENT = SPACE.repeat(23)
|
||||
|
||||
// Override the default logger to add a timestamp and severity level to the logged arguments:
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import EventEmitter from 'eventemitter3'
|
||||
import pRetry, { FailedAttemptError } from 'p-retry'
|
||||
import QuickLRU from 'quick-lru'
|
||||
import { ZodType } from 'zod'
|
||||
|
@ -6,6 +7,7 @@ import * as errors from './errors'
|
|||
import * as types from './types'
|
||||
import type { Agentic } from './agentic'
|
||||
import { SKIP_HOOKS } from './constants'
|
||||
import { TaskEvent, TaskStatus } from './events'
|
||||
import {
|
||||
HumanFeedbackMechanismCLI,
|
||||
HumanFeedbackOptions,
|
||||
|
@ -29,7 +31,7 @@ import { defaultIDGeneratorFn, isValidTaskIdentifier } from './utils'
|
|||
export abstract class BaseTask<
|
||||
TInput extends types.TaskInput = void,
|
||||
TOutput extends types.TaskOutput = string
|
||||
> {
|
||||
> extends EventEmitter {
|
||||
protected _agentic: Agentic
|
||||
protected _id: string
|
||||
|
||||
|
@ -48,6 +50,8 @@ export abstract class BaseTask<
|
|||
}> = []
|
||||
|
||||
constructor(options: types.BaseTaskOptions = {}) {
|
||||
super()
|
||||
|
||||
this._agentic = options.agentic ?? globalThis.__agentic?.deref()
|
||||
|
||||
this._timeoutMs = options.timeoutMs
|
||||
|
@ -190,7 +194,9 @@ export abstract class BaseTask<
|
|||
})
|
||||
|
||||
this.addAfterCallHook(async (output, ctx) => {
|
||||
this._agentic.taskTracker.pause()
|
||||
const feedback = await feedbackMechanism.interact(output)
|
||||
this._agentic.taskTracker.resume()
|
||||
ctx.metadata = { ...ctx.metadata, feedback }
|
||||
if (feedback.editedOutput) {
|
||||
return feedback.editedOutput
|
||||
|
@ -272,6 +278,11 @@ export abstract class BaseTask<
|
|||
}
|
||||
}
|
||||
|
||||
this.emit(TaskStatus.RUNNING, {
|
||||
taskInputs: input,
|
||||
...ctx.metadata
|
||||
})
|
||||
|
||||
for (const { hook: preHook } of this._preHooks) {
|
||||
const preHookResult = await preHook(ctx)
|
||||
if (preHookResult === SKIP_HOOKS) {
|
||||
|
@ -342,6 +353,12 @@ export abstract class BaseTask<
|
|||
ctx.attemptNumber = err.attemptNumber + 1
|
||||
ctx.metadata.error = err
|
||||
|
||||
this.emit(TaskStatus.RETRYING, {
|
||||
taskInputs: input,
|
||||
taskOutput: err,
|
||||
...ctx.metadata
|
||||
})
|
||||
|
||||
if (err instanceof errors.ZodOutputValidationError) {
|
||||
ctx.retryMessage = err.message
|
||||
return
|
||||
|
@ -365,6 +382,12 @@ export abstract class BaseTask<
|
|||
// task for now.
|
||||
return
|
||||
} else {
|
||||
this.emit(TaskStatus.FAILED, {
|
||||
taskInputs: input,
|
||||
taskOutput: err,
|
||||
...ctx.metadata
|
||||
})
|
||||
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
@ -381,6 +404,12 @@ export abstract class BaseTask<
|
|||
|
||||
// ctx.tracker.setOutput(stringifyForDebugging(result, { maxLength: 100 }))
|
||||
|
||||
this.emit(TaskStatus.COMPLETED, {
|
||||
taskInputs: input,
|
||||
taskOutput: result,
|
||||
...ctx.metadata
|
||||
})
|
||||
|
||||
return {
|
||||
result,
|
||||
metadata: ctx.metadata
|
||||
|
@ -397,4 +426,33 @@ export abstract class BaseTask<
|
|||
// input: TInput,
|
||||
// onProgress: types.ProgressFunction
|
||||
// }): Promise<TOutput>
|
||||
|
||||
on<T extends string | symbol>(
|
||||
takStatus: T,
|
||||
fn: (event: TaskEvent<TInput, TOutput>) => void,
|
||||
context?: any
|
||||
): this {
|
||||
return super.on(takStatus, fn, context)
|
||||
}
|
||||
|
||||
emit(taskStatus: string | symbol, payload: object = {}): boolean {
|
||||
if (!Object.values(TaskStatus).includes(taskStatus as TaskStatus)) {
|
||||
throw new Error(`Invalid task status: ${String(taskStatus)}`)
|
||||
}
|
||||
|
||||
const { id, nameForModel } = this
|
||||
const event = new TaskEvent<TInput, TOutput>({
|
||||
payload: {
|
||||
taskStatus: taskStatus as TaskStatus,
|
||||
taskId: id,
|
||||
taskName: nameForModel,
|
||||
...payload
|
||||
}
|
||||
})
|
||||
this._agentic.taskTracker.addEvent(event)
|
||||
|
||||
this._agentic.emit(taskStatus, event)
|
||||
|
||||
return super.emit(taskStatus, event)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -254,3 +254,7 @@ export function isArray(value: any): value is any[] {
|
|||
export function identity<T>(x: T): T {
|
||||
return x
|
||||
}
|
||||
|
||||
export function capitalize(str: string): string {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
lockfileVersion: '6.0'
|
||||
lockfileVersion: '6.1'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
|
@ -125,6 +125,9 @@ importers:
|
|||
debug:
|
||||
specifier: ^4.3.4
|
||||
version: 4.3.4
|
||||
eventemitter3:
|
||||
specifier: ^5.0.1
|
||||
version: 5.0.1
|
||||
expr-eval:
|
||||
specifier: ^2.0.2
|
||||
version: 2.0.2
|
||||
|
@ -176,6 +179,9 @@ importers:
|
|||
replicate:
|
||||
specifier: ^0.12.3
|
||||
version: 0.12.3
|
||||
tree-model:
|
||||
specifier: ^1.0.7
|
||||
version: 1.0.7
|
||||
ts-dedent:
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0
|
||||
|
@ -3222,6 +3228,10 @@ packages:
|
|||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/eventemitter3@5.0.1:
|
||||
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
|
||||
dev: false
|
||||
|
||||
/execa@0.8.0:
|
||||
resolution: {integrity: sha512-zDWS+Rb1E8BlqqhALSt9kUhss8Qq4nN3iof3gsOdyINksElaPyNBtKUMTR62qhvgVWR0CqCX7sdnKe4MnUbFEA==}
|
||||
engines: {node: '>=4'}
|
||||
|
@ -3359,6 +3369,10 @@ packages:
|
|||
to-regex-range: 5.0.1
|
||||
dev: true
|
||||
|
||||
/find-insert-index@0.0.1:
|
||||
resolution: {integrity: sha512-eIqFuQzY7XwpAJ3sHWKFNGLx1nm3w/IhmFASETcx5sUuCaOUd3xDqRK/376SzXMVVJQaJUCPlS7L841T0xpFjQ==}
|
||||
dev: false
|
||||
|
||||
/find-up@4.1.0:
|
||||
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
|
||||
engines: {node: '>=8'}
|
||||
|
@ -4834,6 +4848,10 @@ packages:
|
|||
engines: {node: '>= 8'}
|
||||
dev: true
|
||||
|
||||
/mergesort@0.0.1:
|
||||
resolution: {integrity: sha512-WKghTBzqAvTt9rG5TWS78Dmk2kCCL9VkkX8Zi9kKfJ4iqYpvcGGpeYtkhPHa9NZAPLivZiZsdO/LBG3ENayDmQ==}
|
||||
dev: false
|
||||
|
||||
/mermaid@10.2.3:
|
||||
resolution: {integrity: sha512-cMVE5s9PlQvOwfORkyVpr5beMsLdInrycAosdr+tpZ0WFjG4RJ/bUHST7aTgHNJbujHkdBRAm+N50P3puQOfPw==}
|
||||
dependencies:
|
||||
|
@ -6928,6 +6946,13 @@ packages:
|
|||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/tree-model@1.0.7:
|
||||
resolution: {integrity: sha512-oP4LUbCVtD2gcjcRaeI4L5hY60tHzB+AK/bthIJ2Pq1EUUOio5/xFzPWnGoBZlhtqpqbOkhFDzKIwKLOn0kccQ==}
|
||||
dependencies:
|
||||
find-insert-index: 0.0.1
|
||||
mergesort: 0.0.1
|
||||
dev: false
|
||||
|
||||
/trim-lines@3.0.1:
|
||||
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
|
||||
dev: false
|
||||
|
|
Ładowanie…
Reference in New Issue