char-fork/llama.cpp

git clone https://git.t4t.associates/char-fork/llama.cpp

Aleksander Grygierui: Stores split refactor (#27240)521a64cd0

char/aliceai-t5-moe
3.8 KiB119 linesraw
1/**
2 * ReadMediaService - Reads local media files for the read_media tool
3 *
4 * Encodes image and audio files as base64 data URLs with the metadata the
5 * model needs. No reactive state; consumed by toolsStore.
6 */
7
8import { ToolsService } from './tools.service';
9import {
10	FILE_EXTENSION_SEPARATOR,
11	FILE_PATH_SEPARATOR_REGEX,
12	NEWLINE,
13	PREFIX_FILE,
14	PREFIX_MIME,
15	PREFIX_SIZE,
16	READ_MEDIA_AUDIO_MIME,
17	READ_MEDIA_IMAGE_MIME,
18	RESP_TYPE_BASE64
19} from '$lib/constants';
20import { BuiltInTool, ToolResponseField } from '$lib/enums';
21import type { ToolExecutionResult } from '$lib/types';
22
23/** Modalities of the model the tool call runs for. */
24export interface ReadMediaCapabilities {
25	audio: boolean;
26	vision: boolean;
27}
28
29/** Lowercase extension of a path, without the dot. Empty when the file name has none. */
30function fileExtension(path: string): string {
31	const name = path.split(FILE_PATH_SEPARATOR_REGEX).pop() ?? '';
32	const dot = name.lastIndexOf(FILE_EXTENSION_SEPARATOR);
33
34	return dot > 0 ? name.slice(dot + 1).toLowerCase() : '';
35}
36
37/**
38 * **ReadMediaService** - browser executor for the `read_media` tool
39 *
40 * The tool is synthetic: no such tool exists on the server. It reads the file
41 * through the server `read_file` tool with the `base64` response type, then
42 * turns the bytes into a data URI line. The agentic store lifts that line into
43 * an image or audio attachment on the tool result message, which is what makes
44 * the model perceive the file instead of reading a wall of base64.
45 *
46 * Living in the browser is what lets it exist only for models that can
47 * actually use the result - the server has no idea which model is selected.
48 *
49 * @see buildReadMediaToolDefinition in constants/read-media.ts - tool schema sent to the LLM
50 * @see agenticStore in stores/agentic/index.svelte.ts - tool dispatch and attachment extraction
51 */
52export class ReadMediaService {
53	static async executeTool(
54		params: Record<string, unknown>,
55		capabilities: ReadMediaCapabilities,
56		signal?: AbortSignal,
57		cwd?: string
58	): Promise<ToolExecutionResult> {
59		const path = typeof params.path === 'string' ? params.path : '';
60
61		if (!path) {
62			return { content: 'Error: missing "path" argument.', isError: true };
63		}
64
65		const extension = fileExtension(path);
66		const imageMime = READ_MEDIA_IMAGE_MIME[extension];
67		const audioMime = READ_MEDIA_AUDIO_MIME[extension];
68
69		let resolvedMime: string | undefined;
70
71		if (imageMime && capabilities.vision) resolvedMime = imageMime;
72		else if (audioMime && capabilities.audio) resolvedMime = audioMime;
73
74		if (!resolvedMime) {
75			const supported = [
76				...(capabilities.vision ? Object.keys(READ_MEDIA_IMAGE_MIME) : []),
77				...(capabilities.audio ? Object.keys(READ_MEDIA_AUDIO_MIME) : [])
78			];
79			// an unreadable-by-this-model file is a dead end, so say why instead of failing silently
80			const reason =
81				imageMime || audioMime
82					? `the current model cannot perceive ".${extension}" files`
83					: `".${extension}" is not a supported media type`;
84
85			return {
86				content: `Error: ${reason}. Supported: ${supported.join(', ')}.`,
87				isError: true
88			};
89		}
90
91		const raw = await ToolsService.executeToolRaw(
92			BuiltInTool.SERVER_READ_FILE,
93			{ path },
94			signal,
95			cwd,
96			RESP_TYPE_BASE64
97		);
98
99		if (ToolResponseField.ERROR in raw) {
100			return { content: String(raw[ToolResponseField.ERROR]), isError: true };
101		}
102
103		const base64 = typeof raw.base64 === 'string' ? raw.base64 : '';
104
105		if (!base64) {
106			return { content: `Error: no data returned for ${path}.`, isError: true };
107		}
108
109		const sizeBytes = typeof raw.size_bytes === 'number' ? raw.size_bytes : 0;
110		const content = [
111			`${PREFIX_FILE}${path}`,
112			`${PREFIX_SIZE}${sizeBytes} bytes`,
113			`${PREFIX_MIME}${resolvedMime}`,
114			`data:${resolvedMime};base64,${base64}`
115		].join(NEWLINE);
116
117		return { content, isError: false };
118	}
119}