Proper typechecking for tools
This commit is contained in:
parent
d1325b33b7
commit
af59e03212
12
bun.lock
12
bun.lock
|
|
@ -14,11 +14,11 @@
|
|||
"ace-builds": "1.36.3",
|
||||
"clsx": "2.1.1",
|
||||
"delay": "6.0.0",
|
||||
"lucide-preact": "^0.577.0",
|
||||
"lucide-preact": "0.577.0",
|
||||
"preact": "10.22.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.3.10",
|
||||
"@types/bun": "latest",
|
||||
"@types/html-minifier": "4.0.5",
|
||||
"@types/inquirer": "9.0.7",
|
||||
"@types/web-bluetooth": "0.0.21",
|
||||
|
|
@ -49,7 +49,7 @@
|
|||
|
||||
"@inquirer/type": ["@inquirer/type@1.4.0", "", { "dependencies": { "mute-stream": "^1.0.0" } }, "sha512-AjOqykVyjdJQvtfkNDGUyMYGF8xN50VUxftCQWsOyIo4DFRLr6VQhW0VItGI1JIyQGCGgIpKa7hMMwNhZb4OIw=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="],
|
||||
"@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
|
||||
|
||||
"@types/clean-css": ["@types/clean-css@4.2.11", "", { "dependencies": { "@types/node": "*", "source-map": "^0.6.0" } }, "sha512-Y8n81lQVTAfP2TOdtJJEsCoYl1AnOkqDqMvXb9/7pfgZZ7r8YrEyurrAvAoAjHOGXKRybay+5CsExqIH6liccw=="],
|
||||
|
||||
|
|
@ -81,7 +81,7 @@
|
|||
|
||||
"browser-detect": ["browser-detect@0.2.28", "", { "dependencies": { "core-js": "^2.5.7" } }, "sha512-KeWGHqYQmHDkCFG2dIiX/2wFUgqevbw/rd6wNi9N6rZbaSJFtG5kel0HtprRwCGp8sqpQP79LzDJXf/WCx4WAw=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="],
|
||||
"bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
|
||||
|
||||
"camel-case": ["camel-case@3.0.0", "", { "dependencies": { "no-case": "^2.2.0", "upper-case": "^1.1.1" } }, "sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w=="],
|
||||
|
||||
|
|
@ -163,8 +163,6 @@
|
|||
|
||||
"@types/through/@types/node": ["@types/node@20.12.14", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg=="],
|
||||
|
||||
"bun-types/@types/node": ["@types/node@20.14.10", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ=="],
|
||||
|
||||
"html-minifier/uglify-js": ["uglify-js@3.18.0", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-SyVVbcNBCk0dzr9XL/R/ySrmYf0s372K6/hFklzgcp2lBFyXtw4I7BOdDjlLhE1aVqaI/SHWXWmYdlZxuyF38A=="],
|
||||
|
||||
"@inquirer/core/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
||||
|
|
@ -174,7 +172,5 @@
|
|||
"@types/mute-stream/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
||||
|
||||
"@types/through/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
||||
|
||||
"bun-types/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,258 @@
|
|||
const optional = Symbol();
|
||||
const GlobalObject = Object;
|
||||
const GlobalNumber = Number;
|
||||
const GlobalArray = Array;
|
||||
|
||||
export namespace Type {
|
||||
export function String(args: { description?: string, enum?: string[] } = {}) {
|
||||
const result: TString = {
|
||||
type: 'string',
|
||||
};
|
||||
if (args.enum) {
|
||||
result.enum = args.enum;
|
||||
}
|
||||
if (args.description) {
|
||||
result.description = args.description;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function Number(args: { description?: string, enum?: number[] } = {}) {
|
||||
const result: TNumber = {
|
||||
type: 'number',
|
||||
};
|
||||
if (args.enum) {
|
||||
result.enum = args.enum;
|
||||
}
|
||||
if (args.description) {
|
||||
result.description = args.description;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function Integer(args: { description?: string, enum?: number[] } = {}) {
|
||||
const result: TNumber = {
|
||||
type: 'integer',
|
||||
};
|
||||
if (args.enum) {
|
||||
result.enum = args.enum;
|
||||
}
|
||||
if (args.description) {
|
||||
result.description = args.description;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function Boolean(args: { description?: string } = {}) {
|
||||
const result: TBoolean = {
|
||||
type: 'boolean',
|
||||
};
|
||||
if (args.description) {
|
||||
result.description = args.description;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function Array<T extends TScheme = TScheme>(items: T, args: { description?: string } = {}) {
|
||||
const result: TArray<T> = {
|
||||
type: 'array',
|
||||
items,
|
||||
}
|
||||
if (args.description) {
|
||||
result.description = args.description;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function Optional<T extends TScheme = TScheme>(scheme: T): TOptional<T> {
|
||||
const result = { ...scheme };
|
||||
GlobalObject.defineProperty(result, optional, { value: true, enumerable: false, writable: false, configurable: false });
|
||||
return result as unknown as TOptional<T>;
|
||||
}
|
||||
|
||||
export function Object<T extends Record<string, TScheme> = Record<string, TScheme>>(properties: T, args: { description?: string } = {}) {
|
||||
const result: TObject<T> = {
|
||||
type: 'object',
|
||||
properties,
|
||||
required: GlobalObject.keys(properties)
|
||||
.filter(key => !(properties[key] as any)[optional]),
|
||||
}
|
||||
if (args.description) {
|
||||
result.description = args.description;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
type TEnumType<T extends (string | number) = (string | number)> =
|
||||
T extends string ? TString<T> :
|
||||
T extends number ? TNumber<T> : never;
|
||||
|
||||
export function Enum<T extends TString>(items: Static<T>[], args?: { description?: string }): T;
|
||||
export function Enum<T extends TNumber>(items: Static<T>[], args?: { description?: string }): T;
|
||||
export function Enum<T extends TEnumType = TEnumType>(items: Static<T>[], args: { description?: string } = {}) {
|
||||
if (typeof items?.[0] === 'number') {
|
||||
return Number({ enum: items.map(item => +item!), ...args });
|
||||
}
|
||||
return String({ enum: items.map(item => item!.toString()), ...args });
|
||||
}
|
||||
|
||||
export interface CheckError {
|
||||
path: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
function check(scheme: TScheme, value: unknown, path: string): CheckError[] {
|
||||
if (value == null) {
|
||||
if ((scheme as any)[optional]) return [];
|
||||
return [{ path, message: `Expected ${scheme.type} at ${path}, got ${value}` }];
|
||||
}
|
||||
|
||||
switch (scheme.type) {
|
||||
case 'string': {
|
||||
if (typeof value !== 'string') {
|
||||
return [{ path, message: `Expected string at ${path}, got ${typeof value}` }];
|
||||
}
|
||||
if (scheme.enum && !scheme.enum.includes(value)) {
|
||||
return [{ path, message: `Expected one of [${scheme.enum.join(', ')}] at ${path}, got "${value}"` }];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
case 'number':
|
||||
case 'integer': {
|
||||
if (typeof value !== 'number') {
|
||||
return [{ path, message: `Expected number at ${path}, got ${typeof value}` }];
|
||||
}
|
||||
if (scheme.type === 'integer' && !GlobalNumber.isInteger(value)) {
|
||||
return [{ path, message: `Expected integer at ${path}, got ${value}` }];
|
||||
}
|
||||
const s = scheme as TNumber;
|
||||
if (s.enum && !s.enum.includes(value)) {
|
||||
return [{ path, message: `Expected one of [${s.enum.join(', ')}] at ${path}, got ${value}` }];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
case 'boolean': {
|
||||
if (typeof value !== 'boolean') {
|
||||
return [{ path, message: `Expected boolean at ${path}, got ${typeof value}` }];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
case 'array': {
|
||||
if (!GlobalArray.isArray(value)) {
|
||||
return [{ path, message: `Expected array at ${path}, got ${typeof value}` }];
|
||||
}
|
||||
const s = scheme as TArray;
|
||||
return value.flatMap((item, i) => check(s.items, item, `${path}[${i}]`));
|
||||
}
|
||||
case 'object': {
|
||||
if (typeof value !== 'object' || GlobalArray.isArray(value)) {
|
||||
return [{ path, message: `Expected object at ${path}, got ${typeof value}` }];
|
||||
}
|
||||
const s = scheme as TObject;
|
||||
const errors: CheckError[] = [];
|
||||
for (const key of s.required || []) {
|
||||
if (!(key in (value as Record<string, unknown>))) {
|
||||
errors.push({ path: `${path}.${key}`, message: `Missing required property ${path}.${key}` });
|
||||
}
|
||||
}
|
||||
for (const [key, propScheme] of GlobalObject.entries(s.properties)) {
|
||||
const propValue = (value as Record<string, unknown>)[key];
|
||||
if (propValue === undefined) continue;
|
||||
errors.push(...check(propScheme, propValue, `${path}.${key}`));
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function Check(scheme: TScheme, value: unknown): CheckError[] {
|
||||
return check(scheme, value, '$');
|
||||
}
|
||||
}
|
||||
|
||||
export interface TString<T extends string = string> {
|
||||
type: 'string';
|
||||
enum?: T[];
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface TNumber<T extends number = number> {
|
||||
type: 'number' | 'integer';
|
||||
enum?: T[];
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface TBoolean {
|
||||
type: 'boolean';
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface TArray<T extends TScheme = TScheme> {
|
||||
type: 'array';
|
||||
description?: string;
|
||||
items: T;
|
||||
}
|
||||
|
||||
export interface TObject<T extends Record<string, TScheme> = Record<string, TScheme>> {
|
||||
type: 'object';
|
||||
description?: string;
|
||||
properties: T;
|
||||
required?: string[];
|
||||
}
|
||||
|
||||
export interface TOptionalString<T extends string = string> extends TString<T> {
|
||||
[optional]: true;
|
||||
}
|
||||
|
||||
export interface TOptionalNumber<T extends number = number> extends TNumber<T> {
|
||||
[optional]: true;
|
||||
}
|
||||
|
||||
export interface TOptionalBoolean extends TBoolean {
|
||||
[optional]: true;
|
||||
}
|
||||
|
||||
export interface TOptionalArray<T extends TScheme = TScheme> extends TArray<T> {
|
||||
[optional]: true;
|
||||
}
|
||||
|
||||
export interface TOptionalObject<T extends Record<string, TScheme> = Record<string, TScheme>> extends TObject<T> {
|
||||
[optional]: true;
|
||||
}
|
||||
|
||||
export type TOptional<T extends TScheme = TScheme> =
|
||||
T extends TString<infer S> ? TOptionalString<S> :
|
||||
T extends TNumber<infer N> ? TOptionalNumber<N> :
|
||||
T extends TBoolean ? TOptionalBoolean :
|
||||
T extends TArray<infer I> ? TOptionalArray<I> :
|
||||
T extends TObject<infer P> ? TOptionalObject<P> :
|
||||
never;
|
||||
|
||||
export type IsOptional<T> = T extends { [optional]: true } ? true : false;
|
||||
|
||||
export type TScheme = TString | TNumber | TBoolean | TArray | TObject | TOptionalString | TOptionalNumber | TOptionalBoolean | TOptionalArray | TOptionalObject;
|
||||
|
||||
type Prettify<T> = { [K in keyof T]: T[K] } & {};
|
||||
|
||||
type RequiredKeys<T extends Record<string, TScheme>> = {
|
||||
[K in keyof T]: IsOptional<T[K]> extends true ? never : K
|
||||
}[keyof T];
|
||||
|
||||
type OptionalKeys<T extends Record<string, TScheme>> = {
|
||||
[K in keyof T]: IsOptional<T[K]> extends true ? K : never
|
||||
}[keyof T];
|
||||
|
||||
type StaticObject<T extends Record<string, TScheme>> = Prettify<
|
||||
{ [K in RequiredKeys<T>]: Static<T[K]> } &
|
||||
{ [K in OptionalKeys<T>]?: Static<T[K]> }
|
||||
>;
|
||||
|
||||
export type Static<T extends TScheme> =
|
||||
T extends TString<infer S> ? S :
|
||||
T extends TNumber<infer N> ? N :
|
||||
T extends TBoolean ? boolean :
|
||||
T extends TArray<infer I> ? Static<I>[] :
|
||||
T extends TObject<infer P> ? StaticObject<P> :
|
||||
never;
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { formatError } from '@common/errors';
|
||||
import SSE from '@common/sse';
|
||||
import type { TObject } from '@common/typebox';
|
||||
|
||||
namespace LLM {
|
||||
export interface Connection {
|
||||
|
|
@ -41,45 +41,12 @@ namespace LLM {
|
|||
|
||||
export type ChatMessage = ChatMessageUser | ChatMessageAssistant | ChatMessageSystem | ChatMessageTool;
|
||||
|
||||
export interface ToolStringParameter {
|
||||
type: 'string';
|
||||
enum?: string[];
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface ToolNumberParameter {
|
||||
type: 'number' | 'integer';
|
||||
enum?: number[];
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface ToolBooleanParameter {
|
||||
type: 'boolean';
|
||||
enum?: boolean[];
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface ToolArrayParameter {
|
||||
type: 'array';
|
||||
description?: string;
|
||||
items: ToolParameter;
|
||||
}
|
||||
|
||||
export interface ToolObjectParameter {
|
||||
type: 'object';
|
||||
description?: string;
|
||||
properties: Record<string, ToolParameter>;
|
||||
required?: string[];
|
||||
}
|
||||
|
||||
export type ToolParameter = ToolStringParameter | ToolNumberParameter | ToolBooleanParameter | ToolArrayParameter | ToolObjectParameter;
|
||||
|
||||
export interface Tool {
|
||||
type: 'function';
|
||||
function: {
|
||||
name: string;
|
||||
description?: string;
|
||||
parameters: ToolObjectParameter;
|
||||
parameters: TObject;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@ namespace Prompt {
|
|||
tools: Tools.getTools(),
|
||||
banned_tokens: state.bannedTokens,
|
||||
enable_thinking: enableThinking,
|
||||
max_tokens: model.max_length ? model.max_length / 2 : 2048,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { formatError } from "@common/errors";
|
||||
import { Type, type Static, type TObject } from '@common/typebox';
|
||||
import { LocationScale, type AppState, type Character, type Location } from "../contexts/state";
|
||||
import type LLM from "./llm";
|
||||
|
||||
|
|
@ -10,29 +11,24 @@ const SCALE_DESCRIPTION = Object.entries(LocationScale)
|
|||
const VALID_SCALES = Object.values(LocationScale).filter(v => typeof v === 'number');
|
||||
|
||||
export namespace Tools {
|
||||
interface Tool {
|
||||
interface Tool<T extends TObject = TObject> {
|
||||
description: string;
|
||||
parameters: LLM.ToolObjectParameter;
|
||||
handler(args: string | Record<string, any>, appState: AppState): unknown;
|
||||
parameters: T;
|
||||
handler(args: Static<T>, appState: AppState): unknown;
|
||||
}
|
||||
|
||||
const tool = <T extends TObject = TObject>(t: Tool<T>): Tool<T> => t;
|
||||
|
||||
const TOOLS: Record<string, Tool> = {
|
||||
'append_to_story': {
|
||||
'append_to_story': tool({
|
||||
handler: async (args, appState) => {
|
||||
if (!args || typeof args !== 'object' || !('text' in args)) {
|
||||
return 'Error: Missing required argument "text"';
|
||||
}
|
||||
const { text } = args as { text: string };
|
||||
if (typeof text !== 'string') {
|
||||
return 'Error: Argument "text" must be a string';
|
||||
}
|
||||
if (!appState.currentStory) {
|
||||
return 'Error: No story selected';
|
||||
}
|
||||
appState.dispatch({
|
||||
type: 'EDIT_STORY',
|
||||
id: appState.currentStory.id,
|
||||
text: appState.currentStory.text + text
|
||||
text: appState.currentStory.text + args.text
|
||||
});
|
||||
appState.dispatch({
|
||||
type: 'SET_CURRENT_TAB',
|
||||
|
|
@ -42,33 +38,19 @@ export namespace Tools {
|
|||
return 'Text appended successfully';
|
||||
},
|
||||
description: 'Append text to the current story',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
text: {
|
||||
type: 'string',
|
||||
description: 'The text to append to the story',
|
||||
},
|
||||
},
|
||||
required: ['text'],
|
||||
},
|
||||
},
|
||||
'append_to_lore': {
|
||||
parameters: Type.Object({
|
||||
text: Type.String({ description: 'The text to append to the story' }),
|
||||
}),
|
||||
}),
|
||||
'append_to_lore': tool({
|
||||
handler: async (args, appState) => {
|
||||
if (!args || typeof args !== 'object' || !('text' in args)) {
|
||||
return 'Error: Missing required argument "text"';
|
||||
}
|
||||
const { text } = args as { text: string };
|
||||
if (typeof text !== 'string') {
|
||||
return 'Error: Argument "text" must be a string';
|
||||
}
|
||||
if (!appState.currentStory) {
|
||||
return 'Error: No story selected';
|
||||
}
|
||||
appState.dispatch({
|
||||
type: 'EDIT_LORE',
|
||||
id: appState.currentStory.id,
|
||||
lore: appState.currentStory.lore + text
|
||||
lore: appState.currentStory.lore + args.text
|
||||
});
|
||||
appState.dispatch({
|
||||
type: 'SET_CURRENT_TAB',
|
||||
|
|
@ -78,42 +60,19 @@ export namespace Tools {
|
|||
return 'Text appended to lore successfully';
|
||||
},
|
||||
description: 'Append text to the story lore',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
text: {
|
||||
type: 'string',
|
||||
description: 'The text to append to the lore',
|
||||
},
|
||||
},
|
||||
required: ['text'],
|
||||
},
|
||||
},
|
||||
'add_character': {
|
||||
parameters: Type.Object({
|
||||
text: Type.String({ description: 'The text to append to the lore' }),
|
||||
}),
|
||||
}),
|
||||
'add_character': tool({
|
||||
handler: async (args, appState) => {
|
||||
if (!args || typeof args !== 'object') {
|
||||
return 'Error: Missing required arguments';
|
||||
}
|
||||
const { name, shortDescription, nicknames, description, relations } = args as {
|
||||
name: string;
|
||||
shortDescription: string;
|
||||
nicknames?: string[];
|
||||
description?: string;
|
||||
relations?: { name: string; relation: string }[];
|
||||
};
|
||||
if (typeof name !== 'string' || !name.trim()) {
|
||||
return 'Error: Argument "name" must be a non-empty string';
|
||||
}
|
||||
if (typeof shortDescription !== 'string' || !shortDescription.trim()) {
|
||||
return 'Error: Argument "shortDescription" must be a non-empty string';
|
||||
}
|
||||
if (!appState.currentStory) {
|
||||
return 'Error: No story selected';
|
||||
}
|
||||
// Filter out relations that reference non-existent characters
|
||||
const existingCharacterNames = appState.currentStory.characters.map(c => c.name);
|
||||
const invalidRelations: string[] = [];
|
||||
const validRelations = (relations || []).filter(rel => {
|
||||
const validRelations = (args.relations || []).filter(rel => {
|
||||
if (!existingCharacterNames.includes(rel.name)) {
|
||||
invalidRelations.push(`${rel.name} (${rel.relation})`);
|
||||
return false;
|
||||
|
|
@ -125,10 +84,10 @@ export namespace Tools {
|
|||
storyId: appState.currentStory.id,
|
||||
character: {
|
||||
id: crypto.randomUUID(),
|
||||
name: name.trim(),
|
||||
nicknames: nicknames || [],
|
||||
shortDescription: shortDescription.trim(),
|
||||
description: description || '',
|
||||
name: args.name.trim(),
|
||||
nicknames: args.nicknames || [],
|
||||
shortDescription: args.shortDescription.trim(),
|
||||
description: args.description || '',
|
||||
relations: validRelations,
|
||||
},
|
||||
});
|
||||
|
|
@ -137,75 +96,43 @@ export namespace Tools {
|
|||
id: appState.currentStory.id,
|
||||
tab: 'characters'
|
||||
});
|
||||
let message = `Character "${name.trim()}" added successfully`;
|
||||
let message = `Character "${args.name.trim()}" added successfully`;
|
||||
if (invalidRelations.length > 0) {
|
||||
message += `. Removed invalid relations to non-existent characters: ${invalidRelations.join(', ')}`;
|
||||
}
|
||||
return message;
|
||||
},
|
||||
description: 'Add a new character to the story',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'The character\'s full name',
|
||||
},
|
||||
shortDescription: {
|
||||
type: 'string',
|
||||
description: 'A brief description of the character (one line)',
|
||||
},
|
||||
nicknames: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Optional list of nicknames',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Optional full character description',
|
||||
},
|
||||
relations: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', description: 'Related character name' },
|
||||
relation: { type: 'string', description: 'Relationship type' },
|
||||
},
|
||||
},
|
||||
description: 'Optional list of relationships with other characters',
|
||||
},
|
||||
},
|
||||
required: ['name', 'shortDescription'],
|
||||
},
|
||||
},
|
||||
'edit_character': {
|
||||
parameters: Type.Object({
|
||||
name: Type.String({ description: "The character's full name" }),
|
||||
shortDescription: Type.String({ description: 'A brief description of the character (one line)' }),
|
||||
nicknames: Type.Optional(Type.Array(Type.String(), { description: 'Optional list of nicknames' })),
|
||||
description: Type.Optional(Type.String({ description: 'Optional full character description' })),
|
||||
relations: Type.Optional(Type.Array(
|
||||
Type.Object({
|
||||
name: Type.String({ description: 'Related character name' }),
|
||||
relation: Type.String({ description: 'Relationship type' }),
|
||||
}),
|
||||
{ description: 'Optional list of relationships with other characters' }
|
||||
)),
|
||||
}),
|
||||
}),
|
||||
'edit_character': tool({
|
||||
handler: async (args, appState) => {
|
||||
if (!args || typeof args !== 'object') {
|
||||
return 'Error: Missing required arguments';
|
||||
}
|
||||
const { name, shortDescription, description } = args as {
|
||||
name: string;
|
||||
shortDescription?: string;
|
||||
description?: string;
|
||||
};
|
||||
if (typeof name !== 'string' || !name.trim()) {
|
||||
return 'Error: Argument "name" must be a non-empty string';
|
||||
}
|
||||
if (!appState.currentStory) {
|
||||
return 'Error: No story selected';
|
||||
}
|
||||
const character = appState.currentStory.characters.find(c => c.name === name.trim());
|
||||
const character = appState.currentStory.characters.find(c => c.name === args.name.trim());
|
||||
if (!character) {
|
||||
return `Error: Character "${name.trim()}" not found`;
|
||||
return `Error: Character "${args.name.trim()}" not found`;
|
||||
}
|
||||
// Only include defined values to avoid setting fields to undefined
|
||||
const definedUpdates: Partial<Character> = {};
|
||||
if (shortDescription !== undefined) {
|
||||
definedUpdates.shortDescription = shortDescription;
|
||||
if (args.shortDescription !== undefined) {
|
||||
definedUpdates.shortDescription = args.shortDescription;
|
||||
}
|
||||
if (description !== undefined) {
|
||||
definedUpdates.description = description;
|
||||
if (args.description !== undefined) {
|
||||
definedUpdates.description = args.description;
|
||||
}
|
||||
appState.dispatch({
|
||||
type: 'EDIT_CHARACTER',
|
||||
|
|
@ -218,51 +145,17 @@ export namespace Tools {
|
|||
id: appState.currentStory.id,
|
||||
tab: 'characters'
|
||||
});
|
||||
return `Character "${name.trim()}" updated successfully`;
|
||||
return `Character "${args.name.trim()}" updated successfully`;
|
||||
},
|
||||
description: 'Edit an existing character\'s description',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'The character\'s full name to identify which character to edit',
|
||||
},
|
||||
shortDescription: {
|
||||
type: 'string',
|
||||
description: 'Brief description of the character (one line)',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Full character description',
|
||||
},
|
||||
},
|
||||
required: ['name'],
|
||||
},
|
||||
},
|
||||
'add_location': {
|
||||
description: "Edit an existing character's description",
|
||||
parameters: Type.Object({
|
||||
name: Type.String({ description: "The character's full name to identify which character to edit" }),
|
||||
shortDescription: Type.Optional(Type.String({ description: 'Brief description of the character (one line)' })),
|
||||
description: Type.Optional(Type.String({ description: 'Full character description' })),
|
||||
}),
|
||||
}),
|
||||
'add_location': tool({
|
||||
handler: async (args, appState) => {
|
||||
if (!args || typeof args !== 'object') {
|
||||
return 'Error: Missing required arguments';
|
||||
}
|
||||
const { name, shortDescription, description, scale } = args as {
|
||||
name: string;
|
||||
shortDescription: string;
|
||||
description?: string;
|
||||
scale: number;
|
||||
};
|
||||
if (typeof name !== 'string' || !name.trim()) {
|
||||
return 'Error: Argument "name" must be a non-empty string';
|
||||
}
|
||||
if (typeof shortDescription !== 'string' || !shortDescription.trim()) {
|
||||
return 'Error: Argument "shortDescription" must be a non-empty string';
|
||||
}
|
||||
if (typeof scale !== 'number') {
|
||||
return 'Error: Argument "scale" must be a number';
|
||||
}
|
||||
if (!VALID_SCALES.includes(scale)) {
|
||||
return `Error: Argument "scale" must be a valid LocationScale value (${VALID_SCALES.join(', ')})`;
|
||||
}
|
||||
if (!appState.currentStory) {
|
||||
return 'Error: No story selected';
|
||||
}
|
||||
|
|
@ -271,10 +164,10 @@ export namespace Tools {
|
|||
storyId: appState.currentStory.id,
|
||||
location: {
|
||||
id: crypto.randomUUID(),
|
||||
name: name.trim(),
|
||||
shortDescription: shortDescription.trim(),
|
||||
description: description || '',
|
||||
scale: scale as LocationScale,
|
||||
name: args.name.trim(),
|
||||
shortDescription: args.shortDescription.trim(),
|
||||
description: args.description || '',
|
||||
scale: args.scale,
|
||||
},
|
||||
});
|
||||
appState.dispatch({
|
||||
|
|
@ -282,66 +175,36 @@ export namespace Tools {
|
|||
id: appState.currentStory.id,
|
||||
tab: 'locations'
|
||||
});
|
||||
return `Location "${name.trim()}" added successfully`;
|
||||
return `Location "${args.name.trim()}" added successfully`;
|
||||
},
|
||||
description: 'Add a new location to the story',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'The location\'s full name',
|
||||
},
|
||||
shortDescription: {
|
||||
type: 'string',
|
||||
description: 'A brief description of the location (one line)',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Optional full location description',
|
||||
},
|
||||
scale: {
|
||||
type: 'number',
|
||||
description: `Location scale (enum): ${SCALE_DESCRIPTION}`,
|
||||
enum: VALID_SCALES
|
||||
},
|
||||
},
|
||||
required: ['name', 'shortDescription', 'scale'],
|
||||
},
|
||||
},
|
||||
'edit_location': {
|
||||
parameters: Type.Object({
|
||||
name: Type.String({ description: "The location's full name" }),
|
||||
shortDescription: Type.String({ description: 'A brief description of the location (one line)' }),
|
||||
description: Type.Optional(Type.String({ description: 'Optional full location description' })),
|
||||
scale: Type.Enum(VALID_SCALES, {
|
||||
description: `Location scale (enum): ${SCALE_DESCRIPTION}`,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
'edit_location': tool({
|
||||
handler: async (args, appState) => {
|
||||
if (!args || typeof args !== 'object') {
|
||||
return 'Error: Missing required arguments';
|
||||
}
|
||||
const { name, shortDescription, description, scale } = args as {
|
||||
name: string;
|
||||
shortDescription?: string;
|
||||
description?: string;
|
||||
scale?: number;
|
||||
};
|
||||
if (typeof name !== 'string' || !name.trim()) {
|
||||
return 'Error: Argument "name" must be a non-empty string';
|
||||
}
|
||||
if (!appState.currentStory) {
|
||||
return 'Error: No story selected';
|
||||
}
|
||||
const location = appState.currentStory.locations.find(l => l.name === name.trim());
|
||||
const location = appState.currentStory.locations.find(l => l.name === args.name.trim());
|
||||
if (!location) {
|
||||
return `Error: Location "${name.trim()}" not found`;
|
||||
return `Error: Location "${args.name.trim()}" not found`;
|
||||
}
|
||||
const definedUpdates: Partial<Location> = {};
|
||||
if (shortDescription !== undefined) {
|
||||
definedUpdates.shortDescription = shortDescription;
|
||||
if (args.shortDescription !== undefined) {
|
||||
definedUpdates.shortDescription = args.shortDescription;
|
||||
}
|
||||
if (description !== undefined) {
|
||||
definedUpdates.description = description;
|
||||
if (args.description !== undefined) {
|
||||
definedUpdates.description = args.description;
|
||||
}
|
||||
if (scale !== undefined) {
|
||||
if (!VALID_SCALES.includes(scale)) {
|
||||
return `Error: Argument "scale" must be a valid LocationScale value (${VALID_SCALES.join(', ')})`;
|
||||
}
|
||||
definedUpdates.scale = scale as LocationScale;
|
||||
if (args.scale !== undefined) {
|
||||
definedUpdates.scale = args.scale;
|
||||
}
|
||||
appState.dispatch({
|
||||
type: 'EDIT_LOCATION',
|
||||
|
|
@ -354,32 +217,18 @@ export namespace Tools {
|
|||
id: appState.currentStory.id,
|
||||
tab: 'locations'
|
||||
});
|
||||
return `Location "${name.trim()}" updated successfully`;
|
||||
return `Location "${args.name.trim()}" updated successfully`;
|
||||
},
|
||||
description: 'Edit an existing location\'s description',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'The location\'s full name to identify which location to edit',
|
||||
},
|
||||
shortDescription: {
|
||||
type: 'string',
|
||||
description: 'Brief description of the location (one line)',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Full location description',
|
||||
},
|
||||
scale: {
|
||||
type: 'number',
|
||||
description: `Location scale (enum): ${SCALE_DESCRIPTION}`,
|
||||
},
|
||||
},
|
||||
required: ['name'],
|
||||
},
|
||||
}
|
||||
description: "Edit an existing location's description",
|
||||
parameters: Type.Object({
|
||||
name: Type.String({ description: "The location's full name to identify which location to edit" }),
|
||||
shortDescription: Type.Optional(Type.String({ description: 'Brief description of the location (one line)' })),
|
||||
description: Type.Optional(Type.String({ description: 'Full location description' })),
|
||||
scale: Type.Optional(Type.Enum(VALID_SCALES, {
|
||||
description: `Location scale (enum): ${SCALE_DESCRIPTION}`,
|
||||
})),
|
||||
}),
|
||||
})
|
||||
};
|
||||
|
||||
export function getTools(): LLM.Tool[] {
|
||||
|
|
@ -418,17 +267,18 @@ export namespace Tools {
|
|||
const { function: fn } = toolCall;
|
||||
const args = parseArg(fn.arguments);
|
||||
|
||||
if (!args || typeof args !== 'object') {
|
||||
return 'Error: Arguments must be an object';
|
||||
}
|
||||
|
||||
const handler = TOOLS[fn.name]?.handler;
|
||||
if (!handler) {
|
||||
const tool = TOOLS[fn.name];
|
||||
if (!tool) {
|
||||
return `Unknown tool: ${fn.name}`;
|
||||
}
|
||||
|
||||
const errors = Type.Check(tool.parameters, args);
|
||||
if (errors.length > 0) {
|
||||
return errors.map(e => e.message).join('\n');
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await handler(args, appState);
|
||||
const result = await tool.handler(args as any, appState);
|
||||
if (typeof result === 'string') {
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue