diff --git a/package.json b/package.json index abedd80..9216279 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aslobot-matrix", - "version": "0.8.2", + "version": "0.9.0", "description": "", "license": "ISC", "author": "", diff --git a/src/config.preset.json b/src/config.preset.json index 1b6e656..d408241 100644 --- a/src/config.preset.json +++ b/src/config.preset.json @@ -26,6 +26,7 @@ "api": { "key": "" }, + "password": "", "personalities": [ { "personalityName": "puk", diff --git a/src/modules/ai/ai.ts b/src/modules/ai/ai.ts index d200556..3f2b7f0 100644 --- a/src/modules/ai/ai.ts +++ b/src/modules/ai/ai.ts @@ -59,19 +59,17 @@ const onAI = async ( personality = config.app.ai.personalities[state.personality.index]; - let textMod = text.replace("!ai", "").trim().toLowerCase(); + let textMod = text.replace("!ai", "").trim(); let instructions = { prefferedLanguages: ["english", "slovak"], users: alts.map((alt) => ({ names: alt.keys, alt: alt.alt, - backstory: alt.id - ? getUserById(`${alt.id}:${config.server}`).information - : undefined, })), aiPersonality: personality?.personality ?? "", aiLikes: personality?.likes ?? "", aiDislikes: personality?.dislikes ?? "", + aiMemory: state.aiMemory ?? [], } as IAIInstructions; const username = getUserName(user); @@ -81,6 +79,11 @@ const onAI = async ( } const responseAI = await getTextGemini( + { + client: client, + roomId: roomId, + sender: sender, + }, instructions, `${username}: ${textMod}`, `${repliedUsername}: ${repliedMessage}`, diff --git a/src/modules/ai/index.ts b/src/modules/ai/index.ts index ceedc5b..b69311d 100644 --- a/src/modules/ai/index.ts +++ b/src/modules/ai/index.ts @@ -1,2 +1,4 @@ export * from "./ai.js"; export * from "./types.js"; +export * from "./alts.js"; +export * from "./prices.js"; diff --git a/src/modules/ai/types.ts b/src/modules/ai/types.ts index c7a64cb..7687d52 100644 --- a/src/modules/ai/types.ts +++ b/src/modules/ai/types.ts @@ -16,6 +16,7 @@ interface IAIInstructions { aiPersonality: string; aiLikes: string; aiDislikes: string; + aiMemory: string[]; } export { type IAIAlt, type IAIUser, type IAIInstructions }; diff --git a/src/modules/base/base.ts b/src/modules/base/base.ts index d406df5..26af25f 100644 --- a/src/modules/base/base.ts +++ b/src/modules/base/base.ts @@ -69,7 +69,6 @@ const onHelp = (_text: string, roomId: string) => {
  • !bowling {text} - Repeats your message in bowling
  • !help - Prints this help message
  • !me / !me {mention} - Prints data about you
  • -
  • !myinfo {newinfo} - Updates your description
  • !leaderboard - Prints total user ranking
  • !aileaderboard - Prints total user ai cost
  • diff --git a/src/modules/user/user.ts b/src/modules/user/user.ts index 76cedc5..7c9d9d0 100644 --- a/src/modules/user/user.ts +++ b/src/modules/user/user.ts @@ -21,10 +21,6 @@ const registerModuleUser = ( startConditions: [`${config.app.triggerPrefix}me`], callbackFunc: onMe, }); - callbackStore.messageCallbacks.push({ - startConditions: [`${config.app.triggerPrefix}myinfo `], - callbackFunc: onMyInfo, - }); callbackStore.messageCallbacks.push({ startConditions: [`${config.app.triggerPrefix}leaderboard`], callbackFunc: onLeaderboard, @@ -55,22 +51,6 @@ const onMe = (text: string, roomId: string, sender: string) => { ); }; -const onMyInfo = (text: string, roomId: string, sender: string) => { - const user = getUserById(sender); - const newInformation = text.replace( - `${config.app.triggerPrefix}myinfo `, - "", - ); - - if (!user || newInformation.length < 3) { - return; - } - - user.information = newInformation; - - client.sendTextMessage(roomId, "Information updated"); -}; - const onLeaderboard = (_text: string, roomId: string) => { const mapUsersToLeaderboard = (user: IUser): string => { const level = getLevel(user.experience); diff --git a/src/services/ai/ai.ts b/src/services/ai/ai.ts index ffb434d..31664c7 100644 --- a/src/services/ai/ai.ts +++ b/src/services/ai/ai.ts @@ -1,28 +1,123 @@ import { GoogleGenAI } from "@google/genai"; import { config } from "../../config.js"; -import type { AIResponseImage, AIResponseText } from "./types.js"; +import type { + AIResponseImage, + AIResponseText, + AIToolMatrixData, +} from "./types.js"; import type { IAIInstructions } from "../../modules/ai/types.js"; +import { FunctionCallingConfigMode } from "@google/genai"; +import { toolFunctions, tools } from "./tools.js"; +import type { FunctionResponse } from "@google/genai"; +import type { Content } from "@google/genai"; const googleAI = new GoogleGenAI({ apiKey: config.app.ai.api.key, }); const getTextGemini = async ( + matrixData: AIToolMatrixData, instructions: IAIInstructions, input: string, oldInput?: string, ): Promise => { + const inputContent: Content = { + role: "user", + parts: [ + { + text: input, + }, + ], + }; + const oldInputContent: Content = { + role: "user", + parts: [ + { + text: oldInput ?? "", + }, + ], + }; + + const contents: Content[] = oldInput + ? [oldInputContent, inputContent] + : [inputContent]; + const response = await googleAI.models.generateContent({ model: "gemini-3-flash-preview", - contents: oldInput ? [oldInput, input] : input, + contents: contents, config: { systemInstruction: JSON.stringify(instructions), + toolConfig: { + functionCallingConfig: { + mode: FunctionCallingConfigMode.AUTO, + }, + }, + tools: [{ functionDeclarations: tools }], + }, + }); + + let text = response.text ?? "AI Error"; + let token = response.usageMetadata?.totalTokenCount ?? 0; + + const content = response.candidates?.at(0)?.content; + const functionCall = content?.parts?.at(0)?.functionCall; + + if (response.text || !content || !functionCall) { + return { + text: text, + tokens: token, + }; + } + + text = `Calling function ${functionCall.name}`; + + const func = toolFunctions.find( + (func) => func.name === functionCall.name, + )?.function; + if (!func) { + return { + text: text, + tokens: token, + }; + } + + const output = func(matrixData, functionCall.args); + const functionResponse: FunctionResponse = { + id: functionCall.id ?? "", + name: functionCall.name ?? "", + response: { + output: JSON.stringify(output), + }, + }; + + const responseTool = await googleAI.models.generateContent({ + model: "gemini-3-flash-preview", + contents: [ + ...contents, + content, + { + role: "tool", + parts: [ + { + functionResponse: functionResponse, + }, + ], + }, + ], + config: { + systemInstruction: JSON.stringify(instructions), + toolConfig: { + functionCallingConfig: { + mode: FunctionCallingConfigMode.AUTO, + }, + }, + tools: [{ functionDeclarations: tools }], }, }); return { - text: response.text ?? "AI Error", - tokens: response.usageMetadata?.totalTokenCount ?? 0, + text: responseTool.text ?? "AI Error", + tokens: token + (responseTool.usageMetadata?.totalTokenCount ?? 0), }; }; diff --git a/src/services/ai/index.ts b/src/services/ai/index.ts index ceedc5b..7e1e37a 100644 --- a/src/services/ai/index.ts +++ b/src/services/ai/index.ts @@ -1,2 +1,3 @@ export * from "./ai.js"; export * from "./types.js"; +export * from "./tools.js"; diff --git a/src/services/ai/tools.ts b/src/services/ai/tools.ts new file mode 100644 index 0000000..e2fbb5b --- /dev/null +++ b/src/services/ai/tools.ts @@ -0,0 +1,139 @@ +import type { FunctionDeclaration } from "@google/genai"; +import { config, packageConfig } from "../../config.js"; +import type { AIToolFunction } from "./types.js"; +import { save, state } from "../../store/store.js"; + +const tools: FunctionDeclaration[] = [ + { + name: "getBotInfo", + description: "Gets information about this AI bot", + parametersJsonSchema: { + type: "object", + properties: {}, + required: [], + }, + }, + { + name: "shutdownBot", + description: "Shuts/Turns this AI bot down", + parametersJsonSchema: { + type: "object", + properties: { + password: { + type: "string", + description: "shutdown password provided by the user", + }, + }, + required: ["password"], + }, + }, + { + name: "remember", + description: "Gets information about this AI bot", + parametersJsonSchema: { + type: "object", + properties: { + knowledge: { + type: "string", + description: + "new knowledge to remember and add to bot total knowledge", + }, + }, + required: ["knowledge"], + }, + }, + { + name: "changeUsername", + description: "Changes the username of this AI bot", + parametersJsonSchema: { + type: "object", + properties: { + username: { + type: "string", + description: "new username", + }, + }, + required: ["username"], + }, + }, + { + name: "getFullUserData", + description: "Gets the full user data about all users", + parametersJsonSchema: { + type: "object", + properties: {}, + required: [], + }, + }, +]; + +const toolFunctions: AIToolFunction[] = [ + { + name: "getBotInfo", + function: () => { + return { + botInfo: `Aslobot version ${packageConfig.version}`, + }; + }, + }, + { + name: "shutdownBot", + function: async (matrix, args) => { + if (args.password === config.app.ai.password) { + await matrix.client.sendTextMessage( + matrix.roomId, + "Saving data...", + ); + save(); + + await matrix.client.sendTextMessage( + matrix.roomId, + "Shutting down...", + ); + process.exit(0); + } + return { + message: "incorrect password", + }; + }, + }, + { + name: "remember", + function: (_matrix, args) => { + if (!state.aiMemory) { + state.aiMemory = []; + } + if (args.knowledge) { + state.aiMemory.push(args.knowledge); + } + + return { + message: "success", + }; + }, + }, + { + name: "changeUsername", + function: (matrix, args) => { + if (args.username) { + state.aiMemory.push(args.knowledge); + } + + matrix.client.setDisplayName(args.username); + + return { + message: "success", + }; + }, + }, + { + name: "getFullUserData", + function: (_matrix, _args) => { + return { + users: state.users, + }; + }, + }, +]; + +export { tools, toolFunctions }; diff --git a/src/services/ai/types.ts b/src/services/ai/types.ts index a7cd3db..d5803d3 100644 --- a/src/services/ai/types.ts +++ b/src/services/ai/types.ts @@ -1,3 +1,5 @@ +import type { MatrixClient } from "matrix-js-sdk"; + interface AIResponseText { text: string; tokens: number; @@ -8,4 +10,20 @@ interface AIResponseImage { tokens: number; } -export { type AIResponseText, type AIResponseImage }; +interface AIToolFunction { + name: string; + function: (matrix: AIToolMatrixData, args: any) => object; +} + +interface AIToolMatrixData { + client: MatrixClient; + roomId: string; + sender: string; +} + +export { + type AIResponseText, + type AIResponseImage, + type AIToolFunction, + type AIToolMatrixData, +}; diff --git a/src/store/store.ts b/src/store/store.ts index 1095b2f..6afdce6 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -10,6 +10,7 @@ let state: IState = { index: 0, startTime: 0, }, + aiMemory: [], }; const load = () => { diff --git a/src/store/types.ts b/src/store/types.ts index 9c876c3..5604fa3 100644 --- a/src/store/types.ts +++ b/src/store/types.ts @@ -1,11 +1,11 @@ interface IState { users: IUser[]; personality: IPersonality; + aiMemory: string[]; } interface IUser { id: string; - information?: string; role: TRole; experience: number; money: number;