diff --git a/pkgs/examples/generic_chat/lib/main.dart b/pkgs/examples/generic_chat/lib/main.dart index 0298b3c7e..3b05fa2c5 100644 --- a/pkgs/examples/generic_chat/lib/main.dart +++ b/pkgs/examples/generic_chat/lib/main.dart @@ -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'; diff --git a/pkgs/examples/generic_chat/lib/src/ui_server.dart b/pkgs/examples/generic_chat/lib/src/ui_server.dart index 61004bb3e..572d741e8 100644 --- a/pkgs/examples/generic_chat/lib/src/ui_server.dart +++ b/pkgs/examples/generic_chat/lib/src/ui_server.dart @@ -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 definition); @@ -257,7 +258,7 @@ Future 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. diff --git a/pkgs/examples/generic_chat/pubspec.yaml b/pkgs/examples/generic_chat/pubspec.yaml index 6ae9297c7..a3d4222b1 100644 --- a/pkgs/examples/generic_chat/pubspec.yaml +++ b/pkgs/examples/generic_chat/pubspec.yaml @@ -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 diff --git a/pkgs/flutter_genui/lib/README.md b/pkgs/flutter_genui/lib/README.md index dc273a8d8..05bfdc4eb 100644 --- a/pkgs/flutter_genui/lib/README.md +++ b/pkgs/flutter_genui/lib/README.md @@ -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 '_'. diff --git a/pkgs/examples/generic_chat/lib/src/ai_client/ai_client.dart b/pkgs/flutter_genui/lib/_ai_client/ai_client.dart similarity index 91% rename from pkgs/examples/generic_chat/lib/src/ai_client/ai_client.dart rename to pkgs/flutter_genui/lib/_ai_client/ai_client.dart index 9414d9bbf..be54015bd 100644 --- a/pkgs/examples/generic_chat/lib/src/ai_client/ai_client.dart +++ b/pkgs/flutter_genui/lib/_ai_client/ai_client.dart @@ -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? tools, - ToolConfig? toolConfig, -}); +typedef GenerativeModelFactory = + GenerativeModel Function({ + required AiClient configuration, + Content? systemInstruction, + List? 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); @@ -212,12 +213,10 @@ class AiClient { Iterable 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]. @@ -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 _generateContentWithRetries( @@ -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; } @@ -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 @@ -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); } @@ -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 = { ...uniqueAiToolsByName.keys, @@ -414,8 +417,9 @@ class AiClient { } final candidate = response.candidates.first; - final functionCalls = - candidate.content.parts.whereType().toList(); + final functionCalls = candidate.content.parts + .whereType() + .toList(); if (functionCalls.isEmpty) { _warn( @@ -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)); diff --git a/pkgs/examples/generic_chat/lib/src/tools/tools.dart b/pkgs/flutter_genui/lib/_ai_client/tools.dart similarity index 95% rename from pkgs/examples/generic_chat/lib/src/tools/tools.dart rename to pkgs/flutter_genui/lib/_ai_client/tools.dart index 21bee30c2..5e44f9351 100644 --- a/pkgs/examples/generic_chat/lib/src/tools/tools.dart +++ b/pkgs/flutter_genui/lib/_ai_client/tools.dart @@ -82,11 +82,17 @@ abstract class AiTool> { List 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!}, + ), ]; } } diff --git a/pkgs/flutter_genui/lib/flutter_genui.dart b/pkgs/flutter_genui/lib/flutter_genui.dart index 24d0bd140..680f230aa 100644 --- a/pkgs/flutter_genui/lib/flutter_genui.dart +++ b/pkgs/flutter_genui/lib/flutter_genui.dart @@ -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'; diff --git a/pkgs/flutter_genui/pubspec.yaml b/pkgs/flutter_genui/pubspec.yaml index fc45cae74..95eb92b8c 100644 --- a/pkgs/flutter_genui/pubspec.yaml +++ b/pkgs/flutter_genui/pubspec.yaml @@ -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: