Add MovieRecommenderTool and MovieSearchTool

This commit is contained in:
2026-08-12 21:14:10 +02:00
parent 5140bc5a50
commit 5edeb0ebb9
4 changed files with 137 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
import { z } from 'zod';
import { SchemaLlmTool } from '../../core/llm/SchemaLlmTool';
import type { RunContext } from '../../core/llm/RunContext';
import type { Movie, MovieSuggestion } from '../models';
export interface RecommendInput {
favorites: Movie[];
watched: Movie[];
library: Movie[];
previouslySuggested: Movie[];
count: number;
}
const movieSuggestionSchema = z.object({
title: z.string(),
year: z.number(),
reason: z.string(),
});
export class MovieRecommenderTool extends SchemaLlmTool<RecommendInput, MovieSuggestion[]> {
protected readonly promptName = 'movie-recommender';
protected readonly outputSchema = z.array(movieSuggestionSchema);
protected buildVariables(input: RecommendInput, _ctx: RunContext): Record<string, unknown> {
return {
count: input.count,
favorites: formatMovieList(input.favorites),
watched: formatMovieList(input.watched),
library: formatMovieList(input.library),
previouslySuggested: formatMovieList(input.previouslySuggested),
};
}
}
function formatMovieList(movies: Movie[]): string {
if (movies.length === 0) return '(none)';
return movies.map((m) => `- ${m.name} (${m.year})`).join('\n');
}

View File

@@ -0,0 +1,24 @@
import { z } from 'zod';
import { SchemaLlmTool } from '../../core/llm/SchemaLlmTool';
import type { RunContext } from '../../core/llm/RunContext';
import type { MovieSuggestion } from '../models';
export interface SearchInput {
query: string;
count: number;
}
const movieSuggestionSchema = z.object({
title: z.string(),
year: z.number(),
reason: z.string(),
});
export class MovieSearchTool extends SchemaLlmTool<SearchInput, MovieSuggestion[]> {
protected readonly promptName = 'movie-search';
protected readonly outputSchema = z.array(movieSuggestionSchema);
protected buildVariables(input: SearchInput, _ctx: RunContext): Record<string, unknown> {
return { query: input.query, count: input.count };
}
}

View File

@@ -0,0 +1,60 @@
import { describe, expect, it } from 'vitest';
import { MovieRecommenderTool } from '../../../src/app/tools/MovieRecommenderTool';
import type { RunContext } from '../../../src/core/llm/RunContext';
import type { Movie } from '../../../src/app/models';
function makeTool() {
return new MovieRecommenderTool(
{} as unknown as ConstructorParameters<typeof MovieRecommenderTool>[0],
{} as unknown as ConstructorParameters<typeof MovieRecommenderTool>[1]
) as unknown as {
buildVariables(input: unknown, ctx: RunContext): Record<string, unknown>;
parseOutput(raw: string, ctx: RunContext): unknown;
};
}
const movie = (name: string, year: number): Movie => ({
name,
year,
imdbId: '',
tmdbId: '',
genres: [],
communityRating: 0,
isFavorite: false,
playCount: 0,
});
describe('MovieRecommenderTool.buildVariables', () => {
it('formats favorites/watched/library/previouslySuggested as readable lists', () => {
const tool = makeTool();
const variables = tool.buildVariables(
{
favorites: [movie('Dune', 2021)],
watched: [],
library: [movie('Arrival', 2016)],
previouslySuggested: [],
count: 5,
},
{} as RunContext
);
expect(variables).toEqual({
count: 5,
favorites: '- Dune (2021)',
watched: '(none)',
library: '- Arrival (2016)',
previouslySuggested: '(none)',
});
});
});
describe('MovieRecommenderTool.parseOutput', () => {
it('validates suggestions against the movie suggestion schema', () => {
const tool = makeTool();
const result = tool.parseOutput(
'[{"title":"Dune","year":2021,"reason":"sci-fi epic"}]',
{} as RunContext
);
expect(result).toEqual([{ title: 'Dune', year: 2021, reason: 'sci-fi epic' }]);
});
});

View File

@@ -0,0 +1,15 @@
import { describe, expect, it } from 'vitest';
import { MovieSearchTool } from '../../../src/app/tools/MovieSearchTool';
import type { RunContext } from '../../../src/core/llm/RunContext';
describe('MovieSearchTool.buildVariables', () => {
it('passes the query and requested count through as prompt variables', () => {
const tool = new MovieSearchTool(
{} as unknown as ConstructorParameters<typeof MovieSearchTool>[0],
{} as unknown as ConstructorParameters<typeof MovieSearchTool>[1]
) as unknown as { buildVariables(input: unknown, ctx: RunContext): Record<string, unknown> };
const variables = tool.buildVariables({ query: 'sci-fi like Blade Runner', count: 5 }, {} as RunContext);
expect(variables).toEqual({ query: 'sci-fi like Blade Runner', count: 5 });
});
});