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:
co-authored by
Claude Sonnet 5
parent
7c76913cf9
commit
fd5cd8503a
@@ -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ı.' });
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user