sendMessageStream method
Sends a message to the AI service and returns a stream of responses.
This method enables real-time streaming of AI responses, allowing the UI to display responses as they are generated.
The message parameter is the user's input text.
Returns a Stream<String> that yields response chunks as they arrive.
Throws an Exception if:
- API key is not set
- Network request fails
- API returns an error response
Example:
final stream = aiService.sendMessageStream('Tell me a story');
await for (final chunk in stream) {
print(chunk); // Print each chunk as it arrives
}
Implementation
Stream<String> sendMessageStream(final String message) async* {
if (apiKey == null || apiKey!.isEmpty) {
throw Exception('API key not set. Please configure your OpenAI API key.');
}
try {
final request = http.Request(
'POST',
Uri.parse('$_baseUrl/chat/completions'),
);
request.headers['Content-Type'] = 'application/json';
request.headers['Authorization'] = 'Bearer $apiKey';
request.body = jsonEncode({
'model': _model,
'messages': [
{'role': 'user', 'content': message},
],
'max_tokens': 1000,
'temperature': 0.7,
'stream': true,
});
final streamedResponse = await request.send();
if (streamedResponse.statusCode == 200) {
await for (final chunk in streamedResponse.stream.transform(
utf8.decoder,
)) {
final lines = chunk.split('\n');
for (final line in lines) {
if (line.startsWith('data: ')) {
final data = line.substring(6);
if (data == '[DONE]') {
break;
}
try {
final json = jsonDecode(data) as Map<String, dynamic>;
final choices = json['choices'] as List<dynamic>?;
if (choices != null && choices.isNotEmpty) {
final firstChoice = choices[0] as Map<String, dynamic>;
final delta = firstChoice['delta'] as Map<String, dynamic>?;
final content = delta?['content'] as String?;
if (content != null) {
yield content;
}
}
} on FormatException {
// Skip malformed JSON chunks
continue;
}
}
}
}
} else {
final errorResponse = await streamedResponse.stream.bytesToString();
try {
final error = jsonDecode(errorResponse) as Map<String, dynamic>;
final errorObj = error['error'] as Map<String, dynamic>?;
final errorMessage =
(errorObj?['message'] as String?) ?? 'Unknown API error';
// Provide more user-friendly error messages
var userMessage = errorMessage;
final messageLower = errorMessage.toLowerCase();
if (messageLower.contains('quota') ||
messageLower.contains('billing')) {
userMessage =
'API Quota Exceeded: $errorMessage\n\nPlease check your '
'OpenAI account billing and usage limits.';
} else if (messageLower.contains('invalid_api_key') ||
messageLower.contains('authentication')) {
userMessage =
'Invalid API Key: Please check your OpenAI API key in '
'settings.';
} else if (messageLower.contains('rate_limit')) {
userMessage =
'Rate Limit Exceeded: Please wait a moment and try again.';
}
throw Exception(userMessage);
} on FormatException {
throw Exception(
'API returned an invalid response. Status code: '
'${streamedResponse.statusCode}',
);
}
}
} on FormatException catch (e) {
throw Exception('Invalid response format: $e');
} on Exception {
rethrow;
} catch (e) {
throw Exception('Network error: $e');
}
}