feat: use firm's own templates as style guide for AI-drafted petitions
Belge Şablonları uploads now also get extracted_text (same OCR/office pipeline as documents), and a new POST /api/drafting/generate builds a real AI draft from the case's actual data (title, UYAP parties, latest Dosya Özeti) — if the lawyer picks one of their own templates, its extracted text is passed as a style/format reference only (the prompt explicitly forbids copying the template's old case facts). Dilekçe Hazırlama no longer fakes a draft with setTimeout. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
50f6cbb1b2
commit
fa01432ffe
@@ -68,7 +68,7 @@ async function callAiModel(systemPrompt: string, userText: string): Promise<{ re
|
|||||||
return { resultJson, aiModel };
|
return { resultJson, aiModel };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function callAiModelRaw(systemPrompt: string, userText: string): Promise<{ aiContent: string; aiModel: string }> {
|
export 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`, {
|
||||||
@@ -168,6 +168,27 @@ export const registerLocalDocument = async (req: AuthenticatedRequest, res: Resp
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// uploadDocument ve uploadTemplate'in (template.controller.ts) ortak kullandığı
|
||||||
|
// metin çıkarma adımı: resimler için Tesseract OCR, ofis belgeleri için officeParser.
|
||||||
|
export async function extractTextFromUploadedFile(file: { buffer: Buffer; mimetype: string; originalname: string }): Promise<string> {
|
||||||
|
const mimeType = file.mimetype.toLowerCase();
|
||||||
|
|
||||||
|
if (mimeType.includes('image/')) {
|
||||||
|
let imageBuffer = file.buffer;
|
||||||
|
if (mimeType === 'image/heic' || file.originalname.toLowerCase().endsWith('.heic')) {
|
||||||
|
imageBuffer = await heicConvert({ buffer: file.buffer, format: 'JPEG', quality: 1 });
|
||||||
|
}
|
||||||
|
const { data: { text } } = await Tesseract.recognize(imageBuffer, 'tur');
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// officeParser natively supports pdf, docx, doc, rtf, txt, etc.
|
||||||
|
// officeparser@7.x parseOffice() bir sonuc objesi donduruyor (duz string degil),
|
||||||
|
// duz metin icin .toText() cagirmak gerekiyor.
|
||||||
|
const result: any = await officeParser.parseOffice(file.buffer);
|
||||||
|
return typeof result === 'string' ? result : (result?.toText ? result.toText() : String(result || ''));
|
||||||
|
}
|
||||||
|
|
||||||
export const uploadDocument = async (req: AuthenticatedRequest, res: Response) => {
|
export const uploadDocument = async (req: AuthenticatedRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const file = req.file;
|
const file = req.file;
|
||||||
@@ -179,38 +200,12 @@ export const uploadDocument = async (req: AuthenticatedRequest, res: Response) =
|
|||||||
}
|
}
|
||||||
|
|
||||||
let extractedText = '';
|
let extractedText = '';
|
||||||
const mimeType = file.mimetype.toLowerCase();
|
|
||||||
|
|
||||||
// IMAGE & OCR LOGIC
|
|
||||||
if (mimeType.includes('image/')) {
|
|
||||||
let imageBuffer = file.buffer;
|
|
||||||
|
|
||||||
// Convert HEIC to JPEG
|
|
||||||
if (mimeType === 'image/heic' || file.originalname.toLowerCase().endsWith('.heic')) {
|
|
||||||
imageBuffer = await heicConvert({
|
|
||||||
buffer: file.buffer,
|
|
||||||
format: 'JPEG',
|
|
||||||
quality: 1
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run Tesseract OCR (Turkish language)
|
|
||||||
const { data: { text } } = await Tesseract.recognize(imageBuffer, 'tur');
|
|
||||||
extractedText = text;
|
|
||||||
|
|
||||||
// DOCUMENT LOGIC
|
|
||||||
} else {
|
|
||||||
try {
|
try {
|
||||||
// officeParser natively supports pdf, docx, doc, rtf, txt, etc.
|
extractedText = await extractTextFromUploadedFile(file);
|
||||||
// officeparser@7.x parseOffice() bir sonuc objesi donduruyor (duz string degil),
|
|
||||||
// duz metin icin .toText() cagirmak gerekiyor.
|
|
||||||
const result: any = await officeParser.parseOffice(file.buffer);
|
|
||||||
extractedText = typeof result === 'string' ? result : (result?.toText ? result.toText() : String(result || ''));
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('officeParser Error:', err);
|
console.error('Text Extraction Error:', err);
|
||||||
return res.status(400).json({ error: 'Dosya formatı okunamadı veya desteklenmiyor.' });
|
return res.status(400).json({ error: 'Dosya formatı okunamadı veya desteklenmiyor.' });
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Upload File to Supabase Storage
|
// 2. Upload File to Supabase Storage
|
||||||
const filePath = `${user_id}/${Date.now()}_${file.originalname}`;
|
const filePath = `${user_id}/${Date.now()}_${file.originalname}`;
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { Response } from 'express';
|
||||||
|
import { supabase } from '../lib/supabase';
|
||||||
|
import { AuthenticatedRequest } from '../middleware/auth';
|
||||||
|
import { callAiModelRaw } from './document.controller';
|
||||||
|
|
||||||
|
const DRAFT_SYSTEM_PROMPT = `
|
||||||
|
Sen uzman bir Türk avukatısın. Sana verilen dosya bilgilerine göre, istenen türde
|
||||||
|
bir dilekçe TASLAĞI yazacaksın. Türk hukuk uygulamasına uygun, resmî bir dille yaz
|
||||||
|
(hitap, hususlar, netice-i talep gibi standart dilekçe yapısını kullan).
|
||||||
|
|
||||||
|
EĞER sana bir "BÜRO ŞABLONU" verilmişse: bu şablon, bu hukuk bürosunun kendi imzası
|
||||||
|
olan format/dil/üslup tercihidir — YALNIZCA bunun yapısını, dilini, başlıklandırmasını
|
||||||
|
ve üslubunu takip et. Şablondaki taraf isimleri, esas no'su, tarihler, mahkeme adı gibi
|
||||||
|
ESKİ dosyaya ait somut bilgileri ASLA kullanma veya kopyalama — onların yerine SADECE
|
||||||
|
aşağıda verilen YENİ dosyanın gerçek bilgilerini koy.
|
||||||
|
|
||||||
|
Sadece dilekçe taslağının kendisini döndür — başlık, açıklama veya ek yorum ekleme.
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Dilekçe Hazırlama ekranındaki "AI ile Taslak Oluştur" — dosyanın gerçek bilgilerini
|
||||||
|
// (başlık, UYAP'tan gelen taraflar, varsa Dosya Özeti) ve kullanıcının notlarını,
|
||||||
|
// isteğe bağlı olarak seçilen büro şablonunun DİLİNİ/FORMATINI referans alarak
|
||||||
|
// 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 userId = req.user?.id;
|
||||||
|
|
||||||
|
if (!case_id || !petition_type) {
|
||||||
|
return res.status(400).json({ error: 'case_id ve petition_type zorunludur.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: caseRow, error: caseError } = await supabase
|
||||||
|
.from('cases')
|
||||||
|
.select('id, title, parties, user_id')
|
||||||
|
.eq('id', case_id)
|
||||||
|
.single();
|
||||||
|
if (caseError || !caseRow) {
|
||||||
|
return res.status(404).json({ error: 'Dava dosyası bulunamadı.' });
|
||||||
|
}
|
||||||
|
if (caseRow.user_id !== userId) {
|
||||||
|
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();
|
||||||
|
|
||||||
|
let template: { name: string; extracted_text: string | null } | null = null;
|
||||||
|
if (template_id) {
|
||||||
|
const { data: templateRow } = await supabase
|
||||||
|
.from('templates')
|
||||||
|
.select('name, extracted_text, user_id')
|
||||||
|
.eq('id', template_id)
|
||||||
|
.single();
|
||||||
|
if (templateRow && templateRow.user_id === userId) {
|
||||||
|
template = templateRow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const partiesText = Array.isArray(caseRow.parties) && caseRow.parties.length > 0
|
||||||
|
? caseRow.parties.map((p: any) => `${p.adi || ''} (${p.rol || ''}${p.vekil ? `, Vekil: ${p.vekil}` : ''})`).join('; ')
|
||||||
|
: 'Belirtilmemiş — UYAP Taraf Bilgileri sekmesi henüz çekilmemiş olabilir.';
|
||||||
|
|
||||||
|
const ozet = lastAnalysis?.summary_json?.ozet;
|
||||||
|
|
||||||
|
let userContent = `YENİ DOSYA BİLGİLERİ:\nDosya: ${caseRow.title}\nTaraflar: ${partiesText}\nDilekçe Türü: ${petition_type}\n`;
|
||||||
|
if (notes && String(notes).trim()) {
|
||||||
|
userContent += `Vurgulanmasını istediğiniz noktalar: ${String(notes).trim()}\n`;
|
||||||
|
}
|
||||||
|
if (ozet && typeof ozet === 'string') {
|
||||||
|
userContent += `\nDosya Özeti (bağlam için):\n${ozet.slice(0, 4000)}\n`;
|
||||||
|
}
|
||||||
|
if (template?.extracted_text) {
|
||||||
|
userContent += `\n---\nBÜRO ŞABLONU "${template.name}" (SADECE dil/format/üslup referansı — içeriğini kopyalama):\n${template.extracted_text.slice(0, 6000)}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { aiContent, aiModel } = await callAiModelRaw(DRAFT_SYSTEM_PROMPT, userContent);
|
||||||
|
|
||||||
|
res.json({ draft: aiContent, model: aiModel, usedTemplate: !!template });
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Generate Draft Error:', error);
|
||||||
|
res.status(500).json({ error: error.message || 'Internal Server Error' });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
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 { extractTextFromUploadedFile } from './document.controller';
|
||||||
|
|
||||||
// Belge Şablonları: avukatın büro genelinde kullandığı dilekçe/evrak şablonları.
|
// Belge Şablonları: avukatın büro genelinde kullandığı dilekçe/evrak şablonları.
|
||||||
// Ayrı bir Storage bucket açmaya gerek kalmasın diye documents'ın kullandığı
|
// Ayrı bir Storage bucket açmaya gerek kalmasın diye documents'ın kullandığı
|
||||||
// "case-documents" bucket'ı "templates/" öneki altında paylaşılıyor.
|
// "case-documents" bucket'ı "templates/" öneki altında paylaşılıyor.
|
||||||
|
//
|
||||||
|
// Metni de çıkarıyoruz (documents'taki gibi) — Dilekçe Hazırlama'da bir şablon
|
||||||
|
// seçildiğinde AI'nin bu metni büronun kendi dili/formatı için referans alması
|
||||||
|
// gerekiyor (bkz. drafting.controller.ts).
|
||||||
export const uploadTemplate = async (req: AuthenticatedRequest, res: Response) => {
|
export const uploadTemplate = async (req: AuthenticatedRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const file = req.file;
|
const file = req.file;
|
||||||
@@ -15,6 +20,15 @@ export const uploadTemplate = async (req: AuthenticatedRequest, res: Response) =
|
|||||||
return res.status(400).json({ error: 'file ve name zorunludur.' });
|
return res.status(400).json({ error: 'file ve name zorunludur.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let extractedText: string | null = null;
|
||||||
|
try {
|
||||||
|
extractedText = await extractTextFromUploadedFile(file);
|
||||||
|
} catch (err) {
|
||||||
|
// Metin çıkarımı başarısız olsa da şablon yine de yüklenip indirilebilir olsun —
|
||||||
|
// sadece AI stil referansı olarak kullanılamayacak.
|
||||||
|
console.error('Template Text Extraction Error:', err);
|
||||||
|
}
|
||||||
|
|
||||||
const storagePath = `templates/${userId}/${Date.now()}_${file.originalname}`;
|
const storagePath = `templates/${userId}/${Date.now()}_${file.originalname}`;
|
||||||
const { error: uploadError } = await supabase.storage
|
const { error: uploadError } = await supabase.storage
|
||||||
.from('case-documents')
|
.from('case-documents')
|
||||||
@@ -34,6 +48,7 @@ export const uploadTemplate = async (req: AuthenticatedRequest, res: Response) =
|
|||||||
storage_path: storagePath,
|
storage_path: storagePath,
|
||||||
file_size: file.size,
|
file_size: file.size,
|
||||||
mime_type: file.mimetype,
|
mime_type: file.mimetype,
|
||||||
|
extracted_text: extractedText,
|
||||||
}])
|
}])
|
||||||
.select()
|
.select()
|
||||||
.single();
|
.single();
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import { requireAuth } from '../middleware/auth';
|
||||||
|
import { generateDraft } from '../controllers/drafting.controller';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.post('/generate', requireAuth, generateDraft);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -4,6 +4,7 @@ import chatRoutes from './chat.routes';
|
|||||||
import precedentRoutes from './precedent.routes';
|
import precedentRoutes from './precedent.routes';
|
||||||
import caseRoutes from './case.routes';
|
import caseRoutes from './case.routes';
|
||||||
import templateRoutes from './template.routes';
|
import templateRoutes from './template.routes';
|
||||||
|
import draftingRoutes from './drafting.routes';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@@ -15,6 +16,7 @@ router.use('/documents', documentRoutes);
|
|||||||
router.use('/chat', chatRoutes);
|
router.use('/chat', chatRoutes);
|
||||||
router.use('/cases', caseRoutes);
|
router.use('/cases', caseRoutes);
|
||||||
router.use('/templates', templateRoutes);
|
router.use('/templates', templateRoutes);
|
||||||
|
router.use('/drafting', draftingRoutes);
|
||||||
router.use('/', precedentRoutes); // Keeps the same paths like /api/search-precedents
|
router.use('/', precedentRoutes); // Keeps the same paths like /api/search-precedents
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
Reference in New Issue
Block a user