chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
import chalk from 'chalk'
|
||||
import {
|
||||
Chat,
|
||||
CitationsManager,
|
||||
Component,
|
||||
DefaultComponents,
|
||||
Exit,
|
||||
ListenExit,
|
||||
isComponent,
|
||||
type ExecutionResult,
|
||||
type IterationStatus,
|
||||
type IterationStatuses,
|
||||
type RenderedComponent,
|
||||
} from 'llmz'
|
||||
|
||||
import { prompt } from './buttons'
|
||||
|
||||
type TranscriptItem = {
|
||||
role: 'assistant' | 'user'
|
||||
content: string
|
||||
name?: string
|
||||
}
|
||||
|
||||
export class CLIChat extends Chat {
|
||||
private _controller = new AbortController()
|
||||
public transcript: TranscriptItem[] = []
|
||||
private _buttons: string[] = []
|
||||
|
||||
public turns = 0
|
||||
public status?: IterationStatus
|
||||
public result?: ExecutionResult
|
||||
public citations: CitationsManager = new CitationsManager()
|
||||
|
||||
public renderers: Array<{
|
||||
component: Component
|
||||
render: (component: RenderedComponent) => Promise<void>
|
||||
}> = []
|
||||
|
||||
public constructor() {
|
||||
super({
|
||||
components: () => [DefaultComponents.Text, DefaultComponents.Button, ...this.renderers.map((r) => r.component)],
|
||||
transcript: () => this.transcript,
|
||||
handler: (input) => this._sendMessage(input),
|
||||
})
|
||||
}
|
||||
|
||||
public onExecutionDone(_result: ExecutionResult): void {
|
||||
this.result = _result
|
||||
this.status = _result.iterations.at(-1)?.status
|
||||
}
|
||||
|
||||
public async iterate() {
|
||||
if (this._controller.signal.aborted) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.hasExitedWith(ListenExit)) {
|
||||
await this.prompt()
|
||||
return true
|
||||
}
|
||||
|
||||
if (this.turns++ > 100) {
|
||||
console.warn(chalk.yellow('⚠️ Too many turns, stopping the chat to prevent infinite loop'))
|
||||
return false
|
||||
}
|
||||
|
||||
if (!this.result) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
public hasExited(this: this): this is this & { status: IterationStatuses.ExitSuccess } {
|
||||
return this.status?.type === 'exit_success'
|
||||
}
|
||||
|
||||
public hasExitedWith<R>(this: this, exit: Exit<R>): this is { status: IterationStatuses.ExitSuccess<R> } & this {
|
||||
return this.status?.type === 'exit_success' && this.status.exit_success.exit_name === exit.name
|
||||
}
|
||||
|
||||
public prompt = async (msg: string = chalk.gray('(your reply) ')) => {
|
||||
const reply = await prompt(msg, this._buttons)
|
||||
this._buttons = []
|
||||
this.turns = 0
|
||||
|
||||
if (reply?.trim().length) {
|
||||
this.transcript.push({ role: 'user', content: reply })
|
||||
console.log(`${chalk.bold('👤 User:')} ${reply}`)
|
||||
} else {
|
||||
this.transcript.push({ role: 'user', content: '[silence] (user did not answer)' })
|
||||
}
|
||||
}
|
||||
|
||||
private async _sendMessage(input: RenderedComponent) {
|
||||
let text = ''
|
||||
|
||||
let children: any[] = [input]
|
||||
if (isComponent(input, DefaultComponents.Text)) {
|
||||
children = input.children
|
||||
}
|
||||
|
||||
for (const child of children) {
|
||||
if (isComponent(child, DefaultComponents.Button) && child.props?.label) {
|
||||
this._buttons.push(child.props.label)
|
||||
} else if (
|
||||
typeof child === 'string' ||
|
||||
typeof child === 'number' ||
|
||||
typeof child === 'boolean' ||
|
||||
typeof child === 'bigint'
|
||||
) {
|
||||
text += '\n' + child
|
||||
} else {
|
||||
this.transcript.push({
|
||||
role: 'assistant',
|
||||
content: JSON.stringify(child, null, 2),
|
||||
})
|
||||
|
||||
const renderer = this.renderers.find((r) => isComponent(child, r.component))
|
||||
|
||||
if (renderer) {
|
||||
await renderer.render(child)
|
||||
} else {
|
||||
console.log(chalk.bold('🤖 Agent: ') + chalk.gray('<unknown component> ' + JSON.stringify(child)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
text = text.trim()
|
||||
if (text.length > 0) {
|
||||
const sources: string[] = []
|
||||
const { cleaned } = this.citations.extractCitations(text, (citation) => {
|
||||
const idx = chalk.bgGreenBright.black.bold(` ${sources.length + 1} `)
|
||||
sources.push(`${idx}: ${JSON.stringify(citation.source)}`)
|
||||
return `${idx}`
|
||||
}) ?? { cleaned: text, citations: [] }
|
||||
const buttonsStr = this._buttons.length > 0 ? `\n\n${chalk.bold('Buttons:')} ${this._buttons.join(', ')}` : ''
|
||||
this.transcript.push({ role: 'assistant', content: cleaned + buttonsStr })
|
||||
console.log(`${chalk.bold('🤖 Agent:')} ${cleaned}`)
|
||||
if (sources.length) {
|
||||
console.log(chalk.dim('Citations'))
|
||||
console.log(chalk.dim('========='))
|
||||
console.log(chalk.dim(sources.join('\n')))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public registerComponent<
|
||||
T extends Component,
|
||||
Props extends Record<string, any> = T extends Component ? T['propsType'] : never,
|
||||
>(component: T, render: (component: RenderedComponent<Props>) => Promise<void>): void {
|
||||
if (this.renderers.some((r) => r.component.definition.name === component.definition.name)) {
|
||||
throw new Error(`Component ${component.definition.name} is already registered`)
|
||||
}
|
||||
this.renderers.push({ component, render: render as any })
|
||||
}
|
||||
|
||||
public stop() {
|
||||
this._controller.abort()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user