github-actions[bot]
Deploy demo from GitHub Actions - 2025-12-24 02:23:20
6cdce85
import { NextRequest } from 'next/server';
import { createVLMClient } from '@/lib/api/vlm-client';
import { ALLOWED_TOPICS, BLOCKED_INPUT_PATTERNS } from '@/config/constants';
export const maxDuration = 120;
interface MessageContent {
type: 'text' | 'image_url';
text?: string;
image_url?: { url: string };
}
interface ChatMessage {
role: 'system' | 'user' | 'assistant';
content: string | MessageContent[];
}
interface ChatRequestBody {
messages: ChatMessage[];
stream?: boolean;
}
interface ContentValidation {
valid: boolean;
reason?: string;
isOffTopic?: boolean;
}
/**
* Extract text content from a message for validation
*/
function extractTextContent(content: string | MessageContent[]): string {
if (typeof content === 'string') {
return content;
}
return content
.filter((c): c is MessageContent & { type: 'text'; text: string } => c.type === 'text' && !!c.text)
.map(c => c.text)
.join(' ');
}
/**
* Validate user input for malicious patterns and topic relevance
*/
function validateUserInput(text: string): ContentValidation {
const lowerText = text.toLowerCase();
// Check for blocked patterns (prompt injection, harmful content, etc.)
for (const pattern of BLOCKED_INPUT_PATTERNS) {
if (pattern.test(text)) {
return {
valid: false,
reason: "I can't process this request. Please ask a question related to quantum computing, Qiskit, physics, or mathematics.",
};
}
}
// Check message length (prevent abuse)
if (text.length > 10000) {
return {
valid: false,
reason: 'Message too long. Please keep your question under 10,000 characters.',
};
}
// Check if the message contains any relevant topic keywords
// Images are always allowed (circuit diagrams, Bloch spheres, etc.)
const hasImage = text.includes('[IMAGE]') || text.length < 20; // Short messages might be follow-ups
if (!hasImage) {
const words = lowerText.split(/\s+/);
const hasRelevantTopic = ALLOWED_TOPICS.some(topic => {
// Check for whole word or part of compound word
return words.some(word =>
word.includes(topic.toLowerCase()) ||
topic.toLowerCase().includes(word)
);
});
// Also check for common question patterns
const isQuestion = /^(what|how|why|when|where|can|could|would|should|is|are|do|does|explain|describe|help|show|create|implement|write|generate|build|make)/i.test(lowerText.trim());
const hasCodeContext = /```|def\s|import\s|class\s|function|circuit/i.test(text);
// Be permissive: if it's a question or has code context, allow it
// The model will redirect off-topic questions anyway
if (!hasRelevantTopic && !isQuestion && !hasCodeContext && text.length > 50) {
return {
valid: true, // Still valid, but flag as potentially off-topic
isOffTopic: true,
};
}
}
return { valid: true };
}
/**
* Create off-topic response message
*/
function createOffTopicResponse(): string {
return `I'm **Quantum Assistant**, specialized in quantum computing, Qiskit, physics, and related mathematics.
I can help you with:
- 🔬 **Quantum Computing**: Circuits, gates, algorithms, error correction
- 💻 **Qiskit**: Code generation, debugging, best practices
- 📐 **Physics & Math**: Quantum mechanics, linear algebra, probability
- 🤖 **Quantum ML**: Variational algorithms, optimization, hybrid systems
**Please ask a question related to these topics!**
For example:
- "How do I create a Bell state in Qiskit?"
- "Explain the Grover's algorithm"
- "What is quantum entanglement?"`;
}
function isConnectionError(error: unknown): boolean {
if (error instanceof Error) {
const message = error.message.toLowerCase();
const cause = (error as Error & { cause?: Error })?.cause;
if (message.includes('fetch failed') || message.includes('econnrefused')) {
return true;
}
if (cause && 'code' in cause && cause.code === 'ECONNREFUSED') {
return true;
}
}
return false;
}
function createErrorMessage(isConnection: boolean): string {
if (isConnection) {
const modelUrl = process.env.DEMO_MODEL_URL || 'http://localhost:8000/v1';
return `**Model Server Not Available**\n\nCould not connect to the model at:\n\`${modelUrl}\`\n\n**To use the chat feature:**\n1. Start a VLM server (vLLM, Ollama, etc.)\n2. Configure \`.env.local\` with your endpoint:\n\`\`\`\nDEMO_MODEL_URL=http://your-server:port/v1\nDEMO_MODEL_NAME=your-model-name\nDEMO_API_KEY=your-api-key\n\`\`\`\n3. Restart the demo server\n\n*Examples panel still works - try selecting a test sample!*`;
}
return 'An error occurred while processing your request.';
}
export async function POST(request: NextRequest) {
try {
const body: ChatRequestBody = await request.json();
const { messages, stream = true } = body;
if (!messages || !Array.isArray(messages) || messages.length === 0) {
return new Response(
JSON.stringify({ error: 'Invalid request: messages array required' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
// Find the last user message for validation
const userMessages = messages.filter(m => m.role === 'user');
const lastUserMessage = userMessages[userMessages.length - 1];
if (lastUserMessage) {
const userText = extractTextContent(lastUserMessage.content);
const validation = validateUserInput(userText);
// If input is invalid (malicious/harmful), return error
if (!validation.valid && validation.reason) {
const encoder = new TextEncoder();
if (stream) {
const errorStream = new ReadableStream({
start(controller) {
const data = JSON.stringify({ content: validation.reason, done: false });
controller.enqueue(encoder.encode(`data: ${data}\n\n`));
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ done: true })}\n\n`));
controller.close();
},
});
return new Response(errorStream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
});
} else {
return new Response(
JSON.stringify({ content: validation.reason }),
{ headers: { 'Content-Type': 'application/json' } }
);
}
}
}
const client = createVLMClient();
if (stream) {
const encoder = new TextEncoder();
const readableStream = new ReadableStream({
async start(controller) {
try {
for await (const chunk of client.chatStream(messages)) {
const data = JSON.stringify({ content: chunk, done: false });
controller.enqueue(encoder.encode(`data: ${data}\n\n`));
}
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ done: true })}\n\n`));
controller.close();
} catch (error) {
console.error('Stream error:', error);
const isConnection = isConnectionError(error);
const errorMessage = isConnection
? createErrorMessage(true)
: (error instanceof Error ? error.message : 'Stream error occurred');
controller.enqueue(
encoder.encode(`data: ${JSON.stringify({ error: errorMessage, done: true })}\n\n`)
);
controller.close();
}
},
});
return new Response(readableStream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
});
} else {
const response = await client.chat(messages);
return new Response(
JSON.stringify({ content: response }),
{ headers: { 'Content-Type': 'application/json' } }
);
}
} catch (error) {
console.error('Chat API error:', error);
if (isConnectionError(error)) {
return new Response(
JSON.stringify({ error: createErrorMessage(true) }),
{ status: 503, headers: { 'Content-Type': 'application/json' } }
);
}
const errorMessage =
error instanceof Error ? error.message : 'Internal server error';
return new Response(
JSON.stringify({ error: errorMessage }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
}