Add function calling

This commit is contained in:
Aslan 2025-12-29 12:49:52 +01:00
parent f3a74bc46c
commit a5d6163ef9
13 changed files with 272 additions and 32 deletions

View file

@ -26,6 +26,7 @@
"api": {
"key": ""
},
"password": "",
"personalities": [
{
"personalityName": "puk",

View file

@ -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}`,

View file

@ -1,2 +1,4 @@
export * from "./ai.js";
export * from "./types.js";
export * from "./alts.js";
export * from "./prices.js";

View file

@ -16,6 +16,7 @@ interface IAIInstructions {
aiPersonality: string;
aiLikes: string;
aiDislikes: string;
aiMemory: string[];
}
export { type IAIAlt, type IAIUser, type IAIInstructions };

View file

@ -69,7 +69,6 @@ const onHelp = (_text: string, roomId: string) => {
<li><b>!bowling {text}</b> - Repeats your message in bowling</li>
<li><b>!help</b> - Prints this help message</li>
<li><b>!me / !me {mention}</b> - Prints data about you</li>
<li><b>!myinfo {newinfo}</b> - Updates your description</li>
<li><b>!leaderboard</b> - Prints total user ranking</li>
<li><b>!aileaderboard</b> - Prints total user ai cost</li>
</ul>

View file

@ -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);

View file

@ -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<AIResponseText> => {
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),
};
};

View file

@ -1,2 +1,3 @@
export * from "./ai.js";
export * from "./types.js";
export * from "./tools.js";

139
src/services/ai/tools.ts Normal file
View file

@ -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 };

View file

@ -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,
};

View file

@ -10,6 +10,7 @@ let state: IState = {
index: 0,
startTime: 0,
},
aiMemory: [],
};
const load = () => {

View file

@ -1,11 +1,11 @@
interface IState {
users: IUser[];
personality: IPersonality;
aiMemory: string[];
}
interface IUser {
id: string;
information?: string;
role: TRole;
experience: number;
money: number;