Skip to content
Merged
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
2 changes: 1 addition & 1 deletion pkgs/examples/generic_chat/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import 'package:collection/collection.dart';
import 'package:firebase_app_check/firebase_app_check.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_genui/flutter_genui.dart';

import 'firebase_options.dart';
import 'src/ai_client/ai_client.dart';
import 'src/chat_message.dart';
import 'src/core_catalog.dart';
import 'src/dynamic_ui.dart';
Expand Down
5 changes: 3 additions & 2 deletions pkgs/examples/generic_chat/lib/src/ui_server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import 'package:firebase_ai/firebase_ai.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

import 'ai_client/ai_client.dart';
import 'event_debouncer.dart';
import 'ui_models.dart';
import 'widget_tree_llm_adapter.dart';

import 'package:flutter_genui/flutter_genui.dart';

/// A callback to set the initial UI definition.
@visibleForTesting
typedef SetUiCallback = void Function(Map<String, Object?> definition);
Expand Down Expand Up @@ -257,7 +258,7 @@ Future<void> runUiServer({
widgetTreeLlmAdapter.outputSchema,
systemInstruction: Content.system(
'''You are a helpful assistant who figures out what the user wants to do and then helps suggest options so they can develop a plan and find relevant information.

The user will ask questions, and you will respond by generating appropriate UI elements. Typically, you will first elicit more information to understand the user's needs, then you will start displaying information and the user's plans.

For example, the user may say "I want to plan a trip to Mexico". You will first ask some questions by displaying a combination of UI elements, such as a slider to choose budget, options showing activity preferences etc. Then you will walk the user through choosing a hotel, flight and accomodation.
Expand Down
2 changes: 2 additions & 0 deletions pkgs/examples/generic_chat/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ dependencies:
firebase_core: ^3.2.0
flutter:
sdk: flutter
flutter_genui:
path: ../../flutter_genui
json_rpc_2: ^4.0.0
platform: ^3.1.6
stream_channel: ^2.1.4
Expand Down
2 changes: 1 addition & 1 deletion pkgs/flutter_genui/lib/README.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Files and folders, that are not exported, are prefixed with '_'.
Files and folders, that are not intended to be exported, are prefixed with '_'.
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,22 @@ import 'package:file/file.dart';
import 'package:file/local.dart';
import 'package:firebase_ai/firebase_ai.dart';

import '../tools/tools.dart';
import 'tools.dart';

/// Defines the severity levels for logging messages within the AI client and
/// related components.
typedef GenerativeModelFactory = GenerativeModel Function({
required AiClient configuration,
Content? systemInstruction,
List<Tool>? tools,
ToolConfig? toolConfig,
});
typedef GenerativeModelFactory =
GenerativeModel Function({
required AiClient configuration,
Content? systemInstruction,
List<Tool>? tools,
ToolConfig? toolConfig,
});

enum AiLoggingSeverity { trace, debug, info, warning, error, fatal }

typedef AiClientLoggingCallback = void Function(
AiLoggingSeverity severity, String message);
typedef AiClientLoggingCallback =
void Function(AiLoggingSeverity severity, String message);

class AiClientException implements Exception {
AiClientException(this.message);
Expand Down Expand Up @@ -212,12 +213,10 @@ class AiClient {
Iterable<AiTool> additionalTools = const [],
Content? systemInstruction,
}) async {
return await _generateContentWithRetries(
conversation,
outputSchema,
[...tools, ...additionalTools],
systemInstruction,
);
return await _generateContentWithRetries(conversation, outputSchema, [
...tools,
...additionalTools,
], systemInstruction);
}

/// The default factory function for creating a [GenerativeModel].
Expand All @@ -239,18 +238,24 @@ class AiClient {
}

void _error(String message, [StackTrace? stackTrace]) {
loggingCallback?.call(AiLoggingSeverity.error,
stackTrace != null ? '$message\n$stackTrace' : message);
loggingCallback?.call(
AiLoggingSeverity.error,
stackTrace != null ? '$message\n$stackTrace' : message,
);
}

void _warn(String message, [StackTrace? stackTrace]) {
loggingCallback?.call(AiLoggingSeverity.warning,
stackTrace != null ? '$message\n$stackTrace' : message);
loggingCallback?.call(
AiLoggingSeverity.warning,
stackTrace != null ? '$message\n$stackTrace' : message,
);
}

void _log(String message, [StackTrace? stackTrace]) {
loggingCallback?.call(AiLoggingSeverity.info,
stackTrace != null ? '$message\n$stackTrace' : message);
loggingCallback?.call(
AiLoggingSeverity.info,
stackTrace != null ? '$message\n$stackTrace' : message,
);
}

Future<T?> _generateContentWithRetries<T extends Object>(
Expand Down Expand Up @@ -294,18 +299,17 @@ class AiClient {
);
return result;
} on FirebaseAIException catch (exception) {
if (exception.message.contains(
'$model is not found for API version',
)) {
if (exception.message.contains('$model is not found for API version')) {
// If the model is not found, then just throw an exception.
throw AiClientException(exception.message);
}
await onFail(exception);
} catch (exception, stack) {
_error(
'Received '
'${exception.runtimeType}: $exception',
stack);
'Received '
'${exception.runtimeType}: $exception',
stack,
);
// For other exceptions, rethrow immediately.
rethrow;
}
Expand Down Expand Up @@ -333,9 +337,7 @@ class AiClient {
'MUST call this tool when you are done.',
// Wrap the outputSchema in an object so that the output schema isn't
// limited to objects.
parameters: Schema.object(
properties: {'output': outputSchema},
),
parameters: Schema.object(properties: {'output': outputSchema}),
invokeFunction: (args) async => args, // Invoke is a pass-through
);
// Ensure allAiTools doesn't have duplicates by name, and prioritize the
Expand All @@ -350,7 +352,8 @@ class AiClient {
if (tool.name != tool.fullName) {
if (toolFullNames.contains(tool.fullName)) {
throw AiClientException(
'Duplicate tool ${tool.fullName} registered.');
'Duplicate tool ${tool.fullName} registered.',
);
}
toolFullNames.add(tool.fullName);
}
Expand All @@ -361,10 +364,10 @@ class AiClient {
// are different.
final generativeAiTools = [
Tool.functionDeclarations(
[...uniqueAiToolsByName.values.map((t) => t.toFunctionDeclarations())]
.expand((e) => e)
.toList(),
)
[
...uniqueAiToolsByName.values.map((t) => t.toFunctionDeclarations()),
].expand((e) => e).toList(),
),
];
final allowedFunctionNames = <String>{
...uniqueAiToolsByName.keys,
Expand Down Expand Up @@ -414,8 +417,9 @@ class AiClient {
}

final candidate = response.candidates.first;
final functionCalls =
candidate.content.parts.whereType<FunctionCall>().toList();
final functionCalls = candidate.content.parts
.whereType<FunctionCall>()
.toList();

if (functionCalls.isEmpty) {
_warn(
Expand Down Expand Up @@ -466,11 +470,12 @@ class AiClient {
);
} catch (exception, stack) {
_error(
'Error invoking tool ${aiTool.name} with args ${call.args}: '
'$exception\n',
stack);
'Error invoking tool ${aiTool.name} with args ${call.args}: '
'$exception\n',
stack,
);
toolResult = {
'error': 'Tool ${aiTool.name} failed to execute: $exception'
'error': 'Tool ${aiTool.name} failed to execute: $exception',
};
}
functionResponseParts.add(FunctionResponse(call.name, toolResult));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,17 @@ abstract class AiTool<T extends Map<String, Object?>> {

List<FunctionDeclaration> toFunctionDeclarations() {
return [
FunctionDeclaration(name, description,
parameters: parameters == null ? {} : {'parameters': parameters!}),
FunctionDeclaration(
name,
description,
parameters: parameters == null ? {} : {'parameters': parameters!},
),
if (name != fullName)
FunctionDeclaration(fullName, description,
parameters: parameters == null ? {} : {'parameters': parameters!}),
FunctionDeclaration(
fullName,
description,
parameters: parameters == null ? {} : {'parameters': parameters!},
),
];
}
}
Expand Down
1 change: 1 addition & 0 deletions pkgs/flutter_genui/lib/flutter_genui.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export 'agent/agent.dart';
export 'model/controller.dart';
export 'model/image_catalog.dart';
export 'model/input.dart';
export '_ai_client/ai_client.dart';
9 changes: 9 additions & 0 deletions pkgs/flutter_genui/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,17 @@ environment:
flutter: ">=1.17.0"

dependencies:
collection: ^1.19.1
file: ^7.0.1
firebase_ai: ^2.2.1
firebase_app_check: ^0.3.0+1
firebase_auth: ^5.1.2
firebase_core: ^3.2.0
flutter:
sdk: flutter
json_rpc_2: ^4.0.0
platform: ^3.1.6
stream_channel: ^2.1.4

dev_dependencies:
flutter_test:
Expand Down