Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions lib/src/providers/implementations/firebase_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ class FirebaseProvider extends LlmProvider with ChangeNotifier {
///
/// [chatGenerationConfig] is an optional configuration for controlling the
/// model's generation behavior.
///
/// [onFunctionCalls] is an optional callback invoked with the function calls
/// collected from a single turn of the model's response.
FirebaseProvider({
required GenerativeModel model,
void Function(Iterable<FunctionCall>)? onFunctionCalls,
Expand All @@ -35,6 +38,7 @@ class FirebaseProvider extends LlmProvider with ChangeNotifier {
GenerationConfig? chatGenerationConfig,
Future<Map<String, Object?>?> Function(FunctionCall)? onFunctionCall,
}) : _model = model,
_onFunctionCalls = onFunctionCalls,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While we're at it, should we also rename the callback to willCallFunctions? I'm not a fan of the on prefix, as it doesn't explain when exactly the callback is called relative to the anchor event. Otherwise, the bug fix looks good.

_history = history?.toList() ?? [],
_chatSafetySettings = chatSafetySettings,
_chatGenerationConfig = chatGenerationConfig,
Expand All @@ -43,6 +47,7 @@ class FirebaseProvider extends LlmProvider with ChangeNotifier {
}

final GenerativeModel _model;
final void Function(Iterable<FunctionCall>)? _onFunctionCalls;
final List<SafetySetting>? _chatSafetySettings;
final GenerationConfig? _chatGenerationConfig;
final List<ChatMessage> _history;
Expand Down Expand Up @@ -123,6 +128,9 @@ class FirebaseProvider extends LlmProvider with ChangeNotifier {
break;
}

// Notify the caller of the function calls collected in this turn
_onFunctionCalls?.call(functionCalls);

// Add newline between responses
yield '\n';

Expand Down
2 changes: 2 additions & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ dependencies:
waveform_recorder: ^1.8.0

dev_dependencies:
firebase_core: ^4.13.0
firebase_core_platform_interface: ^8.1.0
flutter_lints: ^6.0.0
flutter_test:
sdk: flutter
Expand Down
162 changes: 162 additions & 0 deletions test/firebase_provider_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// Copyright 2024 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// ignore_for_file: implementation_imports

import 'package:firebase_ai/firebase_ai.dart';
import 'package:firebase_ai/src/base_model.dart';
import 'package:firebase_ai/src/client.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_core_platform_interface/test.dart';
import 'package:flutter_ai_toolkit/flutter_ai_toolkit.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
TestWidgetsFlutterBinding.ensureInitialized();
TestFirebaseCoreHostApi.setUp(_MockFirebaseApp());

late FirebaseApp app;
setUpAll(() async {
app = await Firebase.initializeApp();
});

group('FirebaseProvider.onFunctionCalls', () {
test(
'invokes onFunctionCalls with the calls collected in a turn',
() async {
final client =
_StubApiClient()
..streamResponses.addAll([
[_functionCallResponse],
[_textResponse],
]);

final recordedCalls = <FunctionCall>[];
final provider = FirebaseProvider(
model: _createModel(app, client),
onFunctionCalls: recordedCalls.addAll,
onFunctionCall: (functionCall) async => {'temperature': 60},
);

await provider
.sendMessageStream('What is the temperature?')
.drain<void>();

expect(recordedCalls, hasLength(1));
expect(recordedCalls.single.name, 'getTemperature');
expect(recordedCalls.single.args, {'city': 'Portland'});
},
);

test(
'does not invoke onFunctionCalls when no function calls are made',
() async {
final client = _StubApiClient()..streamResponses.add([_textResponse]);

final recordedCalls = <FunctionCall>[];
final provider = FirebaseProvider(
model: _createModel(app, client),
onFunctionCalls: recordedCalls.addAll,
);

await provider.sendMessageStream('Just say something').drain<void>();

expect(recordedCalls, isEmpty);
},
);
});
}

GenerativeModel _createModel(FirebaseApp app, ApiClient client) =>
createModelWithClient(
app: app,
location: 'us-central1',
model: 'some-model',
client: client,
useAgentPlatform: false,
);

const _functionCallResponse = {
'candidates': [
{
'content': {
'role': 'model',
'parts': [
{
'functionCall': {
'name': 'getTemperature',
'args': {'city': 'Portland'},
},
},
],
},
'finishReason': 'STOP',
},
],
};

const _textResponse = {
'candidates': [
{
'content': {
'role': 'model',
'parts': [
{'text': 'The temperature is 60F.'},
],
},
'finishReason': 'STOP',
},
],
};

final class _StubApiClient implements ApiClient {
final streamResponses = <List<Map<String, Object?>>>[];

@override
Future<Map<String, Object?>> makeRequest(
Uri uri,
Map<String, Object?> body,
) => throw UnimplementedError();

@override
Stream<Map<String, Object?>> streamRequest(
Uri uri,
Map<String, Object?> body,
) => Stream.fromIterable(streamResponses.removeAt(0));
}

class _MockFirebaseApp implements TestFirebaseCoreHostApi {
@override
Future<CoreInitializeResponse> initializeApp(
String appName,
CoreFirebaseOptions initializeAppRequest,
) async => CoreInitializeResponse(
name: appName,
options: initializeAppRequest,
pluginConstants: {},
);

@override
Future<List<CoreInitializeResponse>> initializeCore() async => [
CoreInitializeResponse(
name: defaultFirebaseAppName,
options: CoreFirebaseOptions(
apiKey: '123',
projectId: '123',
appId: '123',
messagingSenderId: '123',
),
pluginConstants: {},
),
];

@override
Future<CoreFirebaseOptions> optionsFromResource() async =>
CoreFirebaseOptions(
apiKey: '123',
projectId: '123',
appId: '123',
messagingSenderId: '123',
);
}