mirror of
https://github.com/sveltejs/ai-tools.git
synced 2026-07-05 03:54:41 +08:00
Compare commits
7 Commits
add-devtoo
...
task-promp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26d30c73c7 | ||
|
|
13c4832d1b | ||
|
|
7c7a1f939f | ||
|
|
5becaf3b5e | ||
|
|
7f858929ad | ||
|
|
6b5588c2f9 | ||
|
|
b4259dd117 |
@@ -1,47 +0,0 @@
|
||||
import type { Node } from 'estree';
|
||||
import type { AST } from 'svelte-eslint-parser';
|
||||
import type { Visitors } from 'zimmerframe';
|
||||
import type { ParseResult } from '../server/analyze/parse.js';
|
||||
|
||||
export type Autofixer = Visitors<
|
||||
Node | AST.SvelteNode,
|
||||
{
|
||||
output: { issues: string[]; suggestions: string[] };
|
||||
parsed: ParseResult;
|
||||
desired_svelte_version: number;
|
||||
}
|
||||
>;
|
||||
|
||||
export const assign_in_effect: Autofixer = {
|
||||
AssignmentExpression(node, { path, state }) {
|
||||
const in_effect = path.findLast(
|
||||
(node) =>
|
||||
node.type === 'CallExpression' &&
|
||||
node.callee.type === 'Identifier' &&
|
||||
node.callee.name === '$effect',
|
||||
);
|
||||
if (
|
||||
in_effect &&
|
||||
in_effect.type === 'CallExpression' &&
|
||||
(in_effect.callee.type === 'Identifier' || in_effect.callee.type === 'MemberExpression')
|
||||
) {
|
||||
if (state.parsed.is_rune(in_effect, ['$effect', '$effect.pre'])) {
|
||||
if (node.left.type === 'Identifier') {
|
||||
const reference = state.parsed.find_reference_by_id(node.left);
|
||||
const definition = reference?.resolved?.defs[0];
|
||||
if (definition && definition.type === 'Variable') {
|
||||
const init = definition.node.init;
|
||||
if (
|
||||
init?.type === 'CallExpression' &&
|
||||
state.parsed.is_rune(init, ['$state', '$state.raw'])
|
||||
) {
|
||||
state.output.suggestions.push(
|
||||
`The stateful variable "${node.left.name}" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
113
src/lib/mcp/autofixers/add-autofixers-issues.test.ts
Normal file
113
src/lib/mcp/autofixers/add-autofixers-issues.test.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { add_autofixers_issues } from './add-autofixers-issues';
|
||||
|
||||
describe('add_autofixers_issues', () => {
|
||||
describe('assign_in_effect', () => {
|
||||
it('should add suggestions when assigning to a stateful variable inside an effect', () => {
|
||||
const content = { issues: [], suggestions: [] };
|
||||
const code = `
|
||||
<script>
|
||||
const count = $state(0);
|
||||
$effect(() => {
|
||||
count = 43;
|
||||
});
|
||||
</script>`;
|
||||
add_autofixers_issues(content, code, 5);
|
||||
|
||||
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
|
||||
expect(content.suggestions).toContain(
|
||||
'The stateful variable "count" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should add a suggestion for each variable assigned within an effect', () => {
|
||||
const content = { issues: [], suggestions: [] };
|
||||
const code = `
|
||||
<script>
|
||||
const count = $state(0);
|
||||
const count2 = $state(0);
|
||||
$effect(() => {
|
||||
count = 43;
|
||||
count2 = 44;
|
||||
});
|
||||
</script>`;
|
||||
add_autofixers_issues(content, code, 5);
|
||||
|
||||
expect(content.suggestions.length).toBeGreaterThanOrEqual(2);
|
||||
expect(content.suggestions).toContain(
|
||||
'The stateful variable "count" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.',
|
||||
);
|
||||
expect(content.suggestions).toContain(
|
||||
'The stateful variable "count2" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.',
|
||||
);
|
||||
});
|
||||
it('should not add a suggestion for variables that are not assigned within an effect', () => {
|
||||
const content = { issues: [], suggestions: [] };
|
||||
const code = `
|
||||
<script>
|
||||
const count = $state(0);
|
||||
</script>
|
||||
|
||||
<button onclick={() => count = 43}>Increment</button>
|
||||
`;
|
||||
add_autofixers_issues(content, code, 5);
|
||||
|
||||
expect(content.suggestions).not.toContain(
|
||||
'The stateful variable "count" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.',
|
||||
);
|
||||
});
|
||||
|
||||
it("should not add a suggestions for variables that are assigned within an effect but aren't stateful", () => {
|
||||
const content = { issues: [], suggestions: [] };
|
||||
const code = `
|
||||
<script>
|
||||
const count = 0;
|
||||
|
||||
$effect(() => {
|
||||
count = 43;
|
||||
});
|
||||
</script>`;
|
||||
add_autofixers_issues(content, code, 5);
|
||||
|
||||
expect(content.suggestions).not.toContain(
|
||||
'The stateful variable "count" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should add a suggestion for variables that are assigned within an effect with an update', () => {
|
||||
const content = { issues: [], suggestions: [] };
|
||||
const code = `
|
||||
<script>
|
||||
let count = $state(0);
|
||||
|
||||
$effect(() => {
|
||||
count++;
|
||||
});
|
||||
</script>
|
||||
`;
|
||||
add_autofixers_issues(content, code, 5);
|
||||
|
||||
expect(content.suggestions).toContain(
|
||||
'The stateful variable "count" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should add a suggestion for variables that are mutated within an effect', () => {
|
||||
const content = { issues: [], suggestions: [] };
|
||||
const code = `
|
||||
<script>
|
||||
let count = $state({ value: 0 });
|
||||
|
||||
$effect(() => {
|
||||
count.value = 42;
|
||||
});
|
||||
</script>
|
||||
`;
|
||||
add_autofixers_issues(content, code, 5);
|
||||
|
||||
expect(content.suggestions).toContain(
|
||||
'The stateful variable "count" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
22
src/lib/mcp/autofixers/add-autofixers-issues.ts
Normal file
22
src/lib/mcp/autofixers/add-autofixers-issues.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { parse } from '../../parse/parse.js';
|
||||
import { walk } from '../../index.js';
|
||||
import type { Node } from 'estree';
|
||||
import * as autofixers from './visitors/index.js';
|
||||
|
||||
export function add_autofixers_issues(
|
||||
content: { issues: string[]; suggestions: string[] },
|
||||
code: string,
|
||||
desired_svelte_version: number,
|
||||
filename = 'Component.svelte',
|
||||
) {
|
||||
const parsed = parse(code, filename);
|
||||
|
||||
// Run each autofixer separately to avoid interrupting logic flow
|
||||
for (const autofixer of Object.values(autofixers)) {
|
||||
walk(
|
||||
parsed.ast as unknown as Node,
|
||||
{ output: content, parsed, desired_svelte_version },
|
||||
autofixer,
|
||||
);
|
||||
}
|
||||
}
|
||||
20
src/lib/mcp/autofixers/add-compile-issues.ts
Normal file
20
src/lib/mcp/autofixers/add-compile-issues.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { compile } from 'svelte/compiler';
|
||||
|
||||
export function add_compile_issues(
|
||||
content: { issues: string[]; suggestions: string[] },
|
||||
code: string,
|
||||
desired_svelte_version: number,
|
||||
filename = 'Component.svelte',
|
||||
) {
|
||||
const compilation_result = compile(code, {
|
||||
filename: filename || 'Component.svelte',
|
||||
generate: false,
|
||||
runes: desired_svelte_version >= 5,
|
||||
});
|
||||
|
||||
for (const warning of compilation_result.warnings) {
|
||||
content.issues.push(
|
||||
`${warning.message} at line ${warning.start?.line}, column ${warning.start?.column}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,7 @@ function base_config(svelte_config: Config): ESLint.Options['baseConfig'] {
|
||||
];
|
||||
}
|
||||
|
||||
export function get_linter(version: number) {
|
||||
function get_linter(version: number) {
|
||||
if (version < 5) {
|
||||
return (svelte_4_linter ??= new ESLint({
|
||||
overrideConfigFile: true,
|
||||
@@ -68,3 +68,23 @@ export function get_linter(version: number) {
|
||||
}),
|
||||
}));
|
||||
}
|
||||
|
||||
export async function add_eslint_issues(
|
||||
content: { issues: string[]; suggestions: string[] },
|
||||
code: string,
|
||||
desired_svelte_version: number,
|
||||
filename = 'Component.svelte',
|
||||
) {
|
||||
const eslint = get_linter(desired_svelte_version);
|
||||
const results = await eslint.lintText(code, { filePath: filename || './Component.svelte' });
|
||||
|
||||
for (const message of results[0].messages) {
|
||||
if (message.severity === 2) {
|
||||
content.issues.push(`${message.message} at line ${message.line}, column ${message.column}`);
|
||||
} else if (message.severity === 1) {
|
||||
content.suggestions.push(
|
||||
`${message.message} at line ${message.line}, column ${message.column}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
16
src/lib/mcp/autofixers/ast/utils.ts
Normal file
16
src/lib/mcp/autofixers/ast/utils.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { Identifier, MemberExpression } from 'estree';
|
||||
|
||||
/**
|
||||
* Gets the left-most identifier of a member expression or identifier.
|
||||
*/
|
||||
export function left_most_id(expression: MemberExpression | Identifier) {
|
||||
while (expression.type === 'MemberExpression') {
|
||||
expression = expression.object as MemberExpression | Identifier;
|
||||
}
|
||||
|
||||
if (expression.type !== 'Identifier') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return expression;
|
||||
}
|
||||
62
src/lib/mcp/autofixers/visitors/assign-in-effect.ts
Normal file
62
src/lib/mcp/autofixers/visitors/assign-in-effect.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import type { AssignmentExpression, Identifier, Node, UpdateExpression } from 'estree';
|
||||
import type { Autofixer, AutofixerState } from '.';
|
||||
import { left_most_id } from '../ast/utils';
|
||||
import type { SvelteNode } from 'svelte-eslint-parser/lib/ast';
|
||||
import type { Context } from 'zimmerframe';
|
||||
|
||||
function run_if_in_effect(path: (Node | SvelteNode)[], state: AutofixerState, to_run: () => void) {
|
||||
const in_effect = path.findLast(
|
||||
(node) =>
|
||||
node.type === 'CallExpression' &&
|
||||
node.callee.type === 'Identifier' &&
|
||||
node.callee.name === '$effect',
|
||||
);
|
||||
|
||||
if (
|
||||
in_effect &&
|
||||
in_effect.type === 'CallExpression' &&
|
||||
(in_effect.callee.type === 'Identifier' || in_effect.callee.type === 'MemberExpression')
|
||||
) {
|
||||
if (state.parsed.is_rune(in_effect, ['$effect', '$effect.pre'])) {
|
||||
to_run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function visitor(
|
||||
node: UpdateExpression | AssignmentExpression,
|
||||
{ state, path }: Context<Node | SvelteNode, AutofixerState>,
|
||||
) {
|
||||
run_if_in_effect(path, state, () => {
|
||||
function check_if_stateful_id(id: Identifier) {
|
||||
const reference = state.parsed.find_reference_by_id(id);
|
||||
const definition = reference?.resolved?.defs[0];
|
||||
if (definition && definition.type === 'Variable') {
|
||||
const init = definition.node.init;
|
||||
if (
|
||||
init?.type === 'CallExpression' &&
|
||||
state.parsed.is_rune(init, ['$state', '$state.raw'])
|
||||
) {
|
||||
state.output.suggestions.push(
|
||||
`The stateful variable "${id.name}" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
const variable = node.type === 'UpdateExpression' ? node.argument : node.left;
|
||||
|
||||
if (variable.type === 'Identifier') {
|
||||
check_if_stateful_id(variable);
|
||||
} else if (variable.type === 'MemberExpression') {
|
||||
const object = left_most_id(variable);
|
||||
if (object) {
|
||||
check_if_stateful_id(object);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const assign_in_effect: Autofixer = {
|
||||
UpdateExpression: visitor,
|
||||
AssignmentExpression: visitor,
|
||||
};
|
||||
14
src/lib/mcp/autofixers/visitors/index.ts
Normal file
14
src/lib/mcp/autofixers/visitors/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { Node } from 'estree';
|
||||
import type { AST } from 'svelte-eslint-parser';
|
||||
import type { Visitors } from 'zimmerframe';
|
||||
import type { ParseResult } from '../../../parse/parse.js';
|
||||
|
||||
export type AutofixerState = {
|
||||
output: { issues: string[]; suggestions: string[] };
|
||||
parsed: ParseResult;
|
||||
desired_svelte_version: number;
|
||||
};
|
||||
|
||||
export type Autofixer = Visitors<Node | AST.SvelteNode, AutofixerState>;
|
||||
|
||||
export * from './assign-in-effect.js';
|
||||
@@ -1,14 +1,11 @@
|
||||
import { walk } from '../index.js';
|
||||
import { ValibotJsonSchemaAdapter } from '@tmcp/adapter-valibot';
|
||||
import { HttpTransport } from '@tmcp/transport-http';
|
||||
import { StdioTransport } from '@tmcp/transport-stdio';
|
||||
import type { Node } from 'estree';
|
||||
import { McpServer } from 'tmcp';
|
||||
import * as v from 'valibot';
|
||||
import { parse } from '../server/analyze/parse.js';
|
||||
import * as autofixers from './autofixers.js';
|
||||
import { get_linter } from './eslint.js';
|
||||
import { compile } from 'svelte/compiler';
|
||||
import { add_autofixers_issues } from './autofixers/add-autofixers-issues.js';
|
||||
import { add_compile_issues } from './autofixers/add-compile-issues.js';
|
||||
import { add_eslint_issues } from './autofixers/add-eslint-issues.js';
|
||||
|
||||
const server = new McpServer(
|
||||
{
|
||||
@@ -64,45 +61,11 @@ server.tool(
|
||||
require_another_tool_call_after_fixing: boolean;
|
||||
} = { issues: [], suggestions: [], require_another_tool_call_after_fixing: false };
|
||||
try {
|
||||
// compile without generating to get warnings and errors
|
||||
add_compile_issues(content, code, desired_svelte_version, filename);
|
||||
|
||||
const compilation_result = compile(code, {
|
||||
filename: filename || 'Component.svelte',
|
||||
generate: false,
|
||||
runes: desired_svelte_version >= 5,
|
||||
});
|
||||
add_autofixers_issues(content, code, desired_svelte_version, filename);
|
||||
|
||||
for (const warning of compilation_result.warnings) {
|
||||
content.issues.push(
|
||||
`${warning.message} at line ${warning.start?.line}, column ${warning.start?.column}`,
|
||||
);
|
||||
}
|
||||
|
||||
const parsed = parse(code, filename ?? 'Component.svelte');
|
||||
|
||||
// Run each autofixer separately to avoid interrupting logic flow
|
||||
for (const autofixer of Object.values(autofixers)) {
|
||||
walk(
|
||||
parsed.ast as unknown as Node,
|
||||
{ output: content, parsed, desired_svelte_version },
|
||||
autofixer,
|
||||
);
|
||||
}
|
||||
|
||||
const eslint = get_linter(desired_svelte_version);
|
||||
const results = await eslint.lintText(code, { filePath: filename || './Component.svelte' });
|
||||
|
||||
for (const message of results[0].messages) {
|
||||
if (message.severity === 2) {
|
||||
content.issues.push(
|
||||
`${message.message} at line ${message.line}, column ${message.column}`,
|
||||
);
|
||||
} else if (message.severity === 1) {
|
||||
content.suggestions.push(
|
||||
`${message.message} at line ${message.line}, column ${message.column}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
await add_eslint_issues(content, code, desired_svelte_version, filename);
|
||||
} catch (e: unknown) {
|
||||
const error = e as Error & { start?: { line: number; column: number } };
|
||||
content.issues.push(
|
||||
@@ -126,6 +89,46 @@ server.tool(
|
||||
},
|
||||
);
|
||||
|
||||
server.prompt(
|
||||
{
|
||||
name: 'svelte-task-prompt',
|
||||
title: 'Svelte Task Prompt',
|
||||
description:
|
||||
'Use this Prompt to ask for any svelte related task. It will automatically instruct the LLM on how to best use the autofixer and how to query for documentation pages.',
|
||||
schema: v.object({
|
||||
task: v.pipe(v.string(), v.description('The task to be performed')),
|
||||
}),
|
||||
},
|
||||
async ({ task }) => {
|
||||
// TODO: implement logic to fetch the available docs paths to return in the prompt
|
||||
const available_docs: string[] = [];
|
||||
|
||||
return {
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: {
|
||||
type: 'text',
|
||||
text: `You are a Svelte expert tasked to build components and utilities for Svelte developers. If you need documentation for anything related to Svelte you can invoke the tool \`get_documentation\` with one of the following paths:
|
||||
<available-docs-paths>
|
||||
${JSON.stringify(available_docs, null, 2)}
|
||||
</available-docs-paths>
|
||||
|
||||
Every time you write a Svelte component or a Svelte module you MUST invoke the \`svelte-autofixer\` tool providing the code. The tool will return a list of issues or suggestions. If there are any issues or suggestions you MUST fix them and call the tool again with the updated code. You MUST keep doing this until the tool returns no issues or suggestions. Only then you can return the code to the user.
|
||||
|
||||
This is the task you will work on:
|
||||
|
||||
<task>
|
||||
${task}
|
||||
</task>
|
||||
`,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export const http_transport = new HttpTransport(server, {
|
||||
cors: true,
|
||||
});
|
||||
|
||||
@@ -12,9 +12,80 @@ import { float_32_array } from './utils';
|
||||
* to the generated migration file
|
||||
*/
|
||||
|
||||
// this is just an example of a vector table...we can change this with the docs table later
|
||||
export const vector_table = sqliteTable('vector_table', {
|
||||
export const distillations = sqliteTable('distillations', {
|
||||
id: integer('id').primaryKey(),
|
||||
text: text('text'),
|
||||
vector: float_32_array('vector', { dimensions: 3 }),
|
||||
preset_name: text('preset_name').notNull(),
|
||||
version: text('version').notNull(),
|
||||
content: text('content').notNull(),
|
||||
size_kb: integer('size_kb').notNull(),
|
||||
document_count: integer('document_count').notNull(),
|
||||
distillation_job_id: integer('distillation_job_id').references(() => distillation_jobs.id),
|
||||
created_at: integer('created_at', { mode: 'timestamp' })
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date()),
|
||||
});
|
||||
|
||||
export const distillation_jobs = sqliteTable('distillation_jobs', {
|
||||
id: integer('id').primaryKey(),
|
||||
preset_name: text('preset_name').notNull(),
|
||||
batch_id: text('batch_id'),
|
||||
status: text('status', { enum: ['pending', 'processing', 'completed', 'failed'] }).notNull(),
|
||||
model_used: text('model_used').notNull(),
|
||||
total_files: integer('total_files').notNull(),
|
||||
processed_files: integer('processed_files').notNull().default(0),
|
||||
successful_files: integer('successful_files').notNull().default(0),
|
||||
minimize_applied: integer('minimize_applied', { mode: 'boolean' }).notNull().default(false),
|
||||
total_input_tokens: integer('total_input_tokens').notNull().default(0),
|
||||
total_output_tokens: integer('total_output_tokens').notNull().default(0),
|
||||
started_at: integer('started_at', { mode: 'timestamp' }),
|
||||
completed_at: integer('completed_at', { mode: 'timestamp' }),
|
||||
error_message: text('error_message'),
|
||||
metadata: text('metadata', { mode: 'json' })
|
||||
.$type<Record<string, unknown>>()
|
||||
.notNull()
|
||||
.default({}),
|
||||
created_at: integer('created_at', { mode: 'timestamp' })
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date()),
|
||||
updated_at: integer('updated_at', { mode: 'timestamp' })
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date()),
|
||||
});
|
||||
|
||||
export const content = sqliteTable('content', {
|
||||
id: integer('id').primaryKey(),
|
||||
path: text('path').notNull(),
|
||||
filename: text('filename').notNull(),
|
||||
content: text('content').notNull(),
|
||||
size_bytes: integer('size_bytes').notNull(),
|
||||
embeddings: float_32_array('embeddings', { dimensions: 1024 }),
|
||||
metadata: text('metadata', { mode: 'json' })
|
||||
.$type<Record<string, unknown>>()
|
||||
.notNull()
|
||||
.default({}),
|
||||
created_at: integer('created_at', { mode: 'timestamp' })
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date()),
|
||||
updated_at: integer('updated_at', { mode: 'timestamp' })
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date()),
|
||||
});
|
||||
|
||||
export const content_distilled = sqliteTable('content_distilled', {
|
||||
id: integer('id').primaryKey(),
|
||||
path: text('path').notNull(),
|
||||
filename: text('filename').notNull(),
|
||||
content: text('content').notNull(),
|
||||
size_bytes: integer('size_bytes').notNull(),
|
||||
embeddings: float_32_array('embeddings', { dimensions: 1024 }),
|
||||
metadata: text('metadata', { mode: 'json' })
|
||||
.$type<Record<string, unknown>>()
|
||||
.notNull()
|
||||
.default({}),
|
||||
created_at: integer('created_at', { mode: 'timestamp' })
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date()),
|
||||
updated_at: integer('updated_at', { mode: 'timestamp' })
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date()),
|
||||
});
|
||||
|
||||
@@ -26,7 +26,9 @@ export function vector(arr: number[]) {
|
||||
* .execute();
|
||||
*/
|
||||
export function distance(column: Column, arr: number[], as = 'distance') {
|
||||
return sql<number>`vector_distance_cos(${column}, vector32(${JSON.stringify(arr)}))`.as(as);
|
||||
return sql<number>`CASE ${column} ISNULL WHEN 1 THEN 1 ELSE vector_distance_cos(${column}, vector32(${JSON.stringify(arr)})) END`.as(
|
||||
as,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import { VOYAGE_API_KEY } from '$env/static/private';
|
||||
import { db } from '$lib/server/db/index.js';
|
||||
import { vector_table } from '$lib/server/db/schema.js';
|
||||
import { distance, vector } from '$lib/server/db/utils.js';
|
||||
import { sql } from 'drizzle-orm';
|
||||
|
||||
async function get_embeddings(text: string) {
|
||||
const result = await fetch('https://api.voyageai.com/v1/embeddings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${VOYAGE_API_KEY}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
input: [text],
|
||||
model: 'voyage-3.5',
|
||||
}),
|
||||
}).then((res) => res.json());
|
||||
|
||||
return result.data[0].embedding as number[];
|
||||
}
|
||||
|
||||
export async function load({ url: { searchParams } }) {
|
||||
const sentence = searchParams.get('sentence');
|
||||
if (!sentence) return { top: [], sentence: '' };
|
||||
const top = await db
|
||||
.select({
|
||||
id: vector_table.id,
|
||||
text: vector_table.text,
|
||||
distance: distance(vector_table.vector, await get_embeddings(sentence)),
|
||||
})
|
||||
.from(vector_table)
|
||||
.orderBy(sql`distance`)
|
||||
.execute();
|
||||
return { top, sentence };
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
async default({ request }) {
|
||||
const data = await request.formData();
|
||||
const text = data.get('text')?.toString();
|
||||
const embeddings = await get_embeddings(text ?? '');
|
||||
if (text && embeddings) {
|
||||
await db
|
||||
.insert(vector_table)
|
||||
.values({ text, vector: vector(embeddings) })
|
||||
.execute();
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -1,21 +1 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
|
||||
let { data } = $props();
|
||||
</script>
|
||||
|
||||
<h1>Official Svelte MCP</h1>
|
||||
|
||||
<form method="POST" use:enhance>
|
||||
<textarea name="text" rows="4" cols="50" placeholder="Enter text to store in the vector database"
|
||||
></textarea>
|
||||
<br />
|
||||
<button>Submit</button>
|
||||
</form>
|
||||
|
||||
Comparing with
|
||||
<pre>{data.sentence}</pre>
|
||||
|
||||
{#each data.top as item (item.id)}
|
||||
<p>{item.text} - {item.distance}</p>
|
||||
{/each}
|
||||
|
||||
Reference in New Issue
Block a user