diff --git a/kouyu_english/lib/core/ai_service.dart b/kouyu_english/lib/core/ai_service.dart index b243294..d67a896 100644 --- a/kouyu_english/lib/core/ai_service.dart +++ b/kouyu_english/lib/core/ai_service.dart @@ -145,10 +145,12 @@ class AiService { try { if (provider == AiProviderType.gemini) { final mimeType = format == 'wav' ? 'audio/wav' : (format == 'mp3' ? 'audio/mp3' : 'audio/mp4'); - final response = await http.post( - uri, - headers: {'x-goog-api-key': key, 'Content-Type': 'application/json'}, - body: jsonEncode({ + final response = await _postJson( + provider: provider, + key: key, + uri: uri, + timeout: const Duration(seconds: 25), + body: { 'contents': [ { 'parts': [ @@ -169,18 +171,17 @@ class AiService { 'thinkingBudget': 1024, }, }, - }), - ).timeout(const Duration(seconds: 25)); - if (response.statusCode < 200 || response.statusCode >= 300) return null; + }, + ); + if (!_isSuccess(response)) return null; return _extractResponseContent(provider, response.body); } else { - final response = await http.post( - uri, - headers: { - 'Authorization': 'Bearer $key', - 'Content-Type': 'application/json', - }, - body: jsonEncode({ + final response = await _postJson( + provider: provider, + key: key, + uri: uri, + timeout: const Duration(seconds: 25), + body: { 'model': model, 'messages': [ { @@ -202,9 +203,9 @@ class AiService { ], 'reasoning_effort': 'low', 'temperature': 0.1, - }), - ).timeout(const Duration(seconds: 25)); - if (response.statusCode < 200 || response.statusCode >= 300) return null; + }, + ); + if (!_isSuccess(response)) return null; final raw = _extractResponseContent(provider, response.body); if (raw == null) return null; var text = raw.trim(); @@ -218,6 +219,151 @@ class AiService { } } + static bool _isHttpUri(Uri uri) => uri.scheme == 'https' || uri.scheme == 'http'; + + static bool _isSuccess(http.Response response) => + response.statusCode >= 200 && response.statusCode < 300; + + static Future _postJson({ + required AiProviderType provider, + required String key, + required Uri uri, + required Map body, + required Duration timeout, + }) { + return http + .post( + uri, + headers: provider == AiProviderType.gemini + ? {'x-goog-api-key': key, 'Content-Type': 'application/json'} + : { + 'Authorization': 'Bearer $key', + 'Content-Type': 'application/json', + }, + body: jsonEncode(body), + ) + .timeout(timeout); + } + + /// Sends a JSON-mode text request and returns the model's content. + /// + /// Returns null when the API key, model or endpoint is unusable, when the + /// request fails, or when the status is not 2xx. With [system] the messages + /// are a role-tagged conversation; without it they are a single prompt. + Future _requestContent({ + required AiProviderType provider, + required String endpoint, + required String model, + String? system, + required List> messages, + double? temperature, + required int maxTokens, + Duration timeout = const Duration(seconds: 30), + }) async { + final key = await resolveApiKey(); + final uri = resolveEndpointUri( + provider: provider, + endpoint: endpoint, + model: model, + ); + if (key == null || + key.isEmpty || + model.trim().isEmpty || + uri == null || + !_isHttpUri(uri)) { + return null; + } + try { + final response = await _postJson( + provider: provider, + key: key, + uri: uri, + timeout: timeout, + body: _buildTextPayload( + provider: provider, + uri: uri, + model: model, + system: system, + messages: messages, + temperature: temperature, + maxTokens: maxTokens, + ), + ); + if (!_isSuccess(response)) return null; + return _extractResponseContent(provider, response.body); + } catch (_) { + return null; + } + } + + Future _requestPrompt({ + required AiProviderType provider, + required String endpoint, + required String model, + required String prompt, + double? temperature, + required int maxTokens, + Duration timeout = const Duration(seconds: 30), + }) { + return _requestContent( + provider: provider, + endpoint: endpoint, + model: model, + messages: [ + {'role': 'user', 'content': prompt}, + ], + temperature: temperature, + maxTokens: maxTokens, + timeout: timeout, + ); + } + + static Map _buildTextPayload({ + required AiProviderType provider, + required Uri uri, + required String model, + String? system, + required List> messages, + double? temperature, + required int maxTokens, + }) { + if (provider != AiProviderType.gemini) { + return _buildOpenAiPayload( + uri: uri, + model: model, + messages: [ + if (system != null) {'role': 'system', 'content': system}, + ...messages, + ], + temperature: temperature, + maxTokens: maxTokens, + ); + } + return { + if (system != null) + 'systemInstruction': { + 'parts': [ + {'text': system}, + ], + }, + 'contents': [ + for (final message in messages) + { + if (system != null) + 'role': message['role'] == 'assistant' ? 'model' : 'user', + 'parts': [ + {'text': message['content']}, + ], + }, + ], + 'generationConfig': _buildGeminiGenerationConfig( + temperature: temperature, + maxOutputTokens: maxTokens, + responseMimeType: 'application/json', + ), + }; + } + static Map _buildOpenAiPayload({ required Uri uri, required String model, @@ -275,65 +421,17 @@ class AiService { text.trim().isEmpty) { return null; } - final key = await resolveApiKey(); - final uri = resolveEndpointUri( + const instruction = + 'Return JSON only: {"definition":"short simplified Chinese meaning"}. Do not include markdown, examples, or teaching claims.'; + final content = await _requestPrompt( provider: provider, endpoint: endpoint, model: model, + prompt: '$instruction\nText: $text', + maxTokens: 200, ); - if (key == null || - key.isEmpty || - model.trim().isEmpty || - uri == null || - (uri.scheme != 'https' && uri.scheme != 'http')) { - return null; - } - const instruction = - 'Return JSON only: {"definition":"short simplified Chinese meaning"}. Do not include markdown, examples, or teaching claims.'; + if (content == null || content.length > 300) return null; try { - final response = await http - .post( - uri, - headers: provider == AiProviderType.gemini - ? {'x-goog-api-key': key, 'Content-Type': 'application/json'} - : { - 'Authorization': 'Bearer $key', - 'Content-Type': 'application/json', - }, - body: jsonEncode( - provider == AiProviderType.gemini - ? { - 'contents': [ - { - 'parts': [ - {'text': '$instruction\nText: $text'}, - ], - }, - ], - 'generationConfig': _buildGeminiGenerationConfig( - maxOutputTokens: 200, - responseMimeType: 'application/json', - ), - } - : _buildOpenAiPayload( - uri: uri, - model: model, - messages: [ - { - 'role': 'user', - 'content': '$instruction\nText: $text', - }, - ], - maxTokens: 200, - ), - ), - ) - .timeout(const Duration(seconds: 30)); - if (response.statusCode < 200 || response.statusCode >= 300) { - return null; - } - final content = _extractResponseContent(provider, response.body); - if (content == null || content.length > 300) return null; final parsed = jsonDecode(content); if (parsed is! Map) return null; final definition = parsed['definition']; @@ -362,20 +460,6 @@ class AiService { return _buildMockSentenceAnalysis(cleanText); } - final key = await resolveApiKey(); - final uri = resolveEndpointUri( - provider: provider, - endpoint: endpoint, - model: model, - ); - if (key == null || - key.isEmpty || - model.trim().isEmpty || - uri == null || - (uri.scheme != 'https' && uri.scheme != 'http')) { - return null; - } - const instruction = 'You are an expert oral English coach for beginner adult learners (A0-A1). ' 'Analyze the given English sentence into clear, encouraging, beginner-friendly Chinese explanations. ' @@ -396,60 +480,20 @@ class AiService { ' ]\n' '}'; - try { - final response = await http - .post( - uri, - headers: provider == AiProviderType.gemini - ? {'x-goog-api-key': key, 'Content-Type': 'application/json'} - : { - 'Authorization': 'Bearer $key', - 'Content-Type': 'application/json', - }, - body: jsonEncode( - provider == AiProviderType.gemini - ? { - 'contents': [ - { - 'parts': [ - {'text': '$instruction\n\nSentence: $cleanText'}, - ], - }, - ], - 'generationConfig': _buildGeminiGenerationConfig( - maxOutputTokens: 800, - responseMimeType: 'application/json', - ), - } - : _buildOpenAiPayload( - uri: uri, - model: model, - messages: [ - { - 'role': 'user', - 'content': '$instruction\n\nSentence: $cleanText', - }, - ], - maxTokens: 800, - ), - ), - ) - .timeout(const Duration(seconds: 30)); - - if (response.statusCode < 200 || response.statusCode >= 300) { - return null; - } - final content = _extractResponseContent(provider, response.body); - if (content == null || content.isEmpty) return null; - return _decodeSentenceAnalysis( - raw: content, - originalText: cleanText, - provider: provider.name, - model: model, - ); - } catch (_) { - return null; - } + final content = await _requestPrompt( + provider: provider, + endpoint: endpoint, + model: model, + prompt: '$instruction\n\nSentence: $cleanText', + maxTokens: 800, + ); + if (content == null || content.isEmpty) return null; + return _decodeSentenceAnalysis( + raw: content, + originalText: cleanText, + provider: provider.name, + model: model, + ); } SentenceAnalysisResult? _decodeSentenceAnalysis({ @@ -614,51 +658,30 @@ class AiService { endpoint: endpoint, model: model, ); - if (uri == null || (uri.scheme != 'https' && uri.scheme != 'http')) { + if (uri == null || !_isHttpUri(uri)) { return const AiConnectionResult( ok: false, message: '请使用有效的 HTTP 或 HTTPS 地址。', ); } try { - final response = await http - .post( - uri, - headers: provider == AiProviderType.gemini - ? {'x-goog-api-key': key, 'Content-Type': 'application/json'} - : { - 'Authorization': 'Bearer $key', - 'Content-Type': 'application/json', - }, - body: jsonEncode( - provider == AiProviderType.gemini - ? { - 'contents': [ - { - 'parts': [ - {'text': probe}, - ], - }, - ], - 'generationConfig': _buildGeminiGenerationConfig( - temperature: 0, - maxOutputTokens: 200, - responseMimeType: 'application/json', - ), - } - : _buildOpenAiPayload( - uri: uri, - model: model, - messages: [ - {'role': 'user', 'content': probe}, - ], - temperature: 0, - maxTokens: 200, - ), - ), - ) - .timeout(const Duration(seconds: 15)); - if (response.statusCode >= 200 && response.statusCode < 300) { + final response = await _postJson( + provider: provider, + key: key, + uri: uri, + timeout: const Duration(seconds: 15), + body: _buildTextPayload( + provider: provider, + uri: uri, + model: model, + messages: [ + {'role': 'user', 'content': probe}, + ], + temperature: 0, + maxTokens: 200, + ), + ); + if (_isSuccess(response)) { final content = _extractResponseContent(provider, response.body); if (_decodeDialogueResponse(content) != null) { return const AiConnectionResult( @@ -716,21 +739,6 @@ class AiService { if (provider == AiProviderType.mock) { return null; } - final key = await resolveApiKey(); - if (key == null || - key.isEmpty || - endpoint.trim().isEmpty || - model.trim().isEmpty) { - return null; - } - final uri = resolveEndpointUri( - provider: provider, - endpoint: endpoint, - model: model, - ); - if (uri == null || (uri.scheme != 'https' && uri.scheme != 'http')) { - return null; - } final vocabularyRule = allowedLanguage.isEmpty ? '' : 'Build your reply from your goal wording, names, numbers and this ' @@ -748,64 +756,16 @@ class AiService { '"translation": "reply 的简体中文翻译", ' '"feedback": "一句中文点评学习者上一句英文,没有要说的就用 null"}. ' 'Only reply is required.'; - try { - final response = await http - .post( - uri, - headers: provider == AiProviderType.gemini - ? {'x-goog-api-key': key, 'Content-Type': 'application/json'} - : { - 'Authorization': 'Bearer $key', - 'Content-Type': 'application/json', - }, - body: jsonEncode( - provider == AiProviderType.gemini - ? { - 'systemInstruction': { - 'parts': [ - {'text': system}, - ], - }, - 'contents': history - .map( - (turn) => { - 'role': turn['role'] == 'assistant' - ? 'model' - : 'user', - 'parts': [ - {'text': turn['content']}, - ], - }, - ) - .toList(), - 'generationConfig': _buildGeminiGenerationConfig( - temperature: 0.3, - maxOutputTokens: 300, - responseMimeType: 'application/json', - ), - } - : _buildOpenAiPayload( - uri: uri, - model: model, - messages: [ - {'role': 'system', 'content': system}, - ...history, - ], - temperature: 0.3, - maxTokens: 300, - ), - ), - ) - .timeout(const Duration(seconds: 30)); - if (response.statusCode < 200 || response.statusCode >= 300) { - return null; - } - return _decodeDialogueResponse( - _extractResponseContent(provider, response.body), - ); - } catch (_) { - return null; - } + final content = await _requestContent( + provider: provider, + endpoint: endpoint, + model: model, + system: system, + messages: history, + temperature: 0.3, + maxTokens: 300, + ); + return _decodeDialogueResponse(content); } /// Generates only a bounded variant of an existing review target. A network @@ -819,82 +779,32 @@ class AiService { bool repairAttempt = false, }) async { if (provider == AiProviderType.mock) return null; - final key = await resolveApiKey(); - if (key == null || - key.isEmpty || - endpoint.trim().isEmpty || - model.trim().isEmpty) { - return null; - } - final uri = resolveEndpointUri( + final instruction = + 'Generate one A0 English review variant for item $targetItemId based on prompt "$basePrompt". Return JSON only with exactly: schemaVersion (must be "review-variant-1"), targetItemId (must be "$targetItemId"), prompt (short Chinese instruction), stimulus (English sentence, maximum 12 words), answer (exact expected English answer, maximum 10 words), acceptedAnswers (array of 1 to 4 strings), requiredAnyPhrases (array of 1 to 3 arrays of strings), forbiddenPhrases (array of up to 4 strings). Stay strictly within A0. Do not introduce new vocabulary. The answer must satisfy the spec.${repairAttempt ? ' Previous response failed schema or constraint validation: repair all errors.' : ''}'; + final content = await _requestPrompt( provider: provider, endpoint: endpoint, model: model, + prompt: instruction, + temperature: 0.2, + maxTokens: 300, ); - if (uri == null || (uri.scheme != 'https' && uri.scheme != 'http')) return null; - final instruction = - 'Generate one A0 English review variant for item $targetItemId based on prompt "$basePrompt". Return JSON only with exactly: schemaVersion (must be "review-variant-1"), targetItemId (must be "$targetItemId"), prompt (short Chinese instruction), stimulus (English sentence, maximum 12 words), answer (exact expected English answer, maximum 10 words), acceptedAnswers (array of 1 to 4 strings), requiredAnyPhrases (array of 1 to 3 arrays of strings), forbiddenPhrases (array of up to 4 strings). Stay strictly within A0. Do not introduce new vocabulary. The answer must satisfy the spec.${repairAttempt ? ' Previous response failed schema or constraint validation: repair all errors.' : ''}'; - try { - final response = await http - .post( - uri, - headers: provider == AiProviderType.gemini - ? {'x-goog-api-key': key, 'Content-Type': 'application/json'} - : { - 'Authorization': 'Bearer $key', - 'Content-Type': 'application/json', - }, - body: jsonEncode( - provider == AiProviderType.gemini - ? { - 'contents': [ - { - 'parts': [ - {'text': instruction}, - ], - }, - ], - 'generationConfig': _buildGeminiGenerationConfig( - temperature: 0.2, - maxOutputTokens: 300, - responseMimeType: 'application/json', - ), - } - : _buildOpenAiPayload( - uri: uri, - model: model, - messages: [ - {'role': 'user', 'content': instruction}, - ], - temperature: 0.2, - maxTokens: 300, - ), - ), - ) - .timeout(const Duration(seconds: 30)); - if (response.statusCode < 200 || response.statusCode >= 300) { - return null; - } - final content = _extractResponseContent(provider, response.body); - if (content == null) return null; - final variant = decodeGeneratedReviewVariant( - content, - expectedTargetItemId: targetItemId, + if (content == null) return null; + final variant = decodeGeneratedReviewVariant( + content, + expectedTargetItemId: targetItemId, + ); + if (variant == null && !repairAttempt) { + return generateReviewVariant( + provider: provider, + endpoint: endpoint, + model: model, + targetItemId: targetItemId, + basePrompt: basePrompt, + repairAttempt: true, ); - if (variant == null && !repairAttempt) { - return generateReviewVariant( - provider: provider, - endpoint: endpoint, - model: model, - targetItemId: targetItemId, - basePrompt: basePrompt, - repairAttempt: true, - ); - } - return variant; - } catch (_) { - return null; } + return variant; } /// Evaluates an open-ended writing response against a bounded schema. @@ -907,19 +817,6 @@ class AiService { required String answer, }) async { if (provider == AiProviderType.mock) return null; - final key = await resolveApiKey(); - if (key == null || - key.isEmpty || - endpoint.trim().isEmpty || - model.trim().isEmpty) { - return null; - } - final uri = resolveEndpointUri( - provider: provider, - endpoint: endpoint, - model: model, - ); - if (uri == null || (uri.scheme != 'https' && uri.scheme != 'http')) return null; final instruction = '''Return JSON only with exactly these fields: schemaVersion, verdict, feedback, suggestion, missing, lessonId. schemaVersion must be "writing-feedback-1" and lessonId must be "$lessonId". @@ -927,56 +824,19 @@ verdict must be accepted, rewrite, or uncertain. feedback is one short helpful C Assess only whether the learner expressed the task. Do not claim pronunciation, do not introduce grammar beyond A0, and do not invent facts the learner did not write. Task: $taskPrompt Learner wrote: $answer'''; - try { - final response = await http - .post( - uri, - headers: provider == AiProviderType.gemini - ? {'x-goog-api-key': key, 'Content-Type': 'application/json'} - : { - 'Authorization': 'Bearer $key', - 'Content-Type': 'application/json', - }, - body: jsonEncode( - provider == AiProviderType.gemini - ? { - 'contents': [ - { - 'parts': [ - {'text': instruction}, - ], - }, - ], - 'generationConfig': _buildGeminiGenerationConfig( - temperature: 0, - maxOutputTokens: 300, - responseMimeType: 'application/json', - ), - } - : _buildOpenAiPayload( - uri: uri, - model: model, - messages: [ - {'role': 'user', 'content': instruction}, - ], - temperature: 0, - maxTokens: 300, - ), - ), - ) - .timeout(const Duration(seconds: 30)); - if (response.statusCode < 200 || response.statusCode >= 300) { - return null; - } - final content = _extractResponseContent(provider, response.body); - if (content == null) return null; - return decodeWritingAiFeedback( - content, - expectedLessonId: lessonId, - ); - } catch (_) { - return null; - } + final content = await _requestPrompt( + provider: provider, + endpoint: endpoint, + model: model, + prompt: instruction, + temperature: 0, + maxTokens: 300, + ); + if (content == null) return null; + return decodeWritingAiFeedback( + content, + expectedLessonId: lessonId, + ); } /// Requests a 4-skill adaptive mini-lesson that re-teaches a failed target. @@ -989,83 +849,34 @@ Learner wrote: $answer'''; bool repairAttempt = false, }) async { if (provider == AiProviderType.mock) return null; - final key = await resolveApiKey(); - if (key == null || - key.isEmpty || - endpoint.trim().isEmpty || - model.trim().isEmpty) { - return null; - } - final uri = resolveEndpointUri( - provider: provider, - endpoint: endpoint, - model: model, - ); - if (uri == null || (uri.scheme != 'https' && uri.scheme != 'http')) return null; final lessonId = 'ai-a0-${targetItemId.toLowerCase()}-1'; final instruction = 'Return JSON only with exactly: schemaVersion, lessonId, revision, stageVersion, source, status, abilityIds, prerequisiteIds, targetItemIds, receptiveChunks, newItemIds, previewItemIds, estimatedMinutes, tasks. Use schemaVersion lesson-2, lessonId $lessonId, revision 1, stageVersion A0-1.0, source aiGenerated, status validated, targetItemIds [$targetItemId], and empty receptiveChunks, newItemIds, previewItemIds. Create exactly four tasks, one listening listenChoice, speaking repeat, reading readAnswer, writing writeAnswer. Every task has exactly taskId, skill, type, prompt, stimulus, answer, targetItemIds, answerSpec and targets [$targetItemId]. answerSpec has exactly requiredAnyPhrases (1-4 lists, each contains 1-4 accepted English phrases), acceptedAnswers (1-4 complete accepted English answers), forbiddenPhrases (possibly empty list). Make answer satisfy its answerSpec. Lesson duration is 8 to 15 minutes. Use only very simple A0 English for $targetLabel. No new vocabulary, markdown, real phone numbers, or personal data.${repairAttempt ? ' Previous response was invalid: repair all constraints.' : ''}'; - try { - final response = await http - .post( - uri, - headers: provider == AiProviderType.gemini - ? {'x-goog-api-key': key, 'Content-Type': 'application/json'} - : { - 'Authorization': 'Bearer $key', - 'Content-Type': 'application/json', - }, - body: jsonEncode( - provider == AiProviderType.gemini - ? { - 'contents': [ - { - 'parts': [ - {'text': instruction}, - ], - }, - ], - 'generationConfig': _buildGeminiGenerationConfig( - temperature: 0.1, - maxOutputTokens: 850, - responseMimeType: 'application/json', - ), - } - : _buildOpenAiPayload( - uri: uri, - model: model, - messages: [ - {'role': 'user', 'content': instruction}, - ], - temperature: 0.1, - maxTokens: 850, - ), - ), - ) - .timeout(const Duration(seconds: 45)); - if (response.statusCode < 200 || response.statusCode >= 300) { - return null; - } - final content = _extractResponseContent(provider, response.body); - if (content == null) return null; - final lesson = decodeGeneratedLesson( - content, - expectedTargetItemId: targetItemId, + final content = await _requestPrompt( + provider: provider, + endpoint: endpoint, + model: model, + prompt: instruction, + temperature: 0.1, + maxTokens: 850, + timeout: const Duration(seconds: 45), + ); + if (content == null) return null; + final lesson = decodeGeneratedLesson( + content, + expectedTargetItemId: targetItemId, + ); + if (lesson == null && !repairAttempt) { + return generateAdaptiveLesson( + provider: provider, + endpoint: endpoint, + model: model, + targetItemId: targetItemId, + targetLabel: targetLabel, + repairAttempt: true, ); - if (lesson == null && !repairAttempt) { - return generateAdaptiveLesson( - provider: provider, - endpoint: endpoint, - model: model, - targetItemId: targetItemId, - targetLabel: targetLabel, - repairAttempt: true, - ); - } - return lesson; - } catch (_) { - return null; } + return lesson; } /// Sends the entire generated lesson structure to an independent LLM audit. @@ -1076,19 +887,6 @@ Learner wrote: $answer'''; required GeneratedLesson lesson, }) async { if (provider == AiProviderType.mock) return true; - final key = await resolveApiKey(); - if (key == null || - key.isEmpty || - endpoint.trim().isEmpty || - model.trim().isEmpty) { - return false; - } - final uri = resolveEndpointUri( - provider: provider, - endpoint: endpoint, - model: model, - ); - if (uri == null || (uri.scheme != 'https' && uri.scheme != 'http')) return false; final lessonJson = jsonEncode({ 'lessonId': lesson.lessonId, 'stageVersion': lesson.stageVersion, @@ -1113,53 +911,15 @@ Learner wrote: $answer'''; }); final instruction = 'Audit this A0 English lesson independently. Check naturalness, that every answer follows its stimulus, that it stays A0, and that each task is solvable without giving the answer. Return JSON only with exactly schemaVersion, approved, reason. schemaVersion must be lesson-audit-1. approved is boolean and reason is a short Chinese string. Lesson: $lessonJson'; - try { - final response = await http - .post( - uri, - headers: provider == AiProviderType.gemini - ? {'x-goog-api-key': key, 'Content-Type': 'application/json'} - : { - 'Authorization': 'Bearer $key', - 'Content-Type': 'application/json', - }, - body: jsonEncode( - provider == AiProviderType.gemini - ? { - 'contents': [ - { - 'parts': [ - {'text': instruction}, - ], - }, - ], - 'generationConfig': _buildGeminiGenerationConfig( - temperature: 0, - maxOutputTokens: 200, - responseMimeType: 'application/json', - ), - } - : _buildOpenAiPayload( - uri: uri, - model: model, - messages: [ - {'role': 'user', 'content': instruction}, - ], - temperature: 0, - maxTokens: 200, - ), - ), - ) - .timeout(const Duration(seconds: 30)); - if (response.statusCode < 200 || response.statusCode >= 300) { - return false; - } - return _decodeLessonAudit( - _extractResponseContent(provider, response.body), - ); - } catch (_) { - return false; - } + final content = await _requestPrompt( + provider: provider, + endpoint: endpoint, + model: model, + prompt: instruction, + temperature: 0, + maxTokens: 200, + ); + return _decodeLessonAudit(content); } bool _decodeLessonAudit(String? raw) {