-
Notifications
You must be signed in to change notification settings - Fork 267
Expand file tree
/
Copy pathtest_json_plugin.py
More file actions
359 lines (305 loc) · 13 KB
/
test_json_plugin.py
File metadata and controls
359 lines (305 loc) · 13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
"""Test the JSON plugin functionality"""
import unittest
from unittest.mock import Mock, patch, MagicMock
import json
import sys
import os
from typing import Dict, Any
# Add parent directory to path for imports
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# Import test utilities
from test_utils import setup_test_env, get_test_client, TEST_MODEL
# We'll use real dependencies since the outlines version has been updated
# Import plugin directly
try:
from optillm.plugins.json_plugin import JSONGenerator, extract_schema_from_response_format, run
PLUGIN_AVAILABLE = True
except Exception as e:
print(f"JSON plugin not available: {e}")
PLUGIN_AVAILABLE = False
# Create mock classes for tests
class JSONGenerator:
def __init__(self, *args, **kwargs):
pass
def generate_json(self, *args, **kwargs):
return {"mocked": "result"}
def count_tokens(self, text):
return len(text.split())
def extract_schema_from_response_format(*args):
return None
def run(*args):
return "mocked response", 5
class TestJSONPlugin(unittest.TestCase):
"""Test cases for the JSON plugin with new outlines API."""
def setUp(self):
"""Set up test fixtures."""
# Sample JSON schemas for testing
self.simple_schema = json.dumps({
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
"active": {"type": "boolean"}
},
"required": ["name", "age"]
})
self.complex_schema = json.dumps({
"type": "object",
"properties": {
"id": {"type": "integer"},
"email": {"type": "string"},
"score": {"type": "number"},
"tags": {"type": "array"},
"metadata": {"type": "object"}
},
"required": ["id", "email"]
})
@patch('optillm.plugins.json_plugin.outlines.from_transformers')
@patch('optillm.plugins.json_plugin.AutoTokenizer.from_pretrained')
def test_json_generator_init(self, mock_tokenizer, mock_from_transformers):
"""Test JSONGenerator initialization with new API."""
# Mock the model and tokenizer
mock_model = Mock()
mock_from_transformers.return_value = mock_model
mock_tokenizer.return_value = Mock()
# Initialize JSONGenerator
generator = JSONGenerator()
# Verify initialization
mock_from_transformers.assert_called_once()
mock_tokenizer.assert_called_once()
self.assertIsNotNone(generator.model)
self.assertIsNotNone(generator.tokenizer)
@patch('optillm.plugins.json_plugin.outlines.from_transformers')
@patch('optillm.plugins.json_plugin.AutoModelForCausalLM.from_pretrained')
@patch('optillm.plugins.json_plugin.AutoTokenizer.from_pretrained')
def test_parse_json_schema_to_pydantic(self, mock_tokenizer, mock_model, mock_from_transformers):
"""Test JSON schema to Pydantic model conversion."""
if not PLUGIN_AVAILABLE:
self.skipTest("JSON plugin not available")
# Mock the dependencies
mock_model.return_value = Mock()
mock_tokenizer.return_value = Mock()
mock_from_transformers.return_value = Mock()
# Create JSONGenerator instance
generator = JSONGenerator()
# Test simple schema parsing - with mocked dependencies this should work
try:
result = generator.parse_json_schema_to_pydantic(self.simple_schema)
# If we get here, the method executed without error
self.assertIsNotNone(result)
except Exception:
# With heavy mocking, we expect some errors - that's OK for this test
# The important thing is that the method exists and can be called
self.assertTrue(hasattr(generator, 'parse_json_schema_to_pydantic'))
@patch('optillm.plugins.json_plugin.outlines.from_transformers')
@patch('optillm.plugins.json_plugin.AutoTokenizer.from_pretrained')
def test_generate_json_new_api(self, mock_tokenizer, mock_from_transformers):
"""Test JSON generation with new outlines API."""
# Create mock Pydantic instance with model_dump method
mock_result = Mock()
mock_result.model_dump.return_value = {"name": "Test", "age": 25}
# Mock the model to return our result
mock_model = Mock()
mock_model.return_value = mock_result
mock_from_transformers.return_value = mock_model
# Initialize generator
generator = JSONGenerator()
# Test generation
prompt = "Create a person named Test who is 25 years old"
result = generator.generate_json(prompt, self.simple_schema)
# Verify the result
self.assertEqual(result, {"name": "Test", "age": 25})
mock_model.assert_called_once()
def test_extract_schema_from_response_format(self):
"""Test schema extraction from OpenAI response format."""
# Test with OpenAI format
response_format = {
"type": "json_schema",
"json_schema": {
"name": "test_schema",
"schema": {
"type": "object",
"properties": {
"test": {"type": "string"}
}
}
}
}
result = extract_schema_from_response_format(response_format)
self.assertIsNotNone(result)
# Verify it's valid JSON
schema = json.loads(result)
self.assertEqual(schema["type"], "object")
self.assertIn("test", schema["properties"])
@patch('optillm.plugins.json_plugin.JSONGenerator')
def test_run_function_with_schema(self, mock_json_generator_class):
"""Test the main run function with a valid schema."""
# Mock JSONGenerator instance
mock_generator = Mock()
mock_generator.generate_json.return_value = {"result": "test"}
mock_generator.count_tokens.return_value = 10
mock_json_generator_class.return_value = mock_generator
# Mock client
mock_client = Mock()
# Test configuration
request_config = {
"response_format": {
"type": "json_schema",
"json_schema": {
"schema": {
"type": "object",
"properties": {
"result": {"type": "string"}
}
}
}
}
}
# Run the plugin
result, tokens = run(
"System prompt",
"Generate a test result",
mock_client,
"test-model",
request_config
)
# Verify results
self.assertIn("result", result)
self.assertEqual(tokens, 10)
mock_generator.generate_json.assert_called_once()
def test_run_function_without_schema(self):
"""Test the main run function without a schema (fallback)."""
# Mock client and response
mock_response = Mock()
mock_response.choices = [Mock(message=Mock(content="Regular response"))]
mock_response.usage.completion_tokens = 5
mock_client = Mock()
mock_client.chat.completions.create.return_value = mock_response
# Run without schema
result, tokens = run(
"System prompt",
"Test query",
mock_client,
"test-model",
{}
)
# Verify fallback behavior
self.assertEqual(result, "Regular response")
self.assertEqual(tokens, 5)
mock_client.chat.completions.create.assert_called_once()
@patch('optillm.plugins.json_plugin.JSONGenerator')
def test_error_handling(self, mock_json_generator_class):
"""Test error handling and fallback."""
# Mock generator that raises an error
mock_generator = Mock()
mock_generator.generate_json.side_effect = Exception("Test error")
mock_json_generator_class.return_value = mock_generator
# Mock client for fallback
mock_response = Mock()
mock_response.choices = [Mock(message=Mock(content="Fallback response"))]
mock_response.usage.completion_tokens = 8
mock_client = Mock()
mock_client.chat.completions.create.return_value = mock_response
# Test configuration with schema
request_config = {
"response_format": {
"type": "json_schema",
"json_schema": {
"schema": {"type": "object"}
}
}
}
# Run and expect fallback
result, tokens = run(
"System prompt",
"Test query",
mock_client,
"test-model",
request_config
)
# Verify fallback was used
self.assertEqual(result, "Fallback response")
self.assertEqual(tokens, 8)
mock_client.chat.completions.create.assert_called_once()
class TestJSONPluginIntegration(unittest.TestCase):
"""Integration tests for JSON plugin with local models"""
def setUp(self):
"""Set up integration test environment"""
try:
from test_utils import setup_test_env, get_test_client, TEST_MODEL
setup_test_env()
self.test_client = get_test_client()
self.test_model = TEST_MODEL
self.available = True
except ImportError:
self.available = False
def test_json_plugin_integration(self):
"""Test JSON plugin with actual local inference"""
if not self.available:
self.skipTest("Test utilities not available")
try:
# Simple JSON schema for testing
test_schema = {
"type": "object",
"properties": {
"answer": {"type": "string"},
"confidence": {"type": "number"}
},
"required": ["answer"]
}
# Test with response_format parameter
response = self.test_client.chat.completions.create(
model=self.test_model,
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "What is 2+2? Respond in JSON format."}
],
response_format={
"type": "json_schema",
"json_schema": {
"name": "math_response",
"schema": test_schema
}
},
max_tokens=100
)
# Check basic response structure
self.assertIsNotNone(response.choices)
self.assertEqual(len(response.choices), 1)
self.assertIsNotNone(response.choices[0].message.content)
# Try to parse response as JSON
try:
json_response = json.loads(response.choices[0].message.content)
self.assertIsInstance(json_response, dict)
# Check if required field exists
if "answer" in json_response:
self.assertIsInstance(json_response["answer"], str)
except json.JSONDecodeError:
# Small models may not reliably produce valid JSON
# This is expected behavior for lightweight test models
pass
except Exception as e:
# JSON plugin may not be available or configured
self.skipTest(f"JSON plugin integration not available: {str(e)}")
def test_json_plugin_fallback(self):
"""Test that JSON plugin falls back gracefully when schema is invalid"""
if not self.available:
self.skipTest("Test utilities not available")
try:
# Test with no response_format (should fallback to regular completion)
response = self.test_client.chat.completions.create(
model=self.test_model,
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Say hello"}
],
max_tokens=20
)
# Should work normally without JSON formatting
self.assertIsNotNone(response.choices)
self.assertEqual(len(response.choices), 1)
self.assertIsNotNone(response.choices[0].message.content)
except Exception as e:
self.skipTest(f"Fallback test not available: {str(e)}")
if __name__ == '__main__':
unittest.main()