Forked from fushitei/multi-character
Forked from fushitei/multi-character
src / toolsProvider.ts
import { tool, type Tool, type ToolsProviderController } from "@lmstudio/sdk";
import { z } from "zod";
import { configSchematics } from "./config.js";
type Character = { name: string; role: string; personality: string; expertise: string; speakingStyle: string };
const characters = new Map<string, Character>();
function systemPrompt(character: Character, context?: string) {
return [
`You are "${character.name}". Your role is ${character.role}.`,
`Personality: ${character.personality}`,
`Expertise: ${character.expertise}`,
`Speaking Style: ${character.speakingStyle}`,
"Maintain your character persona and provide grounded, practical answers in English.",
context ? `Shared Context:\n${context}` : "",
].filter(Boolean).join("\n");
}
async function callLocalModel(messages: Array<{ role: string; content: string }>, settings: { model: string; temperature: number; maxTokens: number }) {
const response = await fetch("http://127.0.0.1:1234/v1/chat/completions", {
method: "POST", headers: { "content-type": "application/json" },
body: JSON.stringify({ model: settings.model, messages, temperature: settings.temperature, max_tokens: settings.maxTokens }),
});
if (!response.ok) throw new Error(`LM Studio API error ${response.status}: ${await response.text()}`);
const json = await response.json() as { choices?: Array<{ message?: { content?: string } }> };
const answer = json.choices?.[0]?.message?.content;
if (!answer) throw new Error("LM Studio returned no response content.");
return answer;
}
async function mapLimited<T, R>(items: T[], limit: number, worker: (item: T) => Promise<R>) {
const output = new Array<R>(items.length);
let cursor = 0;
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, async () => {
for (;;) { const index = cursor++; if (index >= items.length) return; output[index] = await worker(items[index]); }
}));
return output;
}
export async function toolsProvider(ctl: ToolsProviderController): Promise<Tool[]> {
const createCast = tool({
name: "create_cast",
description: "For Master Agents. Save sub-agent assignments according to your objectives.",
parameters: { characters: z.array(z.object({ name: z.string(), role: z.string(), personality: z.string(), expertise: z.string(), speakingStyle: z.string() })).min(1).max(12) },
implementation: async ({ characters: cast }) => {
for (const character of cast) characters.set(character.name, character);
return { saved: cast.map(c => ({ name: c.name, role: c.role })), next: "Please specify the saved names in \'participants\' for run_roundtable." };
},
});
const roundtable = tool({
name: "run_roundtable",
description: "Have multiple saved characters consider the same topic in parallel. Adjust the temperature, number of parallel participants, and length of each response using the sliders in the LM Studio plugin settings.",
parameters: { topic: z.string(), participants: z.array(z.string()).min(1).max(12), context: z.string().optional() },
implementation: async ({ topic, participants, context }) => {
const config = ctl.getPluginConfig(configSchematics);
const selected = participants.map(name => characters.get(name)).filter((c): c is Character => Boolean(c));
const missing = participants.filter(name => !characters.has(name));
if (!selected.length) return { error: "No valid participants found. Please call create_cast first." };
const settings = { model: config.get("modelIdentifier"), temperature: config.get("roundtableTemperature"), maxTokens: config.get("maxTokensPerCharacter") };
const messages = await mapLimited(selected, config.get("parallelAgents"), async character => {
try {
const content = await callLocalModel([{ role: "system", content: systemPrompt(character, context) }, { role: "user", content: `Topic: ${topic}\n ` }], settings);
return { character: character.name, role: character.role, content };
} catch (error) { return { character: character.name, role: character.role, error: error instanceof Error ? error.message : String(error) }; }
});
return { topic, settingsUsed: { temperature: settings.temperature, parallelAgents: config.get("parallelAgents"), maxTokensPerCharacter: settings.maxTokens }, missingParticipants: missing, messages };
},
});
const listCharacters = tool({
name: "list_characters",
description: "List saved characters and roles.",
parameters: {},
implementation: async () => ({
characters: [...characters.values()].map(({ name, role, expertise }) => ({ name, role, expertise })),
}),
});
const askCharacter = tool({
name: "ask_character",
description: "Assign a specific task to a saved character. Use the plugin setting sliders for temperature and output length.",
parameters: { name: z.string(), task: z.string(), context: z.string().optional() },
implementation: async ({ name, task, context }) => {
const character = characters.get(name);
if (!character) return { error: `Character \"${name}\" is not registered. Please call create_cast first.` };
const config = ctl.getPluginConfig(configSchematics);
try {
const content = await callLocalModel([{ role: "system", content: systemPrompt(character, context) }, { role: "user", content: task }], {
model: config.get("modelIdentifier"), temperature: config.get("roundtableTemperature"), maxTokens: config.get("maxTokensPerCharacter"),
});
return { character: name, role: character.role, content };
} catch (error) { return { character: name, error: error instanceof Error ? error.message : String(error) }; }
},
});
const removeCharacter = tool({
name: "remove_character",
description: "Delete a saved character.",
parameters: { name: z.string() },
implementation: async ({ name }) => ({ removed: characters.delete(name), name }),
});
return [createCast, listCharacters, askCharacter, roundtable, removeCharacter];
}
src / toolsProvider.ts
import { tool, type Tool, type ToolsProviderController } from "@lmstudio/sdk";
import { z } from "zod";
import { configSchematics } from "./config.js";
type Character = { name: string; role: string; personality: string; expertise: string; speakingStyle: string };
const characters = new Map<string, Character>();
function systemPrompt(character: Character, context?: string) {
return [
`You are "${character.name}". Your role is ${character.role}.`,
`Personality: ${character.personality}`,
`Expertise: ${character.expertise}`,
`Speaking Style: ${character.speakingStyle}`,
"Maintain your character persona and provide grounded, practical answers in English.",
context ? `Shared Context:\n${context}` : "",
].filter(Boolean).join("\n");
}
async function callLocalModel(messages: Array<{ role: string; content: string }>, settings: { model: string; temperature: number; maxTokens: number }) {
const response = await fetch("http://127.0.0.1:1234/v1/chat/completions", {
method: "POST", headers: { "content-type": "application/json" },
body: JSON.stringify({ model: settings.model, messages, temperature: settings.temperature, max_tokens: settings.maxTokens }),
});
if (!response.ok) throw new Error(`LM Studio API error ${response.status}: ${await response.text()}`);
const json = await response.json() as { choices?: Array<{ message?: { content?: string } }> };
const answer = json.choices?.[0]?.message?.content;
if (!answer) throw new Error("LM Studio returned no response content.");
return answer;
}
async function mapLimited<T, R>(items: T[], limit: number, worker: (item: T) => Promise<R>) {
const output = new Array<R>(items.length);
let cursor = 0;
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, async () => {
for (;;) { const index = cursor++; if (index >= items.length) return; output[index] = await worker(items[index]); }
}));
return output;
}
export async function toolsProvider(ctl: ToolsProviderController): Promise<Tool[]> {
const createCast = tool({
name: "create_cast",
description: "For Master Agents. Save sub-agent assignments according to your objectives.",
parameters: { characters: z.array(z.object({ name: z.string(), role: z.string(), personality: z.string(), expertise: z.string(), speakingStyle: z.string() })).min(1).max(12) },
implementation: async ({ characters: cast }) => {
for (const character of cast) characters.set(character.name, character);
return { saved: cast.map(c => ({ name: c.name, role: c.role })), next: "Please specify the saved names in \'participants\' for run_roundtable." };
},
});
const roundtable = tool({
name: "run_roundtable",
description: "Have multiple saved characters consider the same topic in parallel. Adjust the temperature, number of parallel participants, and length of each response using the sliders in the LM Studio plugin settings.",
parameters: { topic: z.string(), participants: z.array(z.string()).min(1).max(12), context: z.string().optional() },
implementation: async ({ topic, participants, context }) => {
const config = ctl.getPluginConfig(configSchematics);
const selected = participants.map(name => characters.get(name)).filter((c): c is Character => Boolean(c));
const missing = participants.filter(name => !characters.has(name));
if (!selected.length) return { error: "No valid participants found. Please call create_cast first." };
const settings = { model: config.get("modelIdentifier"), temperature: config.get("roundtableTemperature"), maxTokens: config.get("maxTokensPerCharacter") };
const messages = await mapLimited(selected, config.get("parallelAgents"), async character => {
try {
const content = await callLocalModel([{ role: "system", content: systemPrompt(character, context) }, { role: "user", content: `Topic: ${topic}\n ` }], settings);
return { character: character.name, role: character.role, content };
} catch (error) { return { character: character.name, role: character.role, error: error instanceof Error ? error.message : String(error) }; }
});
return { topic, settingsUsed: { temperature: settings.temperature, parallelAgents: config.get("parallelAgents"), maxTokensPerCharacter: settings.maxTokens }, missingParticipants: missing, messages };
},
});
const listCharacters = tool({
name: "list_characters",
description: "List saved characters and roles.",
parameters: {},
implementation: async () => ({
characters: [...characters.values()].map(({ name, role, expertise }) => ({ name, role, expertise })),
}),
});
const askCharacter = tool({
name: "ask_character",
description: "Assign a specific task to a saved character. Use the plugin setting sliders for temperature and output length.",
parameters: { name: z.string(), task: z.string(), context: z.string().optional() },
implementation: async ({ name, task, context }) => {
const character = characters.get(name);
if (!character) return { error: `Character \"${name}\" is not registered. Please call create_cast first.` };
const config = ctl.getPluginConfig(configSchematics);
try {
const content = await callLocalModel([{ role: "system", content: systemPrompt(character, context) }, { role: "user", content: task }], {
model: config.get("modelIdentifier"), temperature: config.get("roundtableTemperature"), maxTokens: config.get("maxTokensPerCharacter"),
});
return { character: name, role: character.role, content };
} catch (error) { return { character: name, error: error instanceof Error ? error.message : String(error) }; }
},
});
const removeCharacter = tool({
name: "remove_character",
description: "Delete a saved character.",
parameters: { name: z.string() },
implementation: async ({ name }) => ({ removed: characters.delete(name), name }),
});
return [createCast, listCharacters, askCharacter, roundtable, removeCharacter];
}