feat: add case-level AI summary endpoint

POST /api/cases/:caseId/summarize combines extracted text from all of
a case's documents and runs the existing AI analysis prompt, storing
the result as a case-level analysis (document_id: null). Powers the
"Özet Hazırla" button on the case detail screen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
mstfyldz
2026-08-09 16:14:41 +03:00
co-authored by Claude Sonnet 5
parent 5ddf6ce0cf
commit 9f5c5c1e45
3 changed files with 52 additions and 4 deletions
+46
View File
@@ -1,6 +1,7 @@
import { Response } from 'express';
import { supabase } from '../lib/supabase';
import { AuthenticatedRequest } from '../middleware/auth';
import { runDocumentAnalysis } from './document.controller';
export const createCase = async (req: AuthenticatedRequest, res: Response) => {
try {
@@ -26,6 +27,51 @@ export const createCase = async (req: AuthenticatedRequest, res: Response) => {
}
};
// Dava dosyasındaki TÜM belgelerin çıkarılmış metnini birleştirip tek bir AI
// özeti üretir (Dosya Ayrıntı ekranındaki "Özet Hazırla" butonu). uploadDocument'ın
// aksine tek bir belgeye değil, dosyanın o anki tüm belgelerine bakıyor.
export const summarizeCase = async (req: AuthenticatedRequest, res: Response) => {
try {
const { caseId } = req.params;
if (!caseId) {
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 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.' });
}
// Tüm belgeleri modele gönderiyoruz; runDocumentAnalysis toplamı ~15000 karaktere
// kırpıyor, o yüzden belge sayısına göre kabaca dengeli bir pay ayırıyoruz.
const perDocLimit = Math.max(1000, Math.floor(15000 / withText.length));
const combinedText = withText
.map((d) => `--- ${d.filename} ---\n${String(d.extracted_text).slice(0, perDocLimit)}`)
.join('\n\n');
const { summaryJson, aiModel } = await runDocumentAnalysis(combinedText);
const { data: analysisRecord, error: analysisError } = await supabase
.from('analyses')
.insert([{ case_id: caseId, document_id: null, summary_json: summaryJson, model_used: aiModel }])
.select()
.single();
if (analysisError) throw analysisError;
res.json({ analysis: analysisRecord });
} catch (error: any) {
console.error('Summarize Case Error:', error);
res.status(500).json({ error: error.message || 'Internal Server Error' });
}
};
export const deleteCase = async (req: AuthenticatedRequest, res: Response) => {
try {
const { caseId } = req.params;
+4 -3
View File
@@ -16,9 +16,10 @@ const ANALYSIS_SYSTEM_PROMPT = `
Sadece ve sadece JSON döndür, başka hiçbir açıklama ekleme.
`;
// uploadDocument ve registerLocalDocument'in ortak kullandığı AI analiz adımı:
// çıkarılmış metni modele gönderip yapılandırılmış özet JSON'u üretir.
async function runDocumentAnalysis(extractedText: string): Promise<{ summaryJson: any; aiModel: string }> {
// uploadDocument, registerLocalDocument ve summarizeCase'in (case.controller.ts)
// ortak kullandığı AI analiz adımı: çıkarılmış metni modele gönderip yapılandırılmış
// özet JSON'u üretir.
export async function runDocumentAnalysis(extractedText: string): Promise<{ summaryJson: any; aiModel: string }> {
const aiModel = process.env.AI_MODEL || 'mizan-fixed';
const runpodUrl = process.env.RUNPOD_API_BASE_URL || 'https://q190env94stwis-11434.proxy.runpod.net';
+2 -1
View File
@@ -1,10 +1,11 @@
import { Router } from 'express';
import { requireAuth } from '../middleware/auth';
import { createCase, deleteCase } from '../controllers/case.controller';
import { createCase, deleteCase, summarizeCase } from '../controllers/case.controller';
const router = Router();
router.post('/', requireAuth, createCase);
router.delete('/:caseId', requireAuth, deleteCase);
router.post('/:caseId/summarize', requireAuth, summarizeCase);
export default router;