Files
tome/src/lib/dispatch.ts
Matte Noble 8505e19861 Refactor Models to normal, sane, class instance setup
Refactors the Model layer to remove the whole “everything must be
static” thing and moves to a normal OO setup.

I had previously misunderstood Svelte’s reactivity and believed you
could only work with plain JS objects (ie. not class instances). Turns
out that was wrong. Svelte is perfectly happy with classes as long as
you define them a specific way.

This big refactor switches to a more sane setup of model classes, with
properties for each column, that are instantiated throughout the app. No
more static functions (where they don’t make sense).

Defining models isn’t vastly different from before, except that you
define the properties in the class itself using Svelte’s `$state`.

```ts
class User extends Base<Row>('users') {
    firstName: string = $state('');
    lastName: string = $state('');
}
```

We no longer have a separate `Instance` interface describing the model –
the class itself functions as that now. We still have a `Row` interface
describing the raw database row, however.

Properties are defined on the class as `$state`. Svelte’s reactivity
works just fine when you instantiate these classes.

> [!NOTE]
> This means model files must have a `.svelte.ts` extension.

The one caveat with this setup is how you instantiate models.
Javascript’s constructor semantics combined with Svelte’s reactivity
makes it so we can’t dynamically assign properties values in a
constructor. I’m not entirely sure why, to be honest.

Instead, you MUST use the `new` static function.

```ts
const user = User.new({ firstName: 'Matte', lastName: 'Noble' });
```

`fromSql` is still static. However, it now returns an instance of the
model. `toSql` is no longer a static function, but otherwise is the
same.

```ts
static async fromSql(row: Row): Promise<User> {
    return User.new({
        firstName: row.first_name,
        lastName: row.last_name,
    });
}

async toSql(): Promise<ToSqlRow<Row>> {
    return {
        first_name: this.firstName,
        last_name: this.lastName,
    };
}
```
2025-06-05 12:29:05 -07:00

77 lines
2.1 KiB
TypeScript

import { invoke } from '@tauri-apps/api/core';
import uuid4 from 'uuid4';
import type { Options } from '$lib/engines/types';
import { error } from '$lib/logger';
import { App, Engine, type IModel, Message, Session } from '$lib/models';
export async function dispatch(session: Session, model: IModel, prompt?: string): Promise<Message> {
const app = App.find(session.appId as number);
const engine = Engine.find(model.engineId);
if (!engine || !engine.client) {
error(`MissingEngineError`, model.id);
throw `MissingEngineError: ${model.id}`;
}
if (!app) {
error(`MissingAppError`, session.appId);
throw `MissingAppError: ${session.appId}`;
}
if (prompt) {
await session.addMessage({
role: 'user',
content: prompt,
});
}
const options: Options = {
num_ctx: session.config.contextWindow,
temperature: session.config.temperature,
};
const message = await engine.client.chat(
model,
session.messages,
await session.tools(),
options
);
if (message.toolCalls?.length) {
for (const call of message.toolCalls) {
// Some engines, like Ollama, don't give tool calls a unique
// identifier. In those cases, do it ourselves, so that future
// calls to engines that do (like OpenAI), don't explode because
// they expect one to be set.
call.id ||= uuid4();
const content: string = await invoke('call_mcp_tool', {
sessionId: session.id,
name: call.function.name,
arguments: call.function.arguments,
});
await session.addMessage({
role: 'assistant',
content: '',
toolCalls: [call],
});
await session.addMessage({
role: 'tool',
content,
toolCallId: call.id,
});
return await dispatch(session, model);
}
}
message.model = model.id;
message.sessionId = session.id;
await message.save();
return message;
}