diff --git a/src/controllers/case.controller.ts b/src/controllers/case.controller.ts index 6f21f57..6d75886 100644 --- a/src/controllers/case.controller.ts +++ b/src/controllers/case.controller.ts @@ -1,7 +1,7 @@ import { Response } from 'express'; import { supabase } from '../lib/supabase'; import { AuthenticatedRequest } from '../middleware/auth'; -import { runDocumentAnalysis } from './document.controller'; +import { runDocumentAnalysis, runIddianameAnalysis } from './document.controller'; import { findOrCreateCaseByTitle } from '../lib/caseLookup'; export const createCase = async (req: AuthenticatedRequest, res: Response) => { @@ -28,9 +28,26 @@ export const createCase = async (req: AuthenticatedRequest, res: Response) => { } }; +// Türkçe İ/I harflerinin regex /i bayrağıyla güvenilir eşleşmemesi (Unicode +// case-folding sorunu) yüzünden manuel normalize ediyoruz. +function normalizeTr(s: string): string { + return s.replace(/İ/g, 'i').replace(/I/g, 'i').toLowerCase(); +} + // Dava dosyasındaki TÜM belgelerin çıkarılmış metnini birleştirip tek bir AI // özeti üretir (Dosya Ayrıntı ekranındaki "Özet Hazırla" butonu). uploadDocument'ın // aksine tek bir belgeye değil, dosyanın o anki tüm belgelerine bakıyor. +// +// Dosyada bir iddianame varsa önce SADECE onun üzerinden dar kapsamlı bir ön +// çıkarım yapılıyor (isnat edilen suç, iddia özeti, kritik tarihler); genel +// değerlendirme bu ön çıkarımı bağlam olarak alıp tüm belgelerle birlikte +// üretiliyor — böylece iddianamedeki temel olgular genel değerlendirmenin +// başlangıç noktası oluyor, kaybolmuyor. +// +// Not: "Bu dosyadaki tüm evraklar işlenene kadar bekle" kontrolü burada DEĞİL, +// frontend'de (CaseDetail) yapılıyor — çünkü eklentiden indirilip henüz OCR/kayıt +// kuyruğunda bekleyen evraklar sadece Electron'un yerel durumunda görünüyor, +// backend'in bundan haberi yok. export const summarizeCase = async (req: AuthenticatedRequest, res: Response) => { try { const { caseId } = req.params; @@ -50,6 +67,21 @@ export const summarizeCase = async (req: AuthenticatedRequest, res: Response) => return res.status(400).json({ error: 'Bu dosyada metni çıkarılmış belge yok, özet hazırlanamıyor.' }); } + const iddianameDoc = withText.find((d) => + normalizeTr(d.filename).includes('iddianame') || + normalizeTr(String(d.extracted_text)).slice(0, 500).includes('iddianame') + ); + + let iddianameJson: any = null; + if (iddianameDoc) { + try { + const iddianameResult = await runIddianameAnalysis(String(iddianameDoc.extracted_text)); + iddianameJson = iddianameResult.summaryJson; + } catch (e) { + console.error('İddianame ön analizi başarısız, genel değerlendirmeye iddianamesiz devam ediliyor:', e); + } + } + // Tüm belgeleri modele gönderiyoruz; runDocumentAnalysis toplamı ~15000 karaktere // kırpıyor, o yüzden belge sayısına göre kabaca dengeli bir pay ayırıyoruz. const perDocLimit = Math.max(1000, Math.floor(15000 / withText.length)); @@ -57,7 +89,15 @@ export const summarizeCase = async (req: AuthenticatedRequest, res: Response) => .map((d) => `--- ${d.filename} ---\n${String(d.extracted_text).slice(0, perDocLimit)}`) .join('\n\n'); - const { summaryJson, aiModel } = await runDocumentAnalysis(combinedText); + const analysisInput = iddianameJson + ? `İDDİANAME ÖN ANALİZİ (genel değerlendirmeyi buna dayandır):\n${JSON.stringify(iddianameJson)}\n\n---\n\nDOSYADAKİ TÜM BELGELER:\n${combinedText}` + : combinedText; + + const { summaryJson, aiModel } = await runDocumentAnalysis(analysisInput); + if (iddianameJson) { + summaryJson.isnatEdilenSuc = iddianameJson.isnatEdilenSuc || null; + summaryJson.iddianameOzeti = iddianameJson.iddiaOzeti || null; + } const { data: analysisRecord, error: analysisError } = await supabase .from('analyses') diff --git a/src/controllers/chat.controller.ts b/src/controllers/chat.controller.ts index 44b5ff9..5cffc74 100644 --- a/src/controllers/chat.controller.ts +++ b/src/controllers/chat.controller.ts @@ -1,14 +1,14 @@ import { Response } from 'express'; import { supabase } from '../lib/supabase'; import { AuthenticatedRequest } from '../middleware/auth'; +import { getAiConfig } from '../lib/aiConfig'; // 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 { try { - const aiModel = process.env.AI_MODEL || 'mizan-fixed'; - const runpodUrl = process.env.RUNPOD_API_BASE_URL || 'https://q190env94stwis-11434.proxy.runpod.net'; + const { runpodUrl, aiModel } = getAiConfig(); const response = await fetch(`${runpodUrl}/api/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -69,8 +69,7 @@ export const chatMessage = async (req: AuthenticatedRequest, res: Response) => { // 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 { runpodUrl, aiModel } = getAiConfig(); const response = await fetch(`${runpodUrl}/api/chat`, { method: 'POST', @@ -145,9 +144,7 @@ export const chatMessage = async (req: AuthenticatedRequest, res: Response) => { 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 { runpodUrl, aiModel } = getAiConfig(); const response = await fetch(`${runpodUrl}/api/chat`, { method: 'POST', diff --git a/src/controllers/document.controller.ts b/src/controllers/document.controller.ts index 8c9f480..cd1129a 100644 --- a/src/controllers/document.controller.ts +++ b/src/controllers/document.controller.ts @@ -4,12 +4,14 @@ import { AuthenticatedRequest } from '../middleware/auth'; import * as officeParser from 'officeparser'; import Tesseract from 'tesseract.js'; import { findOrCreateCaseByTitle } from '../lib/caseLookup'; +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: - - "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 @@ -17,12 +19,20 @@ const ANALYSIS_SYSTEM_PROMPT = ` Sadece ve sadece JSON döndür, başka hiçbir açıklama ekleme. `; -// uploadDocument, registerLocalDocument ve summarizeCase'in (case.controller.ts) -// ortak kullandığı AI analiz adımı: çıkarılmış metni modele gönderip yapılandırılmış -// özet JSON'u üretir. -export 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'; +// 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. + `; + +async function callAiModel(systemPrompt: string, userText: string): Promise<{ resultJson: any; aiModel: string }> { + const { runpodUrl, aiModel } = getAiConfig(); const response = await fetch(`${runpodUrl}/v1/chat/completions`, { method: 'POST', @@ -30,8 +40,8 @@ export async function runDocumentAnalysis(extractedText: string): Promise<{ summ body: JSON.stringify({ model: aiModel, messages: [ - { role: 'system', content: ANALYSIS_SYSTEM_PROMPT }, - { role: 'user', content: extractedText.substring(0, 15000) } + { role: 'system', content: systemPrompt }, + { role: 'user', content: userText.substring(0, 15000) } ], think: false, stream: false, @@ -44,14 +54,27 @@ export async function runDocumentAnalysis(extractedText: string): Promise<{ summ const data = await response.json(); const aiContent = data.message?.content || (data.choices && data.choices[0]?.message?.content); - let summaryJson: any = {}; + let resultJson: any = {}; try { - summaryJson = JSON.parse(aiContent || '{}'); + resultJson = 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 }; + resultJson = { error: 'AI did not return valid JSON', raw: aiContent }; } - return { summaryJson, aiModel }; + return { resultJson, aiModel }; +} + +// uploadDocument, registerLocalDocument ve summarizeCase'in (case.controller.ts) +// ortak kullandığı genel 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 }; } // UYAP Evrak İndirici eklentisinden gelen, Electron ana sürecinde YERELDE metni diff --git a/src/controllers/precedent.controller.ts b/src/controllers/precedent.controller.ts index 24d36b9..cca3a0d 100644 --- a/src/controllers/precedent.controller.ts +++ b/src/controllers/precedent.controller.ts @@ -1,5 +1,6 @@ import { Request, Response } from 'express'; import { AuthenticatedRequest } from '../middleware/auth'; +import { getAiConfig } from '../lib/aiConfig'; export const searchPrecedents = async (req: Request, res: Response) => { try { @@ -13,15 +14,15 @@ export const searchPrecedents = async (req: Request, res: Response) => { // Eğer sorgu 3 kelimeden uzunsa (doğal dil sorusu ise), AI ile anahtar kelimelere dönüştür if (phrase.split(' ').length > 3) { - const aiModel = process.env.AI_MODEL || 'mizan-fixed'; - const runpodUrl = process.env.RUNPOD_API_BASE_URL || 'https://q190env94stwis-11434.proxy.runpod.net'; - - const systemPrompt = `Sen uzman bir Türk avukatısın. Kullanıcının uzun ve doğal dille yazdığı hukuki soruyu, Yargıtay içtihat arama motorunda en iyi sonucu verecek şekilde 2 ila en fazla 4 temel hukuki terime/anahtar kelimeye çevir. + const systemPrompt = `Sen uzman bir Türk avukatısın. Kullanıcının uzun ve doğal dille yazdığı hukuki soruyu, Yargıtay içtihat arama motorunda en iyi sonucu verecek şekilde 2 ila en fazla 4 temel hukuki terime/anahtar kelimeye çevir. SADECE anahtar kelimeleri aralarına boşluk koyarak yaz. Noktalama işareti, açıklama veya ek kelimeler (ve, ile, vb.) kullanma. Örnek Kullanıcı: İşveren tarafından performans düşüklüğü gerekçesiyle iş sözleşmesi feshedilen işçinin işe iade davası açması halinde ispat yükü kime aittir Örnek Cevap: performans düşüklüğü ispat yükü işe iade`; + // AI ile sorgu optimizasyonu isteğe bağlı bir iyileştirme — config eksikse veya + // istek başarısız olursa arama yine de optimize edilmemiş phrase ile devam eder. try { + const { runpodUrl, aiModel } = getAiConfig(); const aiResponse = await fetch(`${runpodUrl}/v1/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -112,8 +113,7 @@ export const analyzePrecedents = async (req: AuthenticatedRequest, res: Response const systemPrompt = 'Sen uzman bir Türk avukatısın. Kullanıcının anlattığı hukuki durumu, aşağıda listelenen emsal karar arama sonuçları ışığında kısaca değerlendir. Hangi kararların duruma daha yakın olabileceğini belirt ve olası bir hukuki değerlendirme sun. Emin olmadığın noktalarda bunu açıkça belirt, uydurma bilgi verme. Cevabın yaklaşık 150-250 kelime olsun.'; const userPrompt = `DURUM:\n${String(situation).slice(0, 3000)}\n\nBULUNAN KARARLAR:\n${listText}`; - const aiModel = process.env.AI_MODEL || 'mizan-fixed'; - const runpodUrl = process.env.RUNPOD_API_BASE_URL || 'https://q190env94stwis-11434.proxy.runpod.net'; + const { runpodUrl, aiModel } = getAiConfig(); const response = await fetch(`${runpodUrl}/api/chat`, { method: 'POST', diff --git a/src/lib/aiConfig.ts b/src/lib/aiConfig.ts new file mode 100644 index 0000000..309c802 --- /dev/null +++ b/src/lib/aiConfig.ts @@ -0,0 +1,12 @@ +// AI model altyapısının adresini kod içinde sabit (hardcoded) tutmuyoruz — gerçek +// RunPod proxy adresi repo'da (git geçmişinde) görünür olur, bu da yetkisiz erişim/ +// kötüye kullanım riski taşır. Ortam değişkeni tanımlı değilse sessizce gerçek bir +// adrese düşmek yerine açıkça hata veriyoruz. +export function getAiConfig() { + const runpodUrl = process.env.RUNPOD_API_BASE_URL; + if (!runpodUrl) { + throw new Error('RUNPOD_API_BASE_URL ortam değişkeni tanımlı değil.'); + } + const aiModel = process.env.AI_MODEL || 'mizan-fixed'; + return { runpodUrl, aiModel }; +} diff --git a/src/lib/openai.ts b/src/lib/openai.ts index df2d6b2..0ae88ed 100644 --- a/src/lib/openai.ts +++ b/src/lib/openai.ts @@ -15,11 +15,11 @@ dotenv.config(); // ---------------------------------------------- // --- MİZAN (RUNPOD) CONFIGURATION --- -const runpodBaseUrl = process.env.RUNPOD_API_BASE_URL || 'https://q190env94stwis-11434.proxy.runpod.net/v1'; +// Gerçek adresi kod içinde sabit tutmuyoruz (bkz. lib/aiConfig.ts) — sadece env'den okunuyor. +const runpodBaseUrl = process.env.RUNPOD_API_BASE_URL ? `${process.env.RUNPOD_API_BASE_URL}/v1` : undefined; const runpodApiKey = process.env.RUNPOD_API_KEY || 'dummy-key'; -export const openai = new OpenAI({ - baseURL: runpodBaseUrl, - apiKey: runpodApiKey, -}); +export const openai = runpodBaseUrl + ? new OpenAI({ baseURL: runpodBaseUrl, apiKey: runpodApiKey }) + : null; // ------------------------------------