feat: file attachments in chat + document-select for case chat
- Yeni POST /api/documents/extract-text: dosyayı hiçbir yere
kaydetmeden sadece metnini çıkarıp döner (chat ataç özelliği için)
- chatMessage ve sendThreadMessage artık body'de attachments
({filename,text}[]) kabul ediyor; AI bağlamına ekleniyor, kayıtlı
mesaja da "[Ek dosya(lar): ...]" notu düşülüyor
- chatMessage ayrıca document_ids kabul ediyor — dosya sohbetinde
kullanıcının bu mesajda özellikle işaret ettiği mevcut belge(ler)
AI'ye vurgulanıyor
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
029402a620
commit
9618ff4513
@@ -39,6 +39,24 @@ function sseWrite(res: Response, payload: Record<string, unknown>) {
|
|||||||
res.write(`data: ${JSON.stringify(payload)}\n\n`);
|
res.write(`data: ${JSON.stringify(payload)}\n\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ChatAttachment { filename: string; text: string; }
|
||||||
|
|
||||||
|
// Sohbet ekranlarındaki ataç (dosya ekleme) özelliği — dosyanın metni frontend'de
|
||||||
|
// /api/documents/extract-text ile önceden çıkarılıp buraya {filename, text} olarak
|
||||||
|
// geliyor. Ham dosya bu isteğe hiç dahil değil. Kaydedilen mesaj metnine kısa bir
|
||||||
|
// "[Ek dosya(lar): ...]" notu eklenir (geçmişte ne eklendiği görünsün diye); AI'ye
|
||||||
|
// giden bağlama ise dosyanın tam metni eklenir.
|
||||||
|
function buildAttachmentNote(attachments: unknown): string {
|
||||||
|
if (!Array.isArray(attachments) || attachments.length === 0) return '';
|
||||||
|
return `\n\n[Ek dosya(lar): ${attachments.map((a: ChatAttachment) => a?.filename || 'dosya').join(', ')}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAttachmentContext(attachments: unknown): string {
|
||||||
|
if (!Array.isArray(attachments) || attachments.length === 0) return '';
|
||||||
|
return '\n\nKULLANICININ BU MESAJA EKLEDİĞİ DOSYA(LAR):\n' +
|
||||||
|
(attachments as ChatAttachment[]).map(a => `--- ${a?.filename || 'dosya'} ---\n${String(a?.text || '').slice(0, 20000)}`).join('\n\n');
|
||||||
|
}
|
||||||
|
|
||||||
export const deleteChatHistory = async (req: AuthenticatedRequest, res: Response) => {
|
export const deleteChatHistory = async (req: AuthenticatedRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { caseId } = req.params;
|
const { caseId } = req.params;
|
||||||
@@ -62,7 +80,7 @@ export const deleteChatHistory = async (req: AuthenticatedRequest, res: Response
|
|||||||
// yerine yazılırken görür; gerçek üretim süresi aynı kalsa da algılanan hız artar.
|
// yerine yazılırken görür; gerçek üretim süresi aynı kalsa da algılanan hız artar.
|
||||||
export const chatMessage = async (req: AuthenticatedRequest, res: Response) => {
|
export const chatMessage = async (req: AuthenticatedRequest, res: Response) => {
|
||||||
const { caseId } = req.params;
|
const { caseId } = req.params;
|
||||||
const { content } = req.body;
|
const { content, attachments, document_ids } = req.body;
|
||||||
const userId = req.user?.id;
|
const userId = req.user?.id;
|
||||||
|
|
||||||
if (!content || !caseId) {
|
if (!content || !caseId) {
|
||||||
@@ -76,9 +94,10 @@ export const chatMessage = async (req: AuthenticatedRequest, res: Response) => {
|
|||||||
const { data: caseRow } = await supabase.from('cases').select('kind').eq('id', caseId).maybeSingle();
|
const { data: caseRow } = await supabase.from('cases').select('kind').eq('id', caseId).maybeSingle();
|
||||||
const caseKind = caseRow?.kind || 'case';
|
const caseKind = caseRow?.kind || 'case';
|
||||||
|
|
||||||
|
const persistedContent = content + buildAttachmentNote(attachments);
|
||||||
const { error: userMsgError } = await supabase
|
const { error: userMsgError } = await supabase
|
||||||
.from('chat_messages')
|
.from('chat_messages')
|
||||||
.insert([{ case_id: caseId, user_id: userId, role: 'user', content }]);
|
.insert([{ case_id: caseId, user_id: userId, role: 'user', content: persistedContent }]);
|
||||||
if (userMsgError) throw userMsgError;
|
if (userMsgError) throw userMsgError;
|
||||||
|
|
||||||
const { data: analyses } = await supabase
|
const { data: analyses } = await supabase
|
||||||
@@ -88,7 +107,7 @@ export const chatMessage = async (req: AuthenticatedRequest, res: Response) => {
|
|||||||
|
|
||||||
const { data: documents } = await supabase
|
const { data: documents } = await supabase
|
||||||
.from('documents')
|
.from('documents')
|
||||||
.select('filename, extracted_text')
|
.select('id, filename, extracted_text')
|
||||||
.eq('case_id', caseId);
|
.eq('case_id', caseId);
|
||||||
|
|
||||||
const { data: pastMessages } = await supabase
|
const { data: pastMessages } = await supabase
|
||||||
@@ -104,6 +123,15 @@ export const chatMessage = async (req: AuthenticatedRequest, res: Response) => {
|
|||||||
if (documents && documents.length > 0) {
|
if (documents && documents.length > 0) {
|
||||||
documentContext = '\n\nDOSYAYA AİT BELGELER:\n' +
|
documentContext = '\n\nDOSYAYA AİT BELGELER:\n' +
|
||||||
documents.map(d => `--- ${d.filename} ---\n${d.extracted_text}`).join('\n\n');
|
documents.map(d => `--- ${d.filename} ---\n${d.extracted_text}`).join('\n\n');
|
||||||
|
// Kullanıcı sohbet ekranında dosyanın belirli belgelerini seçip vurgulamışsa
|
||||||
|
// (bkz. frontend'deki belge seçici), buna dikkat çekiyoruz — belgeler zaten
|
||||||
|
// yukarıda tam olarak mevcut, bu sadece hangisine öncelik vereceğini belirtiyor.
|
||||||
|
if (Array.isArray(document_ids) && document_ids.length > 0) {
|
||||||
|
const selected = documents.filter(d => document_ids.includes(d.id));
|
||||||
|
if (selected.length > 0) {
|
||||||
|
documentContext += '\n\nKULLANICININ BU MESAJDA ÖZELLİKLE İŞARET ETTİĞİ BELGE(LER): ' + selected.map(d => d.filename).join(', ') + '.';
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} 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.';
|
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.';
|
||||||
}
|
}
|
||||||
@@ -112,13 +140,15 @@ export const chatMessage = async (req: AuthenticatedRequest, res: Response) => {
|
|||||||
documentContext += '\n\nANALİZ ÖZETİ:\n' + JSON.stringify(analyses[analyses.length - 1].summary_json);
|
documentContext += '\n\nANALİZ ÖZETİ:\n' + JSON.stringify(analyses[analyses.length - 1].summary_json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
documentContext += buildAttachmentContext(attachments);
|
||||||
|
|
||||||
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 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: ChatMsg[] = [{ role: 'system', content: systemPrompt }];
|
const messages: ChatMsg[] = [{ role: 'system', content: systemPrompt }];
|
||||||
if (pastMessages && pastMessages.length > 0) {
|
if (pastMessages && pastMessages.length > 0) {
|
||||||
pastMessages.forEach(m => messages.push({ role: m.role, content: m.content }));
|
pastMessages.forEach(m => messages.push({ role: m.role, content: m.content }));
|
||||||
} else {
|
} else {
|
||||||
messages.push({ role: 'user', content });
|
messages.push({ role: 'user', content: persistedContent });
|
||||||
}
|
}
|
||||||
|
|
||||||
startSse(res);
|
startSse(res);
|
||||||
@@ -169,7 +199,7 @@ export const chatMessage = async (req: AuthenticatedRequest, res: Response) => {
|
|||||||
// chatMessage ile aynı SSE akış deseni kullanılır.
|
// chatMessage ile aynı SSE akış deseni kullanılır.
|
||||||
export const sendThreadMessage = async (req: AuthenticatedRequest, res: Response) => {
|
export const sendThreadMessage = async (req: AuthenticatedRequest, res: Response) => {
|
||||||
const { threadId } = req.params;
|
const { threadId } = req.params;
|
||||||
const { content } = req.body;
|
const { content, attachments } = req.body;
|
||||||
const userId = req.user?.id;
|
const userId = req.user?.id;
|
||||||
|
|
||||||
if (!content || !threadId) {
|
if (!content || !threadId) {
|
||||||
@@ -186,9 +216,10 @@ export const sendThreadMessage = async (req: AuthenticatedRequest, res: Response
|
|||||||
return res.status(403).json({ error: 'Bu sohbete erişim yetkiniz yok.' });
|
return res.status(403).json({ error: 'Bu sohbete erişim yetkiniz yok.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const persistedContent = content + buildAttachmentNote(attachments);
|
||||||
const { error: userMsgError } = await supabase
|
const { error: userMsgError } = await supabase
|
||||||
.from('thread_messages')
|
.from('thread_messages')
|
||||||
.insert([{ thread_id: threadId, user_id: userId, role: 'user', content }]);
|
.insert([{ thread_id: threadId, user_id: userId, role: 'user', content: persistedContent }]);
|
||||||
if (userMsgError) throw userMsgError;
|
if (userMsgError) throw userMsgError;
|
||||||
|
|
||||||
const { data: pastMessages } = await supabase
|
const { data: pastMessages } = await supabase
|
||||||
@@ -198,12 +229,12 @@ export const sendThreadMessage = async (req: AuthenticatedRequest, res: Response
|
|||||||
.order('created_at', { ascending: true })
|
.order('created_at', { ascending: true })
|
||||||
.limit(20);
|
.limit(20);
|
||||||
|
|
||||||
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 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.' + buildAttachmentContext(attachments);
|
||||||
const messages: ChatMsg[] = [{ role: 'system', content: generalSystemPrompt }];
|
const messages: ChatMsg[] = [{ role: 'system', content: generalSystemPrompt }];
|
||||||
if (pastMessages && pastMessages.length > 0) {
|
if (pastMessages && pastMessages.length > 0) {
|
||||||
pastMessages.forEach(m => messages.push({ role: m.role, content: m.content }));
|
pastMessages.forEach(m => messages.push({ role: m.role, content: m.content }));
|
||||||
} else {
|
} else {
|
||||||
messages.push({ role: 'user', content });
|
messages.push({ role: 'user', content: persistedContent });
|
||||||
}
|
}
|
||||||
|
|
||||||
startSse(res);
|
startSse(res);
|
||||||
|
|||||||
@@ -173,6 +173,29 @@ export async function extractTextFromUploadedFile(file: { buffer: Buffer; mimety
|
|||||||
return typeof result === 'string' ? result : (result?.toText ? result.toText() : String(result || ''));
|
return typeof result === 'string' ? result : (result?.toText ? result.toText() : String(result || ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sohbet ekranlarındaki (genel Sohbet ve dosya bazlı "AyrisLegal'e Sor") ataç
|
||||||
|
// (dosya ekleme) özelliği için — dosyayı hiçbir yere kaydetmeden sadece metnini
|
||||||
|
// çıkarıp döner, çağıran taraf bu metni chat isteğine "attachments" olarak ekler.
|
||||||
|
export const extractAttachmentText = async (req: AuthenticatedRequest, res: Response) => {
|
||||||
|
try {
|
||||||
|
const file = req.file;
|
||||||
|
if (!file) {
|
||||||
|
return res.status(400).json({ error: 'Dosya bulunamadı.' });
|
||||||
|
}
|
||||||
|
let extractedText = '';
|
||||||
|
try {
|
||||||
|
extractedText = await extractTextFromUploadedFile(file);
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('Attachment Text Extraction Error:', err);
|
||||||
|
return res.status(400).json({ error: 'Dosya formatı okunamadı veya desteklenmiyor.' });
|
||||||
|
}
|
||||||
|
res.json({ filename: file.originalname, extracted_text: extractedText });
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Extract Attachment Text Error:', error);
|
||||||
|
res.status(500).json({ error: error.message || 'Internal Server Error' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const uploadDocument = async (req: AuthenticatedRequest, res: Response) => {
|
export const uploadDocument = async (req: AuthenticatedRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const file = req.file;
|
const file = req.file;
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import multer from 'multer';
|
import multer from 'multer';
|
||||||
import { requireAuth } from '../middleware/auth';
|
import { requireAuth } from '../middleware/auth';
|
||||||
import { uploadDocument, registerLocalDocument } from '../controllers/document.controller';
|
import { uploadDocument, registerLocalDocument, extractAttachmentText } from '../controllers/document.controller';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
const upload = multer({ storage: multer.memoryStorage() });
|
const upload = multer({ storage: multer.memoryStorage() });
|
||||||
|
|
||||||
router.post('/upload', upload.single('file'), requireAuth, uploadDocument);
|
router.post('/upload', upload.single('file'), requireAuth, uploadDocument);
|
||||||
router.post('/register-local', requireAuth, registerLocalDocument);
|
router.post('/register-local', requireAuth, registerLocalDocument);
|
||||||
|
router.post('/extract-text', upload.single('file'), requireAuth, extractAttachmentText);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
Reference in New Issue
Block a user