{"id":43802368,"url":"https://github.com/motorro/firebase-ai-chat","last_synced_at":"2026-02-05T22:08:00.379Z","repository":{"id":223446345,"uuid":"758822172","full_name":"motorro/firebase-ai-chat","owner":"motorro","description":"AI Assistant chat with Firebase","archived":false,"fork":false,"pushed_at":"2024-11-29T18:35:47.000Z","size":2188,"stargazers_count":4,"open_issues_count":0,"forks_count":1,"subscribers_count":5,"default_branch":"master","last_synced_at":"2024-12-18T02:39:28.887Z","etag":null,"topics":["ai","cloud-computing","firebase","firestore","gemini","openai","vertex-ai"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/motorro.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-02-17T07:12:47.000Z","updated_at":"2024-11-29T18:34:53.000Z","dependencies_parsed_at":"2024-05-07T12:39:42.706Z","dependency_job_id":"5a9f986e-7ae0-4538-9636-95a6ddb95845","html_url":"https://github.com/motorro/firebase-ai-chat","commit_stats":null,"previous_names":["motorro/firebase-openai-chat","motorro/firebase-ai-chat"],"tags_count":55,"template":false,"template_full_name":null,"purl":"pkg:github/motorro/firebase-ai-chat","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/motorro%2Ffirebase-ai-chat","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/motorro%2Ffirebase-ai-chat/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/motorro%2Ffirebase-ai-chat/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/motorro%2Ffirebase-ai-chat/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/motorro","download_url":"https://codeload.github.com/motorro/firebase-ai-chat/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/motorro%2Ffirebase-ai-chat/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29136048,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-05T21:59:57.939Z","status":"ssl_error","status_checked_at":"2026-02-05T21:59:57.628Z","response_time":65,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai","cloud-computing","firebase","firestore","gemini","openai","vertex-ai"],"created_at":"2026-02-05T22:07:59.375Z","updated_at":"2026-02-05T22:08:00.367Z","avatar_url":"https://github.com/motorro.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Firebase AI Chat\n\nAI assistant chat for front-end applications residing on server with [Firebase technology](https://firebase.google.com/).\nSupported AI engines:\n- [VertexAI](https://cloud.google.com/nodejs/docs/reference/vertexai/latest)\n- [OpenAI](https://platform.openai.com/docs/assistants/overview)\n\nHere's the video for 2024 Google AI application contest to start:\n\n[![IMAGE ALT TEXT HERE](https://img.youtube.com/vi/vian88VIcxM/0.jpg)](https://www.youtube.com/watch?v=vian88VIcxM)\n\n[![Check](https://github.com/motorro/firebase-ai-chat/actions/workflows/test.yml/badge.svg?branch=master)](https://github.com/motorro/firebase-ai-chat/actions/workflows/test.yml)\n\nEngines:\n- Core: [![npm version](https://badge.fury.io/js/@motorro%2Ffirebase-ai-chat-core.svg)](https://badge.fury.io/js/@motorro%2Ffirebase-ai-chat-core)\n- VertexAI: [![npm version](https://badge.fury.io/js/@motorro%2Ffirebase-ai-chat-vertexai.svg)](https://badge.fury.io/js/@motorro%2Ffirebase-ai-chat-vertexai)\n- OpenAI: [![npm version](https://badge.fury.io/js/@motorro%2Ffirebase-ai-chat-openai.svg)](https://badge.fury.io/js/@motorro%2Ffirebase-ai-chat-openai)\n\n\n## Contents\n\n\u003c!-- toc --\u003e\n\n- [A problem statement](#a-problem-statement)\n- [Sample Firebase project](#sample-firebase-project)\n- [Thomas And Friends project](#thomas-and-friends-project)\n- [Components](#components)\n- [Module API](#module-api)\n  * [Scaffolds](#scaffolds)\n  * [Firestore indexes](#firestore-indexes)\n  * [Checking user authentication](#checking-user-authentication)\n  * [Front-facing functions](#front-facing-functions)\n  * [Creating AssistantChat](#creating-assistantchat)\n  * [Creating a new chat](#creating-a-new-chat)\n  * [Handling user messages](#handling-user-messages)\n  * [Running AI](#running-ai)\n  * [Using AI function tools](#using-ai-function-tools)\n- [Tool continuation](#tool-continuation)\n  * [Note on suspending the engine in tool calls with continuation](#note-on-suspending-the-engine-in-tool-calls-with-continuation)\n- [AI message mapping](#ai-message-mapping)\n- [Message middleware](#message-middleware)\n- [Assistant switching and assistant crew](#assistant-switching-and-assistant-crew)\n  * [Switching in method middleware](#switching-in-method-middleware)\n  * [Switching in tools reducers](#switching-in-tools-reducers)\n- [Using multiple engines in a single project](#using-multiple-engines-in-a-single)\n- [Client application](#client-application)\n\n\u003c!-- tocstop --\u003e\n\n## A problem statement\nSince companies has published its LLM APIs, integrating custom chats to your client apps has become a rather easy option.\nGiven that the API is HTTP-based and there are also many [wrapping libraries](https://github.com/aallam/openai-kotlin) \nfor any platform, the one might implement the chat directly in a mobile app or a web-site. However, it might be not \na good architectural decision to go this way. Among the reasons are:\n\n- AI API key protection and management - if used on a front-end app the key leaks.\n- Tools and function call code (a part of business logic) are exposed to app client.\n- The latency in mobile application updates will limit your domain updates as function tool-calls reside on the \n  front-end.\n- Mobile app connectivity is always a problem and given that an AI run takes a considerable time to execute the user \n  experience might not be optimal.\n\nThus, it might be a good idea to put the AI interaction to the back-end and to update your client with just \nthe results of AI runs.\nThe possible flow might be the following:\n![Usecase diagram](http://www.plantuml.com/plantuml/proxy?src=https://raw.githubusercontent.com/motorro/firebase-openai-chat/master/readme/usecase.puml)\n\n- User interacts with a front-end application and posts messages.\n- The App uses YOUR backend endpoints to receive the user gestures.\n- The Back-end executes the Assistant run in a worker function and posts the results to a local database.\n- Function tool calls are managed by your Back-end providing all necessary data back and forth.\n- Upon the run is complete, Back-end updates its storage with AI replies.\n- The App updates itself from the Back-end storage and displays changes to the User.\n\nThis project is an illustration of above-mentioned approach using:\n\n- [Firebase Cloud functions](https://firebase.google.com/docs/functions) as a backend.\n- [Google Cloud tasks](https://cloud.google.com/tasks) as an \"offline\" worker to run AI.\n- [Firebase Cloud Firestore](https://firebase.google.com/docs/firestore) as a storage.\n- [Firebse Authentication](https://firebase.google.com/docs/auth) to authenticate users and restrict the chat access.\n\nThe project is packed as an NPM module in case you'd like to use it in your own application. It handles the complete\nAI chat workflow including running AI engine, message management, state management, and function runs.\n\n## Sample Firebase project\nDue to strict [type-check restrictions](https://github.com/googleapis/nodejs-firestore/issues/760) for\nfirebase types the test example project is in the separate [repository](https://github.com/motorro/firebase-ai-chat-project).\nThe sample includes:\n- A minimal Firebase project\n- A sample Gemini assistant creation script\n- A sample OpenAI assistant creation script\n- A sample mobile application\n\n## Thomas And Friends project\nAs a part of the 2024 Google AI application contest we built a project that is an experimental travel agency concierge using Agentic Flow.\nCheck the sources [here](https://github.com/motorro/ThomasAndFriends). The project uses almost all the features of this engine in a real world application.\nHere's the [article](https://app.readytensor.ai/publications/ZB89eznfyXtK) on Ready Tensor with a short description of the project.\n\n## Components\nLet's take a closer look at the implementation...\n\n![Component diagram](http://www.plantuml.com/plantuml/proxy?src=https://raw.githubusercontent.com/motorro/firebase-ai-chat/master/readme/components.puml)\n\n- Client creates a chat by calling a cloud function.\n- Client posts messages to assistant with a client function.\n- Firebase launches a Cloud Task to run AI assistant.\n- Client messages and Assistant response are written to Firestore.\n- Client gets chat state and messages by subscribing to Firestore documents.\n\n## Module API\nThe module has three classes to use in your project:\n- [AssistantChat](core/src/aichat/AssistantChat.ts) - handles requests from the App user\n- [AiChatWorker](core/src/aichat/workers/ChatWorker.ts) - runs the AI interaction \"off-line\" in a Cloud function\n- [AiChat](openai/src/index.ts) - a factory to create those above\n\n### Scaffolds\n\nTo install OpenAI module use:\n```shell\nnpm i --save @motorro/firebase-ai-chat-openai\n```\nTo install VertexAI module use:\n```shell\nnpm i --save @motorro/firebase-ai-chat-vertexai\n```\n\nThe full example `index.ts` for your Firebase Cloud Functions project is available [here](https://github.com/motorro/firebase-openai-chat-project/blob/master/Firebase/functions/src/index.ts).\nIt demonstrates the technology and AI function tools usage. We will create a simple calculator that can add and subtract\nnumbers from the accumulated state value that is being persisted along with a chat state.\nFor our test project we will use the following state:\n```typescript\nexport interface CalculateChatData extends ChatData{\n  readonly sum: number\n}\n```\n\nFirst things first we need to create:\n- A Firestore collection to hold chats. The collection documents hold the chat state and have\n  a \"messages\" sub-collection where the chat messages are stored.\n- An instance of chat components factory that are to be used to run chats.\n\n```typescript\nimport {AiChat, factory} from \"@motorro/firebase-ai-chat-openai\";\n\n// Chats collection name\nconst CHATS = \"chats\"; \n\nconst db = firestore();\nconst chats = db.collection(CHATS) as CollectionReference\u003cChatState\u003cCalculateChatData\u003e\u003e;\nconst chatFactory: AiChat = factory(firestore(), getFunctions(), \"europe-west1\");\n```\n\nYou may also want to set a custom logger to the library so the log output will get to your functions log:\n\n```typescript\nimport {logger as fLogger} from \"firebase-functions/v2\";\nimport {Logger, setLogger} from \"firebase-ai-chat\";\n\n// Chat processor name\nconst NAME = \"calculator\";\n\nconst logger: Logger = {\n  d: (...args: unknown[]) =\u003e {\n    fLogger.debug([NAME, ...args]);\n  },\n  i: (...args: unknown[]) =\u003e {\n    fLogger.info([NAME, ...args]);\n  },\n  w: (...args: unknown[]) =\u003e {\n    fLogger.warn([NAME, ...args]);\n  },\n  e: (...args: unknown[]) =\u003e {\n    fLogger.error([NAME, ...args]);\n  }\n};\nsetLogger(logger);\n```\n\nAll chats are bound to users authenticated with Firebase Auth. For the client App to be able to get chat snapshots,\nyou need to adjust your [Firestore rules](https://firebase.google.com/docs/firestore/security/get-started):\n\n```cel\nrules_version = '2';\nservice cloud.firestore {\n  match /databases/{database}/documents {\n    \n    // Check the user is authenticated\n    function isAuthenticated() {\n        return null != request.auth \u0026\u0026 null != request.auth.uid;\n    }\n\n    // Check the user owns the document\n    // Include filter `where(\"userId\", \"==\", auth.uid)` to your list requests\n    function isOwner() {\n      return request.auth.uid == resource.data.userId;\n    }\n\n\tmatch /chats/{chat} {\n      // Allow read to those who created the chat\n      allow read: if isAuthenticated() \u0026\u0026 isOwner();\n      allow write: if false;\n\n      // Allow reading messages to chat owners\n      match /messages/{message} {\n        allow read: if isAuthenticated() \u0026\u0026 isOwner();\n        allow write: if false;\n      }\n    }\n  }\n}\n```\n\n### Firestore indexes\nTo be able to run the code Firebase needs two special indexes to sort chat documents and messages.\nYou can get the latest indexes versions [here](firestore.indexes.json).\n\n### Checking user authentication\nAs soon as we want all users to be authenticated to access the functions, let's create a template function to handle \nall function requests. We will later use this function to wrap all client-facing handlers:\n\n```typescript\nimport {CallableRequest, HttpsError} from \"firebase-functions/v2/https\";\n\nasync function ensureAuth\u003cDATA, RES\u003e(request: CallableRequest\u003cDATA\u003e, block: (uid: string, data: DATA) =\u003e Promise\u003cRES\u003e): Promise\u003cRES\u003e {\n  const uid = request.auth?.uid;\n  if (undefined === uid) {\n    logger.w(\"Unauthenticated\");\n    return Promise.reject\u003cRES\u003e(new HttpsError(\"unauthenticated\", \"Unauthenticated\"));\n  }\n  return await block(uid, request.data);\n}\n```\n### Front-facing functions\nAll chat requests from users are handled by [AssistantChat](core/src/aichat/AssistantChat.ts).\nIt is a front-facing API that takes requests from your clients, maintains the chat state and schedules AI runs.\nThe class has three methods:\n\n- `create` - creates a new interactive chat with your app client. \n- `singleRun` - creates a new chat that runs once. You may find it to schedule one-off analyses with tools output.\n- `postMessage` - posts a new client message\n- `closeChat` - finishes the chat deleting all resources (optional)\n\n### Creating AssistantChat\nTo create request processor, use the [AiChat](openai/src/index.ts) factory we have set up in the previous step:\n\n```typescript\n// Functions region where the worker task will run\nconst region = \"europe-west1\";\n// Chat worker function (queue) name to dispatch work\nconst NAME = \"calculator\";\n\nconst assistantChat = chatFactory.chat(\n    NAME\n);\n```\n\n### Creating a new chat\n\nFirst things first we need to create:\n- A Firestore collection to hold chats. The collection documents hold the chat state and have \n  a \"messages\" sub-collection where the chat messages are stored.\n- An instance of chat components factory that are to be \n\nImagine the chat in the App as a sequence of two screens:\n\n- Initial prompt from the User, where he enters the first request to AI\n- The chat screen where he observes the messages and the chat state\n\nThen to start a chat we may need the following request to your cloud function:\n\n```typescript\nexport interface CalculateChatRequest {\n    readonly message: string\n}\n```\n\nIn response to our requests the client will get:\n```typescript\nexport interface CalculateChatResponse {\n  // Created chat document\n  readonly chatDocument: string\n  // Chat status\n  readonly status: ChatStatus,\n  // Chat data so far\n  readonly data: CalculateChatData\n}\n```\n\nTo handle this request at Firebase you may want to create the following function:\n\n```typescript\nimport {CallableOptions, CallableRequest, onCall as onCall2} from \"firebase-functions/v2/https\";\nimport { ChatState } from \"firebase-ai-chat\";\n\nexport const calculate = onCall2(options, async (request: CallableRequest\u003cCalculateChatRequest\u003e) =\u003e {\n  return ensureAuth(request, async (uid, data) =\u003e {\n    // Create a new chat document reference\n    const chat = chats.doc() as DocumentReference\u003cChatState\u003cCalculateChatData\u003e\u003e;\n    // Configure AI assistant\n    const config: OpenAiAssistantConfig = {\n      engine: \"openai\",\n      assistantId: openAiAssistantId.value(),\n      dispatcherId: NAME\n    };\n    // Create a chat document record in CHATS collection\n    const result = await assistantChat.create(\n            chat, // Chat document\n            uid, // Owner ID\n            {sum: 0}, // Initial data state\n            config, // Configuration\n            [data.messages], // Initial message to process\n            {a: 1}, // Optional metadata to pass with worker task. Available in completion handler\n            {\n              aiMessageMeta: { // This meta will be added to all AI messages (optional)\n                  name: \"AI\"\n              },\n              userMessageMeta: { // This meta will be added to all User messages (optional)\n                  name: \"Vasya\"\n              }\n            }\n    );\n    \n    // Return the `CalculateChatResponse` to client App\n    return {\n      chatDocument: chat.path, // Created chat document path\n      status: result.status, // Chat status so far (see `ChatStatus`)\n      data: result.data\n    };\n  });\n});\n```\n\nUnder the hood the processor will:\n- Create a [ChatState](core/src/aichat/data/ChatState.ts) document in `CHATS` collection.\n- Store a `CalculateChatData` of initial data there.\n- Create a [ChatMessage](core/src/aichat/data/ChatMessage.ts) in `messages` sub-collection of chat document.\n- Run a Cloud Task by queueing a [ChatCommand](core/src/aichat/data/ChatCommand.ts) to Cloud Tasks.\n\n### Handling user messages\nUser may respond to AI messages whenever the [ChatState](core/src/aichat/data/ChatState.ts) has one of the \npermitted [ChatStatus](core/src/aichat/data/ChatState.ts):\n- `userInput` - waiting for a user input\n\nThe request to handle a message may look like this:\n\n```typescript\nexport interface PostCalculateRequest {\n    readonly chatDocument: string\n    readonly message: string\n}\n```\n\nTo handle such a request you may want to create the following function:\n\n```typescript\nexport const postToCalculate = onCall2(options, async (request: CallableRequest\u003cPostCalculateRequest\u003e) =\u003e {\n  return ensureAuth(request, async (uid, data) =\u003e {\n    // Creates a new client message and schedules an AI run\n    const result = await assistantChat.postMessage(\n            db.doc(data.chatDocument) as DocumentReference\u003cChatState\u003cCalculateChatData\u003e\u003e,\n            uid,\n            [data.message],\n            {a: 1} // Optional metadata to pass with worker task. Available in completion handler\n    );\n    \n    // Return the `CalculateChatResponse` to client App\n    return {\n      chatDocument: data.chatDocument,\n      status: result.status,\n      data: result.data\n    };\n  });\n});\n```\n\n### Running AI\nThe requests to front-facing functions return to user as fast as possible after changing the chat state in Firestore.\nAs soon as the AI run could take a considerable time, we run theme in a Cloud Task \"offline\" from the client request.\nTo execute the Assistant run we use the second class from the library - the [ChatWorker](core/src/aichat/workers/ChatWorker.ts).\nTo create it, use the [AiChat](openai/src/index.ts) factory we created in previous steps.\n\nTo register the Cloud Task handler you may want to create the following function:\n\n```typescript\nimport {onTaskDispatched} from \"firebase-functions/v2/tasks\";\nimport OpenAI from \"openai\";\nimport {VertexAiWrapper, Meta} from \"@motorro/firebase-ai-chat-openai\";\n\nexport const calculator = onTaskDispatched(\n    {\n      secrets: [openAiApiKey],\n      retryConfig: {\n        maxAttempts: 1,\n        minBackoffSeconds: 30\n      },\n      rateLimits: {\n        maxConcurrentDispatches: 6\n      },\n      region: region\n    },\n    async (req) =\u003e {\n      // Create OpenAI API instance and OpenAI adapter\n      const ai = new OpenAiWrapper(new OpenAI({apiKey: openAiApiKey.value()}));\n      // Create and run a worker\n      // See the `dispatchers` definitions below\n      await chatFactory.worker(ai, dispatchers).dispatch(\n          req,\n          (chatDocumentPath: string, meta: Meta) =\u003e {\n             // Optional task completion handler\n             // Meta - some meta-data passed to chat operation\n          }   \n      );\n    }\n);\n```\nThe `VertexAiChatWorker` handles the [ChatCommand](core/src/aichat/data/ChatCommand.ts) and updates [ChatState](core/src/aichat/data/ChatState.ts)\nwith the results.\nThe client App will later get the results of the run by subscribing the Firebase collection snapshots flow.\n\nWorth mentioning is that if you run several chats with a different state for different purposes you may need only one \nworker function to handle all the tasks. The [ChatCommand](core/src/aichat/data/ChatCommand.ts) has all the required reference\ndata to address the correct chat document and chat data state.\n\nAs the AI run may involve several AI calls which may fail at any intermediate call the possible retry run strategy \nseems unclear at the moment. That is why the worker will set the `failed` state to chat on any error. If you want to \nrestore the thread somehow - create the new chat and copy your messages manually.\n\n### Using AI function tools\nAI model APIs often have a powerful feature called [Function calling](https://platform.openai.com/docs/assistants/tools/function-calling).\nThe use of functions is a bridge between your business logic and the AI. It may be used to retrieve some information\nfrom your domain, to alter some state that resides on your server or to limit the AI \"hallucinations\" when operating\nwith your domain data.\n\nIn this example we create a simple calculator that can add and subtract numbers from some accumulated value stored \nalong with your chat state. Here is our domain state:\n\n```typescript\nexport interface CalculateChatData extends ChatData{\n  readonly sum: number\n}\n```\n\nIn the sample project you can find the script to create a [sample assistant](https://github.com/motorro/firebase-openai-chat-project/blob/master/Firebase/assistant/src/createCalculatorAssistant.ts)\nto be a calculator. Take a look at the prompt and function definitions there for an example.\n\nThe library supports function tool calling by providing a map of function dispatchers to `VertexAiChatWorker`.\nThe [dispatcher](core/src/aichat/ToolsDispatcher.ts) is the following function:\n\n```typescript\nexport interface ToolsDispatcher\u003cDATA extends ChatData\u003e {\n  (data: DATA, name: string, args: Record\u003cstring, unknown\u003e, continuation: ContinuationCommand\u003cunknown\u003e, chatData: ChatDispatchData): ToolDispatcherReturnValue\u003cDATA\u003e\n}\n```\nThe parameters are the following:\n\n- `data` - Chat state data so far\n- `name` - Name of function called\n- `args` - Function arguments\n- `continuation` - Continuation command (see below)\n- `chatData` - Common chat data\n\nThe function returns [ToolDispatcherReturnValue](core/src/aichat/ToolsDispatcher.ts) value which may be:\n\n- Some data - it will be submitted to AI as `{result: \"Response from your tool\"}`\n- Reduction result of `data` state passed to dispatcher. Return `{data: \"Your data state\"}` from the dispatcher and it \n  will go to AI like this\n- Some error describing the problem to AI: `{error: \"You have passed the wrong argument}`\n- Continuation of above (see continuation below)\n- Promise of the above\n\nThe [AiWrapper](openai/src/aichat/AiWrapper.ts) will run your dispatcher and re-run the Assistant with\ndispatcher output.\n\nFor our simple project we define the dispatcher like this:\n\n```typescript\nimport {ToolsDispatcher} from \"@motorro/firebase-ai-chat-openai\";\n\nconst dispatcher: ToolsDispatcher\u003cCalculateChatData\u003e = async function(\n        data: DATA, // Chat data so far\n        name: string, // Function name\n        args: Record\u003cstring, unknown\u003e, // Function arguments\n        continuation: ContinuationCommand\u003cunknown\u003e, // Continuation in case of suspension. See below\n        chatData: ChatDispatchData\u003cCM\u003e, // Chat data\n        handOver: ToolsHandOver\u003cWM, CM\u003e // Chat hand-over to another assistant. See below\n): ToolDispatcherReturnValue\u003cCalculateChatData\u003e {\n  switch (name) {\n    case \"getSum\":\n      logger.d(\"Getting current state...\");\n      return {\n        sum: data.sum\n      };\n    case \"add\":\n      logger.d(\"Adding: \", args);\n      return {\n        sum: data.sum + (args.value as number)\n      };\n    case \"subtract\":\n      logger.d(\"Handing-over subtract: \", args.value);\n      handOver.handOver(\n              {\n                config: {\n                  engine: \"vertexai\",\n                  instructionsId: SUBTRACTOR_NAME\n                },\n                messages: [\n                  `User wants to subtract ${args.value as number}`\n                ],\n                chatMeta: {\n                  aiMessageMeta: {\n                    name: SUBTRACTOR_NAME,\n                    engine: \"VertexAi\"\n                  }\n                }\n              }\n      );\n      return {\n        result: \"The request was passed to Divider. The number is being subtracted. Divider will come back with a new accumulated state\"\n      };\n    case \"multiply\":\n      logger.d(\"Multiply. Suspending multiplication: \", args.value);\n      await taskScheduler.schedule(\n              multiplierQueueName,\n              {\n                data: data,\n                factor: (args.value as number),\n                continuationCommand: continuation\n              }\n      );\n      return Continuation.suspend();\n    default:\n      logger.e(\"Unimplemented function call: \", name, args);\n      throw new HttpsError(\"unimplemented\", \"Unimplemented function call\");\n  }\n};\n\nconst dispatchers: Record\u003cstring, ToolsDispatcher\u003cany\u003e\u003e = {\n  [NAME]: dispatcher\n};\n```\n\nWe pass the dispatchers object to our worker. As mentioned before the single Cloud Task function with a worker\nis enough to handle all AI runs from different chats. That is why we pass a map of dispatchers here. The worker selects\na correct dispatcher getting a command and reading the appropriate chat state from Firebase.\n\n## Tool continuation\nSometimes your dispatcher can't get a response promptly (within the dispatch promise). For example, you may need to launch\nanother queue task to get the response. The dispatching function receives the `continuation` parameter. If you need to \nsuspend tool processing, save this continuation somewhere and when your response is ready, use [ToolsContinuationScheduler](core/src/aichat/workers/ToolsContinuationScheduler.ts)\nwhich you could get from the factory to respawn the tools processing:\n\n```typescript\nconst taskScheduler = taskScheduler || new FirebaseQueueTaskScheduler(functions, location);\nconst continuationSchedulerFactory = toolContinuationSchedulerFactory(firestore, taskScheduler);\n\n// Create the instance of continuation scheduler for our queue name\nconst continuationScheduler: ToolsContinuationScheduler\u003cstring\u003e = continuationSchedulerFactory.getContinuationScheduler(\"calculator\");\n\nlet savedContinuation: ContinuationCommand\u003cunknown\u003e | undefined = undefined;\n\nconst myDispatcher = (data: ChatData, name: string, args: Record\u003cstring, unknown\u003e, continuation: ContinuationCommand\u003cunknown\u003e) =\u003e {\n  // Getting the result requires some data not available promptly\n  // Save passed continuation elswhere\n  savedContinuation = continuation;\n  // Run your offline tool\n  // ...\n  // Suspend tool processing\n  return Continuation.suspend();\n}\n\n// Later when result is ready\nconst whenResultIsReady = async (data: string) =\u003e {\n  // Resume tools processing using saved continuation\n  const continuation = savedContinuation;\n  if (undefined !== continuation) {\n      await continuationScheduler.continue(continuation, {result: {answer: data}});\n  }\n}\n```\nExample of suspending tool dispatch in tools reducer could be found [here](https://github.com/motorro/firebase-ai-chat-project/blob/master/Firebase/functions/src/common/calculator.ts#L42).\nExample of resuming AI run could be found [here](https://github.com/motorro/firebase-ai-chat-project/blob/master/Firebase/functions/src/index.ts#L136).\n\n### Note on suspending the engine in tool calls with continuation\nTake a note that suspending the assistant within tool suspending the calls with continuation may fail if the new assistant\ntakes a long time to hand back. For example, currently (2024-06-28), OpenAI calls tools during the assistant run and times\nout after 10 minutes.\n\n## AI message mapping\nBy default, the library takes text messages from AI and makes text chat messages of them. If you use images or want to \nparse custom data (or metadata) when exchanging messages between the client chat and AI you may want to add a custom \nmessage mapper for each engine. Take a look at default mappers to get the idea:\n- [OpenAI](openai/src/aichat/OpenAiMessageMapper.ts)\n- [VertexAI](vertexai/src/aichat/VertexAiMessageMapper.ts)\n\nThe [NewMessage](core/src/aichat/data/NewMessage.ts) which goes to/from client and AI may be just a string of a structured\ndata that gets to corresponding fields of [ChatMessage](core/src/aichat/data/ChatMessage.ts).\nTo provide your custom message mapper use the corresponding parameter to the `worker` functions of chat factories.\nThe example is available in a sample project:\n- [VertexAI](https://github.com/motorro/firebase-ai-chat-project/blob/master/Firebase/functions/src/openai/openai.ts#L89)\n- [OpenAI](https://github.com/motorro/firebase-ai-chat-project/blob/master/Firebase/functions/src/openai/openai.ts#L89)\n- [Common part](https://github.com/motorro/firebase-ai-chat-project/blob/master/Firebase/functions/src/common/calculator.ts#L196)\n\n## Message middleware\nBy default, the library saves all messages received from AI to the client chat. However, you may want to custom-process\nthose messages to filter special messages or to do some other processing (e.g. [handing over](#assistant-swithching-and-assistant-crew)).\nThe middleware is a function with the following parameters:\n```typescript\nexport interface MessageMiddleware\u003cDATA extends ChatData, CM extends ChatMeta = ChatMeta\u003e {\n  /**\n   * Processes message\n   * @param messages Message received from AI\n   * @param chatDocumentPath Chat document path\n   * @param chatState Chat state\n   * @param control Message processing control\n   */\n  (\n      messages: ReadonlyArray\u003cNewMessage\u003e,\n      chatDocumentPath: string,\n      chatState: ChatState\u003cAssistantConfig, DATA, CM\u003e,\n      control: MessageProcessingControl\u003cDATA, CM\u003e\n  ): Promise\u003cvoid\u003e\n}\n```\nEach time the engine responds with a message, it is being mapped with a mapper and then provided to your middleware along\nwith the chat document path and state. Along with the data you get a [MessageProcessingControl](core/src/aichat/middleware/MessageMiddleware.ts)\nobject to take the next steps. You can custom-process messages, pass them to the next processor or schedule some other\ntask and complete the processing queue. Take a look at the source code for documentation. This is pretty \"low-level\" API\nso some more specialized middleware is already there. Take a look at hand-over middleware below.\nTo provide the middleware to your workers, pass the array of them to the `worker` method of the engine factory.\n\n## Assistant switching and assistant crew\nAs your tasks grow more complex it is worth considering delegating different tasks to different assistants each of them\ntrained to perform certain scope of tasks. Thus, a crew of assistants work as a team to decompose the task and move step\nby step to fulfill it. One of the famous frameworks to build such a team is [Crew AI](https://docs.crewai.com/). This \nlibrary also supports changing assistants on-demand. For example, consider our main Calculator who could add numbers. \nIf user wants to subtract or to divide the accumulated value by some number we could switch chat context to another assistant\n\"trained\" to divide numbers. Here is the example:\n![Switching](/readme/Switching.png)\n\nHere is how it is being done as a sequence diagram:\n![Switching](http://www.plantuml.com/plantuml/proxy?src=https://raw.githubusercontent.com/motorro/firebase-ai-chat/master/readme/Switching.puml)\n\nTo be able to do it there are two methods in [AssistantChat](core/src/aichat/AssistantChat.ts):\n\n- `handOver` - changes the context of chat to use with another assistant.\n- `handBack` - restores context to main assistant\n\n### Switching in method middleware\nYou may also switch during message processing. There is a special [middleware](core/src/aichat/middleware/handOverMiddleware.ts)\navailable to do the switch during the message processing. The `HandOverControl` object in your middleware function will get the\nmethods to hand over and to hand back the chat control.\n\nThe example is available in a sample project. \n1. Instruct the assistant to use some kind of [special message for hand-over](https://github.com/motorro/firebase-ai-chat-project/blob/master/Firebase/functions/src/common/instructions.ts#L12).\n2. Optionally, make a mapper to prepare a message. See above in [AI message mapping](#ai-message-mapping) section.\n3. Set up a middleware [function](https://github.com/motorro/firebase-ai-chat-project/blob/master/Firebase/functions/src/common/calculator.ts#L137).\n4. Provide mappers and middleware to workers: [OpenAI](https://github.com/motorro/firebase-ai-chat-project/blob/master/Firebase/functions/src/openai/openai.ts#L104), [VertexAI](https://github.com/motorro/firebase-ai-chat-project/blob/master/Firebase/functions/src/vertexai/vertexai.ts#L124).\n\n### Switching in tools reducers\nSince core version 10 the library supports switching in tools reducers. You will get a [ToolsHandOver](core/src/aichat/ToolsDispatcher.ts#L70)\nobject to your reducer. Use it to request `handOver` and reply to AI with some message. The tool run will complete and when\nyou return back from the other assistant, add some summary messages to `handBack` to run the calling assistant again.\nThe example is available in a [sample project](https://github.com/motorro/firebase-ai-chat-project/blob/master/Firebase/functions/src/common/calculator.ts#L53).\n\n## Using multiple engines in a single project\nYou may mix several engines in a single project. For example, you may handle different tasks with different engines.\nTo be able to do it:\n1. Import both engine libraries and (optionally) the core.\n2. Create a common function to resolve chat command schedulers:\n   ```typescript\n   export const commandSchedulers = (queueName: string, taskScheduler: TaskScheduler): ReadonlyArray\u003cCommandScheduler\u003e =\u003e {\n      return [\n          ...openAiFactory(firestore(), getFunctions(), region, undefined, undefined, true, true).createDefaultCommandSchedulers(queueName, taskScheduler),\n          ...vertexAiFactory(firestore(), getFunctions(), region, undefined, undefined, true, true).createDefaultCommandSchedulers(queueName, taskScheduler)\n      ];\n   };\n   ```\n3. Pass the function to the [chat factory function](openai/src/index.ts#L135) and to the [worker factory function](openai/src/index.ts#L171)\n4. [ChatWorker](core/src/aichat/workers/ChatWorker.ts) returns `true` if it dispatches successfully and `false` if not.\n   Use this value to iterate workers and [get the one](https://github.com/motorro/firebase-ai-chat-project/blob/master/Firebase/functions/src/index.ts#L137) supporting the command.\n\n## Client application\nThe sample project includes a sample KMP [Android application](https://github.com/motorro/firebase-ai-chat-project/tree/master/Client)\nThe app uses:\n- [firebase-kotlin-sdk](https://github.com/GitLiveApp/firebase-kotlin-sdk) - a cross-platform Firebase client library\n- [CommonStateMachine](https://github.com/motorro/CommonStateMachine) - to run application logic\n- [An article by Carlos Ugaz](https://medium.com/@carlosgub/how-to-implement-firebase-firestore-in-kotlin-multiplatform-mobile-with-compose-multiplatform-32b66cdba9f7) - how to set up and run a cross-platform firebase app \n\nThe chat interface looks like this:\n![Chat application](readme/app.png)\n\nHere we have:\n\n- A list of chat messages\n- Current data state of the calculator\n\nThe app calls the functions described above to start/complete the chat and subscribes to Firestore collections to watch \nchat updates.\n\nTo listen to the chat state, [subscribe](https://github.com/motorro/firebase-ai-chat-project/blob/master/Client/composeApp/src/commonMain/kotlin/com/motorro/aichat/state/Chat.kt#L63) \nto the chat document provided by function response:\n```kotlin\n// Chat document from the server response\nprivate val chatDocument: DocumentReference = Firebase.firestore.document(documentPath)\n\n// Chat state\nprivate data class ChatStateData(\n  val status: ChatStatus,\n  val data: CalculateChatData,\n  val messages: List\u003cPair\u003cString, ChatMessage\u003e\u003e\n)\n\nprivate var stateData: ChatStateData = ChatStateData(\n  ChatStatus.created,\n  CalculateChatData(0),\n  emptyList()\n)\n\nprivate fun subscribeDocument() {\n    chatDocument.snapshots\n        .onEach { snapshot -\u003e\n            val data: ChatState = snapshot.data()\n            stateData = stateData.copy(\n                status = data.status,\n                data = data.data\n            )\n            render()\n        }\n        .catch {\n            Napier.e(it) { \"Error subscribing to chat\" }\n            setMachineState(factory.chatError(it, chatDocument))\n        }\n        .launchIn(stateScope)\n}\n```\n\nEvery time our Cloud function updates the chat status, the client will get the status and data update.\n\nTo listen to the list of messages - [subscribe](https://github.com/motorro/firebase-ai-chat-project/blob/master/Client/composeApp/src/commonMain/kotlin/com/motorro/aichat/state/Chat.kt#L80) \nto messages sub-collection:\n\n```kotlin\nprivate fun subscribeMessages() {\n    chatDocument.collection(\"messages\")\n        // Add a filter by owner user ID so Firestore security \n        // rules allow you to get the list\n        .where { \"userId\".equalTo(userId) }\n        // Add a sorting direction\n        .orderBy(\"createdAt\", Direction.ASCENDING)\n        .snapshots\n        .onEach { snapshots -\u003e\n            stateData = stateData.copy(\n                messages = snapshots.documents.map { document -\u003e\n                    val data: ChatMessage = document.data()\n                    Pair(document.id, data)\n                }\n            )\n            render()\n        }\n        .catch {\n            Napier.e(it) { \"Error subscribing to chat messages\" }\n            setMachineState(factory.chatError(it, chatDocument.path))\n        }\n        .launchIn(stateScope)\n}\n```\n\nWhenever the server creates a new message the app will display it.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmotorro%2Ffirebase-ai-chat","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmotorro%2Ffirebase-ai-chat","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmotorro%2Ffirebase-ai-chat/lists"}