- Introduced SiteAssistant component to assist users in selecting the right tools based on their queries. - Integrated assistant into the main App component. - Implemented message handling and storage for user-assistant interactions. - Added quick prompts for common user queries related to tools. - Enhanced ToolLandingPage and DownloadButton components with SharePanel for sharing tool results. - Updated translations for new assistant features and sharing options. - Added API methods for chat functionality with the assistant, including streaming responses.
84 lines
2.8 KiB
Python
84 lines
2.8 KiB
Python
"""Site assistant routes — global AI helper for the product UI."""
|
|
import json
|
|
|
|
from flask import Blueprint, Response, jsonify, request, stream_with_context
|
|
|
|
from app.extensions import limiter
|
|
from app.services.policy_service import resolve_web_actor
|
|
from app.services.site_assistant_service import chat_with_site_assistant, stream_site_assistant_chat
|
|
|
|
assistant_bp = Blueprint("assistant", __name__)
|
|
|
|
|
|
def _parse_chat_payload():
|
|
payload = request.get_json(silent=True) or {}
|
|
|
|
message = str(payload.get("message", "")).strip()
|
|
if not message:
|
|
return None, (jsonify({"error": "Message is required."}), 400)
|
|
|
|
if len(message) > 4000:
|
|
return None, (jsonify({"error": "Message is too long."}), 400)
|
|
|
|
actor = resolve_web_actor()
|
|
return {
|
|
"message": message,
|
|
"session_id": str(payload.get("session_id", "")).strip() or None,
|
|
"fingerprint": str(payload.get("fingerprint", "")).strip() or "anonymous",
|
|
"tool_slug": str(payload.get("tool_slug", "")).strip(),
|
|
"page_url": str(payload.get("page_url", "")).strip(),
|
|
"locale": str(payload.get("locale", "en")).strip() or "en",
|
|
"user_id": actor.user_id,
|
|
"history": payload.get("history") if isinstance(payload.get("history"), list) else None,
|
|
}, None
|
|
|
|
|
|
def _sse(event: str, data: dict) -> str:
|
|
return f"event: {event}\ndata: {json.dumps(data, ensure_ascii=True)}\n\n"
|
|
|
|
|
|
@assistant_bp.route("/chat", methods=["POST"])
|
|
@limiter.limit("20/minute")
|
|
def assistant_chat_route():
|
|
"""Answer a product-help message and store the conversation for later analysis."""
|
|
chat_kwargs, error_response = _parse_chat_payload()
|
|
if error_response:
|
|
return error_response
|
|
|
|
try:
|
|
result = chat_with_site_assistant(**chat_kwargs)
|
|
except ValueError as exc:
|
|
return jsonify({"error": str(exc)}), 400
|
|
except Exception:
|
|
return jsonify({"error": "Assistant is temporarily unavailable."}), 500
|
|
|
|
return jsonify(result), 200
|
|
|
|
|
|
@assistant_bp.route("/chat/stream", methods=["POST"])
|
|
@limiter.limit("20/minute")
|
|
def assistant_chat_stream_route():
|
|
"""Stream assistant replies incrementally over SSE."""
|
|
chat_kwargs, error_response = _parse_chat_payload()
|
|
if error_response:
|
|
return error_response
|
|
|
|
try:
|
|
events = stream_site_assistant_chat(**chat_kwargs)
|
|
except ValueError as exc:
|
|
return jsonify({"error": str(exc)}), 400
|
|
except Exception:
|
|
return jsonify({"error": "Assistant is temporarily unavailable."}), 500
|
|
|
|
def generate():
|
|
for event in events:
|
|
yield _sse(event["event"], event["data"])
|
|
|
|
return Response(
|
|
stream_with_context(generate()),
|
|
content_type="text/event-stream; charset=utf-8",
|
|
headers={
|
|
"Cache-Control": "no-cache",
|
|
"X-Accel-Buffering": "no",
|
|
},
|
|
) |