updates Mar6

This commit is contained in:
belviskhoremk
2026-03-06 22:37:40 +00:00
parent 2ed998058e
commit 9dccc83293
23 changed files with 2257 additions and 74 deletions

View File

@@ -1,15 +1,73 @@
from fastapi import APIRouter, HTTPException, Depends
from app.models import ChatMessage, ChatResponse, ConversationResponse, MessageResponse
import time
from collections import defaultdict
from fastapi import APIRouter, HTTPException, Depends, Request
from app.models import ChatMessage, ChatResponse, ConversationResponse, MessageResponse, FeedbackCreate
from app.database import get_supabase
from app.dependencies import get_current_user, get_optional_user
from app.services.rag import rag_engine
from app.config import PLAN_LIMITS
from typing import List, Optional
from datetime import datetime
import uuid
import logging
logger = logging.getLogger(__name__)
router = APIRouter(tags=["Chat"])
# ── Simple in-memory rate limiter ────────────────────────────────────────────
_rate_store: dict = defaultdict(list)
_RATE_LIMIT = 30 # max requests
_RATE_WINDOW = 60 # per second window
def _check_rate_limit(client_ip: str):
now = time.time()
_rate_store[client_ip] = [t for t in _rate_store[client_ip] if now - t < _RATE_WINDOW]
if len(_rate_store[client_ip]) >= _RATE_LIMIT:
raise HTTPException(
status_code=429,
detail="Too many requests. Please wait before sending more messages.",
)
_rate_store[client_ip].append(now)
def _check_conversation_limit(chatbot: dict, supabase):
"""Check if the chatbot owner has remaining conversation quota this month."""
company_id = chatbot.get("company_id")
if not company_id:
return
company = supabase.table("companies").select("owner_id").eq("id", company_id).execute()
if not company.data:
return
owner_id = company.data[0]["owner_id"]
sub = supabase.table("subscriptions").select("plan").eq("user_id", owner_id).eq("status", "active").execute()
plan = sub.data[0]["plan"] if sub.data else "free"
limit = PLAN_LIMITS.get(plan, PLAN_LIMITS["free"]).get("conversations_limit", 100)
if limit >= 999999:
return # unlimited
month_start = datetime.utcnow().replace(day=1, hour=0, minute=0, second=0, microsecond=0).isoformat()
chatbots = supabase.table("chatbots").select("id").eq("company_id", company_id).execute()
chatbot_ids = [c["id"] for c in (chatbots.data or [])]
if not chatbot_ids:
return
count_result = supabase.table("conversations").select("id", count="exact") \
.in_("chatbot_id", chatbot_ids) \
.gte("created_at", month_start) \
.execute()
used = count_result.count or 0
if used >= limit:
raise HTTPException(
status_code=429,
detail=f"This chatbot's monthly conversation limit ({limit}) has been reached. Please try again next month.",
)
def _get_public_chatbot(chatbot_id: str, supabase) -> dict:
"""Get a published chatbot (or any chatbot for preview)"""
@@ -23,8 +81,13 @@ def _get_public_chatbot(chatbot_id: str, supabase) -> dict:
async def chat(
chatbot_id: str,
message: ChatMessage,
request: Request,
user=Depends(get_optional_user),
):
# Rate limiting by IP
client_ip = request.client.host if request.client else "unknown"
_check_rate_limit(client_ip)
supabase = get_supabase()
chatbot = _get_public_chatbot(chatbot_id, supabase)
@@ -43,6 +106,13 @@ async def chat(
# Get or create conversation
session_id = message.session_id or str(uuid.uuid4())
# Only enforce conversation limit for new sessions (not ongoing ones)
is_existing = supabase.table("conversations").select("id") \
.eq("chatbot_id", chatbot_id).eq("session_id", session_id).execute()
if not is_existing.data:
_check_conversation_limit(chatbot, supabase)
conversation = _get_or_create_conversation(
chatbot_id=chatbot_id,
session_id=session_id,
@@ -70,6 +140,42 @@ async def chat(
language=message.language,
)
# Compute confidence score
confidence_score = max((s.score for s in result.get("sources", [])), default=0.0)
# Check handoff
is_handoff = False
if chatbot.get("handoff_enabled"):
handoff_keywords = chatbot.get("handoff_keywords", [])
msg_lower = message.message.lower()
if any(kw.lower() in msg_lower for kw in handoff_keywords):
is_handoff = True
# Fire n8n notification (async, non-blocking)
try:
from app.services.n8n_service import send_handoff_notification
from app.config import settings as _settings
company_data_for_handoff = chatbot.get("companies", {}) or {}
await send_handoff_notification(
chatbot_name=chatbot.get("name", ""),
owner_email=chatbot.get("handoff_email") or "",
conversation_history=history,
trigger_message=message.message,
chatbot_id=chatbot_id,
conversation_id=conversation["id"],
webhook_url=_settings.n8n_handoff_webhook_url,
)
except Exception:
pass # never block chat on handoff failure
# Check lead capture
needs_lead_capture = False
if (
chatbot.get("lead_capture_enabled")
and chatbot.get("lead_capture_trigger") == "after_first_message"
and len(history) == 0
):
needs_lead_capture = True
# Save messages
_save_message(conversation["id"], "user", message.message, supabase)
_save_message(
@@ -79,6 +185,8 @@ async def chat(
supabase,
sources=[s.model_dump() for s in result.get("sources", [])],
model=result.get("model", ""),
confidence_score=confidence_score,
is_handoff=is_handoff,
)
# Update conversation message count
@@ -92,6 +200,8 @@ async def chat(
sources=result.get("sources", []),
model_used=result.get("model", ""),
tokens_used=result.get("tokens_used", 0),
needs_lead_capture=needs_lead_capture,
handoff=is_handoff,
)
@@ -129,6 +239,30 @@ async def get_chat_history(
]
@router.post("/chat/{chatbot_id}/feedback")
async def submit_feedback(chatbot_id: str, data: FeedbackCreate):
"""Submit feedback (thumbs up/down) for a message. No auth required."""
import uuid as _uuid
if data.feedback not in ("positive", "negative"):
raise HTTPException(status_code=400, detail="Feedback must be 'positive' or 'negative'")
supabase = get_supabase()
# Verify message exists
msg = supabase.table("messages").select("id, conversation_id").eq("id", data.message_id).execute()
if not msg.data:
raise HTTPException(status_code=404, detail="Message not found")
supabase.table("message_feedback").insert({
"id": str(_uuid.uuid4()),
"message_id": data.message_id,
"chatbot_id": chatbot_id,
"feedback": data.feedback,
}).execute()
return {"success": True}
# ── OLD analytics endpoint REMOVED ───────────────────────────────────────────
# The /analytics/{chatbot_id} endpoint that was here has been replaced by
# the dedicated analytics router (app/routers/analytics.py) which provides:
@@ -188,6 +322,8 @@ def _save_message(
supabase,
sources: Optional[list] = None,
model: str = "",
confidence_score: Optional[float] = None,
is_handoff: bool = False,
):
supabase.table("messages").insert({
"id": str(uuid.uuid4()),
@@ -196,4 +332,6 @@ def _save_message(
"content": content,
"sources": sources,
"model": model,
"confidence_score": confidence_score,
"is_handoff": is_handoff,
}).execute()