Fix data isolation + conversation/decision 500 errors
Data isolation: - GET /tasks/<id>: verify requesting user owns the task (403 if not) - DELETE /tasks/<id>: same ownership check - GET /tasks/status: add @jwt_required() - GET /personas/<id>: add ownership check (403 if created_by != user) - GET /focus-groups/<id>: add ownership check - GET /focus-groups/<id>/messages: add ownership check - POST/DELETE /focus-groups/<id>/participants: add ownership check Fix conversation/decision 500: - Convert POST /conversation/decision to async 202+background (was synchronous LLM → timed out / LLM errors → 500) - Frontend polls waitForTaskResult for decision result before calling generateResponseAsync - GET /conversation/insights: return empty insights (200) on LLM error instead of 500 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f0876649e0
commit
4b47b334d7
6 changed files with 103 additions and 70 deletions
|
|
@ -976,44 +976,49 @@ async def get_conversation_analytics(focus_group_id):
|
|||
async def make_conversation_decision(focus_group_id):
|
||||
"""
|
||||
Make a conversation decision using the LLM decision engine.
|
||||
|
||||
Request body:
|
||||
{
|
||||
"temperature": 0.7, # Optional, defaults to 0.7
|
||||
"mode": "ai" # Optional, "ai" for autonomous mode, "manual" for manual mode, defaults to "ai"
|
||||
}
|
||||
|
||||
Returns:
|
||||
A JSON object containing the decision
|
||||
Returns 202 immediately; poll GET /tasks/{task_id} for result.
|
||||
"""
|
||||
try:
|
||||
user_id = get_jwt_identity()
|
||||
data = (await request.get_json()) or {}
|
||||
temperature = data.get('temperature', 0.7)
|
||||
mode = data.get('mode', 'ai') # Default to 'ai' mode for backward compatibility
|
||||
|
||||
# Make decision
|
||||
decision = await ConversationDecisionService.decide_next_action(focus_group_id, temperature, mode)
|
||||
|
||||
response_data = {
|
||||
"message": "Conversation decision made",
|
||||
"decision": decision,
|
||||
"focus_group_id": focus_group_id
|
||||
}
|
||||
|
||||
return jsonify(response_data), 200
|
||||
|
||||
except ConversationDecisionError as e:
|
||||
current_app.logger.error(f"Error making conversation decision: {str(e)}")
|
||||
return jsonify({
|
||||
"error": "Failed to make conversation decision",
|
||||
"message": str(e)
|
||||
}), 500
|
||||
mode = data.get('mode', 'ai')
|
||||
|
||||
from app.services.task_manager import get_task_manager
|
||||
task_manager = get_task_manager()
|
||||
task_id = task_manager.generate_task_id()
|
||||
app = current_app._get_current_object()
|
||||
bg_task = asyncio.create_task(
|
||||
_run_conversation_decision_bg(app, task_id, user_id, focus_group_id, temperature, mode)
|
||||
)
|
||||
await task_manager.register_task(
|
||||
bg_task, 'conversation_decision', user_id,
|
||||
{'focus_group_id': focus_group_id}, task_id=task_id
|
||||
)
|
||||
return jsonify({'task_id': task_id, 'message': 'Decision started'}), 202
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Unexpected error making conversation decision: {str(e)}")
|
||||
return jsonify({
|
||||
"error": "Failed to make conversation decision",
|
||||
"message": str(e)
|
||||
}), 500
|
||||
current_app.logger.error(f"Error starting conversation decision: {str(e)}")
|
||||
return jsonify({"error": "Failed to start conversation decision", "message": str(e)}), 500
|
||||
|
||||
|
||||
async def _run_conversation_decision_bg(app, task_id, user_id, focus_group_id, temperature, mode):
|
||||
from app.services.task_manager import store_task_result
|
||||
from app.websocket_manager_async import get_async_websocket_manager
|
||||
websocket_manager = get_async_websocket_manager()
|
||||
async with app.app_context():
|
||||
try:
|
||||
decision = await ConversationDecisionService.decide_next_action(focus_group_id, temperature, mode)
|
||||
await store_task_result(task_id, 'completed', result={'decision': decision, 'focus_group_id': focus_group_id})
|
||||
await websocket_manager.emit_to_user(user_id, 'task_completed', {
|
||||
'task_id': task_id, 'task_type': 'conversation_decision'
|
||||
})
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"conversation_decision bg error: {str(e)}")
|
||||
await store_task_result(task_id, 'failed', error=str(e))
|
||||
await websocket_manager.emit_to_user(user_id, 'task_failed', {
|
||||
'task_id': task_id, 'task_type': 'conversation_decision', 'error': str(e)
|
||||
})
|
||||
|
||||
@focus_group_ai_bp.route('/conversation/insights/<focus_group_id>', methods=['GET'])
|
||||
@jwt_required()
|
||||
|
|
@ -1025,27 +1030,20 @@ async def get_conversation_insights(focus_group_id):
|
|||
A JSON object containing conversation insights
|
||||
"""
|
||||
try:
|
||||
# Get insights
|
||||
insights = await ConversationDecisionService.get_conversation_insights(focus_group_id)
|
||||
|
||||
return jsonify({
|
||||
"message": "Conversation insights generated",
|
||||
"insights": insights,
|
||||
"focus_group_id": focus_group_id
|
||||
}), 200
|
||||
|
||||
except ConversationDecisionError as e:
|
||||
except Exception as e:
|
||||
# Return empty insights on LLM error — dashboard handles missing data gracefully
|
||||
current_app.logger.error(f"Error getting conversation insights: {str(e)}")
|
||||
return jsonify({
|
||||
"error": "Failed to get conversation insights",
|
||||
"message": str(e)
|
||||
}), 500
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Unexpected error getting conversation insights: {str(e)}")
|
||||
return jsonify({
|
||||
"error": "Failed to get conversation insights",
|
||||
"message": str(e)
|
||||
}), 500
|
||||
"message": "Insights unavailable",
|
||||
"insights": {},
|
||||
"focus_group_id": focus_group_id
|
||||
}), 200
|
||||
|
||||
@focus_group_ai_bp.route('/conversation/intervene/<focus_group_id>', methods=['POST'])
|
||||
@jwt_required()
|
||||
|
|
|
|||
|
|
@ -326,9 +326,13 @@ async def get_all_focus_groups():
|
|||
@jwt_required()
|
||||
async def get_focus_group(focus_group_id):
|
||||
try:
|
||||
user_id = get_jwt_identity()
|
||||
focus_group = await FocusGroup.find_by_id(focus_group_id)
|
||||
if not focus_group:
|
||||
return jsonify({"message": "Focus group not found"}), 404
|
||||
|
||||
if focus_group.get("created_by") and focus_group.get("created_by") != user_id:
|
||||
return jsonify({"message": "Permission denied"}), 403
|
||||
|
||||
# Process participants count if needed
|
||||
if 'participants' in focus_group and focus_group['participants'] and isinstance(focus_group['participants'], list):
|
||||
|
|
@ -445,18 +449,22 @@ async def delete_focus_group(focus_group_id):
|
|||
@focus_groups_bp.route('/<focus_group_id>/participants', methods=['POST'])
|
||||
@jwt_required()
|
||||
async def add_participant(focus_group_id):
|
||||
user_id = get_jwt_identity()
|
||||
data = await request.get_json()
|
||||
|
||||
|
||||
if not data or not data.get('persona_id'):
|
||||
return jsonify({"message": "Missing persona_id"}), 400
|
||||
|
||||
|
||||
persona_id = data.get('persona_id')
|
||||
|
||||
# Verify focus group exists
|
||||
|
||||
# Verify focus group exists and belongs to user
|
||||
focus_group = await FocusGroup.find_by_id(focus_group_id)
|
||||
if not focus_group:
|
||||
return jsonify({"message": "Focus group not found"}), 404
|
||||
|
||||
|
||||
if focus_group.get("created_by") and focus_group.get("created_by") != user_id:
|
||||
return jsonify({"message": "Permission denied"}), 403
|
||||
|
||||
# Verify persona exists
|
||||
persona = await Persona.find_by_id(persona_id)
|
||||
if not persona:
|
||||
|
|
@ -473,10 +481,14 @@ async def add_participant(focus_group_id):
|
|||
@focus_groups_bp.route('/<focus_group_id>/participants/<persona_id>', methods=['DELETE'])
|
||||
@jwt_required()
|
||||
async def remove_participant(focus_group_id, persona_id):
|
||||
# Verify focus group exists
|
||||
user_id = get_jwt_identity()
|
||||
# Verify focus group exists and belongs to user
|
||||
focus_group = await FocusGroup.find_by_id(focus_group_id)
|
||||
if not focus_group:
|
||||
return jsonify({"message": "Focus group not found"}), 404
|
||||
|
||||
if focus_group.get("created_by") and focus_group.get("created_by") != user_id:
|
||||
return jsonify({"message": "Permission denied"}), 403
|
||||
|
||||
success = await FocusGroup.remove_participant(focus_group_id, persona_id)
|
||||
|
||||
|
|
@ -491,11 +503,15 @@ async def remove_participant(focus_group_id, persona_id):
|
|||
async def get_focus_group_messages(focus_group_id):
|
||||
"""Get all messages for a focus group, including mode events."""
|
||||
try:
|
||||
# Verify focus group exists
|
||||
user_id = get_jwt_identity()
|
||||
# Verify focus group exists and belongs to user
|
||||
focus_group = await FocusGroup.find_by_id(focus_group_id)
|
||||
if not focus_group:
|
||||
return jsonify({"message": "Focus group not found"}), 404
|
||||
|
||||
|
||||
if focus_group.get("created_by") and focus_group.get("created_by") != user_id:
|
||||
return jsonify({"message": "Permission denied"}), 403
|
||||
|
||||
# Get messages and mode events
|
||||
messages = await FocusGroup.get_messages(focus_group_id)
|
||||
mode_events = await FocusGroup.get_mode_events(focus_group_id)
|
||||
|
|
|
|||
|
|
@ -52,10 +52,14 @@ async def get_all_personas():
|
|||
@jwt_required()
|
||||
async def get_persona(persona_id):
|
||||
try:
|
||||
user_id = get_jwt_identity()
|
||||
persona = await Persona.find_by_id(persona_id)
|
||||
if not persona:
|
||||
return jsonify({"message": "Persona not found"}), 404
|
||||
|
||||
|
||||
if persona.get("created_by") and persona.get("created_by") != user_id:
|
||||
return jsonify({"message": "Permission denied"}), 403
|
||||
|
||||
# Make persona serializable
|
||||
serializable_persona = make_serializable(persona)
|
||||
return jsonify(serializable_persona), 200
|
||||
|
|
|
|||
|
|
@ -20,12 +20,17 @@ async def get_task_result(task_id: str):
|
|||
Poll for task status and result.
|
||||
Returns 200 with {task_id, status, task_type, result?, error?} if found.
|
||||
Returns 404 if task not found (expired or never existed).
|
||||
Returns 403 if task belongs to a different user.
|
||||
"""
|
||||
try:
|
||||
user_id = get_jwt_identity()
|
||||
task_manager = get_task_manager()
|
||||
data = await task_manager.get_task_status_dict(task_id)
|
||||
if not data:
|
||||
task_info = await task_manager.get_task_info(task_id)
|
||||
if not task_info:
|
||||
return jsonify({'error': 'Task not found or expired', 'task_id': task_id}), 404
|
||||
if task_info.user_id and task_info.user_id != user_id:
|
||||
return jsonify({'error': 'Access denied', 'task_id': task_id}), 403
|
||||
data = await task_manager.get_task_status_dict(task_id)
|
||||
return jsonify(data), 200
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching task result {task_id}: {str(e)}")
|
||||
|
|
@ -47,12 +52,19 @@ async def cancel_task(task_id: str):
|
|||
try:
|
||||
task_manager = get_task_manager()
|
||||
|
||||
# Get task info before cancellation for WebSocket notification
|
||||
# Get task info before cancellation for ownership check + WebSocket notification
|
||||
task_info = await task_manager.get_task_info(task_id)
|
||||
|
||||
|
||||
if not task_info:
|
||||
return jsonify({'error': 'Task not found or already completed', 'task_id': task_id}), 404
|
||||
|
||||
user_id = get_jwt_identity()
|
||||
if task_info.user_id and task_info.user_id != user_id:
|
||||
return jsonify({'error': 'Access denied', 'task_id': task_id}), 403
|
||||
|
||||
# Attempt to cancel the task
|
||||
cancelled = await task_manager.cancel_task(task_id)
|
||||
|
||||
|
||||
if not cancelled:
|
||||
return jsonify({
|
||||
'error': 'Task not found or already completed',
|
||||
|
|
@ -128,6 +140,7 @@ async def get_user_tasks():
|
|||
|
||||
|
||||
@tasks_bp.route('/status', methods=['GET'])
|
||||
@jwt_required()
|
||||
async def get_task_status():
|
||||
"""
|
||||
Get overall task manager status (for debugging/monitoring).
|
||||
|
|
|
|||
|
|
@ -1024,13 +1024,15 @@ const DiscussionPanel = ({
|
|||
});
|
||||
|
||||
const decisionResponse = await focusGroupAiApi.makeConversationDecision(focusGroupId, 0.7, 'manual');
|
||||
|
||||
// Check if we have a valid decision response
|
||||
if (!decisionResponse || !decisionResponse.data || !decisionResponse.data.decision) {
|
||||
throw new Error("Empty decision response from AI");
|
||||
const decisionTaskId = decisionResponse.data?.task_id;
|
||||
if (!decisionTaskId) throw new Error("Failed to start decision task");
|
||||
|
||||
const decisionTaskResult = await waitForTaskResult(decisionTaskId);
|
||||
if (decisionTaskResult.status !== 'completed' || !decisionTaskResult.result?.decision) {
|
||||
throw new Error(decisionTaskResult.error || "Failed to get AI decision");
|
||||
}
|
||||
|
||||
const decision = decisionResponse.data.decision;
|
||||
|
||||
const decision = decisionTaskResult.result.decision;
|
||||
|
||||
// Check if AI decided a participant should respond
|
||||
if (decision.action === 'participant_respond') {
|
||||
|
|
|
|||
|
|
@ -613,16 +613,16 @@ export const focusGroupAiApi = {
|
|||
api.get(`/focus-group-ai/conversation/analytics/${focusGroupId}`),
|
||||
|
||||
makeConversationDecision: (focusGroupId: string, temperature: number = 0.7, mode: string = 'ai') =>
|
||||
api.post(`/focus-group-ai/conversation/decision/${focusGroupId}`, {
|
||||
api.post(`/focus-group-ai/conversation/decision/${focusGroupId}`, {
|
||||
temperature: temperature,
|
||||
mode: mode
|
||||
}, {
|
||||
timeout: 180000 // 3 minutes for LLM decision making
|
||||
timeout: 10000 // Returns 202 immediately
|
||||
}),
|
||||
|
||||
|
||||
getConversationInsights: (focusGroupId: string) =>
|
||||
api.get(`/focus-group-ai/conversation/insights/${focusGroupId}`, {
|
||||
timeout: 180000 // 3 minutes for LLM insight generation
|
||||
timeout: 180000 // LLM call, keep long timeout
|
||||
}),
|
||||
|
||||
manualIntervention: (focusGroupId: string, action: string, message?: string, participantId?: string) =>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue