Skip to content

Commit 24469de

Browse files
authored
refactor: Consolidate AI examples (#1356)
1 parent 753214b commit 24469de

File tree

8 files changed

+360
-336
lines changed

8 files changed

+360
-336
lines changed

app.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ app.use(passport.initialize());
125125
app.use(passport.session());
126126
app.use(flash());
127127
app.use((req, res, next) => {
128-
if (req.path === '/api/upload' || req.path === '/api/togetherai-camera') {
128+
if (req.path === '/api/upload' || req.path === '/ai/togetherai-camera') {
129129
// Multer multipart/form-data handling needs to occur before the Lusca CSRF check.
130130
// WARN: Any path that is not protected by CSRF here should have lusca.csrf() chained
131131
// in their route handler.
@@ -237,12 +237,12 @@ app.get('/api/trakt', apiController.getTrakt);
237237
* AI Integrations and Boilerplate example routes.
238238
*/
239239
app.get('/ai', aiController.getAi);
240-
app.get('/api/openai-moderation', apiController.getOpenAIModeration);
241-
app.post('/api/openai-moderation', apiController.postOpenAIModeration);
242-
app.get('/api/togetherai-classifier', apiController.getTogetherAIClassifier);
243-
app.post('/api/togetherai-classifier', apiController.postTogetherAIClassifier);
244-
app.get('/api/togetherai-camera', lusca({ csrf: true }), apiController.getTogetherAICamera);
245-
app.post('/api/togetherai-camera', strictLimiter, apiController.imageUploadMiddleware, lusca({ csrf: true }), apiController.postTogetherAICamera);
240+
app.get('/ai/openai-moderation', aiController.getOpenAIModeration);
241+
app.post('/ai/openai-moderation', aiController.postOpenAIModeration);
242+
app.get('/ai/togetherai-classifier', aiController.getTogetherAIClassifier);
243+
app.post('/ai/togetherai-classifier', aiController.postTogetherAIClassifier);
244+
app.get('/ai/togetherai-camera', lusca({ csrf: true }), aiController.getTogetherAICamera);
245+
app.post('/ai/togetherai-camera', strictLimiter, aiController.imageUploadMiddleware, lusca({ csrf: true }), aiController.postTogetherAICamera);
246246
app.get('/ai/rag', aiController.getRag);
247247
app.post('/ai/rag/ingest', aiController.postRagIngest);
248248
app.post('/ai/rag/ask', aiController.postRagAsk);

controllers/ai.js

Lines changed: 295 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
const crypto = require('crypto');
12
const fs = require('fs');
3+
const multer = require('multer');
24
const path = require('path');
3-
const crypto = require('crypto');
45
const { PDFLoader } = require('@langchain/community/document_loaders/fs/pdf');
56
const { RecursiveCharacterTextSplitter } = require('@langchain/textsplitters');
67
const { HuggingFaceInferenceEmbeddings } = require('@langchain/community/embeddings/hf');
@@ -448,3 +449,296 @@ exports.postRagAsk = async (req, res) => {
448449
await client.close();
449450
}
450451
};
452+
453+
/**
454+
* GET /ai/openai-moderation
455+
* OpenAI Moderation API example.
456+
*/
457+
exports.getOpenAIModeration = (req, res) => {
458+
res.render('ai/openai-moderation', {
459+
title: 'OpenAI Input Moderation',
460+
result: null,
461+
error: null,
462+
input: '',
463+
});
464+
};
465+
466+
/**
467+
* POST /ai/openai-moderation
468+
* OpenAI Moderation API example.
469+
*/
470+
exports.postOpenAIModeration = async (req, res) => {
471+
const openAiKey = process.env.OPENAI_API_KEY;
472+
const inputText = req.body.inputText || '';
473+
let result = null;
474+
let error = null;
475+
476+
if (!openAiKey) {
477+
error = 'OpenAI API key is not set in environment variables.';
478+
} else if (!inputText.trim()) {
479+
error = 'Text for input modaration check:';
480+
} else {
481+
try {
482+
const response = await fetch('https://api.openai.com/v1/moderations', {
483+
method: 'POST',
484+
headers: {
485+
'Content-Type': 'application/json',
486+
Authorization: `Bearer ${openAiKey}`,
487+
},
488+
body: JSON.stringify({
489+
model: 'text-moderation-latest',
490+
input: inputText,
491+
}),
492+
});
493+
if (!response.ok) {
494+
const errData = await response.json().catch(() => ({}));
495+
error = errData.error && errData.error.message ? errData.error.message : `API Error: ${response.status}`;
496+
} else {
497+
const data = await response.json();
498+
result = data.results && data.results[0];
499+
}
500+
} catch (err) {
501+
console.error('OpenAI Moderation API Error:', err);
502+
error = 'Failed to call OpenAI Moderation API.';
503+
}
504+
}
505+
506+
res.render('ai/openai-moderation', {
507+
title: 'OpenAI Moderation API',
508+
result,
509+
error,
510+
input: inputText,
511+
});
512+
};
513+
514+
/**
515+
* Helper functions and constants for Together AI API Example
516+
* We are using LLMs to classify text or analyze a picture taken by the user's camera.
517+
*/
518+
519+
// Shared Together AI API caller
520+
const callTogetherAiApi = async (apiRequestBody, apiKey) => {
521+
const response = await fetch('https://api.together.xyz/v1/chat/completions', {
522+
method: 'POST',
523+
headers: {
524+
'Content-Type': 'application/json',
525+
Authorization: `Bearer ${apiKey}`,
526+
},
527+
body: JSON.stringify(apiRequestBody),
528+
});
529+
if (!response.ok) {
530+
const errData = await response.json().catch(() => ({}));
531+
console.error('Together AI API Error Response:', errData);
532+
const errorMessage = errData.error && errData.error.message ? errData.error.message : `API Error: ${response.status}`;
533+
throw new Error(errorMessage);
534+
}
535+
return response.json();
536+
};
537+
538+
// Vision-specific functions
539+
const createVisionLLMRequestBody = (dataUrl, model) => ({
540+
model,
541+
messages: [
542+
{
543+
role: 'user',
544+
content: [
545+
{
546+
type: 'text',
547+
text: 'What is in this image?',
548+
},
549+
{
550+
type: 'image_url',
551+
image_url: {
552+
url: dataUrl,
553+
},
554+
},
555+
],
556+
},
557+
],
558+
});
559+
560+
const extractVisionAnalysis = (data) => {
561+
if (data.choices && Array.isArray(data.choices) && data.choices.length > 0 && data.choices[0].message && data.choices[0].message.content) {
562+
return data.choices[0].message.content;
563+
}
564+
return 'No vision analysis available';
565+
};
566+
567+
// Classifier-specific functions
568+
const createClassifierLLMRequestBody = (inputText, model, systemPrompt) => ({
569+
model,
570+
messages: [
571+
{ role: 'system', content: systemPrompt },
572+
{ role: 'user', content: inputText },
573+
],
574+
temperature: 0,
575+
max_tokens: 64,
576+
});
577+
578+
const extractClassifierResponse = (content) => {
579+
let department = null;
580+
if (content) {
581+
try {
582+
// Try to extract JSON from the response
583+
const jsonStringMatch = content.match(/{.*}/s);
584+
if (jsonStringMatch) {
585+
const parsed = JSON.parse(jsonStringMatch[0].replace(/'/g, '"'));
586+
department = parsed.department;
587+
}
588+
} catch (err) {
589+
console.log('Failed to parse JSON from TogetherAI API response:', err);
590+
// fallback: try to extract department manually
591+
const match = content.match(/"department"\s*:\s*"([^"]+)"/);
592+
if (match) {
593+
[, department] = match;
594+
}
595+
}
596+
}
597+
return department || 'Unknown';
598+
};
599+
600+
// System prompt for the classifier
601+
// This is the system prompt that instructs the LLM on how to classify the customer message
602+
// into the appropriate department.
603+
const messageClassifierSystemPrompt = `You are a customer service classifier for an e-commerce platform. Your role is to identify the primary issue described by the customer and return the result in JSON format. Carefully analyze the customer's message and select one of the following departments as the classification result:
604+
605+
Order Tracking and Status
606+
Returns and Refunds
607+
Payments and Billing Issues
608+
Account Management
609+
Product Inquiries
610+
Technical Support
611+
Shipping and Delivery Issues
612+
Promotions and Discounts
613+
Marketplace Seller Support
614+
Feedback and Complaints
615+
616+
Provide the output in this JSON structure:
617+
618+
{
619+
"department": "<selected_department>"
620+
}
621+
Replace <selected_department> with the name of the most relevant department from the list above. If the inquiry spans multiple categories, choose the department that is most likely to address the customer's issue promptly and effectively.`;
622+
623+
// Image Uploade middleware for Camera uploads
624+
const createImageUploader = () => {
625+
const memoryStorage = multer.memoryStorage();
626+
return multer({
627+
storage: memoryStorage,
628+
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB limit
629+
}).single('image');
630+
};
631+
632+
exports.imageUploadMiddleware = (req, res, next) => {
633+
const uploadToMemory = createImageUploader();
634+
uploadToMemory(req, res, (err) => {
635+
if (err) {
636+
console.error('Upload error:', err);
637+
return res.status(500).json({ error: err.message });
638+
}
639+
next();
640+
});
641+
};
642+
643+
const createImageDataUrl = (file) => {
644+
const base64Image = file.buffer.toString('base64');
645+
return `data:${file.mimetype};base64,${base64Image}`;
646+
};
647+
648+
/**
649+
* GET /ai/togetherai-camera
650+
* Together AI Camera Analysis Example
651+
*/
652+
exports.getTogetherAICamera = (req, res) => {
653+
res.render('ai/togetherai-camera', {
654+
title: 'Together.ai Camera Analysis',
655+
togetherAiModel: process.env.TOGETHERAI_VISION_MODEL,
656+
});
657+
};
658+
659+
/**
660+
* POST /ai/togetherai-camera
661+
* Analyze image using Together AI Vision
662+
*/
663+
exports.postTogetherAICamera = async (req, res) => {
664+
if (!req.file) {
665+
return res.status(400).json({ error: 'No image provided' });
666+
}
667+
try {
668+
const togetherAiKey = process.env.TOGETHERAI_API_KEY;
669+
const togetherAiModel = process.env.TOGETHERAI_VISION_MODEL;
670+
if (!togetherAiKey) {
671+
return res.status(500).json({ error: 'TogetherAI API key is not set' });
672+
}
673+
const dataUrl = createImageDataUrl(req.file);
674+
const apiRequestBody = createVisionLLMRequestBody(dataUrl, togetherAiModel);
675+
// console.log('Making Vision API request to Together AI...');
676+
const data = await callTogetherAiApi(apiRequestBody, togetherAiKey);
677+
const analysis = extractVisionAnalysis(data);
678+
// console.log('Vision analysis completed:', analysis);
679+
res.json({ analysis });
680+
} catch (error) {
681+
console.error('Error analyzing image:', error);
682+
res.status(500).json({ error: `Error analyzing image: ${error.message}` });
683+
}
684+
};
685+
686+
/**
687+
* GET /ai/togetherai-classifier
688+
* Together AI / LLM API Example.
689+
*/
690+
exports.getTogetherAIClassifier = (req, res) => {
691+
res.render('ai/togetherai-classifier', {
692+
title: 'Together.ai/LLM Department Classifier',
693+
result: null,
694+
togetherAiModel: process.env.TOGETHERAI_MODEL,
695+
error: null,
696+
input: '',
697+
});
698+
};
699+
700+
/**
701+
* POST /ai/togetherai-classifier
702+
* Together AI API Example.
703+
* - Classifies customer service inquiries into departments.
704+
* - Uses Together AI API with a foundational LLM model to classify the input text.
705+
* - The systemPrompt is the instructions from the developer to the model for processing
706+
* the user input.
707+
*/
708+
exports.postTogetherAIClassifier = async (req, res) => {
709+
const togetherAiKey = process.env.TOGETHERAI_API_KEY;
710+
const togetherAiModel = process.env.TOGETHERAI_MODEL;
711+
const inputText = (req.body.inputText || '').slice(0, 300);
712+
let result = null;
713+
let error = null;
714+
if (!togetherAiKey) {
715+
error = 'TogetherAI API key is not set in environment variables.';
716+
} else if (!togetherAiModel) {
717+
error = 'TogetherAI model is not set in environment variables.';
718+
} else if (!inputText.trim()) {
719+
error = 'Please enter the customer message to classify.';
720+
} else {
721+
try {
722+
const systemPrompt = messageClassifierSystemPrompt; // Your existing system prompt here
723+
const apiRequestBody = createClassifierLLMRequestBody(inputText, togetherAiModel, systemPrompt);
724+
const data = await callTogetherAiApi(apiRequestBody, togetherAiKey);
725+
const content = data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content;
726+
const department = extractClassifierResponse(content);
727+
result = {
728+
department,
729+
raw: content,
730+
systemPrompt,
731+
};
732+
} catch (err) {
733+
console.log('TogetherAI Classifier API Error:', err);
734+
error = 'Failed to call TogetherAI API.';
735+
}
736+
}
737+
738+
res.render('ai/togetherai-classifier', {
739+
title: 'TogetherAI Department Classifier',
740+
result,
741+
error,
742+
input: inputText,
743+
});
744+
};

0 commit comments

Comments
 (0)