diff --git a/ai/ai-samples/README.md b/ai/ai-samples/README.md index 5d8f9e8c8..aa3c6f268 100644 --- a/ai/ai-samples/README.md +++ b/ai/ai-samples/README.md @@ -16,6 +16,7 @@ This repository demonstrates the following capabilities: * Image Generation * Video Analysis * Grounding with Google Search +* Server Prompt Templates ## Setup & Configuration @@ -51,6 +52,9 @@ npm run dev:auto-function # Automatic Function Calling npm run dev:image # Image Generation npm run dev:video # Video Analysis npm run dev:grounding # Grounding with Google Search +# Note: Server Prompt Templates requires template setup in the Firebase Console first. +# See src/features/server-prompt-templates/README.md for setup instructions before running: +npm run dev:template # Server Prompt Templates ``` After running any of the above commands, open your browser to http://localhost:*** (provided in the console) diff --git a/ai/ai-samples/package.json b/ai/ai-samples/package.json index 5b19c3fa6..a64534329 100644 --- a/ai/ai-samples/package.json +++ b/ai/ai-samples/package.json @@ -13,7 +13,8 @@ "dev:auto-function": "VITE_ISOLATED_FEATURE=automatic-function-calling vite", "dev:image": "VITE_ISOLATED_FEATURE=image-generation vite", "dev:video": "VITE_ISOLATED_FEATURE=video-analysis vite", - "dev:grounding": "VITE_ISOLATED_FEATURE=grounding-with-google-search vite", + "dev:grounding": "VITE_ISOLATED_FEATURE=grounding-with-google-search vite", + "dev:template": "VITE_ISOLATED_FEATURE=server-prompt-templates vite", "build": "tsc -b && vite build", "lint": "eslint .", "preview": "vite preview" diff --git a/ai/ai-samples/src/App.tsx b/ai/ai-samples/src/App.tsx index 9161c259d..8922487c0 100644 --- a/ai/ai-samples/src/App.tsx +++ b/ai/ai-samples/src/App.tsx @@ -10,6 +10,7 @@ const NAV_ITEMS = [ { path: '/image-generation', label: 'Image Generation' }, { path: '/video-analysis', label: 'Video Analysis' }, { path: '/grounding-with-google-search', label: 'Grounding with Google Search' }, + { path: '/server-prompt-templates', label: 'Server Prompt Templates' }, ]; export default function App() { diff --git a/ai/ai-samples/src/features/server-prompt-templates/README.md b/ai/ai-samples/src/features/server-prompt-templates/README.md new file mode 100644 index 000000000..42eafc7dc --- /dev/null +++ b/ai/ai-samples/src/features/server-prompt-templates/README.md @@ -0,0 +1,57 @@ +# Firebase AI Logic: Server Prompt Templates Sample + +This quickstart demonstrates how to fetch and execute centrally managed prompt templates stored in the Firebase Console using the Firebase AI Logic SDK. + +## Why Server Prompt Templates? + +- **No deployment updates**: Change prompts, tune system instructions, or switch underlying models directly in the Firebase Console without having to release a new app version. +- **Security**: Protect against exposing your prompt client-side. + +--- + +## 1. Firebase Console Setup + +Before running this sample, create and lock the template in your Firebase project: + +1. Open the [Firebase Console](https://console.firebase.google.com/) and select your project. +2. In the left navigation, navigate to **AI Logic > Prompt Templates**. +3. Click **Create Template**. Note that these starter templates provide the format and syntax for some common use cases and this tutorial assumes you've selected the `Input + System Instructions` option. Once you clicked that, configure the following: + - **Template ID**: `invoice-generator` + - **Model**: `gemini-3.5-flash-lite` (or any available Gemini model) +4. In the **Prompt Content** / **System Instructions** text area, paste the following prompt: + ```text + {{role "system"}} + All output must be a clearly structured invoice document. + Use a tabular or clearly delineated list format for line items. + + {{role "user"}} + Create an example customer invoice for a customer named {{customerName}}. + ``` +5. Click **Save**. +6. **Important**: While client applications can execute both unlocked (draft) and locked templates during development, you should always **Lock** your template before deploying to production. Locking freezes the prompt configuration, ensuring that subsequent console edits do not accidentally change your production app's behavior. + +--- + +## 2. Running the Sample Locally + +Run the sample inside the full app shell: +```bash +npm run dev +``` + +Or run this feature directly in isolated mode: +```bash +npm run dev:template +``` + +Open your browser to the local URL (e.g., `http://localhost:***` provided in the console). + +--- + +## 3. Possible Troubleshooting & Failure Surfaces + +| Issue | Cause | Resolution | +|---|---|---| +| `NOT_FOUND` / 404 | Template ID mismatch or template doesn't exist in the active Firebase project | Verify that the template ID in your code is exactly `invoice-generator` and that you are initialized in the correct Firebase Project | +| Missing Variable output | Variable names in client code don't match console | Ensure the keys passed to `templateVariables` match the `{{variable}}` placeholders in your prompt template. | +| `PERMISSION_DENIED` | Firebase AI Logic API not enabled or App Check blocked | Follow the Firebase AI Logic guided setup in the console and ensure your API key / App Check tokens are valid. | \ No newline at end of file diff --git a/ai/ai-samples/src/features/server-prompt-templates/index.tsx b/ai/ai-samples/src/features/server-prompt-templates/index.tsx new file mode 100644 index 000000000..2b014ca0e --- /dev/null +++ b/ai/ai-samples/src/features/server-prompt-templates/index.tsx @@ -0,0 +1,148 @@ +import { useState } from 'react'; +import { generateFromTemplate } from './service'; + +export default function ServerPromptTemplatesView() { + const [templateId, setTemplateId] = useState('invoice-generator'); + const [customerName, setCustomerName] = useState('Jane Doe'); + + const [response, setResponse] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const handleExecute = async () => { + const trimmedId = templateId.trim(); + if (!trimmedId) { + setError('Please enter a Template ID.'); + return; + } + + const trimmedCustomerName = customerName.trim(); + if (!trimmedCustomerName) { + setError('Please enter a Customer Name.'); + return; + } + + setLoading(true); + setError(null); + setResponse(''); + + const templateVariables = { + customerName: trimmedCustomerName, + }; + + try { + const text = await generateFromTemplate(trimmedId, templateVariables); + setResponse(text); + } catch (err: unknown) { + const message = + err instanceof Error ? err.message : 'An error occurred while executing the template.'; + setError(message); + } finally { + setLoading(false); + } + }; + + const isNotFoundError = + error && (error.toLowerCase().includes('not found') || error.toLowerCase().includes('not_found')); + + return ( +
+

Server Prompt Templates

+

+ Centrally manage, test, and update AI prompts in the Firebase Console without redeploying client code. +

+ + {/* Prerequisite Setup Notice */} +
+ Console Prerequisite: Requires a published/locked template in the Firebase Console. + Console Prerequisite: Requires a saved template in the Firebase Console. + See README.md in this feature folder for the setup guide. +
+ +
+ + setTemplateId(e.target.value)} + placeholder="invoice-generator" + style={{ width: '100%', padding: '8px', boxSizing: 'border-box' }} + /> +
+ +
+ + setCustomerName(e.target.value)} + style={{ width: '100%', padding: '8px', boxSizing: 'border-box' }} + /> +
+ + + + {error && ( +
+ Error: {error} + {isNotFoundError && ( +
+ Setup Checklist: +
    +
  • Template {templateId} exists in your Firebase project?
  • +
  • Template status is Locked / Published (drafts cannot be called by client SDKs)?
  • +
  • Template is saved in the Firebase Console (and locked for production)?
  • +
  • Your web app configuration matches the Firebase project where the template is stored?
  • +
+
+ )} +
+ )} + + {response && ( +
+

Response:

+

{response}

+
+ )} +
+ ); +} diff --git a/ai/ai-samples/src/features/server-prompt-templates/service.ts b/ai/ai-samples/src/features/server-prompt-templates/service.ts new file mode 100644 index 000000000..9b25aae0d --- /dev/null +++ b/ai/ai-samples/src/features/server-prompt-templates/service.ts @@ -0,0 +1,28 @@ +import { getAiTemplateModel } from '../../services/firebaseAIService'; + +export interface InvoiceTemplateVariables { + customerName: string; +} + +/** + * Executes a server-side prompt template in a single unary request. + * Centrally managed templates allow updating prompts and models without redeploying client code. + * + * @param templateId The ID of the locked template in the Firebase Console (e.g. 'invoice-generator') + * @param variables Key-value map matching the {{variables}} declared in the template + * @returns The text string generated by the model + */ +export async function generateFromTemplate( + templateId: string, + variables: Record +): Promise { + try { + const model = getAiTemplateModel(); + const result = await model.generateContent(templateId, variables); + return result.response.text(); + } catch (error: unknown) { + throw error instanceof Error + ? error + : new Error('An unknown error occurred during template generation.'); + } +} diff --git a/ai/ai-samples/src/index.tsx b/ai/ai-samples/src/index.tsx index 7cffbd39e..040e45c96 100644 --- a/ai/ai-samples/src/index.tsx +++ b/ai/ai-samples/src/index.tsx @@ -11,6 +11,7 @@ import ImageGeneration from './features/image-generation'; import AutomaticFunctionCalling from './features/automatic-function-calling'; import VideoAnalysis from './features/video-analysis'; import GroundingWithGoogleSearch from './features/grounding-with-google-search'; +import ServerPromptTemplates from './features/server-prompt-templates'; const router = createBrowserRouter([ { @@ -27,9 +28,9 @@ const router = createBrowserRouter([ { path: 'image-generation', element: }, { path: 'video-analysis', element: }, { path: 'grounding-with-google-search', element: }, + { path: 'server-prompt-templates', element: }, ], }, - ]); const isolatedFeature = import.meta.env.VITE_ISOLATED_FEATURE; @@ -55,6 +56,9 @@ const renderContent = () => { return ; case 'grounding-with-google-search': return ; + case 'server-prompt-templates': + case 'template': + return ; default: return ; } diff --git a/ai/ai-samples/src/services/firebaseAIService.ts b/ai/ai-samples/src/services/firebaseAIService.ts index ec9ededeb..72431f370 100644 --- a/ai/ai-samples/src/services/firebaseAIService.ts +++ b/ai/ai-samples/src/services/firebaseAIService.ts @@ -1,17 +1,17 @@ -import { initializeApp} from 'firebase/app'; -import { getAI, getGenerativeModel } from 'firebase/ai'; +import { initializeApp } from 'firebase/app'; +import { getAI, getGenerativeModel, getTemplateGenerativeModel, RequestOptions } from 'firebase/ai'; import { initializeAppCheck, ReCaptchaEnterpriseProvider } from 'firebase/app-check'; -const firebaseConfig = import.meta.env.VITE_FIREBASE_CONFIG - ? JSON.parse(import.meta.env.VITE_FIREBASE_CONFIG) +const firebaseConfig = import.meta.env.VITE_FIREBASE_CONFIG + ? JSON.parse(import.meta.env.VITE_FIREBASE_CONFIG) : { - apiKey: "YOUR_API_KEY", - authDomain: "YOUR_AUTH_DOMAIN", - projectId: "YOUR_PROJECT_ID", - storageBucket: "YOUR_STORAGE_BUCKET", - messagingSenderId: "YOUR_MESSAGING_SENDER_ID", - appId: "YOUR_APP_ID" - }; + apiKey: "YOUR_API_KEY", + authDomain: "YOUR_AUTH_DOMAIN", + projectId: "YOUR_PROJECT_ID", + storageBucket: "YOUR_STORAGE_BUCKET", + messagingSenderId: "YOUR_MESSAGING_SENDER_ID", + appId: "YOUR_APP_ID" + }; const app = initializeApp(firebaseConfig); @@ -20,9 +20,9 @@ if (typeof window !== 'undefined') { (window as any).FIREBASE_APPCHECK_DEBUG_TOKEN = true; initializeAppCheck(app, { - // The string here doesn't matter in this specific case, as setting - // FIREBASE_APPCHECK_DEBUG_TOKEN above means it will be ignored. - // However, in production, this MUST be a valid reCAPTCHA site key. + // The string here doesn't matter in this specific case, as setting + // FIREBASE_APPCHECK_DEBUG_TOKEN above means it will be ignored. + // However, in production, this MUST be a valid reCAPTCHA site key. provider: new ReCaptchaEnterpriseProvider('YOUR_RECAPTCHA_SITE_KEY'), isTokenAutoRefreshEnabled: true }); @@ -32,4 +32,8 @@ const ai = getAI(app); export const getAiModel = (modelName: string = 'gemini-3.5-flash-lite', additionalConfig: Record = {}) => { return getGenerativeModel(ai, { model: modelName, ...additionalConfig }); +}; + +export const getAiTemplateModel = (requestOptions?: RequestOptions) => { + return getTemplateGenerativeModel(ai, requestOptions); }; \ No newline at end of file