movie-agent
v1.1.4
Published
Movie Agent - A TypeScript-based movie recommendation agent
Readme
Movie Agent
An intelligent movie recommendation system that helps users discover movies based on their preferences and shows where they're available to stream in Canada.
Features
- 🎬 Personalized movie recommendations based on mood, genre, and preferences
- 🤖 AI-powered output formatting with LangChain and Google Gemini
- 🌊 Streaming and non-streaming output modes
- 📺 Real-time streaming availability for Canadian platforms
- ⚡ Fast responses using TMDb API
- 🎯 Smart filtering by runtime, release year, and streaming platforms
- 📦 Easy integration into web applications and APIs
- 🔒 Multi-tenant cache isolation for secure deployments (see Cache Isolation Guide)
Prerequisites
- Node.js (v18 or higher)
- TMDb API key (Get one here)
- LLM API key for AI formatting (optional):
- Google Gemini API key - Get one here
- OR Azure OpenAI credentials - Learn more
Installation
npm install movie-agentQuick Start
Basic Usage - Get Structured Data
import { MovieAgent } from 'movie-agent';
// Create agent
const agent = new MovieAgent();
// Get structured recommendations
const response = await agent.getRecommendations({
mood: 'excited',
platforms: ['Netflix', 'Prime Video'],
});
console.log(response.recommendations);AI-Formatted Output - Invoke Mode (Waits for complete response)
import { MovieAgent } from 'movie-agent';
const agent = new MovieAgent();
// Get AI-formatted markdown output
const output = await agent.invoke({
mood: 'happy',
platforms: ['Netflix'],
});
console.log(output); // Formatted markdown stringAI-Formatted Output - Stream Mode (Real-time streaming)
import { MovieAgent } from 'movie-agent';
const agent = new MovieAgent();
// Stream AI-formatted output in real-time
await agent.stream({
mood: 'excited',
platforms: ['Netflix'],
}, (chunk) => {
process.stdout.write(chunk); // or update your UI
});Using with MovieAgentFactory
import { MovieAgentFactory } from 'movie-agent';
// Create agent with explicit configuration (Gemini)
const agent = MovieAgentFactory.create({
tmdbAccessToken: process.env.TMDB_ACCESS_TOKEN!,
tmdbRegion: 'CA',
llmProvider: 'gemini',
geminiApiKey: process.env.GEMINI_API_KEY,
debug: true,
});
// Or use Azure OpenAI
const agentWithAzure = MovieAgentFactory.create({
tmdbAccessToken: process.env.TMDB_ACCESS_TOKEN!,
tmdbRegion: 'CA',
llmProvider: 'azure',
azureOpenAiApiKey: process.env.AZURE_OPENAI_API_KEY,
azureOpenAiEndpoint: process.env.AZURE_OPENAI_ENDPOINT,
azureOpenAiDeployment: process.env.AZURE_OPENAI_DEPLOYMENT,
debug: true,
});
// All three methods available: getRecommendations, invoke, stream
const structured = await agent.getRecommendations({ mood: 'happy' });
const formatted = await agent.invoke({ mood: 'happy' });
await agent.stream({ mood: 'happy' }, (chunk) => console.log(chunk));API Reference
MovieAgent Methods
getRecommendations(input: UserInput): Promise<AgentResponse | ErrorResponse>
Returns structured movie recommendations with metadata.
const response = await agent.getRecommendations({
mood: 'happy',
platforms: ['Netflix']
});
// Returns: { recommendations: [...], metadata: {...} }invoke(input: UserInput): Promise<string | ErrorResponse>
Returns AI-formatted markdown output (non-streaming). Waits for complete response before returning.
const output = await agent.invoke({
mood: 'excited',
platforms: ['Netflix']
});
// Returns: Formatted markdown stringstream(input: UserInput, onChunk: (chunk: string) => void): Promise<void | ErrorResponse>
Streams AI-formatted output in real-time. Best for interactive UIs.
await agent.stream({
mood: 'relaxed',
platforms: ['Netflix']
}, (chunk) => {
process.stdout.write(chunk);
});Input Parameters
interface UserInput {
mood?: string; // e.g., 'excited', 'relaxed', 'thoughtful', 'happy', 'scared'
genre?: string | string[]; // e.g., 'Action' or ['Action', 'Thriller']
platforms?: string[]; // e.g., ['Netflix', 'Prime Video', 'Disney+']
runtime?: {
min?: number; // Minimum runtime in minutes
max?: number; // Maximum runtime in minutes
};
releaseYear?: number | { // Single year or range
from?: number;
to?: number;
};
}Output Comparison
getRecommendations() - Structured Data
{
"recommendations": [
{
"tmdbId": 123,
"title": "Movie Title",
"releaseYear": "2024",
"runtime": 120,
"genres": ["Action", "Adventure"],
"description": "...",
"streamingPlatforms": [...],
"matchReason": "...",
"posterUrl": "https://image.tmdb.org/t/p/w500/abc123.jpg"
}
],
"metadata": {...}
}invoke() / stream() - AI-Formatted Markdown
Hey there! 👋 Looking for some action-packed thrills? Here are your recommendations:
### **Movie Title (2024)** - 120 minutes
*Genres: Action, Adventure*
A compelling description...
📺 Available on: Netflix
✨ Why: Perfect for your excited mood with thrilling action!API Examples
// Simple mood-based search
await agent.getRecommendations({ mood: 'happy' });
// AI-formatted output
await agent.invoke({ mood: 'happy' });
// Streaming output
await agent.stream({ mood: 'happy' }, (chunk) => console.log(chunk));
// Genre-specific with platform filter
await agent.getRecommendations({
genre: 'Action',
platforms: ['Disney+']
});
// Complex filtering
await agent.getRecommendations({
mood: 'adventurous',
platforms: ['Netflix', 'Prime Video'],
runtime: { min: 90, max: 150 },
releaseYear: { from: 2020, to: 2024 }
});
// Multiple genres
await agent.getRecommendations({
genre: ['Comedy', 'Romance'],
platforms: ['Netflix'],
runtime: { max: 120 }
});Integration Examples
Next.js API Route (Structured Data)
// pages/api/recommendations.ts
import { MovieAgent } from 'movie-agent';
import type { NextApiRequest, NextApiResponse } from 'next';
const agent = new MovieAgent();
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
const { mood, genre, platforms } = req.body;
const recommendations = await agent.getRecommendations({ mood, genre, platforms });
res.status(200).json(recommendations);
} catch (error) {
console.error('Error getting recommendations:', error);
res.status(500).json({ error: 'Failed to get recommendations' });
}
}Next.js API Route (AI-Formatted Streaming)
// pages/api/recommendations-stream.ts
import { MovieAgent } from 'movie-agent';
import type { NextApiRequest, NextApiResponse } from 'next';
const agent = new MovieAgent();
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
const { mood, genre, platforms } = req.body;
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.setHeader('Transfer-Encoding', 'chunked');
await agent.stream({ mood, genre, platforms }, (chunk) => {
res.write(chunk);
});
res.end();
} catch (error) {
console.error('Error streaming recommendations:', error);
res.status(500).json({ error: 'Failed to stream recommendations' });
}
}Express Server (Streaming)
import express from 'express';
import { MovieAgent } from 'movie-agent';
const app = express();
app.use(express.json());
const agent = new MovieAgent();
// Structured data endpoint
app.post('/api/recommendations', async (req, res) => {
try {
const recommendations = await agent.getRecommendations(req.body);
res.json(recommendations);
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Failed to get recommendations' });
}
});
// Streaming endpoint
app.post('/api/recommendations/stream', async (req, res) => {
try {
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.setHeader('Transfer-Encoding', 'chunked');
await agent.stream(req.body, (chunk) => {
res.write(chunk);
});
res.end();
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Failed to stream recommendations' });
}
});
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});React Component with Streaming
import { useState } from 'react';
function MovieRecommendations() {
const [output, setOutput] = useState('');
const [loading, setLoading] = useState(false);
const getRecommendations = async () => {
setLoading(true);
setOutput('');
const response = await fetch('/api/recommendations/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mood: 'happy', platforms: ['Netflix'] }),
});
const reader = response.body?.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
setOutput(prev => prev + chunk);
}
setLoading(false);
};
return (
<div>
<button onClick={getRecommendations} disabled={loading}>
Get Recommendations
</button>
<div style={{ whiteSpace: 'pre-wrap' }}>{output}</div>
</div>
);
}Configuration
Environment Variables
# Required
TMDB_ACCESS_TOKEN=your_tmdb_api_key_here
# LLM Provider Selection (optional)
LLM_PROVIDER=gemini # Options: gemini, azure
# Option 1: Google Gemini (for AI-formatted output)
GEMINI_API_KEY=your_gemini_api_key_here
# Option 2: Azure OpenAI (for AI-formatted output)
AZURE_OPENAI_API_KEY=your_azure_openai_api_key_here
AZURE_OPENAI_ENDPOINT=https://your-resource-name.openai.azure.com/
AZURE_OPENAI_DEPLOYMENT=your_deployment_name
# Optional
TMDB_REGION=CA
CACHE_TTL=86400
MAX_RECOMMENDATIONS=5
MIN_RECOMMENDATIONS=3Note: If no LLM API key is provided, the invoke() and stream() methods will use a fallback formatter.
LLM Provider Configuration
The package supports multiple LLM providers for AI-formatted output:
Google Gemini (Default)
const agent = MovieAgentFactory.create({
tmdbAccessToken: 'your-tmdb-key',
llmProvider: 'gemini',
geminiApiKey: 'your-gemini-key',
});Azure OpenAI
const agent = MovieAgentFactory.create({
tmdbAccessToken: 'your-tmdb-key',
llmProvider: 'azure',
azureOpenAiApiKey: 'your-azure-key',
azureOpenAiEndpoint: 'https://your-resource.openai.azure.com/',
azureOpenAiDeployment: 'your-deployment-name',
});Using Environment Variables
# Set LLM_PROVIDER in .env
LLM_PROVIDER=azure
AZURE_OPENAI_API_KEY=your_key
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/
AZURE_OPENAI_DEPLOYMENT=gpt-4// Automatically uses Azure OpenAI from environment
const agent = MovieAgentFactory.fromEnv();Input Parameters
interface UserInput {
mood?: string; // e.g., 'excited', 'relaxed', 'thoughtful'
genre?: string | string[]; // e.g., 'Action' or ['Action', 'Thriller']
platforms?: string[]; // e.g., ['Netflix', 'Prime Video']
runtime?: {
min?: number; // Minimum runtime in minutes
max?: number; // Maximum runtime in minutes
};
releaseYear?: number | { // Single year or range
from: number;
to: number;
};
}Error Handling
const result = await agent.getRecommendations(input);
if ('error' in result) {
console.error(`Error: ${result.errorType} - ${result.message}`);
switch (result.errorType) {
case 'INVALID_API_KEY':
// Handle invalid API key
break;
case 'RATE_LIMIT_EXCEEDED':
// Handle rate limit
break;
case 'NO_RESULTS':
// No movies found matching criteria
break;
}
} else {
// Success! Use recommendations
console.log(result.recommendations);
}Supported Streaming Platforms (Canada)
- Netflix
- Prime Video
- Crave
- Disney+
- Apple TV+
- Paramount+
- And many more regional platforms
TypeScript Support
The package is fully typed. Import types as needed:
import {
MovieAgentFactory,
UserInput,
AgentResponse,
MovieRecommendation,
ErrorResponse,
buildPosterUrl, // Utility to build poster URLs with different sizes
TMDB_IMAGE_BASE_URL, // Base URL for TMDb images
} from 'movie-agent';
// Build poster URL with custom size (default is w500)
const smallPoster = buildPosterUrl('/abc123.jpg', 'w185');
// => "https://image.tmdb.org/t/p/w185/abc123.jpg"Response Format
Success Response
interface AgentResponse {
recommendations: MovieRecommendation[];
metadata?: {
timestamp?: string;
inputParameters?: UserInput;
};
}
interface MovieRecommendation {
tmdbId: number;
title: string;
releaseYear: string;
runtime: number;
genres: string[];
overview: string;
streamingPlatforms: StreamingPlatform[];
matchReason: string;
posterUrl: string | null; // Full URL to movie poster image (w500 size)
}Error Response
interface ErrorResponse {
error: true;
errorType: 'VALIDATION_ERROR' | 'INVALID_API_KEY' | 'RATE_LIMIT_EXCEEDED' | 'NO_RESULTS' | 'UNKNOWN_ERROR';
message: string;
timestamp: string;
}Development
For Contributors
If you want to develop or modify this package:
# Clone the repository
git clone https://github.com/imWayneWY/movie-agent.git
cd movie-agent
# Install dependencies
npm install
# Copy environment variables template
cp .env.example .env
# Add your API keys to .envBuilding
# Type checking
npm run type-check
# Lint code
npm run lint
# Fix linting issues automatically
npm run lint:fix
# Format code
npm run format
# Check formatting without modifying files
npm run format:check
# Run all validations (type-check + lint + coverage)
npm run validateBuilding
# Clean build artifacts
npm run clean
# Build TypeScript to JavaScript
npm run build
# Clean and build (runs validation first)
npm run prebuild && npm run buildTesting
The project follows Test-Driven Development (TDD) practices with comprehensive test coverage.
Running Tests
# Run all tests (unit + E2E)
npm test
# Run only unit tests (excludes E2E and live integration tests)
npm run test:unit
# Run E2E tests
npm run test:e2e
# Run live integration tests (requires API keys)
npm run test:integration
# Run tests with coverage report
npm run test:coverage
# Run tests in watch mode (for development)
npm run test:watch
# Run tests in CI mode (with coverage)
npm run test:ciTest Structure
- Unit Tests (
src/__tests__/*.test.ts) - Test individual modules and functions - E2E Tests (
src/__tests__/e2e.test.ts) - Test complete recommendation pipeline - Integration Tests (
src/__tests__/*.live.test.ts) - Test with real APIs (requires credentials)
Coverage Requirements
The project enforces minimum 90% code coverage across:
- Branches: 90%
- Functions: 90%
- Lines: 90%
- Statements: 90%
View coverage report after running tests:
npm run test:coverage
open coverage/lcov-report/index.htmlCI/CD Workflow
GitHub Actions Example
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm ci
- run: npm run type-check
- run: npm run lint
- run: npm run test:ci
- uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.infoPre-commit Workflow
Recommended pre-commit hook (.git/hooks/pre-commit):
#!/bin/sh
npm run type-check && npm run lint && npm run test:unitDevelopment Workflow
Create a new branch
git checkout -b feature/your-featureMake changes with TDD
# Write tests first npm run test:watch # Implement feature # Ensure tests passValidate before commit
npm run validateCommit and push
git add . git commit -m "feat: your feature description" git push origin feature/your-featureCreate Pull Request
- Ensure CI passes
- Request code review
- Merge when approved
Contributing
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
License
MIT
Acknowledgments
- The Movie Database (TMDb) for movie data and streaming availability
- LangChain.js for agent framework
- Google Gemini and Azure OpenAI for LLM capabilities
