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:
co-authored by
Claude Sonnet 5
parent
c939dcf564
commit
50f6cbb1b2
@@ -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')
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user