feat: WhatsApp AI hattı (n8n + Evolution API) için yeni uç nokta
chat.controller.ts'teki dosya-bağlamı toplama mantığı (belgeler + analiz özeti + geçmiş mesajlar) buildCaseChatContext() olarak paylaşılan hale getirildi — mevcut SSE sohbeti (chatMessage) davranış değişmeden bunu kullanıyor, yeni POST /api/whatsapp/message uç noktası da aynı motoru tek-seferlik (non-streaming) çağırıyor. Kimlik doğrulama farklı: bu uç nokta bir Supabase oturumu taşımıyor (n8n'den gelen anonim WhatsApp mesajı), X-Webhook-Secret ile korunuyor. Telefon → hesap eşleştirmesi profiles.phone üzerinden, hangi dava dosyasıyla konuşulduğu whatsapp_sessions'ta tutuluyor (bkz. laawos sql/20_whatsapp_integration.sql). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
fed881de49
commit
5fe4b0b8bc
@@ -90,6 +90,91 @@ export const deleteChatHistory = async (req: AuthenticatedRequest, res: Response
|
||||
}
|
||||
};
|
||||
|
||||
interface CaseChatContextOpts {
|
||||
chat_mode?: string;
|
||||
persona?: string;
|
||||
document_ids?: string[];
|
||||
attachments?: unknown;
|
||||
historyLimit?: number;
|
||||
// pastMessages sorgusu boş dönerse (normalde olmaz, çağıran taraf zaten kendi
|
||||
// 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;
|
||||
}
|
||||
|
||||
// chatMessage (masaüstü "AyrisLegal'e Sor" sohbeti) ve WhatsApp uç noktasının
|
||||
// (whatsapp.controller.ts) İKİSİ de aynı dosya bağlamını (belgeler + analiz özeti +
|
||||
// geçmiş mesajlar) aynı şekilde topluyor — tek fark biri SSE ile akıtıyor, diğeri
|
||||
// tek seferde JSON dönüyor. Bu fonksiyon o ortak mantığı taşıyor; caller kendi
|
||||
// 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 targetPersona = persona || 'judge';
|
||||
|
||||
const { data: analyses } = await supabase
|
||||
.from('analyses')
|
||||
.select('summary_json')
|
||||
.eq('case_id', caseId);
|
||||
|
||||
const { data: documents } = await supabase
|
||||
.from('documents')
|
||||
.select('id, filename, extracted_text')
|
||||
.eq('case_id', caseId);
|
||||
|
||||
const { data: pastMessages } = await supabase
|
||||
.from('chat_messages')
|
||||
.select('role, content')
|
||||
.eq('case_id', caseId)
|
||||
.eq('chat_mode', chat_mode)
|
||||
.order('created_at', { ascending: true })
|
||||
.limit(historyLimit);
|
||||
|
||||
// Belge bağlamı: her zaman TAM metin gönderiliyor (özet-önce yaklaşımı, kullanıcı
|
||||
// uzun bir mesaj/belge verdiğinde sohbet ekranında soruna yol açtığı için geri alındı).
|
||||
let documentContext = '';
|
||||
if (documents && documents.length > 0) {
|
||||
documentContext = '\n\nDOSYAYA AİT BELGELER:\n' +
|
||||
documents.map(d => `--- ${d.filename} ---\n${d.extracted_text}`).join('\n\n');
|
||||
if (Array.isArray(document_ids) && document_ids.length > 0) {
|
||||
const selected = documents.filter(d => document_ids.includes(d.id));
|
||||
if (selected.length > 0) {
|
||||
documentContext += '\n\nKULLANICININ BU MESAJDA ÖZELLİKLE İŞARET ETTİĞİ BELGE(LER): ' + selected.map(d => d.filename).join(', ') + '.';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
|
||||
documentContext += buildAttachmentContext(attachments);
|
||||
|
||||
let systemPrompt = `Sen bir hukuk asistanısın. Bu dava dosyasıyla ilgili kullanıcının sorularını, aşağıda verilen belge içeriğine dayanarak cevaplıyorsun. Eğer belge yoksa veya soruyu cevaplamaya yetmiyorsa, bunu açıkça belirt, uydurma bilgi verme.${documentContext}`;
|
||||
|
||||
if (chat_mode === 'intern') {
|
||||
systemPrompt = `Sen bir dijital stajyersin (Canlı Arama Asistanı). Kullanıcı şu anda duruşmada veya acil bir durumda ve senden ÇOK HIZLI, KISA ve NOKTA ATIŞI bir bilgi bekliyor. Asla uzun cümleler kurma, merhaba/nasılsın deme. Yalnızca istenen tarihi, belge adını veya spesifik bilgiyi 1-2 cümleyle ver.\n${documentContext}`;
|
||||
} else if (chat_mode === 'simulator') {
|
||||
if (targetPersona === 'opponent') {
|
||||
systemPrompt = `Sen bu davanın KARŞI TARAF AVUKATISIN. Amacın avukat kullanıcının tezlerindeki çelişkileri, mantık hatalarını ve delil eksikliklerini sert bir şekilde VURMAK ve YÜZÜNE VURMAKTIR. Çapraz sorgu yap, açık arayan zorlayıcı sorular sor ve onun tezini zayıflatmak için çaba sarf et. Asla kibarlık veya tavsiye yapma, tamamen karşı taraf rolünü oyna!\n${documentContext}`;
|
||||
} else { // judge
|
||||
systemPrompt = `Sen bu davanın MAHKEME HAKİMİSİN. HMK/CMK/İYUK usul kurallarına son derece bağlı, titiz ve sorgulayıcı bir duruşma hakimisin. Avukat kullanıcının her iddiasını somut delil ve kanun maddesiyle ispatlamasını iste, açıklarını yüzüne vur ve çapraz sorgula.\n${documentContext}`;
|
||||
}
|
||||
} else if (chat_mode === 'whatsapp') {
|
||||
systemPrompt = `Sen bir hukuk asistanısın. Bu dava dosyasıyla ilgili kullanıcının WhatsApp üzerinden sorduğu soruları, aşağıda verilen belge içeriğine dayanarak cevaplıyorsun. KISA VE ÖZ cevap ver — bir WhatsApp mesajı yazıyorsun, uzun paragraflar yerine net ve aksiyona dönük bilgi ver. Eğer belge yoksa veya soruyu cevaplamaya yetmiyorsa, bunu açıkça belirt, uydurma bilgi verme.${documentContext}`;
|
||||
}
|
||||
|
||||
const messages: ChatMsg[] = [{ role: 'system', content: systemPrompt }];
|
||||
if (pastMessages && pastMessages.length > 0) {
|
||||
pastMessages.forEach(m => messages.push({ role: m.role, content: m.content }));
|
||||
} else if (fallbackUserContent) {
|
||||
messages.push({ role: 'user', content: fallbackUserContent });
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
// Bu dosyanın "AyrisLegal'e Sor" sohbeti — yanıtı Server-Sent Events ile token
|
||||
// token akıtır (bkz. lib/aiClient.ts streamOllama). Kullanıcı tüm yanıtı beklemek
|
||||
// yerine yazılırken görür; gerçek üretim süresi aynı kalsa da algılanan hız artar.
|
||||
@@ -117,63 +202,13 @@ export const chatMessage = async (req: AuthenticatedRequest, res: Response) => {
|
||||
.insert([{ case_id: caseId, user_id: userId, role: 'user', content: persistedContent, chat_mode }]);
|
||||
if (userMsgError) throw userMsgError;
|
||||
|
||||
const { data: analyses } = await supabase
|
||||
.from('analyses')
|
||||
.select('summary_json')
|
||||
.eq('case_id', caseId);
|
||||
|
||||
const { data: documents } = await supabase
|
||||
.from('documents')
|
||||
.select('id, filename, extracted_text')
|
||||
.eq('case_id', caseId);
|
||||
|
||||
const { data: pastMessages } = await supabase
|
||||
.from('chat_messages')
|
||||
.select('role, content')
|
||||
.eq('case_id', caseId)
|
||||
.eq('chat_mode', chat_mode)
|
||||
.order('created_at', { ascending: true })
|
||||
.limit(10);
|
||||
|
||||
// Belge bağlamı: her zaman TAM metin gönderiliyor (özet-önce yaklaşımı, kullanıcı
|
||||
// uzun bir mesaj/belge verdiğinde sohbet ekranında soruna yol açtığı için geri alındı).
|
||||
let documentContext = '';
|
||||
if (documents && documents.length > 0) {
|
||||
documentContext = '\n\nDOSYAYA AİT BELGELER:\n' +
|
||||
documents.map(d => `--- ${d.filename} ---\n${d.extracted_text}`).join('\n\n');
|
||||
if (Array.isArray(document_ids) && document_ids.length > 0) {
|
||||
const selected = documents.filter(d => document_ids.includes(d.id));
|
||||
if (selected.length > 0) {
|
||||
documentContext += '\n\nKULLANICININ BU MESAJDA ÖZELLİKLE İŞARET ETTİĞİ BELGE(LER): ' + selected.map(d => d.filename).join(', ') + '.';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
|
||||
documentContext += buildAttachmentContext(attachments);
|
||||
|
||||
let systemPrompt = `Sen bir hukuk asistanısın. Bu dava dosyasıyla ilgili kullanıcının sorularını, aşağıda verilen belge içeriğine dayanarak cevaplıyorsun. Eğer belge yoksa veya soruyu cevaplamaya yetmiyorsa, bunu açıkça belirt, uydurma bilgi verme.${documentContext}`;
|
||||
|
||||
if (chat_mode === 'intern') {
|
||||
systemPrompt = `Sen bir dijital stajyersin (Canlı Arama Asistanı). Kullanıcı şu anda duruşmada veya acil bir durumda ve senden ÇOK HIZLI, KISA ve NOKTA ATIŞI bir bilgi bekliyor. Asla uzun cümleler kurma, merhaba/nasılsın deme. Yalnızca istenen tarihi, belge adını veya spesifik bilgiyi 1-2 cümleyle ver.\n${documentContext}`;
|
||||
} else if (chat_mode === 'simulator') {
|
||||
if (targetPersona === 'opponent') {
|
||||
systemPrompt = `Sen bu davanın KARŞI TARAF AVUKATISIN. Amacın avukat kullanıcının tezlerindeki çelişkileri, mantık hatalarını ve delil eksikliklerini sert bir şekilde VURMAK ve YÜZÜNE VURMAKTIR. Çapraz sorgu yap, açık arayan zorlayıcı sorular sor ve onun tezini zayıflatmak için çaba sarf et. Asla kibarlık veya tavsiye yapma, tamamen karşı taraf rolünü oyna!\n${documentContext}`;
|
||||
} else { // judge
|
||||
systemPrompt = `Sen bu davanın MAHKEME HAKİMİSİN. HMK/CMK/İYUK usul kurallarına son derece bağlı, titiz ve sorgulayıcı bir duruşma hakimisin. Avukat kullanıcının her iddiasını somut delil ve kanun maddesiyle ispatlamasını iste, açıklarını yüzüne vur ve çapraz sorgula.\n${documentContext}`;
|
||||
}
|
||||
}
|
||||
const messages: ChatMsg[] = [{ role: 'system', content: systemPrompt }];
|
||||
if (pastMessages && pastMessages.length > 0) {
|
||||
pastMessages.forEach(m => messages.push({ role: m.role, content: m.content }));
|
||||
} else {
|
||||
messages.push({ role: 'user', content: persistedContent });
|
||||
}
|
||||
const messages = await buildCaseChatContext(caseId as string, {
|
||||
chat_mode,
|
||||
persona: targetPersona,
|
||||
document_ids,
|
||||
attachments,
|
||||
fallbackUserContent: persistedContent,
|
||||
});
|
||||
|
||||
startSse(res);
|
||||
const aiContent = await streamOllama(messages, (delta) => sseWrite(res, { delta }), { numPredict: 2048 });
|
||||
@@ -188,7 +223,10 @@ export const chatMessage = async (req: AuthenticatedRequest, res: Response) => {
|
||||
// bu gerçek bir dava dosyası değil de dosyasız bir sohbetse, ilk mesajdan kısa
|
||||
// bir başlık üretip yeniden adlandır.
|
||||
let newTitle: string | null = null;
|
||||
const isFirstMessage = !pastMessages || pastMessages.length <= 1;
|
||||
// messages = [system, ...pastMessages]; pastMessages her zaman en az bu isteğin
|
||||
// kendi mesajını içeriyor (buildCaseChatContext'ten önce insert edildi) — bu
|
||||
// yüzden messages.length<=2 ("ilk mesaj" hali) eskiden !pastMessages||pastMessages.length<=1 ile aynı anlama geliyor.
|
||||
const isFirstMessage = messages.length <= 2;
|
||||
if (isFirstMessage && caseKind === 'chat') {
|
||||
newTitle = await generateShortTitle(content);
|
||||
if (newTitle) {
|
||||
|
||||
Reference in New Issue
Block a user