- 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>
276 lines
12 KiB
TypeScript
276 lines
12 KiB
TypeScript
import { Response } from 'express';
|
||
import { supabase } from '../lib/supabase';
|
||
import { AuthenticatedRequest } from '../middleware/auth';
|
||
import * as officeParser from 'officeparser';
|
||
import Tesseract from 'tesseract.js';
|
||
import { findOrCreateCaseByTitle } from '../lib/caseLookup';
|
||
import { callOllama } from '../lib/aiClient';
|
||
import { getAiConfig } from '../lib/aiConfig';
|
||
const heicConvert = require('heic-convert');
|
||
|
||
// "taraflar" (davacı/davalı/sanık) burada YOK — o bilgiyi UYAP Evrak İndirici
|
||
// eklentisinden gerçek veri olarak alıyoruz (cases.parties), AI'ye tahmin ettirmiyoruz.
|
||
const ANALYSIS_SYSTEM_PROMPT = `
|
||
Sen uzman bir Türk avukatısın. Sana verilen dava dosyasını/belgeyi okuyup analiz edeceksin.
|
||
Çıktıyı KESİNLİKLE geçerli bir JSON formatında vermelisin. JSON şu anahtarları içermelidir:
|
||
- "tarihler": (string) Belgedeki kritik tarihler (Dava tarihi, tebliğ vb.)
|
||
- "hukukiDayanak": (string) Belgedeki ilgili kanun maddeleri ve hukuki temeller
|
||
- "ilkDegerlendirme": (string) Dosyaya dair ilk stratejik ve hukuki değerlendirmen
|
||
|
||
Sadece ve sadece JSON döndür, başka hiçbir açıklama ekleme.
|
||
`;
|
||
|
||
// summarizeCase (case.controller.ts) dosyada iddianame bulursa önce SADECE onun
|
||
// üzerinden bir ön çıkarım yapıyor, genel değerlendirme buna dayanıyor.
|
||
const IDDIANAME_SYSTEM_PROMPT = `
|
||
Sen uzman bir Türk ceza avukatısın. Sana bir iddianame metni verilecek.
|
||
Çıktıyı KESİNLİKLE geçerli bir JSON formatında ver. JSON şu anahtarları içermelidir:
|
||
- "isnatEdilenSuc": (string) İsnat edilen suç(lar) ve ilgili kanun maddeleri
|
||
- "iddiaOzeti": (string) İddia makamının olay örgüsü, delilleri ve talebinin özeti
|
||
- "onemliTarihler": (string) İddianamedeki kritik tarihler
|
||
|
||
Sadece ve sadece JSON döndür, başka hiçbir açıklama ekleme.
|
||
`;
|
||
|
||
// Dosya Ayrıntı ekranındaki "Dosya Özeti" kartı için — kısa alan-alan JSON yerine
|
||
// verilen örnekteki gibi kapsamlı, bölümlere ayrılmış bir anlatı raporu istiyoruz.
|
||
// Serbest metin JSON içine gömülüp modelin onu doğru kaçışlaması (escape) riskli
|
||
// olduğundan, bunun çıktısını JSON olarak PARSE ETMİYORUZ — ham metni alıp
|
||
// { ozet: <metin> } olarak biz sarıyoruz (bkz. callAiModelRaw / runCaseNarrativeAnalysis).
|
||
const CASE_NARRATIVE_SYSTEM_PROMPT = `
|
||
Sen uzman bir Türk avukatısın. Sana bir dava dosyasındaki TÜM belgelerin metni
|
||
verilecek (varsa önce bir iddianame ön analizi de eklenmiş olabilir, buna öncelik ver).
|
||
Bu dosya hakkında kapsamlı, iyi yapılandırılmış bir "Dosya Özeti" raporu yaz.
|
||
|
||
Rapor Markdown formatında olsun ve şu yapıyı izlesin:
|
||
- Kısa bir giriş paragrafı: mahkeme/daire, esas no'su ve konusu.
|
||
- "1. Olayın/Uyuşmazlığın Özeti" — ne olduğunu anlat.
|
||
- "2. Yargılama Süreci ve Kararlar" — ilk derece, istinaf/temyiz, bozma vb. aşamaları madde madde anlat.
|
||
- "3. Tarafların İddia ve Savunmaları" — davacı/davalı ya da sanık/müştekinin ne dediğini özetle.
|
||
- "4. Önemli Bulgular ve Belgeler" — bilirkişi raporu, banka kayıtları, HTS/baz istasyonu, tanık ifadeleri gibi somut delilleri madde madde listele.
|
||
- Kısa bir "Sonuç" paragrafı: dosyanın şu anki durumu ve beklenen sonraki adım.
|
||
|
||
Dava türüne göre terminolojiyi uyarla (ceza davasında "sanık/müşteki", hukuk davasında
|
||
"davacı/davalı", icra dosyasında "alacaklı/borçlu" gibi). SADECE dosyada gerçekten
|
||
geçen bilgileri kullan, uydurma bilgi ekleme; bir bölüm için yeterli bilgi yoksa o
|
||
bölümü kısa geç veya atla. SADECE Markdown metni döndür — JSON, kod bloğu işareti
|
||
veya başka bir açıklama ekleme.
|
||
`;
|
||
|
||
async function callAiModel(systemPrompt: string, userText: string): Promise<{ resultJson: any; aiModel: string }> {
|
||
const { aiContent, aiModel } = await callAiModelRaw(systemPrompt, userText);
|
||
let resultJson: any = {};
|
||
try {
|
||
resultJson = JSON.parse(aiContent || '{}');
|
||
} catch (e) {
|
||
console.error('Failed to parse AI response as JSON:', aiContent);
|
||
resultJson = { error: 'AI did not return valid JSON', raw: aiContent };
|
||
}
|
||
return { resultJson, aiModel };
|
||
}
|
||
|
||
export async function callAiModelRaw(systemPrompt: string, userText: string, numPredict = 1500): Promise<{ aiContent: string; aiModel: string }> {
|
||
const { aiModel } = getAiConfig();
|
||
const aiContent = await callOllama([
|
||
{ role: 'system', content: systemPrompt },
|
||
{ role: 'user', content: userText.substring(0, 15000) },
|
||
], { numPredict });
|
||
return { aiContent, aiModel };
|
||
}
|
||
|
||
// uploadDocument, registerLocalDocument'in kullandığı genel (kısa, alan-alan) analiz adımı.
|
||
export async function runDocumentAnalysis(extractedText: string): Promise<{ summaryJson: any; aiModel: string }> {
|
||
const { resultJson, aiModel } = await callAiModel(ANALYSIS_SYSTEM_PROMPT, extractedText);
|
||
return { summaryJson: resultJson, aiModel };
|
||
}
|
||
|
||
// summarizeCase'in iddianame ön-analizi için kullandığı, dar kapsamlı adım.
|
||
export async function runIddianameAnalysis(extractedText: string): Promise<{ summaryJson: any; aiModel: string }> {
|
||
const { resultJson, aiModel } = await callAiModel(IDDIANAME_SYSTEM_PROMPT, extractedText);
|
||
return { summaryJson: resultJson, aiModel };
|
||
}
|
||
|
||
// summarizeCase'in Dosya Özeti raporu için kullandığı adım — çıktı serbest Markdown
|
||
// metni, JSON'a çevrilmeye çalışılmıyor.
|
||
export async function runCaseNarrativeAnalysis(combinedText: string): Promise<{ summaryJson: any; aiModel: string }> {
|
||
const { aiContent, aiModel } = await callAiModelRaw(CASE_NARRATIVE_SYSTEM_PROMPT, combinedText, 2500);
|
||
return { summaryJson: { ozet: aiContent }, aiModel };
|
||
}
|
||
|
||
// UYAP Evrak İndirici eklentisinden gelen, Electron ana sürecinde YERELDE metni
|
||
// çıkarılmış belgeleri kaydeder. Ham dosya baytları bu isteğe hiç dahil değil —
|
||
// sadece çıkarılan metin (JSON) geliyor, Supabase Storage'a hiçbir yükleme yapılmıyor.
|
||
export const registerLocalDocument = async (req: AuthenticatedRequest, res: Response) => {
|
||
try {
|
||
const userId = req.user?.id;
|
||
const { case_title, filename, extracted_text } = req.body || {};
|
||
|
||
if (!case_title || !filename || typeof extracted_text !== 'string') {
|
||
return res.status(400).json({ error: 'case_title, filename ve extracted_text zorunludur.' });
|
||
}
|
||
|
||
const caseId = await findOrCreateCaseByTitle(userId as string, case_title);
|
||
|
||
const { data: documentRecord, error: docError } = await supabase
|
||
.from('documents')
|
||
.insert([{
|
||
case_id: caseId,
|
||
user_id: userId,
|
||
storage_path: 'local', // ham dosya sadece kullanıcının cihazında; Storage'da bir karşılığı yok
|
||
filename,
|
||
mime_type: null,
|
||
file_size: extracted_text.length,
|
||
ocr_status: extracted_text ? 'done' : 'failed',
|
||
extracted_text,
|
||
}])
|
||
.select()
|
||
.single();
|
||
if (docError) throw docError;
|
||
|
||
let analysisRecord = null;
|
||
if (extracted_text.trim()) {
|
||
try {
|
||
const { summaryJson, aiModel } = await runDocumentAnalysis(extracted_text);
|
||
const { data: analysis, error: analysisError } = await supabase
|
||
.from('analyses')
|
||
.insert([{ case_id: caseId, document_id: documentRecord.id, summary_json: summaryJson, model_used: aiModel }])
|
||
.select()
|
||
.single();
|
||
if (analysisError) throw analysisError;
|
||
analysisRecord = analysis;
|
||
} catch (analysisErr) {
|
||
// Analiz başarısız olsa da belge zaten kaydedildi — sohbet daha sonra
|
||
// extracted_text'i yine de kullanabilir.
|
||
console.error('Local Document Analysis Error:', analysisErr);
|
||
}
|
||
}
|
||
|
||
res.json({ case_id: caseId, document: documentRecord, analysis: analysisRecord });
|
||
} catch (error: any) {
|
||
console.error('Register Local Document Error:', error);
|
||
res.status(500).json({ error: error.message || 'Internal Server Error' });
|
||
}
|
||
};
|
||
|
||
// uploadDocument ve uploadTemplate'in (template.controller.ts) ortak kullandığı
|
||
// metin çıkarma adımı: resimler için Tesseract OCR, ofis belgeleri için officeParser.
|
||
export async function extractTextFromUploadedFile(file: { buffer: Buffer; mimetype: string; originalname: string }): Promise<string> {
|
||
const mimeType = file.mimetype.toLowerCase();
|
||
|
||
if (mimeType.includes('image/')) {
|
||
let imageBuffer = file.buffer;
|
||
if (mimeType === 'image/heic' || file.originalname.toLowerCase().endsWith('.heic')) {
|
||
imageBuffer = await heicConvert({ buffer: file.buffer, format: 'JPEG', quality: 1 });
|
||
}
|
||
const { data: { text } } = await Tesseract.recognize(imageBuffer, 'tur');
|
||
return text;
|
||
}
|
||
|
||
// officeParser natively supports pdf, docx, doc, rtf, txt, etc.
|
||
// officeparser@7.x parseOffice() bir sonuc objesi donduruyor (duz string degil),
|
||
// duz metin icin .toText() cagirmak gerekiyor.
|
||
const result: any = await officeParser.parseOffice(file.buffer);
|
||
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) => {
|
||
try {
|
||
const file = req.file;
|
||
const { case_id } = req.body;
|
||
const user_id = req.user?.id;
|
||
|
||
if (!file || !case_id || !user_id) {
|
||
return res.status(400).json({ error: 'Missing file or case_id' });
|
||
}
|
||
|
||
let extractedText = '';
|
||
try {
|
||
extractedText = await extractTextFromUploadedFile(file);
|
||
} catch (err: any) {
|
||
console.error('Text Extraction Error:', err);
|
||
return res.status(400).json({ error: 'Dosya formatı okunamadı veya desteklenmiyor.' });
|
||
}
|
||
|
||
// 2. Upload File to Supabase Storage
|
||
const filePath = `${user_id}/${Date.now()}_${file.originalname}`;
|
||
const { error: uploadError } = await supabase.storage
|
||
.from('case-documents')
|
||
.upload(filePath, file.buffer, {
|
||
contentType: file.mimetype,
|
||
});
|
||
|
||
if (uploadError) {
|
||
console.error('Storage Upload Error:', uploadError);
|
||
return res.status(500).json({ error: 'Failed to upload document to storage' });
|
||
}
|
||
|
||
// 3. Veritabanına Dosya Bilgisini Kaydet (documents tablosu)
|
||
const { data: documentRecord, error: docError } = await supabase
|
||
.from('documents')
|
||
.insert([{
|
||
case_id,
|
||
user_id,
|
||
storage_path: filePath,
|
||
filename: file.originalname,
|
||
mime_type: file.mimetype,
|
||
file_size: file.size,
|
||
ocr_status: 'done',
|
||
extracted_text: extractedText
|
||
}])
|
||
.select()
|
||
.single();
|
||
|
||
if (docError) throw docError;
|
||
|
||
// 3. DeepSeek (OpenAI) ile Hukuki Analiz Çıkarımı
|
||
const { summaryJson, aiModel } = await runDocumentAnalysis(extractedText);
|
||
|
||
// 4. Analiz sonucunu veritabanına kaydet (analyses tablosu)
|
||
const { data: analysisRecord, error: analysisError } = await supabase
|
||
.from('analyses')
|
||
.insert([{
|
||
case_id,
|
||
document_id: documentRecord.id,
|
||
summary_json: summaryJson,
|
||
model_used: aiModel
|
||
}])
|
||
.select()
|
||
.single();
|
||
|
||
if (analysisError) throw analysisError;
|
||
|
||
// Başarıyla frontend'e döndür
|
||
res.json({
|
||
message: 'Document processed successfully',
|
||
document: documentRecord,
|
||
analysis: analysisRecord
|
||
});
|
||
} catch (error: any) {
|
||
console.error('Upload Error:', error);
|
||
res.status(500).json({ error: error.message || 'Internal Server Error' });
|
||
}
|
||
};
|