Files
baiyue-logistics-chat/tests/test_model_clarification.py
T

105 lines
4.8 KiB
Python

"""Model-selected clarifications must survive grounded rendering and persistence."""
import json
import os
from pathlib import Path
import tempfile
import unittest
from unittest.mock import Mock, patch
from logistics_chat import config, create_app, db
from logistics_chat.chat_tools import validate_calls
def clarification_plan(**extra):
return {'role': 'assistant', 'content': None, 'tool_calls': [{
'id': 'clarify_synthetic', 'type': 'function', 'function': {
'name': 'read_service_info',
'arguments': json.dumps({'question': '还是按刚才说的那个办吧', **extra}, ensure_ascii=False),
},
}]}
def completion_response(message):
response = Mock(status_code=200)
response.json.return_value = {'choices': [{
'finish_reason': 'tool_calls' if message.get('tool_calls') else 'stop',
'message': message,
}]}
return response
class ModelClarificationTests(unittest.TestCase):
def setUp(self):
directory = tempfile.TemporaryDirectory()
self.addCleanup(directory.cleanup)
root = Path(directory.name)
root_patch = patch.object(config, 'ROOT_DIR', root)
root_patch.start()
self.addCleanup(root_patch.stop)
environment = patch.dict(os.environ, {}, clear=True)
environment.start()
self.addCleanup(environment.stop)
network = patch('requests.sessions.Session.request',
side_effect=AssertionError('Clarification tests must not access the network'))
network.start()
self.addCleanup(network.stop)
self.app = create_app({
'TESTING': True, 'INITIALIZE_RUNTIME': False,
'DATABASE': root / 'history.db', 'UPLOAD_DIR': root / 'uploads',
'BACKUP_DIR': root / 'backups', 'LOG_DIR': root / 'logs',
'CHAT_API_KEY': 'synthetic-key', 'DEEPSEEK_API_KEY': '',
'CHAT_USE_MODEL_TOOLS': True, 'MODEL_PROVIDER': 'compatible',
'MODEL_URL': 'https://model.example.invalid/chat/completions',
'MODEL_NAME': 'synthetic-model', 'CUSTOMER_DEMO_ENABLED': False,
})
with self.app.app_context():
db.init_db()
self.client = self.app.test_client()
self.client.post('/api/visitor/session', json={'key': '8' * 64})
self.cid = self.client.post('/api/new_chat').json['id']
with self.app.app_context():
db.save_message(self.cid, 'user', '请比较此前的两种方案')
db.save_message(self.cid, 'assistant', '已展示方案甲和方案乙,请明确选择哪一种。')
def ask(self, final_content):
responses = [completion_response(clarification_plan(clarification='quote_option')),
completion_response({'role': 'assistant', 'content': final_content})]
with patch('logistics_chat.ai_client.post_completion', side_effect=responses) as model:
response = self.client.post('/stream_chat', json={
'msg': '还是按刚才说的那个办吧', 'conv_id': self.cid})
events = [json.loads(frame[6:]) for frame in response.get_data(as_text=True).split('\n\n')
if frame.startswith('data: ')]
self.assertEqual(events[-1], {'d': 1})
self.assertEqual(model.call_count, 2)
evidence = json.loads(model.call_args_list[1].args[0]['messages'][-1]['content'])
return events[0], evidence
def test_model_selects_quote_clarification_and_renders_its_fact(self):
answer, evidence = self.ask('{"facts":["fact_0"],"question":""}')
self.assertEqual(evidence['status'], 'needs_clarification')
self.assertEqual(answer['t'], evidence['result'])
self.assertIn('报价方案编号或渠道', answer['t'])
self.assertNotIn('您好', answer['t'])
self.assertNotIn('fact_0', answer['t'])
history = self.client.get('/api/get_messages', query_string={'conv_id': self.cid}).json
self.assertEqual(history[-1]['content'], answer['t'])
def test_untrusted_rewrite_falls_back_to_the_same_clarification(self):
answer, evidence = self.ask('已选择方案甲,运费9999元。')
self.assertEqual(answer['t'], evidence['result'])
self.assertNotIn('已选择', answer['t'])
self.assertNotIn('9999', answer['t'])
def test_clarification_is_optional_and_rejects_unknown_values(self):
self.assertEqual(validate_calls(clarification_plan())[0][1], 'read_service_info')
valid = validate_calls(clarification_plan(clarification='quote_option'))
self.assertEqual(valid[0][2]['clarification'], 'quote_option')
for value in ('choose_first', '', None):
with self.subTest(value=value), self.assertRaises(ValueError):
validate_calls(clarification_plan(clarification=value))
if __name__ == '__main__':
unittest.main()