Jul 9 - Chat - save chat sessions

This commit is contained in:
2026-07-09 13:40:56 -04:00
parent 71aef4fe8e
commit 5cf85c564b
12 changed files with 471 additions and 50 deletions
+122 -33
View File
@@ -6,7 +6,8 @@ from flask import (Blueprint, render_template, redirect, url_for,
from flask_login import login_required, current_user
from app import db
from app.models.support import SupportTicket, SupportTicketReply
from app.models.support import (SupportTicket, SupportTicketReply,
SupportChatSession, SupportChatMessage)
from app.models.user import User
from app.models.facility import Facility
from app.utils.decorators import supervisor_required
@@ -65,11 +66,29 @@ def chat():
.filter(Facility.id.in_(cids), Facility.active == True)
.order_by(Facility.name).all()) if cids else []
# Load the customer's most recent conversation so it continues on return.
# A ?new=1 param (New conversation button) starts a fresh, empty window.
start_new = request.args.get('new')
session = None
if not start_new:
session = (SupportChatSession.query
.filter_by(customer_id=current_user.id)
.order_by(SupportChatSession.updated_at.desc())
.first())
chat_history = []
if session:
chat_history = [
{'role': m.role, 'content': m.content}
for m in session.messages.order_by(SupportChatMessage.created_at.asc()).all()
]
groq_ready = bool(os.environ.get('GROQ_API_KEY'))
return render_template('support/chat.html',
faqs=FAQS,
facilities=facilities,
groq_ready=groq_ready)
groq_ready=groq_ready,
chat_session_id=(session.id if session else None),
chat_history=chat_history)
# ── Groq chat AJAX endpoint ───────────────────────────────────────────────────
@@ -80,47 +99,92 @@ def chat_message():
if current_user.role != 'customer':
return jsonify({'error': 'Forbidden'}), 403
api_key = os.environ.get('GROQ_API_KEY')
if not api_key:
return jsonify({'reply': (
"I'm sorry, the AI assistant isn't configured right now. "
"Please use the **Submit to Support** form to reach our team directly."
)})
data = request.get_json(silent=True) or {}
history = data.get('messages', []) # list of {role, content} dicts
user_message = data.get('message', '').strip()
session_id = data.get('session_id')
if not user_message:
return jsonify({'error': 'Empty message'}), 400
try:
from groq import Groq
client = Groq(api_key=api_key)
# ── Generate the reply ────────────────────────────────────────────────
api_key = os.environ.get('GROQ_API_KEY')
if not api_key:
reply = ("I'm sorry, the AI assistant isn't configured right now. "
"Please use the **Submit to Support** form to reach our team directly.")
else:
try:
from groq import Groq
client = Groq(api_key=api_key)
messages = [{'role': 'system', 'content': _SYSTEM_PROMPT}]
# Append prior conversation (cap at last 20 turns to control token usage)
for m in history[-20:]:
if m.get('role') in ('user', 'assistant') and m.get('content'):
messages.append({'role': m['role'], 'content': m['content']})
messages.append({'role': 'user', 'content': user_message})
messages = [{'role': 'system', 'content': _SYSTEM_PROMPT}]
# Append prior conversation (cap at last 20 turns to control token usage)
for m in history[-20:]:
if m.get('role') in ('user', 'assistant') and m.get('content'):
messages.append({'role': m['role'], 'content': m['content']})
messages.append({'role': 'user', 'content': user_message})
model = os.environ.get('GROQ_MODEL', 'llama-3.3-70b-versatile')
completion = client.chat.completions.create(
model=model,
messages=messages,
max_tokens=512,
temperature=0.5,
)
reply = completion.choices[0].message.content.strip()
return jsonify({'reply': reply})
model = os.environ.get('GROQ_MODEL', 'llama-3.3-70b-versatile')
completion = client.chat.completions.create(
model=model,
messages=messages,
max_tokens=512,
temperature=0.5,
)
reply = completion.choices[0].message.content.strip()
except Exception as exc:
logger.error('SUPPORT | Groq error: %s', exc)
reply = ("I ran into a problem reaching the AI assistant. "
"Please try again, or use **Submit to Support** to contact our team.")
except Exception as exc:
logger.error('SUPPORT | Groq error: %s', exc)
return jsonify({'reply': (
"I ran into a problem reaching the AI assistant. "
"Please try again, or use **Submit to Support** to contact our team."
)})
# ── Persist the turn (user message + assistant reply) ─────────────────
now = now_eastern()
session = None
if session_id:
session = db.session.get(SupportChatSession, session_id)
if session is not None and session.customer_id != current_user.id:
session = None # never write into someone else's session
if session is None:
session = SupportChatSession(customer_id=current_user.id,
created_at=now, updated_at=now)
db.session.add(session)
db.session.flush() # assign session.id
db.session.add(SupportChatMessage(session_id=session.id, role='user',
content=user_message, created_at=now))
db.session.add(SupportChatMessage(session_id=session.id, role='assistant',
content=reply, created_at=now))
session.updated_at = now
db.session.commit()
return jsonify({'reply': reply, 'session_id': session.id})
# ── Customer: saved chat conversations ────────────────────────────────────────
@bp.route('/my-conversations')
@login_required
def my_conversations():
if current_user.role != 'customer':
abort(403)
sessions = (SupportChatSession.query
.filter_by(customer_id=current_user.id)
.order_by(SupportChatSession.updated_at.desc())
.all())
return render_template('support/my_conversations.html', sessions=sessions)
@bp.route('/my-conversations/<int:session_id>')
@login_required
def conversation_detail(session_id):
if current_user.role != 'customer':
abort(403)
session = db.session.get(SupportChatSession, session_id)
if session is None or session.customer_id != current_user.id:
abort(404)
messages = session.messages.order_by(SupportChatMessage.created_at.asc()).all()
return render_template('support/conversation_detail.html',
session=session, messages=messages)
# ── Submit support ticket ─────────────────────────────────────────────────────
@@ -327,6 +391,31 @@ def admin_ticket_detail(ticket_id):
replies=replies)
# ── Admin: view saved chat conversations (read-only) ──────────────────────────
@bp.route('/admin/conversations')
@login_required
@supervisor_required
def admin_conversations():
page = request.args.get('page', 1, type=int)
sessions = (SupportChatSession.query
.order_by(SupportChatSession.updated_at.desc())
.paginate(page=page, per_page=25, error_out=False))
return render_template('support/admin_conversations.html', sessions=sessions)
@bp.route('/admin/conversations/<int:session_id>')
@login_required
@supervisor_required
def admin_conversation_detail(session_id):
session = db.session.get(SupportChatSession, session_id)
if session is None:
abort(404)
messages = session.messages.order_by(SupportChatMessage.created_at.asc()).all()
return render_template('support/admin_conversation_detail.html',
session=session, messages=messages)
def _notify_customer_reply(ticket, reply):
"""Create an in-app notification and send an email to the customer."""
if not ticket.customer: