From 901f5e320f0658ab9346751aedb8e7241b03b6ca Mon Sep 17 00:00:00 2001 From: Riceball LEE Date: Fri, 16 Aug 2024 11:27:58 +0800 Subject: [PATCH] refactor!: extract brain command and common funcs as packages --- .gitignore | 1 + package.json | 45 +++-- src/lib/brain.ts | 263 --------------------------- src/lib/init-tools.ts | 14 +- src/lib/load-config.ts | 69 ------- src/lib/u-text.ts | 34 ---- src/oclif/commands/brain/download.ts | 165 ----------------- src/oclif/commands/brain/index.ts | 89 --------- src/oclif/commands/brain/list.ts | 87 --------- src/oclif/commands/brain/refresh.ts | 39 ---- src/oclif/commands/config/index.ts | 4 +- src/oclif/commands/config/save.ts | 2 +- src/oclif/commands/run/index.ts | 8 +- src/oclif/commands/run/world.ts | 18 +- src/oclif/commands/test/index.ts | 8 +- src/oclif/hooks/init-tools.ts | 9 +- src/oclif/lib/ai-command.ts | 126 +------------ src/oclif/lib/help.ts | 27 +-- 18 files changed, 57 insertions(+), 951 deletions(-) delete mode 100644 src/lib/brain.ts delete mode 100644 src/lib/load-config.ts delete mode 100644 src/lib/u-text.ts delete mode 100644 src/oclif/commands/brain/download.ts delete mode 100644 src/oclif/commands/brain/index.ts delete mode 100644 src/oclif/commands/brain/list.ts delete mode 100644 src/oclif/commands/brain/refresh.ts diff --git a/.gitignore b/.gitignore index f4f8ff0..67e39dd 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ /tmp /node_modules oclif.manifest.json +dev.md yarn.lock diff --git a/package.json b/package.json index a414e8f..07d2fbc 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@offline-ai/cli", "description": "Your Offline(local) AI agents client", "version": "0.1.3", - "author": "Riceball LEE @snowyu", + "author": "Riceball LEE ", "bin": { "ai": "./bin/run.js" }, @@ -15,28 +15,24 @@ "@isdk/ai-tool-downloader": "workspace:*", "@isdk/ai-tool-llm": "workspace:*", "@isdk/ai-tool-llm-llamacpp": "workspace:*", - "@isdk/ai-tool-model": "workspace:*", "@isdk/ai-tool-prompt": "workspace:*", - "@isdk/ai-tool-sqlite": "workspace:*", "@isdk/detect-text-language": "^0.1.0", - "@oclif/core": "^4.0.8", - "@oclif/plugin-autocomplete": "^3.1.5", - "@oclif/plugin-help": "^6.2.4", - "@oclif/plugin-not-found": "^3.2.8", + "@oclif/core": "^4.0.17", + "@oclif/plugin-autocomplete": "^3.2.0", + "@oclif/plugin-help": "^6.2.8", + "@oclif/plugin-not-found": "^3.2.16", "@oclif/plugin-plugins": "^5", - "@oclif/plugin-version": "^2.2.5", + "@oclif/plugin-version": "^2.2.10", "@oclif/plugin-warn-if-update-available": "^3.1.11", + "@offline-ai/cli-common": "workspace:*", + "@offline-ai/cli-plugin-cmd-brain": "workspace:*", "ansi-colors": "^4.1.3", - "ansis": "^3.2.0", "better-sqlite3": "^11.1.2", - "cfonts": "^3.3.0", - "cli-spinners": "^3.1.0", "color-json": "^3.0.5", "enquirer": "^2.4.1", - "figlet": "^1.7.0", "lodash-es": "^4.17.21", - "log-update": "^6.0.0", - "luxon": "^3.4.4", + "log-update": "^6.1.0", + "luxon": "^3.5.0", "node-fetch-native": "^1.6.4", "util-ex": "2.0.0-alpha.18" }, @@ -44,21 +40,20 @@ "@oclif/prettier-config": "^0.2.1", "@oclif/test": "^4", "@types/chai": "^4", - "@types/figlet": "^1.5.8", "@types/lodash-es": "^4.17.12", "@types/mocha": "^10", "@types/node": "^20", "chai": "^5", "eslint": "^8", - "eslint-config-oclif": "^5.2.0", + "eslint-config-oclif": "^5.2.1", "eslint-config-oclif-typescript": "^3", "eslint-config-prettier": "^9", "mocha": "^10", - "oclif": "^4.13.12", + "oclif": "^4.14.19", "shx": "^0.3.4", "ts-node": "^10", - "tsup": "^8.1.0", - "tsx": "^4.16.0", + "tsup": "^8.2.4", + "tsx": "^4.17.0", "typescript": "^5" }, "engines": { @@ -84,17 +79,21 @@ "commands": "./dist/oclif/commands", "helpClass": "./dist/oclif/lib/help", "hooks": { - "init_tools": "./dist/oclif/hooks/init-tools" + "config:load": "./dist/oclif/hooks/init-tools" }, "plugins": [ "@oclif/plugin-help", "@oclif/plugin-plugins", "@oclif/plugin-version", "@oclif/plugin-autocomplete", - "@oclif/plugin-warn-if-update-available" + "@oclif/plugin-warn-if-update-available", + "@offline-ai/cli-plugin-cmd-brain" ], - "scope": "isdk", - "pluginPrefix": "ai-plugin", + "additionalHelpFlags": [ + "-h" + ], + "scope": "offline-ai", + "pluginPrefix": "cli-plugin", "theme": "theme.json", "topicSeparator": " ", "warn-if-update-available": { diff --git a/src/lib/brain.ts b/src/lib/brain.ts deleted file mode 100644 index 824a3af..0000000 --- a/src/lib/brain.ts +++ /dev/null @@ -1,263 +0,0 @@ -import path from 'path' -import { - ServerTools as ToolFunc, -} from '@isdk/ai-tool' -import { AIModelQuantType, AIModelSettings } from '@isdk/ai-tool-llm' -import type { LlmModelsFunc } from '@isdk/ai-tool-model' -import { DownloadProgressEventName, DownloadStatusEventName, FileDownloadStatus, download } from '@isdk/ai-tool-downloader' -import { BRAINS_FUNC_NAME } from './init-tools.js' -import EventEmitter from 'events' - -EventEmitter.defaultMaxListeners = 1000 - -export async function upgradeBrains(flags?: any) { - const brains = ToolFunc.get(BRAINS_FUNC_NAME) as LlmModelsFunc - let _models: AIModelSettings[]|undefined - let shouldBreak: boolean|undefined - - brains.on('brain:refresh', onRefresh) - process.on('SIGINT', interrupted) - - try { - const count = await brains.$refresh(flags) - return count - } finally { - brains.off('brain:refresh', onRefresh) - process.off('SIGINT', interrupted) - if (shouldBreak) {console.log('saved.')} - } - - function onRefresh(tool_name: string, act: string, model: AIModelSettings, models: AIModelSettings[]|string){ - let s = model._id! - if (s.includes(':')) {s = ' ' + s} - if (Array.isArray(models)) { - _models = models - } else if(models && typeof models === 'string') { - s += ' ' + models - } - console.log(act, s) - if (shouldBreak) {this.result = true} - } - - async function interrupted() { - if (_models) { - console.log('wait to exit...') - shouldBreak = true - } else { - process.exit(0) - } - } -} - -export async function listBrains(userConfig: any, flags: any) { - const brainDir = userConfig.brainDir - const result = await searchBrains(brainDir, flags) - return result -} - -export async function searchBrains(brainDir: string, flags: any) { - const brains = ToolFunc.get(BRAINS_FUNC_NAME) as LlmModelsFunc - let result: AIModelSettings[]|undefined - if (flags.name && flags.name.startsWith('hf://')) { - const model = await brains.getModel(flags.name, flags.hubUrl) - if (model) {result = [model]} - } else { - const filter:any = flags.search ?? {} - const onlyFeatured = flags.onlyFeatured - if (!flags.all) { - if (flags.downloaded) { - filter.downloaded = {'=': true} - } else { - filter.$or = [{downloaded: {'!=': true}}, {downloaded: null}] - } - // filter.downloaded = flags.downloaded ? {'=': true} : {'!=': true} - // if (!flags.downloaded && onlyFeatured === undefined) { - // // the defaults for online is true - // onlyFeatured = true - // } - - if (onlyFeatured) { - filter.featured = true - } - } - - if (flags.name) { - filter._id = {'$like': `%${flags.name}%`} - } - - result = brains.$search({filter}) as AIModelSettings[] - } - return result -} - -export async function verifyBrains(brains: AIModelSettings[]) { - const brainFunc = ToolFunc.get(BRAINS_FUNC_NAME) as LlmModelsFunc - for (const brain of brains) { - const changed = await brainFunc.verifyFileExists(brain) - if (changed) { - brainFunc.put({id: brain._id, val: brain}) - } - } -} - -export function printBrains(brains: AIModelSettings[], flags?: {count?: number}) { - let maxArrayLength = flags?.count ?? 100 - if (maxArrayLength >= 0) {maxArrayLength--} - for (let i=0; i 0) && (i >= maxArrayLength)) {break} - } - if (brains.length > maxArrayLength) { - maxArrayLength++ - console.log((maxArrayLength+1)+`. …… and ${brains.length - maxArrayLength} more\n`) - } - console.log('total:', brains.length) -} - -export function getQuantsFromBrain(brain: AIModelSettings) { - return getFileInfo(brain).map(item => item.quant) -} - -export async function downloadBrain(brain: AIModelSettings, options: { - quant: number, url?: string, dryRun?: boolean, onStatus?: Function, onProgress?: Function, - logLevel?: string, -}) { - const quant = options.quant - const onProgress = options.onProgress - const brains = ToolFunc.get(BRAINS_FUNC_NAME) as LlmModelsFunc - const dryRun = options.dryRun - - if (typeof onProgress === 'function') { - brains.on('model:'+DownloadStatusEventName, (_name: string, status: string, info: any) => { - if (info.old === info.quant) {delete info.old} - onProgress('status', status, info) - }) - } - - let downTasks = await brains.$download({id: brain._id, quant, url: options.url, dryRun}) - if (downTasks) { - if (!Array.isArray(downTasks)) {downTasks = [downTasks]} - downTasks = downTasks.filter(Boolean) - } - - if (dryRun) return downTasks - - return new Promise((resolve, reject) => { - if (downTasks) { - // if (!Array.isArray(downTasks)) {downTasks = [downTasks]} - // downTasks = downTasks.filter(Boolean) - let leftCount = downTasks.length - const errs: string[] = [] - for (const downTask of downTasks) { - if (typeof onProgress === 'function') { - download.on(DownloadProgressEventName+':'+downTask!.id, onProgress) - } - download.on(DownloadStatusEventName+':'+downTask!.id, function(_name: string, status: FileDownloadStatus, idInfo: {url: string, id: string, filepath: string}) { - // console.log(_name, status, idInfo) - if (status === 'completed') { - --leftCount - if (leftCount === 0) resolve(downTasks) - } else if (status !== 'downloading') { - --leftCount - errs.push(`download ${idInfo.filepath} failed : ${status}`) - console.error(`download ${idInfo.filepath} failed : ${status}`) - } - if (leftCount === 0) { - if (errs.length > 0) { - reject(new Error(errs.join('\n'))) - } else { - resolve(downTasks) - } - } - if (typeof options.onStatus === 'function') { - try { - options.onStatus.call(this, _name, status, idInfo) - } catch (err) { - console.error(err) - } - } - }) - } - } else { - reject(new Error('no download')) - } - }) -} - -function sprintBrainInfo(brain: AIModelSettings) { - const keys = ['name', 'likes', 'downloads', 'hf_repo'] - let result = '' - for (let i=0; i acc + cur.file_size!, 0) - const downloadedCount = file.filter(item => item.downloaded).length - if (downloadedCount === file.length) { - downloaded = '[downloaded]' - } else if (downloadedCount > 0) { - downloaded = `[${downloadedCount}/${file.length} downloaded]` - } - - file = {...file[0], count: file.length, file_size} - } else {continue} - } else { - downloaded = file.downloaded ? '[downloaded]' : '' - } - result.push({quant: AIModelQuantType[file.quant!],file_name: path.basename(file.location || file.file_name!), count: file.count, file_size: file.file_size!, downloaded}) - } - return result -} - -function sprintBrainFileInfo(brain: AIModelSettings) { - const info = getFileInfo(brain) - let result = '' - for (let i = 0; i < info.length; i++) { - const item = info[i] - result += ` * ${item.quant}: ${item.file_name}` - if (item.count > 1) { - result += ` (${item.count} files)` - } - const fileSize = item.file_size - if (fileSize > 0) { - result += ' - ' + sizeToStr(fileSize) - } - if (item.downloaded) { - result += ' - ' + item.downloaded - } - - if (i < info.length - 1) { - result += '\n' - } - } - return result -} - -function sizeToStr(num: number, fractionDigits = 2) { - let result = '' - if (num >= 1e12) { result = (num / 1e12).toFixed(fractionDigits) + 'T' } - else if (num >= 1e9) { result = (num / 1e9).toFixed(fractionDigits) + 'G' } - else if (num >= 1e6) { result = (num / 1e6).toFixed(fractionDigits) + 'M' } - else if (num >= 1e3) { result = (num / 1e3).toFixed(fractionDigits) + 'K' } - else { result = num.toFixed(fractionDigits) } - return result -} diff --git a/src/lib/init-tools.ts b/src/lib/init-tools.ts index 471019b..5cc5545 100644 --- a/src/lib/init-tools.ts +++ b/src/lib/init-tools.ts @@ -6,18 +6,14 @@ import { ServerTools, } from '@isdk/ai-tool' import { llm } from '@isdk/ai-tool-llm' -import {LlmModelsFunc } from '@isdk/ai-tool-model' import { LlamaCppProviderName, llamaCpp } from '@isdk/ai-tool-llm-llamacpp' import { AIPromptsFunc, AIPromptsName } from '@isdk/ai-tool-prompt' import { download } from '@isdk/ai-tool-downloader' +import type { Hook, Config } from '@oclif/core' export const BRAINS_FUNC_NAME = 'llm.brains' -let initialized: boolean = false -export async function initTools(userConfig: any) { - if (initialized) return - - initialized = true +export async function initTools(this: Hook.Context, userConfig: any, _config: Config) { try { const promptsFunc = new AIPromptsFunc(AIPromptsName, {dbPath: ':memory:', initDir: userConfig.promptsDir}) @@ -31,12 +27,6 @@ export async function initTools(userConfig: any) { backendEventable(ResServerTools) ResServerTools.register(download) - if (userConfig.brainDir) { - const brainsFunc = new LlmModelsFunc(BRAINS_FUNC_NAME, {rootDir: userConfig.brainDir, dbPath: '.brainsdb'}) - ResServerTools.register(brainsFunc) - // brainsFunc.updateDBFromDir() - } - if (userConfig.apiUrl) { llamaCpp.apiUrl = userConfig.apiUrl } diff --git a/src/lib/load-config.ts b/src/lib/load-config.ts deleted file mode 100644 index 0b24db2..0000000 --- a/src/lib/load-config.ts +++ /dev/null @@ -1,69 +0,0 @@ -export const DEFAULT_CONFIG_NAME = '.ai' - -import { ConfigFile, expandObjEnv} from "@isdk/ai-tool"; -import { defaultsDeep, omit } from "lodash-es" -import path from 'path' -import type { Config } from "@oclif/core"; - -export function loadConfigFile(filename: string, searchPaths: string[] = ['.']) { - if (path.isAbsolute(filename)) {return ConfigFile.loadSync(filename)} - - const configs = searchPaths.map(p => { - return ConfigFile.loadSync(path.resolve(p, filename)) - }).filter(Boolean); - return defaultsDeep({}, ...configs) -} - -export function expandConfig(config: any, defaultConfig: any) { - const processEnv = { ...process.env, ...defaultConfig } - - return expandObjEnv(config, { - processEnv, - parsed: processEnv - }) -} - -export function loadConfig(filename: string, config: Config) { - let defaultConfig = ConfigFile.loadSync(path.resolve(config.configDir, filename)) - if (!defaultConfig) { - defaultConfig = { - configDirs: ['$XDG_BIN_HOME', config.configDir, '$HOME'], - brainDir: [path.join(config.dataDir, 'brain')], - agentDirs: [path.join(config.dataDir, 'agent'), '$PWD'], - promptDirs: [path.join(config.dataDir, 'prompt')], - chatsDir: path.join(config.dataDir, 'log', 'chats'), - inputsDir: path.join(config.dataDir, 'log', 'inputs'), - } - } - const XDGConfigs = getXDGConfigs(config) - for (const [key, value] of Object.entries(XDGConfigs)) { - defaultConfig[key] = value - } - - expandConfig(defaultConfig, defaultConfig) - const searchPaths = defaultConfig.configDirs - if (defaultConfig.AI_CONFIG_BASENAME) { - filename = defaultConfig.AI_CONFIG_BASENAME - } - const result = expandConfig(loadConfigFile(filename, searchPaths), defaultConfig) - return defaultsDeep(result, omit(defaultConfig, Object.keys(XDGConfigs))) -} - -export function loadAIConfig(config: Config) { - return loadConfig(DEFAULT_CONFIG_NAME, config) -} - -export function getXDGConfigs(config: Config) { - const result = { - XDG_CONFIG_HOME: config.configDir, - XDG_DATA_HOME: config.dataDir, - XDG_CACHE_HOME: config.cacheDir, - XDG_BIN_HOME: path.dirname(config.options.root), - } - return result -} - -export function expandPath(path: string, config?: any) { - const result = expandConfig(path, config) - return result -} diff --git a/src/lib/u-text.ts b/src/lib/u-text.ts deleted file mode 100644 index b39fa9b..0000000 --- a/src/lib/u-text.ts +++ /dev/null @@ -1,34 +0,0 @@ -import colors from 'ansi-colors' -import figlet from 'figlet' - -const textSync = figlet.textSync - -const excludeFonts = new Set([ - 'Gradient', 'Hex', 'Trek', 'Term', 'Runic', 'Runyc', 'Rot13', 'Octal', 'Mshebrew210', 'Mike', '1Row', 'DWhistled', - 'Tengwar', 'Small Tengwar', 'Benjamin', -]) - -const fonts = figlet.fontsSync().filter((f: string) => !excludeFonts.has(f)) - -function getRandomInt(size: number) { - return Math.floor(Math.random() * size); -} - -export function uText(s: string, options: any= {}) { - const color = options.color || 'gray' - if (!options.font) { - // '3D-ASCII' - options.font = fonts[getRandomInt(fonts.length)] - } - // horizontalLayout: 'default', - // verticalLayout: 'default', - return colors[color as any]( - textSync(s, options) - ); -} - -// import cfonts from 'cfonts' - -// export function uText(s: string, options: any = {}) { -// cfonts.say(s, options) -// } diff --git a/src/oclif/commands/brain/download.ts b/src/oclif/commands/brain/download.ts deleted file mode 100644 index 02d8642..0000000 --- a/src/oclif/commands/brain/download.ts +++ /dev/null @@ -1,165 +0,0 @@ -import path from 'path' -import enquier from 'enquirer' -import { Args, Flags } from '@oclif/core' -import { AICommand } from '../../lib/ai-command.js' -import { showBanner } from '../../lib/help.js' -import { downloadBrain, getQuantsFromBrain, listBrains } from '../../../lib/brain.js' -import { AIModelQuantType } from '@isdk/ai-tool-llm' -import logUpdate from 'log-update' - -const AIModelQuantTypes = Object.keys(AIModelQuantType).filter(k => (typeof AIModelQuantType[k] === 'number') && k !== 'Guessed') -const prompt = enquier.prompt - -export default class DownloadBrainCommand extends AICommand { - static aliases = ['brain:dn', 'brain:down'] - static args = { - name: Args.string({ - description: 'the brain name to download', - }) - } - - static flags = { - ...AICommand.flags, - brainDir: Flags.directory({char: 'b', description: 'the brains(LLM) directory', exists: true}), - quant: Flags.string({ - char: 'q', - description: 'the quantization of the model, defaults to 4bit', - options: AIModelQuantTypes, - }), - hubUrl: Flags.string({ - char: 'u', - aliases: ['hub-url'], - description: 'the hub mirror url', - }), - dryRun: Flags.boolean({ - char: 'd', - aliases: ['dry-run'], - description: 'dry run, do not download', - }), - } - - static summary = '🧠 The AI Agent Brains(LLM) Downloader.' - - static description = ` - 📥 download 🧠 brains to brainDir. -` - - static examples = [` -<%= config.bin %> <%= command.id %> [-q ] -`, - ] - - async run(): Promise { - const opts = await this.parse(DownloadBrainCommand) - const isJson = this.jsonEnabled() - const {args, flags} = opts - const userConfig = this.loadConfig(flags.config, opts) - await this.config.runHook('init_tools', {id: 'brain:download', userConfig}) - - process.on('SIGINT', ()=>{ - process.exit(0) - }) - - if (userConfig.banner && !isJson) {showBanner('Brain')} - flags.name = args.name - if (!flags.name) { - const {name} = await prompt<{name: string}>({ - type: 'input', - name: 'name', - message: 'Enter the brain(LLM) name to download', - }) - if (!name) { - this.log('No brain name provided') - return - } - flags.name = name - } - flags.onlyFeatured = false - flags.all = true - let brain: any = await listBrains(userConfig, flags) - if (!brain || brain.length === 0) { - this.log('No Such brains found') - return - } else if (brain.length > 1) { - const {index} = await prompt<{index: number}>({ - type: 'autocomplete', - name: 'index', - message: 'Select the brain(LLM) to download', - choices: brain.map((b, index) => ({message: b.author +'/' + b.name, value: index})), - }) - if (index === undefined) { - this.log('No brain name provided') - return - } - brain = brain[index] - } else { - brain = brain[0] - } - - const quants = getQuantsFromBrain(brain) - if (flags.quant && !quants.includes(flags.quant)) { - this.log(`Your chosen brain has no such quantization level: ${flags.quant}`) - flags.quant = undefined - } - - if (!flags.quant) { - const {quant} = await prompt<{quant: string}>({ - type: 'autocomplete', - name: 'quant', - message: 'Choose the quantization level for the brain compression (lossy)', - choices: quants, - }) - if (!quant) { - this.log('No quantization provided') - return - } - flags.quant = quant - } - - const quant = AIModelQuantType[flags.quant] - const progresses: any = {} - const onProgress = function(_name: string, progress: {percent:number, totalBytes:number, transferredBytes:number}, idInfo: {url: string, id?: string, filepath?: string}) { - if (_name === 'status') { - const status = progress as unknown as string - const info = idInfo as unknown as {url: string, quant: number, old: number} - const quant = AIModelQuantType[info.quant] - const old = AIModelQuantType[info.old] - logUpdate(status + ' ' + info.url + ' quant' + (info.old ? `(from ${old} to ${quant})` : `(${quant})`) ) - return - } - // console.log('🚀 ~ DownloadBrainCommand ~ onProgress ~ progress:', arguments) - progresses[idInfo.url] = `Downloading ${idInfo.url}... ${(progress.percent * 100).toFixed(2)}% ${progress.transferredBytes} bytes` - const info = Object.keys(progresses).map(k => progresses[k]).join('\n') - logUpdate(info) - } - this.log(brain._id, flags.quant, 'Downloading to ' + userConfig.brainDir) - let result: any[] - try { - result = await downloadBrain(brain, { - quant, onProgress, url: flags.hubUrl, dryRun: flags.dryRun, - logLevel: userConfig.logLevel, - }) - } finally { - logUpdate.clear() - } - if (result?.length && !isJson) { - const width = calcIntWidth(result.length) - // result = result.map(item => omitBy(item, (v, k) => k === 'id' || v == null)) - result.forEach((item, index) => { - this.log((++index).toString().padStart(width, ' ') + '. ' + item.url) - this.log(' '.padStart(width, ' ') + ' ' + path.join(userConfig.brainDir, item.filepath)) - }) - } - this.log('done') - return result - } -} - -function calcIntWidth(num: number) { - let width = 0 - while (num > 0) { - num = Math.floor(num / 10) - width++ - } - return width -} \ No newline at end of file diff --git a/src/oclif/commands/brain/index.ts b/src/oclif/commands/brain/index.ts deleted file mode 100644 index 5e8c4d6..0000000 --- a/src/oclif/commands/brain/index.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { Args, Flags } from '@oclif/core' -import { AICommand } from '../../lib/ai-command.js' -import { showBanner } from '../../lib/help.js' -import { parseJsJson } from '@isdk/ai-tool' -import { listBrains, printBrains, upgradeBrains, verifyBrains } from '../../../lib/brain.js' - -export default class Brain extends AICommand { - static args = { - name: Args.string({ - description: 'the brain name to search', - }) - } - - static flags = { - ...AICommand.flags, - brainDir: Flags.directory({char: 'b', description: 'the brains(LLM) directory', exists: true}), - search: Flags.string({ - char: 's', - description: 'the json filter to search for brains', - parse: (input: string) => parseJsJson(input), - }), - count: Flags.integer({ - char: 'n', - description: 'the max number of brains to list, 0 means all.', - default: 100, - }), - refresh: Flags.boolean({ - char: 'r', - description: 'refresh the online brains list', - }), - hubUrl: Flags.string({ - char: 'u', - aliases: ['hub-url'], - description: 'the hub mirror url', - dependsOn: ['refresh'], - }), - verifyQuant: Flags.boolean({ - char: 'v', - aliases: ['verify-quant'], - description: 'whether verify quant when refresh', - dependsOn: ['refresh'], - }), - } - - static summary = '🧠 The AI Agent Brains(LLM) Manager.' - - static description = ` - Manage AI Agent brains 🧠 here. - 📜 List downloaded or online brains - 🔎 search for brains - 📥 download brains - ❌ delete brains -` - - static examples = [` -<%= config.bin %> <%= command.id %> # list download brains -<%= config.bin %> <%= command.id %> list --online # list online brains -<%= config.bin %> <%= command.id %> download -`, - ] - - async run(): Promise { - const opts = await this.parse(Brain) - const isJson = this.jsonEnabled() - const {args, flags} = opts - const userConfig = this.loadConfig(flags.config, opts) - await this.config.runHook('init_tools', {id: 'brain', userConfig}) - - if (flags.refresh) { - const count = await upgradeBrains(flags) - this.log(`${count} brains updated`) - return count - } - - if (userConfig.banner && !isJson) {showBanner('Brain')} - flags.name = args.name - flags.downloaded = true - const result = await listBrains(userConfig, flags) - if (result) { await verifyBrains(result) } - if (!isJson) { - if (!result || result.length === 0) { - this.log('No brains found') - } else { - printBrains(result, flags as any) - } - } - return result - } -} diff --git a/src/oclif/commands/brain/list.ts b/src/oclif/commands/brain/list.ts deleted file mode 100644 index 92d423f..0000000 --- a/src/oclif/commands/brain/list.ts +++ /dev/null @@ -1,87 +0,0 @@ -// import util from 'util' -import { parseJsJson } from '@isdk/ai-tool' -import { listBrains, printBrains, upgradeBrains } from '../../../lib/brain.js' -import { AICommand } from '../../lib/ai-command.js' -import { showBanner } from '../../lib/help.js' -import { Args, Flags } from '@oclif/core' - -export default class AIBrainListCommand extends AICommand { - static summary = '📜 List downloaded or online brains, defaults to downloaded.' - static aliases = ['brain:search'] - - // static description = '' - - static args = { - name: Args.string({ - description: 'the brain name to search', - }) - } - - static flags = { - ...AICommand.flags, - downloaded: Flags.boolean({ - char: 'd', - description: 'list downloaded brains', - default: false, - }), - all: Flags.boolean({ - char: 'a', - description: 'list all brains(include downloaded and online)', - default: false, - }), - brainDir: Flags.directory({char: 'b', description: 'the brains(LLM) directory', exists: true}), - onlyFeatured: Flags.boolean({ - char: 'f', - aliases: ['only-featured'], - description: 'only list featured brains', - allowNo: true, - }), - search: Flags.string({ - char: 's', - description: 'the json filter to search for brains', - parse: (input: string) => parseJsJson(input), - }), - count: Flags.integer({ - char: 'n', - description: 'the max number of brains to list, 0 means all.', - default: 100, - }), - refresh: Flags.boolean({ - char: 'r', - description: 'refresh the online brains list', - }), - hubUrl: Flags.string({ - char: 'u', - aliases: ['hub-url'], - description: 'the hub mirror url', - dependsOn: ['refresh'], - }), - } - - async run(): Promise { - const opts = await this.parse(AIBrainListCommand) - const isJson = this.jsonEnabled() - const {args, flags} = opts - const userConfig = this.loadConfig(flags.config, opts) - await this.config.runHook('init_tools', {id: 'brain', userConfig}) - - if (flags.refresh) { - const count = await upgradeBrains(flags) - this.log(`${count} brains updated`) - } - - if (userConfig.banner && !isJson) {showBanner('Brain')} - flags.name = args.name - const result = await listBrains(userConfig, flags) - if (!isJson) { - if (!result || result.length === 0) { - this.log('No brains found') - } else { - printBrains(result, flags as any) - // console.log(util.inspect(result, {showHidden: false, depth: 10, colors: true, maxArrayLength: 6})) - // console.log('total:', result.length) - } - } - return result - } -} diff --git a/src/oclif/commands/brain/refresh.ts b/src/oclif/commands/brain/refresh.ts deleted file mode 100644 index 9b3016b..0000000 --- a/src/oclif/commands/brain/refresh.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Flags } from '@oclif/core' -import { upgradeBrains } from '../../../lib/brain.js' -import { AICommand } from '../../lib/ai-command.js' -import { showBanner } from '../../lib/help.js' - -export default class AIBrainRefreshCommand extends AICommand { - static summary = '🔄 refresh online brains.' - - static description = 'refresh brain index from huggingface.co' - static flags = { - brainDir: Flags.directory({char: 'b', description: 'the brains(LLM) directory', exists: true}), - hubUrl: Flags.string({ - char: 'u', - aliases: ['hub-url'], - description: 'the hub mirror url', - }), - verifyQuant: Flags.boolean({ - char: 'v', - aliases: ['verify-quant'], - description: 'whether verify quant when refresh', - default: true, - }), - } - - async run(): Promise { - const opts = await this.parse(AIBrainRefreshCommand) - const isJson = this.jsonEnabled() - const {flags} = opts - const userConfig = this.loadConfig(flags.config, opts) - await this.config.runHook('init_tools', {id: 'brain', userConfig}) - - if (userConfig.banner && !isJson) {showBanner('Refresh Brains')} - - const count = await upgradeBrains(flags) - this.log(`${count} brains updated`) - - return count - } -} diff --git a/src/oclif/commands/config/index.ts b/src/oclif/commands/config/index.ts index 7f35b01..526103f 100644 --- a/src/oclif/commands/config/index.ts +++ b/src/oclif/commands/config/index.ts @@ -1,5 +1,5 @@ import { Args } from '@oclif/core' -import { getXDGConfigs } from '../../../lib/load-config.js' +import { getXDGConfigs } from '@offline-ai/cli-common' import { AICommand } from '../../lib/ai-command.js' import { showBanner } from '../../lib/help.js' import { get as getByPath } from 'lodash-es' @@ -38,7 +38,7 @@ AI Configuration: const opts = await this.parse(AIConfigCommand) const {args, flags} = opts const isJson = this.jsonEnabled() - const userConfig = this.loadConfig(flags.config, opts) + const userConfig = await this.loadConfig(flags.config, {...opts, skipLoadHook: true}) const hasBanner = flags.banner ?? userConfig.banner ?? true if (hasBanner && !isJson) {showBanner('Config')} this.log('AI Configuration Envs:') diff --git a/src/oclif/commands/config/save.ts b/src/oclif/commands/config/save.ts index 6f4b804..e108a3b 100644 --- a/src/oclif/commands/config/save.ts +++ b/src/oclif/commands/config/save.ts @@ -26,7 +26,7 @@ export default class AIConfigSaveCommand extends AICommand { const opts = await this.parse(AIConfigSaveCommand) const {flags} = opts const isJson = this.jsonEnabled() - const userConfig = this.loadConfig(flags.config, opts) + const userConfig = await this.loadConfig(flags.config, {...opts, skipLoadHook: true}) if (userConfig.banner && !isJson) {showBanner('Config')} saveConfigFile(userConfig.configFile, userConfig) diff --git a/src/oclif/commands/run/index.ts b/src/oclif/commands/run/index.ts index 103c9ee..e85027a 100644 --- a/src/oclif/commands/run/index.ts +++ b/src/oclif/commands/run/index.ts @@ -6,7 +6,7 @@ import { LogLevelMap, logLevel } from '@isdk/ai-tool-agent' import { AICommand, AICommonFlags } from '../../lib/ai-command.js' import {runScript} from '../../../lib/run-script.js' import { showBanner } from '../../lib/help.js' -import { expandPath } from '../../../lib/load-config.js' +import { expandPath } from '@offline-ai/cli-common' export default class RunScript extends AICommand { static args = { @@ -44,11 +44,11 @@ export default class RunScript extends AICommand { } async run(): Promise { - const opts = await this.parse(RunScript) + const opts = await this.parse(RunScript as any) const {flags} = opts // console.log('🚀 ~ RunScript ~ run ~ flags:', flags) const isJson = this.jsonEnabled() - const userConfig = this.loadConfig(flags.config, opts) + const userConfig = await this.loadConfig(flags.config, {...opts, skipLoadHook: true}) logLevel.json = isJson const hasBanner = userConfig.banner ?? userConfig.interactive let script = userConfig.script @@ -65,7 +65,7 @@ export default class RunScript extends AICommand { } try { - await this.config.runHook('init_tools', {id: 'run', userConfig}) + await this.config.runHook('config:load', {id: 'run', userConfig}) let result = await runScript(script, userConfig) if (LogLevelMap[userConfig.logLevel] >= LogLevelMap.info && result?.content) { result = result.content diff --git a/src/oclif/commands/run/world.ts b/src/oclif/commands/run/world.ts index f25cb1a..38159bd 100644 --- a/src/oclif/commands/run/world.ts +++ b/src/oclif/commands/run/world.ts @@ -1,6 +1,6 @@ // import { color } from 'console-log-colors'; import colors from 'ansi-colors' -import cliSpinners from 'cli-spinners'; +// import cliSpinners from 'cli-spinners'; // import {randomSpinner} from 'cli-spinners'; import {Command} from '@oclif/core' @@ -48,7 +48,7 @@ hello world! (./src/commands/run/world.ts) const store = new HistoryStore({ path: `his.json` }) do { - const spinner = cliSpinners.mindblown + // const spinner = cliSpinners.mindblown const prompt = new Input({ message: '', initial: '', @@ -65,16 +65,16 @@ hello world! (./src/commands/run/world.ts) } }, separator() {return ''}, - prefix(state) { - return getFrame(spinner.frames, state.timer?.tick); - }, + // prefix(state) { + // return getFrame(spinner.frames, state.timer?.tick); + // }, // separator(state) { // return frame(rhythm, state.timer.tick)('❤'); // }, - timers: { - // separator: 250, - prefix: spinner.interval, - }, + // timers: { + // // separator: 250, + // prefix: spinner.interval, + // }, }); diff --git a/src/oclif/commands/test/index.ts b/src/oclif/commands/test/index.ts index 083cb7a..7bd6872 100644 --- a/src/oclif/commands/test/index.ts +++ b/src/oclif/commands/test/index.ts @@ -7,7 +7,7 @@ import { LogLevelMap, logLevel, parseFrontMatter, parseYaml } from '@isdk/ai-too import { AICommand, AICommonFlags } from '../../lib/ai-command.js' import {runScript} from '../../../lib/run-script.js' import { showBanner } from '../../lib/help.js' -import { expandPath } from '../../../lib/load-config.js' +import { expandPath } from '@offline-ai/cli-common' import { getKeysPath, getMultiLevelExtname } from '@isdk/ai-tool' import { get as getByPath, omit } from 'lodash-es' @@ -33,11 +33,11 @@ export default class RunTest extends AICommand { } async run(): Promise { - const opts = await this.parse(RunTest) + const opts = await this.parse(RunTest as any) const {flags} = opts // console.log('🚀 ~ RunScript ~ run ~ flags:', flags) const isJson = this.jsonEnabled() - const userConfig = this.loadConfig(flags.config, opts) + const userConfig = await this.loadConfig(flags.config, opts) logLevel.json = isJson const hasBanner = userConfig.banner ?? userConfig.interactive let script = userConfig.script @@ -77,7 +77,7 @@ export default class RunTest extends AICommand { userConfig.logLevel = userConfig.interactive ? 'error' : 'warn' } - await this.config.runHook('init_tools', {id: 'run', userConfig}) + // await this.config.runHook('init_tools', {id: 'run', userConfig}) let failedCount = 0 let passedCount = 0 diff --git a/src/oclif/hooks/init-tools.ts b/src/oclif/hooks/init-tools.ts index f8362ce..1f78f29 100644 --- a/src/oclif/hooks/init-tools.ts +++ b/src/oclif/hooks/init-tools.ts @@ -2,9 +2,14 @@ import type { Hook, Config } from '@oclif/core' import { initTools as _initTools } from '../../lib/init-tools.js' +let initialized: boolean export async function init_tools(this: Hook.Context, options: {userConfig: any, config: Config}) { - await _initTools(options.userConfig) - this.config.runHook('register', {...options}) + if (initialized) return + initialized = true + + await _initTools.call(this, options.userConfig, options.config) + await this.config.runHook('register', {...options}) + initialized = true } export default init_tools diff --git a/src/oclif/lib/ai-command.ts b/src/oclif/lib/ai-command.ts index dc89404..76dbf1d 100644 --- a/src/oclif/lib/ai-command.ts +++ b/src/oclif/lib/ai-command.ts @@ -1,126 +1,2 @@ -import path from 'path' -import {Command, Flags} from '@oclif/core' -import { DEFAULT_CONFIG_NAME, loadAIConfig, loadConfigFile } from '../../lib/load-config.js' -import { defaultsDeep } from 'lodash-es' -import { parseJsJson } from '@isdk/ai-tool' +export {AICommand, AICommonFlags} from '@offline-ai/cli-common' -// const CONFIG_BASE_NAME = '.ai' - -export abstract class AICommand extends Command { - static enableJsonFlag = true - - static flags : Record = { - config: Flags.file({char: 'c', description: 'the config file', exists: true}), - banner: Flags.boolean({description: 'show banner', allowNo: true}), - } - - loadConfig(configFile?: string, {args, flags}: any = {}) { - let result = loadAIConfig(this.config) - if (configFile) { - configFile = path.resolve(configFile) - const config = loadConfigFile(configFile) - if (!config) { - this.error(`config file ${configFile} not found`) - } - result = defaultsDeep(config, result) - } - result.theme = this.config.theme - if (flags) { - if (flags.interactive !== undefined) {result.interactive = flags.interactive} - if (flags.brainDir) {result.brainDir = flags.brainDir} - if (flags.agentDirs) {result.agentDirs = flags.agentDirs} - if (flags.histories) {result.chatsDir = flags.histories} - if (flags.newChat) {result.newChat = flags.newChat} - if (result.newChat === undefined && !result.interactive) { - result.newChat = true - } - if (flags['no-chats']) {result.chatsDir = undefined} - if (flags.inputs) {result.inputsDir = flags.inputs} - if (flags['no-inputs']) {result.inputsDir = undefined} - if (flags.stream !== undefined) {result.stream = flags.stream} - if (result.stream === undefined) {result.stream = true} - if (flags.banner !== undefined) {result.banner = flags.banner} - const api = flags.api?.toString() - if (api) {result.apiUrl = api} - if (!result.apiUrl) {result.apiUrl = 'http://localhost:8080'} - if (flags.logLevel) {result.logLevel = flags.logLevel} - if (flags.script) {result.script = flags.script} - - if (flags.arguments) {result.arguments = result.arguments ? defaultsDeep(flags.arguments, result.arguments) : flags.arguments} - if (flags.dataFile) {result.dataFile = flags.dataFile} - - let data = result.arguments - const dataFile = result.dataFile - if (dataFile) { - const _data = loadConfigFile(dataFile) - if (_data) { - if (data) { - data = defaultsDeep(data, _data) - } else { - data = _data - } - } else { - const whetherInFile = result.arguments ? '' : ' in config file' - this.error(`The data file "${dataFile}" not found${whetherInFile}!`) - } - } - Object.defineProperty(result, 'data', { - value: data, - enumerable: false, - writable: true, - }) - defaultsDeep(result, flags) - } - - if (!result.AI_CONFIG_BASENAME) {result.AI_CONFIG_BASENAME = DEFAULT_CONFIG_NAME} - if (!configFile) { - configFile = result.AI_CONFIG_BASENAME as string - } - if (!path.isAbsolute(configFile)) {configFile = path.resolve(this.config.configDir, configFile)} - Object.defineProperty(result, 'configFile', { - value: configFile, - enumerable: false, - }) - - if (args?.data) { - const data = args.data - if (result.hasOwnProperty('data')) { - result.data = typeof data !== 'string' ? defaultsDeep(data, result.data) : data - } else { - Object.defineProperty(result, 'data', { - value: data, - enumerable: false, - writable: true, - }) - } - } - - return result - } -} - -export const AICommonFlags = { - api: Flags.url({char: 'u', description: 'the api URL'}), - agentDirs: Flags.directory({char: 's', description: 'the search paths for ai-agent script file', exists: true, multiple: true}), - logLevel: Flags.string({ - char: 'l', description: 'the log level', - aliases: ['loglevel', 'log-level'], - options: ['silence', 'fatal', 'error', 'warn', 'info', 'debug', 'trace'], - }), - interactive: Flags.boolean({char: 'i', description: 'interactive mode', allowNo: true}), - histories: Flags.directory({char: 'h', description: 'the chat histories folder to record', exists: true}), - newChat: Flags.boolean({char:'n', aliases:['new-chat'], description: 'whether to start a new chat history, defaults to false in interactive mode, true in non-interactive', allowNo: true}), - backupChat: Flags.boolean({char:'k', aliases:['backup-chat'], description: 'whether to backup chat history before start, defaults to false'}), - inputs: Flags.directory({char: 't', description: 'the input histories folder for interactive mode to record', exists: true, dependsOn: ['interactive']}), - 'no-chats': Flags.boolean({description: 'disable chat histories, defaults to false'}), - 'no-inputs': Flags.boolean({description: 'disable input histories, defaults to false', dependsOn: ['interactive']}), - stream: Flags.boolean({char: 'm', description: 'stream mode, defaults to true', allowNo: true}), - script: Flags.string({char: 'f', description: 'the ai-agent script file name or id'}), - dataFile: Flags.file({char: 'd', description: 'the data file which will be passed to the ai-agent script'}), - arguments: Flags.string({ - char: 'a', description: 'the json data which will be passed to the ai-agent script', - parse: (input: string) => parseJsJson(input), - }), - brainDir: Flags.directory({char: 'b', description: 'the brains(LLM) directory', exists: true}), - promptDirs: Flags.directory({char: 'p', description: 'the prompts template directory', exists: true, multiple: true}), -} diff --git a/src/oclif/lib/help.ts b/src/oclif/lib/help.ts index 9798a70..584ceff 100644 --- a/src/oclif/lib/help.ts +++ b/src/oclif/lib/help.ts @@ -1,25 +1,6 @@ -import { type Command, Help } from '@oclif/core'; -import { uText } from '../../lib/u-text.js'; +// import { type Command, Help } from '@oclif/core'; +import { CustomHelp } from '@offline-ai/cli-common' -const defaultBanner = 'AI' +export { showBanner } from '@offline-ai/cli-common' -export function showBanner(s?: string) { - if (s) {s = defaultBanner + ' ' + s} else {s = defaultBanner + ' Agent'} - console.log(uText(s, {color: 'blue', })); // font: 'ANSI Shadow' - // uText('AI Agent', {font: 'block', colors:['yellow', 'white'], gradient: 'yellow,red'}) -} - -export default class CustomHelp extends Help { - async showHelp(args: string[]) { - // console.log(uText('AI Agent', 'green')); - showBanner(); - super.showHelp(args); - // console.dir('This will be displayed in multi-command CLIs', args); - } - - async showCommandHelp(command: Command.Loadable) { - // showBanner(); - super.showCommandHelp(command); - // console.log('This will be displayed in single-command CLIs'); - } -} +export default CustomHelp \ No newline at end of file