diff --git a/src/core/athena.ts b/src/core/athena.ts index 52ef530..8ccc006 100644 --- a/src/core/athena.ts +++ b/src/core/athena.ts @@ -5,19 +5,72 @@ import logger from "../utils/logger.js"; export type Dict = { [key: string]: T }; -export interface IAthenaArgument { - type: "string" | "number" | "boolean" | "object" | "array"; +type IAthenaArgumentPrimitive = { + type: "string" | "number" | "boolean"; desc: string; required: boolean; - of?: Dict | IAthenaArgument; -} +}; + +export type IAthenaArgument = + | IAthenaArgumentPrimitive + | { + type: "object" | "array"; + desc: string; + required: boolean; + of?: Dict | IAthenaArgument; + }; +type IAthenaArgumentInstance = + T extends IAthenaArgumentPrimitive + ? T["type"] extends "string" + ? T["required"] extends true + ? string + : string | undefined + : T["type"] extends "number" + ? T["required"] extends true + ? number + : number | undefined + : T["type"] extends "boolean" + ? T["required"] extends true + ? boolean + : boolean | undefined + : never + : T extends { of: Dict } + ? T["required"] extends true + ? { [K in keyof T["of"]]: IAthenaArgumentInstance } + : + | { [K in keyof T["of"]]: IAthenaArgumentInstance } + | undefined + : T extends { of: IAthenaArgument } + ? T["required"] extends true + ? IAthenaArgumentInstance[] + : IAthenaArgumentInstance[] | undefined + : T extends { type: "object" } + ? T["required"] extends true + ? { [K in keyof T["of"]]: any } + : { [K in keyof T["of"]]: any } | undefined + : T extends { type: "array" } + ? T["required"] extends true + ? any[] + : (any | undefined)[] + : never; -export interface IAthenaTool { +export interface IAthenaTool< + Args extends Dict = Dict, + RetArgs extends Dict = Dict, +> { name: string; desc: string; - args: Dict; - retvals: Dict; - fn: (args: Dict) => Promise>; + args: Args; + retvals: RetArgs; + fn: (args: { + [K in keyof Args]: Args[K] extends IAthenaArgument + ? IAthenaArgumentInstance + : never; + }) => Promise<{ + [K in keyof RetArgs]: RetArgs[K] extends IAthenaArgument + ? IAthenaArgumentInstance + : never; + }>; explain_args?: (args: Dict) => IAthenaExplanation; explain_retvals?: (args: Dict, retvals: Dict) => IAthenaExplanation; } @@ -38,7 +91,7 @@ export class Athena extends EventEmitter { config: Dict; states: Dict>; plugins: Dict; - tools: Dict; + tools: Map>; events: Dict; constructor(config: Dict, states: Dict>) { @@ -46,7 +99,7 @@ export class Athena extends EventEmitter { this.config = config; this.states = states; this.plugins = {}; - this.tools = {}; + this.tools = new Map(); this.events = {}; } @@ -103,11 +156,31 @@ export class Athena extends EventEmitter { logger.warn(`Plugin ${name} is unloaded`); } - registerTool(tool: IAthenaTool) { + registerTool< + Args extends Dict, + RetArgs extends Dict, + Tool extends IAthenaTool, + >( + config: { + name: string; + desc: string; + args: Args; + retvals: RetArgs; + }, + toolImpl: { + fn: Tool["fn"]; + explain_args?: Tool["explain_args"]; + explain_retvals?: Tool["explain_retvals"]; + }, + ) { + const tool = { + ...config, + ...toolImpl, + }; if (tool.name in this.tools) { throw new Error(`Tool ${tool.name} already registered`); } - this.tools[tool.name] = tool; + this.tools.set(tool.name, tool as unknown as IAthenaTool); logger.warn(`Tool ${tool.name} is registered`); } @@ -115,7 +188,7 @@ export class Athena extends EventEmitter { if (!(name in this.tools)) { throw new Error(`Tool ${name} not registered`); } - delete this.tools[name]; + this.tools.delete(name); logger.warn(`Tool ${name} is deregistered`); } @@ -159,7 +232,10 @@ export class Athena extends EventEmitter { if (!(name in this.tools)) { throw new Error(`Tool ${name} not registered`); } - const tool = this.tools[name]; + const tool = this.tools.get(name); + if (!tool) { + throw new Error(`Tool ${name} not found`); + } if (tool.explain_args) { this.emitPrivateEvent("athena/tool-call", tool.explain_args(args)); } diff --git a/src/plugins/amadeus/init.ts b/src/plugins/amadeus/init.ts index b492875..b631085 100644 --- a/src/plugins/amadeus/init.ts +++ b/src/plugins/amadeus/init.ts @@ -11,108 +11,112 @@ export default class AmadeusPlugin extends PluginBase { clientId: this.config.client_id, clientSecret: this.config.client_secret, }); - athena.registerTool({ - name: "amadeus/flight-offers-search", - desc: "Return list of Flight Offers based on searching criteria.", - args: { - originLocationCode: { - type: "string", - desc: "city/airport IATA code from which the traveler will depart, e.g. BOS for Boston\nExample : SYD", - required: true, + athena.registerTool( + { + name: "amadeus/flight-offers-search", + desc: "Return list of Flight Offers based on searching criteria.", + args: { + originLocationCode: { + type: "string", + desc: "city/airport IATA code from which the traveler will depart, e.g. BOS for Boston\nExample : SYD", + required: true, + }, + destinationLocationCode: { + type: "string", + desc: "city/airport IATA code to which the traveler is going, e.g. PAR for Paris\nExample : BKK", + required: true, + }, + departureDate: { + type: "string", + desc: "the date on which the traveler will depart from the origin to go to the destination. Dates are specified in the ISO 8601 YYYY-MM-DD format, e.g. 2017-12-25\nExample : 2023-05-02", + required: true, + }, + returnDate: { + type: "string", + desc: "the date on which the traveler will depart from the origin to go to the destination. Dates are specified in the ISO 8601 YYYY-MM-DD format, e.g. 2017-12-25\nExample : 2023-05-02", + required: false, + }, + adults: { + type: "number", + desc: "the number of adult travelers (age 12 or older on date of departure). The total number of seated travelers (adult and children) can not exceed 9.\nDefault value : 1", + required: true, + }, + children: { + type: "number", + desc: "the number of child travelers (older than age 2 and younger than age 12 on date of departure) who will each have their own separate seat. If specified, this number should be greater than or equal to 0\nThe total number of seated travelers (adult and children) can not exceed 9.", + required: false, + }, + infants: { + type: "number", + desc: "the number of infant travelers (whose age is less or equal to 2 on date of departure). Infants travel on the lap of an adult traveler, and thus the number of infants must not exceed the number of adults. If specified, this number should be greater than or equal to 0", + required: false, + }, + travelClass: { + type: "string", + desc: "most of the flight time should be spent in a cabin of this quality or higher. The accepted travel class is economy, premium economy, business or first class. If no travel class is specified, the search considers any travel class\nAvailable values : ECONOMY, PREMIUM_ECONOMY, BUSINESS, FIRST", + required: false, + }, + includedAirlineCodes: { + type: "string", + desc: "This option ensures that the system will only consider these airlines. This can not be cumulated with parameter excludedAirlineCodes.\nAirlines are specified as IATA airline codes and are comma-separated, e.g. 6X,7X,8X", + required: false, + }, + excludedAirlineCodes: { + type: "string", + desc: "This option ensures that the system will ignore these airlines. This can not be cumulated with parameter includedAirlineCodes.\nAirlines are specified as IATA airline codes and are comma-separated, e.g. 6X,7X,8X", + required: false, + }, + nonStop: { + type: "boolean", + desc: "if set to true, the search will find only flights going from the origin to the destination with no stop in between\nDefault value : false", + required: false, + }, + currencyCode: { + type: "string", + desc: "the preferred currency for the flight offers. Currency is specified in the ISO 4217 format, e.g. EUR for Euro", + required: false, + }, + maxPrice: { + type: "number", + desc: "maximum price per traveler. By default, no limit is applied. If specified, the value should be a positive number with no decimals", + required: false, + }, + max: { + type: "number", + desc: "maximum number of flight offers to return. If specified, the value should be greater than or equal to 1\nDefault value : 250", + required: false, + }, }, - destinationLocationCode: { - type: "string", - desc: "city/airport IATA code to which the traveler is going, e.g. PAR for Paris\nExample : BKK", - required: true, - }, - departureDate: { - type: "string", - desc: "the date on which the traveler will depart from the origin to go to the destination. Dates are specified in the ISO 8601 YYYY-MM-DD format, e.g. 2017-12-25\nExample : 2023-05-02", - required: true, - }, - returnDate: { - type: "string", - desc: "the date on which the traveler will depart from the origin to go to the destination. Dates are specified in the ISO 8601 YYYY-MM-DD format, e.g. 2017-12-25\nExample : 2023-05-02", - required: false, - }, - adults: { - type: "number", - desc: "the number of adult travelers (age 12 or older on date of departure). The total number of seated travelers (adult and children) can not exceed 9.\nDefault value : 1", - required: true, - }, - children: { - type: "number", - desc: "the number of child travelers (older than age 2 and younger than age 12 on date of departure) who will each have their own separate seat. If specified, this number should be greater than or equal to 0\nThe total number of seated travelers (adult and children) can not exceed 9.", - required: false, - }, - infants: { - type: "number", - desc: "the number of infant travelers (whose age is less or equal to 2 on date of departure). Infants travel on the lap of an adult traveler, and thus the number of infants must not exceed the number of adults. If specified, this number should be greater than or equal to 0", - required: false, - }, - travelClass: { - type: "string", - desc: "most of the flight time should be spent in a cabin of this quality or higher. The accepted travel class is economy, premium economy, business or first class. If no travel class is specified, the search considers any travel class\nAvailable values : ECONOMY, PREMIUM_ECONOMY, BUSINESS, FIRST", - required: false, - }, - includedAirlineCodes: { - type: "string", - desc: "This option ensures that the system will only consider these airlines. This can not be cumulated with parameter excludedAirlineCodes.\nAirlines are specified as IATA airline codes and are comma-separated, e.g. 6X,7X,8X", - required: false, - }, - excludedAirlineCodes: { - type: "string", - desc: "This option ensures that the system will ignore these airlines. This can not be cumulated with parameter includedAirlineCodes.\nAirlines are specified as IATA airline codes and are comma-separated, e.g. 6X,7X,8X", - required: false, - }, - nonStop: { - type: "boolean", - desc: "if set to true, the search will find only flights going from the origin to the destination with no stop in between\nDefault value : false", - required: false, - }, - currencyCode: { - type: "string", - desc: "the preferred currency for the flight offers. Currency is specified in the ISO 4217 format, e.g. EUR for Euro", - required: false, - }, - maxPrice: { - type: "number", - desc: "maximum price per traveler. By default, no limit is applied. If specified, the value should be a positive number with no decimals", - required: false, - }, - max: { - type: "number", - desc: "maximum number of flight offers to return. If specified, the value should be greater than or equal to 1\nDefault value : 250", - required: false, + retvals: { + data: { + desc: "The flight offers", + type: "object", + required: true, + }, }, }, - retvals: { - data: { - desc: "The flight offers", - type: "object", - required: true, + { + fn: async (args) => { + const response = await this.amadeus.shopping.flightOffersSearch.get({ + originLocationCode: args.originLocationCode, + destinationLocationCode: args.destinationLocationCode, + departureDate: args.departureDate, + returnDate: args.returnDate, + adults: args.adults, + children: args.children, + infants: args.infants, + travelClass: args.travelClass, + includedAirlineCodes: args.includedAirlineCodes, + excludedAirlineCodes: args.excludedAirlineCodes, + nonStop: args.nonStop, + currencyCode: args.currencyCode, + maxPrice: args.maxPrice, + max: args.max, + }); + return { data: response.data }; }, }, - fn: async (args: Dict) => { - const response = await this.amadeus.shopping.flightOffersSearch.get({ - originLocationCode: args.originLocationCode, - destinationLocationCode: args.destinationLocationCode, - departureDate: args.departureDate, - returnDate: args.returnDate, - adults: args.adults, - children: args.children, - infants: args.infants, - travelClass: args.travelClass, - includedAirlineCodes: args.includedAirlineCodes, - excludedAirlineCodes: args.excludedAirlineCodes, - nonStop: args.nonStop, - currencyCode: args.currencyCode, - maxPrice: args.maxPrice, - max: args.max, - }); - return { data: response.data }; - }, - }); + ); } async unload(athena: Athena) { diff --git a/src/plugins/athena/init.ts b/src/plugins/athena/init.ts index bd26312..24dd917 100644 --- a/src/plugins/athena/init.ts +++ b/src/plugins/athena/init.ts @@ -9,57 +9,61 @@ export default class AthenaPlugin extends PluginBase { } async load(athena: Athena) { - athena.registerTool({ - name: "athena/load-plugin", - desc: "Loads a plugin.", - args: { - name: { - type: "string", - desc: "The name of the plugin to load.", - required: true, - }, + athena.registerTool( + { + name: "athena/load-plugin", + desc: "Loads a plugin.", args: { - type: "object", - desc: "The arguments to pass to the plugin.", - required: true, + name: { + type: "string", + desc: "The name of the plugin to load.", + required: true, + }, + args: { + type: "object", + desc: "The arguments to pass to the plugin.", + required: true, + }, }, - }, - retvals: { - status: { - type: "string", - desc: "The status of the operation.", - required: true, + retvals: { + status: { + type: "string", + desc: "The status of the operation.", + required: true, + }, }, }, - fn: async (args: Dict) => { - if (athena.plugins[args.name]) { - await athena.unloadPlugin(args.name); - } - await new Promise((resolve, reject) => { - exec("pnpm fast-build", (error, stdout, stderr) => { - if (error) { - reject(Error(stdout)); - } else { - resolve(); - } + { + fn: async (args) => { + if (athena.plugins[args.name]) { + await athena.unloadPlugin(args.name); + } + await new Promise((resolve, reject) => { + exec("pnpm fast-build", (error, stdout, stderr) => { + if (error) { + reject(Error(stdout)); + } else { + resolve(); + } + }); }); - }); - try { - await athena.loadPlugin(args.name, args.args); - athena.emit("plugins-loaded"); - } catch (e) { try { - await athena.unloadPlugin(args.name); + await athena.loadPlugin(args.name, args.args); + athena.emit("plugins-loaded"); } catch (e) { - if (args.name in athena.plugins) { - delete athena.plugins[args.name]; + try { + await athena.unloadPlugin(args.name); + } catch (e) { + if (args.name in athena.plugins) { + delete athena.plugins[args.name]; + } } + throw e; } - throw e; - } - return { status: "success" }; + return { status: "success" }; + }, }, - }); + ); } async unload(athena: Athena) { diff --git a/src/plugins/browser/browser-use.ts b/src/plugins/browser/browser-use.ts index c6e9c6d..4b48632 100644 --- a/src/plugins/browser/browser-use.ts +++ b/src/plugins/browser/browser-use.ts @@ -9,10 +9,10 @@ interface IPageState { pageNodes: IPageNode[]; } -interface IPageMetadata { +type IBrowserUsePageMetadata = { url: string; title: string; -} +}; interface IElementData { tagName: string; @@ -141,7 +141,7 @@ export class BrowserUse extends EventEmitter { await this.waitForLoading(page); } - async getPageMetadata(pageIndex: number): Promise { + async getPageMetadata(pageIndex: number): Promise { const pageState = this.pages[pageIndex]; const page = pageState.page; return { diff --git a/src/plugins/browser/init.ts b/src/plugins/browser/init.ts index f792ff3..7ccd34c 100644 --- a/src/plugins/browser/init.ts +++ b/src/plugins/browser/init.ts @@ -103,412 +103,453 @@ export default class Browser extends PluginBase { }; }, }); - athena.registerTool({ - name: "browser/new-page", - desc: "Opens a new page in the browser.", - args: { - url: { type: "string", desc: "The URL to open.", required: true }, - }, - retvals: { - index: { - type: "number", - desc: "The index of the new page.", - required: true, - }, - url: { - type: "string", - desc: "The URL of the new page.", - required: true, - }, - title: { - type: "string", - desc: "The title of the new page.", - required: true, - }, - content: { - type: "array", - desc: "The content of the new page.", - required: true, - }, - }, - explain_args: (args: Dict) => { - return { - summary: `Opening ${args.url} in the browser...`, - }; - }, - explain_retvals: (args: Dict, retvals: Dict) => { - return { - summary: `${args.url} is successfully opened at page ${retvals.index}.`, - details: `${retvals.url}\n${retvals.title}\n${JSON.stringify( - retvals.content, - )}`, - }; - }, - fn: async (args: Dict) => { - return await this.withLock(async () => { - const index = await this.browserUse.newPage(args.url); - const metadata = await this.browserUse.getPageMetadata(index); + athena.registerTool( + { + name: "browser/new-page", + desc: "Opens a new page in the browser.", + args: { + url: { type: "string", desc: "The URL to open.", required: true }, + }, + retvals: { + index: { + type: "number", + desc: "The index of the new page.", + required: true, + }, + url: { + type: "string", + desc: "The URL of the new page.", + required: true, + }, + title: { + type: "string", + desc: "The title of the new page.", + required: true, + }, + content: { + type: "array", + desc: "The content of the new page.", + required: true, + }, + }, + }, + { + explain_args: (args: Dict) => { return { - index, - ...metadata, - content: await this.browserUse.getPageContent(index), + summary: `Opening ${args.url} in the browser...`, }; - }); - }, - }); - athena.registerTool({ - name: "browser/close-page", - desc: "Closes the page. You must call this tool after you are done with the page to release the resources. This tool won't affect the index of any other pages. You won't be able to access the page after closing it.", - args: { - index: { - type: "number", - desc: "The index of the page.", - required: true, }, - }, - retvals: { - status: { - type: "string", - desc: "The status of the operation.", - required: true, - }, - }, - explain_args: (args: Dict) => { - return { - summary: `Closing the page at index ${args.index}...`, - }; - }, - explain_retvals: (args: Dict, retvals: Dict) => { - return { - summary: `The page at index ${args.index} is closed.`, - }; - }, - fn: async (args: Dict) => { - return await this.withLock(async () => { - await this.browserUse.closePage(args.index); + explain_retvals: (args: Dict, retvals: Dict) => { return { - status: "success", + summary: `${args.url} is successfully opened at page ${retvals.index}.`, + details: `${retvals.url}\n${retvals.title}\n${JSON.stringify( + retvals.content, + )}`, }; - }); - }, - }); - athena.registerTool({ - name: "browser/click", - desc: "Clicks on an element.", - args: { - page_index: { - type: "number", - desc: "The index of the page.", - required: true, }, - node_index: { - type: "number", - desc: "The index of the element to click. If you want to click a checkbox or a radio button in a list, you must click the one before the corresponding text, not after; otherwise, the wrong element will be clicked.", - required: true, + fn: async (args: Dict) => { + return await this.withLock(async () => { + const index = await this.browserUse.newPage(args.url); + const metadata = await this.browserUse.getPageMetadata(index); + return { + index, + ...metadata, + content: await this.browserUse.getPageContent(index), + }; + }); }, }, - retvals: { - url: { - type: "string", - desc: "The URL of the page.", - required: true, + ); + athena.registerTool( + { + name: "browser/close-page", + desc: "Closes the page. You must call this tool after you are done with the page to release the resources. This tool won't affect the index of any other pages. You won't be able to access the page after closing it.", + args: { + index: { + type: "number", + desc: "The index of the page.", + required: true, + }, + }, + retvals: { + status: { + type: "string", + desc: "The status of the operation.", + required: true, + }, + }, + }, + { + explain_args: (args: Dict) => { + return { + summary: `Closing the page at index ${args.index}...`, + }; }, - title: { - type: "array", - desc: "The content of the page after clicking the element.", - required: true, + explain_retvals: (args: Dict, retvals: Dict) => { + return { + summary: `The page at index ${args.index} is closed.`, + }; }, - content: { - type: "array", - desc: "The content of the page after clicking the element.", - required: true, + fn: async (args: Dict) => { + return await this.withLock(async () => { + await this.browserUse.closePage(args.index); + return { + status: "success", + }; + }); }, }, - explain_args: (args: Dict) => { - return { - summary: `Clicking on the element at page ${args.page_index} and index ${args.node_index}...`, - }; - }, - explain_retvals: (args: Dict, retvals: Dict) => { - return { - summary: `The element at page ${args.page_index} and index ${args.node_index} is clicked.`, - details: `${retvals.url}\n${retvals.title}\n${JSON.stringify( - retvals.content, - )}`, - }; - }, - fn: async (args: Dict) => { - return await this.withLock(async () => { - await this.browserUse.clickElement(args.page_index, args.node_index); - const metadata = await this.browserUse.getPageMetadata( - args.page_index, - ); + ); + athena.registerTool( + { + name: "browser/click", + desc: "Clicks on an element.", + args: { + page_index: { + type: "number", + desc: "The index of the page.", + required: true, + }, + node_index: { + type: "number", + desc: "The index of the element to click. If you want to click a checkbox or a radio button in a list, you must click the one before the corresponding text, not after; otherwise, the wrong element will be clicked.", + required: true, + }, + }, + retvals: { + url: { + type: "string", + desc: "The URL of the page.", + required: true, + }, + title: { + type: "string", + desc: "The content of the page after clicking the element.", + required: true, + }, + content: { + type: "array", + desc: "The content of the page after clicking the element.", + required: true, + }, + }, + }, + { + explain_args: (args: Dict) => { return { - ...metadata, - content: await this.browserUse.getPageContent(args.page_index), + summary: `Clicking on the element at page ${args.page_index} and index ${args.node_index}...`, }; - }); - }, - }); - athena.registerTool({ - name: "browser/fill", - desc: "Fills text into an element.", - args: { - page_index: { - type: "number", - desc: "The index of the page.", - required: true, }, - node_index: { - type: "number", - desc: "The index of the element to fill text into.", - required: true, + explain_retvals: (args: Dict, retvals: Dict) => { + return { + summary: `The element at page ${args.page_index} and index ${args.node_index} is clicked.`, + details: `${retvals.url}\n${retvals.title}\n${JSON.stringify( + retvals.content, + )}`, + }; }, - text: { - type: "string", - desc: "The text to fill into the element.", - required: true, + fn: async (args) => { + return await this.withLock(async () => { + await this.browserUse.clickElement( + args.page_index, + args.node_index, + ); + const metadata = await this.browserUse.getPageMetadata( + args.page_index, + ); + return { + ...metadata, + content: await this.browserUse.getPageContent(args.page_index), + }; + }); }, }, - retvals: { - url: { - type: "string", - desc: "The URL of the page.", - required: true, + ); + athena.registerTool( + { + name: "browser/fill", + desc: "Fills text into an element.", + args: { + page_index: { + type: "number", + desc: "The index of the page.", + required: true, + }, + node_index: { + type: "number", + desc: "The index of the element to fill text into.", + required: true, + }, + text: { + type: "string", + desc: "The text to fill into the element.", + required: true, + }, + }, + retvals: { + url: { + type: "string", + desc: "The URL of the page.", + required: true, + }, + title: { + type: "string", + desc: "The content of the page after filling text into the element.", + required: true, + }, + content: { + type: "array", + desc: "The content of the page after filling text into the element.", + required: true, + }, + }, + }, + { + explain_args: (args: Dict) => { + return { + summary: `Filling ${args.text} into the element at page ${args.page_index} and index ${args.node_index}...`, + }; }, - title: { - type: "array", - desc: "The content of the page after filling text into the element.", - required: true, + explain_retvals: (args: Dict, retvals: Dict) => { + return { + summary: `The element at page ${args.page_index} and index ${args.node_index} is filled with ${args.text}.`, + details: `${retvals.url}\n${retvals.title}\n${JSON.stringify( + retvals.content, + )}`, + }; }, - content: { - type: "array", - desc: "The content of the page after filling text into the element.", - required: true, + fn: async (args) => { + return await this.withLock( + async (): Promise<{ + title: string; + url: string; + content: any[]; + }> => { + await this.browserUse.fillElement( + args.page_index, + args.node_index, + args.text, + ); + const metadata = await this.browserUse.getPageMetadata( + args.page_index, + ); + return { + ...metadata, + content: await this.browserUse.getPageContent(args.page_index), + }; + }, + ); }, }, - explain_args: (args: Dict) => { - return { - summary: `Filling ${args.text} into the element at page ${args.page_index} and index ${args.node_index}...`, - }; - }, - explain_retvals: (args: Dict, retvals: Dict) => { - return { - summary: `The element at page ${args.page_index} and index ${args.node_index} is filled with ${args.text}.`, - details: `${retvals.url}\n${retvals.title}\n${JSON.stringify( - retvals.content, - )}`, - }; - }, - fn: async (args: Dict) => { - return await this.withLock(async () => { - await this.browserUse.fillElement( - args.page_index, - args.node_index, - args.text, - ); - const metadata = await this.browserUse.getPageMetadata( - args.page_index, - ); + ); + athena.registerTool( + { + name: "browser/get-content", + desc: "Gets the content of the page.", + args: { + index: { + type: "number", + desc: "The index of the page.", + required: true, + }, + }, + retvals: { + url: { + type: "string", + desc: "The URL of the page.", + required: true, + }, + title: { + type: "string", + desc: "The content of the page.", + required: true, + }, + content: { + type: "array", + desc: "The content of the page.", + required: true, + }, + }, + }, + { + explain_args: (args: Dict) => { return { - ...metadata, - content: await this.browserUse.getPageContent(args.page_index), + summary: `Getting the content of the page at index ${args.index}...`, }; - }); - }, - }); - athena.registerTool({ - name: "browser/get-content", - desc: "Gets the content of the page.", - args: { - index: { - type: "number", - desc: "The index of the page.", - required: true, - }, - }, - retvals: { - url: { - type: "string", - desc: "The URL of the page.", - required: true, }, - title: { - type: "array", - desc: "The content of the page.", - required: true, + explain_retvals: (args: Dict, retvals: Dict) => { + return { + summary: `The content of the page at index ${args.index} is retrieved.`, + details: `${retvals.url}\n${retvals.title}\n${JSON.stringify( + retvals.content, + )}`, + }; }, - content: { - type: "array", - desc: "The content of the page.", - required: true, + fn: async (args) => { + return await this.withLock(async () => { + const metadata = await this.browserUse.getPageMetadata(args.index); + return { + ...metadata, + content: await this.browserUse.getPageContent(args.index), + }; + }); }, }, - explain_args: (args: Dict) => { - return { - summary: `Getting the content of the page at index ${args.index}...`, - }; - }, - explain_retvals: (args: Dict, retvals: Dict) => { - return { - summary: `The content of the page at index ${args.index} is retrieved.`, - details: `${retvals.url}\n${retvals.title}\n${JSON.stringify( - retvals.content, - )}`, - }; - }, - fn: async (args: Dict) => { - return await this.withLock(async () => { - const metadata = await this.browserUse.getPageMetadata(args.index); + ); + athena.registerTool( + { + name: "browser/get-element-data", + desc: "Gets the tag name and attributes of an element. Use this tool if you need to get the src of an image, the href of a link, or etc.", + args: { + page_index: { + type: "number", + desc: "The index of the page.", + required: true, + }, + node_index: { + type: "number", + desc: "The index of the element.", + required: true, + }, + }, + retvals: { + tagName: { + type: "string", + desc: "The tag name of the element.", + required: true, + }, + attributes: { + type: "object", + desc: "The attributes of the element.", + required: true, + }, + }, + }, + { + explain_args: (args: Dict) => { return { - ...metadata, - content: await this.browserUse.getPageContent(args.index), + summary: `Getting the tag name and attributes of the element at page ${args.page_index} and index ${args.node_index}...`, }; - }); - }, - }); - athena.registerTool({ - name: "browser/get-element-data", - desc: "Gets the tag name and attributes of an element. Use this tool if you need to get the src of an image, the href of a link, or etc.", - args: { - page_index: { - type: "number", - desc: "The index of the page.", - required: true, }, - node_index: { - type: "number", - desc: "The index of the element.", - required: true, - }, - }, - retvals: { - tagName: { - type: "string", - desc: "The tag name of the element.", - required: true, + explain_retvals: (args: Dict, retvals: Dict) => { + return { + summary: `The tag name and attributes of the element at page ${args.page_index} and index ${args.node_index} are retrieved.`, + details: `${retvals.tagName}\n${JSON.stringify(retvals.attributes)}`, + }; }, - attributes: { - type: "object", - desc: "The attributes of the element.", - required: true, + fn: async (args: Dict) => { + return await this.withLock(async () => { + return this.browserUse.getElementData( + args.page_index, + args.node_index, + ); + }); }, }, - explain_args: (args: Dict) => { - return { - summary: `Getting the tag name and attributes of the element at page ${args.page_index} and index ${args.node_index}...`, - }; - }, - explain_retvals: (args: Dict, retvals: Dict) => { - return { - summary: `The tag name and attributes of the element at page ${args.page_index} and index ${args.node_index} are retrieved.`, - details: `${retvals.tagName}\n${JSON.stringify(retvals.attributes)}`, - }; - }, - fn: async (args: Dict) => { - return await this.withLock(async () => { - return this.browserUse.getElementData( - args.page_index, - args.node_index, - ); - }); - }, - }); - athena.registerTool({ - name: "browser/screenshot", - desc: "Takes a screenshot of the page.", - args: { - index: { - type: "number", - desc: "The index of the page.", - required: true, + ); + athena.registerTool( + { + name: "browser/screenshot", + desc: "Takes a screenshot of the page.", + args: { + index: { + type: "number", + desc: "The index of the page.", + required: true, + }, + path: { + type: "string", + desc: "The path to save the screenshot.", + required: true, + }, + }, + retvals: { + status: { + type: "string", + desc: "The status of the operation.", + required: true, + }, + }, + }, + { + explain_args: (args: Dict) => { + return { + summary: `Taking a screenshot of the page at index ${args.index} and saving it to ${args.path}...`, + }; }, - path: { - type: "string", - desc: "The path to save the screenshot.", - required: true, + explain_retvals: (args: Dict, retvals: Dict) => { + return { + summary: `The screenshot of the page at index ${args.index} is taken and saved to ${args.path}.`, + }; }, - }, - retvals: { - status: { - type: "string", - desc: "The status of the operation.", - required: true, + fn: async (args: Dict) => { + return await this.withLock(async () => { + await this.browserUse.screenshot( + this.browserUse.pages[args.index].page, + args.path, + ); + return { + status: "success", + }; + }); }, }, - explain_args: (args: Dict) => { - return { - summary: `Taking a screenshot of the page at index ${args.index} and saving it to ${args.path}...`, - }; - }, - explain_retvals: (args: Dict, retvals: Dict) => { - return { - summary: `The screenshot of the page at index ${args.index} is taken and saved to ${args.path}.`, - }; - }, - fn: async (args: Dict) => { - return await this.withLock(async () => { - await this.browserUse.screenshot( - this.browserUse.pages[args.index].page, - args.path, - ); + ); + athena.registerTool( + { + name: "browser/scroll-down", + desc: "Scrolls down the page to load more content. If the webpage loads more content when you scroll down and you need to access the new content, you must call this tool to scroll to the bottom of the page.", + args: { + index: { + type: "number", + desc: "The index of the page.", + required: true, + }, + }, + retvals: { + url: { + type: "string", + desc: "The URL of the page.", + required: true, + }, + title: { + type: "string", + desc: "The content of the page after scrolling down.", + required: true, + }, + content: { + type: "array", + desc: "The content of the page after scrolling down.", + required: true, + }, + }, + }, + { + explain_args: (args: Dict) => { return { - status: "success", + summary: `Scrolling down the page at index ${args.index}...`, }; - }); - }, - }); - athena.registerTool({ - name: "browser/scroll-down", - desc: "Scrolls down the page to load more content. If the webpage loads more content when you scroll down and you need to access the new content, you must call this tool to scroll to the bottom of the page.", - args: { - index: { - type: "number", - desc: "The index of the page.", - required: true, - }, - }, - retvals: { - url: { - type: "string", - desc: "The URL of the page.", - required: true, - }, - title: { - type: "array", - desc: "The content of the page after scrolling down.", - required: true, - }, - content: { - type: "array", - desc: "The content of the page after scrolling down.", - required: true, }, - }, - explain_args: (args: Dict) => { - return { - summary: `Scrolling down the page at index ${args.index}...`, - }; - }, - explain_retvals: (args: Dict, retvals: Dict) => { - return { - summary: `The page at index ${args.index} is scrolled down.`, - details: `${retvals.url}\n${retvals.title}\n${JSON.stringify( - retvals.content, - )}`, - }; - }, - fn: async (args: Dict) => { - return await this.withLock(async () => { - await this.browserUse.scrollDown(args.index); - const metadata = await this.browserUse.getPageMetadata(args.index); + explain_retvals: (args: Dict, retvals: Dict) => { return { - ...metadata, - content: await this.browserUse.getPageContent(args.index), + summary: `The page at index ${args.index} is scrolled down.`, + details: `${retvals.url}\n${retvals.title}\n${JSON.stringify( + retvals.content, + )}`, }; - }); + }, + fn: async (args) => { + return await this.withLock(async () => { + await this.browserUse.scrollDown(args.index); + const metadata = await this.browserUse.getPageMetadata(args.index); + return { + ...metadata, + content: await this.browserUse.getPageContent(args.index), + }; + }); + }, }, - }); + ); this.browserUse.on("popup", this.boundPopupHandler); this.browserUse.on("download-started", this.boundDownloadStartedHandler); this.browserUse.on( diff --git a/src/plugins/cerebrum/init.ts b/src/plugins/cerebrum/init.ts index 7c348df..1007dbf 100644 --- a/src/plugins/cerebrum/init.ts +++ b/src/plugins/cerebrum/init.ts @@ -47,36 +47,40 @@ export default class Cerebrum extends PluginBase { this.boundAthenaPrivateEventHandler = this.athenaPrivateEventHandler.bind(this); if (this.config.image_supported) { - athena.registerTool({ - name: "image/check-out", - desc: "Check out an image. Whenever you want to see an image, or the user asks you to see an image, use this tool.", - args: { - image: { - type: "string", - desc: "The URL or local path of the image to check out.", - required: true, + athena.registerTool( + { + name: "image/check-out", + desc: "Check out an image. Whenever you want to see an image, or the user asks you to see an image, use this tool.", + args: { + image: { + type: "string", + desc: "The URL or local path of the image to check out.", + required: true, + }, }, - }, - retvals: { - result: { - type: "string", - desc: "The result of checking out the image.", - required: true, + retvals: { + result: { + type: "string", + desc: "The result of checking out the image.", + required: true, + }, }, }, - fn: async (args: Dict) => { - let image = args.image; - if (!image.startsWith("http")) { - image = await image2uri(image); - } - this.imageUrls.push(image); - return { result: "success" }; + { + fn: async (args) => { + let image = args.image; + if (!image.startsWith("http")) { + image = await image2uri(image); + } + this.imageUrls.push(image); + return { result: "success" }; + }, + explain_args: (args: Dict) => ({ + summary: "Checking out the image...", + details: args.image, + }), }, - explain_args: (args: Dict) => ({ - summary: "Checking out the image...", - details: args.image, - }), - }); + ); } athena.on("event", this.boundAthenaEventHandler); athena.on("private-event", this.boundAthenaPrivateEventHandler); diff --git a/src/plugins/cli-ui/init.ts b/src/plugins/cli-ui/init.ts index 0f17a49..b608c6e 100644 --- a/src/plugins/cli-ui/init.ts +++ b/src/plugins/cli-ui/init.ts @@ -43,28 +43,32 @@ export default class CLIUI extends PluginBase { }, }, }); - athena.registerTool({ - name: "ui/send-message", - desc: "Sends a message to the user.", - args: { - content: { - type: "string", - desc: "The message to send to the user. Don't output any Markdown formatting.", - required: true, + athena.registerTool( + { + name: "ui/send-message", + desc: "Sends a message to the user.", + args: { + content: { + type: "string", + desc: "The message to send to the user. Don't output any Markdown formatting.", + required: true, + }, }, - }, - retvals: { - status: { - type: "string", - desc: "Status of the operation.", - required: true, + retvals: { + status: { + type: "string", + desc: "Status of the operation.", + required: true, + }, }, }, - fn: async (args: Dict) => { - this.printOutput(` ${args.content}\n`); - return { status: "success" }; + { + fn: async (args: Dict) => { + this.printOutput(` ${args.content}\n`); + return { status: "success" }; + }, }, - }); + ); athena.once("plugins-loaded", async () => { process.stdin.setRawMode(true); process.stdin.on("data", this.boundHandleStdin); diff --git a/src/plugins/clock/init.ts b/src/plugins/clock/init.ts index 7ccd4e7..a7b90b7 100644 --- a/src/plugins/clock/init.ts +++ b/src/plugins/clock/init.ts @@ -45,211 +45,231 @@ export default class Clock extends PluginBase { details: args.reason, }), }); - athena.registerTool({ - name: "clock/get-time", - desc: "Get the current date and time.", - args: {}, - retvals: { - time: { - type: "string", - desc: "The current date and time.", - required: true, + athena.registerTool( + { + name: "clock/get-time", + desc: "Get the current date and time.", + args: {}, + retvals: { + time: { + type: "string", + desc: "The current date and time.", + required: true, + }, }, }, - fn: async () => { - return { time: new Date().toString() }; - }, - explain_args: () => ({ - summary: "Getting the current date and time...", - }), - explain_retvals: (args: Dict, retvals: Dict) => ({ - summary: `The current date and time is ${retvals.time}.`, - }), - }); - athena.registerTool({ - name: "clock/set-timer", - desc: "Set a timer.", - args: { - seconds: { - type: "number", - desc: "The number of seconds to wait before triggering the timer.", - required: false, + { + fn: async () => { + return { time: new Date().toString() }; }, - minutes: { - type: "number", - desc: "The number of minutes to wait before triggering the timer.", - required: false, - }, - hours: { - type: "number", - desc: "The number of hours to wait before triggering the timer.", - required: false, - }, - reason: { - type: "string", - desc: "The reason why the timer was set. Include as much detail as possible.", - required: true, + explain_args: () => ({ + summary: "Getting the current date and time...", + }), + explain_retvals: (args: Dict, retvals: Dict) => ({ + summary: `The current date and time is ${retvals.time}.`, + }), + }, + ); + athena.registerTool( + { + name: "clock/set-timer", + desc: "Set a timer.", + args: { + seconds: { + type: "number", + desc: "The number of seconds to wait before triggering the timer.", + required: false, + }, + minutes: { + type: "number", + desc: "The number of minutes to wait before triggering the timer.", + required: false, + }, + hours: { + type: "number", + desc: "The number of hours to wait before triggering the timer.", + required: false, + }, + reason: { + type: "string", + desc: "The reason why the timer was set. Include as much detail as possible.", + required: true, + }, + recurring: { + type: "boolean", + desc: "Whether the timer is recurring.", + required: true, + }, }, - recurring: { - type: "boolean", - desc: "Whether the timer is recurring.", - required: true, + retvals: { + status: { + type: "string", + desc: "The status of the operation.", + required: true, + }, }, }, - retvals: { - status: { - type: "string", - desc: "The status of the operation.", - required: true, + { + fn: async (args: Dict) => { + const interval = + (args.seconds || 0) * 1000 + + (args.minutes || 0) * 60 * 1000 + + (args.hours || 0) * 60 * 60 * 1000; + this.timeouts.push({ + reason: args.reason, + next_trigger_time: Date.now() + interval, + recurring: args.recurring, + interval, + }); + this.updateTimeout(); + return { status: "success" }; }, + explain_args: (args: Dict) => ({ + summary: `Setting a ${ + args.recurring ? "recurring" : "one-time" + } timer for ${args.hours || 0} hours, ${ + args.minutes || 0 + } minutes, and ${args.seconds || 0} seconds...`, + details: args.reason, + }), }, - fn: async (args: Dict) => { - const interval = - (args.seconds || 0) * 1000 + - (args.minutes || 0) * 60 * 1000 + - (args.hours || 0) * 60 * 60 * 1000; - this.timeouts.push({ - reason: args.reason, - next_trigger_time: Date.now() + interval, - recurring: args.recurring, - interval, - }); - this.updateTimeout(); - return { status: "success" }; - }, - explain_args: (args: Dict) => ({ - summary: `Setting a ${ - args.recurring ? "recurring" : "one-time" - } timer for ${args.hours || 0} hours, ${ - args.minutes || 0 - } minutes, and ${args.seconds || 0} seconds...`, - details: args.reason, - }), - }); - athena.registerTool({ - name: "clock/set-alarm", - desc: "Set an alarm.", - args: { - time: { - type: "string", - desc: "The date and time to set the alarm for. Need to specify timezone.", - required: true, - }, - reason: { - type: "string", - desc: "The reason why the alarm was set. Include as much detail as possible.", - required: true, + ); + athena.registerTool( + { + name: "clock/set-alarm", + desc: "Set an alarm.", + args: { + time: { + type: "string", + desc: "The date and time to set the alarm for. Need to specify timezone.", + required: true, + }, + reason: { + type: "string", + desc: "The reason why the alarm was set. Include as much detail as possible.", + required: true, + }, + recurring: { + type: "boolean", + desc: "Whether the alarm is recurring.", + required: true, + }, }, - recurring: { - type: "boolean", - desc: "Whether the alarm is recurring.", - required: true, + retvals: { + status: { + type: "string", + desc: "The status of the operation.", + required: true, + }, }, }, - retvals: { - status: { - type: "string", - desc: "The status of the operation.", - required: true, + { + fn: async (args: Dict) => { + const time = new Date(args.time); + const now = new Date(); + if (time <= now) { + throw new Error("Alarm time must be in the future."); + } + this.timeouts.push({ + reason: args.reason, + next_trigger_time: time.getTime(), + recurring: args.recurring, + interval: 24 * 60 * 60 * 1000, + }); + this.updateTimeout(); + return { status: "success" }; }, + explain_args: (args: Dict) => ({ + summary: `Setting a ${ + args.recurring ? "recurring" : "one-time" + } alarm for ${args.time}...`, + details: args.reason, + }), }, - fn: async (args: Dict) => { - const time = new Date(args.time); - const now = new Date(); - if (time <= now) { - throw new Error("Alarm time must be in the future."); - } - this.timeouts.push({ - reason: args.reason, - next_trigger_time: time.getTime(), - recurring: args.recurring, - interval: 24 * 60 * 60 * 1000, - }); - this.updateTimeout(); - return { status: "success" }; - }, - explain_args: (args: Dict) => ({ - summary: `Setting a ${ - args.recurring ? "recurring" : "one-time" - } alarm for ${args.time}...`, - details: args.reason, - }), - }); - athena.registerTool({ - name: "clock/clear-timeout", - desc: "Clear a timeout.", - args: { - index: { - type: "number", - desc: "The index of the timeout to clear.", - required: true, + ); + athena.registerTool( + { + name: "clock/clear-timeout", + desc: "Clear a timeout.", + args: { + index: { + type: "number", + desc: "The index of the timeout to clear.", + required: true, + }, }, - }, - retvals: { - status: { - type: "string", - desc: "The status of the operation.", - required: true, + retvals: { + status: { + type: "string", + desc: "The status of the operation.", + required: true, + }, }, }, - fn: async (args: Dict) => { - this.timeouts.splice(args.index, 1); - this.updateTimeout(); - return { status: "success" }; + { + fn: async (args: Dict) => { + this.timeouts.splice(args.index, 1); + this.updateTimeout(); + return { status: "success" }; + }, + explain_args: (args: Dict) => ({ + summary: `Clearing the timeout at index ${args.index}...`, + }), }, - explain_args: (args: Dict) => ({ - summary: `Clearing the timeout at index ${args.index}...`, - }), - }); - athena.registerTool({ - name: "clock/list-timeouts", - desc: "List all timeouts.", - args: {}, - retvals: { - timeouts: { - type: "array", - desc: "The list of timeouts.", - required: true, - of: { - type: "object", - desc: "A timeout.", + ); + athena.registerTool( + { + name: "clock/list-timeouts", + desc: "List all timeouts.", + args: {}, + retvals: { + timeouts: { + type: "array", + desc: "The list of timeouts.", required: true, of: { - reason: { - type: "string", - desc: "The reason why the timeout was set.", - required: true, - }, - next_trigger_time: { - type: "string", - desc: "The next trigger time of the timeout.", - required: true, - }, - recurring: { - type: "boolean", - desc: "Whether the timeout is recurring.", - required: true, - }, - interval: { - type: "number", - desc: "The interval of the timeout, in seconds.", - required: true, + type: "object", + desc: "A timeout.", + required: true, + of: { + reason: { + type: "string", + desc: "The reason why the timeout was set.", + required: true, + }, + next_trigger_time: { + type: "string", + desc: "The next trigger time of the timeout.", + required: true, + }, + recurring: { + type: "boolean", + desc: "Whether the timeout is recurring.", + required: true, + }, + interval: { + type: "number", + desc: "The interval of the timeout, in seconds.", + required: true, + }, }, }, }, }, }, - fn: async () => { - return { - timeouts: this.timeouts.map((t) => ({ - reason: t.reason, - next_trigger_time: new Date(t.next_trigger_time).toString(), - recurring: t.recurring, - interval: t.interval / 1000, - })), - }; + { + fn: async () => { + return { + timeouts: this.timeouts.map((t) => ({ + reason: t.reason, + next_trigger_time: new Date(t.next_trigger_time).toString(), + recurring: t.recurring, + interval: t.interval / 1000, + })), + }; + }, }, - }); + ); } async unload(athena: Athena) { diff --git a/src/plugins/discord/init.ts b/src/plugins/discord/init.ts index e4e6d76..6854798 100644 --- a/src/plugins/discord/init.ts +++ b/src/plugins/discord/init.ts @@ -299,187 +299,199 @@ export default class Discord extends PluginBase { }, }); - athena.registerTool({ - name: "discord/send-message", - desc: "Send a message to a chat in Discord.", - args: { - channel_id: { - type: "string", - desc: "The ID of the channel to send the message to.", - required: true, - }, - reply_to_message_id: { - type: "string", - desc: "The ID of the message to reply to.", - required: false, - }, - content: { - type: "string", - desc: "The content of the message.", - required: true, - }, - files: { - type: "array", - desc: "The files to attach to the message.", - required: false, - of: { - type: "object", - desc: "The file to attach.", + athena.registerTool( + { + name: "discord/send-message", + desc: "Send a message to a chat in Discord.", + args: { + channel_id: { + type: "string", + desc: "The ID of the channel to send the message to.", required: true, + }, + reply_to_message_id: { + type: "string", + desc: "The ID of the message to reply to.", + required: false, + }, + content: { + type: "string", + desc: "The content of the message.", + required: true, + }, + files: { + type: "array", + desc: "The files to attach to the message.", + required: false, of: { - name: { - type: "string", - desc: "The name of the file.", - required: true, - }, - desc: { - type: "string", - desc: "The description of the file.", - required: false, - }, - path: { - type: "string", - desc: "The path to the file. Could be local path or URL.", - required: true, + type: "object", + desc: "The file to attach.", + required: true, + of: { + name: { + type: "string", + desc: "The name of the file.", + required: true, + }, + desc: { + type: "string", + desc: "The description of the file.", + required: false, + }, + path: { + type: "string", + desc: "The path to the file. Could be local path or URL.", + required: true, + }, }, }, }, + components: { + type: "array", + desc: "The components to attach to the message.", + required: false, + of: { + type: "object", + desc: "The component to attach to the message.", + required: true, + }, + }, }, - components: { - type: "array", - desc: "The components to attach to the message.", - required: false, - of: { - type: "object", - desc: "The component to attach to the message.", + retvals: { + id: { + type: "string", + desc: "The ID of the message sent.", required: true, }, }, }, - retvals: { - id: { - type: "string", - desc: "The ID of the message sent.", - required: true, + { + fn: async (args: Dict) => { + const channel = await this.client.channels.fetch(args.channel_id); + if (!channel) { + throw new Error("The channel does not exist."); + } + if (!channel.isTextBased()) { + throw new Error("The channel is not text-based."); + } + return { + id: ( + await (channel as SendableChannels).send({ + content: args.content, + reply: args.reply_to_message_id + ? { + messageReference: args.reply_to_message_id, + } + : undefined, + files: args.files + ? args.files.map((file: Dict) => ({ + attachment: file.path, + name: file.name, + description: file.desc, + })) + : undefined, + components: args.components, + }) + ).id, + }; }, }, - fn: async (args: Dict) => { - const channel = await this.client.channels.fetch(args.channel_id); - if (!channel) { - throw new Error("The channel does not exist."); - } - if (!channel.isTextBased()) { - throw new Error("The channel is not text-based."); - } - return { - id: ( - await (channel as SendableChannels).send({ - content: args.content, - reply: args.reply_to_message_id - ? { - messageReference: args.reply_to_message_id, - } - : undefined, - files: args.files - ? args.files.map((file: Dict) => ({ - attachment: file.path, - name: file.name, - description: file.desc, - })) - : undefined, - components: args.components, - }) - ).id, - }; - }, - }); + ); - athena.registerTool({ - name: "discord/edit-message", - desc: "Edit a message in Discord.", - args: { - channel_id: { - type: "string", - desc: "The ID of the channel the message is in.", - required: true, - }, - message_id: { - type: "string", - desc: "The ID of the message to edit.", - required: true, + athena.registerTool( + { + name: "discord/edit-message", + desc: "Edit a message in Discord.", + args: { + channel_id: { + type: "string", + desc: "The ID of the channel the message is in.", + required: true, + }, + message_id: { + type: "string", + desc: "The ID of the message to edit.", + required: true, + }, + content: { + type: "string", + desc: "The new content of the message.", + required: true, + }, }, - content: { - type: "string", - desc: "The new content of the message.", - required: true, + retvals: { + status: { + type: "string", + desc: "The status of the operation.", + required: true, + }, }, }, - retvals: { - status: { - type: "string", - desc: "The status of the operation.", - required: true, + { + fn: async (args: Dict) => { + const channel = await this.client.channels.fetch(args.channel_id); + if (!channel) { + throw new Error("The channel does not exist."); + } + if (!channel.isTextBased()) { + throw new Error("The channel is not text-based."); + } + const message = await (channel as SendableChannels).messages.fetch( + args.message_id, + ); + if (!message) { + throw new Error("The message does not exist."); + } + await message.edit(args.content); + return { status: "success" }; }, }, - fn: async (args: Dict) => { - const channel = await this.client.channels.fetch(args.channel_id); - if (!channel) { - throw new Error("The channel does not exist."); - } - if (!channel.isTextBased()) { - throw new Error("The channel is not text-based."); - } - const message = await (channel as SendableChannels).messages.fetch( - args.message_id, - ); - if (!message) { - throw new Error("The message does not exist."); - } - await message.edit(args.content); - return { status: "success" }; - }, - }); + ); - athena.registerTool({ - name: "discord/delete-message", - desc: "Delete a message in Discord.", - args: { - channel_id: { - type: "string", - desc: "The ID of the channel the message is in.", - required: true, + athena.registerTool( + { + name: "discord/delete-message", + desc: "Delete a message in Discord.", + args: { + channel_id: { + type: "string", + desc: "The ID of the channel the message is in.", + required: true, + }, + message_id: { + type: "string", + desc: "The ID of the message to delete.", + required: true, + }, }, - message_id: { - type: "string", - desc: "The ID of the message to delete.", - required: true, + retvals: { + status: { + type: "string", + desc: "The status of the operation.", + required: true, + }, }, }, - retvals: { - status: { - type: "string", - desc: "The status of the operation.", - required: true, + { + fn: async (args: Dict) => { + const channel = await this.client.channels.fetch(args.channel_id); + if (!channel) { + throw new Error("The channel does not exist."); + } + if (!channel.isTextBased()) { + throw new Error("The channel is not text-based."); + } + const message = await (channel as SendableChannels).messages.fetch( + args.message_id, + ); + if (!message) { + throw new Error("The message does not exist."); + } + await message.delete(); + return { status: "success" }; }, }, - fn: async (args: Dict) => { - const channel = await this.client.channels.fetch(args.channel_id); - if (!channel) { - throw new Error("The channel does not exist."); - } - if (!channel.isTextBased()) { - throw new Error("The channel is not text-based."); - } - const message = await (channel as SendableChannels).messages.fetch( - args.message_id, - ); - if (!message) { - throw new Error("The message does not exist."); - } - await message.delete(); - return { status: "success" }; - }, - }); + ); athena.once("plugins-loaded", () => { this.client.on(Events.MessageCreate, async (message) => { diff --git a/src/plugins/file-system/init.ts b/src/plugins/file-system/init.ts index a80faa7..b037e38 100644 --- a/src/plugins/file-system/init.ts +++ b/src/plugins/file-system/init.ts @@ -15,341 +15,383 @@ export default class FileSystem extends PluginBase { } async load(athena: Athena) { - athena.registerTool({ - name: "fs/list", - desc: "List a directory", - args: { - path: { - type: "string", - desc: "The path to list", - required: true, + athena.registerTool( + { + name: "fs/list", + desc: "List a directory", + args: { + path: { + type: "string", + desc: "The path to list", + required: true, + }, }, - }, - retvals: { - content: { - type: "array", - desc: "The content of the directory", - required: true, - of: { - type: "object", - desc: "The file or directory", + retvals: { + content: { + type: "array", + desc: "The content of the directory", required: true, of: { - name: { - type: "string", - desc: "The name of the file or directory", - required: true, - }, - type: { - type: "string", - desc: "The type of the file or directory", - required: true, - }, - size: { - type: "number", - desc: "The size of the file in bytes", - required: false, + type: "object", + desc: "The file or directory", + required: true, + of: { + name: { + type: "string", + desc: "The name of the file or directory", + required: true, + }, + type: { + type: "string", + desc: "The type of the file or directory", + required: true, + }, + size: { + type: "number", + desc: "The size of the file in bytes", + required: false, + }, }, }, }, }, }, - fn: async (args: Dict) => { - const content = await fs.readdir(args.path, { withFileTypes: true }); - const ret = []; - for (const entry of content) { - ret.push({ - name: entry.name, - type: entry.isDirectory() ? "directory" : "file", - size: entry.isFile() - ? (await fs.stat(`${args.path}/${entry.name}`)).size - : undefined, - }); - } - return { content: ret }; - }, - explain_args: (args: Dict) => ({ - summary: `Listing the directory ${args.path}...`, - }), - explain_retvals: (args: Dict, retvals: Dict) => ({ - summary: `The directory ${args.path} is successfully listed.`, - details: retvals.content.map((item: Dict) => item.name).join(", "), - }), - }); - athena.registerTool({ - name: "fs/read", - desc: "Read a file. This tool cannot be used to read binary files.", - args: { - path: { - type: "string", - desc: "The path to the file", - required: true, + { + fn: async (args: Dict) => { + const content = await fs.readdir(args.path, { withFileTypes: true }); + const ret = []; + for (const entry of content) { + ret.push({ + name: entry.name, + type: entry.isDirectory() ? "directory" : "file", + size: entry.isFile() + ? (await fs.stat(`${args.path}/${entry.name}`)).size + : undefined, + }); + } + return { content: ret }; }, + explain_args: (args: Dict) => ({ + summary: `Listing the directory ${args.path}...`, + }), + explain_retvals: (args: Dict, retvals: Dict) => ({ + summary: `The directory ${args.path} is successfully listed.`, + details: retvals.content + .map((item: Dict) => item.name) + .join(", "), + }), }, - retvals: { - content: { - type: "string", - desc: "The content of the file", - required: true, + ); + athena.registerTool( + { + name: "fs/read", + desc: "Read a file. This tool cannot be used to read binary files.", + args: { + path: { + type: "string", + desc: "The path to the file", + required: true, + }, + }, + retvals: { + content: { + type: "string", + desc: "The content of the file", + required: true, + }, }, }, - fn: async (args: Dict) => { - const buffer = await fs.readFile(args.path); - if (isBinary(args.path, buffer)) { - throw new Error("File is binary"); - } - return { content: buffer.toString("utf8") }; + { + fn: async (args: Dict) => { + const buffer = await fs.readFile(args.path); + if (isBinary(args.path, buffer)) { + throw new Error("File is binary"); + } + return { content: buffer.toString("utf8") }; + }, + explain_args: (args: Dict) => ({ + summary: `Reading the file ${args.path}...`, + }), + explain_retvals: (args: Dict, retvals: Dict) => ({ + summary: `The file ${args.path} is successfully read.`, + details: retvals.content, + }), }, - explain_args: (args: Dict) => ({ - summary: `Reading the file ${args.path}...`, - }), - explain_retvals: (args: Dict, retvals: Dict) => ({ - summary: `The file ${args.path} is successfully read.`, - details: retvals.content, - }), - }); - athena.registerTool({ - name: "fs/write", - desc: "Write to a file", - args: { - path: { - type: "string", - desc: "The path to the file", - required: true, + ); + athena.registerTool( + { + name: "fs/write", + desc: "Write to a file", + args: { + path: { + type: "string", + desc: "The path to the file", + required: true, + }, + content: { + type: "string", + desc: "The content to write", + required: true, + }, }, - content: { - type: "string", - desc: "The content to write", - required: true, + retvals: { + status: { + type: "string", + desc: "The status of the write operation", + required: true, + }, }, }, - retvals: { - status: { - type: "string", - desc: "The status of the write operation", - required: true, + { + fn: async (args: Dict) => { + await fs.writeFile(args.path, args.content, "utf8"); + return { status: "success" }; }, + explain_args: (args: Dict) => ({ + summary: `Writing to the file ${args.path}...`, + details: args.content, + }), }, - fn: async (args: Dict) => { - await fs.writeFile(args.path, args.content, "utf8"); - return { status: "success" }; - }, - explain_args: (args: Dict) => ({ - summary: `Writing to the file ${args.path}...`, - details: args.content, - }), - }); - athena.registerTool({ - name: "fs/delete", - desc: "Delete a file or directory", - args: { - path: { - type: "string", - desc: "The path to the file or directory", - required: true, + ); + athena.registerTool( + { + name: "fs/delete", + desc: "Delete a file or directory", + args: { + path: { + type: "string", + desc: "The path to the file or directory", + required: true, + }, }, - }, - retvals: { - status: { - type: "string", - desc: "The status of the delete operation", - required: true, + retvals: { + status: { + type: "string", + desc: "The status of the delete operation", + required: true, + }, }, }, - fn: async (args: Dict) => { - await fs.rm(args.path, { recursive: true }); - return { status: "success" }; + { + fn: async (args: Dict) => { + await fs.rm(args.path, { recursive: true }); + return { status: "success" }; + }, + explain_args: (args: Dict) => ({ + summary: `Deleting the file or directory ${args.path}...`, + }), }, - explain_args: (args: Dict) => ({ - summary: `Deleting the file or directory ${args.path}...`, - }), - }); - athena.registerTool({ - name: "fs/copy", - desc: "Copy a file or directory", - args: { - src: { - type: "string", - desc: "The source path", - required: true, + ); + athena.registerTool( + { + name: "fs/copy", + desc: "Copy a file or directory", + args: { + src: { + type: "string", + desc: "The source path", + required: true, + }, + dst: { + type: "string", + desc: "The destination path", + required: true, + }, }, - dst: { - type: "string", - desc: "The destination path", - required: true, + retvals: { + status: { + type: "string", + desc: "The status of the copy operation", + required: true, + }, }, }, - retvals: { - status: { - type: "string", - desc: "The status of the copy operation", - required: true, + { + fn: async (args: Dict) => { + const stat = await fs.stat(args.src); + if (stat.isFile()) { + await fs.copyFile(args.src, args.dst); + } else if (stat.isDirectory()) { + await fs.cp(args.src, args.dst, { recursive: true }); + } else { + throw new Error("Unknown file type"); + } + return { status: "success" }; }, + explain_args: (args: Dict) => ({ + summary: `Copying the file or directory ${args.src} to ${args.dst}...`, + }), }, - fn: async (args: Dict) => { - const stat = await fs.stat(args.src); - if (stat.isFile()) { - await fs.copyFile(args.src, args.dst); - } else if (stat.isDirectory()) { - await fs.cp(args.src, args.dst, { recursive: true }); - } else { - throw new Error("Unknown file type"); - } - return { status: "success" }; - }, - explain_args: (args: Dict) => ({ - summary: `Copying the file or directory ${args.src} to ${args.dst}...`, - }), - }); - athena.registerTool({ - name: "fs/move", - desc: "Move or rename a file or directory", - args: { - src: { - type: "string", - desc: "The source path", - required: true, + ); + athena.registerTool( + { + name: "fs/move", + desc: "Move or rename a file or directory", + args: { + src: { + type: "string", + desc: "The source path", + required: true, + }, + dst: { + type: "string", + desc: "The destination path", + required: true, + }, }, - dst: { - type: "string", - desc: "The destination path", - required: true, + retvals: { + status: { + type: "string", + desc: "The status of the move operation", + required: true, + }, }, }, - retvals: { - status: { - type: "string", - desc: "The status of the move operation", - required: true, + { + fn: async (args: Dict) => { + await fs.rename(args.src, args.dst); + return { status: "success" }; }, + explain_args: (args: Dict) => ({ + summary: `Moving or renaming the file or directory ${args.src} to ${args.dst}...`, + }), }, - fn: async (args: Dict) => { - await fs.rename(args.src, args.dst); - return { status: "success" }; - }, - explain_args: (args: Dict) => ({ - summary: `Moving or renaming the file or directory ${args.src} to ${args.dst}...`, - }), - }); - athena.registerTool({ - name: "fs/mkdir", - desc: "Create a directory recursively", - args: { - path: { - type: "string", - desc: "The path to the directory", - required: true, + ); + athena.registerTool( + { + name: "fs/mkdir", + desc: "Create a directory recursively", + args: { + path: { + type: "string", + desc: "The path to the directory", + required: true, + }, }, - }, - retvals: { - status: { - type: "string", - desc: "The status of the mkdir operation", - required: true, + retvals: { + status: { + type: "string", + desc: "The status of the mkdir operation", + required: true, + }, }, }, - fn: async (args: Dict) => { - await fs.mkdir(args.path, { recursive: true }); - return { status: "success" }; - }, - explain_args: (args: Dict) => ({ - summary: `Creating the directory ${args.path}...`, - }), - }); - athena.registerTool({ - name: "fs/cd", - desc: "Change the current working directory.", - args: { - directory: { - type: "string", - desc: "Directory to change to. Could be an absolute or relative path.", - required: true, + { + fn: async (args: Dict) => { + await fs.mkdir(args.path, { recursive: true }); + return { status: "success" }; }, + explain_args: (args: Dict) => ({ + summary: `Creating the directory ${args.path}...`, + }), }, - retvals: { - result: { - desc: "Result of the cd command", - required: true, - type: "string", + ); + athena.registerTool( + { + name: "fs/cd", + desc: "Change the current working directory.", + args: { + directory: { + type: "string", + desc: "Directory to change to. Could be an absolute or relative path.", + required: true, + }, + }, + retvals: { + result: { + desc: "Result of the cd command", + required: true, + type: "string", + }, }, }, - fn: async (args: Dict) => { - process.chdir(args.directory); - return { result: "success" }; - }, - explain_args: (args: Dict) => ({ - summary: `Changing the current working directory to ${args.directory}...`, - }), - }); - athena.registerTool({ - name: "fs/find-replace", - desc: "Find and replace a string in a file. If you need to fix a bug in a file, you should use this tool instead of using fs/write to change the entire file.", - args: { - path: { - type: "string", - desc: "The path to the file", - required: true, + { + fn: async (args: Dict) => { + process.chdir(args.directory); + return { result: "success" }; }, - old: { - type: "string", - desc: "The string to find", - required: true, + explain_args: (args: Dict) => ({ + summary: `Changing the current working directory to ${args.directory}...`, + }), + }, + ); + athena.registerTool( + { + name: "fs/find-replace", + desc: "Find and replace a string in a file. If you need to fix a bug in a file, you should use this tool instead of using fs/write to change the entire file.", + args: { + path: { + type: "string", + desc: "The path to the file", + required: true, + }, + old: { + type: "string", + desc: "The string to find", + required: true, + }, + new: { + type: "string", + desc: "The string to replace the old string with", + required: true, + }, }, - new: { - type: "string", - desc: "The string to replace the old string with", - required: true, + retvals: { + status: { + type: "string", + desc: "The status of the find-replace operation", + required: true, + }, }, }, - retvals: { - status: { - type: "string", - desc: "The status of the find-replace operation", - required: true, + { + fn: async (args: Dict) => { + const content = await fs.readFile(args.path, "utf8"); + const newContent = content.replace(args.old, args.new); + await fs.writeFile(args.path, newContent, "utf8"); + return { status: "success" }; }, + explain_args: (args: Dict) => ({ + summary: `Finding and replacing ${args.path}...`, + details: `The old string is ${args.old} and the new string is ${args.new}.`, + }), }, - fn: async (args: Dict) => { - const content = await fs.readFile(args.path, "utf8"); - const newContent = content.replace(args.old, args.new); - await fs.writeFile(args.path, newContent, "utf8"); - return { status: "success" }; - }, - explain_args: (args: Dict) => ({ - summary: `Finding and replacing ${args.path}...`, - details: `The old string is ${args.old} and the new string is ${args.new}.`, - }), - }); - athena.registerTool({ - name: "fs/append", - desc: "Append to a file", - args: { - path: { - type: "string", - desc: "The path to the file", - required: true, + ); + athena.registerTool( + { + name: "fs/append", + desc: "Append to a file", + args: { + path: { + type: "string", + desc: "The path to the file", + required: true, + }, + content: { + type: "string", + desc: "The content to append", + required: true, + }, }, - content: { - type: "string", - desc: "The content to append", - required: true, + retvals: { + status: { + type: "string", + desc: "The status of the append operation", + required: true, + }, }, }, - retvals: { - status: { - type: "string", - desc: "The status of the append operation", - required: true, + { + fn: async (args: Dict) => { + await fs.appendFile(args.path, args.content, "utf8"); + return { status: "success" }; }, + explain_args: (args: Dict) => ({ + summary: `Appending to the file ${args.path}...`, + details: args.content, + }), }, - fn: async (args: Dict) => { - await fs.appendFile(args.path, args.content, "utf8"); - return { status: "success" }; - }, - explain_args: (args: Dict) => ({ - summary: `Appending to the file ${args.path}...`, - details: args.content, - }), - }); + ); } async unload(athena: Athena) { diff --git a/src/plugins/http/init.ts b/src/plugins/http/init.ts index e01c1fd..7b62172 100644 --- a/src/plugins/http/init.ts +++ b/src/plugins/http/init.ts @@ -42,226 +42,242 @@ export default class Http extends PluginBase { athena.on("private-event", this.boundAthenaPrivateEventHandler); athena.emitPrivateEvent("webapp-ui/request-token", {}); - athena.registerTool({ - name: "http/fetch", - desc: "Fetches an HTTP/HTTPS URL.", - args: { - url: { - type: "string", - desc: "The URL to fetch.", - required: true, - }, - method: { - type: "string", - desc: "The HTTP method to use. Defaults to GET.", - required: false, - }, - headers: { - type: "object", - desc: "The headers to send with the request.", - required: false, - }, - body: { - type: "string", - desc: "The body to send with the request.", - required: false, - }, - }, - retvals: { - result: { - type: "string", - desc: "The result of the fetch.", - required: true, - }, - }, - fn: async (args: Dict) => { - const response = await fetch(args.url, { - method: args.method, - headers: args.headers - ? { - ...this.headers, - ...args.headers, - } - : this.headers, - body: args.body, - redirect: "follow", - }); - return { result: convert(await response.text()) }; - }, - explain_args: (args: Dict) => ({ - summary: `Fetching the URL ${args.url}...`, - }), - explain_retvals: (args: Dict, retvals: Dict) => ({ - summary: `The URL ${args.url} was fetched successfully.`, - details: retvals.result, - }), - }); - if (this.config.jina) { - athena.registerTool({ - name: "http/search", - desc: "Searches the web for information.", + athena.registerTool( + { + name: "http/fetch", + desc: "Fetches an HTTP/HTTPS URL.", args: { - query: { + url: { type: "string", - desc: "The query to search for.", + desc: "The URL to fetch.", required: true, }, + method: { + type: "string", + desc: "The HTTP method to use. Defaults to GET.", + required: false, + }, + headers: { + type: "object", + desc: "The headers to send with the request.", + required: false, + }, + body: { + type: "string", + desc: "The body to send with the request.", + required: false, + }, }, retvals: { - results: { - type: "array", - desc: "The results of the search.", + result: { + type: "string", + desc: "The result of the fetch.", required: true, - of: { - type: "object", - desc: "The result of the search.", - of: { - title: { - type: "string", - desc: "The title of the result.", - required: true, - }, - url: { - type: "string", - desc: "The URL of the result.", - required: true, - }, - desc: { - type: "string", - desc: "The description of the result.", - required: true, - }, - }, - required: true, - }, }, }, + }, + { fn: async (args: Dict) => { - const results = await this.jina.search(args.query); - return { results }; + const response = await fetch(args.url, { + method: args.method, + headers: args.headers + ? { + ...this.headers, + ...args.headers, + } + : this.headers, + body: args.body, + redirect: "follow", + }); + return { result: convert(await response.text()) }; }, explain_args: (args: Dict) => ({ - summary: `Searching the web for ${args.query}...`, + summary: `Fetching the URL ${args.url}...`, }), explain_retvals: (args: Dict, retvals: Dict) => ({ - summary: `Found ${retvals.results.length} results for ${args.query}.`, - details: JSON.stringify(retvals.results), + summary: `The URL ${args.url} was fetched successfully.`, + details: retvals.result, }), - }); + }, + ); + if (this.config.jina) { + athena.registerTool( + { + name: "http/search", + desc: "Searches the web for information.", + args: { + query: { + type: "string", + desc: "The query to search for.", + required: true, + }, + }, + retvals: { + results: { + type: "array", + desc: "The results of the search.", + required: true, + of: { + type: "object", + desc: "The result of the search.", + of: { + title: { + type: "string", + desc: "The title of the result.", + required: true, + }, + url: { + type: "string", + desc: "The URL of the result.", + required: true, + }, + desc: { + type: "string", + desc: "The description of the result.", + required: true, + }, + }, + required: true, + }, + }, + }, + }, + { + fn: async (args: Dict) => { + const results = await this.jina.search(args.query); + return { results }; + }, + explain_args: (args: Dict) => ({ + summary: `Searching the web for ${args.query}...`, + }), + explain_retvals: (args: Dict, retvals: Dict) => ({ + summary: `Found ${retvals.results.length} results for ${args.query}.`, + details: JSON.stringify(retvals.results), + }), + }, + ); } if (this.config.exa) { - athena.registerTool({ - name: "http/exa-search", - desc: "Searches the web for information using Exa API.", - args: { - query: { - type: "string", - desc: "The query to search for.", - required: true, + athena.registerTool( + { + name: "http/exa-search", + desc: "Searches the web for information using Exa API.", + args: { + query: { + type: "string", + desc: "The query to search for.", + required: true, + }, }, - }, - retvals: { - results: { - type: "array", - desc: "The results of the search.", - required: true, - of: { - type: "object", - desc: "A single search result.", + retvals: { + results: { + type: "array", + desc: "The results of the search.", + required: true, of: { - title: { - type: "string", - desc: "The title of the result.", - required: true, - }, - url: { - type: "string", - desc: "The URL of the result.", - required: true, - }, - text: { - type: "string", - desc: "Text content snippet.", - required: true, + type: "object", + desc: "A single search result.", + of: { + title: { + type: "string", + desc: "The title of the result.", + required: true, + }, + url: { + type: "string", + desc: "The URL of the result.", + required: true, + }, + text: { + type: "string", + desc: "Text content snippet.", + required: true, + }, }, + required: true, }, - required: true, }, }, }, - fn: async (args: Dict) => { - const results = await this.exa.search(args.query); - return { results }; + { + fn: async (args: Dict) => { + const results = await this.exa.search(args.query); + return { results }; + }, + explain_args: (args: Dict) => ({ + summary: `Searching the web with Exa for ${args.query}...`, + }), + explain_retvals: (args: Dict, retvals: Dict) => ({ + summary: `Found ${retvals.results.length} results with Exa for ${args.query}.`, + details: JSON.stringify(retvals.results), + }), }, - explain_args: (args: Dict) => ({ - summary: `Searching the web with Exa for ${args.query}...`, - }), - explain_retvals: (args: Dict, retvals: Dict) => ({ - summary: `Found ${retvals.results.length} results with Exa for ${args.query}.`, - details: JSON.stringify(retvals.results), - }), - }); + ); } - athena.registerTool({ - name: "http/download-file", - desc: "Downloads a file from an HTTP/HTTPS URL.", - args: { - url: { - type: "string", - desc: "The URL to download the file from.", - required: true, - }, - filename: { - type: "string", - desc: "The filename to save the file as.", - required: true, + athena.registerTool( + { + name: "http/download-file", + desc: "Downloads a file from an HTTP/HTTPS URL.", + args: { + url: { + type: "string", + desc: "The URL to download the file from.", + required: true, + }, + filename: { + type: "string", + desc: "The filename to save the file as.", + required: true, + }, }, - }, - retvals: { - result: { - type: "string", - desc: "The result of the download.", - required: true, + retvals: { + result: { + type: "string", + desc: "The result of the download.", + required: true, + }, }, }, - fn: (args: Dict) => { - return new Promise((resolve, reject) => { - const file = fs.createWriteStream(args.filename); + { + fn: (args: Dict) => { + return new Promise((resolve, reject) => { + const file = fs.createWriteStream(args.filename); - const request = https.get(args.url, { - headers: this.headers, - }); + const request = https.get(args.url, { + headers: this.headers, + }); - request.on("error", reject); + request.on("error", reject); - request.on("response", (response) => { - if (response.statusCode !== 200) { - reject( - new Error(`Failed to download file: ${response.statusCode}`), - ); - return; - } + request.on("response", (response) => { + if (response.statusCode !== 200) { + reject( + new Error(`Failed to download file: ${response.statusCode}`), + ); + return; + } - response.pipe(file); + response.pipe(file); - file.on("finish", () => { - file.close(); - resolve({ result: "success" }); - }); + file.on("finish", () => { + file.close(); + resolve({ result: "success" }); + }); - file.on("error", (err) => { - fs.unlink(args.filename, () => reject(err)); + file.on("error", (err) => { + fs.unlink(args.filename, () => reject(err)); + }); }); }); - }); + }, + explain_args: (args: Dict) => ({ + summary: `Downloading the file from ${args.url} to ${args.filename}...`, + }), + explain_retvals: (args: Dict, retvals: Dict) => ({ + summary: `The file ${args.filename} was downloaded successfully.`, + }), }, - explain_args: (args: Dict) => ({ - summary: `Downloading the file from ${args.url} to ${args.filename}...`, - }), - explain_retvals: (args: Dict, retvals: Dict) => ({ - summary: `The file ${args.filename} was downloaded successfully.`, - }), - }); + ); } async unload(athena: Athena) { diff --git a/src/plugins/llm/init.ts b/src/plugins/llm/init.ts index 30ed623..c911204 100644 --- a/src/plugins/llm/init.ts +++ b/src/plugins/llm/init.ts @@ -20,145 +20,153 @@ export default class Llm extends PluginBase { this.athenaPrivateEventHandler.bind(this); athena.on("private-event", this.boundAthenaPrivateEventHandler); athena.emitPrivateEvent("webapp-ui/request-token", {}); - athena.registerTool({ - name: "llm/chat", - desc: "Chat with the LLM. Only chat with an LLM if absolutely necessary. For easy tasks such as translation or summarization, do not use this tool.", - args: { - message: { - type: "string", - desc: "The message to send to the LLM. The LLM doesn't have access to your context, so you need to include all necessary information in the message. Don't use any placeholders.", - required: true, - }, - image: { - type: "string", - desc: "The image to send to the LLM, if you need to. You can only send images to models that support them. Don't send the image in the message. Supports both URL and local image.", - required: false, - }, - model: { - type: "string", - desc: `The model to use. Available models: ${JSON.stringify( - this.config.models.chat, - )}`, - required: true, - }, - temperature: { - type: "number", - desc: "The temperature to use. 0 is the most deterministic, 1 is the most random.", - required: false, - }, - }, - retvals: { - result: { - type: "string", - desc: "The result of the LLM.", - required: true, + athena.registerTool( + { + name: "llm/chat", + desc: "Chat with the LLM. Only chat with an LLM if absolutely necessary. For easy tasks such as translation or summarization, do not use this tool.", + args: { + message: { + type: "string", + desc: "The message to send to the LLM. The LLM doesn't have access to your context, so you need to include all necessary information in the message. Don't use any placeholders.", + required: true, + }, + image: { + type: "string", + desc: "The image to send to the LLM, if you need to. You can only send images to models that support them. Don't send the image in the message. Supports both URL and local image.", + required: false, + }, + model: { + type: "string", + desc: `The model to use. Available models: ${JSON.stringify( + this.config.models.chat, + )}`, + required: true, + }, + temperature: { + type: "number", + desc: "The temperature to use. 0 is the most deterministic, 1 is the most random.", + required: false, + }, }, - citations: { - type: "array", - desc: "The citations of the LLM.", - required: false, - of: { + retvals: { + result: { type: "string", - desc: "The citation of the LLM.", + desc: "The result of the LLM.", required: true, }, + citations: { + type: "array", + desc: "The citations of the LLM.", + required: false, + of: { + type: "string", + desc: "The citation of the LLM.", + required: true, + }, + }, }, }, - fn: async (args: Dict) => { - let image; - if (args.image) { - if (args.image.startsWith("http")) { - image = args.image; - } else { - image = await image2uri(args.image); + { + fn: async (args: Dict) => { + let image; + if (args.image) { + if (args.image.startsWith("http")) { + image = args.image; + } else { + image = await image2uri(args.image); + } } - } - const response = await this.openai.chat.completions.create({ - messages: [ - { - role: "user", - content: [ - { - type: "text", - text: args.message, - }, - ...(image - ? [ - { - type: "image_url", - image_url: { - url: image, + const response = await this.openai.chat.completions.create({ + messages: [ + { + role: "user", + content: [ + { + type: "text", + text: args.message, + }, + ...(image + ? [ + { + type: "image_url", + image_url: { + url: image, + }, }, - }, - ] - : []), - ] as ChatCompletionContentPart[], - }, - ], - model: args.model, - temperature: args.temperature, - }); - return { - result: response.choices[0].message.content, - citations: (response as Dict).citations, - }; - }, - explain_args: (args: Dict) => ({ - summary: `Chatting with ${args.model}...`, - details: args.message, - }), - explain_retvals: (args: Dict, retvals: Dict) => ({ - summary: `${args.model} responded.`, - details: retvals.result, - }), - }); - athena.registerTool({ - name: "llm/generate-image", - desc: "Generate an image with an image generation model.", - args: { - prompt: { - type: "string", - desc: "The prompt to use for the image generation.", - required: true, - }, - model: { - type: "string", - desc: `The model to use. Available models: ${JSON.stringify( - this.config.models.image, - )}`, - required: true, + ] + : []), + ] as ChatCompletionContentPart[], + }, + ], + model: args.model, + temperature: args.temperature, + }); + return { + result: response.choices[0].message.content!, + citations: (response as Dict).citations, + }; }, + explain_args: (args: Dict) => ({ + summary: `Chatting with ${args.model}...`, + details: args.message, + }), + explain_retvals: (args: Dict, retvals: Dict) => ({ + summary: `${args.model} responded.`, + details: retvals.result, + }), }, - retvals: { - urls: { - type: "array", - desc: "The URLs of the generated images.", - required: true, - of: { + ); + athena.registerTool( + { + name: "llm/generate-image", + desc: "Generate an image with an image generation model.", + args: { + prompt: { + type: "string", + desc: "The prompt to use for the image generation.", + required: true, + }, + model: { type: "string", - desc: "The URL of the generated image.", + desc: `The model to use. Available models: ${JSON.stringify( + this.config.models.image, + )}`, required: true, }, }, + retvals: { + urls: { + type: "array", + desc: "The URLs of the generated images.", + required: true, + of: { + type: "string", + desc: "The URL of the generated image.", + required: true, + }, + }, + }, }, - fn: async (args: Dict) => { - const response = await this.openai.images.generate({ - prompt: args.prompt, - model: args.model, - }); - return { - urls: response.data.map((image) => image.url), - }; + { + fn: async (args) => { + const response = await this.openai.images.generate({ + prompt: args.prompt, + model: args.model, + }); + return { + urls: response.data.map((image) => image.url!), + }; + }, + explain_args: (args: Dict) => ({ + summary: `Generating an image with ${args.model}...`, + details: args.prompt, + }), + explain_retvals: (args: Dict, retvals: Dict) => ({ + summary: `The image is generated by ${args.model}.`, + details: retvals.urls.join(", "), + }), }, - explain_args: (args: Dict) => ({ - summary: `Generating an image with ${args.model}...`, - details: args.prompt, - }), - explain_retvals: (args: Dict, retvals: Dict) => ({ - summary: `The image is generated by ${args.model}.`, - details: retvals.urls.join(", "), - }), - }); + ); } async unload(athena: Athena) { diff --git a/src/plugins/long-term-memory/init.ts b/src/plugins/long-term-memory/init.ts index 3d704e4..50b73af 100644 --- a/src/plugins/long-term-memory/init.ts +++ b/src/plugins/long-term-memory/init.ts @@ -47,116 +47,134 @@ export default class LongTermMemory extends PluginBase { defaultHeaders: openaiDefaultHeaders, }); - athena.registerTool({ - name: "ltm/store", - desc: "Store some data to your long-term memory.", - args: { - desc: { - type: "string", - desc: "A description of the data.", - required: true, + athena.registerTool( + { + name: "ltm/store", + desc: "Store some data to your long-term memory.", + args: { + desc: { + type: "string", + desc: "A description of the data.", + required: true, + }, + data: { + type: "object", + desc: "The data to store.", + required: true, + }, }, - data: { - type: "object", - desc: "The data to store.", - required: true, + retvals: { + status: { + type: "string", + desc: "The status of the operation.", + required: true, + }, }, }, - retvals: { - status: { - type: "string", - desc: "The status of the operation.", - required: true, + { + fn: async (args: Dict) => { + const embedding = await this.openai.embeddings.create({ + model: this.config.vector_model, + dimensions: this.config.dimensions, + input: args.desc, + encoding_format: "float", + }); + insertStmt.run( + Float32Array.from(embedding.data[0].embedding), + args.desc, + JSON.stringify(args.data), + ); + return { status: "success" }; }, }, - fn: async (args: Dict) => { - const embedding = await this.openai.embeddings.create({ - model: this.config.vector_model, - dimensions: this.config.dimensions, - input: args.desc, - encoding_format: "float", - }); - insertStmt.run( - Float32Array.from(embedding.data[0].embedding), - args.desc, - JSON.stringify(args.data), - ); - return { status: "success" }; - }, - }); + ); // TODO: Implement remove - athena.registerTool({ - name: "ltm/list", - desc: "List your long-term memory.", - args: {}, - retvals: { - list: { - type: "array", - desc: "The list of metadata of the long-term memory.", - required: true, - of: { - type: "object", - desc: "The metadata of the long-term memory.", - required: false, + athena.registerTool( + { + name: "ltm/list", + desc: "List your long-term memory.", + args: {}, + retvals: { + list: { + type: "array", + desc: "The list of metadata of the long-term memory.", + required: true, of: { - desc: { - type: "string", - desc: "The description of the data.", - required: true, + type: "object", + desc: "The metadata of the long-term memory.", + required: false, + of: { + desc: { + type: "string", + desc: "The description of the data.", + required: true, + }, }, }, }, }, }, - fn: async (args: Dict) => { - const list = this.db.prepare("SELECT desc, data FROM vec_items").all(); - return { list: list }; - }, - }); - athena.registerTool({ - name: "ltm/retrieve", - desc: "Retrieve data from your long-term memory.", - args: { - query: { - type: "string", - desc: "The query to retrieve the data.", - required: true, + { + fn: async (args: Dict) => { + const list = this.db + .prepare("SELECT desc, data FROM vec_items") + .all(); + return { + list: list.map((item) => ({ + desc: String(item.desc), + data: JSON.parse(String(item.data)), + })), + }; }, }, - retvals: { - list: { - type: "array", - desc: "Query results list of metadata of the long-term memory.", - required: true, - of: { - type: "object", - desc: "The desc and data of the long-term memory.", - required: false, + ); + athena.registerTool( + { + name: "ltm/retrieve", + desc: "Retrieve data from your long-term memory.", + args: { + query: { + type: "string", + desc: "The query to retrieve the data.", + required: true, + }, + }, + retvals: { + list: { + type: "array", + desc: "Query results list of metadata of the long-term memory.", + required: true, of: { - desc: { - type: "string", - desc: "The description of the data.", - required: true, - }, - data: { - type: "object", - desc: "The data.", - required: true, + type: "object", + desc: "The desc and data of the long-term memory.", + required: false, + of: { + desc: { + type: "string", + desc: "The description of the data.", + required: true, + }, + data: { + type: "object", + desc: "The data.", + required: true, + }, }, }, }, }, }, - fn: async (args: Dict) => { - const embedding = await this.openai.embeddings.create({ - model: this.config.vector_model, - dimensions: this.config.dimensions, - input: args.query, - encoding_format: "float", - }); - const results = this.db - .prepare( - `SELECT + { + fn: async (args) => { + const embedding = await this.openai.embeddings.create({ + model: this.config.vector_model, + dimensions: this.config.dimensions, + input: args.query, + encoding_format: "float", + }); + const results = this.db + .prepare( + `SELECT distance, desc, data @@ -164,22 +182,25 @@ export default class LongTermMemory extends PluginBase { WHERE embedding MATCH ? ORDER BY distance LIMIT ${this.config.max_query_results}`, - ) - .all(Float32Array.from(embedding.data[0].embedding)); - if (!results || results.length === 0) { - throw new Error("No results found"); - } - return results.map((result) => { - if (!result || typeof result !== "object") { - throw new Error("Invalid result format"); + ) + .all(Float32Array.from(embedding.data[0].embedding)); + if (!results || results.length === 0) { + throw new Error("No results found"); } return { - desc: String(result.desc), - data: JSON.parse(String(result.data)), + list: results.map((result) => { + if (!result || typeof result !== "object") { + throw new Error("Invalid result format"); + } + return { + desc: String(result.desc), + data: JSON.parse(String(result.data)), + }; + }), }; - }); + }, }, - }); + ); } async unload(athena: Athena) { diff --git a/src/plugins/python/init.ts b/src/plugins/python/init.ts index b571ce6..e180a61 100644 --- a/src/plugins/python/init.ts +++ b/src/plugins/python/init.ts @@ -7,116 +7,130 @@ import { PluginBase } from "../plugin-base.js"; export default class Python extends PluginBase { async load(athena: Athena) { - athena.registerTool({ - name: "python/exec", - desc: "Executes Python code. Whenever you need to run Python code or do *any* kind of math calculations, or the user's request requires running Python code or doing math calculations, use this tool. You must print the final result to get it. Otherwise the stdout will be empty. Only use Python when it's necessary. If you already have all the information you need for some simple task, don't use Python. Whenever you need to get stock data, use Python with the yfinance package. If you need to plot something, use Python with the matplotlib package; use seaborn-v0_8-darkgrid or fivethirtyeight style and configurations of your choice (like font size, DPI=300, etc.) to get a high quality plot.", - args: { - code: { - type: "string", - desc: "Python code", - required: true, + athena.registerTool( + { + name: "python/exec", + desc: "Executes Python code. Whenever you need to run Python code or do *any* kind of math calculations, or the user's request requires running Python code or doing math calculations, use this tool. You must print the final result to get it. Otherwise the stdout will be empty. Only use Python when it's necessary. If you already have all the information you need for some simple task, don't use Python. Whenever you need to get stock data, use Python with the yfinance package. If you need to plot something, use Python with the matplotlib package; use seaborn-v0_8-darkgrid or fivethirtyeight style and configurations of your choice (like font size, DPI=300, etc.) to get a high quality plot.", + args: { + code: { + type: "string", + desc: "Python code", + required: true, + }, }, - }, - retvals: { - stdout: { - type: "string", - desc: "Standard output of the code", - required: true, + retvals: { + stdout: { + type: "string", + desc: "Standard output of the code", + required: true, + }, }, }, - fn: async (args: Dict) => { - return { stdout: (await PythonShell.runString(args.code)).join("\n") }; - }, - explain_args: (args: Dict) => ({ - summary: `Executing Python code...`, - details: args.code, - }), - explain_retvals: (args: Dict, retvals: Dict) => ({ - summary: `The Python code has finished.`, - details: retvals.stdout, - }), - }); - athena.registerTool({ - name: "python/exec-file", - desc: "Executes Python code from a file. Whenever you need to run a Python file, or the user's request requires running Python file, use this tool.", - args: { - path: { - type: "string", - desc: "Path to the Python file", - required: true, + { + fn: async (args: Dict) => { + return { + stdout: (await PythonShell.runString(args.code)).join("\n"), + }; }, + explain_args: (args: Dict) => ({ + summary: `Executing Python code...`, + details: args.code, + }), + explain_retvals: (args: Dict, retvals: Dict) => ({ + summary: `The Python code has finished.`, + details: retvals.stdout, + }), + }, + ); + athena.registerTool( + { + name: "python/exec-file", + desc: "Executes Python code from a file. Whenever you need to run a Python file, or the user's request requires running Python file, use this tool.", args: { - type: "array", - desc: "Arguments to pass to the Python file", - required: false, - of: { + path: { type: "string", - desc: "Argument to pass", + desc: "Path to the Python file", required: true, }, + args: { + type: "array", + desc: "Arguments to pass to the Python file", + required: false, + of: { + type: "string", + desc: "Argument to pass", + required: true, + }, + }, }, - }, - retvals: { - stdout: { - desc: "Standard output of the code", - required: true, - type: "string", + retvals: { + stdout: { + desc: "Standard output of the code", + required: true, + type: "string", + }, }, }, - fn: async (args: Dict) => { - return { - stdout: (await PythonShell.run(args.path, { args: args.args })).join( - "\n", - ), - }; - }, - explain_args: (args: Dict) => ({ - summary: `Executing Python file ${args.path}...`, - details: args.args ? args.args.join(", ") : "", - }), - explain_retvals: (args: Dict, retvals: Dict) => ({ - summary: `The Python file has finished.`, - details: retvals.stdout, - }), - }); - athena.registerTool({ - name: "python/pip-install", - desc: "Installs a Python package using pip. Whenever you need to use a package that is not installed, or the user's request requires installing a package, use this tool. Don't use shell to install packages.", - args: { - package: { - type: "string", - desc: "Package name", - required: true, + { + fn: async (args: Dict) => { + return { + stdout: ( + await PythonShell.run(args.path, { args: args.args }) + ).join("\n"), + }; }, + explain_args: (args: Dict) => ({ + summary: `Executing Python file ${args.path}...`, + details: args.args ? args.args.join(", ") : "", + }), + explain_retvals: (args: Dict, retvals: Dict) => ({ + summary: `The Python file has finished.`, + details: retvals.stdout, + }), }, - retvals: { - result: { - desc: "Result of the installation", - required: true, - type: "string", + ); + athena.registerTool( + { + name: "python/pip-install", + desc: "Installs a Python package using pip. Whenever you need to use a package that is not installed, or the user's request requires installing a package, use this tool. Don't use shell to install packages.", + args: { + package: { + type: "string", + desc: "Package name", + required: true, + }, + }, + retvals: { + result: { + desc: "Result of the installation", + required: true, + type: "string", + }, }, }, - fn: (args: Dict) => { - return new Promise((resolve, reject) => { - exec( - `python -m pip install ${args.package} --break-system-packages --index-url https://download.pytorch.org/whl/cpu --extra-index-url https://pypi.org/simple`, - (error, stdout, stderr) => { - if (error) { - reject(Error(stderr)); - } else { - resolve({ result: "success" }); - } - }, - ); - }); + { + fn: (args: Dict) => { + return new Promise((resolve, reject) => { + exec( + `python -m pip install ${args.package} --break-system-packages --index-url https://download.pytorch.org/whl/cpu --extra-index-url https://pypi.org/simple`, + (error, stdout, stderr) => { + if (error) { + reject(Error(stderr)); + } else { + resolve({ result: "success" }); + } + }, + ); + }); + }, + explain_args: (args: Dict) => ({ + summary: `Installing Python package ${args.package}...`, + }), + explain_retvals: (args: Dict, retvals: Dict) => ({ + summary: `The Python package ${args.package} is installed.`, + }), }, - explain_args: (args: Dict) => ({ - summary: `Installing Python package ${args.package}...`, - }), - explain_retvals: (args: Dict, retvals: Dict) => ({ - summary: `The Python package ${args.package} is installed.`, - }), - }); + ); } async unload(athena: Athena) { diff --git a/src/plugins/shell/init.ts b/src/plugins/shell/init.ts index 69f1e87..6426b6c 100644 --- a/src/plugins/shell/init.ts +++ b/src/plugins/shell/init.ts @@ -53,157 +53,173 @@ export default class Shell extends PluginBase { details: args.stdout, }), }); - athena.registerTool({ - name: "shell/exec", - desc: "Executes a shell command. Whenever you need to run a shell command, or the user's request requires running a shell command, use this tool. When this tool returns, the command is still running. You need to wait for it to output or terminate.", - args: { - command: { - type: "string", - desc: "Shell command", - required: true, + athena.registerTool( + { + name: "shell/exec", + desc: "Executes a shell command. Whenever you need to run a shell command, or the user's request requires running a shell command, use this tool. When this tool returns, the command is still running. You need to wait for it to output or terminate.", + args: { + command: { + type: "string", + desc: "Shell command", + required: true, + }, }, - }, - retvals: { - pid: { - type: "number", - desc: "The running process ID", - required: true, + retvals: { + pid: { + type: "number", + desc: "The running process ID", + required: true, + }, }, }, - fn: async (args: Dict) => { - const process = new ShellProcess(args.command); - process.on("stdout", (data) => { - athena.emitEvent("shell/stdout", { - pid: process.pid, - stdout: data, + { + fn: async (args: Dict) => { + const process = new ShellProcess(args.command); + process.on("stdout", (data) => { + athena.emitEvent("shell/stdout", { + pid: process.pid, + stdout: data, + }); }); - }); - process.on("close", (code) => { - delete this.processes[process.pid]; - athena.emitEvent("shell/terminated", { - pid: process.pid, - code, - stdout: process.stdout, + process.on("close", (code) => { + delete this.processes[process.pid]; + athena.emitEvent("shell/terminated", { + pid: process.pid, + code, + stdout: process.stdout, + }); }); - }); - this.processes[process.pid] = process; - return { pid: process.pid }; + this.processes[process.pid] = process; + return { pid: process.pid }; + }, + explain_args: (args: Dict) => ({ + summary: `Executing shell command ${args.command}...`, + }), + explain_retvals: (args: Dict, retvals: Dict) => ({ + summary: `The shell command is assigned PID ${retvals.pid}.`, + }), }, - explain_args: (args: Dict) => ({ - summary: `Executing shell command ${args.command}...`, - }), - explain_retvals: (args: Dict, retvals: Dict) => ({ - summary: `The shell command is assigned PID ${retvals.pid}.`, - }), - }); - athena.registerTool({ - name: "shell/write-stdin", - desc: "Write string to stdin of the specified process.", - args: { - pid: { - type: "number", - desc: "Process ID", - required: true, + ); + athena.registerTool( + { + name: "shell/write-stdin", + desc: "Write string to stdin of the specified process.", + args: { + pid: { + type: "number", + desc: "Process ID", + required: true, + }, + data: { + type: "string", + desc: "The data to write to stdin.", + required: true, + }, }, - data: { - type: "string", - desc: "The data to write to stdin.", - required: true, + retvals: { + result: { + type: "string", + desc: "Result of the operation", + required: true, + }, }, }, - retvals: { - result: { - type: "string", - desc: "Result of the operation", - required: true, + { + fn: async (args: Dict) => { + const process = this.processes[args.pid]; + if (!process) { + throw new Error("Process not found"); + } + process.write(args.data); + return { result: "success" }; }, + explain_args: (args: Dict) => ({ + summary: `Writing to process ${args.pid}...`, + details: args.data, + }), }, - fn: async (args: Dict) => { - const process = this.processes[args.pid]; - if (!process) { - throw new Error("Process not found"); - } - process.write(args.data); - return { result: "success" }; - }, - explain_args: (args: Dict) => ({ - summary: `Writing to process ${args.pid}...`, - details: args.data, - }), - }); - athena.registerTool({ - name: "shell/kill", - desc: "Kills a running shell command. Whenever you need to kill a running shell command, or the user's request requires killing a running shell command, use this tool. Do not use this tool to terminate any other processes. Use shell/exec to execute a kill command instead.", - args: { - pid: { - type: "number", - desc: "Process ID", - required: true, + ); + athena.registerTool( + { + name: "shell/kill", + desc: "Kills a running shell command. Whenever you need to kill a running shell command, or the user's request requires killing a running shell command, use this tool. Do not use this tool to terminate any other processes. Use shell/exec to execute a kill command instead.", + args: { + pid: { + type: "number", + desc: "Process ID", + required: true, + }, + signal: { + type: "string", + desc: "Signal to send to the process", + required: false, + }, }, - signal: { - type: "string", - desc: "Signal to send to the process", - required: false, + retvals: { + result: { + type: "string", + desc: "Result of the operation", + required: true, + }, }, }, - retvals: { - result: { - type: "string", - desc: "Result of the operation", - required: true, + { + fn: async (args: Dict) => { + const process = this.processes[args.pid]; + if (!process) { + throw new Error("Process not found"); + } + process.kill(args.signal); + return { result: "success" }; }, + explain_args: (args: Dict) => ({ + summary: `Killing process ${args.pid}...`, + details: args.signal, + }), + explain_retvals: (args: Dict, retvals: Dict) => ({ + summary: `The process ${args.pid} is killed.`, + }), }, - fn: async (args: Dict) => { - const process = this.processes[args.pid]; - if (!process) { - throw new Error("Process not found"); - } - process.kill(args.signal); - return { result: "success" }; - }, - explain_args: (args: Dict) => ({ - summary: `Killing process ${args.pid}...`, - details: args.signal, - }), - explain_retvals: (args: Dict, retvals: Dict) => ({ - summary: `The process ${args.pid} is killed.`, - }), - }); - athena.registerTool({ - name: "shell/apt-install", - desc: "Installs a package using apt. Whenever you need to install a package using apt, or the user's request requires installing a package using apt, use this tool.", - args: { - package: { - type: "string", - desc: "Package name", - required: true, + ); + athena.registerTool( + { + name: "shell/apt-install", + desc: "Installs a package using apt. Whenever you need to install a package using apt, or the user's request requires installing a package using apt, use this tool.", + args: { + package: { + type: "string", + desc: "Package name", + required: true, + }, }, - }, - retvals: { - result: { - desc: "Result of the installation", - required: true, - type: "string", + retvals: { + result: { + desc: "Result of the installation", + required: true, + type: "string", + }, }, }, - fn: (args: Dict) => { - return new Promise((resolve, reject) => { - exec(`apt install ${args.package} -y`, (error, stdout, stderr) => { - if (error) { - reject(Error(stderr)); - } else { - resolve({ result: "success" }); - } + { + fn: (args: Dict) => { + return new Promise((resolve, reject) => { + exec(`apt install ${args.package} -y`, (error, stdout, stderr) => { + if (error) { + reject(Error(stderr)); + } else { + resolve({ result: "success" }); + } + }); }); - }); + }, + explain_args: (args: Dict) => ({ + summary: `Installing package ${args.package} using apt...`, + }), + explain_retvals: (args: Dict, retvals: Dict) => ({ + summary: `The package ${args.package} is installed.`, + }), }, - explain_args: (args: Dict) => ({ - summary: `Installing package ${args.package} using apt...`, - }), - explain_retvals: (args: Dict, retvals: Dict) => ({ - summary: `The package ${args.package} is installed.`, - }), - }); + ); } async unload(athena: Athena) { diff --git a/src/plugins/short-term-memory/init.ts b/src/plugins/short-term-memory/init.ts index 0a5837d..279b7f9 100644 --- a/src/plugins/short-term-memory/init.ts +++ b/src/plugins/short-term-memory/init.ts @@ -16,83 +16,95 @@ export default class ShortTermMemory extends PluginBase { } async load(athena: Athena) { - athena.registerTool({ - name: "stm/append-tasks", - desc: "Append an array of tasks to the short-term memory.", - args: { - tasks: { - type: "array", - desc: "The array of tasks to append.", - required: true, - of: { + athena.registerTool( + { + name: "stm/append-tasks", + desc: "Append an array of tasks to the short-term memory.", + args: { + tasks: { + type: "array", + desc: "The array of tasks to append.", + required: true, + of: { + type: "string", + desc: "The content of the task.", + required: true, + }, + }, + }, + retvals: { + status: { type: "string", - desc: "The content of the task.", + desc: "The status of the operation.", required: true, }, }, }, - retvals: { - status: { - type: "string", - desc: "The status of the operation.", - required: true, + { + fn: async (args: Dict) => { + this.tasks.push( + ...args.tasks.map((task: string) => ({ + content: task, + finished: false, + })), + ); + return { status: "success" }; }, }, - fn: async (args: Dict) => { - this.tasks.push( - ...args.tasks.map((task: string) => ({ - content: task, - finished: false, - })), - ); - return { status: "success" }; - }, - }); - athena.registerTool({ - name: "stm/mark-task-finished", - desc: "Mark tasks as finished.", - args: { - indices: { - type: "array", - desc: "The indices of the tasks to mark as finished.", - required: true, - of: { - type: "number", - desc: "The index of the task to mark as finished.", + ); + athena.registerTool( + { + name: "stm/mark-task-finished", + desc: "Mark tasks as finished.", + args: { + indices: { + type: "array", + desc: "The indices of the tasks to mark as finished.", required: true, + of: { + type: "number", + desc: "The index of the task to mark as finished.", + required: true, + }, }, }, - }, - retvals: { - status: { - type: "string", - desc: "The status of the operation.", - required: true, + retvals: { + status: { + type: "string", + desc: "The status of the operation.", + required: true, + }, }, }, - fn: async (args: Dict) => { - args.indices.forEach((index: number) => { - this.tasks[index].finished = true; - }); - return { status: "success" }; + { + fn: async (args: Dict) => { + args.indices.forEach((index: number) => { + this.tasks[index].finished = true; + }); + return { status: "success" }; + }, }, - }); - athena.registerTool({ - name: "stm/clear-tasks", - desc: "Clear all tasks.", - args: {}, - retvals: { - status: { - type: "string", - desc: "The status of the operation.", - required: true, + ); + athena.registerTool( + { + name: "stm/clear-tasks", + desc: "Clear all tasks.", + args: {}, + retvals: { + status: { + type: "string", + desc: "The status of the operation.", + required: true, + }, }, }, - fn: async () => { - this.tasks = []; - return { status: "success" }; + { + fn: async () => { + this.tasks = []; + return { status: "success" }; + }, }, - }); + ); } async unload(athena: Athena) { diff --git a/src/plugins/telegram/init.ts b/src/plugins/telegram/init.ts index 5ab40c1..fde4792 100644 --- a/src/plugins/telegram/init.ts +++ b/src/plugins/telegram/init.ts @@ -397,136 +397,152 @@ export default class Telegram extends PluginBase { }, }); - athena.registerTool({ - name: "telegram/send-message", - desc: "Send a message to a chat in Telegram.", - args: { - chat_id: { - type: "number", - desc: "Unique identifier for the target chat or username of the target channel.", - required: true, - }, - reply_to_message_id: { - type: "number", - desc: "Identifier of the message that will be replied to in the current chat.", - required: false, - }, - text: { - type: "string", - desc: "Text of the message to be sent, 1-4096 characters after entities parsing.", - required: true, - }, - photo: { - type: "string", - desc: "Photo to send. Pass a local filename or a URL.", - required: false, - }, - file: { - type: "string", - desc: "File to send. Pass a local filename or a URL.", - required: false, - }, - reply_markup: { - type: "object", - desc: "Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user.", - required: false, + athena.registerTool( + { + name: "telegram/send-message", + desc: "Send a message to a chat in Telegram.", + args: { + chat_id: { + type: "number", + desc: "Unique identifier for the target chat or username of the target channel.", + required: true, + }, + reply_to_message_id: { + type: "number", + desc: "Identifier of the message that will be replied to in the current chat.", + required: false, + }, + text: { + type: "string", + desc: "Text of the message to be sent, 1-4096 characters after entities parsing.", + required: true, + }, + photo: { + type: "string", + desc: "Photo to send. Pass a local filename or a URL.", + required: false, + }, + file: { + type: "string", + desc: "File to send. Pass a local filename or a URL.", + required: false, + }, + reply_markup: { + type: "object", + desc: "Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user.", + required: false, + }, }, - }, - retvals: { - message_id: { - type: "number", - desc: "Unique message identifier inside this chat.", - required: true, + retvals: { + message_id: { + type: "number", + desc: "Unique message identifier inside this chat.", + required: true, + }, }, }, - fn: async (args: Dict) => { - if (args.photo) { - const message = await this.bot.sendPhoto(args.chat_id, args.photo, { - caption: args.text, - reply_to_message_id: args.reply_to_message_id, - reply_markup: args.reply_markup, - }); - return { message_id: message.message_id }; - } - if (args.file) { - const message = await this.bot.sendDocument(args.chat_id, args.file, { - caption: args.text, + { + fn: async (args: Dict) => { + if (args.photo) { + const message = await this.bot.sendPhoto(args.chat_id, args.photo, { + caption: args.text, + reply_to_message_id: args.reply_to_message_id, + reply_markup: args.reply_markup, + }); + return { message_id: message.message_id }; + } + if (args.file) { + const message = await this.bot.sendDocument( + args.chat_id, + args.file, + { + caption: args.text, + reply_to_message_id: args.reply_to_message_id, + reply_markup: args.reply_markup, + }, + ); + return { message_id: message.message_id }; + } + const message = await this.bot.sendMessage(args.chat_id, args.text, { reply_to_message_id: args.reply_to_message_id, reply_markup: args.reply_markup, }); return { message_id: message.message_id }; - } - const message = await this.bot.sendMessage(args.chat_id, args.text, { - reply_to_message_id: args.reply_to_message_id, - reply_markup: args.reply_markup, - }); - return { message_id: message.message_id }; + }, }, - }); + ); - athena.registerTool({ - name: "telegram/edit-message", - desc: "Edit a message in Telegram.", - args: { - chat_id: { - type: "number", - desc: "Unique identifier for the target chat or username of the target channel.", - required: true, - }, - message_id: { - type: "number", - desc: "Unique message identifier inside this chat.", - required: true, + athena.registerTool( + { + name: "telegram/edit-message", + desc: "Edit a message in Telegram.", + args: { + chat_id: { + type: "number", + desc: "Unique identifier for the target chat or username of the target channel.", + required: true, + }, + message_id: { + type: "number", + desc: "Unique message identifier inside this chat.", + required: true, + }, + text: { + type: "string", + desc: "New text of the message.", + required: true, + }, }, - text: { - type: "string", - desc: "New text of the message.", - required: true, + retvals: { + status: { + type: "string", + desc: "Status of the operation.", + required: true, + }, }, }, - retvals: { - status: { - type: "string", - desc: "Status of the operation.", - required: true, + { + fn: async (args: Dict) => { + await this.bot.editMessageText(args.text, { + chat_id: args.chat_id, + message_id: args.message_id, + }); + return { status: "success" }; }, }, - fn: async (args: Dict) => { - await this.bot.editMessageText(args.text, { - chat_id: args.chat_id, - message_id: args.message_id, - }); - return { status: "success" }; - }, - }); + ); - athena.registerTool({ - name: "telegram/delete-message", - desc: "Delete a message in Telegram.", - args: { - chat_id: { - type: "number", - desc: "Unique identifier for the target chat or username of the target channel.", - required: true, + athena.registerTool( + { + name: "telegram/delete-message", + desc: "Delete a message in Telegram.", + args: { + chat_id: { + type: "number", + desc: "Unique identifier for the target chat or username of the target channel.", + required: true, + }, + message_id: { + type: "number", + desc: "Unique message identifier inside this chat.", + required: true, + }, }, - message_id: { - type: "number", - desc: "Unique message identifier inside this chat.", - required: true, + retvals: { + status: { + type: "string", + desc: "Status of the operation.", + required: true, + }, }, }, - retvals: { - status: { - type: "string", - desc: "Status of the operation.", - required: true, + { + fn: async (args: Dict) => { + await this.bot.deleteMessage(args.chat_id, args.message_id); + return { status: "success" }; }, }, - fn: async (args: Dict) => { - await this.bot.deleteMessage(args.chat_id, args.message_id); - return { status: "success" }; - }, - }); + ); athena.once("plugins-loaded", () => { this.bot.on("message", async (msg) => { diff --git a/src/plugins/webapp-ui/init.ts b/src/plugins/webapp-ui/init.ts index 1191ce1..bd5d860 100644 --- a/src/plugins/webapp-ui/init.ts +++ b/src/plugins/webapp-ui/init.ts @@ -110,115 +110,119 @@ export default class WebappUI extends PluginBase { }, }, }); - athena.registerTool({ - name: "ui/send-message", - desc: "Sends a message to the user.", - args: { - content: { - type: "string", - desc: "The message to send to the user. This should be a valid Markdown message.", - required: true, - }, - files: { - type: "array", - desc: "Files to send to the user. Whenever the user requests a file or a download link to a file, you should use this argument to send the file to the user.", - required: false, - of: { - type: "object", - desc: "A file to send to the user.", + athena.registerTool( + { + name: "ui/send-message", + desc: "Sends a message to the user.", + args: { + content: { + type: "string", + desc: "The message to send to the user. This should be a valid Markdown message.", required: true, + }, + files: { + type: "array", + desc: "Files to send to the user. Whenever the user requests a file or a download link to a file, you should use this argument to send the file to the user.", + required: false, of: { - name: { - type: "string", - desc: "The name of the file.", - required: true, - }, - location: { - type: "string", - desc: "The location of the file. Send URL or absolute path. Don't send relative paths.", - required: true, + type: "object", + desc: "A file to send to the user.", + required: true, + of: { + name: { + type: "string", + desc: "The name of the file.", + required: true, + }, + location: { + type: "string", + desc: "The location of the file. Send URL or absolute path. Don't send relative paths.", + required: true, + }, }, }, }, }, - }, - retvals: { - status: { - type: "string", - desc: "Status of the operation.", - required: true, + retvals: { + status: { + type: "string", + desc: "Status of the operation.", + required: true, + }, }, }, - fn: async (args: Dict) => { - if (args.files) { - for (const file of args.files) { - if ( - file.location.startsWith( - "https://oaidalleapiprodscus.blob.core.windows.net", - ) - ) { - // This is an image from DALL-E. Download it and upload it to Supabase to avoid expiration. - const response = await fetch(file.location); - const buffer = await response.arrayBuffer(); - const tempPath = `./${Date.now()}-${file.name}`; - await fs.promises.writeFile(tempPath, Buffer.from(buffer)); - file.location = tempPath; + { + fn: async (args: Dict) => { + if (args.files) { + for (const file of args.files) { + if ( + file.location.startsWith( + "https://oaidalleapiprodscus.blob.core.windows.net", + ) + ) { + // This is an image from DALL-E. Download it and upload it to Supabase to avoid expiration. + const response = await fetch(file.location); + const buffer = await response.arrayBuffer(); + const tempPath = `./${Date.now()}-${file.name}`; + await fs.promises.writeFile(tempPath, Buffer.from(buffer)); + file.location = tempPath; + } + if (file.location.startsWith("http")) { + continue; + } + const digest = await fileDigest(file.location); + const storagePath = `${this.userId}/${digest.slice( + 0, + 2, + )}/${digest.slice(2, 12)}/${encodeURIComponent(file.name).replace( + /%/g, + "_", + )}`; + const contentType = mime.lookup(file.location); + const { error } = await this.supabase.storage + .from(this.config.supabase.files_bucket) + .upload(storagePath, fs.createReadStream(file.location), { + upsert: true, + contentType: contentType + ? contentType + : "application/octet-stream", + duplex: "half", + }); + if (error) { + throw new Error( + `Error uploading file ${file.name}: ${error.message}`, + ); + } + file.location = this.supabase.storage + .from(this.config.supabase.files_bucket) + .getPublicUrl(storagePath).data.publicUrl; } - if (file.location.startsWith("http")) { - continue; - } - const digest = await fileDigest(file.location); - const storagePath = `${this.userId}/${digest.slice( - 0, - 2, - )}/${digest.slice(2, 12)}/${encodeURIComponent(file.name).replace( - /%/g, - "_", - )}`; - const contentType = mime.lookup(file.location); - const { error } = await this.supabase.storage - .from(this.config.supabase.files_bucket) - .upload(storagePath, fs.createReadStream(file.location), { - upsert: true, - contentType: contentType - ? contentType - : "application/octet-stream", - duplex: "half", - }); - if (error) { - throw new Error( - `Error uploading file ${file.name}: ${error.message}`, - ); - } - file.location = this.supabase.storage - .from(this.config.supabase.files_bucket) - .getPublicUrl(storagePath).data.publicUrl; } - } - const message: IWebappUIMessage = { - type: "message", - data: { - role: "assistant", - content: args.content, - files: args.files, - timestamp: Date.now(), - }, - }; - this.supabase - .from("messages") - .insert({ - context_id: this.config.context_id, - message, - }) - .then(({ error }) => { - if (error) { - this.logger.error(error); - } - }); - await this.sendMessage(message); - return { status: "success" }; + const message: IWebappUIMessage = { + type: "message", + data: { + role: "assistant", + content: args.content, + files: args.files, + timestamp: Date.now(), + }, + }; + this.supabase + .from("messages") + .insert({ + context_id: this.config.context_id, + message, + }) + .then(({ error }) => { + if (error) { + this.logger.error(error); + } + }); + await this.sendMessage(message); + return { status: "success" }; + }, }, - }); + ); athena.once("plugins-loaded", async () => { this.wss = new WebSocketServer({ port: this.config.port }); this.wss.on("connection", (ws, req) => {