diff --git a/src/app/tools/MovieRecommenderTool.ts b/src/app/tools/MovieRecommenderTool.ts new file mode 100644 index 0000000..35be5c0 --- /dev/null +++ b/src/app/tools/MovieRecommenderTool.ts @@ -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 { + protected readonly promptName = 'movie-recommender'; + protected readonly outputSchema = z.array(movieSuggestionSchema); + + protected buildVariables(input: RecommendInput, _ctx: RunContext): Record { + 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'); +} diff --git a/src/app/tools/MovieSearchTool.ts b/src/app/tools/MovieSearchTool.ts new file mode 100644 index 0000000..fa91546 --- /dev/null +++ b/src/app/tools/MovieSearchTool.ts @@ -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 { + protected readonly promptName = 'movie-search'; + protected readonly outputSchema = z.array(movieSuggestionSchema); + + protected buildVariables(input: SearchInput, _ctx: RunContext): Record { + return { query: input.query, count: input.count }; + } +} diff --git a/test/app/tools/MovieRecommenderTool.test.ts b/test/app/tools/MovieRecommenderTool.test.ts new file mode 100644 index 0000000..96adcdb --- /dev/null +++ b/test/app/tools/MovieRecommenderTool.test.ts @@ -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[0], + {} as unknown as ConstructorParameters[1] + ) as unknown as { + buildVariables(input: unknown, ctx: RunContext): Record; + 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' }]); + }); +}); diff --git a/test/app/tools/MovieSearchTool.test.ts b/test/app/tools/MovieSearchTool.test.ts new file mode 100644 index 0000000..4d681c4 --- /dev/null +++ b/test/app/tools/MovieSearchTool.test.ts @@ -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[0], + {} as unknown as ConstructorParameters[1] + ) as unknown as { buildVariables(input: unknown, ctx: RunContext): Record }; + + 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 }); + }); +});