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 }));
|
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) => {
|
export const createCase = async (req: AuthenticatedRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const userId = req.user?.id;
|
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' });
|
return res.status(400).json({ error: 'Geçersiz caseId' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: docs, error: docsError } = await supabase
|
const docs = await resolveCaseDocuments(String(caseId), req.body?.documents);
|
||||||
.from('documents')
|
|
||||||
.select('filename, extracted_text')
|
|
||||||
.eq('case_id', caseId)
|
|
||||||
.order('uploaded_at', { ascending: true });
|
|
||||||
if (docsError) throw docsError;
|
|
||||||
|
|
||||||
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) {
|
if (withText.length === 0) {
|
||||||
return res.status(400).json({ error: 'Bu dosyada metni çıkarılmış belge yok, özet hazırlanamıyor.' });
|
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' });
|
return res.status(400).json({ error: 'Geçersiz caseId' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: docs, error: docsError } = await supabase
|
const docs = await resolveCaseDocuments(String(caseId), req.body?.documents);
|
||||||
.from('documents')
|
|
||||||
.select('filename, extracted_text')
|
|
||||||
.eq('case_id', caseId)
|
|
||||||
.order('uploaded_at', { ascending: true });
|
|
||||||
if (docsError) throw docsError;
|
|
||||||
|
|
||||||
// Sadece ifade, sorgu, beyan, tutanak, duruşma içeren belgeleri filtrele.
|
// Sadece ifade, sorgu, beyan, tutanak, duruşma içeren belgeleri filtrele.
|
||||||
// "tutanak" tek başına çok geniş — "Tebligat Tutanağı" gibi ifade/beyan
|
// "tutanak" tek başına çok geniş — "Tebligat Tutanağı" gibi ifade/beyan
|
||||||
// İÇERMEYEN evrakları da yakalıyordu, çelişki analizine gürültü katıyordu.
|
// İÇERMEYEN evrakları da yakalıyordu, çelişki analizine gürültü katıyordu.
|
||||||
const keywords = ['ifade', 'sorgu', 'beyan', 'tutanak', 'duruşma'];
|
const keywords = ['ifade', 'sorgu', 'beyan', 'tutanak', 'duruşma'];
|
||||||
const excludeKeywords = ['tebligat', 'tebliğ'];
|
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;
|
if (!d.extracted_text || !String(d.extracted_text).trim()) return false;
|
||||||
const fn = normalizeTr(d.filename);
|
const fn = normalizeTr(d.filename);
|
||||||
if (excludeKeywords.some(kw => fn.includes(kw))) return false;
|
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' });
|
return res.status(400).json({ error: 'Geçersiz caseId' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: docs, error: docsError } = await supabase
|
const docs = await resolveCaseDocuments(String(caseId), req.body?.documents);
|
||||||
.from('documents')
|
|
||||||
.select('filename, extracted_text')
|
|
||||||
.eq('case_id', caseId)
|
|
||||||
.order('uploaded_at', { ascending: true });
|
|
||||||
if (docsError) throw docsError;
|
|
||||||
|
|
||||||
// Çöp/makbuz niteliğindeki teknik zarf belgeleri hariç metni olan tüm evrakları dahil et
|
// Çö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 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;
|
if (!d.extracted_text || !String(d.extracted_text).trim()) return false;
|
||||||
const fn = normalizeTr(d.filename);
|
const fn = normalizeTr(d.filename);
|
||||||
return !trashKeywords.some((kw) => fn.includes(kw));
|
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' });
|
return res.status(400).json({ error: 'clientName (Müvekkil Adı) zorunludur' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: docs, error: docsError } = await supabase
|
const docs = await resolveCaseDocuments(String(caseId), req.body?.documents);
|
||||||
.from('documents')
|
|
||||||
.select('filename, extracted_text')
|
|
||||||
.eq('case_id', caseId)
|
|
||||||
.order('uploaded_at', { ascending: true });
|
|
||||||
if (docsError) throw docsError;
|
|
||||||
|
|
||||||
// Gereksiz makbuz, pul vb. hariç tüm evrakları strateji için kullan
|
// 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 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;
|
if (!d.extracted_text || !String(d.extracted_text).trim()) return false;
|
||||||
const fn = normalizeTr(d.filename);
|
const fn = normalizeTr(d.filename);
|
||||||
return !trashKeywords.some(kw => fn.includes(kw));
|
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' });
|
return res.status(400).json({ error: 'clientName (Müvekkil Adı) zorunludur' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: docs, error: docsError } = await supabase
|
const docs = await resolveCaseDocuments(String(caseId), req.body?.documents);
|
||||||
.from('documents')
|
|
||||||
.select('filename, extracted_text')
|
|
||||||
.eq('case_id', caseId)
|
|
||||||
.order('uploaded_at', { ascending: true });
|
|
||||||
if (docsError) throw docsError;
|
|
||||||
|
|
||||||
const trashKeywords = ['tebligat', 'makbuz', 'pul', 'zarf', 'masraf', 'havale', 'barkod', 'alındı', 'görüldü', 'ilan', 'talimat', 'nüfus', 'sicil', 'sabıka'];
|
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;
|
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');
|
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));
|
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.' });
|
return res.status(400).json({ error: 'Soru (question) girmelisiniz.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: docs, error: docsError } = await supabase
|
const docs = await resolveCaseDocuments(String(caseId), req.body?.documents);
|
||||||
.from('documents')
|
|
||||||
.select('filename, extracted_text')
|
|
||||||
.eq('case_id', caseId)
|
|
||||||
.order('uploaded_at', { ascending: true });
|
|
||||||
if (docsError) throw docsError;
|
|
||||||
|
|
||||||
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) {
|
if (targetDocs.length === 0) {
|
||||||
return res.status(400).json({ error: 'Bu dosyada arama yapılabilecek evrak bulunamadı.' });
|
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
|
// 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ış.
|
// son çare — orijinal chatMessage'daki else dalıyla aynı savunma amaçlı davranış.
|
||||||
fallbackUserContent?: string;
|
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
|
// 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
|
// kullanıcı mesajını chat_messages'a insert ETTİKTEN SONRA çağırmalı, ki pastMessages
|
||||||
// sorgusu o mesajı da (en sonda) içersin.
|
// sorgusu o mesajı da (en sonda) içersin.
|
||||||
export async function buildCaseChatContext(caseId: string, opts: CaseChatContextOpts = {}): Promise<ChatMsg[]> {
|
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 targetPersona = persona || 'judge';
|
||||||
|
|
||||||
const { data: analyses } = await supabase
|
const hasBodyDocuments = Array.isArray(caseDocuments) && caseDocuments.length > 0;
|
||||||
.from('analyses')
|
const documents = hasBodyDocuments ? caseDocuments : (await supabase
|
||||||
.select('summary_json')
|
|
||||||
.eq('case_id', caseId);
|
|
||||||
|
|
||||||
const { data: documents } = await supabase
|
|
||||||
.from('documents')
|
.from('documents')
|
||||||
.select('id, filename, extracted_text')
|
.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
|
const { data: pastMessages } = await supabase
|
||||||
.from('chat_messages')
|
.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.';
|
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) {
|
if (latestAnalysis) {
|
||||||
documentContext += '\n\nANALİZ ÖZETİ:\n' + JSON.stringify(analyses[analyses.length - 1].summary_json);
|
documentContext += '\n\nANALİZ ÖZETİ:\n' + JSON.stringify(latestAnalysis);
|
||||||
}
|
}
|
||||||
|
|
||||||
documentContext += buildAttachmentContext(attachments);
|
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.
|
// 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) => {
|
export const chatMessage = async (req: AuthenticatedRequest, res: Response) => {
|
||||||
const { caseId } = req.params;
|
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 messageText = content || message;
|
||||||
const targetPersona = simulator_persona || persona || 'judge';
|
const targetPersona = simulator_persona || persona || 'judge';
|
||||||
const userId = req.user?.id;
|
const userId = req.user?.id;
|
||||||
@@ -208,6 +218,8 @@ export const chatMessage = async (req: AuthenticatedRequest, res: Response) => {
|
|||||||
document_ids,
|
document_ids,
|
||||||
attachments,
|
attachments,
|
||||||
fallbackUserContent: persistedContent,
|
fallbackUserContent: persistedContent,
|
||||||
|
caseDocuments: Array.isArray(case_documents) ? case_documents : undefined,
|
||||||
|
latestAnalysisSummaryJson: latest_analysis_summary_json,
|
||||||
});
|
});
|
||||||
|
|
||||||
startSse(res);
|
startSse(res);
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ function toTraceableLegislation(matches: LegislationMatch[]) {
|
|||||||
// gerçek bir AI taslağı üretir.
|
// gerçek bir AI taslağı üretir.
|
||||||
export const generateDraft = async (req: AuthenticatedRequest, res: Response) => {
|
export const generateDraft = async (req: AuthenticatedRequest, res: Response) => {
|
||||||
try {
|
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;
|
const userId = req.user?.id;
|
||||||
|
|
||||||
if (!case_id || !petition_type) {
|
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.' });
|
return res.status(403).json({ error: 'Bu dosyaya erişim yetkiniz yok.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: lastAnalysis } = await supabase
|
// Faz 2 — analyses artık kalıcı olarak Postgres'te tutulmuyor. İstekte
|
||||||
.from('analyses')
|
// gönderilmişse (Electron'un yerel SQLite'tan okuduğu son analiz) öncelikli
|
||||||
.select('summary_json')
|
// kaynak; yoksa (eski istemci/web) mevcut Postgres sorgusuna düşülür.
|
||||||
.eq('case_id', case_id)
|
let lastAnalysis: { summary_json: any } | null = latest_analysis_summary_json !== undefined
|
||||||
.order('created_at', { ascending: false })
|
? { summary_json: latest_analysis_summary_json }
|
||||||
.limit(1)
|
: null;
|
||||||
.maybeSingle();
|
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;
|
let template: { name: string; extracted_text: string | null } | null = null;
|
||||||
if (template_id) {
|
if (template_id) {
|
||||||
|
|||||||
Reference in New Issue
Block a user