plex-mcp 0.5.1 → 0.5.2
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/index.js +496 -115
- package/llm-utils.js +393 -0
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -16,6 +16,12 @@ const fs = require('fs');
|
|
|
16
16
|
const path = require('path');
|
|
17
17
|
const os = require('os');
|
|
18
18
|
const { HttpLogger } = require('./http-logger');
|
|
19
|
+
const {
|
|
20
|
+
buildEnhancedPrompt,
|
|
21
|
+
validateTokenLimits,
|
|
22
|
+
validateResponse,
|
|
23
|
+
handleLLMError
|
|
24
|
+
} = require('./llm-utils');
|
|
19
25
|
|
|
20
26
|
// Initialize HTTP logger with Plex MCP specific configuration
|
|
21
27
|
const httpLogger = new HttpLogger({
|
|
@@ -110,7 +116,8 @@ class PlexAuthManager {
|
|
|
110
116
|
device: process.env.PLEX_DEVICE || 'PlexMCP',
|
|
111
117
|
version: process.env.PLEX_VERSION || '1.0.0',
|
|
112
118
|
forwardUrl: process.env.PLEX_REDIRECT_URL || 'https://app.plex.tv/auth#!',
|
|
113
|
-
platform: process.env.PLEX_PLATFORM || 'Web'
|
|
119
|
+
platform: process.env.PLEX_PLATFORM || 'Web',
|
|
120
|
+
urlencode: true // Ensure proper URL encoding by the OAuth library
|
|
114
121
|
};
|
|
115
122
|
|
|
116
123
|
this.plexOauth = new PlexOauth(clientInfo);
|
|
@@ -175,7 +182,7 @@ class PlexMCPServer {
|
|
|
175
182
|
this.authManager = new PlexAuthManager();
|
|
176
183
|
this.connectionVerified = false;
|
|
177
184
|
// Allow dependency injection for testing
|
|
178
|
-
this.axios = options.axios ||
|
|
185
|
+
this.axios = options.axios || _axiosWithLogging;
|
|
179
186
|
this.setupToolHandlers();
|
|
180
187
|
this.setupResourceHandlers();
|
|
181
188
|
this.setupPromptHandlers();
|
|
@@ -248,15 +255,28 @@ class PlexMCPServer {
|
|
|
248
255
|
|
|
249
256
|
${verification.error}
|
|
250
257
|
|
|
251
|
-
|
|
258
|
+
**📋 Complete Authentication Flow:**
|
|
259
|
+
|
|
260
|
+
**Step 1:** Run \`authenticate_plex\` to start the authentication process
|
|
261
|
+
**Step 2:** Open the provided URL in your browser and sign into Plex
|
|
262
|
+
**Step 3:** Grant permission to PlexMCP when prompted
|
|
263
|
+
**Step 4:** Return here and run \`check_auth_status\` to complete authentication
|
|
264
|
+
**Step 5:** Try your Plex operation again
|
|
265
|
+
|
|
266
|
+
**⚡ Quick Alternative:** Set the \`PLEX_TOKEN\` environment variable for persistent authentication (skips OAuth flow).
|
|
267
|
+
|
|
268
|
+
**💡 Need your token?** Find it at: https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/` :
|
|
252
269
|
`❌ **Connection Failed**
|
|
253
270
|
|
|
254
271
|
${verification.error}
|
|
255
272
|
|
|
256
|
-
|
|
257
|
-
- Verify PLEX_URL is correct and accessible
|
|
258
|
-
- Check if server is running and reachable
|
|
259
|
-
- Ensure network connectivity
|
|
273
|
+
**🔧 Troubleshooting:**
|
|
274
|
+
- Verify \`PLEX_URL\` environment variable is correct and accessible
|
|
275
|
+
- Check if your Plex server is running and reachable from this network
|
|
276
|
+
- Ensure network connectivity and firewall settings allow access
|
|
277
|
+
- Try testing the URL directly in a browser
|
|
278
|
+
|
|
279
|
+
**🌐 Current Server:** ${process.env.PLEX_URL || 'https://app.plex.tv (default)'}`;
|
|
260
280
|
|
|
261
281
|
return {
|
|
262
282
|
success: false,
|
|
@@ -278,6 +298,7 @@ ${verification.error}
|
|
|
278
298
|
getHttpsAgent() {
|
|
279
299
|
const verifySSL = process.env.PLEX_VERIFY_SSL !== 'false';
|
|
280
300
|
const https = require('https');
|
|
301
|
+
const tls = require('tls');
|
|
281
302
|
|
|
282
303
|
return new https.Agent({
|
|
283
304
|
rejectUnauthorized: verifySSL,
|
|
@@ -290,9 +311,20 @@ ${verification.error}
|
|
|
290
311
|
return undefined;
|
|
291
312
|
}
|
|
292
313
|
|
|
314
|
+
// Check if certificate is for a plex.direct domain (common case)
|
|
315
|
+
// Plex servers always use plex.direct certificates regardless of hostname
|
|
316
|
+
const certSubject = cert.subject?.CN || '';
|
|
317
|
+
const certSANs = cert.subjectaltname || '';
|
|
318
|
+
const isPlexDirectCert = certSubject.includes('plex.direct') || certSANs.includes('plex.direct');
|
|
319
|
+
|
|
320
|
+
if (isPlexDirectCert && !verifySSL) {
|
|
321
|
+
// Allow plex.direct certificates when SSL verification is disabled
|
|
322
|
+
return undefined;
|
|
323
|
+
}
|
|
324
|
+
|
|
293
325
|
// For non-plex.direct domains, use default behavior
|
|
294
326
|
if (verifySSL) {
|
|
295
|
-
return
|
|
327
|
+
return tls.checkServerIdentity(hostname, cert);
|
|
296
328
|
}
|
|
297
329
|
|
|
298
330
|
// If SSL verification is disabled, skip all checks
|
|
@@ -1233,6 +1265,25 @@ ${verification.error}
|
|
|
1233
1265
|
properties: {},
|
|
1234
1266
|
required: []
|
|
1235
1267
|
}
|
|
1268
|
+
},
|
|
1269
|
+
{
|
|
1270
|
+
name: 'validate_llm_response',
|
|
1271
|
+
description: 'Validate LLM response format and content against expected schemas for different prompt types',
|
|
1272
|
+
inputSchema: {
|
|
1273
|
+
type: 'object',
|
|
1274
|
+
properties: {
|
|
1275
|
+
response: {
|
|
1276
|
+
type: 'object',
|
|
1277
|
+
description: 'The LLM response object to validate'
|
|
1278
|
+
},
|
|
1279
|
+
prompt_type: {
|
|
1280
|
+
type: 'string',
|
|
1281
|
+
enum: ['playlist_description', 'content_recommendation', 'smart_playlist_rules', 'media_analysis'],
|
|
1282
|
+
description: 'The type of prompt that generated this response'
|
|
1283
|
+
}
|
|
1284
|
+
},
|
|
1285
|
+
required: ['response', 'prompt_type']
|
|
1286
|
+
}
|
|
1236
1287
|
}
|
|
1237
1288
|
]
|
|
1238
1289
|
};
|
|
@@ -1288,45 +1339,191 @@ ${verification.error}
|
|
|
1288
1339
|
return await this.handleCheckAuthStatus(request.params.arguments);
|
|
1289
1340
|
case 'clear_auth':
|
|
1290
1341
|
return await this.handleClearAuth(request.params.arguments);
|
|
1342
|
+
case 'validate_llm_response':
|
|
1343
|
+
return await this.handleValidateLLMResponse(request.params.arguments);
|
|
1291
1344
|
default:
|
|
1292
1345
|
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
1293
1346
|
}
|
|
1294
1347
|
});
|
|
1295
1348
|
}
|
|
1296
1349
|
|
|
1350
|
+
async handleValidateLLMResponse(args) {
|
|
1351
|
+
const { response, prompt_type: promptType } = args;
|
|
1352
|
+
|
|
1353
|
+
try {
|
|
1354
|
+
const validation = validateResponse(response, promptType);
|
|
1355
|
+
|
|
1356
|
+
return {
|
|
1357
|
+
content: [
|
|
1358
|
+
{
|
|
1359
|
+
type: 'text',
|
|
1360
|
+
text: `🔍 **LLM Response Validation Results**
|
|
1361
|
+
|
|
1362
|
+
**Prompt Type:** ${promptType}
|
|
1363
|
+
**Valid:** ${validation.valid ? '✅ Yes' : '❌ No'}
|
|
1364
|
+
|
|
1365
|
+
${validation.errors.length > 0 ? `**❌ Errors:**\n${validation.errors.map(e => `- ${e}`).join('\n')}\n` : ''}
|
|
1366
|
+
${validation.warnings.length > 0 ? `**⚠️ Warnings:**\n${validation.warnings.map(w => `- ${w}`).join('\n')}\n` : ''}
|
|
1367
|
+
|
|
1368
|
+
${validation.valid ?
|
|
1369
|
+
'✅ **Response is valid and ready to use!**' :
|
|
1370
|
+
'❌ **Response needs to be corrected before use.**'
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
**💡 Tips for Better Responses:**
|
|
1374
|
+
- Ensure all required fields are present
|
|
1375
|
+
- Check data types match expected formats
|
|
1376
|
+
- Verify content meets length requirements
|
|
1377
|
+
- Follow the specified JSON structure
|
|
1378
|
+
|
|
1379
|
+
**🔧 Response Structure for ${promptType}:**
|
|
1380
|
+
${this.getResponseStructureHelp(promptType)}`
|
|
1381
|
+
}
|
|
1382
|
+
]
|
|
1383
|
+
};
|
|
1384
|
+
} catch (error) {
|
|
1385
|
+
return {
|
|
1386
|
+
content: [
|
|
1387
|
+
{
|
|
1388
|
+
type: 'text',
|
|
1389
|
+
text: `❌ **Validation Error**
|
|
1390
|
+
|
|
1391
|
+
${error.message}
|
|
1392
|
+
|
|
1393
|
+
**Available prompt types for validation:**
|
|
1394
|
+
- \`playlist_description\` - Validates playlist description responses
|
|
1395
|
+
- \`content_recommendation\` - Validates content recommendation lists
|
|
1396
|
+
- \`smart_playlist_rules\` - Validates smart playlist criteria
|
|
1397
|
+
- \`media_analysis\` - Validates media library analysis results
|
|
1398
|
+
|
|
1399
|
+
**Usage Example:**
|
|
1400
|
+
\`\`\`json
|
|
1401
|
+
{
|
|
1402
|
+
"response": {"description": "Your playlist description here"},
|
|
1403
|
+
"prompt_type": "playlist_description"
|
|
1404
|
+
}
|
|
1405
|
+
\`\`\``
|
|
1406
|
+
}
|
|
1407
|
+
],
|
|
1408
|
+
isError: true
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
getResponseStructureHelp(promptType) {
|
|
1414
|
+
switch (promptType) {
|
|
1415
|
+
case 'playlist_description':
|
|
1416
|
+
return `\`\`\`json
|
|
1417
|
+
{
|
|
1418
|
+
"description": "2-3 sentence engaging description (50-500 chars)"
|
|
1419
|
+
}
|
|
1420
|
+
\`\`\``;
|
|
1421
|
+
|
|
1422
|
+
case 'content_recommendation':
|
|
1423
|
+
return `\`\`\`json
|
|
1424
|
+
{
|
|
1425
|
+
"recommendations": [
|
|
1426
|
+
{
|
|
1427
|
+
"title": "Content Title",
|
|
1428
|
+
"year": 2023,
|
|
1429
|
+
"reason": "Why it's similar (20+ chars)",
|
|
1430
|
+
"appeal": "What makes it appealing",
|
|
1431
|
+
"features": ["Notable feature 1", "Feature 2"]
|
|
1432
|
+
}
|
|
1433
|
+
]
|
|
1434
|
+
}
|
|
1435
|
+
\`\`\``;
|
|
1436
|
+
|
|
1437
|
+
case 'smart_playlist_rules':
|
|
1438
|
+
return `\`\`\`json
|
|
1439
|
+
{
|
|
1440
|
+
"criteria": {
|
|
1441
|
+
"filters": [{"type": "genre", "value": "rock"}],
|
|
1442
|
+
"sorting": {"field": "rating", "order": "desc"},
|
|
1443
|
+
"advanced_criteria": ["Recent additions", "High rated"],
|
|
1444
|
+
"tips": ["Keep playlist fresh", "Review periodically"]
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
\`\`\``;
|
|
1448
|
+
|
|
1449
|
+
case 'media_analysis':
|
|
1450
|
+
return `\`\`\`json
|
|
1451
|
+
{
|
|
1452
|
+
"insights": {
|
|
1453
|
+
"patterns": ["Pattern 1", "Pattern 2"],
|
|
1454
|
+
"recommendations": ["Suggestion 1", "Suggestion 2"],
|
|
1455
|
+
"statistics": {"total_items": 100, "genres": 15},
|
|
1456
|
+
"trends": ["Trend 1", "Trend 2"]
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
\`\`\``;
|
|
1460
|
+
|
|
1461
|
+
default:
|
|
1462
|
+
return 'Unknown prompt type - no structure help available';
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1297
1466
|
async handleAuthenticatePlex(_args) {
|
|
1298
1467
|
try {
|
|
1299
1468
|
const { loginUrl, pinId } = await this.authManager.requestAuthUrl();
|
|
1300
1469
|
|
|
1470
|
+
// Properly encode the entire URL to handle all special characters
|
|
1471
|
+
const encodedUrl = encodeURI(loginUrl);
|
|
1472
|
+
|
|
1301
1473
|
return {
|
|
1302
1474
|
content: [
|
|
1303
1475
|
{
|
|
1304
1476
|
type: 'text',
|
|
1305
|
-
text:
|
|
1477
|
+
text: `🔑 Plex Authentication Started
|
|
1306
1478
|
|
|
1307
|
-
|
|
1308
|
-
|
|
1479
|
+
**📋 Step-by-Step Instructions:**
|
|
1480
|
+
|
|
1481
|
+
1. **Copy and open this authentication URL in your browser:**
|
|
1309
1482
|
|
|
1310
1483
|
\`\`\`
|
|
1311
|
-
${
|
|
1484
|
+
${encodedUrl}
|
|
1312
1485
|
\`\`\`
|
|
1313
1486
|
|
|
1314
|
-
2. Sign into your Plex account when prompted
|
|
1315
|
-
3. **
|
|
1316
|
-
4.
|
|
1487
|
+
2. **Sign into your Plex account** when prompted
|
|
1488
|
+
3. **Grant permission** to PlexMCP when asked
|
|
1489
|
+
4. **Return here and run \`check_auth_status\`** to complete authentication
|
|
1490
|
+
5. **Verify success** - you'll see a confirmation message when complete
|
|
1491
|
+
|
|
1492
|
+
**🔢 Pin ID:** \`${pinId}\`
|
|
1493
|
+
**⏰ Session Timeout:** ~15 minutes
|
|
1317
1494
|
|
|
1318
|
-
**
|
|
1495
|
+
⚠️ **CRITICAL:** Authentication is not complete until you run \`check_auth_status\` and receive confirmation!
|
|
1319
1496
|
|
|
1320
|
-
|
|
1497
|
+
**💡 Alternative:** For persistent authentication, set the \`PLEX_TOKEN\` environment variable instead.`
|
|
1321
1498
|
}
|
|
1322
1499
|
]
|
|
1323
1500
|
};
|
|
1324
1501
|
} catch (_error) {
|
|
1502
|
+
let errorMessage = `Failed to start authentication: ${_error.message}`;
|
|
1503
|
+
|
|
1504
|
+
// Provide specific troubleshooting based on error type
|
|
1505
|
+
if (_error.message.includes('network') || _error.message.includes('connect')) {
|
|
1506
|
+
errorMessage += `
|
|
1507
|
+
|
|
1508
|
+
**🔧 Troubleshooting:**
|
|
1509
|
+
- Check your internet connection
|
|
1510
|
+
- Verify Plex services are accessible
|
|
1511
|
+
- Try again in a few moments`;
|
|
1512
|
+
} else if (_error.message.includes('client')) {
|
|
1513
|
+
errorMessage += `
|
|
1514
|
+
|
|
1515
|
+
**🔧 Troubleshooting:**
|
|
1516
|
+
- Check PLEX_CLIENT_ID environment variable
|
|
1517
|
+
- Verify client configuration is correct`;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1325
1520
|
return {
|
|
1326
1521
|
content: [
|
|
1327
1522
|
{
|
|
1328
1523
|
type: 'text',
|
|
1329
|
-
text: `❌ Authentication Error
|
|
1524
|
+
text: `❌ **Authentication Error**
|
|
1525
|
+
|
|
1526
|
+
${errorMessage}`
|
|
1330
1527
|
}
|
|
1331
1528
|
],
|
|
1332
1529
|
isError: true
|
|
@@ -1342,42 +1539,119 @@ ${loginUrl.replace(/\[/g, '%5B').replace(/\]/g, '%5D').replace(/!/g, '%21')}
|
|
|
1342
1539
|
|
|
1343
1540
|
if (authToken) {
|
|
1344
1541
|
this.connectionVerified = false; // Reset so next tool use will verify the new token
|
|
1345
|
-
return {
|
|
1346
|
-
content: [
|
|
1347
|
-
{
|
|
1348
|
-
type: 'text',
|
|
1349
|
-
text: `✅ Plex Authentication Successful!
|
|
1350
1542
|
|
|
1351
|
-
|
|
1543
|
+
// Verify the token works by testing connection
|
|
1544
|
+
try {
|
|
1545
|
+
const verification = await this.verifyConnection();
|
|
1546
|
+
if (verification.verified) {
|
|
1547
|
+
return {
|
|
1548
|
+
content: [
|
|
1549
|
+
{
|
|
1550
|
+
type: 'text',
|
|
1551
|
+
text: `✅ **Plex Authentication Successful!**
|
|
1552
|
+
|
|
1553
|
+
🎉 **You're all set!** Your authentication token has been saved and verified.
|
|
1352
1554
|
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1555
|
+
**✓ Token Status:** Active and working
|
|
1556
|
+
**✓ Connection:** Verified to Plex server
|
|
1557
|
+
**✓ Ready to use:** All Plex tools are now available
|
|
1558
|
+
|
|
1559
|
+
**🔄 Session Storage:** Token saved for this session
|
|
1560
|
+
**💾 Persistent Option:** Set \`PLEX_TOKEN\` environment variable for permanent authentication
|
|
1561
|
+
|
|
1562
|
+
**Next Steps:** You can now use any Plex tool like \`search_plex\`, \`browse_libraries\`, etc.`
|
|
1563
|
+
}
|
|
1564
|
+
]
|
|
1565
|
+
};
|
|
1566
|
+
} else {
|
|
1567
|
+
return {
|
|
1568
|
+
content: [
|
|
1569
|
+
{
|
|
1570
|
+
type: 'text',
|
|
1571
|
+
text: `⚠️ **Authentication Completed with Issues**
|
|
1572
|
+
|
|
1573
|
+
Your token was retrieved successfully, but there's a connection issue:
|
|
1574
|
+
|
|
1575
|
+
**Issue:** ${verification.error}
|
|
1576
|
+
|
|
1577
|
+
**Next Steps:**
|
|
1578
|
+
1. Check your Plex server settings
|
|
1579
|
+
2. Verify PLEX_URL is correctly configured
|
|
1580
|
+
3. Try using Plex tools - they may still work`
|
|
1581
|
+
}
|
|
1582
|
+
]
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
} catch (verifyError) {
|
|
1586
|
+
// Token was saved but connection verification failed - still a success
|
|
1587
|
+
return {
|
|
1588
|
+
content: [
|
|
1589
|
+
{
|
|
1590
|
+
type: 'text',
|
|
1591
|
+
text: `✅ **Authentication Token Saved**
|
|
1592
|
+
|
|
1593
|
+
Your Plex authentication token has been successfully retrieved and stored.
|
|
1594
|
+
|
|
1595
|
+
**⚠️ Connection Test:** Could not verify server connection (${verifyError.message})
|
|
1596
|
+
|
|
1597
|
+
**This is likely fine** - try using Plex tools to confirm everything works.`
|
|
1598
|
+
}
|
|
1599
|
+
]
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1357
1602
|
} else {
|
|
1603
|
+
const pinInfo = pin_id ? ` (Pin: ${pin_id})` : '';
|
|
1358
1604
|
return {
|
|
1359
1605
|
content: [
|
|
1360
1606
|
{
|
|
1361
1607
|
type: 'text',
|
|
1362
|
-
text: `⏳ Authentication Pending
|
|
1608
|
+
text: `⏳ **Authentication Pending**${pinInfo}
|
|
1609
|
+
|
|
1610
|
+
The authentication process is not yet complete. Please:
|
|
1363
1611
|
|
|
1364
|
-
|
|
1612
|
+
**📋 Steps to Complete:**
|
|
1613
|
+
1. ✅ Open the authentication URL from \`authenticate_plex\`
|
|
1614
|
+
2. ✅ Sign into your Plex account in the browser
|
|
1615
|
+
3. ⏸️ **Grant permission** to PlexMCP when prompted
|
|
1616
|
+
4. 🔄 Return here and run \`check_auth_status\` again
|
|
1365
1617
|
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
3. Try checking the auth status again in a few moments
|
|
1618
|
+
**⏰ Timeout:** Authentication links expire after ~15 minutes
|
|
1619
|
+
**🔄 Still waiting?** Try running \`check_auth_status\` again in 30 seconds
|
|
1369
1620
|
|
|
1370
|
-
|
|
1621
|
+
**💡 Tip:** If the browser window closed, you may need to restart with \`authenticate_plex\``
|
|
1371
1622
|
}
|
|
1372
1623
|
]
|
|
1373
1624
|
};
|
|
1374
1625
|
}
|
|
1375
1626
|
} catch (_error) {
|
|
1627
|
+
let errorMessage = _error.message;
|
|
1628
|
+
let troubleshooting = '';
|
|
1629
|
+
|
|
1630
|
+
// Provide specific guidance based on error type
|
|
1631
|
+
if (_error.message.includes('No pin ID')) {
|
|
1632
|
+
troubleshooting = `
|
|
1633
|
+
|
|
1634
|
+
**🔧 Solution:** Run \`authenticate_plex\` first to get a new authentication URL`;
|
|
1635
|
+
} else if (_error.message.includes('expired') || _error.message.includes('timeout')) {
|
|
1636
|
+
troubleshooting = `
|
|
1637
|
+
|
|
1638
|
+
**🔧 Solution:** The authentication session expired. Run \`authenticate_plex\` to start over`;
|
|
1639
|
+
} else if (_error.message.includes('network') || _error.message.includes('connect')) {
|
|
1640
|
+
troubleshooting = `
|
|
1641
|
+
|
|
1642
|
+
**🔧 Troubleshooting:**
|
|
1643
|
+
- Check your internet connection
|
|
1644
|
+
- Verify Plex services are accessible
|
|
1645
|
+
- Try again in a few moments`;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1376
1648
|
return {
|
|
1377
1649
|
content: [
|
|
1378
1650
|
{
|
|
1379
1651
|
type: 'text',
|
|
1380
|
-
text: `❌
|
|
1652
|
+
text: `❌ **Authentication Check Failed**
|
|
1653
|
+
|
|
1654
|
+
${errorMessage}${troubleshooting}`
|
|
1381
1655
|
}
|
|
1382
1656
|
],
|
|
1383
1657
|
isError: true
|
|
@@ -1394,21 +1668,52 @@ You can run check_auth_status again to check if authentication is complete.`
|
|
|
1394
1668
|
content: [
|
|
1395
1669
|
{
|
|
1396
1670
|
type: 'text',
|
|
1397
|
-
text: `🔄 Authentication Cleared
|
|
1671
|
+
text: `🔄 **Authentication Cleared Successfully**
|
|
1672
|
+
|
|
1673
|
+
All stored authentication credentials have been removed from this session.
|
|
1674
|
+
|
|
1675
|
+
**🧹 What was cleared:**
|
|
1676
|
+
- Session authentication token
|
|
1677
|
+
- Stored OAuth credentials
|
|
1678
|
+
- Cached connection verification
|
|
1679
|
+
|
|
1680
|
+
**🔐 To authenticate again, choose one option:**
|
|
1681
|
+
|
|
1682
|
+
**Option 1 - Interactive OAuth (Recommended):**
|
|
1683
|
+
1. Run \`authenticate_plex\` to get a new login URL
|
|
1684
|
+
2. Follow the authentication flow in your browser
|
|
1685
|
+
3. Run \`check_auth_status\` to complete the process
|
|
1398
1686
|
|
|
1399
|
-
|
|
1687
|
+
**Option 2 - Environment Variable:**
|
|
1688
|
+
1. Set \`PLEX_TOKEN\` environment variable with your token
|
|
1689
|
+
2. Restart the MCP server to use the new token
|
|
1400
1690
|
|
|
1401
|
-
|
|
1402
|
-
2. Run the authenticate_plex tool to sign in again`
|
|
1691
|
+
**💡 Note:** Environment variable tokens persist across sessions and don't require re-authentication.`
|
|
1403
1692
|
}
|
|
1404
1693
|
]
|
|
1405
1694
|
};
|
|
1406
1695
|
} catch (_error) {
|
|
1696
|
+
let errorMessage = _error.message;
|
|
1697
|
+
let troubleshooting = '';
|
|
1698
|
+
|
|
1699
|
+
if (_error.message.includes('file') || _error.message.includes('permission')) {
|
|
1700
|
+
troubleshooting = `
|
|
1701
|
+
|
|
1702
|
+
**🔧 Troubleshooting:**
|
|
1703
|
+
- Check file system permissions
|
|
1704
|
+
- Verify home directory is accessible
|
|
1705
|
+
- The session token was still cleared from memory`;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1407
1708
|
return {
|
|
1408
1709
|
content: [
|
|
1409
1710
|
{
|
|
1410
1711
|
type: 'text',
|
|
1411
|
-
text: `❌ Clear
|
|
1712
|
+
text: `❌ **Clear Authentication Error**
|
|
1713
|
+
|
|
1714
|
+
${errorMessage}${troubleshooting}
|
|
1715
|
+
|
|
1716
|
+
**ℹ️ Note:** In-memory authentication may still be cleared even if file operations failed.`
|
|
1412
1717
|
}
|
|
1413
1718
|
],
|
|
1414
1719
|
isError: true
|
|
@@ -5876,20 +6181,25 @@ The smart playlist has been created and is now available in your Plex library!`
|
|
|
5876
6181
|
this.server.setRequestHandler(GetPromptRequestSchema, async(request) => {
|
|
5877
6182
|
const { name, arguments: args } = request.params;
|
|
5878
6183
|
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
6184
|
+
try {
|
|
6185
|
+
// Build enhanced prompt with validation and context
|
|
6186
|
+
const promptInfo = buildEnhancedPrompt(name, args, {
|
|
6187
|
+
userLibrarySize: args?.library_size,
|
|
6188
|
+
preferredGenres: args?.preferred_genres,
|
|
6189
|
+
recentActivity: args?.recent_activity
|
|
6190
|
+
});
|
|
5884
6191
|
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
6192
|
+
switch (name) {
|
|
6193
|
+
case 'playlist_description':
|
|
6194
|
+
const playlistName = args?.playlist_name || 'Your Playlist';
|
|
6195
|
+
const genre = args?.genre || '';
|
|
6196
|
+
const contentInfo = args?.content_info || '';
|
|
6197
|
+
|
|
6198
|
+
// Check token limits for content
|
|
6199
|
+
const tokenCheck = validateTokenLimits(contentInfo, 'claude-3-sonnet');
|
|
6200
|
+
const finalContentInfo = tokenCheck.withinLimits ? contentInfo : tokenCheck.content;
|
|
6201
|
+
|
|
6202
|
+
const promptText = `Create a creative, engaging description for a playlist called "${playlistName}"${genre ? ` in the ${genre} genre` : ''}${finalContentInfo ? `.\n\nPlaylist content information:\n${finalContentInfo}` : ''}${promptInfo.context}.
|
|
5893
6203
|
|
|
5894
6204
|
The description should:
|
|
5895
6205
|
- Be 2-3 sentences long
|
|
@@ -5898,25 +6208,41 @@ The description should:
|
|
|
5898
6208
|
- Use vivid, descriptive language
|
|
5899
6209
|
${genre ? `- Reflect the ${genre} genre characteristics` : ''}
|
|
5900
6210
|
|
|
5901
|
-
Make it sound compelling and professional, like something you'd see on a streaming service
|
|
6211
|
+
Make it sound compelling and professional, like something you'd see on a streaming service.
|
|
6212
|
+
|
|
6213
|
+
**Response Format:** Return a JSON object with a "description" field containing the playlist description.`;
|
|
6214
|
+
|
|
6215
|
+
return {
|
|
6216
|
+
description: `Generate a creative and engaging description for the playlist "${playlistName}"`,
|
|
6217
|
+
messages: [
|
|
6218
|
+
{
|
|
6219
|
+
role: 'user',
|
|
6220
|
+
content: {
|
|
6221
|
+
type: 'text',
|
|
6222
|
+
text: promptText
|
|
6223
|
+
}
|
|
5902
6224
|
}
|
|
6225
|
+
],
|
|
6226
|
+
// Include LLM-specific metadata
|
|
6227
|
+
_metadata: {
|
|
6228
|
+
promptType: name,
|
|
6229
|
+
modelParameters: promptInfo.modelParameters,
|
|
6230
|
+
tokenEstimate: promptInfo.tokenValidation.estimatedTokens,
|
|
6231
|
+
withinLimits: promptInfo.tokenValidation.withinLimits,
|
|
6232
|
+
validationSchema: 'playlist_description'
|
|
5903
6233
|
}
|
|
5904
|
-
|
|
5905
|
-
};
|
|
6234
|
+
};
|
|
5906
6235
|
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
|
|
6236
|
+
case 'content_recommendation':
|
|
6237
|
+
const likedContent = args?.liked_content || '';
|
|
6238
|
+
const contentType = args?.content_type || 'content';
|
|
6239
|
+
const moodOrGenre = args?.mood_or_genre || '';
|
|
5911
6240
|
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
content: {
|
|
5918
|
-
type: 'text',
|
|
5919
|
-
text: `Based on these titles I've enjoyed: ${likedContent}
|
|
6241
|
+
// Validate and truncate liked content if needed
|
|
6242
|
+
const likedContentCheck = validateTokenLimits(likedContent, 'claude-3-sonnet', 2048);
|
|
6243
|
+
const finalLikedContent = likedContentCheck.withinLimits ? likedContent : likedContentCheck.content;
|
|
6244
|
+
|
|
6245
|
+
const recommendationPrompt = `Based on these titles I've enjoyed: ${finalLikedContent}${promptInfo.context}
|
|
5920
6246
|
|
|
5921
6247
|
Please recommend similar ${contentType}${moodOrGenre ? ` in the ${moodOrGenre} style` : ''}.
|
|
5922
6248
|
|
|
@@ -5926,24 +6252,42 @@ For each recommendation, provide:
|
|
|
5926
6252
|
- What makes it appealing
|
|
5927
6253
|
- Any notable cast, creators, or standout features
|
|
5928
6254
|
|
|
5929
|
-
Focus on finding hidden gems and quality content that matches my taste preferences shown in the titles I mentioned
|
|
6255
|
+
Focus on finding hidden gems and quality content that matches my taste preferences shown in the titles I mentioned.
|
|
6256
|
+
|
|
6257
|
+
**Response Format:** Return a JSON object with a "recommendations" array, where each recommendation has fields: title, year, reason, appeal, and features.`;
|
|
6258
|
+
|
|
6259
|
+
return {
|
|
6260
|
+
description: 'Generate personalized content recommendations',
|
|
6261
|
+
messages: [
|
|
6262
|
+
{
|
|
6263
|
+
role: 'user',
|
|
6264
|
+
content: {
|
|
6265
|
+
type: 'text',
|
|
6266
|
+
text: recommendationPrompt
|
|
6267
|
+
}
|
|
5930
6268
|
}
|
|
6269
|
+
],
|
|
6270
|
+
_metadata: {
|
|
6271
|
+
promptType: name,
|
|
6272
|
+
modelParameters: promptInfo.modelParameters,
|
|
6273
|
+
tokenEstimate: promptInfo.tokenValidation.estimatedTokens,
|
|
6274
|
+
withinLimits: likedContentCheck.withinLimits,
|
|
6275
|
+
validationSchema: 'content_recommendation'
|
|
5931
6276
|
}
|
|
5932
|
-
|
|
5933
|
-
};
|
|
6277
|
+
};
|
|
5934
6278
|
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
6279
|
+
case 'smart_playlist_rules':
|
|
6280
|
+
const intent = args?.intent || '';
|
|
6281
|
+
const libraryType = args?.library_type || 'media';
|
|
5938
6282
|
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
6283
|
+
return {
|
|
6284
|
+
description: 'Generate smart playlist criteria and filtering rules',
|
|
6285
|
+
messages: [
|
|
6286
|
+
{
|
|
6287
|
+
role: 'user',
|
|
6288
|
+
content: {
|
|
6289
|
+
type: 'text',
|
|
6290
|
+
text: `I want to create a smart playlist for: ${intent}
|
|
5947
6291
|
|
|
5948
6292
|
Library type: ${libraryType}
|
|
5949
6293
|
|
|
@@ -5954,23 +6298,23 @@ Please suggest specific filtering criteria and rules that would work well for th
|
|
|
5954
6298
|
- Tips for keeping the playlist fresh and relevant
|
|
5955
6299
|
|
|
5956
6300
|
Provide practical, actionable suggestions that would create a great automated playlist.`
|
|
6301
|
+
}
|
|
5957
6302
|
}
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
};
|
|
6303
|
+
]
|
|
6304
|
+
};
|
|
5961
6305
|
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
6306
|
+
case 'media_analysis':
|
|
6307
|
+
const contentData = args?.content_data || '';
|
|
6308
|
+
const analysisType = args?.analysis_type || 'general analysis';
|
|
5965
6309
|
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
6310
|
+
return {
|
|
6311
|
+
description: 'Analyze media library content and patterns',
|
|
6312
|
+
messages: [
|
|
6313
|
+
{
|
|
6314
|
+
role: 'user',
|
|
6315
|
+
content: {
|
|
6316
|
+
type: 'text',
|
|
6317
|
+
text: `Please analyze this media library data for ${analysisType}:
|
|
5974
6318
|
|
|
5975
6319
|
${contentData}
|
|
5976
6320
|
|
|
@@ -5982,23 +6326,23 @@ Provide insights about:
|
|
|
5982
6326
|
- Any interesting observations about the content
|
|
5983
6327
|
|
|
5984
6328
|
Format the analysis in a clear, organized way that's easy to understand and actionable.`
|
|
6329
|
+
}
|
|
5985
6330
|
}
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
};
|
|
6331
|
+
]
|
|
6332
|
+
};
|
|
5989
6333
|
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
6334
|
+
case 'server_troubleshooting':
|
|
6335
|
+
const errorDetails = args?.error_details || '';
|
|
6336
|
+
const serverInfo = args?.server_info || '';
|
|
5993
6337
|
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6338
|
+
return {
|
|
6339
|
+
description: 'Help diagnose and resolve Plex server issues',
|
|
6340
|
+
messages: [
|
|
6341
|
+
{
|
|
6342
|
+
role: 'user',
|
|
6343
|
+
content: {
|
|
6344
|
+
type: 'text',
|
|
6345
|
+
text: `I'm having an issue with my Plex server. Here are the details:
|
|
6002
6346
|
|
|
6003
6347
|
Error/Issue: ${errorDetails}
|
|
6004
6348
|
|
|
@@ -6011,13 +6355,50 @@ Please help me:
|
|
|
6011
6355
|
4. Identify if this is a common issue with known solutions
|
|
6012
6356
|
|
|
6013
6357
|
Focus on practical, actionable solutions that don't require advanced technical expertise.`
|
|
6358
|
+
}
|
|
6014
6359
|
}
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
};
|
|
6360
|
+
]
|
|
6361
|
+
};
|
|
6018
6362
|
|
|
6019
|
-
|
|
6020
|
-
|
|
6363
|
+
default:
|
|
6364
|
+
throw new Error(`Unknown prompt: ${name}`);
|
|
6365
|
+
}
|
|
6366
|
+
} catch (error) {
|
|
6367
|
+
// Handle prompt-specific errors with enhanced error information
|
|
6368
|
+
const errorInfo = handleLLMError(error, name);
|
|
6369
|
+
|
|
6370
|
+
return {
|
|
6371
|
+
description: `Error generating prompt for ${name}`,
|
|
6372
|
+
messages: [
|
|
6373
|
+
{
|
|
6374
|
+
role: 'system',
|
|
6375
|
+
content: {
|
|
6376
|
+
type: 'text',
|
|
6377
|
+
text: `❌ **Prompt Generation Error**
|
|
6378
|
+
|
|
6379
|
+
${errorInfo.message}
|
|
6380
|
+
|
|
6381
|
+
**Error Type:** ${errorInfo.error}
|
|
6382
|
+
**Retryable:** ${errorInfo.retryable ? 'Yes' : 'No'}
|
|
6383
|
+
${errorInfo.retryAfter ? `**Retry After:** ${errorInfo.retryAfter} seconds` : ''}
|
|
6384
|
+
|
|
6385
|
+
**Troubleshooting:**
|
|
6386
|
+
- Check that all required arguments are provided
|
|
6387
|
+
- Verify argument formats match expected types
|
|
6388
|
+
- Ensure content length is within reasonable limits
|
|
6389
|
+
- Try simplifying the request if it's too complex
|
|
6390
|
+
|
|
6391
|
+
**Available Prompts:** playlist_description, content_recommendation, smart_playlist_rules, media_analysis, server_troubleshooting`
|
|
6392
|
+
}
|
|
6393
|
+
}
|
|
6394
|
+
],
|
|
6395
|
+
_metadata: {
|
|
6396
|
+
error: true,
|
|
6397
|
+
errorType: errorInfo.error,
|
|
6398
|
+
retryable: errorInfo.retryable,
|
|
6399
|
+
originalError: error.message
|
|
6400
|
+
}
|
|
6401
|
+
};
|
|
6021
6402
|
}
|
|
6022
6403
|
});
|
|
6023
6404
|
}
|
package/llm-utils.js
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM Utilities and Best Practices Framework
|
|
3
|
+
* Provides tools for proper LLM interaction patterns including
|
|
4
|
+
* response validation, token management, error handling, and prompt optimization
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Estimate token count for text content
|
|
9
|
+
* Uses a simple approximation: 1 token ≈ 4 characters
|
|
10
|
+
* More accurate tokenization would require model-specific tokenizers
|
|
11
|
+
*/
|
|
12
|
+
function estimateTokenCount(text) {
|
|
13
|
+
if (typeof text !== 'string') { return 0; }
|
|
14
|
+
// Rough approximation: 1 token per 4 characters
|
|
15
|
+
// This varies by model and language, but provides a baseline
|
|
16
|
+
return Math.ceil(text.length / 4);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Token limit configurations for different model types
|
|
21
|
+
*/
|
|
22
|
+
const TOKEN_LIMITS = {
|
|
23
|
+
'gpt-3.5-turbo': { input: 4096, output: 4096 },
|
|
24
|
+
'gpt-4': { input: 8192, output: 8192 },
|
|
25
|
+
'gpt-4-32k': { input: 32768, output: 32768 },
|
|
26
|
+
'claude-3-sonnet': { input: 200000, output: 4096 },
|
|
27
|
+
'claude-3-opus': { input: 200000, output: 4096 },
|
|
28
|
+
'claude-3-haiku': { input: 200000, output: 4096 },
|
|
29
|
+
default: { input: 4096, output: 1024 }
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Validate and potentially truncate content to fit within token limits
|
|
34
|
+
*/
|
|
35
|
+
function validateTokenLimits(content, modelType = 'default', reserveOutputTokens = 1024) {
|
|
36
|
+
const limits = TOKEN_LIMITS[modelType] || TOKEN_LIMITS.default;
|
|
37
|
+
const maxInputTokens = limits.input - reserveOutputTokens;
|
|
38
|
+
|
|
39
|
+
const estimatedTokens = estimateTokenCount(content);
|
|
40
|
+
|
|
41
|
+
if (estimatedTokens <= maxInputTokens) {
|
|
42
|
+
return {
|
|
43
|
+
content,
|
|
44
|
+
withinLimits: true,
|
|
45
|
+
estimatedTokens,
|
|
46
|
+
maxInputTokens
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Calculate truncation point (leave some buffer)
|
|
51
|
+
const targetChars = Math.floor(maxInputTokens * 4 * 0.9); // 90% of limit for safety
|
|
52
|
+
const truncatedContent = content.substring(0, targetChars) + '...\n[Content truncated to fit token limits]';
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
content: truncatedContent,
|
|
56
|
+
withinLimits: false,
|
|
57
|
+
estimatedTokens: estimateTokenCount(truncatedContent),
|
|
58
|
+
originalTokens: estimatedTokens,
|
|
59
|
+
maxInputTokens,
|
|
60
|
+
truncated: true
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Response validation schemas for different prompt types
|
|
66
|
+
*/
|
|
67
|
+
const RESPONSE_SCHEMAS = {
|
|
68
|
+
playlist_description: {
|
|
69
|
+
type: 'object',
|
|
70
|
+
required: ['description'],
|
|
71
|
+
properties: {
|
|
72
|
+
description: {
|
|
73
|
+
type: 'string',
|
|
74
|
+
minLength: 50,
|
|
75
|
+
maxLength: 500
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
content_recommendation: {
|
|
81
|
+
type: 'object',
|
|
82
|
+
required: ['recommendations'],
|
|
83
|
+
properties: {
|
|
84
|
+
recommendations: {
|
|
85
|
+
type: 'array',
|
|
86
|
+
minItems: 1,
|
|
87
|
+
maxItems: 10,
|
|
88
|
+
items: {
|
|
89
|
+
type: 'object',
|
|
90
|
+
required: ['title', 'reason'],
|
|
91
|
+
properties: {
|
|
92
|
+
title: { type: 'string', minLength: 1 },
|
|
93
|
+
year: { type: 'number', minimum: 1900, maximum: 2100 },
|
|
94
|
+
reason: { type: 'string', minLength: 20 },
|
|
95
|
+
appeal: { type: 'string' },
|
|
96
|
+
features: { type: 'array', items: { type: 'string' } }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
smart_playlist_rules: {
|
|
104
|
+
type: 'object',
|
|
105
|
+
required: ['criteria'],
|
|
106
|
+
properties: {
|
|
107
|
+
criteria: {
|
|
108
|
+
type: 'object',
|
|
109
|
+
properties: {
|
|
110
|
+
filters: { type: 'array', items: { type: 'object' } },
|
|
111
|
+
sorting: { type: 'object' },
|
|
112
|
+
advanced_criteria: { type: 'array' },
|
|
113
|
+
tips: { type: 'array', items: { type: 'string' } }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
media_analysis: {
|
|
120
|
+
type: 'object',
|
|
121
|
+
required: ['insights'],
|
|
122
|
+
properties: {
|
|
123
|
+
insights: {
|
|
124
|
+
type: 'object',
|
|
125
|
+
properties: {
|
|
126
|
+
patterns: { type: 'array' },
|
|
127
|
+
recommendations: { type: 'array' },
|
|
128
|
+
statistics: { type: 'object' },
|
|
129
|
+
trends: { type: 'array' }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Validate LLM response against expected schema
|
|
138
|
+
*/
|
|
139
|
+
function validateResponse(response, promptType) {
|
|
140
|
+
const schema = RESPONSE_SCHEMAS[promptType];
|
|
141
|
+
if (!schema) {
|
|
142
|
+
return {
|
|
143
|
+
valid: true,
|
|
144
|
+
warnings: [`No validation schema defined for prompt type: ${promptType}`]
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const errors = [];
|
|
149
|
+
const warnings = [];
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
// Basic structure validation
|
|
153
|
+
if (typeof response !== 'object' || response === null) {
|
|
154
|
+
errors.push('Response must be a valid object');
|
|
155
|
+
return { valid: false, errors, warnings };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Check required fields
|
|
159
|
+
if (schema.required) {
|
|
160
|
+
for (const field of schema.required) {
|
|
161
|
+
if (!(field in response)) {
|
|
162
|
+
errors.push(`Missing required field: ${field}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Validate specific fields based on prompt type
|
|
168
|
+
switch (promptType) {
|
|
169
|
+
case 'playlist_description':
|
|
170
|
+
if (response.description && response.description.length < 50) {
|
|
171
|
+
warnings.push('Description is quite short, consider expanding');
|
|
172
|
+
}
|
|
173
|
+
break;
|
|
174
|
+
|
|
175
|
+
case 'content_recommendation':
|
|
176
|
+
if (response.recommendations && response.recommendations.length === 0) {
|
|
177
|
+
errors.push('At least one recommendation is required');
|
|
178
|
+
}
|
|
179
|
+
break;
|
|
180
|
+
|
|
181
|
+
case 'smart_playlist_rules':
|
|
182
|
+
if (response.criteria && !response.criteria.filters) {
|
|
183
|
+
warnings.push('No specific filters provided in criteria');
|
|
184
|
+
}
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
} catch (error) {
|
|
188
|
+
errors.push(`Validation error: ${error.message}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
valid: errors.length === 0,
|
|
193
|
+
errors,
|
|
194
|
+
warnings
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Retry logic with exponential backoff for external API calls
|
|
200
|
+
*/
|
|
201
|
+
async function retryWithBackoff(fn, maxRetries = 3, baseDelay = 1000) {
|
|
202
|
+
let lastError;
|
|
203
|
+
|
|
204
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
205
|
+
try {
|
|
206
|
+
return await fn();
|
|
207
|
+
} catch (error) {
|
|
208
|
+
lastError = error;
|
|
209
|
+
|
|
210
|
+
// Don't retry on certain error types
|
|
211
|
+
if (error.code === 'INVALID_REQUEST' || error.status === 400) {
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// If this is the last attempt, throw the error
|
|
216
|
+
if (attempt === maxRetries - 1) {
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Calculate delay with exponential backoff and jitter
|
|
221
|
+
const delay = baseDelay * Math.pow(2, attempt) + Math.random() * 1000;
|
|
222
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
throw lastError;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Model parameter configurations for different use cases
|
|
231
|
+
*/
|
|
232
|
+
const MODEL_PARAMETERS = {
|
|
233
|
+
creative: {
|
|
234
|
+
temperature: 0.8,
|
|
235
|
+
top_p: 0.9,
|
|
236
|
+
frequency_penalty: 0.1,
|
|
237
|
+
presence_penalty: 0.1
|
|
238
|
+
},
|
|
239
|
+
analytical: {
|
|
240
|
+
temperature: 0.3,
|
|
241
|
+
top_p: 0.8,
|
|
242
|
+
frequency_penalty: 0.0,
|
|
243
|
+
presence_penalty: 0.0
|
|
244
|
+
},
|
|
245
|
+
balanced: {
|
|
246
|
+
temperature: 0.6,
|
|
247
|
+
top_p: 0.85,
|
|
248
|
+
frequency_penalty: 0.05,
|
|
249
|
+
presence_penalty: 0.05
|
|
250
|
+
},
|
|
251
|
+
conservative: {
|
|
252
|
+
temperature: 0.1,
|
|
253
|
+
top_p: 0.7,
|
|
254
|
+
frequency_penalty: 0.0,
|
|
255
|
+
presence_penalty: 0.0
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Get recommended model parameters for a prompt type
|
|
261
|
+
*/
|
|
262
|
+
function getModelParameters(promptType, style = 'balanced') {
|
|
263
|
+
const baseParams = MODEL_PARAMETERS[style] || MODEL_PARAMETERS.balanced;
|
|
264
|
+
|
|
265
|
+
// Adjust parameters based on prompt type
|
|
266
|
+
switch (promptType) {
|
|
267
|
+
case 'playlist_description':
|
|
268
|
+
return { ...baseParams, temperature: 0.8 }; // More creative
|
|
269
|
+
|
|
270
|
+
case 'content_recommendation':
|
|
271
|
+
return { ...baseParams, temperature: 0.6 }; // Balanced
|
|
272
|
+
|
|
273
|
+
case 'smart_playlist_rules':
|
|
274
|
+
return { ...baseParams, temperature: 0.3 }; // More analytical
|
|
275
|
+
|
|
276
|
+
case 'media_analysis':
|
|
277
|
+
return { ...baseParams, temperature: 0.2 }; // Very analytical
|
|
278
|
+
|
|
279
|
+
default:
|
|
280
|
+
return baseParams;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Enhanced prompt builder with context inclusion and validation
|
|
286
|
+
*/
|
|
287
|
+
function buildEnhancedPrompt(promptType, args, context = {}) {
|
|
288
|
+
const errors = [];
|
|
289
|
+
|
|
290
|
+
// Validate required arguments based on prompt type
|
|
291
|
+
switch (promptType) {
|
|
292
|
+
case 'playlist_description':
|
|
293
|
+
if (!args.playlist_name) { errors.push('playlist_name is required'); }
|
|
294
|
+
break;
|
|
295
|
+
case 'content_recommendation':
|
|
296
|
+
if (!args.liked_content) { errors.push('liked_content is required'); }
|
|
297
|
+
break;
|
|
298
|
+
case 'smart_playlist_rules':
|
|
299
|
+
if (!args.intent) { errors.push('intent is required'); }
|
|
300
|
+
break;
|
|
301
|
+
case 'media_analysis':
|
|
302
|
+
if (!args.content_data) { errors.push('content_data is required'); }
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (errors.length > 0) {
|
|
307
|
+
throw new Error(`Prompt validation failed: ${errors.join(', ')}`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Include relevant context
|
|
311
|
+
const contextInfo = [];
|
|
312
|
+
if (context.userLibrarySize) {
|
|
313
|
+
contextInfo.push(`User has ${context.userLibrarySize} items in their library`);
|
|
314
|
+
}
|
|
315
|
+
if (context.preferredGenres) {
|
|
316
|
+
contextInfo.push(`Preferred genres: ${context.preferredGenres.join(', ')}`);
|
|
317
|
+
}
|
|
318
|
+
if (context.recentActivity) {
|
|
319
|
+
contextInfo.push(`Recent activity: ${context.recentActivity}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const contextString = contextInfo.length > 0 ?
|
|
323
|
+
`\n\nContext: ${contextInfo.join('. ')}\n` :
|
|
324
|
+
'';
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
promptType,
|
|
328
|
+
args,
|
|
329
|
+
context: contextString,
|
|
330
|
+
modelParameters: getModelParameters(promptType),
|
|
331
|
+
tokenValidation: validateTokenLimits(JSON.stringify(args) + contextString)
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Error handling patterns for LLM interactions
|
|
337
|
+
*/
|
|
338
|
+
function handleLLMError(error, promptType) {
|
|
339
|
+
const baseMessage = `Error in ${promptType} prompt`;
|
|
340
|
+
|
|
341
|
+
if (error.code === 'rate_limit_exceeded') {
|
|
342
|
+
return {
|
|
343
|
+
error: 'Rate limit exceeded',
|
|
344
|
+
message: `${baseMessage}: Too many requests. Please try again later.`,
|
|
345
|
+
retryable: true,
|
|
346
|
+
retryAfter: error.retry_after || 60
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (error.code === 'insufficient_quota') {
|
|
351
|
+
return {
|
|
352
|
+
error: 'Quota exceeded',
|
|
353
|
+
message: `${baseMessage}: API quota exceeded. Check your billing.`,
|
|
354
|
+
retryable: false
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (error.code === 'model_overloaded') {
|
|
359
|
+
return {
|
|
360
|
+
error: 'Model overloaded',
|
|
361
|
+
message: `${baseMessage}: Model is overloaded. Try again shortly.`,
|
|
362
|
+
retryable: true,
|
|
363
|
+
retryAfter: 30
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (error.code === 'invalid_request_error') {
|
|
368
|
+
return {
|
|
369
|
+
error: 'Invalid request',
|
|
370
|
+
message: `${baseMessage}: Request format is invalid. Check prompt structure.`,
|
|
371
|
+
retryable: false
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
error: 'Unknown error',
|
|
377
|
+
message: `${baseMessage}: ${error.message}`,
|
|
378
|
+
retryable: true
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
module.exports = {
|
|
383
|
+
estimateTokenCount,
|
|
384
|
+
validateTokenLimits,
|
|
385
|
+
validateResponse,
|
|
386
|
+
retryWithBackoff,
|
|
387
|
+
getModelParameters,
|
|
388
|
+
buildEnhancedPrompt,
|
|
389
|
+
handleLLMError,
|
|
390
|
+
TOKEN_LIMITS,
|
|
391
|
+
MODEL_PARAMETERS,
|
|
392
|
+
RESPONSE_SCHEMAS
|
|
393
|
+
};
|