diff --git a/routes/ai_summary.py b/routes/ai_summary.py index b6de276..d9f73ef 100644 --- a/routes/ai_summary.py +++ b/routes/ai_summary.py @@ -450,6 +450,72 @@ def delete_criterion_view(criterion_id): return redirect(url_for("ai_summary.ai_summary")) +@ai_summary_bp.route("/chat", methods=["POST"]) +@login_required +def chat(): + """Follow-up Q&A about a previously analyzed document.""" + user = session["user"] + data = request.get_json(silent=True) or {} + analysis_id = data.get("analysis_id") + messages = data.get("messages") or [] + + if not analysis_id: + return jsonify({"error": "analysis_id is required."}), 400 + if not messages or not isinstance(messages, list): + return jsonify({"error": "messages is required."}), 400 + + record = get_ai_analysis_detail(int(analysis_id)) + if not record: + return jsonify({"error": "Analysis not found."}), 404 + + api_key = (os.environ.get("GROQ_API_KEY") or "").strip() or get_setting("groq.api_key", "").strip() + if not api_key: + return jsonify({"error": "Groq API key is not configured."}), 400 + + model = get_setting("groq.model", GROQ_MODELS[0]) + summary = (record.get("summary_text") or "")[:8000] + + system_msg = ( + "You are a government procurement analyst assistant. " + "The user has questions about a solicitation document that has already been analyzed. " + "Answer questions based solely on the analysis summary below. " + "If the information is not in the summary, say so clearly. Be concise and direct.\n\n" + "## ANALYZED DOCUMENT SUMMARY\n\n" + summary + ) + + # Sanitize and cap conversation history at 20 turns + api_messages = [] + for m in messages[-20:]: + role = m.get("role") if isinstance(m, dict) else None + content = (m.get("content") or "").strip() if isinstance(m, dict) else "" + if role in ("user", "assistant") and content: + api_messages.append({"role": role, "content": content}) + + if not api_messages or api_messages[-1]["role"] != "user": + return jsonify({"error": "Last message must be from the user."}), 400 + + try: + response = http_requests.post( + "https://api.groq.com/openai/v1/chat/completions", + headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}, + json={ + "model": model, + "messages": [{"role": "system", "content": system_msg}] + api_messages, + "max_tokens": 1024, + "temperature": 0.2, + }, + timeout=60, + ) + if not response.ok: + logger.error(f"Groq chat {response.status_code}: {response.text[:200]}") + response.raise_for_status() + reply = response.json()["choices"][0]["message"]["content"] or "" + return jsonify({"reply": reply}) + except Exception as e: + logger.error(f"AI chat error: {e}") + return jsonify({"error": f"Chat failed: {e}"}), 500 + + @ai_summary_bp.route("/history/") @login_required def analysis_detail(analysis_id): diff --git a/templates/ai_summary.html b/templates/ai_summary.html index 693dcce..4cdbabe 100644 --- a/templates/ai_summary.html +++ b/templates/ai_summary.html @@ -96,6 +96,7 @@ + @@ -269,16 +270,33 @@ + + + {% endblock %}