Upgrade to Pro — share decks privately, control downloads, hide ads and more …

How I built a second brain using Firebase, Gemi...

How I built a second brain using Firebase, Gemini, Genkit, and Siri

In our fast paced world, there is just too much information, and it often seems impossible to keep up with everything that’s going on.

If you ever felt that you couldn’t possibly remember everything you saw, read, or even didn’t read, come to this talk and I will show you how I build an app that allows me to do just that.

I will show you how I

* used SwiftUI to build a beautiful app that works across Apple’s platforms
* used Cloud Firestore to store gigabytes of data, keeping it in sync across all of my devices
* used Gemini, Google’s LLM, to summarise articles, and ask my app questions about articles
* used Genkit - an AI integration framework - to connect Gemini to my personal data store
* used Siri to provide a natural language interface that allows me to query my knowledge base hands-free

Peter Friese

August 07, 2024
Tweet

More Decks by Peter Friese

Other Decks in Programming

Transcript

  1. Peter Friese Developer Advocate, Firebase @peterfriese How I built a

    second brain Using Firebase, Gemini, Genkit, and Siri
  2. Codename Sofia ✨Add links via iOS Share Sheet ✨Extract OpenGraph

    Metadata ✨Simplified formatting ✨Offline reading
  3. Google AI SDK ✨Access Gemini models ✨SDKs for Python, Node,

    Go, Kotlin, Dart, Swift ✨Use for prototyping ✨Relatively low RPM ✨Requires an API key https: / / github.com/google-gemini/generative-ai-swift
  4. Vertex AI in Firebase SDK ✨Access Gemini models ✨SDKs for

    Swift, Kotlin, JS, Dart ✨Part of Firebase ✨Enterprise-ready ✨No API key required ✨Supports App Check https: / / firebase.google.com/docs/vertex-ai
  5. Using the VertexAI in Firebase SDK public func summarise(text: String)

    async throws -> String { let prompt = "Please summarise the following text for me: \(text)" let flash = "gemini-1.5-flash" let model = VertexAI.vertexAI().generativeModel(modelName: flash) let response = try await model.generateContent(prompt) if let text = response.text { return text } else { return "Couldn't compute summary" } } async, as we will be calling a remote system
  6. Using the VertexAI in Firebase SDK public func summarise(text: String)

    async throws -> String { let prompt = "Please summarise the following text for me: \(text)" let flash = "gemini-1.5-flash" let model = VertexAI.vertexAI().generativeModel(modelName: flash) let response = try await model.generateContent(prompt) if let text = response.text { return text } else { return "Couldn't compute summary" } }
  7. Using the VertexAI in Firebase SDK public func summarise(text: String)

    async throws -> String { let prompt = "Please summarise the following text for me: \(text)" let flash = "gemini-1.5-flash" let model = VertexAI.vertexAI().generativeModel(modelName: flash) let response = try await model.generateContent(prompt) if let text = response.text { return text } else { return "Couldn't compute summary" } } here’s the remote system call
  8. Using the VertexAI in Firebase SDK public func summarise(text: String)

    async throws -> String { let prompt = "Please summarise the following text for me: \(text)" let flash = "gemini-1.5-flash" let model = VertexAI.vertexAI().generativeModel(modelName: flash) let response = try await model.generateContent(prompt) if let text = response.text { return text } else { return "Couldn't compute summary" } }
  9. Conclusion ✨Use AI Studio to prototype your prompts ✨Use Google

    AI SDK to call LLMs from your client ✨Use Vertex AI in Firebase SDK to go to production ✨Secure your app with App Check ✨Call LLM directly from the client
  10. Task: Find all words that are food in the following

    sentence “I went down to Aberystwyth on foot to buy some welsh cakes and a few berries. When I finished doing my groceries, I had a latte at Coffee #1, where I met a few other speakers.”
  11. Task: Find all words that are food in the following

    sentence “I went down to Aberystwyth on foot to buy some welsh cakes and a few berries. When I finished doing my groceries, I had a latte at Coffee #1, where I met a few other speakers.”
  12. Task: Find all words that are food in the following

    sentence “I went down to Aberystwyth on foot to buy some welsh cakes and a few berries. When I finished doing my groceries, I had a latte at Coffee #1, where I met a few other speakers.”
  13. Vector embedding for food: [-0.018035058, 0.013980114, -0.01309541, 0.024956783, 0.02708295, -0.074924484,

    0.03496225, 0.0125780115, . .. ] Vector embedding for foot: [-0.016025933, 0.008207399, -0.03572462, 0.020942606, -0.0003162824, -0.041694388, 0.050102886, 0.007380137, . .. ]
  14. 01 Serverless (There are servers, but Google manages them for

    you) 02 03 04 Automatic scaling Scale down to zero. Support for min/max instances. Trusted environment JavaScript / TypeScript / Python
  15. Data wri tt en New user created User signing in

    Image uploaded Crashlytics ale r Analytics Conversions File deleted Data deleted Test run completed Data updated New con fi guration Con fi guration rollback Triggers
  16. Computing Vector Embeddings export const updateEmbeddings = onDocumentWritten( { document:

    'artifacts/{documentId}', minInstances: 1 }, async (event) => { const newData = event.data ?. after.data() ?? {}; const previousData = event.data ?. before.data() ? ? {}; if (newData.fullText === previousData.fullText) { return; } const embeddings = await embed({ embedder: textEmbeddingGecko, content: Document.fromText(newData.fullText), }); await event.data ?. after.ref.update({ embeddings: FieldValue.vector(embeddings), }); }); Firestore trigger
  17. Computing Vector Embeddings export const updateEmbeddings = onDocumentWritten( { document:

    'artifacts/{documentId}', minInstances: 1 }, async (event) => { const newData = event.data ?. after.data() ?? {}; const previousData = event.data ?. before.data() ? ? {}; if (newData.fullText === previousData.fullText) { return; } const embeddings = await embed({ embedder: textEmbeddingGecko, content: Document.fromText(newData.fullText), }); await event.data ?. after.ref.update({ embeddings: FieldValue.vector(embeddings), }); }); Read data from document
  18. Computing Vector Embeddings export const updateEmbeddings = onDocumentWritten( { document:

    'artifacts/{documentId}', minInstances: 1 }, async (event) => { const newData = event.data ?. after.data() ?? {}; const previousData = event.data ?. before.data() ? ? {}; if (newData.fullText === previousData.fullText) { return; } const embeddings = await embed({ embedder: textEmbeddingGecko, content: Document.fromText(newData.fullText), }); await event.data ?. after.ref.update({ embeddings: FieldValue.vector(embeddings), }); }); Compute embeddings
  19. Computing Vector Embeddings export const updateEmbeddings = onDocumentWritten( { document:

    'artifacts/{documentId}', minInstances: 1 }, async (event) => { const newData = event.data ?. after.data() ?? {}; const previousData = event.data ?. before.data() ? ? {}; if (newData.fullText === previousData.fullText) { return; } const embeddings = await embed({ embedder: textEmbeddingGecko, content: Document.fromText(newData.fullText), }); await event.data ?. after.ref.update({ embeddings: FieldValue.vector(embeddings), }); }); Update Firestore document
  20. 01 Compute embeddings for user’s query Using embedding model 02

    03 04 Find nearest neighbors Using Vector Similarity Search Inject context / history into prompt Using result of vector search / chat history Generate answer Using LLM Retrieval Augmented Generation
  21. const messageSchema = z.object({ message: z.string(), role: z.enum(['user', 'system']), });

    export const inputSchema = z.object({ question: z.string(), history: z.array(messageSchema).optional(), }); export const semanticQAFlow = onFlow( { name: 'semanticQAFlow', httpsOptions: { minInstances: 1 }, inputSchema: inputSchema, outputSchema: z.string(), authPolicy: firebaseAuth((user) = > { }), Genkit AI flow RAG-powered Q&A with Genkit
  22. const messageSchema = z.object({ message: z.string(), role: z.enum(['user', 'system']), });

    export const inputSchema = z.object({ question: z.string(), history: z.array(messageSchema).optional(), }); export const semanticQAFlow = onFlow( { name: 'semanticQAFlow', httpsOptions: { minInstances: 1 }, inputSchema: inputSchema, outputSchema: z.string(), authPolicy: firebaseAuth((user) = > { }), Format of the data we will pass in RAG-powered Q&A with Genkit
  23. export const inputSchema = z.object({ question: z.string(), history: z.array(messageSchema).optional(), });

    export const semanticQAFlow = onFlow( { name: 'semanticQAFlow', httpsOptions: { minInstances: 1 }, inputSchema: inputSchema, outputSchema: z.string(), authPolicy: firebaseAuth((user) = > { }), }, async (input) => { const docs = await retrieve({ retriever: firestoreRetriever, query: input.question, options: {limit: 3}, }); const context = docs.map((doc) => doc.text()).join('\n\n'); const history = input.history ? . map((message) => Fetch three best matches from Firestore RAG-powered Q&A with Genkit
  24. }, inputSchema: inputSchema, outputSchema: z.string(), authPolicy: firebaseAuth((user) = > {

    }), }, async (input) => { const docs = await retrieve({ retriever: firestoreRetriever, query: input.question, options: {limit: 3}, }); const context = docs.map((doc) => doc.text()).join('\n\n'); const history = input.history ? . map((message) => `${message.role}: ${message.message}`).join('\n'); const prompt = ` Your name is Sofia, and you are a knowledge assistant. You were created by Peter Friese, who is a Developer Advocate on the Firebase team at Google. Peter created you to showcase how to use Firebase and AI to build a second brain. Purpose: * Utilize my personal knowledge base to answer any questions I have. Build context using the three best matching documents RAG-powered Q&A with Genkit
  25. }, inputSchema: inputSchema, outputSchema: z.string(), authPolicy: firebaseAuth((user) = > {

    }), }, async (input) => { const docs = await retrieve({ retriever: firestoreRetriever, query: input.question, options: {limit: 3}, }); const context = docs.map((doc) => doc.text()).join('\n\n'); const history = input.history ? . map((message) => `${message.role}: ${message.message}`).join('\n'); const prompt = ` Your name is Sofia, and you are a knowledge assistant. You were created by Peter Friese, who is a Developer Advocate on the Firebase team at Google. Peter created you to showcase how to use Firebase and AI to build a second brain. Purpose: * Utilize my personal knowledge base to answer any questions I have. Build chat history RAG-powered Q&A with Genkit
  26. query: input.question, options: {limit: 3}, }); const context = docs.map((doc)

    => doc.text()).join('\n\n'); const history = input.history ? . map((message) => `${message.role}: ${message.message}`).join('\n'); const prompt = ` Your name is Sofia, and you are a knowledge assistant. You were created by Peter Friese, who is a Developer Advocate on the Firebase team at Google. Peter created you to showcase how to use Firebase and AI to build a second brain. Purpose: * Utilize my personal knowledge base to answer any questions I have. Question: ${input.question} Behavior: 1. Analyze the context provided by me. 2. Reference my personal knowledge base and gather relevant information. 3. Synthesize the information and provide a concise, accurate response. The Q&A prompt RAG-powered Q&A with Genkit
  27. query: input.question, options: {limit: 3}, }); const context = docs.map((doc)

    => doc.text()).join('\n\n'); const history = input.history ? . map((message) => `${message.role}: ${message.message}`).join('\n'); const prompt = ` Your name is Sofia, and you are a knowledge assistant. You were created by Peter Friese, who is a Developer Advocate on the Firebase team at Google. Peter created you to showcase how to use Firebase and AI to build a second brain. Purpose: * Utilize my personal knowledge base to answer any questions I have. Question: ${input.question} Behavior: 1. Analyze the context provided by me. 2. Reference my personal knowledge base and gather relevant information. 3. Synthesize the information and provide a concise, accurate response. The user’s question RAG-powered Q&A with Genkit
  28. a second brain. Purpose: * Utilize my personal knowledge base

    to answer any questions I have. Question: ${input.question} Behavior: 1. Analyze the context provided by me. 2. Reference my personal knowledge base and gather relevant information. 3. Synthesize the information and provide a concise, accurate response. 4. If there is insufficient information or the knowledge base does not contain relevant information, clearly state you don't know the answer. 5. Do not generate responses based on assumptions or speculation. 6. Respond in a professional and helpful manner. 7. When I ask details about you as a person, just provide basic info, and then try to get the conversation back on track to talk about knowledge in my knowledgebase. 8. If I ask you about what you know, tell me that you only know what is in my knowledge base. Expectations: * Accurate and relevant responses. Specifying model behavior RAG-powered Q&A with Genkit
  29. 2. Reference my personal knowledge base and gather relevant information.

    3. Synthesize the information and provide a concise, accurate response. 4. If there is insufficient information or the knowledge base does not contain relevant information, clearly state you don't know the answer. 5. Do not generate responses based on assumptions or speculation. 6. Respond in a professional and helpful manner. 7. When I ask details about you as a person, just provide basic info, and then try to get the conversation back on track to talk about knowledge in my knowledgebase. 8. If I ask you about what you know, tell me that you only know what is in my knowledge base. Expectations: * Accurate and relevant responses. * Clear indication when an answer is not known. * No fabrication or speculation in responses. * Provide relevant code snippets. Context: ${context} History: ${history} ` More instructions RAG-powered Q&A with Genkit
  30. 2. Reference my personal knowledge base and gather relevant information.

    3. Synthesize the information and provide a concise, accurate response. 4. If there is insufficient information or the knowledge base does not contain relevant information, clearly state you don't know the answer. 5. Do not generate responses based on assumptions or speculation. 6. Respond in a professional and helpful manner. 7. When I ask details about you as a person, just provide basic info, and then try to get the conversation back on track to talk about knowledge in my knowledgebase. 8. If I ask you about what you know, tell me that you only know what is in my knowledge base. Expectations: * Accurate and relevant responses. * Clear indication when an answer is not known. * No fabrication or speculation in responses. * Provide relevant code snippets. Context: ${context} History: ${history} ` Inject context (from the three documents) RAG-powered Q&A with Genkit
  31. 2. Reference my personal knowledge base and gather relevant information.

    3. Synthesize the information and provide a concise, accurate response. 4. If there is insufficient information or the knowledge base does not contain relevant information, clearly state you don't know the answer. 5. Do not generate responses based on assumptions or speculation. 6. Respond in a professional and helpful manner. 7. When I ask details about you as a person, just provide basic info, and then try to get the conversation back on track to talk about knowledge in my knowledgebase. 8. If I ask you about what you know, tell me that you only know what is in my knowledge base. Expectations: * Accurate and relevant responses. * Clear indication when an answer is not known. * No fabrication or speculation in responses. * Provide relevant code snippets. Context: ${context} History: ${history} ` Inject history (from the chat) RAG-powered Q&A with Genkit
  32. 7. When I ask details about you as a person,

    just provide basic info, and then try to get the conversation back on track to talk about knowledge in my knowledgebase. 8. If I ask you about what you know, tell me that you only know what is in my knowledge base. Expectations: * Accurate and relevant responses. * Clear indication when an answer is not known. * No fabrication or speculation in responses. * Provide relevant code snippets. Context: ${context} History: ${history} ` const answer = await generate({model: gemini15Flash, prompt}); return answer.text(); } ); Send prompt to model RAG-powered Q&A with Genkit
  33. 7. When I ask details about you as a person,

    just provide basic info, and then try to get the conversation back on track to talk about knowledge in my knowledgebase. 8. If I ask you about what you know, tell me that you only know what is in my knowledge base. Expectations: * Accurate and relevant responses. * Clear indication when an answer is not known. * No fabrication or speculation in responses. * Provide relevant code snippets. Context: ${context} History: ${history} ` const answer = await generate({model: gemini15Flash, prompt}); return answer.text(); } ); Return answer RAG-powered Q&A with Genkit
  34. RAG-powered Q&A with Genkit private var functions = FirebaseConnector.shared.functions public

    func performQuery(question: String, history: [ChatMessage]? = nil) async -> Response { let semanticQAFlowCallable: Callable<Query, Response> = functions.httpsCallable("semanticQAFlow") do { let query = Query(question: question, history: history) let response = try await semanticQAFlowCallable(query) return response } catch { return Response(shortSummary: "I am sorry, but something went wrong.", answer: "Something went wrong. Please try again.", source: nil) } } Callable Cloud Function
  35. RAG-powered Q&A with Genkit private var functions = FirebaseConnector.shared.functions public

    func performQuery(question: String, history: [ChatMessage]? = nil) async -> Response { let semanticQAFlowCallable: Callable<Query, Response> = functions.httpsCallable("semanticQAFlow") do { let query = Query(question: question, history: history) let response = try await semanticQAFlowCallable(query) return response } catch { return Response(shortSummary: "I am sorry, but something went wrong.", answer: "Something went wrong. Please try again.", source: nil) } } Invoke callable function
  36. @MainActor public struct SemanticQAIntent: @preconcurrency AppIntent { private var semanticQAService

    = SemanticQAService.shared @Parameter(title: "Question", description: "Answer from your knowledge base") var question: String? public static let title: LocalizedStringResource = "Search" static let description: LocalizedStringResource = "Search your saved artifacts" public static var parameterSummary: some ParameterSummary { Summary("You asked: \(\.$question)") } public init() { } public func perform() async throws -> some ProvidesDialog & ShowsSnippetView { guard let providedPhrase = question else { Conform to AppIntent Siri: App Intents
  37. @MainActor public struct SemanticQAIntent: @preconcurrency AppIntent { private var semanticQAService

    = SemanticQAService.shared @Parameter(title: "Question", description: "Answer from your knowledge base") var question: String? public static let title: LocalizedStringResource = "Search" static let description: LocalizedStringResource = "Search your saved artifacts" public static var parameterSummary: some ParameterSummary { Summary("You asked: \(\.$question)") } public init() { } public func perform() async throws -> some ProvidesDialog & ShowsSnippetView { guard let providedPhrase = question else { Define input parameters Siri: App Intents
  38. public static var parameterSummary: some ParameterSummary { Summary("You asked: \(\.$question)")

    } public init() { } public func perform() async throws -> some ProvidesDialog & ShowsSnippetView { guard let providedPhrase = question else { throw $question.needsValueError("Sure - what would you like to know?") } let answer = await semanticQAService.performQuery(question: providedPhrase) return .result(dialog: IntentDialog(stringLiteral: "Here is your answer")) { Markdown(answer) .padding() .markdownBlockStyle(\.codeBlock) { configuration in configuration.label .relativeLineSpacing(.em(0.25)) .markdownTextStyle { FontFamilyVariant(.monospaced) FontSize(.em(0.85)) } Siri: App Intents This will be run when your Intent is invoked
  39. public static var parameterSummary: some ParameterSummary { Summary("You asked: \(\.$question)")

    } public init() { } public func perform() async throws -> some ProvidesDialog & ShowsSnippetView { guard let providedPhrase = question else { throw $question.needsValueError("Sure - what would you like to know?") } let answer = await semanticQAService.performQuery(question: providedPhrase) return .result(dialog: IntentDialog(stringLiteral: "Here is your answer")) { Markdown(answer) .padding() .markdownBlockStyle(\.codeBlock) { configuration in configuration.label .relativeLineSpacing(.em(0.25)) .markdownTextStyle { FontFamilyVariant(.monospaced) FontSize(.em(0.85)) } Siri: App Intents Call the Q&A Genkit flow
  40. public static var parameterSummary: some ParameterSummary { Summary("You asked: \(\.$question)")

    } public init() { } public func perform() async throws -> some ProvidesDialog & ShowsSnippetView { guard let providedPhrase = question else { throw $question.needsValueError("Sure - what would you like to know?") } let answer = await semanticQAService.performQuery(question: providedPhrase) return .result(dialog: IntentDialog(stringLiteral: "Here is your answer")) { Markdown(answer) .padding() .markdownBlockStyle(\.codeBlock) { configuration in configuration.label .relativeLineSpacing(.em(0.25)) .markdownTextStyle { FontFamilyVariant(.monospaced) FontSize(.em(0.85)) } Siri: App Intents Result is a dialog
  41. public static var parameterSummary: some ParameterSummary { Summary("You asked: \(\.$question)")

    } public init() { } public func perform() async throws -> some ProvidesDialog & ShowsSnippetView { guard let providedPhrase = question else { throw $question.needsValueError("Sure - what would you like to know?") } let answer = await semanticQAService.performQuery(question: providedPhrase) return .result(dialog: IntentDialog(stringLiteral: "Here is your answer")) { Markdown(answer) .padding() .markdownBlockStyle(\.codeBlock) { configuration in configuration.label .relativeLineSpacing(.em(0.25)) .markdownTextStyle { FontFamilyVariant(.monospaced) FontSize(.em(0.85)) } Siri: App Intents gonzalezreal/swift-markdown-ui
  42. S1 E4 Building a Second Brain App - Data Access

    􀉉 Friday, November 15th 􀐫 18 : 00 CET / 9 : 00 PST / 12 : 00 EST / 17 : 00 UTC Livestream