1
1
mirror of https://github.com/leon-ai/leon.git synced 2024-10-05 13:47:49 +03:00

feat(server): preparing LLM TCP server loading

This commit is contained in:
louistiti 2024-02-13 16:22:32 +08:00
parent 1c0c8080bf
commit 49ac3218f7
No known key found for this signature in database
GPG Key ID: 334C1BA3550BBDE5
15 changed files with 174 additions and 110 deletions

View File

@ -7,14 +7,14 @@ import prettyBytes from 'pretty-bytes'
import {
PYTHON_BRIDGE_SRC_PATH,
TCP_SERVER_SRC_PATH,
PYTHON_TCP_SERVER_SRC_PATH,
BINARIES_FOLDER_NAME,
NODEJS_BRIDGE_DIST_PATH,
PYTHON_BRIDGE_DIST_PATH,
TCP_SERVER_DIST_PATH,
PYTHON_TCP_SERVER_DIST_PATH,
NODEJS_BRIDGE_BIN_NAME,
PYTHON_BRIDGE_BIN_NAME,
TCP_SERVER_BIN_NAME,
PYTHON_TCP_SERVER_BIN_NAME,
NODEJS_BRIDGE_ROOT_PATH
} from '@/constants'
import { OSTypes } from '@/types'
@ -48,13 +48,13 @@ BUILD_TARGETS.set('python-bridge', {
dotVenvPath: path.join(PYTHON_BRIDGE_SRC_PATH, '.venv')
})
BUILD_TARGETS.set('tcp-server', {
name: 'TCP server',
name: 'Python TCP server',
needsPythonEnv: true,
pipfilePath: path.join(TCP_SERVER_SRC_PATH, 'Pipfile'),
setupFilePath: path.join(TCP_SERVER_SRC_PATH, 'setup.py'),
distPath: TCP_SERVER_DIST_PATH,
archiveName: `${TCP_SERVER_BIN_NAME}-${BINARIES_FOLDER_NAME}.zip`,
dotVenvPath: path.join(TCP_SERVER_SRC_PATH, '.venv')
pipfilePath: path.join(PYTHON_TCP_SERVER_SRC_PATH, 'Pipfile'),
setupFilePath: path.join(PYTHON_TCP_SERVER_SRC_PATH, 'setup.py'),
distPath: PYTHON_TCP_SERVER_DIST_PATH,
archiveName: `${PYTHON_TCP_SERVER_BIN_NAME}-${BINARIES_FOLDER_NAME}.zip`,
dotVenvPath: path.join(PYTHON_TCP_SERVER_SRC_PATH, '.venv')
})
;(async () => {
LoaderHelper.start()

View File

@ -18,8 +18,8 @@ import {
LEON_VERSION,
NODEJS_BRIDGE_BIN_PATH,
PYTHON_BRIDGE_BIN_PATH,
TCP_SERVER_BIN_PATH,
TCP_SERVER_VERSION,
PYTHON_TCP_SERVER_BIN_PATH,
PYTHON_TCP_SERVER_VERSION,
NODEJS_BRIDGE_VERSION,
PYTHON_BRIDGE_VERSION,
INSTANCE_ID
@ -50,8 +50,8 @@ dotenv.config()
can_run: { title: 'Run', type: 'error', v: true },
can_run_skill: { title: 'Run skills', type: 'error', v: true },
can_text: { title: 'Reply you by texting', type: 'error', v: true },
can_start_tcp_server: {
title: 'Start the TCP server',
can_start_python_tcp_server: {
title: 'Start the Python TCP server',
type: 'error',
v: true
},
@ -118,7 +118,7 @@ dotenv.config()
output: null,
error: null
},
tcpServer: {
pythonTCPServer: {
version: null,
startTime: null,
command: null,
@ -286,39 +286,39 @@ dotenv.config()
}
/**
* TCP server startup checking
* Python TCP server startup checking
*/
LogHelper.success(`TCP server version: ${TCP_SERVER_VERSION}`)
reportDataInput.tcpServer.version = TCP_SERVER_VERSION
LogHelper.success(`Python TCP server version: ${PYTHON_TCP_SERVER_VERSION}`)
reportDataInput.pythonTCPServer.version = PYTHON_TCP_SERVER_VERSION
LogHelper.info('Starting the TCP server...')
LogHelper.info('Starting the Python TCP server...')
const tcpServerCommand = `${TCP_SERVER_BIN_PATH} en`
const tcpServerStart = Date.now()
const p = spawn(tcpServerCommand, { shell: true })
const pythonTCPServerCommand = `${PYTHON_TCP_SERVER_BIN_PATH} en`
const pythonTCPServerStart = Date.now()
const p = spawn(pythonTCPServerCommand, { shell: true })
const ignoredWarnings = [
'UserWarning: Unable to retrieve source for @torch.jit._overload function'
]
LogHelper.info(tcpServerCommand)
reportDataInput.tcpServer.command = tcpServerCommand
LogHelper.info(pythonTCPServerCommand)
reportDataInput.pythonTCPServer.command = pythonTCPServerCommand
if (osInfo.platform === 'darwin') {
LogHelper.info(
'For the first start, it may take a few minutes to cold start the TCP server on macOS. No worries it is a one-time thing'
'For the first start, it may take a few minutes to cold start the Python TCP server on macOS. No worries it is a one-time thing'
)
}
let tcpServerOutput = ''
let pythonTCPServerOutput = ''
p.stdout.on('data', (data) => {
const newData = data.toString()
tcpServerOutput += newData
pythonTCPServerOutput += newData
if (newData?.toLowerCase().includes('waiting for')) {
kill(p.pid)
LogHelper.success('The TCP server can successfully start')
LogHelper.success('The Python TCP server can successfully start')
}
})
@ -327,10 +327,10 @@ dotenv.config()
// Ignore given warnings on stderr output
if (!ignoredWarnings.some((w) => newData.includes(w))) {
tcpServerOutput += newData
report.can_start_tcp_server.v = false
reportDataInput.tcpServer.error = newData
LogHelper.error(`Cannot start the TCP server: ${newData}`)
pythonTCPServerOutput += newData
report.can_start_python_tcp_server.v = false
reportDataInput.pythonTCPServer.error = newData
LogHelper.error(`Cannot start the Python TCP server: ${newData}`)
}
})
@ -339,18 +339,20 @@ dotenv.config()
setTimeout(() => {
kill(p.pid)
const error = `The TCP server timed out after ${timeout}ms`
const error = `The Python TCP server timed out after ${timeout}ms`
LogHelper.error(error)
reportDataInput.tcpServer.error = error
report.can_start_tcp_server.v = false
reportDataInput.pythonTCPServer.error = error
report.can_start_python_tcp_server.v = false
}, timeout)
p.stdout.on('end', async () => {
const tcpServerEnd = Date.now()
reportDataInput.tcpServer.output = tcpServerOutput
reportDataInput.tcpServer.startTime = `${tcpServerEnd - tcpServerStart}ms`
const pythonTCPServerEnd = Date.now()
reportDataInput.pythonTCPServer.output = pythonTCPServerOutput
reportDataInput.pythonTCPServer.startTime = `${
pythonTCPServerEnd - pythonTCPServerStart
}ms`
LogHelper.info(
`TCP server startup time: ${reportDataInput.tcpServer.startTime}\n`
`Python TCP server startup time: ${reportDataInput.pythonTCPServer.startTime}\n`
)
/**
@ -564,7 +566,7 @@ dotenv.config()
report.can_run.v &&
report.can_run_skill.v &&
report.can_text.v &&
report.can_start_tcp_server.v
report.can_start_python_tcp_server.v
) {
LogHelper.success('Hooray! Leon can run correctly')
LogHelper.info(

View File

@ -6,7 +6,7 @@ import { command } from 'execa'
import {
NODEJS_BRIDGE_SRC_PATH,
PYTHON_BRIDGE_SRC_PATH,
TCP_SERVER_SRC_PATH
PYTHON_TCP_SERVER_SRC_PATH
} from '@/constants'
import { LogHelper } from '@/helpers/log-helper'
import { LoaderHelper } from '@/helpers/loader-helper'
@ -29,7 +29,7 @@ BUILD_TARGETS.set('python-bridge', {
})
BUILD_TARGETS.set('tcp-server', {
workflowFileName: 'pre-release-tcp-server.yml',
versionFilePath: path.join(TCP_SERVER_SRC_PATH, 'version.py')
versionFilePath: path.join(PYTHON_TCP_SERVER_SRC_PATH, 'version.py')
})
;(async () => {
LoaderHelper.start()

View File

@ -81,7 +81,6 @@ async function downloadLLM() {
await stream.promises.finished(llmWriter)
LogHelper.success(`${LLM_NAME_WITH_VERSION} downloaded`)
LogHelper.success(`${LLM_NAME_WITH_VERSION} ready`)
} else {
LogHelper.success(

View File

@ -21,7 +21,10 @@ export const BINARIES_FOLDER_NAME = SystemHelper.getBinariesFolderName()
export const BRIDGES_PATH = path.join(process.cwd(), 'bridges')
export const NODEJS_BRIDGE_ROOT_PATH = path.join(BRIDGES_PATH, 'nodejs')
export const PYTHON_BRIDGE_ROOT_PATH = path.join(BRIDGES_PATH, 'python')
export const TCP_SERVER_ROOT_PATH = path.join(process.cwd(), 'tcp_server')
export const PYTHON_TCP_SERVER_ROOT_PATH = path.join(
process.cwd(),
'tcp_server'
)
export const NODEJS_BRIDGE_DIST_PATH = path.join(
NODEJS_BRIDGE_ROOT_PATH,
@ -31,11 +34,17 @@ export const PYTHON_BRIDGE_DIST_PATH = path.join(
PYTHON_BRIDGE_ROOT_PATH,
'dist'
)
export const TCP_SERVER_DIST_PATH = path.join(TCP_SERVER_ROOT_PATH, 'dist')
export const PYTHON_TCP_SERVER_DIST_PATH = path.join(
PYTHON_TCP_SERVER_ROOT_PATH,
'dist'
)
export const NODEJS_BRIDGE_SRC_PATH = path.join(NODEJS_BRIDGE_ROOT_PATH, 'src')
export const PYTHON_BRIDGE_SRC_PATH = path.join(PYTHON_BRIDGE_ROOT_PATH, 'src')
export const TCP_SERVER_SRC_PATH = path.join(TCP_SERVER_ROOT_PATH, 'src')
export const PYTHON_TCP_SERVER_SRC_PATH = path.join(
PYTHON_TCP_SERVER_ROOT_PATH,
'src'
)
const NODEJS_BRIDGE_VERSION_FILE_PATH = path.join(
NODEJS_BRIDGE_SRC_PATH,
@ -45,8 +54,8 @@ const PYTHON_BRIDGE_VERSION_FILE_PATH = path.join(
PYTHON_BRIDGE_SRC_PATH,
'version.py'
)
const TCP_SERVER_VERSION_FILE_PATH = path.join(
TCP_SERVER_SRC_PATH,
const PYTHON_TCP_SERVER_VERSION_FILE_PATH = path.join(
PYTHON_TCP_SERVER_SRC_PATH,
'version.py'
)
export const [, NODEJS_BRIDGE_VERSION] = fs
@ -55,18 +64,18 @@ export const [, NODEJS_BRIDGE_VERSION] = fs
export const [, PYTHON_BRIDGE_VERSION] = fs
.readFileSync(PYTHON_BRIDGE_VERSION_FILE_PATH, 'utf8')
.split("'")
export const [, TCP_SERVER_VERSION] = fs
.readFileSync(TCP_SERVER_VERSION_FILE_PATH, 'utf8')
export const [, PYTHON_TCP_SERVER_VERSION] = fs
.readFileSync(PYTHON_TCP_SERVER_VERSION_FILE_PATH, 'utf8')
.split("'")
export const NODEJS_BRIDGE_BIN_NAME = 'leon-nodejs-bridge.js'
export const PYTHON_BRIDGE_BIN_NAME = 'leon-python-bridge'
export const TCP_SERVER_BIN_NAME = 'leon-tcp-server'
export const PYTHON_TCP_SERVER_BIN_NAME = 'leon-tcp-server'
export const TCP_SERVER_BIN_PATH = path.join(
TCP_SERVER_DIST_PATH,
export const PYTHON_TCP_SERVER_BIN_PATH = path.join(
PYTHON_TCP_SERVER_DIST_PATH,
BINARIES_FOLDER_NAME,
TCP_SERVER_BIN_NAME
PYTHON_TCP_SERVER_BIN_NAME
)
export const PYTHON_BRIDGE_BIN_PATH = path.join(
PYTHON_BRIDGE_DIST_PATH,
@ -121,8 +130,12 @@ export const HAS_OVER_HTTP = process.env['LEON_OVER_HTTP'] === 'true'
export const HTTP_API_KEY = process.env['LEON_HTTP_API_KEY']
export const HTTP_API_LANG = process.env['LEON_HTTP_API_LANG']
export const TCP_SERVER_HOST = process.env['LEON_PY_TCP_SERVER_HOST']
export const TCP_SERVER_PORT = Number(process.env['LEON_PY_TCP_SERVER_PORT'])
export const PYTHON_TCP_SERVER_HOST = process.env['LEON_PY_TCP_SERVER_HOST']
export const PYTHON_TCP_SERVER_PORT = Number(
process.env['LEON_PY_TCP_SERVER_PORT']
)
export const LLM_TCP_SERVER_HOST = '0.0.0.0'
export const LLM_TCP_SERVER_PORT = 52420
export const IS_TELEMETRY_ENABLED = process.env['LEON_TELEMETRY'] === 'true'

View File

@ -22,7 +22,7 @@ import {
STT_PROVIDER,
TTS_PROVIDER
} from '@/constants'
import { TCP_CLIENT } from '@/core'
import { PYTHON_TCP_CLIENT } from '@/core'
import Nlu from '@/core/nlu'
import Brain from '@/core/brain'
import Asr from '@/core/asr/asr'
@ -221,10 +221,10 @@ server.handleOnConnection = (socket) => {
const provider = await addProvider(socket.id)
// Check whether the TCP client is connected to the TCP server
if (TCP_CLIENT.isConnected) {
if (PYTHON_TCP_CLIENT.isConnected) {
socket.emit('ready')
} else {
TCP_CLIENT.ee.on('connected', () => {
PYTHON_TCP_CLIENT.ee.on('connected', () => {
socket.emit('ready')
})
}

View File

@ -1,4 +1,11 @@
import { HOST, PORT, TCP_SERVER_HOST, TCP_SERVER_PORT } from '@/constants'
import {
HOST,
PORT,
PYTHON_TCP_SERVER_HOST,
PYTHON_TCP_SERVER_PORT,
LLM_TCP_SERVER_HOST,
LLM_TCP_SERVER_PORT
} from '@/constants'
import TCPClient from '@/core/tcp-client'
import HTTPServer from '@/core/http-server/http-server'
import SocketServer from '@/core/socket-server'
@ -11,13 +18,23 @@ import NaturalLanguageUnderstanding from '@/core/nlp/nlu/nlu'
import Brain from '@/core/brain/brain'
/**
* Register core singletons
* Register core nodes
*/
export const TCP_CLIENT = new TCPClient(
String(TCP_SERVER_HOST),
TCP_SERVER_PORT
export const PYTHON_TCP_CLIENT = new TCPClient(
'Python',
String(PYTHON_TCP_SERVER_HOST),
PYTHON_TCP_SERVER_PORT
)
export const LLM_TCP_CLIENT = new LLMTCPClient(
'LLM',
LLM_TCP_SERVER_HOST,
LLM_TCP_SERVER_PORT
)
/**
* Register core singletons
*/
export const HTTP_SERVER = new HTTPServer(String(HOST), PORT)

View File

@ -0,0 +1,13 @@
/**
* Duties:
*
* Custom NER
* Summarization
* Translation
* More accurate NLU (per domain list vs per skill list) / Utterance shortener or paraphraser
* Knowledge base / RAG
* Question answering
* Sentiment analysis
* Chit chat
* Intent fallback
*/

View File

@ -13,7 +13,7 @@ import type {
SkillCustomRegexEntityTypeSchema,
SkillCustomTrimEntityTypeSchema
} from '@/schemas/skill-schemas'
import { BRAIN, MODEL_LOADER, TCP_CLIENT } from '@/core'
import { BRAIN, MODEL_LOADER, PYTHON_TCP_CLIENT } from '@/core'
import { LogHelper } from '@/helpers/log-helper'
import { StringHelper } from '@/helpers/string-helper'
import { SkillDomainHelper } from '@/helpers/skill-domain-helper'
@ -197,10 +197,13 @@ export default class NER {
resolve(spacyEntities)
}
TCP_CLIENT.ee.removeAllListeners()
TCP_CLIENT.ee.on('spacy-entities-received', spacyEntitiesReceivedHandler)
PYTHON_TCP_CLIENT.ee.removeAllListeners()
PYTHON_TCP_CLIENT.ee.on(
'spacy-entities-received',
spacyEntitiesReceivedHandler
)
TCP_CLIENT.emit('get-spacy-entities', utterance)
PYTHON_TCP_CLIENT.emit('get-spacy-entities', utterance)
})
}

View File

@ -14,8 +14,14 @@ import type {
NLUResult
} from '@/core/nlp/types'
import { langs } from '@@/core/langs.json'
import { TCP_SERVER_BIN_PATH } from '@/constants'
import { TCP_CLIENT, BRAIN, SOCKET_SERVER, MODEL_LOADER, NER } from '@/core'
import { PYTHON_TCP_SERVER_BIN_PATH } from '@/constants'
import {
PYTHON_TCP_CLIENT,
BRAIN,
SOCKET_SERVER,
MODEL_LOADER,
NER
} from '@/core'
import { LogHelper } from '@/helpers/log-helper'
import { LangHelper } from '@/helpers/lang-helper'
import { ActionLoop } from '@/core/nlp/nlu/action-loop'
@ -70,14 +76,17 @@ export default class NLU {
BRAIN.talk(`${BRAIN.wernicke('random_language_switch')}.`, true)
// Recreate a new TCP server process and reconnect the TCP client
kill(global.tcpServerProcess.pid as number, () => {
global.tcpServerProcess = spawn(`${TCP_SERVER_BIN_PATH} ${locale}`, {
shell: true
})
kill(global.pythonTCPServerProcess.pid as number, () => {
global.pythonTCPServerProcess = spawn(
`${PYTHON_TCP_SERVER_BIN_PATH} ${locale}`,
{
shell: true
}
)
TCP_CLIENT.connect()
TCP_CLIENT.ee.removeListener('connected', connectedHandler)
TCP_CLIENT.ee.on('connected', connectedHandler)
PYTHON_TCP_CLIENT.connect()
PYTHON_TCP_CLIENT.ee.removeListener('connected', connectedHandler)
PYTHON_TCP_CLIENT.ee.on('connected', connectedHandler)
})
}

View File

@ -4,7 +4,7 @@ import { Server as SocketIOServer, Socket } from 'socket.io'
import { LANG, HAS_STT, HAS_TTS, IS_DEVELOPMENT_ENV } from '@/constants'
import {
HTTP_SERVER,
TCP_CLIENT,
PYTHON_TCP_CLIENT,
ASR,
STT,
TTS,
@ -86,11 +86,11 @@ export default class SocketServer {
// TODO
// const provider = await addProvider(socket.id)
// Check whether the TCP client is connected to the TCP server
if (TCP_CLIENT.isConnected) {
// Check whether the Python TCP client is connected to the Python TCP server
if (PYTHON_TCP_CLIENT.isConnected) {
this.socket?.emit('ready')
} else {
TCP_CLIENT.ee.on('connected', () => {
PYTHON_TCP_CLIENT.ee.on('connected', () => {
this.socket?.emit('ready')
})
}

View File

@ -15,10 +15,9 @@ interface ChunkData {
topic: string
data: unknown
}
type TCPClientName = 'Python' | 'LLM'
export default class TCPClient {
private static instance: TCPClient
private reconnectCounter = 0
private tcpSocket = new Net.Socket()
private _isConnected = false
@ -34,23 +33,21 @@ export default class TCPClient {
}
constructor(
private readonly name: TCPClientName,
private readonly host: string,
private readonly port: number
) {
if (!TCPClient.instance) {
LogHelper.title('TCP Client')
LogHelper.success('New instance')
TCPClient.instance = this
}
LogHelper.title(`${name} TCP Client`)
LogHelper.success('New instance')
this.name = name
this.host = host
this.port = port
this.tcpSocket.on('connect', () => {
LogHelper.title('TCP Client')
LogHelper.title(`${this.name} TCP Client`)
LogHelper.success(
`Connected to the TCP server tcp://${this.host}:${this.port}`
`Connected to the ${this.name} TCP server tcp://${this.host}:${this.port}`
)
this.reconnectCounter = 0
@ -59,7 +56,7 @@ export default class TCPClient {
})
this.tcpSocket.on('data', (chunk: ChunkData) => {
LogHelper.title('TCP Client')
LogHelper.title(`${this.name} TCP Client`)
LogHelper.info(`Received data: ${String(chunk)}`)
const data = JSON.parse(String(chunk))
@ -67,7 +64,7 @@ export default class TCPClient {
})
this.tcpSocket.on('error', (err: NodeJS.ErrnoException) => {
LogHelper.title('TCP Client')
LogHelper.title(`${this.name} TCP Client`)
if (err.code === 'ECONNREFUSED') {
this.reconnectCounter += 1
@ -75,17 +72,17 @@ export default class TCPClient {
const { type: osType } = SystemHelper.getInformation()
if (this.reconnectCounter >= RETRIES_NB) {
LogHelper.error('Failed to connect to the TCP server')
LogHelper.error(`Failed to connect to the ${this.name} TCP server`)
this.tcpSocket.end()
}
if (this.reconnectCounter >= 1) {
LogHelper.info('Trying to connect to the TCP server...')
LogHelper.info(`Trying to connect to the ${this.name} TCP server...`)
if (this.reconnectCounter >= 5) {
if (osType === OSTypes.MacOS) {
LogHelper.warning(
'The cold start of the TCP server can take a few more seconds on macOS. It should be a one-time thing, no worries'
`The cold start of the ${this.name} TCP server can take a few more seconds on macOS. It should be a one-time thing, no worries`
)
}
}
@ -95,15 +92,17 @@ export default class TCPClient {
}, INTERVAL * this.reconnectCounter)
}
} else {
LogHelper.error(`Failed to connect to the TCP server: ${err}`)
LogHelper.error(
`Failed to connect to the ${this.name} TCP server: ${err}`
)
}
this._isConnected = false
})
this.tcpSocket.on('end', () => {
LogHelper.title('TCP Client')
LogHelper.success('Disconnected from the TCP server')
LogHelper.title(`${this.name} TCP Client`)
LogHelper.success(`Disconnected from the ${this.name} TCP server`)
this._isConnected = false
})

View File

@ -1,12 +1,9 @@
import type { ChildProcessWithoutNullStreams } from 'node:child_process'
import TCPClient from '@/core/tcp-client'
declare global {
/* eslint-disable no-var */
var tcpServerProcess: ChildProcessWithoutNullStreams
var tcpClient: TCPClient
var pythonTCPServerProcess: ChildProcessWithoutNullStreams
}
export {}

View File

@ -5,26 +5,26 @@ import {
IS_DEVELOPMENT_ENV,
IS_TELEMETRY_ENABLED,
LANG as LEON_LANG,
TCP_SERVER_BIN_PATH
PYTHON_TCP_SERVER_BIN_PATH
} from '@/constants'
import { TCP_CLIENT, HTTP_SERVER, SOCKET_SERVER } from '@/core'
import { PYTHON_TCP_CLIENT, HTTP_SERVER, SOCKET_SERVER } from '@/core'
import { Telemetry } from '@/telemetry'
import { LangHelper } from '@/helpers/lang-helper'
import { LogHelper } from '@/helpers/log-helper'
;(async (): Promise<void> => {
process.title = 'leon'
// Start the TCP server
global.tcpServerProcess = spawn(
`${TCP_SERVER_BIN_PATH} ${LangHelper.getShortCode(LEON_LANG)}`,
// Start the Python TCP server
global.pythonTCPServerProcess = spawn(
`${PYTHON_TCP_SERVER_BIN_PATH} ${LangHelper.getShortCode(LEON_LANG)}`,
{
shell: true,
detached: IS_DEVELOPMENT_ENV
}
)
// Connect the TCP client to the TCP server
TCP_CLIENT.connect()
// Connect the Python TCP client to the Python TCP server
PYTHON_TCP_CLIENT.connect()
// Start the HTTP server
await HTTP_SERVER.init()
@ -65,7 +65,7 @@ import { LogHelper } from '@/helpers/log-helper'
process.on(eventType, () => {
Telemetry.stop()
global.tcpServerProcess.kill()
global.pythonTCPServerProcess.kill()
setTimeout(() => {
process.exit(0)

View File

@ -126,6 +126,18 @@
"[Add|Append] @todos"
],
"entities": [
{
"type": "llm",
"items": {
"type": "array",
"items": {
"type": "string"
}
},
"list_name": {
"type": "string"
}
},
{
"type": "trim",
"name": "todos",