|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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(' '); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function validateUserInput(text: string): ContentValidation { |
|
|
const lowerText = text.toLowerCase(); |
|
|
|
|
|
|
|
|
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.", |
|
|
}; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (text.length > 10000) { |
|
|
return { |
|
|
valid: false, |
|
|
reason: 'Message too long. Please keep your question under 10,000 characters.', |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const hasImage = text.includes('[IMAGE]') || text.length < 20; |
|
|
|
|
|
if (!hasImage) { |
|
|
const words = lowerText.split(/\s+/); |
|
|
const hasRelevantTopic = ALLOWED_TOPICS.some(topic => { |
|
|
|
|
|
return words.some(word => |
|
|
word.includes(topic.toLowerCase()) || |
|
|
topic.toLowerCase().includes(word) |
|
|
); |
|
|
}); |
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
|
|
|
if (!hasRelevantTopic && !isQuestion && !hasCodeContext && text.length > 50) { |
|
|
return { |
|
|
valid: true, |
|
|
isOffTopic: true, |
|
|
}; |
|
|
} |
|
|
} |
|
|
|
|
|
return { valid: true }; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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' } } |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
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 (!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' } } |
|
|
); |
|
|
} |
|
|
} |
|
|
|