Spaces:
Running
Running
| """ | |
| agents/reflection_agent.py — Self-Check / Reflection Agent | |
| AGENTIC PATTERN: Reflection Pattern | |
| ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | |
| Reflection pattern: Agent kendi çıktısını veya mevcut state'i | |
| "dışarıdan bakarak" değerlendirir; eksiklik veya tutarsızlık | |
| varsa bunu işaretler ve ilgili adıma geri döner. | |
| Bu agent, onay ekranı gösterilmeden önce çalışır: | |
| - Tüm zorunlu slot'lar dolu mu? | |
| - Kefil zorunluysa TCKN var mı? | |
| - Çapraz kurallar hâlâ tutarlı mı? (örn: fatura değişince oran) | |
| Neden Reflection önemli? | |
| - Slot filling sırasında oluşabilecek edge case'leri yakalar | |
| - LLM'e "güvenme, kontrol et" felsefesini uygular | |
| - Başvuru DB'ye hatalı kayıt yazılmasını önler | |
| """ | |
| import json | |
| import time | |
| from langchain_core.messages import AIMessage | |
| from llm.factory import get_router_llm | |
| from llm.prompts import REFLECTION_SYSTEM | |
| from graph.state import ConversationState | |
| from observability.audit_logger import log_llm_call | |
| from config import NEW_CAR_GUARANTOR_THRESHOLD | |
| # Ham slot adlarını müşteriye gösterilecek dostça etiketlere çevir | |
| SLOT_LABELS = { | |
| "invoice_amount": "Proforma fatura tutarı", | |
| "vehicle_model": "Araç modeli", | |
| "requested_amount_new": "İstenen finansman tutarı", | |
| "guarantor_tckn": "Kefil TC kimlik numarası", | |
| "kasko_value": "Kasko değeri", | |
| "vehicle_age": "Araç yaşı", | |
| "requested_amount_used": "İstenen finansman tutarı", | |
| "seller_tckn": "Satıcı TC kimlik numarası", | |
| } | |
| # Geçersiz veya eksik slot için yeniden sorulacak sorular | |
| SLOT_QUESTIONS = { | |
| "invoice_amount": "Proforma fatura tutarı nedir? (TL olarak)", | |
| "vehicle_model": "Aracın marka ve modelini belirtir misiniz? (örn: Toyota Corolla)", | |
| "requested_amount_new": "Ne kadar finansman almak istiyorsunuz? (TL olarak)", | |
| "guarantor_tckn": "Kefil TC kimlik numarası nedir?", | |
| "kasko_value": "Aracın güncel kasko değeri nedir? (TL olarak)", | |
| "vehicle_age": "Aracın model yılı veya yaşı nedir? (örn: 2021 model veya 3 yaşında)", | |
| "requested_amount_used": "Ne kadar finansman almak istiyorsunuz? (TL olarak)", | |
| "seller_tckn": "Satıcının TC kimlik numarası nedir? (Bilmiyorsanız 'geç' yazabilirsiniz)", | |
| } | |
| def run_reflection(state: ConversationState) -> dict: | |
| """ | |
| Reflection node fonksiyonu. | |
| Returns: | |
| complete=True → recap'e geç | |
| complete=False → eksik/geçersiz alana geri dön | |
| """ | |
| vehicle_type = state.get("vehicle_type") | |
| # Deterministik kontroller (LLM çağrısı yapmadan) | |
| issues = [] # kural ihlali mesajları (dostça metin) | |
| missing = [] # hiç doldurulmamış (None) slot'lar | |
| reset_slots = [] # dolu ama geçersiz → temizlenip yeniden sorulacak slot'lar | |
| if vehicle_type == "new": | |
| if not state.get("invoice_amount"): | |
| missing.append("invoice_amount") | |
| if not state.get("vehicle_model"): | |
| missing.append("vehicle_model") | |
| if not state.get("requested_amount_new"): | |
| missing.append("requested_amount_new") | |
| # Kefil kontrolü | |
| if state.get("kefil_required") and not state.get("guarantor_tckn"): | |
| missing.append("guarantor_tckn") | |
| # Çapraz kural: fatura değiştiyse oran hâlâ geçerli mi? | |
| if state.get("invoice_amount") and state.get("requested_amount_new"): | |
| max_allowed = state["invoice_amount"] * 0.60 | |
| if state["requested_amount_new"] > max_allowed: | |
| issues.append( | |
| f"Finansman tutarı ({_fmt(state['requested_amount_new'])} TL), " | |
| f"proforma tutarının %60'ını ({_fmt(max_allowed)} TL) aşıyor." | |
| ) | |
| reset_slots.append("requested_amount_new") | |
| elif vehicle_type == "used": | |
| if not state.get("kasko_value"): | |
| missing.append("kasko_value") | |
| if state.get("vehicle_age") is None: | |
| missing.append("vehicle_age") | |
| if not state.get("requested_amount_used"): | |
| missing.append("requested_amount_used") | |
| # Çapraz kural | |
| if state.get("kasko_value") and state.get("requested_amount_used"): | |
| max_allowed = min(state["kasko_value"] * 0.40, 3_000_000) | |
| if state["requested_amount_used"] > max_allowed: | |
| issues.append( | |
| f"Finansman tutarı ({_fmt(state['requested_amount_used'])} TL) limiti aşıyor. " | |
| f"Maksimum: {_fmt(max_allowed)} TL." | |
| ) | |
| reset_slots.append("requested_amount_used") | |
| if missing or issues: | |
| parts = list(issues) | |
| if missing: | |
| labels = [SLOT_LABELS.get(m, m) for m in missing] | |
| parts.append("Eksik bilgiler: " + ", ".join(labels)) | |
| # Önce geçersiz değeri düzelt, yoksa ilk eksik alanı tamamlat | |
| target = reset_slots[0] if reset_slots else (missing[0] if missing else None) | |
| if target: | |
| question = SLOT_QUESTIONS.get(target, f"Lütfen {SLOT_LABELS.get(target, target).lower()} bilgisini girin.") | |
| parts.append(question) | |
| msg = "Başvuruyu kontrol ettim, düzeltmemiz gereken noktalar var:\n" + "\n".join(f"• {p}" for p in parts) | |
| updates = { | |
| "current_step": "reflection_issues_found", | |
| "messages": state["messages"] + [AIMessage(content=msg)], | |
| "errors": state["errors"] + issues, | |
| "edit_target_slot": target, | |
| } | |
| # Geçersiz slot'ları temizle ki intake yeniden sorabilsin | |
| for s in reset_slots: | |
| updates[s] = None | |
| # Yeniden sorulacak slot'u awaited olarak işaretle → kullanıcı cevabı parse edilir | |
| if target: | |
| updates["awaited_slot"] = target | |
| return updates | |
| # Her şey tamam | |
| return {"current_step": "reflection_passed"} | |
| def _fmt(amount: float) -> str: | |
| return f"{int(amount):,}".replace(",", ".") | |