movie-agent 1.0.0 ā 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +319 -39
- package/bin/movie-agent +33 -8
- package/dist/agent.d.ts +31 -4
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +109 -16
- package/dist/agent.js.map +1 -1
- package/dist/cache.d.ts +40 -5
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +95 -11
- package/dist/cache.js.map +1 -1
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +23 -1
- package/dist/factory.js.map +1 -1
- package/dist/format.d.ts +14 -1
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +22 -1
- package/dist/format.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/llm.d.ts +33 -0
- package/dist/llm.d.ts.map +1 -0
- package/dist/llm.js +221 -0
- package/dist/llm.js.map +1 -0
- package/dist/providers.d.ts +9 -1
- package/dist/providers.d.ts.map +1 -1
- package/dist/providers.js +27 -0
- package/dist/providers.js.map +1 -1
- package/dist/rateLimiter.d.ts +59 -0
- package/dist/rateLimiter.d.ts.map +1 -0
- package/dist/rateLimiter.js +99 -0
- package/dist/rateLimiter.js.map +1 -0
- package/dist/sanitize.d.ts +24 -0
- package/dist/sanitize.d.ts.map +1 -0
- package/dist/sanitize.js +137 -0
- package/dist/sanitize.js.map +1 -0
- package/dist/tmdbApi.d.ts +37 -1
- package/dist/tmdbApi.d.ts.map +1 -1
- package/dist/tmdbApi.js +70 -22
- package/dist/tmdbApi.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/validate.d.ts +5 -0
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +38 -1
- package/dist/validate.js.map +1 -1
- package/package.json +9 -3
package/README.md
CHANGED
|
@@ -5,15 +5,21 @@ An intelligent movie recommendation system that helps users discover movies base
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- š¬ Personalized movie recommendations based on mood, genre, and preferences
|
|
8
|
+
- š¤ AI-powered output formatting with LangChain and Google Gemini
|
|
9
|
+
- š Streaming and non-streaming output modes
|
|
8
10
|
- šŗ Real-time streaming availability for Canadian platforms
|
|
9
11
|
- ā” Fast responses using TMDb API
|
|
10
12
|
- šÆ Smart filtering by runtime, release year, and streaming platforms
|
|
11
13
|
- š¦ Easy integration into web applications and APIs
|
|
14
|
+
- š Multi-tenant cache isolation for secure deployments (see [Cache Isolation Guide](docs/CACHE_ISOLATION.md))
|
|
12
15
|
|
|
13
16
|
## Prerequisites
|
|
14
17
|
|
|
15
18
|
- Node.js (v18 or higher)
|
|
16
19
|
- TMDb API key ([Get one here](https://www.themoviedb.org/settings/api))
|
|
20
|
+
- LLM API key for AI formatting (optional):
|
|
21
|
+
- Google Gemini API key - [Get one here](https://aistudio.google.com/app/apikey)
|
|
22
|
+
- **OR** Azure OpenAI credentials - [Learn more](https://azure.microsoft.com/en-us/products/ai-services/openai-service)
|
|
17
23
|
|
|
18
24
|
## Installation
|
|
19
25
|
|
|
@@ -23,55 +29,186 @@ npm install movie-agent
|
|
|
23
29
|
|
|
24
30
|
## Quick Start
|
|
25
31
|
|
|
26
|
-
###
|
|
32
|
+
### Basic Usage - Get Structured Data
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
import { MovieAgent } from 'movie-agent';
|
|
36
|
+
|
|
37
|
+
// Create agent
|
|
38
|
+
const agent = new MovieAgent();
|
|
39
|
+
|
|
40
|
+
// Get structured recommendations
|
|
41
|
+
const response = await agent.getRecommendations({
|
|
42
|
+
mood: 'excited',
|
|
43
|
+
platforms: ['Netflix', 'Prime Video'],
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
console.log(response.recommendations);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### AI-Formatted Output - Invoke Mode (Waits for complete response)
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import { MovieAgent } from 'movie-agent';
|
|
53
|
+
|
|
54
|
+
const agent = new MovieAgent();
|
|
55
|
+
|
|
56
|
+
// Get AI-formatted markdown output
|
|
57
|
+
const output = await agent.invoke({
|
|
58
|
+
mood: 'happy',
|
|
59
|
+
platforms: ['Netflix'],
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
console.log(output); // Formatted markdown string
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### AI-Formatted Output - Stream Mode (Real-time streaming)
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
import { MovieAgent } from 'movie-agent';
|
|
69
|
+
|
|
70
|
+
const agent = new MovieAgent();
|
|
71
|
+
|
|
72
|
+
// Stream AI-formatted output in real-time
|
|
73
|
+
await agent.stream({
|
|
74
|
+
mood: 'excited',
|
|
75
|
+
platforms: ['Netflix'],
|
|
76
|
+
}, (chunk) => {
|
|
77
|
+
process.stdout.write(chunk); // or update your UI
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Using with MovieAgentFactory
|
|
27
82
|
|
|
28
83
|
```typescript
|
|
29
84
|
import { MovieAgentFactory } from 'movie-agent';
|
|
30
85
|
|
|
31
|
-
// Create agent with explicit configuration
|
|
86
|
+
// Create agent with explicit configuration (Gemini)
|
|
32
87
|
const agent = MovieAgentFactory.create({
|
|
33
88
|
tmdbApiKey: process.env.TMDB_API_KEY!,
|
|
34
89
|
tmdbRegion: 'CA',
|
|
35
|
-
|
|
90
|
+
llmProvider: 'gemini',
|
|
91
|
+
geminiApiKey: process.env.GEMINI_API_KEY,
|
|
92
|
+
debug: true,
|
|
36
93
|
});
|
|
37
94
|
|
|
38
|
-
//
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
95
|
+
// Or use Azure OpenAI
|
|
96
|
+
const agentWithAzure = MovieAgentFactory.create({
|
|
97
|
+
tmdbApiKey: process.env.TMDB_API_KEY!,
|
|
98
|
+
tmdbRegion: 'CA',
|
|
99
|
+
llmProvider: 'azure',
|
|
100
|
+
azureOpenAiApiKey: process.env.AZURE_OPENAI_API_KEY,
|
|
101
|
+
azureOpenAiEndpoint: process.env.AZURE_OPENAI_ENDPOINT,
|
|
102
|
+
azureOpenAiDeployment: process.env.AZURE_OPENAI_DEPLOYMENT,
|
|
103
|
+
debug: true,
|
|
43
104
|
});
|
|
44
105
|
|
|
45
|
-
|
|
106
|
+
// All three methods available: getRecommendations, invoke, stream
|
|
107
|
+
const structured = await agent.getRecommendations({ mood: 'happy' });
|
|
108
|
+
const formatted = await agent.invoke({ mood: 'happy' });
|
|
109
|
+
await agent.stream({ mood: 'happy' }, (chunk) => console.log(chunk));
|
|
46
110
|
```
|
|
47
111
|
|
|
48
|
-
|
|
112
|
+
## API Reference
|
|
113
|
+
|
|
114
|
+
### MovieAgent Methods
|
|
115
|
+
|
|
116
|
+
#### `getRecommendations(input: UserInput): Promise<AgentResponse | ErrorResponse>`
|
|
117
|
+
Returns structured movie recommendations with metadata.
|
|
49
118
|
|
|
50
119
|
```typescript
|
|
51
|
-
|
|
52
|
-
|
|
120
|
+
const response = await agent.getRecommendations({
|
|
121
|
+
mood: 'happy',
|
|
122
|
+
platforms: ['Netflix']
|
|
123
|
+
});
|
|
124
|
+
// Returns: { recommendations: [...], metadata: {...} }
|
|
125
|
+
```
|
|
53
126
|
|
|
54
|
-
|
|
55
|
-
|
|
127
|
+
#### `invoke(input: UserInput): Promise<string | ErrorResponse>`
|
|
128
|
+
Returns AI-formatted markdown output (non-streaming). Waits for complete response before returning.
|
|
56
129
|
|
|
57
|
-
|
|
58
|
-
const
|
|
130
|
+
```typescript
|
|
131
|
+
const output = await agent.invoke({
|
|
132
|
+
mood: 'excited',
|
|
133
|
+
platforms: ['Netflix']
|
|
134
|
+
});
|
|
135
|
+
// Returns: Formatted markdown string
|
|
136
|
+
```
|
|
59
137
|
|
|
60
|
-
|
|
61
|
-
|
|
138
|
+
#### `stream(input: UserInput, onChunk: (chunk: string) => void): Promise<void | ErrorResponse>`
|
|
139
|
+
Streams AI-formatted output in real-time. Best for interactive UIs.
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
await agent.stream({
|
|
62
143
|
mood: 'relaxed',
|
|
63
|
-
|
|
64
|
-
|
|
144
|
+
platforms: ['Netflix']
|
|
145
|
+
}, (chunk) => {
|
|
146
|
+
process.stdout.write(chunk);
|
|
65
147
|
});
|
|
66
148
|
```
|
|
67
149
|
|
|
150
|
+
## Input Parameters
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
interface UserInput {
|
|
154
|
+
mood?: string; // e.g., 'excited', 'relaxed', 'thoughtful', 'happy', 'scared'
|
|
155
|
+
genre?: string | string[]; // e.g., 'Action' or ['Action', 'Thriller']
|
|
156
|
+
platforms?: string[]; // e.g., ['Netflix', 'Prime Video', 'Disney+']
|
|
157
|
+
runtime?: {
|
|
158
|
+
min?: number; // Minimum runtime in minutes
|
|
159
|
+
max?: number; // Maximum runtime in minutes
|
|
160
|
+
};
|
|
161
|
+
releaseYear?: number | { // Single year or range
|
|
162
|
+
from?: number;
|
|
163
|
+
to?: number;
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Output Comparison
|
|
169
|
+
|
|
170
|
+
### `getRecommendations()` - Structured Data
|
|
171
|
+
```json
|
|
172
|
+
{
|
|
173
|
+
"recommendations": [
|
|
174
|
+
{
|
|
175
|
+
"tmdbId": 123,
|
|
176
|
+
"title": "Movie Title",
|
|
177
|
+
"releaseYear": "2024",
|
|
178
|
+
"runtime": 120,
|
|
179
|
+
"genres": ["Action", "Adventure"],
|
|
180
|
+
"description": "...",
|
|
181
|
+
"streamingPlatforms": [...],
|
|
182
|
+
"matchReason": "...",
|
|
183
|
+
"posterUrl": "https://image.tmdb.org/t/p/w500/abc123.jpg"
|
|
184
|
+
}
|
|
185
|
+
],
|
|
186
|
+
"metadata": {...}
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### `invoke()` / `stream()` - AI-Formatted Markdown
|
|
191
|
+
```markdown
|
|
192
|
+
Hey there! š Looking for some action-packed thrills? Here are your recommendations:
|
|
193
|
+
|
|
194
|
+
### **Movie Title (2024)** - 120 minutes
|
|
195
|
+
*Genres: Action, Adventure*
|
|
196
|
+
A compelling description...
|
|
197
|
+
šŗ Available on: Netflix
|
|
198
|
+
⨠Why: Perfect for your excited mood with thrilling action!
|
|
199
|
+
```
|
|
200
|
+
|
|
68
201
|
## API Examples
|
|
69
202
|
|
|
70
203
|
```typescript
|
|
71
204
|
// Simple mood-based search
|
|
72
|
-
await agent.getRecommendations({
|
|
73
|
-
|
|
74
|
-
|
|
205
|
+
await agent.getRecommendations({ mood: 'happy' });
|
|
206
|
+
|
|
207
|
+
// AI-formatted output
|
|
208
|
+
await agent.invoke({ mood: 'happy' });
|
|
209
|
+
|
|
210
|
+
// Streaming output
|
|
211
|
+
await agent.stream({ mood: 'happy' }, (chunk) => console.log(chunk));
|
|
75
212
|
|
|
76
213
|
// Genre-specific with platform filter
|
|
77
214
|
await agent.getRecommendations({
|
|
@@ -97,17 +234,14 @@ await agent.getRecommendations({
|
|
|
97
234
|
|
|
98
235
|
## Integration Examples
|
|
99
236
|
|
|
100
|
-
### Next.js API Route
|
|
237
|
+
### Next.js API Route (Structured Data)
|
|
101
238
|
|
|
102
239
|
```typescript
|
|
103
240
|
// pages/api/recommendations.ts
|
|
104
|
-
import {
|
|
241
|
+
import { MovieAgent } from 'movie-agent';
|
|
105
242
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
|
106
243
|
|
|
107
|
-
const agent =
|
|
108
|
-
tmdbApiKey: process.env.TMDB_API_KEY!,
|
|
109
|
-
tmdbRegion: 'CA',
|
|
110
|
-
});
|
|
244
|
+
const agent = new MovieAgent();
|
|
111
245
|
|
|
112
246
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|
113
247
|
if (req.method !== 'POST') {
|
|
@@ -125,23 +259,50 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|
|
125
259
|
}
|
|
126
260
|
```
|
|
127
261
|
|
|
128
|
-
###
|
|
262
|
+
### Next.js API Route (AI-Formatted Streaming)
|
|
129
263
|
|
|
130
264
|
```typescript
|
|
131
|
-
|
|
132
|
-
import {
|
|
133
|
-
import
|
|
265
|
+
// pages/api/recommendations-stream.ts
|
|
266
|
+
import { MovieAgent } from 'movie-agent';
|
|
267
|
+
import type { NextApiRequest, NextApiResponse } from 'next';
|
|
268
|
+
|
|
269
|
+
const agent = new MovieAgent();
|
|
134
270
|
|
|
135
|
-
|
|
271
|
+
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|
272
|
+
if (req.method !== 'POST') {
|
|
273
|
+
return res.status(405).json({ error: 'Method not allowed' });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
const { mood, genre, platforms } = req.body;
|
|
278
|
+
|
|
279
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
280
|
+
res.setHeader('Transfer-Encoding', 'chunked');
|
|
281
|
+
|
|
282
|
+
await agent.stream({ mood, genre, platforms }, (chunk) => {
|
|
283
|
+
res.write(chunk);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
res.end();
|
|
287
|
+
} catch (error) {
|
|
288
|
+
console.error('Error streaming recommendations:', error);
|
|
289
|
+
res.status(500).json({ error: 'Failed to stream recommendations' });
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Express Server (Streaming)
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
import express from 'express';
|
|
298
|
+
import { MovieAgent } from 'movie-agent';
|
|
136
299
|
|
|
137
300
|
const app = express();
|
|
138
301
|
app.use(express.json());
|
|
139
302
|
|
|
140
|
-
const agent =
|
|
141
|
-
tmdbApiKey: process.env.TMDB_API_KEY!,
|
|
142
|
-
tmdbRegion: 'CA',
|
|
143
|
-
});
|
|
303
|
+
const agent = new MovieAgent();
|
|
144
304
|
|
|
305
|
+
// Structured data endpoint
|
|
145
306
|
app.post('/api/recommendations', async (req, res) => {
|
|
146
307
|
try {
|
|
147
308
|
const recommendations = await agent.getRecommendations(req.body);
|
|
@@ -152,11 +313,72 @@ app.post('/api/recommendations', async (req, res) => {
|
|
|
152
313
|
}
|
|
153
314
|
});
|
|
154
315
|
|
|
316
|
+
// Streaming endpoint
|
|
317
|
+
app.post('/api/recommendations/stream', async (req, res) => {
|
|
318
|
+
try {
|
|
319
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
320
|
+
res.setHeader('Transfer-Encoding', 'chunked');
|
|
321
|
+
|
|
322
|
+
await agent.stream(req.body, (chunk) => {
|
|
323
|
+
res.write(chunk);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
res.end();
|
|
327
|
+
} catch (error) {
|
|
328
|
+
console.error(error);
|
|
329
|
+
res.status(500).json({ error: 'Failed to stream recommendations' });
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
|
|
155
333
|
app.listen(3000, () => {
|
|
156
334
|
console.log('Server running on http://localhost:3000');
|
|
157
335
|
});
|
|
158
336
|
```
|
|
159
337
|
|
|
338
|
+
### React Component with Streaming
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
import { useState } from 'react';
|
|
342
|
+
|
|
343
|
+
function MovieRecommendations() {
|
|
344
|
+
const [output, setOutput] = useState('');
|
|
345
|
+
const [loading, setLoading] = useState(false);
|
|
346
|
+
|
|
347
|
+
const getRecommendations = async () => {
|
|
348
|
+
setLoading(true);
|
|
349
|
+
setOutput('');
|
|
350
|
+
|
|
351
|
+
const response = await fetch('/api/recommendations/stream', {
|
|
352
|
+
method: 'POST',
|
|
353
|
+
headers: { 'Content-Type': 'application/json' },
|
|
354
|
+
body: JSON.stringify({ mood: 'happy', platforms: ['Netflix'] }),
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
const reader = response.body?.getReader();
|
|
358
|
+
const decoder = new TextDecoder();
|
|
359
|
+
|
|
360
|
+
while (true) {
|
|
361
|
+
const { done, value } = await reader.read();
|
|
362
|
+
if (done) break;
|
|
363
|
+
|
|
364
|
+
const chunk = decoder.decode(value);
|
|
365
|
+
setOutput(prev => prev + chunk);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
setLoading(false);
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
return (
|
|
372
|
+
<div>
|
|
373
|
+
<button onClick={getRecommendations} disabled={loading}>
|
|
374
|
+
Get Recommendations
|
|
375
|
+
</button>
|
|
376
|
+
<div style={{ whiteSpace: 'pre-wrap' }}>{output}</div>
|
|
377
|
+
</div>
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
160
382
|
## Configuration
|
|
161
383
|
|
|
162
384
|
### Environment Variables
|
|
@@ -165,6 +387,17 @@ app.listen(3000, () => {
|
|
|
165
387
|
# Required
|
|
166
388
|
TMDB_API_KEY=your_tmdb_api_key_here
|
|
167
389
|
|
|
390
|
+
# LLM Provider Selection (optional)
|
|
391
|
+
LLM_PROVIDER=gemini # Options: gemini, azure
|
|
392
|
+
|
|
393
|
+
# Option 1: Google Gemini (for AI-formatted output)
|
|
394
|
+
GEMINI_API_KEY=your_gemini_api_key_here
|
|
395
|
+
|
|
396
|
+
# Option 2: Azure OpenAI (for AI-formatted output)
|
|
397
|
+
AZURE_OPENAI_API_KEY=your_azure_openai_api_key_here
|
|
398
|
+
AZURE_OPENAI_ENDPOINT=https://your-resource-name.openai.azure.com/
|
|
399
|
+
AZURE_OPENAI_DEPLOYMENT=your_deployment_name
|
|
400
|
+
|
|
168
401
|
# Optional
|
|
169
402
|
TMDB_REGION=CA
|
|
170
403
|
CACHE_TTL=86400
|
|
@@ -172,6 +405,46 @@ MAX_RECOMMENDATIONS=5
|
|
|
172
405
|
MIN_RECOMMENDATIONS=3
|
|
173
406
|
```
|
|
174
407
|
|
|
408
|
+
**Note:** If no LLM API key is provided, the `invoke()` and `stream()` methods will use a fallback formatter.
|
|
409
|
+
|
|
410
|
+
### LLM Provider Configuration
|
|
411
|
+
|
|
412
|
+
The package supports multiple LLM providers for AI-formatted output:
|
|
413
|
+
|
|
414
|
+
#### Google Gemini (Default)
|
|
415
|
+
```typescript
|
|
416
|
+
const agent = MovieAgentFactory.create({
|
|
417
|
+
tmdbApiKey: 'your-tmdb-key',
|
|
418
|
+
llmProvider: 'gemini',
|
|
419
|
+
geminiApiKey: 'your-gemini-key',
|
|
420
|
+
});
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
#### Azure OpenAI
|
|
424
|
+
```typescript
|
|
425
|
+
const agent = MovieAgentFactory.create({
|
|
426
|
+
tmdbApiKey: 'your-tmdb-key',
|
|
427
|
+
llmProvider: 'azure',
|
|
428
|
+
azureOpenAiApiKey: 'your-azure-key',
|
|
429
|
+
azureOpenAiEndpoint: 'https://your-resource.openai.azure.com/',
|
|
430
|
+
azureOpenAiDeployment: 'your-deployment-name',
|
|
431
|
+
});
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
#### Using Environment Variables
|
|
435
|
+
```bash
|
|
436
|
+
# Set LLM_PROVIDER in .env
|
|
437
|
+
LLM_PROVIDER=azure
|
|
438
|
+
AZURE_OPENAI_API_KEY=your_key
|
|
439
|
+
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/
|
|
440
|
+
AZURE_OPENAI_DEPLOYMENT=gpt-4
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
```typescript
|
|
444
|
+
// Automatically uses Azure OpenAI from environment
|
|
445
|
+
const agent = MovieAgentFactory.fromEnv();
|
|
446
|
+
```
|
|
447
|
+
|
|
175
448
|
### Input Parameters
|
|
176
449
|
|
|
177
450
|
```typescript
|
|
@@ -236,7 +509,13 @@ import {
|
|
|
236
509
|
AgentResponse,
|
|
237
510
|
MovieRecommendation,
|
|
238
511
|
ErrorResponse,
|
|
512
|
+
buildPosterUrl, // Utility to build poster URLs with different sizes
|
|
513
|
+
TMDB_IMAGE_BASE_URL, // Base URL for TMDb images
|
|
239
514
|
} from 'movie-agent';
|
|
515
|
+
|
|
516
|
+
// Build poster URL with custom size (default is w500)
|
|
517
|
+
const smallPoster = buildPosterUrl('/abc123.jpg', 'w185');
|
|
518
|
+
// => "https://image.tmdb.org/t/p/w185/abc123.jpg"
|
|
240
519
|
```
|
|
241
520
|
|
|
242
521
|
## Response Format
|
|
@@ -261,6 +540,7 @@ interface MovieRecommendation {
|
|
|
261
540
|
overview: string;
|
|
262
541
|
streamingPlatforms: StreamingPlatform[];
|
|
263
542
|
matchReason: string;
|
|
543
|
+
posterUrl: string | null; // Full URL to movie poster image (w500 size)
|
|
264
544
|
}
|
|
265
545
|
```
|
|
266
546
|
|
|
@@ -461,4 +741,4 @@ MIT
|
|
|
461
741
|
|
|
462
742
|
- [The Movie Database (TMDb)](https://www.themoviedb.org/) for movie data and streaming availability
|
|
463
743
|
- [LangChain.js](https://js.langchain.com/) for agent framework
|
|
464
|
-
- [OpenAI](https://
|
|
744
|
+
- [Google Gemini](https://ai.google.dev/) and [Azure OpenAI](https://azure.microsoft.com/en-us/products/ai-services/openai-service) for LLM capabilities
|
package/bin/movie-agent
CHANGED
|
@@ -10,6 +10,7 @@ const fs = require('fs');
|
|
|
10
10
|
function parseArgs() {
|
|
11
11
|
const args = process.argv.slice(2);
|
|
12
12
|
const input = {};
|
|
13
|
+
let useStreaming = false;
|
|
13
14
|
|
|
14
15
|
for (let i = 0; i < args.length; i++) {
|
|
15
16
|
const arg = args[i];
|
|
@@ -75,13 +76,15 @@ function parseArgs() {
|
|
|
75
76
|
printUsage();
|
|
76
77
|
process.exit(1);
|
|
77
78
|
}
|
|
79
|
+
} else if (arg === '--stream') {
|
|
80
|
+
useStreaming = true;
|
|
78
81
|
} else if (arg === '--help' || arg === '-h') {
|
|
79
82
|
printUsage();
|
|
80
83
|
process.exit(0);
|
|
81
84
|
}
|
|
82
85
|
}
|
|
83
86
|
|
|
84
|
-
return input;
|
|
87
|
+
return { input, useStreaming };
|
|
85
88
|
}
|
|
86
89
|
|
|
87
90
|
/**
|
|
@@ -100,11 +103,12 @@ Options:
|
|
|
100
103
|
--year <number> Specific release year
|
|
101
104
|
--yearFrom <number> Release year range start
|
|
102
105
|
--yearTo <number> Release year range end
|
|
106
|
+
--stream Enable streaming output (real-time generation)
|
|
103
107
|
--help, -h Show this help message
|
|
104
108
|
|
|
105
109
|
Examples:
|
|
106
110
|
movie-agent --mood excited --platforms Netflix
|
|
107
|
-
movie-agent --genre Action,Thriller --runtimeMax 120
|
|
111
|
+
movie-agent --genre Action,Thriller --runtimeMax 120 --stream
|
|
108
112
|
movie-agent --mood relaxed --yearFrom 2020 --yearTo 2023
|
|
109
113
|
`);
|
|
110
114
|
}
|
|
@@ -173,10 +177,15 @@ Or create a .env.development file with:
|
|
|
173
177
|
process.exit(1);
|
|
174
178
|
}
|
|
175
179
|
|
|
180
|
+
// Check for Gemini API key (optional, agent will work without it)
|
|
181
|
+
if (!process.env.GEMINI_API_KEY) {
|
|
182
|
+
console.warn(`\nā ļø Warning: Gemini API key not found. Using fallback formatting.\n`);
|
|
183
|
+
}
|
|
184
|
+
|
|
176
185
|
// Now it's safe to load the agent module
|
|
177
186
|
const { MovieAgent } = require('../dist/agent');
|
|
178
187
|
|
|
179
|
-
const input = parseArgs();
|
|
188
|
+
const { input, useStreaming } = parseArgs();
|
|
180
189
|
|
|
181
190
|
// Show help if no arguments provided
|
|
182
191
|
if (Object.keys(input).length === 0) {
|
|
@@ -184,19 +193,35 @@ Or create a .env.development file with:
|
|
|
184
193
|
process.exit(0);
|
|
185
194
|
}
|
|
186
195
|
|
|
187
|
-
console.log('
|
|
196
|
+
console.log('\\nš Finding movie recommendations...\\n');
|
|
188
197
|
|
|
189
198
|
const agent = new MovieAgent();
|
|
190
|
-
const response = await agent.getRecommendations(input);
|
|
191
199
|
|
|
192
|
-
|
|
193
|
-
|
|
200
|
+
if (useStreaming) {
|
|
201
|
+
// Use streaming output
|
|
202
|
+
const result = await agent.stream(input, (chunk) => {
|
|
203
|
+
process.stdout.write(chunk);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
if (result && 'error' in result) {
|
|
207
|
+
throw new Error(result.message);
|
|
208
|
+
}
|
|
209
|
+
} else {
|
|
210
|
+
// Use non-streaming output
|
|
211
|
+
const output = await agent.invoke(input);
|
|
212
|
+
|
|
213
|
+
if (typeof output === 'object' && 'error' in output) {
|
|
214
|
+
throw new Error(output.message);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
console.log(output);
|
|
218
|
+
}
|
|
194
219
|
|
|
195
220
|
} catch (error) {
|
|
196
221
|
console.error('\nā Error:', error.message);
|
|
197
222
|
|
|
198
223
|
if (error.message.includes('API') || error.message.includes('network')) {
|
|
199
|
-
console.error('\nPlease check your internet connection and
|
|
224
|
+
console.error('\nPlease check your internet connection and API keys.');
|
|
200
225
|
} else if (error.message.includes('Invalid') || error.message.includes('validation')) {
|
|
201
226
|
console.error('\nPlease check your input parameters and try again.');
|
|
202
227
|
console.error('Use --help for usage information.');
|
package/dist/agent.d.ts
CHANGED
|
@@ -6,12 +6,22 @@ import TmdbApiClient from './tmdbApi';
|
|
|
6
6
|
export declare class MovieAgent {
|
|
7
7
|
private tmdbClient;
|
|
8
8
|
private logger;
|
|
9
|
+
private llmService;
|
|
10
|
+
private debug;
|
|
9
11
|
/**
|
|
10
12
|
* Creates a new MovieAgent instance
|
|
11
13
|
* @param tmdbClient - Optional TMDb API client for testing
|
|
12
14
|
* @param logger - Optional logger function for debugging
|
|
15
|
+
* @param enableLLM - Whether to enable LLM formatting (default: true if GEMINI_API_KEY is set)
|
|
16
|
+
* @param llmProvider - LLM provider to use ('gemini' or 'azure')
|
|
17
|
+
* @param llmApiKey - API key for the LLM provider
|
|
18
|
+
* @param azureConfig - Azure OpenAI configuration (endpoint and deployment)
|
|
19
|
+
* @param debug - Whether to include detailed error information in responses (default: false for production safety)
|
|
13
20
|
*/
|
|
14
|
-
constructor(tmdbClient?: TmdbApiClient, logger?: (message: string) => void
|
|
21
|
+
constructor(tmdbClient?: TmdbApiClient, logger?: (message: string) => void, enableLLM?: boolean, llmProvider?: 'gemini' | 'azure', llmApiKey?: string, azureConfig?: {
|
|
22
|
+
endpoint?: string;
|
|
23
|
+
deployment?: string;
|
|
24
|
+
}, debug?: boolean);
|
|
15
25
|
/**
|
|
16
26
|
* Gets movie recommendations based on user input
|
|
17
27
|
* @param input - User input with mood, platforms, genres, runtime, and year preferences
|
|
@@ -52,11 +62,11 @@ export declare class MovieAgent {
|
|
|
52
62
|
*/
|
|
53
63
|
private discoverCandidates;
|
|
54
64
|
/**
|
|
55
|
-
*
|
|
56
|
-
* @param candidates - Array of movie details
|
|
65
|
+
* Extracts watch providers from movie details and filters to those with providers
|
|
66
|
+
* @param candidates - Array of movie details with embedded providers
|
|
57
67
|
* @returns Array of movies with platform information
|
|
58
68
|
*/
|
|
59
|
-
private
|
|
69
|
+
private extractWatchProviders;
|
|
60
70
|
/**
|
|
61
71
|
* Applies user-specified filters to movies
|
|
62
72
|
* @param movies - Movies with provider information
|
|
@@ -78,6 +88,23 @@ export declare class MovieAgent {
|
|
|
78
88
|
* @returns Array of formatted recommendations
|
|
79
89
|
*/
|
|
80
90
|
private formatRecommendations;
|
|
91
|
+
/**
|
|
92
|
+
* Get recommendations with AI-formatted output (invoke mode)
|
|
93
|
+
* @param input - User input with mood, platforms, genres, runtime, and year preferences
|
|
94
|
+
* @returns Promise resolving to AI-formatted string output or error response
|
|
95
|
+
*/
|
|
96
|
+
invoke(input: UserInput): Promise<string | ErrorResponse>;
|
|
97
|
+
/**
|
|
98
|
+
* Get recommendations with AI-formatted streaming output
|
|
99
|
+
* @param input - User input with mood, platforms, genres, runtime, and year preferences
|
|
100
|
+
* @param onChunk - Callback function called for each chunk of streamed content
|
|
101
|
+
* @returns Promise resolving when streaming is complete, or error response
|
|
102
|
+
*/
|
|
103
|
+
stream(input: UserInput, onChunk: (chunk: string) => void): Promise<void | ErrorResponse>;
|
|
104
|
+
/**
|
|
105
|
+
* Fallback formatting when LLM is not available
|
|
106
|
+
*/
|
|
107
|
+
private fallbackFormat;
|
|
81
108
|
/**
|
|
82
109
|
* Generates a match reason for a movie
|
|
83
110
|
* @param movie - Movie to explain
|
package/dist/agent.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"AACA,OAAO,EACL,SAAS,EACT,aAAa,EAGb,aAAa,EACd,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"AACA,OAAO,EACL,SAAS,EACT,aAAa,EAGb,aAAa,EACd,MAAM,SAAS,CAAC;AAWjB,OAAO,aAGN,MAAM,WAAW,CAAC;AAcnB;;GAEG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,UAAU,CAAgB;IAClC,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,UAAU,CAAoB;IACtC,OAAO,CAAC,KAAK,CAAU;IAEvB;;;;;;;;;OASG;gBAED,UAAU,CAAC,EAAE,aAAa,EAC1B,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,EAClC,SAAS,CAAC,EAAE,OAAO,EACnB,WAAW,CAAC,EAAE,QAAQ,GAAG,OAAO,EAChC,SAAS,CAAC,EAAE,MAAM,EAClB,WAAW,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,EACxD,KAAK,CAAC,EAAE,OAAO;IAgBjB;;;;OAIG;IACG,kBAAkB,CACtB,KAAK,EAAE,SAAS,GACf,OAAO,CAAC,aAAa,GAAG,aAAa,CAAC;IAkEzC;;;;;;OAMG;IACH,OAAO,CAAC,mBAAmB;IAoB3B;;;;OAIG;IACH,OAAO,CAAC,WAAW;IAmEnB;;;;OAIG;IACH,OAAO,CAAC,aAAa;IA+BrB;;;;OAIG;IACH,OAAO,CAAC,aAAa;IAerB;;;;;OAKG;YACW,kBAAkB;IA2ChC;;;;OAIG;IACH,OAAO,CAAC,qBAAqB;IA8B7B;;;;;OAKG;IACH,OAAO,CAAC,YAAY;IA+BpB;;;;;OAKG;IACH,OAAO,CAAC,aAAa;IAoCrB;;;;;OAKG;IACH,OAAO,CAAC,qBAAqB;IAkC7B;;;;OAIG;IACG,MAAM,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,aAAa,CAAC;IAoB/D;;;;;OAKG;IACG,MAAM,CACV,KAAK,EAAE,SAAS,EAChB,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAC/B,OAAO,CAAC,IAAI,GAAG,aAAa,CAAC;IAwBhC;;OAEG;IACH,OAAO,CAAC,cAAc;IAsBtB;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;CAuC5B"}
|