Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 107 additions & 12 deletions routes/main_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,19 @@
from flask_login import login_required, current_user
from models import db, User, Analysis, get_model_details
from datetime import datetime
import hashlib
import json
import logging
from collections import OrderedDict
from urllib.parse import unquote
from sqlalchemy.exc import SQLAlchemyError

from utils.ai_service import OpenAIServiceError, is_ai_enabled
from utils.ai_usage import consume_user_ai_quota
from utils.recommendation_ai import review_recommendation


logger = logging.getLogger(__name__)
main = Blueprint('main', __name__)


Expand Down Expand Up @@ -542,6 +552,7 @@ def results():
data_distribution = request.form.get('data_distribution', '')
relationship_type = request.form.get('relationship_type', '')
variables_correlated = request.form.get('variables_correlated', 'unknown')
use_ai_review = request.form.get('use_ai_review') == 'on'
# Get model database from app config
MODEL_DATABASE = current_app.config.get('MODEL_DATABASE', {})
# For clustering analysis, dependent variable can be empty
Expand All @@ -567,17 +578,6 @@ def results():
# Find a replacement model if the recommended one doesn't exist
recommended_model = get_default_model(analysis_goal, dependent_variable_type)
explanation = f"Based on your analysis goal ({analysis_goal}) and dependent variable type ({dependent_variable_type}), we recommend using {recommended_model}."
# Save analysis if user is logged in
if current_user.is_authenticated:
try:
save_user_analysis(
current_user.id, research_question, recommended_model, analysis_goal, dependent_variable_type,
independent_variables, sample_size, missing_data, data_distribution, relationship_type,
variables_correlated
)
flash('Your analysis has been saved to your profile.', 'info')
except Exception as e:
flash(f'Could not save analysis to your profile: {str(e)}', 'danger')
# Filter for alternative models (don't include the primary recommendation)
if recommended_model in MODEL_DATABASE:
# Find similar models based on the same analysis goal and dependent variable type
Expand All @@ -596,6 +596,99 @@ def results():
alternative_models = [model for model in alternative_models if model in MODEL_DATABASE]
else:
alternative_models = []

ai_review = None
ai_review_status = 'not_requested'
if use_ai_review:
if not current_user.is_authenticated:
ai_review_status = 'authentication_required'
elif not is_ai_enabled():
ai_review_status = 'unavailable'
else:
try:
allowed, _ = consume_user_ai_quota(current_user.id)
if not allowed:
ai_review_status = 'quota_reached'
else:
candidate_models = [
recommended_model,
*alternative_models,
]
safety_identifier = hashlib.sha256(
(
f"{current_app.config['SECRET_KEY']}:"
f"{current_user.id}"
).encode()
).hexdigest()
ai_review = review_recommendation(
research_question=research_question,
analysis_inputs={
'analysis_goal': analysis_goal,
'dependent_variable_type': dependent_variable_type,
'independent_variable_types': independent_variables,
'sample_size': sample_size,
'missing_data': missing_data,
'data_distribution': data_distribution,
'relationship_type': relationship_type,
'variables_correlated': variables_correlated,
},
candidate_models=candidate_models,
model_database=MODEL_DATABASE,
safety_identifier=safety_identifier,
)
if ai_review.get('recommended_model') not in candidate_models:
raise ValueError(
"AI review selected a model outside the shortlist."
)
rules_engine_model = recommended_model
recommended_model = ai_review['recommended_model']
if recommended_model != rules_engine_model:
alternative_models = [
rules_engine_model,
*[
model for model in alternative_models
if model != recommended_model
],
][:4]
explanation = generate_explanation(
recommended_model,
analysis_goal,
dependent_variable_type,
independent_variables,
sample_size,
missing_data,
data_distribution,
relationship_type,
variables_correlated,
)
ai_review_status = 'completed'
except SQLAlchemyError:
db.session.rollback()
logger.exception(
"Could not record recommendation AI usage for user %s.",
current_user.id,
)
ai_review_status = 'unavailable'
except (OpenAIServiceError, ValueError):
logger.warning(
"AI recommendation review failed for user %s.",
current_user.id,
exc_info=True,
)
ai_review_status = 'unavailable'

# Save the final, validated recommendation if the user is logged in.
if current_user.is_authenticated:
try:
save_user_analysis(
current_user.id, research_question, recommended_model, analysis_goal, dependent_variable_type,
independent_variables, sample_size, missing_data, data_distribution, relationship_type,
variables_correlated
)
flash('Your analysis has been saved to your profile.', 'info')
except Exception as e:
flash(f'Could not save analysis to your profile: {str(e)}', 'danger')

return render_template(
'results.html',
research_question=research_question,
Expand All @@ -610,7 +703,9 @@ def results():
recommended_model=recommended_model,
explanation=explanation,
MODEL_DATABASE=MODEL_DATABASE,
alternative_models=alternative_models
alternative_models=alternative_models,
ai_review=ai_review,
ai_review_status=ai_review_status,
)
@main.route('/user_analysis/<int:analysis_id>')
@login_required
Expand Down
28 changes: 24 additions & 4 deletions templates/analysis_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ <h1 class="form-title">Describe your research design</h1>
</div>

<form method="POST" action="{{ url_for('main.results') }}">
<div class="form-group">
<div class="form-group">
<label for="research_question" class="form-label required-field">What are you trying to find out?</label>
<input type="text" class="form-control" id="research_question" name="research_question" required
placeholder="Example: Does exercise affect weight loss?">
Expand Down Expand Up @@ -341,9 +341,29 @@ <h1 class="form-title">Describe your research design</h1>
<div class="help-text">
<i class="bi bi-graph-up-arrow me-2"></i>If you're not sure, that's okay! We'll consider all possibilities.
</div>
</div>

<div class="text-center mt-4 mb-2">
</div>

{% if current_user.is_authenticated %}
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="use_ai_review" name="use_ai_review" checked>
<label class="form-check-label fw-semibold" for="use_ai_review">
Add an AI-assisted review
</label>
</div>
<div class="help-text">
<i class="bi bi-stars me-2"></i>OpenAI will review the verified shortlist, compare tradeoffs, and check whether another shortlisted model is a better fit. Uses one AI request.
</div>
</div>
{% else %}
<div class="form-group">
<div class="help-text">
<i class="bi bi-stars me-2"></i><a href="{{ url_for('auth.login', next=url_for('main.analysis_form')) }}">Sign in</a> to add an optional AI review. The rules-based recommendation remains available without an account.
</div>
</div>
{% endif %}

<div class="text-center mt-4 mb-2">
<button type="submit" class="btn btn-primary btn-lg">
<i class="bi bi-magic me-2"></i>
<span>Find My Statistical Model</span>
Expand Down
50 changes: 50 additions & 0 deletions templates/results.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@
.result-panel h2 { margin-bottom: 1rem; font-size: 1.25rem; }
.rationale { color: #29485b; }
.rationale ul { padding-left: 1.2rem; }
.ai-review { margin-top: 1.25rem; padding: 1.25rem; background: #f2fbfa; border: 1px solid #a7ded7; border-radius: .9rem; }
.ai-review-heading { display: flex; flex-wrap: wrap; align-items: center; gap: .65rem; }
.ai-badge { display: inline-flex; align-items: center; gap: .35rem; padding: .3rem .55rem; color: #0f514b; background: #d5f3ef; border-radius: 999px; font-size: .74rem; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; }
.ai-review h3 { margin: 0; color: #153f49; font-size: 1.05rem; }
.ai-review h4 { margin: 1rem 0 .4rem; color: #29485b; font-size: .9rem; }
.ai-review ul { margin-bottom: 0; padding-left: 1.2rem; color: #29485b; }
.ai-review li + li { margin-top: .35rem; }
.ai-review-status { margin-top: 1rem; padding: .75rem .9rem; color: #6d5724; background: #fff9e8; border: 1px solid #f2dfa5; border-radius: .75rem; font-size: .85rem; }
.next-actions { display: grid; gap: .65rem; }
.next-actions .btn { display: flex; justify-content: space-between; align-items: center; padding: .75rem .9rem; text-align: left; }
.caution-note { margin-top: 1rem; padding: .9rem; color: #6d5724; background: #fff9e8; border: 1px solid #f2dfa5; border-radius: .75rem; font-size: .85rem; }
Expand Down Expand Up @@ -45,6 +53,48 @@ <h1>{{ recommended_model }}</h1>
<div class="page-eyebrow">Why it fits</div>
<h2>Recommendation rationale</h2>
<div class="rationale">{{ explanation | safe }}</div>

{% if ai_review_status == 'completed' and ai_review %}
<div class="ai-review">
<div class="ai-review-heading">
<span class="ai-badge"><i class="bi bi-stars"></i> AI-assisted review</span>
<h3>Shortlist reviewed</h3>
</div>
{% if ai_review.summary %}
<p class="mt-3 mb-0">{{ ai_review.summary }}</p>
{% endif %}
{% if ai_review.why_it_fits %}
<h4>Why this candidate leads</h4>
<ul>
{% for reason in ai_review.why_it_fits %}
<li>{{ reason }}</li>
{% endfor %}
</ul>
{% endif %}
{% if ai_review.assumptions_to_check %}
<h4>Verify before fitting</h4>
<ul>
{% for assumption in ai_review.assumptions_to_check %}
<li>{{ assumption }}</li>
{% endfor %}
</ul>
{% endif %}
{% if ai_review.alternative_tradeoffs %}
<h4>When an alternative may be better</h4>
<ul>
{% for tradeoff in ai_review.alternative_tradeoffs %}
<li><strong>{{ tradeoff.model }}:</strong> {{ tradeoff.when_to_prefer }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% elif ai_review_status == 'quota_reached' %}
<div class="ai-review-status">Your hourly AI limit has been reached. The rules-based recommendation is shown above.</div>
{% elif ai_review_status == 'unavailable' %}
<div class="ai-review-status">The AI review is temporarily unavailable. The rules-based recommendation is still complete and usable.</div>
{% elif ai_review_status == 'authentication_required' %}
<div class="ai-review-status">Sign in to use AI review. The rules-based recommendation is shown above.</div>
{% endif %}
</section>

<aside class="result-panel">
Expand Down
Loading