mito-ai 0.1.33__py3-none-any.whl → 0.1.35__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mito-ai might be problematic. Click here for more details.
- mito_ai/_version.py +1 -1
- mito_ai/anthropic_client.py +52 -54
- mito_ai/app_builder/handlers.py +2 -4
- mito_ai/completions/models.py +15 -1
- mito_ai/completions/prompt_builders/agent_system_message.py +10 -2
- mito_ai/completions/providers.py +79 -39
- mito_ai/constants.py +11 -24
- mito_ai/gemini_client.py +44 -48
- mito_ai/openai_client.py +30 -44
- mito_ai/tests/message_history/test_generate_short_chat_name.py +0 -4
- mito_ai/tests/open_ai_utils_test.py +18 -22
- mito_ai/tests/{test_anthropic_client.py → providers/test_anthropic_client.py} +37 -32
- mito_ai/tests/providers/test_azure.py +2 -6
- mito_ai/tests/providers/test_capabilities.py +120 -0
- mito_ai/tests/{test_gemini_client.py → providers/test_gemini_client.py} +40 -36
- mito_ai/tests/providers/test_mito_server_utils.py +448 -0
- mito_ai/tests/providers/test_model_resolution.py +130 -0
- mito_ai/tests/providers/test_openai_client.py +57 -0
- mito_ai/tests/providers/test_provider_completion_exception.py +66 -0
- mito_ai/tests/providers/test_provider_limits.py +42 -0
- mito_ai/tests/providers/test_providers.py +382 -0
- mito_ai/tests/providers/test_retry_logic.py +389 -0
- mito_ai/tests/providers/utils.py +85 -0
- mito_ai/tests/test_constants.py +15 -2
- mito_ai/tests/test_telemetry.py +12 -0
- mito_ai/utils/anthropic_utils.py +21 -29
- mito_ai/utils/gemini_utils.py +18 -22
- mito_ai/utils/mito_server_utils.py +92 -0
- mito_ai/utils/open_ai_utils.py +22 -46
- mito_ai/utils/provider_utils.py +49 -0
- mito_ai/utils/telemetry_utils.py +11 -1
- {mito_ai-0.1.33.data → mito_ai-0.1.35.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +1 -1
- {mito_ai-0.1.33.data → mito_ai-0.1.35.data}/data/share/jupyter/labextensions/mito_ai/package.json +2 -2
- {mito_ai-0.1.33.data → mito_ai-0.1.35.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +1 -1
- mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.281f4b9af60d620c6fb1.js → mito_ai-0.1.35.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.a20772bc113422d0f505.js +737 -319
- mito_ai-0.1.35.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.a20772bc113422d0f505.js.map +1 -0
- mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.4f1d00fd0c58fcc05d8d.js → mito_ai-0.1.35.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.d2eea6519fa332d79efb.js +13 -16
- mito_ai-0.1.35.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.d2eea6519fa332d79efb.js.map +1 -0
- mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.06083e515de4862df010.js → mito_ai-0.1.35.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.76efcc5c3be4056457ee.js +6 -2
- mito_ai-0.1.35.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.76efcc5c3be4056457ee.js.map +1 -0
- {mito_ai-0.1.33.dist-info → mito_ai-0.1.35.dist-info}/METADATA +1 -1
- {mito_ai-0.1.33.dist-info → mito_ai-0.1.35.dist-info}/RECORD +52 -43
- mito_ai/tests/providers_test.py +0 -438
- mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.281f4b9af60d620c6fb1.js.map +0 -1
- mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.4f1d00fd0c58fcc05d8d.js.map +0 -1
- mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.06083e515de4862df010.js.map +0 -1
- mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_html2canvas_dist_html2canvas_js.ea47e8c8c906197f8d19.js +0 -7842
- mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_html2canvas_dist_html2canvas_js.ea47e8c8c906197f8d19.js.map +0 -1
- {mito_ai-0.1.33.data → mito_ai-0.1.35.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
- {mito_ai-0.1.33.data → mito_ai-0.1.35.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +0 -0
- {mito_ai-0.1.33.data → mito_ai-0.1.35.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
- {mito_ai-0.1.33.data → mito_ai-0.1.35.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.9795f79265ddb416864b.js +0 -0
- {mito_ai-0.1.33.data → mito_ai-0.1.35.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.9795f79265ddb416864b.js.map +0 -0
- {mito_ai-0.1.33.data → mito_ai-0.1.35.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js +0 -0
- {mito_ai-0.1.33.data → mito_ai-0.1.35.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js.map +0 -0
- {mito_ai-0.1.33.dist-info → mito_ai-0.1.35.dist-info}/WHEEL +0 -0
- {mito_ai-0.1.33.dist-info → mito_ai-0.1.35.dist-info}/entry_points.txt +0 -0
- {mito_ai-0.1.33.dist-info → mito_ai-0.1.35.dist-info}/licenses/LICENSE +0 -0
|
@@ -5,12 +5,13 @@ import pytest
|
|
|
5
5
|
import ast
|
|
6
6
|
import inspect
|
|
7
7
|
import requests
|
|
8
|
-
from mito_ai.gemini_client import GeminiClient,
|
|
9
|
-
from mito_ai.utils.gemini_utils import get_gemini_completion_function_params
|
|
8
|
+
from mito_ai.gemini_client import GeminiClient, get_gemini_system_prompt_and_messages
|
|
9
|
+
from mito_ai.utils.gemini_utils import get_gemini_completion_function_params, FAST_GEMINI_MODEL
|
|
10
10
|
from google.genai.types import Part, GenerateContentResponse, Candidate, Content
|
|
11
11
|
from mito_ai.completions.models import ResponseFormatInfo, AgentResponse
|
|
12
12
|
from unittest.mock import MagicMock, patch
|
|
13
13
|
from typing import List, Dict, Any
|
|
14
|
+
from mito_ai.completions.models import MessageType
|
|
14
15
|
|
|
15
16
|
# Dummy base64 image (1x1 PNG)
|
|
16
17
|
DUMMY_IMAGE_DATA_URL = (
|
|
@@ -73,7 +74,7 @@ async def test_json_response_handling():
|
|
|
73
74
|
)
|
|
74
75
|
|
|
75
76
|
# Create a mock client with the response
|
|
76
|
-
client = GeminiClient(api_key="test_key"
|
|
77
|
+
client = GeminiClient(api_key="test_key")
|
|
77
78
|
client.client = MagicMock()
|
|
78
79
|
client.client.models.generate_content.return_value = mock_response
|
|
79
80
|
|
|
@@ -81,6 +82,7 @@ async def test_json_response_handling():
|
|
|
81
82
|
response_format_info = ResponseFormatInfo(name="agent_response", format=AgentResponse)
|
|
82
83
|
result = await client.request_completions(
|
|
83
84
|
messages=[{"role": "user", "content": "Test message"}],
|
|
85
|
+
model="test-model",
|
|
84
86
|
response_format_info=response_format_info
|
|
85
87
|
)
|
|
86
88
|
assert result == '{"key": "value"}'
|
|
@@ -107,7 +109,7 @@ async def test_json_response_handling_with_invalid_json():
|
|
|
107
109
|
)
|
|
108
110
|
|
|
109
111
|
# Create a mock client with the response
|
|
110
|
-
client = GeminiClient(api_key="test_key"
|
|
112
|
+
client = GeminiClient(api_key="test_key")
|
|
111
113
|
client.client = MagicMock()
|
|
112
114
|
client.client.models.generate_content.return_value = mock_response
|
|
113
115
|
|
|
@@ -115,6 +117,7 @@ async def test_json_response_handling_with_invalid_json():
|
|
|
115
117
|
response_format_info = ResponseFormatInfo(name="agent_response", format=AgentResponse)
|
|
116
118
|
result = await client.request_completions(
|
|
117
119
|
messages=[{"role": "user", "content": "Test message"}],
|
|
120
|
+
model="test-model",
|
|
118
121
|
response_format_info=response_format_info
|
|
119
122
|
)
|
|
120
123
|
# Should return the raw string even if JSON is invalid
|
|
@@ -138,7 +141,7 @@ async def test_json_response_handling_with_multiple_parts():
|
|
|
138
141
|
)
|
|
139
142
|
|
|
140
143
|
# Create a mock client with the response
|
|
141
|
-
client = GeminiClient(api_key="test_key"
|
|
144
|
+
client = GeminiClient(api_key="test_key")
|
|
142
145
|
client.client = MagicMock()
|
|
143
146
|
client.client.models.generate_content.return_value = mock_response
|
|
144
147
|
|
|
@@ -146,46 +149,47 @@ async def test_json_response_handling_with_multiple_parts():
|
|
|
146
149
|
response_format_info = ResponseFormatInfo(name="agent_response", format=AgentResponse)
|
|
147
150
|
result = await client.request_completions(
|
|
148
151
|
messages=[{"role": "user", "content": "Test message"}],
|
|
152
|
+
model="test-model",
|
|
149
153
|
response_format_info=response_format_info
|
|
150
154
|
)
|
|
151
155
|
# Should concatenate all parts
|
|
152
156
|
assert result == 'Here is the JSON: {"key": "value"} End of response'
|
|
153
157
|
|
|
154
|
-
CUSTOM_MODEL = "gemini-
|
|
155
|
-
@pytest.mark.parametrize("
|
|
156
|
-
(
|
|
157
|
-
(
|
|
158
|
+
CUSTOM_MODEL = "smart-gemini-model"
|
|
159
|
+
@pytest.mark.parametrize("message_type, expected_model", [
|
|
160
|
+
(MessageType.CHAT, CUSTOM_MODEL), #
|
|
161
|
+
(MessageType.SMART_DEBUG, CUSTOM_MODEL), #
|
|
162
|
+
(MessageType.CODE_EXPLAIN, CUSTOM_MODEL), #
|
|
163
|
+
(MessageType.AGENT_EXECUTION, CUSTOM_MODEL), #
|
|
164
|
+
(MessageType.AGENT_AUTO_ERROR_FIXUP, CUSTOM_MODEL), #
|
|
165
|
+
(MessageType.INLINE_COMPLETION, FAST_GEMINI_MODEL), #
|
|
166
|
+
(MessageType.CHAT_NAME_GENERATION, FAST_GEMINI_MODEL), #
|
|
158
167
|
])
|
|
159
|
-
@pytest.mark.asyncio
|
|
160
|
-
async def
|
|
168
|
+
@pytest.mark.asyncio
|
|
169
|
+
async def test_get_completion_model_selection_based_on_message_type(message_type, expected_model):
|
|
161
170
|
"""
|
|
162
|
-
Tests that the correct model is selected based on
|
|
171
|
+
Tests that the correct model is selected based on the message type.
|
|
163
172
|
"""
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
)
|
|
177
|
-
)
|
|
178
|
-
]
|
|
179
|
-
)
|
|
180
|
-
client.client.models.generate_content.return_value = mock_response
|
|
181
|
-
|
|
182
|
-
with patch('mito_ai.gemini_client.get_gemini_completion_function_params', wraps=get_gemini_completion_function_params) as mock_get_params:
|
|
173
|
+
with patch('google.genai.Client') as mock_genai_class:
|
|
174
|
+
mock_client = MagicMock()
|
|
175
|
+
mock_models = MagicMock()
|
|
176
|
+
mock_client.models = mock_models
|
|
177
|
+
mock_genai_class.return_value = mock_client
|
|
178
|
+
|
|
179
|
+
client = GeminiClient(api_key="test_key")
|
|
180
|
+
|
|
181
|
+
# Create a mock response
|
|
182
|
+
mock_response = 'test-response'
|
|
183
|
+
mock_models.generate_content.return_value = mock_response
|
|
184
|
+
|
|
183
185
|
await client.request_completions(
|
|
184
186
|
messages=[{"role": "user", "content": "Test message"}],
|
|
185
|
-
|
|
187
|
+
model=CUSTOM_MODEL,
|
|
188
|
+
message_type=message_type,
|
|
189
|
+
response_format_info=None
|
|
186
190
|
)
|
|
187
191
|
|
|
188
|
-
# Verify that
|
|
189
|
-
|
|
190
|
-
call_args =
|
|
191
|
-
assert call_args[1]['model'] == expected_model
|
|
192
|
+
# Verify that generate_content was called with the expected model
|
|
193
|
+
mock_models.generate_content.assert_called_once()
|
|
194
|
+
call_args = mock_models.generate_content.call_args
|
|
195
|
+
assert call_args[1]['model'] == expected_model
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
# Copyright (c) Saga Inc.
|
|
2
|
+
# Distributed under the terms of the GNU Affero General Public License v3.0 License.
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
from unittest.mock import MagicMock, patch, AsyncMock
|
|
8
|
+
from tornado.httpclient import HTTPResponse
|
|
9
|
+
|
|
10
|
+
from mito_ai.utils.mito_server_utils import (
|
|
11
|
+
ProviderCompletionException,
|
|
12
|
+
get_response_from_mito_server
|
|
13
|
+
)
|
|
14
|
+
from mito_ai.completions.models import MessageType
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture
|
|
18
|
+
def mock_request_params():
|
|
19
|
+
"""Standard request parameters for testing."""
|
|
20
|
+
return {
|
|
21
|
+
"url": "https://api.example.com",
|
|
22
|
+
"headers": {"Content-Type": "application/json"},
|
|
23
|
+
"data": {"query": "test query"},
|
|
24
|
+
"timeout": 30,
|
|
25
|
+
"max_retries": 3,
|
|
26
|
+
"message_type": MessageType.CHAT,
|
|
27
|
+
"provider_name": "Test Provider"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.fixture
|
|
32
|
+
def mock_http_dependencies():
|
|
33
|
+
"""Mock the HTTP client and related dependencies."""
|
|
34
|
+
with patch('mito_ai.utils.mito_server_utils._create_http_client') as mock_create_client, \
|
|
35
|
+
patch('mito_ai.utils.mito_server_utils.update_mito_server_quota') as mock_update_quota, \
|
|
36
|
+
patch('mito_ai.utils.mito_server_utils.check_mito_server_quota') as mock_check_quota, \
|
|
37
|
+
patch('mito_ai.utils.mito_server_utils.time.time') as mock_time:
|
|
38
|
+
|
|
39
|
+
# Setup mock HTTP client
|
|
40
|
+
mock_http_client = MagicMock()
|
|
41
|
+
mock_create_client.return_value = (mock_http_client, 30)
|
|
42
|
+
|
|
43
|
+
# Setup mock time
|
|
44
|
+
mock_time.side_effect = [0.0, 1.5] # start_time, end_time
|
|
45
|
+
|
|
46
|
+
yield {
|
|
47
|
+
'mock_check_quota': mock_check_quota,
|
|
48
|
+
'mock_create_client': mock_create_client,
|
|
49
|
+
'mock_http_client': mock_http_client,
|
|
50
|
+
'mock_update_quota': mock_update_quota,
|
|
51
|
+
'mock_time': mock_time
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def create_mock_response(body_content: dict):
|
|
56
|
+
"""Helper to create mock HTTP response."""
|
|
57
|
+
mock_response = MagicMock(spec=HTTPResponse)
|
|
58
|
+
mock_response.body.decode.return_value = json.dumps(body_content)
|
|
59
|
+
return mock_response
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class TestProviderCompletionException:
|
|
63
|
+
"""Test the ProviderCompletionException class."""
|
|
64
|
+
|
|
65
|
+
@pytest.mark.parametrize("error_message,provider_name,error_type,expected_title,expected_hint_contains", [
|
|
66
|
+
(
|
|
67
|
+
"Something went wrong",
|
|
68
|
+
"LLM Provider",
|
|
69
|
+
"LLMProviderError",
|
|
70
|
+
"LLM Provider Error: Something went wrong",
|
|
71
|
+
"LLM Provider"
|
|
72
|
+
),
|
|
73
|
+
(
|
|
74
|
+
"API key is invalid",
|
|
75
|
+
"OpenAI",
|
|
76
|
+
"AuthenticationError",
|
|
77
|
+
"OpenAI Error: API key is invalid",
|
|
78
|
+
"OpenAI"
|
|
79
|
+
),
|
|
80
|
+
(
|
|
81
|
+
"There was an error accessing the Anthropic API: Error code: 529 - {'type': 'error', 'error': {'type': 'overloaded_error', 'message': 'Overloaded'}}",
|
|
82
|
+
"Anthropic",
|
|
83
|
+
"LLMProviderError",
|
|
84
|
+
"Anthropic Error: There was an error accessing the Anthropic API: Error code: 529 - {'type': 'error', 'error': {'type': 'overloaded_error', 'message': 'Overloaded'}}",
|
|
85
|
+
"Anthropic"
|
|
86
|
+
),
|
|
87
|
+
])
|
|
88
|
+
def test_exception_initialization(
|
|
89
|
+
self,
|
|
90
|
+
error_message: str,
|
|
91
|
+
provider_name: str,
|
|
92
|
+
error_type: str,
|
|
93
|
+
expected_title: str,
|
|
94
|
+
expected_hint_contains: str
|
|
95
|
+
):
|
|
96
|
+
"""Test exception initialization with various parameter combinations."""
|
|
97
|
+
exception = ProviderCompletionException(
|
|
98
|
+
error_message,
|
|
99
|
+
provider_name=provider_name,
|
|
100
|
+
error_type=error_type
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
assert exception.error_message == error_message
|
|
104
|
+
assert exception.provider_name == provider_name
|
|
105
|
+
assert exception.error_type == error_type
|
|
106
|
+
assert exception.user_friendly_title == expected_title
|
|
107
|
+
assert expected_hint_contains in exception.user_friendly_hint
|
|
108
|
+
assert str(exception) == expected_title
|
|
109
|
+
assert exception.args[0] == expected_title
|
|
110
|
+
|
|
111
|
+
def test_default_initialization(self):
|
|
112
|
+
"""Test exception initialization with default values."""
|
|
113
|
+
error_msg = "Something went wrong"
|
|
114
|
+
exception = ProviderCompletionException(error_msg)
|
|
115
|
+
|
|
116
|
+
assert exception.error_message == error_msg
|
|
117
|
+
assert exception.provider_name == "LLM Provider"
|
|
118
|
+
assert exception.error_type == "LLMProviderError"
|
|
119
|
+
assert exception.user_friendly_title == "LLM Provider Error: Something went wrong"
|
|
120
|
+
assert "LLM Provider" in exception.user_friendly_hint
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class TestGetResponseFromMitoServer:
|
|
124
|
+
"""Test the get_response_from_mito_server function."""
|
|
125
|
+
|
|
126
|
+
@pytest.mark.parametrize("completion_value,message_type", [
|
|
127
|
+
("This is the AI response", MessageType.CHAT),
|
|
128
|
+
("Code completion here", MessageType.INLINE_COMPLETION),
|
|
129
|
+
("", MessageType.CHAT), # Empty string
|
|
130
|
+
(None, MessageType.INLINE_COMPLETION), # None value
|
|
131
|
+
("Multi-line\nresponse\nhere", MessageType.CHAT), # Multi-line response
|
|
132
|
+
])
|
|
133
|
+
@pytest.mark.asyncio
|
|
134
|
+
async def test_successful_completion_responses(
|
|
135
|
+
self,
|
|
136
|
+
completion_value,
|
|
137
|
+
message_type: MessageType,
|
|
138
|
+
mock_request_params,
|
|
139
|
+
mock_http_dependencies
|
|
140
|
+
):
|
|
141
|
+
"""Test successful responses with various completion values."""
|
|
142
|
+
# Setup
|
|
143
|
+
response_body = {"completion": completion_value}
|
|
144
|
+
mock_response = create_mock_response(response_body)
|
|
145
|
+
mock_http_dependencies['mock_http_client'].fetch = AsyncMock(return_value=mock_response)
|
|
146
|
+
|
|
147
|
+
# Update request params
|
|
148
|
+
mock_request_params["message_type"] = message_type
|
|
149
|
+
|
|
150
|
+
# Execute
|
|
151
|
+
result = await get_response_from_mito_server(**mock_request_params)
|
|
152
|
+
|
|
153
|
+
# Verify
|
|
154
|
+
assert result == completion_value
|
|
155
|
+
mock_http_dependencies['mock_check_quota'].assert_called_once_with(message_type)
|
|
156
|
+
mock_http_dependencies['mock_update_quota'].assert_called_once_with(message_type)
|
|
157
|
+
mock_http_dependencies['mock_http_client'].close.assert_called_once()
|
|
158
|
+
|
|
159
|
+
# Verify HTTP request was made correctly
|
|
160
|
+
mock_http_dependencies['mock_http_client'].fetch.assert_called_once_with(
|
|
161
|
+
mock_request_params["url"],
|
|
162
|
+
method="POST",
|
|
163
|
+
headers=mock_request_params["headers"],
|
|
164
|
+
body=json.dumps(mock_request_params["data"]),
|
|
165
|
+
request_timeout=30
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
@pytest.mark.parametrize("error_message,provider_name,expected_exception_provider", [
|
|
169
|
+
(
|
|
170
|
+
"There was an error accessing the Anthropic API: Error code: 529 - {'type': 'error', 'error': {'type': 'overloaded_error', 'message': 'Overloaded'}}",
|
|
171
|
+
"Anthropic",
|
|
172
|
+
"Anthropic"
|
|
173
|
+
),
|
|
174
|
+
(
|
|
175
|
+
"Rate limit exceeded",
|
|
176
|
+
"OpenAI",
|
|
177
|
+
"OpenAI"
|
|
178
|
+
),
|
|
179
|
+
(
|
|
180
|
+
"Invalid API key",
|
|
181
|
+
"Custom Provider",
|
|
182
|
+
"Custom Provider"
|
|
183
|
+
),
|
|
184
|
+
(
|
|
185
|
+
"Server timeout",
|
|
186
|
+
"Mito Server",
|
|
187
|
+
"Mito Server"
|
|
188
|
+
),
|
|
189
|
+
])
|
|
190
|
+
@pytest.mark.asyncio
|
|
191
|
+
async def test_error_responses_from_server(
|
|
192
|
+
self,
|
|
193
|
+
error_message: str,
|
|
194
|
+
provider_name: str,
|
|
195
|
+
expected_exception_provider: str,
|
|
196
|
+
mock_request_params,
|
|
197
|
+
mock_http_dependencies
|
|
198
|
+
):
|
|
199
|
+
"""Test server returns error response with various error messages and providers."""
|
|
200
|
+
# Setup
|
|
201
|
+
response_body = {"error": error_message}
|
|
202
|
+
mock_response = create_mock_response(response_body)
|
|
203
|
+
mock_http_dependencies['mock_http_client'].fetch = AsyncMock(return_value=mock_response)
|
|
204
|
+
|
|
205
|
+
# Update request params
|
|
206
|
+
mock_request_params["provider_name"] = provider_name
|
|
207
|
+
|
|
208
|
+
# Execute and verify exception
|
|
209
|
+
with pytest.raises(ProviderCompletionException) as exc_info:
|
|
210
|
+
await get_response_from_mito_server(**mock_request_params)
|
|
211
|
+
|
|
212
|
+
# Verify exception details
|
|
213
|
+
exception = exc_info.value
|
|
214
|
+
assert exception.error_message == error_message
|
|
215
|
+
assert exception.provider_name == expected_exception_provider
|
|
216
|
+
assert f"{expected_exception_provider} Error" in str(exception)
|
|
217
|
+
|
|
218
|
+
# Verify quota was updated and client was closed
|
|
219
|
+
mock_http_dependencies['mock_update_quota'].assert_called_once_with(mock_request_params["message_type"])
|
|
220
|
+
mock_http_dependencies['mock_http_client'].close.assert_called_once()
|
|
221
|
+
|
|
222
|
+
@pytest.mark.parametrize("response_body,expected_error_contains", [
|
|
223
|
+
({"some_other_field": "value"}, "No completion found in response"),
|
|
224
|
+
({"data": "value", "status": "ok"}, "No completion found in response"),
|
|
225
|
+
({}, "No completion found in response"),
|
|
226
|
+
({"completion": None, "error": "also present"}, None), # completion takes precedence
|
|
227
|
+
])
|
|
228
|
+
@pytest.mark.asyncio
|
|
229
|
+
async def test_invalid_response_formats(
|
|
230
|
+
self,
|
|
231
|
+
response_body: dict,
|
|
232
|
+
expected_error_contains: str,
|
|
233
|
+
mock_request_params,
|
|
234
|
+
mock_http_dependencies
|
|
235
|
+
):
|
|
236
|
+
"""Test responses with invalid formats."""
|
|
237
|
+
# Setup
|
|
238
|
+
mock_response = create_mock_response(response_body)
|
|
239
|
+
mock_http_dependencies['mock_http_client'].fetch = AsyncMock(return_value=mock_response)
|
|
240
|
+
|
|
241
|
+
if "completion" in response_body:
|
|
242
|
+
# This should succeed because completion field exists
|
|
243
|
+
result = await get_response_from_mito_server(**mock_request_params)
|
|
244
|
+
assert result == response_body["completion"]
|
|
245
|
+
mock_http_dependencies['mock_update_quota'].assert_called_once()
|
|
246
|
+
else:
|
|
247
|
+
# Execute and verify exception
|
|
248
|
+
with pytest.raises(ProviderCompletionException) as exc_info:
|
|
249
|
+
await get_response_from_mito_server(**mock_request_params)
|
|
250
|
+
|
|
251
|
+
# Verify exception details
|
|
252
|
+
exception = exc_info.value
|
|
253
|
+
assert expected_error_contains in exception.error_message
|
|
254
|
+
assert str(response_body) in exception.error_message
|
|
255
|
+
assert exception.provider_name == mock_request_params["provider_name"]
|
|
256
|
+
|
|
257
|
+
# Verify quota was NOT updated
|
|
258
|
+
mock_http_dependencies['mock_update_quota'].assert_called_once_with(mock_request_params["message_type"])
|
|
259
|
+
|
|
260
|
+
# Client should always be closed
|
|
261
|
+
mock_http_dependencies['mock_http_client'].close.assert_called_once()
|
|
262
|
+
|
|
263
|
+
@pytest.mark.parametrize("invalid_json_content,expected_error_contains", [
|
|
264
|
+
("invalid json content", "Error parsing response"),
|
|
265
|
+
('{"incomplete": json', "Error parsing response"),
|
|
266
|
+
("", "Error parsing response"),
|
|
267
|
+
('{"malformed":', "Error parsing response"),
|
|
268
|
+
])
|
|
269
|
+
@pytest.mark.asyncio
|
|
270
|
+
async def test_json_parsing_errors(
|
|
271
|
+
self,
|
|
272
|
+
invalid_json_content: str,
|
|
273
|
+
expected_error_contains: str,
|
|
274
|
+
mock_request_params,
|
|
275
|
+
mock_http_dependencies
|
|
276
|
+
):
|
|
277
|
+
"""Test response with invalid or malformed JSON."""
|
|
278
|
+
# Setup
|
|
279
|
+
mock_response = MagicMock(spec=HTTPResponse)
|
|
280
|
+
mock_response.body.decode.return_value = invalid_json_content
|
|
281
|
+
mock_http_dependencies['mock_http_client'].fetch = AsyncMock(return_value=mock_response)
|
|
282
|
+
|
|
283
|
+
# Execute and verify exception
|
|
284
|
+
with pytest.raises(ProviderCompletionException) as exc_info:
|
|
285
|
+
await get_response_from_mito_server(**mock_request_params)
|
|
286
|
+
|
|
287
|
+
# Verify exception details
|
|
288
|
+
exception = exc_info.value
|
|
289
|
+
assert expected_error_contains in exception.error_message
|
|
290
|
+
assert exception.provider_name == mock_request_params["provider_name"]
|
|
291
|
+
|
|
292
|
+
# Verify quota was updated and client was closed
|
|
293
|
+
mock_http_dependencies['mock_update_quota'].assert_called_once_with(mock_request_params["message_type"])
|
|
294
|
+
mock_http_dependencies['mock_http_client'].close.assert_called_once()
|
|
295
|
+
|
|
296
|
+
@pytest.mark.parametrize("timeout,max_retries", [
|
|
297
|
+
(30, 3),
|
|
298
|
+
(45, 5),
|
|
299
|
+
(60, 1),
|
|
300
|
+
(15, 0),
|
|
301
|
+
])
|
|
302
|
+
@pytest.mark.asyncio
|
|
303
|
+
async def test_http_client_creation_parameters(
|
|
304
|
+
self,
|
|
305
|
+
timeout: int,
|
|
306
|
+
max_retries: int,
|
|
307
|
+
mock_request_params,
|
|
308
|
+
mock_http_dependencies
|
|
309
|
+
):
|
|
310
|
+
"""Test that HTTP client is created with correct parameters."""
|
|
311
|
+
# Setup
|
|
312
|
+
response_body = {"completion": "test response"}
|
|
313
|
+
mock_response = create_mock_response(response_body)
|
|
314
|
+
mock_http_dependencies['mock_http_client'].fetch = AsyncMock(return_value=mock_response)
|
|
315
|
+
|
|
316
|
+
# Update request params
|
|
317
|
+
mock_request_params["timeout"] = timeout
|
|
318
|
+
mock_request_params["max_retries"] = max_retries
|
|
319
|
+
|
|
320
|
+
# Execute
|
|
321
|
+
await get_response_from_mito_server(**mock_request_params)
|
|
322
|
+
|
|
323
|
+
# Verify HTTP client creation
|
|
324
|
+
mock_http_dependencies['mock_create_client'].assert_called_once_with(timeout, max_retries)
|
|
325
|
+
|
|
326
|
+
@pytest.mark.parametrize("exception_type,exception_message", [
|
|
327
|
+
(Exception, "Network error"),
|
|
328
|
+
(ConnectionError, "Connection failed"),
|
|
329
|
+
(TimeoutError, "Request timed out"),
|
|
330
|
+
(RuntimeError, "Runtime error occurred"),
|
|
331
|
+
])
|
|
332
|
+
@pytest.mark.asyncio
|
|
333
|
+
async def test_http_client_always_closed_on_exception(
|
|
334
|
+
self,
|
|
335
|
+
exception_type,
|
|
336
|
+
exception_message: str,
|
|
337
|
+
mock_request_params,
|
|
338
|
+
mock_http_dependencies
|
|
339
|
+
):
|
|
340
|
+
"""Test that HTTP client is always closed even when exceptions occur."""
|
|
341
|
+
# Setup - make fetch raise an exception
|
|
342
|
+
test_exception = exception_type(exception_message)
|
|
343
|
+
mock_http_dependencies['mock_http_client'].fetch = AsyncMock(side_effect=test_exception)
|
|
344
|
+
|
|
345
|
+
# Execute and expect exception to bubble up
|
|
346
|
+
with pytest.raises(exception_type, match=exception_message):
|
|
347
|
+
await get_response_from_mito_server(**mock_request_params)
|
|
348
|
+
|
|
349
|
+
# Verify client was still closed despite the exception
|
|
350
|
+
mock_http_dependencies['mock_http_client'].close.assert_called_once()
|
|
351
|
+
|
|
352
|
+
@pytest.mark.asyncio
|
|
353
|
+
async def test_default_provider_name(self, mock_http_dependencies):
|
|
354
|
+
"""Test that default provider name is used when not specified."""
|
|
355
|
+
# Setup
|
|
356
|
+
error_message = "Test error"
|
|
357
|
+
response_body = {"error": error_message}
|
|
358
|
+
mock_response = create_mock_response(response_body)
|
|
359
|
+
mock_http_dependencies['mock_http_client'].fetch = AsyncMock(return_value=mock_response)
|
|
360
|
+
|
|
361
|
+
# Test data without provider_name parameter
|
|
362
|
+
request_params = {
|
|
363
|
+
"url": "https://api.example.com",
|
|
364
|
+
"headers": {"Content-Type": "application/json"},
|
|
365
|
+
"data": {"query": "test query"},
|
|
366
|
+
"timeout": 30,
|
|
367
|
+
"max_retries": 3,
|
|
368
|
+
"message_type": MessageType.CHAT,
|
|
369
|
+
# Note: not providing provider_name parameter
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
# Execute and verify exception
|
|
373
|
+
with pytest.raises(ProviderCompletionException) as exc_info:
|
|
374
|
+
await get_response_from_mito_server(**request_params) # type: ignore
|
|
375
|
+
|
|
376
|
+
# Verify default provider name is used
|
|
377
|
+
exception = exc_info.value
|
|
378
|
+
assert exception.provider_name == "Mito Server"
|
|
379
|
+
assert "Mito Server Error" in str(exception)
|
|
380
|
+
|
|
381
|
+
@pytest.mark.asyncio
|
|
382
|
+
async def test_provider_completion_exception_reraised(self, mock_request_params, mock_http_dependencies):
|
|
383
|
+
"""Test that ProviderCompletionException is re-raised correctly during JSON parsing."""
|
|
384
|
+
# Setup - simulate ProviderCompletionException during JSON parsing
|
|
385
|
+
mock_response = MagicMock(spec=HTTPResponse)
|
|
386
|
+
mock_response.body.decode.return_value = "some json content" # This will trigger json.loads
|
|
387
|
+
|
|
388
|
+
def mock_json_loads(content, **kwargs):
|
|
389
|
+
raise ProviderCompletionException("Custom parsing error", "Custom Provider")
|
|
390
|
+
|
|
391
|
+
mock_http_dependencies['mock_http_client'].fetch = AsyncMock(return_value=mock_response)
|
|
392
|
+
|
|
393
|
+
with patch('mito_ai.utils.mito_server_utils.json.loads', side_effect=mock_json_loads), \
|
|
394
|
+
patch('mito_ai.utils.mito_server_utils.check_mito_server_quota') as mock_check_quota:
|
|
395
|
+
|
|
396
|
+
# Execute and verify exception
|
|
397
|
+
with pytest.raises(ProviderCompletionException) as exc_info:
|
|
398
|
+
await get_response_from_mito_server(**mock_request_params)
|
|
399
|
+
|
|
400
|
+
# Verify the original exception is preserved
|
|
401
|
+
exception = exc_info.value
|
|
402
|
+
assert exception.error_message == "Custom parsing error"
|
|
403
|
+
assert exception.provider_name == "Custom Provider"
|
|
404
|
+
|
|
405
|
+
# Verify quota check was called
|
|
406
|
+
mock_check_quota.assert_called_once_with(mock_request_params["message_type"])
|
|
407
|
+
|
|
408
|
+
# Verify client was closed
|
|
409
|
+
mock_http_dependencies['mock_http_client'].close.assert_called_once()
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
@pytest.mark.parametrize("scenario,response_setup,main_exception,quota_exception", [
|
|
413
|
+
("successful_with_quota_error", {"completion": "Success"}, None, Exception("Quota update failed")),
|
|
414
|
+
("server_error_with_quota_error", {"error": "Server error"}, ProviderCompletionException, Exception("Quota update failed")),
|
|
415
|
+
("invalid_format_with_quota_error", {"invalid": "format"}, ProviderCompletionException, RuntimeError("Quota system down")),
|
|
416
|
+
("success_with_quota_timeout", {"completion": "Success"}, None, TimeoutError("Quota service timeout")),
|
|
417
|
+
])
|
|
418
|
+
@pytest.mark.asyncio
|
|
419
|
+
async def test_quota_update_exceptions_do_not_interfere(
|
|
420
|
+
self,
|
|
421
|
+
scenario: str,
|
|
422
|
+
response_setup: dict,
|
|
423
|
+
main_exception,
|
|
424
|
+
quota_exception,
|
|
425
|
+
mock_request_params,
|
|
426
|
+
mock_http_dependencies
|
|
427
|
+
):
|
|
428
|
+
"""Test that quota update exceptions don't interfere with main function logic."""
|
|
429
|
+
# Setup
|
|
430
|
+
mock_response = create_mock_response(response_setup)
|
|
431
|
+
mock_http_dependencies['mock_http_client'].fetch = AsyncMock(return_value=mock_response)
|
|
432
|
+
mock_http_dependencies['mock_update_quota'].side_effect = quota_exception
|
|
433
|
+
|
|
434
|
+
# Execute
|
|
435
|
+
if main_exception:
|
|
436
|
+
with pytest.raises(main_exception) as exc_info:
|
|
437
|
+
await get_response_from_mito_server(**mock_request_params)
|
|
438
|
+
|
|
439
|
+
# Verify the original error is preserved, not the quota error
|
|
440
|
+
if "error" in response_setup:
|
|
441
|
+
assert exc_info.value.error_message == response_setup["error"]
|
|
442
|
+
else:
|
|
443
|
+
# Should still succeed despite quota update failure
|
|
444
|
+
result = await get_response_from_mito_server(**mock_request_params)
|
|
445
|
+
assert result == response_setup["completion"]
|
|
446
|
+
|
|
447
|
+
# Verify quota update was attempted
|
|
448
|
+
mock_http_dependencies['mock_update_quota'].assert_called_once_with(mock_request_params["message_type"])
|