diff --git a/src/controllers/document.controller.ts b/src/controllers/document.controller.ts index ee80272..09b0fc6 100644 --- a/src/controllers/document.controller.ts +++ b/src/controllers/document.controller.ts @@ -5,6 +5,128 @@ import * as officeParser from 'officeparser'; import Tesseract from 'tesseract.js'; const heicConvert = require('heic-convert'); +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: + - "taraflar": (string) Davacı ve Davalı bilgileri + - "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. + `; + +// uploadDocument ve registerLocalDocument'in ortak kullandığı AI analiz adımı: +// çıkarılmış metni modele gönderip yapılandırılmış özet JSON'u üretir. +async function runDocumentAnalysis(extractedText: string): Promise<{ summaryJson: any; aiModel: string }> { + 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}/v1/chat/completions`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model: aiModel, + messages: [ + { role: 'system', content: ANALYSIS_SYSTEM_PROMPT }, + { role: 'user', content: extractedText.substring(0, 15000) } + ], + 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 || (data.choices && data.choices[0]?.message?.content); + let summaryJson: any = {}; + try { + summaryJson = JSON.parse(aiContent || '{}'); + } catch (e) { + console.error('Failed to parse AI response as JSON:', aiContent); + summaryJson = { error: 'AI did not return valid JSON', raw: aiContent }; + } + return { summaryJson, 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.' }); + } + + // Dava başlığına göre mevcut case'i bul (case-insensitive), yoksa oluştur. + const { data: existing } = await supabase + .from('cases') + .select('id, title') + .eq('user_id', userId) + .ilike('title', case_title) + .limit(1) + .maybeSingle(); + + let caseId: string; + if (existing) { + caseId = existing.id; + } else { + const { data: newCase, error: caseError } = await supabase + .from('cases') + .insert([{ title: String(case_title).slice(0, 200), user_id: userId }]) + .select('id') + .single(); + if (caseError) throw caseError; + caseId = newCase.id; + } + + 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' }); + } +}; + export const uploadDocument = async (req: AuthenticatedRequest, res: Response) => { try { const file = req.file; @@ -39,7 +161,10 @@ export const uploadDocument = async (req: AuthenticatedRequest, res: Response) = } else { try { // officeParser natively supports pdf, docx, doc, rtf, txt, etc. - extractedText = (await officeParser.parseOffice(file.buffer)) as unknown as string; + // 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); + extractedText = typeof result === 'string' ? result : (result?.toText ? result.toText() : String(result || '')); } catch (err: any) { console.error('officeParser Error:', err); return res.status(400).json({ error: 'Dosya formatı okunamadı veya desteklenmiyor.' }); @@ -78,49 +203,7 @@ export const uploadDocument = async (req: AuthenticatedRequest, res: Response) = if (docError) throw docError; // 3. DeepSeek (OpenAI) ile Hukuki Analiz Çıkarımı - // Promptu tasarlıyoruz. JSON dönmesini istiyoruz. - const systemPrompt = ` - 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: - - "taraflar": (string) Davacı ve Davalı bilgileri - - "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. - `; - - 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}/v1/chat/completions`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - model: aiModel, - messages: [ - { role: 'system', content: systemPrompt }, - { role: 'user', content: extractedText.substring(0, 15000) } - ], - think: false, - stream: false, - }), - }); - - if (!response.ok) { - throw new Error(`AI Model request failed: ${response.statusText}`); - } - - const data = await response.json(); - // Support both custom (data.message.content) and OpenAI standard (data.choices[0].message.content) formats just in case - const aiContent = data.message?.content || (data.choices && data.choices[0]?.message?.content); - let summaryJson = {}; - try { - summaryJson = JSON.parse(aiContent || '{}'); - } catch (e) { - console.error('Failed to parse AI response as JSON:', aiContent); - summaryJson = { error: 'AI did not return valid JSON', raw: aiContent }; - } + const { summaryJson, aiModel } = await runDocumentAnalysis(extractedText); // 4. Analiz sonucunu veritabanına kaydet (analyses tablosu) const { data: analysisRecord, error: analysisError } = await supabase diff --git a/src/routes/document.routes.ts b/src/routes/document.routes.ts index 1adff95..1fc2175 100644 --- a/src/routes/document.routes.ts +++ b/src/routes/document.routes.ts @@ -1,11 +1,12 @@ import { Router } from 'express'; import multer from 'multer'; import { requireAuth } from '../middleware/auth'; -import { uploadDocument } from '../controllers/document.controller'; +import { uploadDocument, registerLocalDocument } from '../controllers/document.controller'; const router = Router(); const upload = multer({ storage: multer.memoryStorage() }); router.post('/upload', upload.single('file'), requireAuth, uploadDocument); +router.post('/register-local', requireAuth, registerLocalDocument); export default router;