feat: documents/analyses/drafts Postgres yazmalarını geçiş bayrağının arkasına al
Yerel-öncelikli veri mimarisi (Faz 3) — registerLocalDocument, case. controller.ts'in 6 fonksiyonu (5 analiz + Dijital Stajyer) ve drafting. controller.ts'in generateDraft/generateAngarya'sı artık PERSIST_TO_ POSTGRES ortam değişkenine göre davranıyor. Bayrak varsayılan olarak true (mevcut davranış birebir korunuyor) — kod deploy'u gerçek davranışı DEĞİŞTİRMİYOR. Kullanıcı gerçek veriyle Faz 1/2'yi (yerel SQLite'a çift yazma + istekten metin kabul etme) test edip güvendiğinde, PERSIST_TO_POSTGRES=false ayarıyla (kod değişikliği gerekmeden) yazmalar durur; geri dönüş de aynı şekilde anlık. case_events ve chat_messages kapsam dışı, dokunulmadı. Angarya taslakları için bilinen bir sınırlama var: Faz 1'de yerel dual-write eklenmediğinden, bayrak kapatıldığında bu taslaklar hiçbir yerde kalıcı olmayacak. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
fd5cd8503a
commit
ab20d88422
@@ -4,6 +4,8 @@ import { AuthenticatedRequest } from '../middleware/auth';
|
||||
import { runIddianameAnalysis, runCaseNarrativeAnalysis, runStatementAnalysis, runDeficiencyAnalysis, runStrategyAnalysis, runMediationAnalysis, runDigitalInternQa } from './document.controller';
|
||||
import { findOrCreateCaseByTitle } from '../lib/caseLookup';
|
||||
import { findRelevantLegislation, LegislationMatch } from '../lib/legislationLookup';
|
||||
import { shouldPersistLegacyTables } from '../lib/localFirstFlag';
|
||||
import crypto from 'crypto';
|
||||
|
||||
function formatLegislationBlock(matches: LegislationMatch[]): string {
|
||||
if (matches.length === 0) return '';
|
||||
@@ -39,6 +41,32 @@ async function resolveCaseDocuments(caseId: string, bodyDocuments: unknown): Pro
|
||||
return data || [];
|
||||
}
|
||||
|
||||
// Faz 3 — analyses artık kalıcı olarak Postgres'e YAZILMIYOR (PERSIST_TO_POSTGRES=false
|
||||
// olduğunda). Bayrak açıkken (varsayılan, geçiş güvenliği için) davranış birebir
|
||||
// eskisi gibi kalır. Bayrak kapatılınca DB'ye insert yapılmaz, çağırana yine de
|
||||
// aynı şekle sahip (id/created_at üretilmiş) bir kayıt döner ki yanıt formatı
|
||||
// ve Electron'un yerel SQLite'a yazması etkilenmesin.
|
||||
async function persistOrBuildAnalysis(caseId: string, documentId: string | null, summaryJson: unknown, aiModel: string, extraColumns: Record<string, unknown> = {}) {
|
||||
if (shouldPersistLegacyTables()) {
|
||||
const { data, error } = await supabase
|
||||
.from('analyses')
|
||||
.insert([{ case_id: caseId, document_id: documentId, summary_json: summaryJson, model_used: aiModel, ...extraColumns }])
|
||||
.select()
|
||||
.single();
|
||||
if (error) throw error;
|
||||
return data;
|
||||
}
|
||||
return {
|
||||
id: crypto.randomUUID(),
|
||||
case_id: caseId,
|
||||
document_id: documentId,
|
||||
summary_json: summaryJson,
|
||||
model_used: aiModel,
|
||||
created_at: new Date().toISOString(),
|
||||
...extraColumns,
|
||||
};
|
||||
}
|
||||
|
||||
export const createCase = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const userId = req.user?.id;
|
||||
@@ -127,12 +155,7 @@ export const summarizeCase = async (req: AuthenticatedRequest, res: Response) =>
|
||||
|
||||
const { summaryJson, aiModel } = await runCaseNarrativeAnalysis(analysisInput);
|
||||
|
||||
const { data: analysisRecord, error: analysisError } = await supabase
|
||||
.from('analyses')
|
||||
.insert([{ case_id: caseId, document_id: null, summary_json: { ...summaryJson, kullanilan_mevzuat: toTraceableLegislation(legislationMatches) }, model_used: aiModel }])
|
||||
.select()
|
||||
.single();
|
||||
if (analysisError) throw analysisError;
|
||||
const analysisRecord = await persistOrBuildAnalysis(String(caseId), null, { ...summaryJson, kullanilan_mevzuat: toTraceableLegislation(legislationMatches) }, aiModel);
|
||||
|
||||
res.json({ analysis: analysisRecord });
|
||||
} catch (error: any) {
|
||||
@@ -177,13 +200,7 @@ export const analyzeStatements = async (req: AuthenticatedRequest, res: Response
|
||||
|
||||
const { summaryJson, aiModel } = await runStatementAnalysis(analysisInput);
|
||||
|
||||
// Çelişki analizi sonucunu 'analyses' tablosuna kaydet
|
||||
const { data: analysisRecord, error: analysisError } = await supabase
|
||||
.from('analyses')
|
||||
.insert([{ case_id: caseId, document_id: null, summary_json: { statementAnalysis: summaryJson, kullanilan_mevzuat: toTraceableLegislation(legislationMatches) }, model_used: aiModel }])
|
||||
.select()
|
||||
.single();
|
||||
if (analysisError) throw analysisError;
|
||||
const analysisRecord = await persistOrBuildAnalysis(String(caseId), null, { statementAnalysis: summaryJson, kullanilan_mevzuat: toTraceableLegislation(legislationMatches) }, aiModel);
|
||||
|
||||
res.json({ analysis: analysisRecord });
|
||||
} catch (error: any) {
|
||||
@@ -222,13 +239,7 @@ export const analyzeDeficiencies = async (req: AuthenticatedRequest, res: Respon
|
||||
|
||||
const { summaryJson, aiModel } = await runDeficiencyAnalysis(analysisInput);
|
||||
|
||||
// Eksiklik analizi sonucunu 'analyses' tablosuna kaydet
|
||||
const { data: analysisRecord, error: analysisError } = await supabase
|
||||
.from('analyses')
|
||||
.insert([{ case_id: caseId, document_id: null, summary_json: { deficiencyAnalysis: summaryJson }, model_used: aiModel }])
|
||||
.select()
|
||||
.single();
|
||||
if (analysisError) throw analysisError;
|
||||
const analysisRecord = await persistOrBuildAnalysis(String(caseId), null, { deficiencyAnalysis: summaryJson }, aiModel);
|
||||
|
||||
res.json({ analysis: analysisRecord });
|
||||
} catch (error: any) {
|
||||
@@ -275,13 +286,7 @@ export const analyzeStrategy = async (req: AuthenticatedRequest, res: Response)
|
||||
|
||||
const { summaryJson, aiModel } = await runStrategyAnalysis(analysisInput, nameToUse, roleToUse);
|
||||
|
||||
// Strateji analizi sonucunu 'analyses' tablosuna kaydet
|
||||
const { data: analysisRecord, error: analysisError } = await supabase
|
||||
.from('analyses')
|
||||
.insert([{ case_id: caseId, document_id: null, summary_json: { strategyAnalysis: summaryJson, clientName: nameToUse, clientRole: roleToUse }, model_used: aiModel }])
|
||||
.select()
|
||||
.single();
|
||||
if (analysisError) throw analysisError;
|
||||
const analysisRecord = await persistOrBuildAnalysis(String(caseId), null, { strategyAnalysis: summaryJson, clientName: nameToUse, clientRole: roleToUse }, aiModel);
|
||||
|
||||
res.json({ analysis: analysisRecord });
|
||||
} catch (error: any) {
|
||||
@@ -326,12 +331,7 @@ export const analyzeMediation = async (req: AuthenticatedRequest, res: Response)
|
||||
|
||||
const { summaryJson, aiModel } = await runMediationAnalysis(analysisInput, nameToUse);
|
||||
|
||||
const { data: analysisRecord, error: analysisError } = await supabase
|
||||
.from('analyses')
|
||||
.insert([{ case_id: caseId, document_id: null, summary_json: { mediationAnalysis: summaryJson, clientName: nameToUse }, analysis_type: 'mediation', model_used: aiModel }])
|
||||
.select()
|
||||
.single();
|
||||
if (analysisError) throw analysisError;
|
||||
const analysisRecord = await persistOrBuildAnalysis(String(caseId), null, { mediationAnalysis: summaryJson, clientName: nameToUse }, aiModel, { analysis_type: 'mediation' });
|
||||
|
||||
res.json({ analysis: analysisRecord });
|
||||
} catch (error: any) {
|
||||
@@ -420,10 +420,15 @@ export const analyzeDigitalIntern = async (req: AuthenticatedRequest, res: Respo
|
||||
const { summaryJson, aiModel } = await runDigitalInternQa(analysisInput, questionToUse);
|
||||
const userId = req.user?.id;
|
||||
|
||||
// Dijital Stajyer sonucunu hem 'analyses' hem de 'chat_messages' tablosuna kaydet
|
||||
await supabase
|
||||
.from('analyses')
|
||||
.insert([{ case_id: caseId, document_id: null, summary_json: { digitalInternResult: summaryJson, question: questionToUse }, model_used: aiModel }]);
|
||||
// Dijital Stajyer sonucunu 'chat_messages'a her zaman kaydet (kapsam dışı,
|
||||
// dokunulmadı); 'analyses'a ise sadece PERSIST_TO_POSTGRES açıkken —
|
||||
// bu zaten anlık/tek seferlik bir cevap, Faz 1'de yerel SQLite'a da hiç
|
||||
// yazılmıyor, bu yüzden throwaway kayda gerek yok.
|
||||
if (shouldPersistLegacyTables()) {
|
||||
await supabase
|
||||
.from('analyses')
|
||||
.insert([{ case_id: caseId, document_id: null, summary_json: { digitalInternResult: summaryJson, question: questionToUse }, model_used: aiModel }]);
|
||||
}
|
||||
|
||||
if (userId) {
|
||||
await supabase.from('chat_messages').insert([
|
||||
|
||||
@@ -8,6 +8,8 @@ import iconv from 'iconv-lite';
|
||||
import { findOrCreateCaseByTitle } from '../lib/caseLookup';
|
||||
import { callOllama, callOllamaVisionOcr } from '../lib/aiClient';
|
||||
import { getAiConfig } from '../lib/aiConfig';
|
||||
import { shouldPersistLegacyTables } from '../lib/localFirstFlag';
|
||||
import crypto from 'crypto';
|
||||
const heicConvert = require('heic-convert');
|
||||
|
||||
// Ceza davası analizlerinde (Dava Özeti, Dava İlerlemesi, Çelişki Avcısı, Strateji)
|
||||
@@ -330,6 +332,7 @@ export const registerLocalDocument = async (req: AuthenticatedRequest, res: Resp
|
||||
}
|
||||
|
||||
const caseId = await findOrCreateCaseByTitle(userId as string, case_title);
|
||||
const persistLegacy = shouldPersistLegacyTables();
|
||||
|
||||
// "Yeniden İşle" aynı case_title+filename ile bu uca tekrar tekrar istek
|
||||
// atabiliyor (OCR ilk seferde boş sonuç döndüğünde) — daha önce burada koşulsuz
|
||||
@@ -344,15 +347,21 @@ export const registerLocalDocument = async (req: AuthenticatedRequest, res: Resp
|
||||
.maybeSingle();
|
||||
if (existingErr) throw existingErr;
|
||||
|
||||
// Faz 3 (PERSIST_TO_POSTGRES=false): documents artık extracted_text'i
|
||||
// KALICI olarak saklamıyor — sadece case_events FK'sının ve Belgeler
|
||||
// ekranındaki dosya listesinin çalışmaya devam etmesi için hafif bir
|
||||
// metadata satırı tutulur. Yanıtta extracted_text yine de döner (Electron
|
||||
// bunu kendi yerel SQLite'ına yazsın diye) — burada dönen değer DB'den
|
||||
// değil, doğrudan istekten geliyor.
|
||||
const docColumns = persistLegacy
|
||||
? { file_size: extracted_text.length, ocr_status: extracted_text ? 'done' : 'failed', extracted_text }
|
||||
: { file_size: extracted_text.length, ocr_status: extracted_text ? 'done' : 'failed', extracted_text: null };
|
||||
|
||||
let documentRecord;
|
||||
if (existingDoc) {
|
||||
const { data, error: updateErr } = await supabase
|
||||
.from('documents')
|
||||
.update({
|
||||
file_size: extracted_text.length,
|
||||
ocr_status: extracted_text ? 'done' : 'failed',
|
||||
extracted_text,
|
||||
})
|
||||
.update(docColumns)
|
||||
.eq('id', existingDoc.id)
|
||||
.select()
|
||||
.single();
|
||||
@@ -367,29 +376,44 @@ export const registerLocalDocument = async (req: AuthenticatedRequest, res: Resp
|
||||
storage_path: 'local', // ham dosya sadece kullanıcının cihazında; Storage'da bir karşılığı yok
|
||||
filename,
|
||||
mime_type: null,
|
||||
file_size: extracted_text.length,
|
||||
ocr_status: extracted_text ? 'done' : 'failed',
|
||||
extracted_text,
|
||||
...docColumns,
|
||||
}])
|
||||
.select()
|
||||
.single();
|
||||
if (docError) throw docError;
|
||||
documentRecord = data;
|
||||
}
|
||||
if (!persistLegacy) {
|
||||
// DB'ye yazılmadı ama yanıtta hâlâ ihtiyaç var (Electron'un yerel
|
||||
// SQLite'a yazacağı değer) — istekten geleni doğrudan geri koy.
|
||||
documentRecord = { ...documentRecord, extracted_text };
|
||||
}
|
||||
|
||||
let analysisRecord = null;
|
||||
if (extracted_text.trim()) {
|
||||
try {
|
||||
const { summaryJson, aiModel } = await runDocumentAnalysis(extracted_text);
|
||||
const { data: analysis, error: analysisError } = await supabase
|
||||
.from('analyses')
|
||||
.insert([{ case_id: caseId, document_id: documentRecord.id, summary_json: summaryJson, model_used: aiModel }])
|
||||
.select()
|
||||
.single();
|
||||
if (analysisError) throw analysisError;
|
||||
analysisRecord = analysis;
|
||||
|
||||
// 5. Takvim (Case Events) kayıtlarını oluştur
|
||||
if (persistLegacy) {
|
||||
const { data: analysis, error: analysisError } = await supabase
|
||||
.from('analyses')
|
||||
.insert([{ case_id: caseId, document_id: documentRecord.id, summary_json: summaryJson, model_used: aiModel }])
|
||||
.select()
|
||||
.single();
|
||||
if (analysisError) throw analysisError;
|
||||
analysisRecord = analysis;
|
||||
} else {
|
||||
analysisRecord = {
|
||||
id: crypto.randomUUID(),
|
||||
case_id: caseId,
|
||||
document_id: documentRecord.id,
|
||||
summary_json: summaryJson,
|
||||
model_used: aiModel,
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
// 5. Takvim (Case Events) kayıtlarını oluştur — case_events kapsam
|
||||
// dışı, PERSIST_TO_POSTGRES bayrağından bağımsız her zaman yazılır.
|
||||
if (summaryJson.events && Array.isArray(summaryJson.events)) {
|
||||
const eventsToInsert = summaryJson.events
|
||||
.filter((e: any) => e.date && e.title)
|
||||
|
||||
@@ -3,6 +3,8 @@ import { supabase } from '../lib/supabase';
|
||||
import { AuthenticatedRequest } from '../middleware/auth';
|
||||
import { callAiModelRaw, CEZA_MUHAKEMESI_REFERANSI, HUKUK_MAHKEMELERI_REFERANSI } from './document.controller';
|
||||
import { findRelevantLegislation, LegislationMatch } from '../lib/legislationLookup';
|
||||
import { shouldPersistLegacyTables } from '../lib/localFirstFlag';
|
||||
import crypto from 'crypto';
|
||||
|
||||
const DRAFT_SYSTEM_PROMPT = `
|
||||
Sen uzman bir Türk avukatısın. Sana verilen dosya bilgilerine göre, istenen türde
|
||||
@@ -118,22 +120,29 @@ export const generateDraft = async (req: AuthenticatedRequest, res: Response) =>
|
||||
|
||||
const { aiContent, aiModel } = await callAiModelRaw(DRAFT_SYSTEM_PROMPT, userContent, 2500, 180_000);
|
||||
|
||||
// Taslağı kalıcı olarak kaydediyoruz — hem "Dilekçelerim" listesinde hem ilgili
|
||||
// dava dosyasının kendi "Dilekçeler" bölümünde görünsün, sayfadan çıkınca kaybolmasın.
|
||||
const { data: draftRecord, error: draftError } = await supabase
|
||||
.from('drafts')
|
||||
.insert([{
|
||||
user_id: userId,
|
||||
case_id,
|
||||
petition_type,
|
||||
content: aiContent,
|
||||
template_id: template_id || null,
|
||||
used_legislation: toTraceableLegislation(legislationMatches),
|
||||
}])
|
||||
.select('id, created_at')
|
||||
.single();
|
||||
if (draftError) {
|
||||
console.error('Save Draft Error:', draftError);
|
||||
// Faz 3 — taslak artık kalıcı olarak Postgres'e YAZILMIYOR (PERSIST_TO_POSTGRES=false
|
||||
// olduğunda); "Dilekçelerim" listesi artık yerel SQLite'tan (Faz 1/2) besleniyor.
|
||||
// Bayrak açıkken (varsayılan) davranış birebir eskisi gibi kalır.
|
||||
let draftRecord: { id: string; created_at?: string } | null = null;
|
||||
if (shouldPersistLegacyTables()) {
|
||||
const { data, error: draftError } = await supabase
|
||||
.from('drafts')
|
||||
.insert([{
|
||||
user_id: userId,
|
||||
case_id,
|
||||
petition_type,
|
||||
content: aiContent,
|
||||
template_id: template_id || null,
|
||||
used_legislation: toTraceableLegislation(legislationMatches),
|
||||
}])
|
||||
.select('id, created_at')
|
||||
.single();
|
||||
if (draftError) {
|
||||
console.error('Save Draft Error:', draftError);
|
||||
}
|
||||
draftRecord = data;
|
||||
} else {
|
||||
draftRecord = { id: crypto.randomUUID(), created_at: new Date().toISOString() };
|
||||
}
|
||||
|
||||
res.json({ draft: aiContent, draftId: draftRecord?.id || null, model: aiModel, usedTemplate: !!template, usedLegislation: toTraceableLegislation(legislationMatches) });
|
||||
@@ -205,22 +214,29 @@ export const generateAngarya = async (req: AuthenticatedRequest, res: Response)
|
||||
// Fast generation using a lightweight model or quick params
|
||||
const { aiContent, aiModel } = await callAiModelRaw(ANGARYA_SYSTEM_PROMPT, userContent, 500);
|
||||
|
||||
const { data: draftRecord, error: draftError } = await supabase
|
||||
.from('drafts')
|
||||
.insert([{
|
||||
user_id: userId,
|
||||
case_id,
|
||||
petition_type: petitionType,
|
||||
content: aiContent,
|
||||
template_id: null,
|
||||
}])
|
||||
.select('id, created_at')
|
||||
.single();
|
||||
|
||||
if (draftError) {
|
||||
console.error('Save Draft Error:', draftError);
|
||||
// Faz 3 — angarya taslakları da PERSIST_TO_POSTGRES=false olduğunda
|
||||
// kalıcı yazılmıyor. NOT: Angarya için Faz 1'de bir yerel SQLite dual-write
|
||||
// eklenmedi (kapsam dışı tutulan düşük öncelikli özellik) — bayrak
|
||||
// kapatıldığında bu taslaklar sadece HTTP yanıtında var olacak, hiçbir
|
||||
// yerde kalıcı olmayacak. Bilinen ve kabul edilmiş bir sınırlama.
|
||||
let draftRecord: { id: string; created_at?: string } | null = null;
|
||||
if (shouldPersistLegacyTables()) {
|
||||
const { data, error: draftError } = await supabase
|
||||
.from('drafts')
|
||||
.insert([{
|
||||
user_id: userId,
|
||||
case_id,
|
||||
petition_type: petitionType,
|
||||
content: aiContent,
|
||||
template_id: null,
|
||||
}])
|
||||
.select('id, created_at')
|
||||
.single();
|
||||
if (draftError) console.error('Save Angarya Draft Error:', draftError);
|
||||
draftRecord = data;
|
||||
} else {
|
||||
draftRecord = { id: crypto.randomUUID(), created_at: new Date().toISOString() };
|
||||
}
|
||||
|
||||
res.json({ draft: aiContent, draftId: draftRecord?.id || null, model: aiModel, usedTemplate: false });
|
||||
} catch (error: any) {
|
||||
console.error('Generate Angarya Error:', error);
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
// Yerel-öncelikli veri mimarisi (Faz 3) geçiş bayrağı — bkz.
|
||||
// laawos/docs/mimari-karar-yerel-oncelikli-veri-modeli.md
|
||||
//
|
||||
// documents/analyses/drafts artık Postgres'e kalıcı olarak YAZILMAMASI
|
||||
// gerekiyor (belge/analiz içeriği artık kullanıcının kendi bilgisayarında +
|
||||
// Drive'ında tutuluyor). Ama bu geçiş kod deploy'uyla AYNI ANDA gerçek
|
||||
// davranışı değiştirmesin diye — kullanıcı önce gerçek veriyle Faz 1/2'yi
|
||||
// (yerel SQLite'a çift yazma + istekten metin kabul etme) test edip güvenene
|
||||
// kadar — varsayılan davranış DEĞİŞMİYOR: Postgres'e yazmaya devam ediyoruz.
|
||||
//
|
||||
// PERSIST_TO_POSTGRES=false ayarlanınca (kod deploy'u GEREKMEDEN, sadece ortam
|
||||
// değişkeni) yazmalar durur. Geri dönüş de aynı şekilde anlık — değişkeni
|
||||
// kaldırmak veya "true" yapmak yeterli.
|
||||
export function shouldPersistLegacyTables(): boolean {
|
||||
return process.env.PERSIST_TO_POSTGRES !== 'false';
|
||||
}
|
||||
Reference in New Issue
Block a user