Files
lagos-back/src/controllers/chat.controller.ts
T
mstfyldz 0e40aaef96 feat: add case creation endpoint and auto-title from first message
POST /api/cases lets the frontend actually create a new dava dosyası
(no working "Yeni Dosya" flow existed before). chatMessage now
generates a short title from a case's first message (like Claude/
ChatGPT auto-titling) and renames the case, returned as `newTitle`.
2026-08-09 10:34:55 +03:00

200 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Response } from 'express';
import { supabase } from '../lib/supabase';
import { AuthenticatedRequest } from '../middleware/auth';
// Bir dosyanın ilk mesajından kısa bir başlık üretir (Claude/ChatGPT'nin yaptığı gibi).
// Başarısız olursa null döner — çağıran taraf bu durumda "Yeni Sohbet" başlığını korur,
// asıl sohbet cevabını etkilemez.
async function generateCaseTitle(firstMessage: string): Promise<string | null> {
try {
const aiModel = process.env.AI_MODEL || 'mizan-fixed';
const runpodUrl = process.env.RUNPOD_API_BASE_URL || 'https://q190env94stwis-11434.proxy.runpod.net';
const response = await fetch(`${runpodUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: aiModel,
messages: [
{
role: 'system',
content: 'Kullanıcının hukuki sorusunu en fazla 5 kelimelik, kısa ve açıklayıcı bir Türkçe başlığa çevir. Sadece başlığı yaz; tırnak işareti, noktalama veya başka açıklama ekleme.'
},
{ role: 'user', content: firstMessage.slice(0, 2000) },
],
think: false,
stream: false,
}),
});
if (!response.ok) return null;
const data = await response.json();
const raw = (data.message?.content || '').toString().trim().replace(/^["'“”]+|["'“”]+$/g, '');
if (!raw) return null;
return raw.slice(0, 80);
} catch (err) {
console.error('Generate Case Title Error:', err);
return null;
}
}
export const deleteChatHistory = async (req: AuthenticatedRequest, res: Response) => {
try {
const { caseId } = req.params;
if (!caseId || caseId === 'general') {
return res.status(400).json({ error: 'Geçersiz caseId' });
}
// requireAuth zaten bu caseId'nin isteği yapan kullanıcıya ait olduğunu doğruladı.
// Silme işlemi burada (backend'de, service_role ile) yapılıyor — frontend'in anon
// client'ı chat_messages'a RLS nedeniyle yazamıyor/silemiyor olabilir.
const { error } = await supabase.from('chat_messages').delete().eq('case_id', caseId);
if (error) throw error;
res.json({ ok: true });
} catch (error: any) {
console.error('Delete Chat History Error:', error);
res.status(500).json({ error: error.message || 'Internal Server Error' });
}
};
export const chatMessage = async (req: AuthenticatedRequest, res: Response) => {
try {
const { caseId } = req.params;
const { content } = req.body;
const userId = req.user?.id;
if (!content || !caseId) {
return res.status(400).json({ error: 'Missing caseId or content' });
}
// "general" = dosyasız/genel sohbet. Şemada chat_messages.case_id NOT NULL bir
// cases FK'si olduğundan bu moddaki mesajları veritabanına yazmıyoruz — sadece
// AI'ye iletip yanıtı doğrudan döndürüyoruz.
if (caseId === 'general') {
const generalSystemPrompt = 'Sen bir hukuk asistanısın. Belirli bir dava dosyasına bağlı olmayan genel hukuki soruları cevaplıyorsun. Emin olmadığın noktalarda bunu açıkça belirt, uydurma bilgi verme.';
const aiModel = process.env.AI_MODEL || 'mizan-fixed';
const runpodUrl = process.env.RUNPOD_API_BASE_URL || 'https://q190env94stwis-11434.proxy.runpod.net';
const response = await fetch(`${runpodUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: aiModel,
messages: [
{ role: 'system', content: generalSystemPrompt },
{ role: 'user', content },
],
think: false,
stream: false,
}),
});
if (!response.ok) {
throw new Error(`AI Model request failed: ${response.statusText}`);
}
const data = await response.json();
const aiContent = data.message?.content || 'Yanıt alınamadı.';
return res.json({
message: { id: `general-${Date.now()}`, case_id: 'general', role: 'assistant', content: aiContent },
});
}
const { data: userMessage, error: userMsgError } = await supabase
.from('chat_messages')
.insert([{ case_id: caseId, user_id: userId, role: 'user', content }])
.select().single();
if (userMsgError) throw userMsgError;
// 1. Dosyaya ait belge analizlerini çek (EKSİK OLAN KISIM)
const { data: analyses } = await supabase
.from('analyses')
.select('summary_json')
.eq('case_id', caseId);
const { data: documents } = await supabase
.from('documents')
.select('filename, extracted_text')
.eq('case_id', caseId);
// 2. Geçmiş sohbet
const { data: pastMessages } = await supabase
.from('chat_messages')
.select('role, content')
.eq('case_id', caseId)
.order('created_at', { ascending: true })
.limit(10);
// 3. Belge bağlamını sistem promptuna göm
let documentContext = '';
if (documents && documents.length > 0) {
documentContext = '\n\nDOSYAYA AİT BELGELER:\n' +
documents.map(d => `--- ${d.filename} ---\n${d.extracted_text}`).join('\n\n');
} else {
documentContext = '\n\nBu dosyaya henüz hiçbir belge yüklenmemiştir. Genel hukuki bilgiyle cevap ver, ama bunu kullanıcıya açıkça belirt.';
}
if (analyses && analyses.length > 0) {
documentContext += '\n\nÖNCEKİ ANALİZ ÖZETİ:\n' + JSON.stringify(analyses[analyses.length - 1].summary_json);
}
const systemPrompt = `Sen bir hukuk asistanısın. Bu dava dosyasıyla ilgili kullanıcının sorularını, aşağıda verilen belge içeriğine dayanarak cevaplıyorsun. Eğer belge yoksa veya soruyu cevaplamaya yetmiyorsa, bunu açıkça belirt, uydurma bilgi verme.${documentContext}`;
const messages: any[] = [{ role: 'system', content: systemPrompt }];
if (pastMessages) {
pastMessages.forEach(m => messages.push({ role: m.role, content: m.content }));
} else {
messages.push({ role: 'user', content });
}
const aiModel = process.env.AI_MODEL || 'mizan-fixed';
// Native endpoint'e geçiş
const runpodUrl = process.env.RUNPOD_API_BASE_URL || 'https://q190env94stwis-11434.proxy.runpod.net';
const response = await fetch(`${runpodUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: aiModel,
messages: messages,
think: false,
stream: false,
}),
});
if (!response.ok) {
throw new Error(`AI Model request failed: ${response.statusText}`);
}
const data = await response.json();
const aiContent = data.message?.content || 'Yanıt alınamadı.';
const { data: aiMessage, error: aiMsgError } = await supabase
.from('chat_messages')
.insert([{ case_id: caseId, user_id: userId, role: 'assistant', content: aiContent }])
.select().single();
if (aiMsgError) throw aiMsgError;
// Bu dosyanın ilk mesajıysa (kullanıcının bu isteklen önce hiç mesajı yoktu),
// ilk mesajdan kısa bir başlık üretip dosyayı yeniden adlandır.
let newTitle: string | null = null;
const isFirstMessage = !pastMessages || pastMessages.length <= 1;
if (isFirstMessage) {
newTitle = await generateCaseTitle(content);
if (newTitle) {
const { error: titleError } = await supabase
.from('cases')
.update({ title: newTitle })
.eq('id', caseId);
if (titleError) {
console.error('Case Title Update Error:', titleError);
newTitle = null;
}
}
}
res.json({ message: aiMessage, newTitle });
} catch (error: any) {
console.error('Chat Error:', error);
res.status(500).json({ error: error.message || 'Internal Server Error' });
}
};