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
4 changes: 2 additions & 2 deletions templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
<link rel="stylesheet" href="{{ url_for('static', filename='css/app.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/chatbot.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/chatbot.css', v='20260725.1') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/page_search.css') }}">
{% block extra_css %}{% endblock %}
</head>
Expand Down Expand Up @@ -184,7 +184,7 @@ <h3><i class="bi bi-robot me-2"></i>Statistical assistant</h3>
</button>

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="{{ url_for('static', filename='js/chatbot.js') }}"></script>
<script src="{{ url_for('static', filename='js/chatbot.js', v='20260725.1') }}"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(function (element) {
Expand Down
6 changes: 3 additions & 3 deletions templates/blank.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">

<!-- Chatbot CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/chatbot.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/chatbot.css', v='20260725.1') }}">

<!-- Page Search CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/page_search.css') }}">
Expand Down Expand Up @@ -92,11 +92,11 @@ <h3><i class="bi bi-robot me-2"></i>AI Assistant</h3>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>

<!-- Chatbot JS -->
<script src="{{ url_for('static', filename='js/chatbot.js') }}"></script>
<script src="{{ url_for('static', filename='js/chatbot.js', v='20260725.1') }}"></script>

<!-- Page Search JS -->
<script src="{{ url_for('static', filename='js/page_search.js') }}"></script>

{% block scripts %}{% endblock %}
</body>
</html>
</html>
42 changes: 42 additions & 0 deletions tests/test_integrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,47 @@ def test_openai_supports_strict_structured_outputs(monkeypatch):
}


def test_openai_allows_a_larger_feature_specific_output_budget(monkeypatch):
monkeypatch.setenv("AI_ENHANCEMENT_ENABLED", "true")
monkeypatch.setenv("OPENAI_API_KEY", "sk-test")
monkeypatch.setenv("AI_MAX_OUTPUT_TOKENS", "400")
response = Mock(output_text='{"answer":"complete"}', status="completed")
client = Mock()
client.responses.create.return_value = response

with patch("utils.ai_service.OpenAI", return_value=client):
result = call_openai_api(
"Return the complete review.",
max_output_tokens=1_500,
)

assert result == '{"answer":"complete"}'
assert (
client.responses.create.call_args.kwargs["max_output_tokens"]
== 1_500
)


def test_openai_rejects_an_incomplete_provider_response(monkeypatch):
monkeypatch.setenv("AI_ENHANCEMENT_ENABLED", "true")
monkeypatch.setenv("OPENAI_API_KEY", "sk-test")
response = Mock(
output_text='{"answer":"cut off',
status="incomplete",
)
client = Mock()
client.responses.create.return_value = response

with patch("utils.ai_service.OpenAI", return_value=client):
with pytest.raises(
OpenAIServiceError,
match="exceeded its output limit",
) as error:
call_openai_api("Return the complete review.")

assert error.value.status_code == 502


def test_recommendation_ai_is_limited_to_verified_candidates():
model_database = {
"Linear Regression": {
Expand Down Expand Up @@ -128,6 +169,7 @@ def test_recommendation_ai_is_limited_to_verified_candidates():
}
]
schema = generate.call_args.kwargs["response_schema"]
assert generate.call_args.kwargs["max_output_tokens"] == 1_500
assert schema["properties"]["recommended_model"]["enum"] == [
"Linear Regression",
"Random Forest",
Expand Down
2 changes: 2 additions & 0 deletions tests/test_main_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ def test_home_page(self, client):
response = client.get('/')
assert response.status_code == 200
assert b'statistical' in response.data.lower() or b'model' in response.data.lower()
assert b'chatbot.js?v=20260725.1' in response.data
assert b'chatbot.css?v=20260725.1' in response.data
def test_analysis_form_page(self, client):
"""Test that the analysis form page loads."""
response = client.get('/analysis-form')
Expand Down
18 changes: 14 additions & 4 deletions utils/ai_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,15 @@ def _timeout_seconds() -> float:
return 45.0


def _max_output_tokens() -> int:
raw_limit = os.environ.get("AI_MAX_OUTPUT_TOKENS", "400")
def _max_output_tokens(override: Optional[int] = None) -> int:
raw_limit = (
override
if override is not None
else os.environ.get("AI_MAX_OUTPUT_TOKENS", "400")
)
try:
return min(max(int(raw_limit), 100), 1_500)
except ValueError:
except (TypeError, ValueError):
return 400


Expand All @@ -75,6 +79,7 @@ def call_openai_api(
safety_identifier: Optional[str] = None,
response_schema: Optional[dict[str, Any]] = None,
schema_name: str = "structured_response",
max_output_tokens: Optional[int] = None,
) -> str:
"""Generate text through OpenAI's Responses API."""
if not is_ai_enabled():
Expand All @@ -100,7 +105,7 @@ def call_openai_api(
"model": target_model,
"instructions": system_prompt or DEFAULT_SYSTEM_PROMPT,
"input": cleaned_prompt,
"max_output_tokens": _max_output_tokens(),
"max_output_tokens": _max_output_tokens(max_output_tokens),
"reasoning": {"effort": _reasoning_effort()},
"store": False,
}
Expand Down Expand Up @@ -169,6 +174,11 @@ def call_openai_api(

if response is None:
raise OpenAIServiceError("The AI provider returned no response.", 502)
if getattr(response, "status", None) == "incomplete":
raise OpenAIServiceError(
"The AI provider response exceeded its output limit.",
502,
)
content = response.output_text
if not isinstance(content, str) or not content.strip():
raise OpenAIServiceError("The AI provider returned an empty response.", 502)
Expand Down
1 change: 1 addition & 0 deletions utils/recommendation_ai.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ def review_recommendation(
safety_identifier=safety_identifier,
response_schema=_review_schema(verified_candidates),
schema_name="model_recommendation_review",
max_output_tokens=1_500,
)

try:
Expand Down