feat: add site assistant component for guided tool selection
- 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.
This commit is contained in:
84
backend/app/routes/assistant.py
Normal file
84
backend/app/routes/assistant.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""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",
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user