perf: improve AI prompt cache reuse
This commit is contained in:
@@ -26,6 +26,16 @@ void main() {
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(
|
||||
{for (final item in descriptors) item.id: item.promptVersion},
|
||||
{
|
||||
'speech-transcription': 1,
|
||||
'lexicon-explanation': 2,
|
||||
'dialogue-coach': 3,
|
||||
'answer-evaluation': 2,
|
||||
'review-generation': 2,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:kouyu_english/core/ai_service.dart';
|
||||
import 'package:kouyu_english/core/models.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
test(
|
||||
'lexicon requests keep a stable system prefix and dynamic JSON last',
|
||||
() async {
|
||||
AiService.instance.setFallbackApiKey('test-key');
|
||||
final captured = <List<dynamic>>[];
|
||||
|
||||
Future<void> request(String text) async {
|
||||
await http.runWithClient(
|
||||
() => AiService.instance.temporaryDefinition(
|
||||
provider: AiProviderType.compatible,
|
||||
endpoint: 'https://api.deepseek.com',
|
||||
model: 'deepseek-flash',
|
||||
text: text,
|
||||
),
|
||||
() => MockClient((request) async {
|
||||
final body = jsonDecode(request.body) as Map<String, dynamic>;
|
||||
captured.add(body['messages'] as List<dynamic>);
|
||||
return http.Response(
|
||||
jsonEncode({
|
||||
'choices': [
|
||||
{
|
||||
'message': {'content': '{"definition":"测试"}'},
|
||||
},
|
||||
],
|
||||
}),
|
||||
200,
|
||||
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
await request('apple');
|
||||
await request('airport');
|
||||
|
||||
expect(captured, hasLength(2));
|
||||
expect(captured[0], hasLength(2));
|
||||
expect(captured[0][0]['role'], 'system');
|
||||
expect(captured[0][0]['content'], captured[1][0]['content']);
|
||||
expect(captured[0][0]['content'], isNot(contains('apple')));
|
||||
expect(jsonDecode(captured[0][1]['content'] as String), {
|
||||
'text': 'apple',
|
||||
});
|
||||
expect(jsonDecode(captured[1][1]['content'] as String), {
|
||||
'text': 'airport',
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'dialogue evaluation keeps learner data out of the cached prefix',
|
||||
() async {
|
||||
AiService.instance.setFallbackApiKey('test-key');
|
||||
final captured = <List<dynamic>>[];
|
||||
|
||||
Future<void> request({
|
||||
required String learnerText,
|
||||
required String turnId,
|
||||
}) async {
|
||||
await http.runWithClient(
|
||||
() => AiService.instance.checkDialogueIntervention(
|
||||
provider: AiProviderType.compatible,
|
||||
endpoint: 'https://api.deepseek.com',
|
||||
model: 'deepseek-flash',
|
||||
partnerLine: 'How are you?',
|
||||
taskLabel: 'Say how you feel.',
|
||||
learnerText: learnerText,
|
||||
turnId: turnId,
|
||||
),
|
||||
() => MockClient((request) async {
|
||||
final body = jsonDecode(request.body) as Map<String, dynamic>;
|
||||
captured.add(body['messages'] as List<dynamic>);
|
||||
return http.Response(
|
||||
jsonEncode({
|
||||
'choices': [
|
||||
{
|
||||
'message': {
|
||||
'content': jsonEncode({
|
||||
'schemaVersion': 'dialogue-intervention-2',
|
||||
'turnId': turnId,
|
||||
'accepted': true,
|
||||
'goalSatisfied': true,
|
||||
'verdict': 'accepted',
|
||||
'reasonCode': 'goal-met',
|
||||
'suggestion': null,
|
||||
'explanation': '表达自然。',
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
200,
|
||||
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
await request(learnerText: 'I am fine.', turnId: 'turn-1');
|
||||
await request(learnerText: 'I feel tired.', turnId: 'turn-2');
|
||||
|
||||
final firstSystem = captured[0][0]['content'] as String;
|
||||
final secondSystem = captured[1][0]['content'] as String;
|
||||
expect(firstSystem, secondSystem);
|
||||
expect(firstSystem, isNot(contains('I am fine.')));
|
||||
expect(firstSystem, isNot(contains('turn-1')));
|
||||
|
||||
final firstInput = jsonDecode(captured[0][1]['content'] as String);
|
||||
final secondInput = jsonDecode(captured[1][1]['content'] as String);
|
||||
expect(firstInput['learnerText'], 'I am fine.');
|
||||
expect(firstInput['turnId'], 'turn-1');
|
||||
expect(secondInput['learnerText'], 'I feel tired.');
|
||||
expect(secondInput['turnId'], 'turn-2');
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -100,13 +100,15 @@ void main() {
|
||||
);
|
||||
|
||||
expect(reply?.reply, 'Where are you from?');
|
||||
expect(messages, hasLength(3));
|
||||
expect(messages, hasLength(4));
|
||||
expect(messages![0]['role'], 'system');
|
||||
expect(jsonDecode(messages![1]['content'] as String), {
|
||||
expect(messages![1]['role'], 'user');
|
||||
expect(messages![1]['content'], startsWith('[ConversationConfig]'));
|
||||
expect(jsonDecode(messages![2]['content'] as String), {
|
||||
'reply': "Hi! What's your name?",
|
||||
});
|
||||
expect(messages![2]['role'], 'user');
|
||||
final last = messages![2]['content'] as String;
|
||||
expect(messages![3]['role'], 'user');
|
||||
final last = messages![3]['content'] as String;
|
||||
expect(last, startsWith('My name is Alex.'));
|
||||
expect(
|
||||
last,
|
||||
@@ -114,28 +116,33 @@ void main() {
|
||||
);
|
||||
expect(
|
||||
last,
|
||||
endsWith('Do not repeat any questions or greetings already asked or answered.'),
|
||||
endsWith(
|
||||
'Do not repeat any questions or greetings already asked or answered.',
|
||||
),
|
||||
);
|
||||
expect(messages![0]['content'], isNot(contains('Ask where the learner')));
|
||||
});
|
||||
|
||||
test('dialogue system prompt stays identical across turns', () {
|
||||
test('dialogue system prompt stays identical across levels and lessons', () {
|
||||
final words = ['Excuse me.', 'Can you help me?'];
|
||||
final a = AiService.dialogueSystemPrompt(
|
||||
level: 'A1',
|
||||
allowedLanguage: words,
|
||||
);
|
||||
final b = AiService.dialogueSystemPrompt(
|
||||
level: 'A1',
|
||||
allowedLanguage: words,
|
||||
);
|
||||
final a = AiService.dialogueSystemPrompt();
|
||||
final b = AiService.dialogueSystemPrompt();
|
||||
expect(a, b);
|
||||
expect(a, contains('CEFR A1'));
|
||||
expect(a, contains('Strictly do not repeat any question'));
|
||||
expect(a, endsWith('Taught language: Excuse me.; Can you help me?'));
|
||||
expect(a, isNot(contains('Excuse me.')));
|
||||
|
||||
final config = AiService.dialogueConversationConfig(
|
||||
level: 'A1',
|
||||
allowedLanguage: words,
|
||||
);
|
||||
expect(config, contains('A1'));
|
||||
expect(config, contains('Excuse me.'));
|
||||
expect(
|
||||
AiService.dialogueSystemPrompt(level: 'A0', allowedLanguage: const []),
|
||||
contains('A0 English conversation partner'),
|
||||
AiService.dialogueConversationConfig(
|
||||
level: 'A0',
|
||||
allowedLanguage: const [],
|
||||
),
|
||||
contains('simple, common A0 English'),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -57,7 +57,11 @@ void main() {
|
||||
() => MockClient((request) async {
|
||||
final body = jsonDecode(request.body) as Map<String, dynamic>;
|
||||
final messages = body['messages'] as List<dynamic>;
|
||||
prompts.add((messages.single as Map<String, dynamic>)['content']);
|
||||
expect(messages, hasLength(2));
|
||||
expect(messages.first['role'], 'system');
|
||||
expect(messages.last['role'], 'user');
|
||||
prompts.add(messages.first['content'] as String);
|
||||
prompts.add(messages.last['content'] as String);
|
||||
return http.Response(
|
||||
jsonEncode({
|
||||
'choices': [
|
||||
@@ -73,7 +77,8 @@ void main() {
|
||||
);
|
||||
|
||||
expect(variant, isNotNull);
|
||||
expect(prompts, hasLength(1));
|
||||
expect(prompts, hasLength(2));
|
||||
final fullPrompt = prompts.join('\n');
|
||||
for (final field in [
|
||||
'schemaVersion',
|
||||
'variantId',
|
||||
@@ -81,10 +86,10 @@ void main() {
|
||||
'prompt',
|
||||
'expectedAnswer',
|
||||
]) {
|
||||
expect(prompts.single, contains(field));
|
||||
expect(fullPrompt, contains(field));
|
||||
}
|
||||
for (final field in ['stimulus', 'acceptedAnswers', 'forbiddenPhrases']) {
|
||||
expect(prompts.single, isNot(contains(field)));
|
||||
expect(fullPrompt, isNot(contains(field)));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user