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: } 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 { 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' }); } };