Skip to content

Conversation

jacobsimionato
Copy link
Collaborator

@jacobsimionato jacobsimionato commented Sep 3, 2025

This pull request significantly refactors the travel_app's AI communication layer. The core change involves migrating from the firebase_ai_client package to a direct integration with Firebase AI Logic, which is intended to simplify and improve the integration of AI tools. This update streamlines the application's interaction with generative AI models and addresses existing issues related to the generative UI framework.

Highlights

  • AI Client Migration: The application's AI communication layer has been migrated from using the firebase_ai_client package to directly interacting with Firebase AI Logic.
  • New GeminiClient: A new GeminiClient class has been introduced to manage AI model interactions, including tool declaration adaptation and content generation with tool usage cycles.
  • Conversation Model Refactoring: The conversation message types have been refactored into a new Turn hierarchy, providing a clearer and more robust way to manage different types of conversational turns.
  • Dependency Injection Removal: The aiClient dependency injection has been removed from TravelApp and TravelPlannerPage, simplifying their initialization.
Changelog
  • examples/travel_app/lib/main.dart
    • Updated imports to use firebase_ai directly and introduced new GeminiClient and Turn types.
    • Removed the aiClient parameter from TravelApp and TravelPlannerPage constructors, simplifying their initialization.
    • Refactored the AI interaction logic to use the new GeminiClient and the Turn conversation model.
    • Added a new logging configuration function (_configureLogging).
  • examples/travel_app/lib/src/catalog/text_input_chip.dart
    • Clarified the description of the _schema for TextInputChip to emphasize its usage within an InputGroup.
  • examples/travel_app/lib/src/gemini_client.dart
    • New file: Implements the GeminiClient class, responsible for managing AI model interactions, including tool declaration adaptation and content generation with tool usage cycles.
  • examples/travel_app/lib/src/turn.dart
    • New file: Defines a sealed class Turn and its subclasses (UserTurn, UserUiInteractionTurn, AiTextTurn, GenUiTurn) to represent different types of conversational turns, replacing the previous ChatMessage hierarchy.
  • examples/travel_app/lib/src/widgets/conversation.dart
    • Adapted to use the new Turn hierarchy for displaying conversation messages.
    • Simplified rendering logic by using Dart's switch expression on Turn types.
    • Removed previously supported userPromptBuilder and showInternalMessages options.
  • examples/travel_app/test/main_test.dart
    • Updated tests to reflect the removal of the aiClient parameter from TravelApp's constructor.
  • examples/travel_app/test/widgets/conversation_test.dart
    • Updated tests to use the new Turn types (UserTurn, GenUiTurn) for conversation messages.
    • Removed a test case related to userPromptBuilder.
  • packages/flutter_genui/lib/flutter_genui.dart
    • Updated exports to include GeminiSchemaAdapter and tools.dart, and removed the export for the moved chat_primitives.dart.
  • packages/flutter_genui/lib/src/core/widgets/chat_primitives.dart
    • File moved/renamed: The ChatMessageWidget implementation was moved from the flutter_genui package to the travel_app example, now located at examples/travel_app/lib/src/widgets/chat_message.dart.

This is intended to help fix #40 and then #134

@jacobsimionato
Copy link
Collaborator Author

/gemini summarize

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request refactors the travel app to directly use the Firebase AI SDK instead of the firebase_ai_client, introducing a new GeminiClient and replacing ChatMessage with a Turn sealed class. The changes are a good step towards a cleaner architecture. However, I've identified a critical issue with testability due to the removal of dependency injection, as well as a couple of high-severity issues related to unsafe list access that could lead to runtime errors. I've also included some medium-severity suggestions for improving code quality and maintainability.

Comment on lines 80 to +81
/// Creates a new [TravelPlannerPage].
///
/// An optional [aiClient] can be provided, which is useful for testing
/// or using a custom AI client implementation. If not provided, a default
/// [FirebaseAiClient] is created.
const TravelPlannerPage({this.aiClient, super.key});

/// The AI client to use for the application.
///
/// If null, a default instance of [FirebaseAiClient] will be created within
/// the page's state.
final AiClient? aiClient;
const TravelPlannerPage({super.key});

Choose a reason for hiding this comment

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

critical

The removal of dependency injection for the AI client has made TravelPlannerPage difficult to test and has broken existing unit tests in main_test.dart. The page now creates its own GeminiClient instance directly, preventing the injection of a mock client during tests. This forces tests to either run against the actual Firebase backend (making them integration tests) or fail.

To maintain testability, please consider re-introducing dependency injection. You could pass a GeminiClient instance to the TravelPlannerPage widget, which can be provided from TravelApp. This is crucial for effective unit testing.

.whereType<Content>()
.toList();
final result = await _geminiClient.generate(contentHistory);
final value = result.candidates.first.content.parts

Choose a reason for hiding this comment

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

high

Accessing result.candidates.first without checking if the list is empty can lead to a StateError if the model returns no candidates (e.g., due to safety filters). This would be caught by the try/finally, but the user would see the thinking indicator disappear without any feedback. It's safer to check if the candidates list is empty before accessing its first element.


final functionResponses = <fai.FunctionResponse>[];
for (final call in functionCalls) {
final tool = tools.firstWhere((t) => t.name == call.name);

Choose a reason for hiding this comment

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

high

Using firstWhere without an orElse clause can lead to a StateError if the AI model hallucinates a function call to a tool that doesn't exist in the tools list. This will interrupt the inference flow and result in a poor user experience as the app will just stop responding without an error.

A safer approach would be to use a method that doesn't throw, like indexWhere followed by a check for -1, or package:collection's firstWhereOrNull. If a tool is not found, you could log a warning and add a FunctionResponse to the history informing the model about the error, allowing it to potentially recover.

Comment on lines +35 to +45
void _configureLogging() {
hierarchicalLoggingEnabled = true;
Logger.root.level = Level.ALL;
Logger.root.onRecord.listen((record) {
// ignore: avoid_print
print(
'[${record.level.name}] ${record.time}: '
'${record.loggerName}: ${record.message}',
);
});
}

Choose a reason for hiding this comment

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

medium

The _configureLogging function uses print to output log records. The // ignore: avoid_print indicates you're aware of the lint. For better practice, consider using log from dart:developer (you'll need to import it as e.g. 'developer'). It's automatically stripped from release builds and integrates well with developer tools.

Suggested change
void _configureLogging() {
hierarchicalLoggingEnabled = true;
Logger.root.level = Level.ALL;
Logger.root.onRecord.listen((record) {
// ignore: avoid_print
print(
'[${record.level.name}] ${record.time}: '
'${record.loggerName}: ${record.message}',
);
});
}
void _configureLogging() {
hierarchicalLoggingEnabled = true;
Logger.root.level = Level.ALL;
Logger.root.onRecord.listen((record) {
developer.log(
record.message,
time: record.time,
level: record.level.value,
name: record.loggerName,
error: record.error,
stackTrace: record.stackTrace,
);
});
}

@@ -386,6 +368,7 @@ to the user.
3. Create an initial itinerary, which will be iterated over in subsequent
steps. This involves planning out each day of the trip, including the
specific locations and draft activities. For shorter trips where the

Choose a reason for hiding this comment

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

medium

An extra newline has been added here, which seems unintentional. While it may not have a significant impact, it's best to keep prompts clean from accidental formatting changes as they can sometimes subtly affect model behavior.

Comment on lines +14 to +34
class UserTurn extends Turn {
final String text;

const UserTurn(this.text);

@override
firebase_ai.Content toContent() {
return firebase_ai.Content('user', [firebase_ai.TextPart(text)]);
}
}

class UserUiInteractionTurn extends Turn {
final String text;

const UserUiInteractionTurn(this.text);

@override
firebase_ai.Content toContent() {
return firebase_ai.Content('user', [firebase_ai.TextPart(text)]);
}
}

Choose a reason for hiding this comment

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

medium

The classes UserTurn and UserUiInteractionTurn are structurally identical. To avoid code duplication and improve maintainability, consider creating a common abstract base class for them that contains the shared text field and toContent implementation. The distinct concrete classes are still useful for how they are handled differently in the UI.

Example:

abstract class _UserTextTurn extends Turn {
  final String text;
  const _UserTextTurn(this.text);

  @override
  firebase_ai.Content toContent() {
    return firebase_ai.Content('user', [firebase_ai.TextPart(text)]);
  }
}

class UserTurn extends _UserTextTurn {
  const UserTurn(super.text);
}

class UserUiInteractionTurn extends _UserTextTurn {
  const UserUiInteractionTurn(super.text);
}

Copy link

Summary of Changes

This pull request significantly refactors the travel_app's AI communication layer. The core change involves migrating from the firebase_ai_client package to a direct integration with Firebase AI Logic, which is intended to simplify and improve the integration of AI tools. This update streamlines the application's interaction with generative AI models and addresses existing issues related to the generative UI framework.

Highlights

  • AI Client Migration: The application's AI communication layer has been migrated from using the firebase_ai_client package to directly interacting with Firebase AI Logic.
  • New GeminiClient: A new GeminiClient class has been introduced to manage AI model interactions, including tool declaration adaptation and content generation with tool usage cycles.
  • Conversation Model Refactoring: The conversation message types have been refactored into a new Turn hierarchy, providing a clearer and more robust way to manage different types of conversational turns.
  • Dependency Injection Removal: The aiClient dependency injection has been removed from TravelApp and TravelPlannerPage, simplifying their initialization.
Changelog
  • examples/travel_app/lib/main.dart
    • Updated imports to use firebase_ai directly and introduced new GeminiClient and Turn types.
    • Removed the aiClient parameter from TravelApp and TravelPlannerPage constructors, simplifying their initialization.
    • Refactored the AI interaction logic to use the new GeminiClient and the Turn conversation model.
    • Added a new logging configuration function (_configureLogging).
  • examples/travel_app/lib/src/catalog/text_input_chip.dart
    • Clarified the description of the _schema for TextInputChip to emphasize its usage within an InputGroup.
  • examples/travel_app/lib/src/gemini_client.dart
    • New file: Implements the GeminiClient class, responsible for managing AI model interactions, including tool declaration adaptation and content generation with tool usage cycles.
  • examples/travel_app/lib/src/turn.dart
    • New file: Defines a sealed class Turn and its subclasses (UserTurn, UserUiInteractionTurn, AiTextTurn, GenUiTurn) to represent different types of conversational turns, replacing the previous ChatMessage hierarchy.
  • examples/travel_app/lib/src/widgets/conversation.dart
    • Adapted to use the new Turn hierarchy for displaying conversation messages.
    • Simplified rendering logic by using Dart's switch expression on Turn types.
    • Removed previously supported userPromptBuilder and showInternalMessages options.
  • examples/travel_app/test/main_test.dart
    • Updated tests to reflect the removal of the aiClient parameter from TravelApp's constructor.
  • examples/travel_app/test/widgets/conversation_test.dart
    • Updated tests to use the new Turn types (UserTurn, GenUiTurn) for conversation messages.
    • Removed a test case related to userPromptBuilder.
  • packages/flutter_genui/lib/flutter_genui.dart
    • Updated exports to include GeminiSchemaAdapter and tools.dart, and removed the export for the moved chat_primitives.dart.
  • packages/flutter_genui/lib/src/core/widgets/chat_primitives.dart
    • File moved/renamed: The ChatMessageWidget implementation was moved from the flutter_genui package to the travel_app example, now located at examples/travel_app/lib/src/widgets/chat_message.dart.
Activity
  • The author, jacobsimionato, initiated the summary generation.
  • gemini-code-assist[bot] provided several review comments:
    • Critical issue: The removal of dependency injection for the AI client in TravelPlannerPage has broken unit tests and made the component difficult to test.
    • High priority issue: The code accesses result.candidates.first without checking if the list is empty, which could lead to a StateError if no candidates are returned.
    • High priority issue: The use of firstWhere without an orElse clause in GeminiClient could cause a StateError if a non-existent tool is called by the AI model.
    • Medium priority issue: The _configureLogging function uses print instead of dart:developer.log for logging, which is less ideal for production builds.
    • Medium priority issue: An unintentional extra newline was introduced in the prompt, which might subtly affect model behavior.
    • Medium priority issue: Suggested refactoring UserTurn and UserUiInteractionTurn to use a common abstract base class to reduce code duplication.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Demonstrate use of Gen UI alongside other tools which provide a real source of truth
1 participant