Skip to content

Conversation

paulb777
Copy link
Member

@paulb777 paulb777 commented Oct 9, 2025

Apple implementation of Server Prompt Templates inspired by Flutterfire PR.
Googlers: see API review at go/firebase-ai-server-prompt-template

Summary of Changes

This pull request significantly enhances the Firebase AI SDK by introducing support for server-side prompt templates. This allows developers to define and use reusable templates for generative AI models, making it easier to manage complex prompts and integrate dynamic data. The changes include new model types for generative and image AI, a dedicated chat session for template-based conversations, and a refactored, thread-safe history management system. These additions provide a more robust and flexible framework for building AI-powered applications.

Highlights

  • Server-Side Prompt Templates: Introduced new TemplateGenerativeModel and TemplateImagenModel classes to support server-side prompt templates, allowing for more flexible and dynamic AI interactions.
  • Template Chat Sessions: Added TemplateChatSession to enable conversational interactions with generative models using predefined templates and dynamic variables, supporting both single messages and streaming responses.
  • Refactored Chat History Management: Extracted chat history logic into a new, dedicated History class, improving modularity, reusability, and thread safety for managing conversation history in both standard and template-based chat sessions.
  • Dynamic Template Variables: Implemented TemplateVariable enum to handle various data types (String, Int, Double, Bool, Array, Dictionary) for substituting values into prompt templates, including automatic conversion for Float to Double.
  • New API Request Structures: Created specific request types (TemplateGenerateContentRequest, TemplateGenerateImagesRequest) and internal API method enums (APIMethod, ImageAPIMethod) to support the new template-based API endpoints.
  • Comprehensive Testing: Added extensive integration and unit tests for the new template models and chat sessions, covering various scenarios including text generation, image generation, and chat history management.
Changelog
  • FirebaseAI/Sources/Chat.swift
    • Refactored to use the new History class for managing chat history, removing internal history management logic and related locks.
    • Updated appendHistory calls to use the _history instance of the new History class.
  • FirebaseAI/Sources/FirebaseAI.swift
    • Added public factory methods templateGenerativeModel() and templateImagenModel() to initialize new template-based AI models.
  • FirebaseAI/Sources/GenerateContentRequest.swift
    • Removed the APIMethod enum, which has been moved to a new, dedicated internal file.
  • FirebaseAI/Sources/GenerativeAIService.swift
    • Changed the firebaseInfo property from private let to let to allow access from newly introduced template models.
  • FirebaseAI/Sources/History.swift
    • Added new file: Introduces a History class to encapsulate chat history management, including thread-safe appending of ModelContent and aggregation of content chunks.
  • FirebaseAI/Sources/TemplateChatSession.swift
    • Added new file: Defines TemplateChatSession for managing chat conversations with server-side prompt templates, providing sendMessage and sendMessageStream methods.
  • FirebaseAI/Sources/TemplateGenerateContentRequest.swift
    • Added new file: Defines the request structure for generating content using prompt templates, including template, variables, history, and project ID.
  • FirebaseAI/Sources/TemplateGenerateImagesRequest.swift
    • Added new file: Defines the request structure for generating images using prompt templates, including template, variables, and project ID.
  • FirebaseAI/Sources/TemplateGenerativeModel.swift
    • Added new file: Introduces TemplateGenerativeModel for interacting with generative AI models using templates, including methods for content generation and starting chat sessions.
  • FirebaseAI/Sources/TemplateImagenModel.swift
    • Added new file: Introduces TemplateImagenModel for interacting with image generation models using templates.
  • FirebaseAI/Sources/TemplateVariable.swift
    • Added new file: Defines TemplateVariable enum to handle various data types for template variables, including a conversion from Float to Double during initialization.
  • FirebaseAI/Sources/Types/Internal/APIMethod.swift
    • Added new file: Moved the APIMethod enum (generateContent, streamGenerateContent, countTokens) here from GenerateContentRequest.swift.
  • FirebaseAI/Sources/Types/Internal/ImageAPIMethod.swift
    • Added new file: Defines ImageAPIMethod enum specifically for image generation API methods, such as templatePredict.
  • FirebaseAI/Tests/TestApp/Tests/Integration/ServerPromptTemplateIntegrationTests.swift
    • Added new file: Contains integration tests for TemplateGenerativeModel and TemplateImagenModel, covering text generation, image generation, and chat sessions with templates and media.
  • FirebaseAI/Tests/Unit/TemplateChatSessionTests.swift
    • Added new file: Provides unit tests for the TemplateChatSession class, verifying sendMessage and sendMessageStream functionality.
  • FirebaseAI/Tests/Unit/TemplateGenerativeModelTests.swift
    • Added new file: Includes unit tests for the TemplateGenerativeModel class, testing generateContent and generateContentStream methods.
  • FirebaseAI/Tests/Unit/TemplateImagenModelTests.swift
    • Added new file: Contains unit tests for the TemplateImagenModel class, specifically testing the generateImages method.
  • FirebaseAI/Tests/Unit/TestUtilities/GenerativeModelTestUtil.swift
    • Modified httpRequestHandler to include an isTemplateRequest parameter, allowing for conditional URL path assertions to support template-based requests in mock tests.
    • Added a new helper function collectTextFromStream to simplify collecting text from AsyncThrowingStream responses in tests.
Activity
  • The Danger bot initially flagged a missing changelog entry.
  • A review comment from andrewheard suggested extracting chat history management into a separate type, which was subsequently implemented by paulb777.
  • Multiple automated review comments from gemini-code-assist[bot] highlighted potential issues with force unwrapping URLs in new request types and suggested percent-encoding template names for robustness. paulb777 acknowledged these, noting consistency with existing code.
  • A suggestion from gemini-code-assist[bot] to handle Float values in TemplateVariable initialization was addressed and implemented in the changes.
  • Other low-priority comments from gemini-code-assist[bot] included a TODO for streaming and a hardcoded path in a test utility.
@google-oss-bot
Copy link

1 Warning
⚠️ Did you forget to add a changelog entry? (Add #no-changelog to the PR description to silence this warning.)

Generated by 🚫 Danger


/// A chat session that allows for conversation with a model.
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
public class TemplateChatSession {
Copy link
Contributor

Choose a reason for hiding this comment

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

Just a quick thing I noticed: public final class TemplateChatSession: Sendable. This would likely require some refactoring with the history. I'm wondering if we should extract

private let historyLock = NSLock()
private nonisolated(unsafe) var _history: [ModelContent] = []
/// The previous content from the chat that has been successfully sent and received from the
/// model. This will be provided to the model for each message sent as context for the discussion.
public var history: [ModelContent] {
get {
historyLock.withLock { _history }
}
set {
historyLock.withLock { _history = newValue }
}
}
private func appendHistory(contentsOf: [ModelContent]) {
historyLock.withLock {
_history.append(contentsOf: contentsOf)
}
}
private func appendHistory(_ newElement: ModelContent) {
historyLock.withLock {
_history.append(newElement)
}
}
into a type and use it for both?

Copy link
Member Author

Choose a reason for hiding this comment

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

Done in 004fb94

@paulb777
Copy link
Member Author

/gemini review

Copy link
Contributor

@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 introduces server-side prompt templates, a significant feature addition. The changes include new models (TemplateGenerativeModel, TemplateImagenModel), request types, and chat session logic to support using templates with variables. The code is well-structured, with good separation of concerns, such as moving history management into its own History class. I've identified a couple of areas where force unwraps on URL creation could be made safer. Overall, this is a solid implementation with good test coverage.

}
let templateName = template.hasSuffix(".prompt") ? template : "\(template).prompt"
urlString += "/templates/\(templateName):\(ImageAPIMethod.generateImages.rawValue)"
return URL(string: urlString)!
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

Using a force unwrap ! for URL creation can lead to a runtime crash if the URL string is malformed for any reason. It's safer to use guard let to safely unwrap the optional URL. This makes the code more robust and easier to debug by providing a more descriptive error message if the URL construction fails.

 guard let url = URL(string: urlString) else { fatalError("Could not construct URL for GenerateImagesRequest. URL string was: \(urlString)") } return url
Copy link
Member Author

Choose a reason for hiding this comment

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

Don't want a fatal error and the force unwrap is consistent with GenerateContentRequest

// TODO: Fix this.
urlString += "?alt=sse"
}
return URL(string: urlString)!
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

Using a force unwrap ! for URL creation can lead to a runtime crash if the URL string is malformed. It's safer to use guard let to safely unwrap the optional URL and provide a descriptive error message if it fails. This improves code robustness and debuggability.

 guard let url = URL(string: urlString) else { fatalError("Could not construct URL for TemplateGenerateContentRequest. URL string was: \(urlString)") } return url
@paulb777
Copy link
Member Author

/gemini review

Copy link
Contributor

@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 introduces support for server-side prompt templates for both generative and image generation models. This is a significant feature addition, including new model types like TemplateGenerativeModel and TemplateImagenModel, new request structures, and a TemplateChatSession for conversational AI with templates. The changes also involve some nice refactoring, such as abstracting history management into a dedicated History class. The overall implementation is well-structured and comes with a good set of unit and integration tests. My feedback is focused on improving robustness by avoiding force-unwraps in URL construction, ensuring proper URL encoding of user-provided data, and making the TemplateVariable type more versatile by handling Float values. I've also included a minor suggestion to clean up test code.

Comment on lines 22 to 31
public var url: URL {
var urlString =
"\(apiConfig.service.endpoint.rawValue)/\(apiConfig.version.rawValue)/projects/\(projectID)"
if case let .vertexAI(_, location) = apiConfig.service {
urlString += "/locations/\(location)"
}
let templateName = template.hasSuffix(".prompt") ? template : "\(template).prompt"
urlString += "/templates/\(templateName):\(ImageAPIMethod.generateImages.rawValue)"
return URL(string: urlString)!
}
Copy link
Contributor

Choose a reason for hiding this comment

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

high

The url computed property uses force unwrapping (!) to create a URL object. This can lead to a runtime crash if urlString is not a valid URL. The template parameter, which is part of the URL, comes from user input and is not being percent-encoded. If it contains characters that are not allowed in a URL path, URL(string: urlString) will return nil, causing a crash.

It's safer to percent-encode the template name and handle the potential failure of URL creation gracefully instead of force-unwrapping.

Suggested change
public var url: URL {
var urlString =
"\(apiConfig.service.endpoint.rawValue)/\(apiConfig.version.rawValue)/projects/\(projectID)"
if case let .vertexAI(_, location) = apiConfig.service {
urlString += "/locations/\(location)"
}
let templateName = template.hasSuffix(".prompt") ? template : "\(template).prompt"
urlString += "/templates/\(templateName):\(ImageAPIMethod.generateImages.rawValue)"
return URL(string: urlString)!
}
public var url: URL {
var urlString =
"\(apiConfig.service.endpoint.rawValue)/\(apiConfig.version.rawValue)/projects/\(projectID)"
if case let .vertexAI(_, location) = apiConfig.service {
urlString += "/locations/\(location)"
}
let templateName = template.hasSuffix(".prompt") ? template : "\(template).prompt"
guard let encodedTemplateName = templateName.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else {
fatalError("Failed to percent-encode template name: \(templateName)")
}
urlString += "/templates/\(encodedTemplateName):\(ImageAPIMethod.generateImages.rawValue)"
guard let url = URL(string: urlString) else {
fatalError("Could not construct a valid URL from string: \(urlString). This is a bug.")
}
return url
}
Comment on lines 46 to 57
var url: URL {
var urlString =
"\(apiConfig.service.endpoint.rawValue)/\(apiConfig.version.rawValue)/projects/\(projectID)"
if case let .vertexAI(_, location) = apiConfig.service {
urlString += "/locations/\(location)"
}
let templateName = template.hasSuffix(".prompt") ? template : "\(template).prompt"
urlString += "/templates/\(templateName):templateGenerateContent"
if stream {
urlString += "?alt=sse"
}
return URL(string: urlString)!
}
Copy link
Contributor

Choose a reason for hiding this comment

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

high

The url computed property uses force unwrapping (!) which can cause a crash if the URL string is invalid. The template name is part of the URL and comes from user input, but it's not percent-encoded. This could lead to an invalid URL if the template name contains special characters.

Please percent-encode the template name and avoid force-unwrapping to make the URL creation more robust.

 var url: URL { var urlString = "\(apiConfig.service.endpoint.rawValue)/\(apiConfig.version.rawValue)/projects/\(projectID)" if case let .vertexAI(_, location) = apiConfig.service { urlString += "/locations/\(location)" } let templateName = template.hasSuffix(".prompt") ? template : "\(template).prompt" guard let encodedTemplateName = templateName.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else { fatalError("Failed to percent-encode template name: \(templateName)") } urlString += "/templates/\(encodedTemplateName):templateGenerateContent" if stream { urlString += "?alt=sse" } guard let url = URL(string: urlString) else { fatalError("Could not construct a valid URL from string: \(urlString). This is a bug.") } return url }
@paulb777
Copy link
Member Author

/gemini summarize

@firebase firebase deleted a comment from gemini-code-assist bot Oct 14, 2025
@paulb777 paulb777 marked this pull request as ready for review October 14, 2025 23:13
@paulb777
Copy link
Member Author

@andrewheard @cynthiajoan Still some test tweaks once the backend supports both API providers with the same templates and this won't merge until after M172, but should now be ready for review.

@paulb777 paulb777 added this to the 12.6.0 - M173 milestone Oct 14, 2025
paulb777 and others added 16 commits October 16, 2025 15:27
Updated `GenerateImagesRequest` to encode the `variables` property as `inputs`, similar to how `TemplateGenerateContentRequest` handles it. This ensures consistency across the API.
Updated `GenerateImagesRequest` to correctly construct the URL, including the `projectID` and `location`. This mirrors the URL construction in `TemplateGenerateContentRequest` and fixes a 404 error that was occurring in the `testGenerateImages` integration test. The `TemplateImagenModel` was also updated to pass the `projectID` to the `GenerateImagesRequest` initializer.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

3 participants