feat: analiz/sohbet/dilekçe endpoint'lerine istekten belge/analiz kabul etme desteği ekle

Yerel-öncelikli veri mimarisi (Faz 2) — case.controller.ts'in 5 analiz
fonksiyonu ve analyzeDigitalIntern artık req.body.documents'ı öncelikli
kaynak olarak kullanıyor (Electron'un yerel SQLite'tan gönderdiği),
yoksa mevcut Postgres sorgusuna düşüyor. buildCaseChatContext ve
generateDraft da aynı şekilde caseDocuments/latestAnalysisSummaryJson
opsiyonel alanlarını kabul ediyor. WhatsApp entegrasyonu (whatsapp.
controller.ts) bilinçli olarak dokunulmadı — yerel dosya erişimi
olmadığından mevcut Postgres yedek yoluna düşmeye devam edecek.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Mustafa Yildiz
2026-08-20 00:23:07 +03:00
co-authored by Claude Sonnet 5
parent 7c76913cf9
commit fd5cd8503a
3 changed files with 74 additions and 61 deletions
+34 -42
View File
@@ -17,6 +17,28 @@ function toTraceableLegislation(matches: LegislationMatch[]) {
return matches.map(({ maddeNo, baslik, kanun }) => ({ maddeNo, baslik, kanun }));
}
interface RequestDocument { filename: string; extracted_text: string | null; }
// Faz 2 — documents artık kalıcı olarak Postgres'te tutulmuyor (Faz 3'te
// yazma da kaldırılacak); istek gövdesinde gönderilen belgeler (Electron'un
// yerel SQLite'tan okuyup gönderdiği) ÖNCELİKLİ kaynak. Gövdede belge yoksa
// (eski istemci sürümü, web vb.) mevcut Postgres sorgusuna düşülür — geçiş
// süresince iki kaynak da destekleniyor.
async function resolveCaseDocuments(caseId: string, bodyDocuments: unknown): Promise<RequestDocument[]> {
if (Array.isArray(bodyDocuments) && bodyDocuments.length > 0) {
return bodyDocuments
.map((d: any) => ({ filename: String(d?.filename ?? d?.name ?? ''), extracted_text: d?.extracted_text ?? null }))
.filter((d) => d.filename);
}
const { data, error } = await supabase
.from('documents')
.select('filename, extracted_text')
.eq('case_id', caseId)
.order('uploaded_at', { ascending: true });
if (error) throw error;
return data || [];
}
export const createCase = async (req: AuthenticatedRequest, res: Response) => {
try {
const userId = req.user?.id;
@@ -68,14 +90,9 @@ export const summarizeCase = async (req: AuthenticatedRequest, res: Response) =>
return res.status(400).json({ error: 'Geçersiz caseId' });
}
const { data: docs, error: docsError } = await supabase
.from('documents')
.select('filename, extracted_text')
.eq('case_id', caseId)
.order('uploaded_at', { ascending: true });
if (docsError) throw docsError;
const docs = await resolveCaseDocuments(String(caseId), req.body?.documents);
const withText = (docs || []).filter((d) => d.extracted_text && String(d.extracted_text).trim());
const withText = docs.filter((d) => d.extracted_text && String(d.extracted_text).trim());
if (withText.length === 0) {
return res.status(400).json({ error: 'Bu dosyada metni çıkarılmış belge yok, özet hazırlanamıyor.' });
}
@@ -132,19 +149,14 @@ export const analyzeStatements = async (req: AuthenticatedRequest, res: Response
return res.status(400).json({ error: 'Geçersiz caseId' });
}
const { data: docs, error: docsError } = await supabase
.from('documents')
.select('filename, extracted_text')
.eq('case_id', caseId)
.order('uploaded_at', { ascending: true });
if (docsError) throw docsError;
const docs = await resolveCaseDocuments(String(caseId), req.body?.documents);
// Sadece ifade, sorgu, beyan, tutanak, duruşma içeren belgeleri filtrele.
// "tutanak" tek başına çok geniş — "Tebligat Tutanağı" gibi ifade/beyan
// İÇERMEYEN evrakları da yakalıyordu, çelişki analizine gürültü katıyordu.
const keywords = ['ifade', 'sorgu', 'beyan', 'tutanak', 'duruşma'];
const excludeKeywords = ['tebligat', 'tebliğ'];
const statementDocs = (docs || []).filter((d) => {
const statementDocs = docs.filter((d) => {
if (!d.extracted_text || !String(d.extracted_text).trim()) return false;
const fn = normalizeTr(d.filename);
if (excludeKeywords.some(kw => fn.includes(kw))) return false;
@@ -187,16 +199,11 @@ export const analyzeDeficiencies = async (req: AuthenticatedRequest, res: Respon
return res.status(400).json({ error: 'Geçersiz caseId' });
}
const { data: docs, error: docsError } = await supabase
.from('documents')
.select('filename, extracted_text')
.eq('case_id', caseId)
.order('uploaded_at', { ascending: true });
if (docsError) throw docsError;
const docs = await resolveCaseDocuments(String(caseId), req.body?.documents);
// Çöp/makbuz niteliğindeki teknik zarf belgeleri hariç metni olan tüm evrakları dahil et
const trashKeywords = ['makbuz', 'pul', 'zarf', 'masraf', 'havale', 'barkod', 'alındı', 'görüldü', 'ilan'];
const targetDocs = (docs || []).filter((d) => {
const targetDocs = docs.filter((d) => {
if (!d.extracted_text || !String(d.extracted_text).trim()) return false;
const fn = normalizeTr(d.filename);
return !trashKeywords.some((kw) => fn.includes(kw));
@@ -245,16 +252,11 @@ export const analyzeStrategy = async (req: AuthenticatedRequest, res: Response)
return res.status(400).json({ error: 'clientName (Müvekkil Adı) zorunludur' });
}
const { data: docs, error: docsError } = await supabase
.from('documents')
.select('filename, extracted_text')
.eq('case_id', caseId)
.order('uploaded_at', { ascending: true });
if (docsError) throw docsError;
const docs = await resolveCaseDocuments(String(caseId), req.body?.documents);
// Gereksiz makbuz, pul vb. hariç tüm evrakları strateji için kullan
const trashKeywords = ['tebligat', 'makbuz', 'pul', 'zarf', 'masraf', 'havale', 'barkod', 'alındı', 'görüldü', 'ilan', 'talimat', 'nüfus', 'sicil', 'sabıka'];
const targetDocs = (docs || []).filter(d => {
const targetDocs = docs.filter(d => {
if (!d.extracted_text || !String(d.extracted_text).trim()) return false;
const fn = normalizeTr(d.filename);
return !trashKeywords.some(kw => fn.includes(kw));
@@ -302,15 +304,10 @@ export const analyzeMediation = async (req: AuthenticatedRequest, res: Response)
return res.status(400).json({ error: 'clientName (Müvekkil Adı) zorunludur' });
}
const { data: docs, error: docsError } = await supabase
.from('documents')
.select('filename, extracted_text')
.eq('case_id', caseId)
.order('uploaded_at', { ascending: true });
if (docsError) throw docsError;
const docs = await resolveCaseDocuments(String(caseId), req.body?.documents);
const trashKeywords = ['tebligat', 'makbuz', 'pul', 'zarf', 'masraf', 'havale', 'barkod', 'alındı', 'görüldü', 'ilan', 'talimat', 'nüfus', 'sicil', 'sabıka'];
const targetDocs = (docs || []).filter(d => {
const targetDocs = docs.filter(d => {
if (!d.extracted_text || !String(d.extracted_text).trim()) return false;
const fn = (d.filename || '').toLowerCase().replace(/ı/g, 'i').replace(/ğ/g, 'g').replace(/ü/g, 'u').replace(/ş/g, 's').replace(/ö/g, 'o').replace(/ç/g, 'c');
return !trashKeywords.some(kw => fn.includes(kw));
@@ -405,14 +402,9 @@ export const analyzeDigitalIntern = async (req: AuthenticatedRequest, res: Respo
return res.status(400).json({ error: 'Soru (question) girmelisiniz.' });
}
const { data: docs, error: docsError } = await supabase
.from('documents')
.select('filename, extracted_text')
.eq('case_id', caseId)
.order('uploaded_at', { ascending: true });
if (docsError) throw docsError;
const docs = await resolveCaseDocuments(String(caseId), req.body?.documents);
const targetDocs = (docs || []).filter(d => d.extracted_text && String(d.extracted_text).trim());
const targetDocs = docs.filter(d => d.extracted_text && String(d.extracted_text).trim());
if (targetDocs.length === 0) {
return res.status(400).json({ error: 'Bu dosyada arama yapılabilecek evrak bulunamadı.' });
+23 -11
View File
@@ -100,6 +100,12 @@ interface CaseChatContextOpts {
// mesajını insert ettikten SONRA bu fonksiyonu çağırıyor olmalı) kullanılacak
// son çare — orijinal chatMessage'daki else dalıyla aynı savunma amaçlı davranış.
fallbackUserContent?: string;
// Faz 2 — documents/analyses artık kalıcı olarak Postgres'te tutulmuyor.
// Electron (yerel SQLite'tan okuyarak) bunları isteğe ekleyebilir; verilirse
// ÖNCELİKLİ kaynaktır. Verilmezse (örn. WhatsApp — yerel dosya erişimi yok)
// aşağıdaki Postgres sorgularına düşülür, davranış aynen korunur.
caseDocuments?: { id?: string; filename: string; extracted_text: string | null }[];
latestAnalysisSummaryJson?: unknown;
}
// chatMessage (masaüstü "AyrisLegal'e Sor" sohbeti) ve WhatsApp uç noktasının
@@ -109,18 +115,22 @@ interface CaseChatContextOpts {
// kullanıcı mesajını chat_messages'a insert ETTİKTEN SONRA çağırmalı, ki pastMessages
// sorgusu o mesajı da (en sonda) içersin.
export async function buildCaseChatContext(caseId: string, opts: CaseChatContextOpts = {}): Promise<ChatMsg[]> {
const { chat_mode = 'chat', persona, document_ids, attachments, historyLimit = 10, fallbackUserContent } = opts;
const { chat_mode = 'chat', persona, document_ids, attachments, historyLimit = 10, fallbackUserContent, caseDocuments, latestAnalysisSummaryJson } = opts;
const targetPersona = persona || 'judge';
const { data: analyses } = await supabase
.from('analyses')
.select('summary_json')
.eq('case_id', caseId);
const { data: documents } = await supabase
const hasBodyDocuments = Array.isArray(caseDocuments) && caseDocuments.length > 0;
const documents = hasBodyDocuments ? caseDocuments : (await supabase
.from('documents')
.select('id, filename, extracted_text')
.eq('case_id', caseId);
.eq('case_id', caseId)).data;
const latestAnalysis = latestAnalysisSummaryJson !== undefined ? latestAnalysisSummaryJson : (await (async () => {
const { data: analyses } = await supabase
.from('analyses')
.select('summary_json')
.eq('case_id', caseId);
return analyses && analyses.length > 0 ? analyses[analyses.length - 1].summary_json : undefined;
})());
const { data: pastMessages } = await supabase
.from('chat_messages')
@@ -146,8 +156,8 @@ export async function buildCaseChatContext(caseId: string, opts: CaseChatContext
documentContext = '\n\nBu dosyaya henüz hiçbir belge yüklenmemiştir. Genel hukuki bilgiyle cevap ver, ama bunu kullanıcıya açıkça belirt.';
}
if (analyses && analyses.length > 0) {
documentContext += '\n\nANALİZ ÖZETİ:\n' + JSON.stringify(analyses[analyses.length - 1].summary_json);
if (latestAnalysis) {
documentContext += '\n\nANALİZ ÖZETİ:\n' + JSON.stringify(latestAnalysis);
}
documentContext += buildAttachmentContext(attachments);
@@ -180,7 +190,7 @@ export async function buildCaseChatContext(caseId: string, opts: CaseChatContext
// yerine yazılırken görür; gerçek üretim süresi aynı kalsa da algılanan hız artar.
export const chatMessage = async (req: AuthenticatedRequest, res: Response) => {
const { caseId } = req.params;
const { content, message, attachments, document_ids, chat_mode = 'chat', persona, simulator_persona } = req.body || {};
const { content, message, attachments, document_ids, chat_mode = 'chat', persona, simulator_persona, case_documents, latest_analysis_summary_json } = req.body || {};
const messageText = content || message;
const targetPersona = simulator_persona || persona || 'judge';
const userId = req.user?.id;
@@ -208,6 +218,8 @@ export const chatMessage = async (req: AuthenticatedRequest, res: Response) => {
document_ids,
attachments,
fallbackUserContent: persistedContent,
caseDocuments: Array.isArray(case_documents) ? case_documents : undefined,
latestAnalysisSummaryJson: latest_analysis_summary_json,
});
startSse(res);
+17 -8
View File
@@ -47,7 +47,7 @@ function toTraceableLegislation(matches: LegislationMatch[]) {
// gerçek bir AI taslağı üretir.
export const generateDraft = async (req: AuthenticatedRequest, res: Response) => {
try {
const { case_id, petition_type, notes, template_id } = req.body || {};
const { case_id, petition_type, notes, template_id, latest_analysis_summary_json } = req.body || {};
const userId = req.user?.id;
if (!case_id || !petition_type) {
@@ -66,13 +66,22 @@ export const generateDraft = async (req: AuthenticatedRequest, res: Response) =>
return res.status(403).json({ error: 'Bu dosyaya erişim yetkiniz yok.' });
}
const { data: lastAnalysis } = await supabase
.from('analyses')
.select('summary_json')
.eq('case_id', case_id)
.order('created_at', { ascending: false })
.limit(1)
.maybeSingle();
// Faz 2 — analyses artık kalıcı olarak Postgres'te tutulmuyor. İstekte
// gönderilmişse (Electron'un yerel SQLite'tan okuduğu son analiz) öncelikli
// kaynak; yoksa (eski istemci/web) mevcut Postgres sorgusuna düşülür.
let lastAnalysis: { summary_json: any } | null = latest_analysis_summary_json !== undefined
? { summary_json: latest_analysis_summary_json }
: null;
if (!lastAnalysis) {
const { data } = await supabase
.from('analyses')
.select('summary_json')
.eq('case_id', case_id)
.order('created_at', { ascending: false })
.limit(1)
.maybeSingle();
lastAnalysis = data;
}
let template: { name: string; extracted_text: string | null } | null = null;
if (template_id) {