feat: narrative "Dosya Özeti" report instead of field-by-field JSON

summarizeCase now produces a full Markdown narrative report (Olayın
Özeti, Yargılama Süreci ve Kararlar, Taraf İddia/Savunmaları, Önemli
Bulgular ve Belgeler, Sonuç) instead of short structured fields - the
iddianame pre-analysis (when found) still runs first and is fed in as
context. The raw AI text is stored as {ozet: string} rather than
JSON-parsed, since asking the model to escape a multi-paragraph report
inside JSON was fragile.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
mstfyldz
2026-08-09 20:09:31 +03:00
co-authored by Claude Sonnet 5
parent c939dcf564
commit 50f6cbb1b2
2 changed files with 60 additions and 28 deletions
+13 -17
View File
@@ -1,7 +1,7 @@
import { Response } from 'express'; import { Response } from 'express';
import { supabase } from '../lib/supabase'; import { supabase } from '../lib/supabase';
import { AuthenticatedRequest } from '../middleware/auth'; import { AuthenticatedRequest } from '../middleware/auth';
import { runDocumentAnalysis, runIddianameAnalysis } from './document.controller'; import { runIddianameAnalysis, runCaseNarrativeAnalysis } from './document.controller';
import { findOrCreateCaseByTitle } from '../lib/caseLookup'; import { findOrCreateCaseByTitle } from '../lib/caseLookup';
export const createCase = async (req: AuthenticatedRequest, res: Response) => { export const createCase = async (req: AuthenticatedRequest, res: Response) => {
@@ -34,15 +34,15 @@ function normalizeTr(s: string): string {
return s.replace(/İ/g, 'i').replace(/I/g, 'i').toLowerCase(); return s.replace(/İ/g, 'i').replace(/I/g, 'i').toLowerCase();
} }
// Dava dosyasındaki TÜM belgelerin çıkarılmış metnini birleştirip tek bir AI // Dava dosyasındaki TÜM belgelerin çıkarılmış metnini birleştirip kapsamlı, anlatı
// özeti üretir (Dosya Ayrıntı ekranındaki "Özet Hazırla" butonu). uploadDocument'ın // biçiminde bir "Dosya Özeti" raporu üretir (Dosya Ayrıntı ekranındaki "Özet Hazırla"
// aksine tek bir belgeye değil, dosyanın o anki tüm belgelerine bakıyor. // 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 // 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 // çıkarım yapılıyor (isnat edilen suç, iddia özeti, kritik tarihler); anlatı
// değerlendirme bu ön çıkarımı bağlam olarak alıp tüm belgelerle birlikte // raporu bu ön çıkarımı bağlam olarak alıp tüm belgelerle birlikte üretiliyor —
// üretiliyor — böylece iddianamedeki temel olgular genel değerlendirmenin // böylece iddianamedeki temel olgular raporun başlangıç noktası oluyor.
// başlangıç noktası oluyor, kaybolmuyor.
// //
// Not: "Bu dosyadaki tüm evraklar işlenene kadar bekle" kontrolü burada DEĞİL, // 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 // frontend'de (CaseDetail) yapılıyor — çünkü eklentiden indirilip henüz OCR/kayıt
@@ -78,26 +78,22 @@ export const summarizeCase = async (req: AuthenticatedRequest, res: Response) =>
const iddianameResult = await runIddianameAnalysis(String(iddianameDoc.extracted_text)); const iddianameResult = await runIddianameAnalysis(String(iddianameDoc.extracted_text));
iddianameJson = iddianameResult.summaryJson; iddianameJson = iddianameResult.summaryJson;
} catch (e) { } catch (e) {
console.error('İddianame ön analizi başarısız, genel değerlendirmeye iddianamesiz devam ediliyor:', e); console.error('İddianame ön analizi başarısız, rapora iddianamesiz devam ediliyor:', e);
} }
} }
// Tüm belgeleri modele gönderiyoruz; runDocumentAnalysis toplamı ~15000 karaktere // Tüm belgeleri modele gönderiyoruz; ~15000 karaktere kırpılıyor, o yüzden belge
// kırpıyor, o yüzden belge sayısına göre kabaca dengeli bir pay ayırıyoruz. // sayısına göre kabaca dengeli bir pay ayırıyoruz.
const perDocLimit = Math.max(1000, Math.floor(15000 / withText.length)); const perDocLimit = Math.max(1000, Math.floor(15000 / withText.length));
const combinedText = withText const combinedText = withText
.map((d) => `--- ${d.filename} ---\n${String(d.extracted_text).slice(0, perDocLimit)}`) .map((d) => `--- ${d.filename} ---\n${String(d.extracted_text).slice(0, perDocLimit)}`)
.join('\n\n'); .join('\n\n');
const analysisInput = iddianameJson 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}` ? `İDDİANAME ÖN ANALİZİ (rapora bunu öncelikli bağlam olarak yansıt):\n${JSON.stringify(iddianameJson)}\n\n---\n\nDOSYADAKİ TÜM BELGELER:\n${combinedText}`
: combinedText; : combinedText;
const { summaryJson, aiModel } = await runDocumentAnalysis(analysisInput); const { summaryJson, aiModel } = await runCaseNarrativeAnalysis(analysisInput);
if (iddianameJson) {
summaryJson.isnatEdilenSuc = iddianameJson.isnatEdilenSuc || null;
summaryJson.iddianameOzeti = iddianameJson.iddiaOzeti || null;
}
const { data: analysisRecord, error: analysisError } = await supabase const { data: analysisRecord, error: analysisError } = await supabase
.from('analyses') .from('analyses')
+47 -11
View File
@@ -31,7 +31,44 @@ const IDDIANAME_SYSTEM_PROMPT = `
Sadece ve sadece JSON döndür, başka hiçbir açıklama ekleme. 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 }> { 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 };
}
async function callAiModelRaw(systemPrompt: string, userText: string): Promise<{ aiContent: string; aiModel: string }> {
const { runpodUrl, aiModel } = getAiConfig(); const { runpodUrl, aiModel } = getAiConfig();
const response = await fetch(`${runpodUrl}/v1/chat/completions`, { const response = await fetch(`${runpodUrl}/v1/chat/completions`, {
@@ -53,19 +90,11 @@ async function callAiModel(systemPrompt: string, userText: string): Promise<{ re
} }
const data = await response.json(); const data = await response.json();
const aiContent = data.message?.content || (data.choices && data.choices[0]?.message?.content); const aiContent = data.message?.content || (data.choices && data.choices[0]?.message?.content) || '';
let resultJson: any = {}; return { aiContent: String(aiContent).trim(), aiModel };
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 };
} }
// uploadDocument, registerLocalDocument ve summarizeCase'in (case.controller.ts) // uploadDocument, registerLocalDocument'in kullandığı genel (kısa, alan-alan) analiz adımı.
// ortak kullandığı genel analiz adımı.
export async function runDocumentAnalysis(extractedText: string): Promise<{ summaryJson: any; aiModel: string }> { export async function runDocumentAnalysis(extractedText: string): Promise<{ summaryJson: any; aiModel: string }> {
const { resultJson, aiModel } = await callAiModel(ANALYSIS_SYSTEM_PROMPT, extractedText); const { resultJson, aiModel } = await callAiModel(ANALYSIS_SYSTEM_PROMPT, extractedText);
return { summaryJson: resultJson, aiModel }; return { summaryJson: resultJson, aiModel };
@@ -77,6 +106,13 @@ export async function runIddianameAnalysis(extractedText: string): Promise<{ sum
return { summaryJson: resultJson, aiModel }; 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);
return { summaryJson: { ozet: aiContent }, aiModel };
}
// UYAP Evrak İndirici eklentisinden gelen, Electron ana sürecinde YERELDE metni // 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 — // çı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. // sadece çıkarılan metin (JSON) geliyor, Supabase Storage'a hiçbir yükleme yapılmıyor.