chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:30:30 +08:00
commit 914fea506e
2793 changed files with 802106 additions and 0 deletions
+201
View File
@@ -0,0 +1,201 @@
.DS_Store
__pycache__/
.idea/
.iml
.vscode/
.venv/
env
bin
lib
# See http://help.github.com/ignore-files/ for more about ignoring files.
# Compiled output
/**/frontend/dist
/**/frontend/tmp
/**/frontend/out-tsc
/**/frontend/bazel-out
# Node
/**/frontend/node_modules
/**/frontend/npm-debug.log
/**/frontend/yarn-error.log
# IDEs and editors
/**/frontend/.idea/
/**/frontend/.project
/**/frontend/.classpath
/**/frontend/.c9/
/**/frontend/*.launch
/**/frontend/.settings/
/**/frontend/*.sublime-workspace
# Visual Studio Code
/**/frontend/.vscode/*
/**/frontend/!.vscode/settings.json
/**/frontend/!.vscode/tasks.json
/**/frontend/!.vscode/launch.json
/**/frontend/!.vscode/extensions.json
/**/frontend/.history/*
# Miscellaneous
/**/frontend/.angular/cache
/**/frontend/.sass-cache/
/**/frontend/connect.lock
/**/frontend/coverage
/**/frontend/libpeerconnection.log
/**/frontend/testem.log
/**/frontend/typings
# System files
/**/frontend/.DS_Store
/**/frontend/Thumbs.db
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
build/*
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
*.whl
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
#ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# Local Configs
config.yaml
# Terraform cache
.terraform/
.terraform.lock.hcl
@@ -0,0 +1,43 @@
# Contributor Code of Conduct
As contributors and maintainers of this project,
and in the interest of fostering an open and welcoming community,
we pledge to respect all people who contribute through reporting issues,
posting feature requests, updating documentation,
submitting pull requests or patches, and other activities.
We are committed to making participation in this project
a harassment-free experience for everyone,
regardless of level of experience, gender, gender identity and expression,
sexual orientation, disability, personal appearance,
body size, race, ethnicity, age, religion, or nationality.
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery
- Personal attacks
- Trolling or insulting/derogatory comments
- Public or private harassment
- Publishing other's private information,
such as physical or electronic
addresses, without explicit permission
- Other unethical or unprofessional conduct.
Project maintainers have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct.
By adopting this Code of Conduct,
project maintainers commit themselves to fairly and consistently
applying these principles to every aspect of managing this project.
Project maintainers who do not follow or enforce the Code of Conduct
may be permanently removed from the project team.
This code of conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community.
Instances of abusive, harassing, or otherwise unacceptable behavior
may be reported by opening an issue
or contacting one or more of the project maintainers.
This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0,
available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/)
@@ -0,0 +1,35 @@
# How to become a contributor and submit your own code
## Contributor License Agreements
We'd love to accept your sample use cases and patches! Before we can take them, we
have to jump a couple of legal hurdles.
Please fill out either the individual or corporate Contributor License Agreement
(CLA).
* If you are an individual writing original source code and you're sure you
own the intellectual property, then you'll need to sign an [individual CLA]
(https://developers.google.com/open-source/cla/individual).
* If you work for a company that wants to allow you to contribute your work,
then you'll need to sign a [corporate CLA]
(https://developers.google.com/open-source/cla/corporate).
Follow either of the two links above to access the appropriate CLA and
instructions for how to sign and return it. Once we receive it, we'll be able to
accept your pull requests.
## Contributing A Patch
1. Submit an issue describing your proposed change to the repo in question.
2. The repo owner will respond to your issue promptly.
3. If your proposed change is accepted, and you haven't already done so, sign a
Contributor License Agreement (see details above).
4. Fork the desired repo, develop and test your code changes.
5. Ensure that your code adheres to the existing style in the sample to which
you are contributing. Refer to the
[Google Cloud Platform Samples Style Guide]
(https://github.com/GoogleCloudPlatform/Template/wiki/style.html) for the
recommended coding standards for this organization.
6. Ensure that your code has an appropriate set of unit tests which all pass.
7. Submit a pull request.
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+342
View File
@@ -0,0 +1,342 @@
# 🚀 Quickbot
![Angular](https://img.shields.io/badge/angular-%23DD0031.svg?style=for-the-badge&logo=angular&logoColor=white)
![FastAPI](https://img.shields.io/badge/FastAPI-005571?style=for-the-badge&logo=fastapi)
![Google Gemini](https://img.shields.io/badge/google%20gemini-8E75B2?style=for-the-badge&logo=google%20gemini&logoColor=white)
![Google Cloud](https://img.shields.io/badge/GoogleCloud-%234285F4.svg?style=for-the-badge&logo=google-cloud&logoColor=white)
[![linting: pylint](https://img.shields.io/badge/linting-pylint-yellowgreen?style=for-the-badge)](https://github.com/pylint-dev/pylint)
[![Code Style: Google](https://img.shields.io/badge/code%20style-google-blueviolet.svg?style=for-the-badge)](https://github.com/google/gts)
![TailwindCSS](https://img.shields.io/badge/tailwindcss-%2338B2AC.svg?style=for-the-badge&logo=tailwind-css&logoColor=white)
Quickbot is an innovative, out-of-the-box solution enabling users to deploy sophisticated AI Agents as full-stack cloud applications on their own Google Cloud Platform (GCP) accounts, entirely without requiring any coding expertise. It empowers you to seamlessly integrate and customize Google's latest AI models and protocols through intuitive templates.
## Our Mission
To democratize Agentic AI application deployment. We provide a seamless, one-click style solution that installs fully functional and visually engaging Agentic AI applications (encompassing both backend and frontend infrastructure, leveraging diverse templates) into your own GCP account. This process is designed to be completed quickly.
Quickbot addresses the challenge users face in independently building and deploying custom AI Agents as full-stack cloud applications within their own GCP environments simply and directly. We believe users want to **own the code** in their GCP accounts and **customize it as needed**.
## Features
* **Preset Templates:** Quickly create custom agents with ready-to-use options.
* **Fast Deployment:** Get sophisticated AI applications running in your GCP account in under 10 minutes.
* **Customizable Codebase:** Full access to the open-source code (Angular frontend, Python FastAPI backend) for direct interaction and modification.
* **Cloud Run Deployment:** Each template can be deployed as an independent application on Google Cloud Run.
* **Standardized Architecture:** Every agent template includes an Angular frontend and a FastAPI Python backend, ensuring a consistent development experience.
* **Google Cloud Authentication:** Uses your default Google Cloud credentials for seamless integration.
* **Scalable Resources:** Deploys necessary Google Cloud resources based on each template's complexity.
* **Latest AI Integration:** Easily leverage Google's cutting-edge AI models and protocols (e.g., Gemini, Imagen, Vertex AI Search (ex Agent Builder)).
* **Diverse Template Ecosystem:** Utilizes a variety of Agentic Templates, including our own and inspirations from others like Agent Garden, A2A, etc.
## Architecture
Quickbot templates follow a standard and straightforward architecture:
* `frontend/`: Contains the Angular application that provides the user interface.
* `backend/`: Contains the FastAPI (Python) application that powers the agent's logic and interacts with Google Cloud services.
This separation allows for clear development workflows and independent scaling of frontend and backend services.
## Available Templates
Quickbot provides a growing set of pre-built templates:
* **[ADK Travel Concierge MultiAgent Template](./multi-agent-travel-concierge-with-adk/):** Leveraging the [ADK](https://google.github.io/adk-docs/) + [Agent Engine](https://cloud.google.com/vertex-ai/generative-ai/docs/agent-engine/overview) capabilities, and taking advantage from the usage of [Agent Garden](https://console.cloud.google.com/vertex-ai/agents/agent-garden), this [Travel MultiAgent](https://github.com/google/adk-samples/tree/main/python/agents/travel-concierge) orchestrates personalized travel experiences and provides support throughout the users journey, from initial planning to real-time itinerary alerts.
**Here's a look at our Travel Concierge MultiAgent in action!**
<video controls autoplay loop muted width="100%" style="max-width: 1200px;">
<source src="./multi-agent-travel-concierge-with-adk/assets/quickbot-adk-travel-multiagent.mp4" type="video/mp4">
Your browser does not support the video tag. You can <a href="./multi-agent-travel-concierge-with-adk/assets/quickbot-adk-travel-multiagent.mp4">download the video here</a>.
</video>
* **[Image Generation Template](./image-generation-template/):** A custom AI Agent that integrates with <a href="https://deepmind.google/models/imagen/" target="_blank" class="underline font-bold">Imagen 4</a>, <a href="https://ai.google.dev/gemini-api/docs/imagen?hl=es-419" target="_blank" class="underline font-bold">Imagen 3</a> and <a href="https://cloud.google.com/vertex-ai/generative-ai/docs/models/gemini/2-0-flash?hl=es-419" target="_blank" class="underline font-bold">Gemini 2.0</a> for text-to-image generation.
* **[LinkedIn Profile Image Generation Template](./linkedin-profile-image-generation-template/):** A specialized AI Agent leveraging Imagen's latest features for image editing and recognition to help you create customized professional corporate profile photos.
* **[Background Changer Image Generation Template](./background-changer-image-generation-template/):** An AI Agent using Imagen's image editing capabilities to customize your product, graphic, car, or pet images, generating professional catalog-style photos with new backgrounds.
* **[Document Search Template](./document-search-template/):** An AI Agent that provides answers based on documents and information you provide beforehand. Integrated with Vertex AI Search (ex Agent Builder), Cloud Storage, and Cloud Run.
* **[Website Search Template](./website-search-template/):** An advanced AI Agent that learns from your website. Provide a URL, and it crawls your domain, extracts information, converts it to vectors stored in Vertex AI, and then generates answers based on a custom prompt, the vector database, and user questions.
* **[Single Playbook Template](./single-playbook-template/):** Generates an AI Agent that detects specific intents (keywords) in user questions and responds with a corresponding specialized Chatbot, providing a tailored experience.
* **[Multi Playbook Template](./multi-playbook-template/):** An evolution of the Single Playbook, this agent detects intents and routes to one of several specialized Chatbots, offering a more nuanced and personalized experience for diverse user inquiries.
## Getting Started
Follow these general steps to get a Quickbot template running in your local environment.
### Prerequisites
1. **Google Cloud Project:** You'll need a GCP project with billing enabled.
2. **Google Cloud SDK (`gcloud`):** [Install and initialize the gcloud CLI](https://cloud.google.com/sdk/docs/install).
3. **Node.js and npm:** We recommend using [nvm (Node Version Manager)](https://github.com/nvm-sh/nvm) to manage Node.js versions.
* Install nvm, then install a compatible Node.js version (e.g., v18.x or later).
* Install Angular CLI: `npm install -g @angular/cli@18` (or the version specified by the template).
4. **Python:** Python 3.8+ and `pip` are required.
* `sudo apt install python3 python3-pip` (Linux example)
5. **Git:** For cloning the repository.
6. **(Optional) Docker and docker-compose:** Useful for containerized development and deployment.
### Option 1: Using Docker Compose (Recommended for Quick Start)
This is the **simplest way to get the entire application (frontend and backend) up and running!** You just need to run `docker compose up` after initial setup. See the next steps:
1. **Ensure Docker and Docker Compose v2 are installed and running.**
2. **Authenticate with Google Cloud:**
Your template (agents with Agent Engine, Imagen 4, VertexAI Search, etc.) will need to interact with Google Cloud services, so you need to provide Google Cloud credentials. For local development with ADC:
```bash
gcloud auth application-default login
gcloud config set project <your-gcp-project-id> # If using a specific GCP project
gcloud auth application-default set-quota-project <your-gcp-project-id> # If using a specific GCP project
# Verify your configuration
gcloud auth list
gcloud config list project
```
The `docker-compose.yml` file is configured to mount these local credentials into the backend container.
> **IMPORTANT!!** Update the `_PROJECT_ID` and `GCLOUD_PROJECT` in `docker-compose.yml` so it points to your project!
> **Windows Users:** The path to ADC might differ. Adjust volume mounts in `docker-compose.yml` if needed.
> **Note:** Ensure any required APIs are enabled in your Google Cloud project if used.
3. **Run the application:**
Build the Docker images for the frontend and backend services, and start the services, all with one simple command:
```bash
docker compose up
```
The backend will be configured using environment variables (see "Environment Variables" section), including any necessary API keys for travel services, ADK configurations, or Agent Engine settings.
The frontend should typically be available at `http://localhost:4200` (or as configured) and the backend API at `http://localhost:8080`.
### Option 2: Manual Setup (for Development and Customization)
Follow these steps if you prefer to run the frontend and backend services manually on your local machine.
**A. Backend Setup**
1. **Navigate to the `backend/` directory.**
```bash
cd backend
```
2. **Create a virtual environment and install dependencies:**
```bash
# Check if you are already in an environment
pip -V
# If not, create and activate (for Linux/macOS)
python3 -m venv .venv
source .venv/bin/activate
# Install requirements
pip3 install -r requirements.txt
```
> **VS Code Tip:** If VS Code doesn't recognize your virtual environment, press `Ctrl + Shift + P` (or `Cmd + Shift + P` on Mac), type "Python: Select Interpreter", choose "Enter interpreter path...", and then find and select `.venv/bin/python` inside your `backend` directory.
3. **Setup Google Cloud (`gcloud`) credentials (if applicable):**
If your backend, agents, or Agent Engine interact with GCP, ensure you're authenticated.
```bash
gcloud auth login # Login with your user account
gcloud config set project <your-gcp-project-id> # If using a specific GCP project
# For services using Application Default Credentials (ADC) locally
gcloud auth application-default login
gcloud auth application-default set-quota-project <your-gcp-project-id> # If using a specific GCP project
# Verify configuration
gcloud auth list
gcloud config list project
```
4. **Configure Environment Variables:**
Backend configuration is managed via environment variables. Create a `.local.env` file in the `backend/` directory (copy from `.local.env.example` if one exists). This file should be in `.gitignore`.
* **For Mac/Windows (or zsh console on Linux):**
Source the variables directly (from the `backend/` directory):
```bash
. ./.local.env
```
* **For Linux (bash):**
Open `backend/.venv/bin/activate` and append the `export` commands from your `backend/.local.env` file after the `PATH` export section. For example:
```sh
# ... existing activate script content ...
_OLD_VIRTUAL_PATH="$PATH"
PATH="$VIRTUAL_ENV/bin:$PATH"
export PATH
# Quickbot env variables (copied from .local.env)
export ENVIRONMENT="development"
export FRONTEND_URL="http://localhost:4200"
# ADK, Agent Engine, and Travel API variables
# export ADK_CONFIG_PATH="/path/to/adk_config.json"
# export AGENT_ENGINE_ENDPOINT="http://localhost:xxxx/api/agent-engine" # Or other config
# export FLIGHT_API_KEY="your_flight_api_key"
# export HOTEL_API_KEY="your_hotel_api_key"
# export WEATHER_API_KEY="your_weather_api_key"
# ... other necessary agent or backend variables ...
```
Verify the variables are set by running `env` in your activated terminal.
5. **Run the setup script (if applicable):**
This script might perform initial configurations for the ADK, Agent Engine, or agent registration.
```bash
# from the backend/ directory
python3 setup.py
```
6. **Run the backend application:**
```bash
# from the backend/ directory
uvicorn main:app --reload --port 8080
```
**B. Frontend Setup**
(These instructions assume a typical TypeScript/Angular frontend. Adjust as necessary based on your `frontend/README.md`.)
1. **Navigate to the `frontend/` directory.**
```bash
cd frontend
```
2. **Install dependencies:**
```bash
npm install
```
3. **Environment Variables (if applicable):**
The frontend might require its own environment configuration (e.g., via a `.env` file or Angular's `environment.ts` files for API endpoints). Check the `frontend/` directory or its `README.md` for specific instructions.
4. **Run the frontend application:**
```bash
npm start
# Or, for many Angular projects:
# ng serve
```
The application will typically be available at `http://localhost:4200`.
## Authentication
Quickbot templates can be integrated with **Firebase Authentication** for user sign-up, sign-in, and management. Configuration details for Firebase are typically found in the frontend's `src/environments/environment.ts` file. You can use existing Firebase projects or set up new ones to connect with your deployed agents.
## Code Styling & Commit Guidelines
To maintain code quality and consistency:
* **TypeScript (Frontend):** We follow [Angular Coding Style Guide](https://angular.dev/style-guide) by leveraging the use of [Google's TypeScript Style Guide](https://github.com/google/gts) using `gts`. This includes a formatter, linter, and automatic code fixer.
* **Python (Backend):** We adhere to the [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html), using tools like `pylint` and `black` for linting and formatting.
* **Commit Messages:** We suggest following [Angular's Commit Message Guidelines](https://github.com/angular/angular/blob/main/contributing-docs/commit-message-guidelines.md) to create clear and descriptive commit messages.
#### Frontend (TypeScript with `gts`)
1. **Initialize `gts` (if not already done in the project):**
Navigate to the `frontend/` directory and run:
```bash
npx gts init
```
This will set up `gts` and create necessary configuration files (like `tsconfig.json`). Ensure your `tsconfig.json` (or a related gts config file like `.gtsrc`) includes an extension for `gts` defaults, typically:
```json
{
"extends": "./node_modules/gts/tsconfig-google.json",
// ... other configurations
}
```
2. **Check for linting issues:**
```bash
npm run lint
```
(This assumes a `lint` script is defined in `package.json`, e.g., `"lint": "gts lint"`)
3. **Fix linting issues automatically (where possible):**
```bash
npm run fix
```
(This assumes a `fix` script is defined in `package.json`, e.g., `"fix": "gts fix"`)
#### Backend (Python with `pylint` and `black`)
1. **Ensure Dependencies are Installed:**
Add `pylint` and `black` to your `backend/requirements.txt` file:
```
pylint
black
```
Then install them within your virtual environment:
```bash
pip install pylint black
# or pip install -r requirements.txt
```
2. **Configure `pylint`:**
It's recommended to have a `.pylintrc` file in your `backend/` directory to configure `pylint` rules. You might need to copy a standard one or generate one (`pylint --generate-rcfile > .pylintrc`).
3. **Check for linting issues with `pylint`:**
Navigate to the `backend/` directory and run:
```bash
pylint .
```
(Or specify modules/packages: `pylint your_module_name`)
4. **Format code with `black`:**
To automatically format all Python files in the current directory and subdirectories:
```bash
python -m black . --line-length=80
```
## Contributing
We welcome contributions to Quickbot! Whether it's new templates, features, bug fixes, or documentation improvements, your help is valued.
### Prerequisites for Contributing
* A **GitHub Account**.
* **2-Factor Authentication (2FA)** enabled on your GitHub account.
* Familiarity with the "Getting Started" section to set up your development environment.
### Branching Model
We follow the [Git Flow](https://nvie.com/posts/a-successful-git-branching-model/) branching model. Please create feature branches from `dev` and submit pull requests back to `dev`.
For more detailed contribution guidelines, please refer to the `CONTRIBUTING.md` file.
## Feedback
* **Found an issue or have a suggestion?** Please [raise an issue](https://github.com/GoogleCloudPlatform/generative-ai/issues) on our GitHub repository.
* **Share your experience!** We'd love to hear about how you're using Quickbot or any success stories. Feel free to reach out to us at quick-bot-team@google.com or discuss in the GitHub discussions.
## Contributors
[Robby Singh](mailto:robbysingh@google.com): Project Lead
[Mauro Cominotti](mailto:maurocominotti@google.com): Project Manager | Tech Lead
[Manuel Correa Freisztav](mailto:manucf@google.com): AI Engineer
[Tomasz Świtoń](mailto:switon@google.com): AI Engineer
[San Srinivasan](mailto:sansrinivasan@google.com): Infra Engineer
[Agnieszka Kołkiewicz](mailto:akolkiewicz@google.com): Product Manager
# Relevant Terms of Service
[Google Cloud Platform TOS](https://cloud.google.com/terms)
[Google Cloud Privacy Notice](https://cloud.google.com/terms/cloud-privacy-notice)
# Responsible Use
Building and deploying generative AI agents requires a commitment to responsible development practices. Quickbot provides to you the tools to build agents, but you must also provide the commitment to ethical and fair use of these agents. We encourage you to:
* **Start with a Risk Assessment:** Before deploying your agent, identify potential risks related to bias, privacy, safety, and accuracy.
* **Implement Monitoring and Evaluation:** Continuously monitor your agent's performance and gather user feedback.
* **Iterate and Improve:** Use monitoring data and user feedback to identify areas for improvement and update your agent's prompts and configuration.
* **Stay Informed:** The field of AI ethics is constantly evolving. Stay up-to-date on best practices and emerging guidelines.
* **Document Your Process:** Maintain detailed records of your development process, including data sources, models, configurations, and mitigation strategies.
# Disclaimer
**This is not an officially supported Google product.**
Copyright 2025 Google LLC. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+13
View File
@@ -0,0 +1,13 @@
# Releases
## v0.1.0 (Initial Release) - May 8, 2025
### Features
First major release
➡️ 🚀
### Fixes
### Dependencies
@@ -0,0 +1,12 @@
node_modules
npm-debug.log
tests
coverage
Dockerfile
.dockerignore
.env
*.md
.git
.vscode
.gitignore
Makefile
@@ -0,0 +1,297 @@
# Quickbot - Conversational App with Multiple Playbooks using Custom RAG & Agent Builder
Quickbot Conversational App Template is a powerful conversational application. It features a user-friendly frontend interface and a robust backend API that leverages a **custom Google Cloud Function for its Retrieval Augmented Generation (RAG) capabilities**. This works alongside Google Cloud's Agent Builder to manage conversational flows across **multiple, configurable playbooks**.
## Overview
This project allows users to interact with a conversational agent. The backend uses a custom Google Cloud Function (e.g., `process-query-for-rag`) for advanced information retrieval and generation. This RAG capability is integrated into conversational flows managed by a Vertex AI Conversation agent, which can orchestrate interactions across various playbooks. This README guides you through setting up the necessary cloud infrastructure (primarily the RAG Cloud Function) and then running the frontend, backend, and configuring the Agent Builder components.
## Demo
Watch how the Multi Playbook Template routes to specialized chatbots:
![Multi Playbook Demo](./assets/quickbot-multi-playbook.png)
## Prerequisites
Before you begin, ensure you have the following installed:
* **Docker and Docker Compose v2:** Essential for the containerized deployment.
* Verify your Docker Compose version with `docker compose version`. If you have an older `docker-compose` (with a hyphen), you might need to upgrade to use `docker compose` in the commands.
* **Google Cloud SDK (`gcloud` CLI):** Required for manual backend setup, authentication, and interaction with Google Cloud services.
* **Python 3.x:** For backend development (if not using Docker).
* **Node.js and npm (or yarn):** For frontend development (if not using Docker).
## Getting Started
You have two main options to get the application running:
### Option 1: Using Docker Compose (Recommended for Quick Start)
This is the **simplest way to get the entire application (frontend and backend) up and running!** You just need to run `docker compose up` and you will be all set! But in order to do that, you may need to authenticate with gcloud. See the next steps:
1. **Ensure Docker and Docker Compose v2 are installed and running.**
2. **Authenticate with Google Cloud for Agent/Playbook Creation:**
If you intend for the application (specifically the backend running in Docker) to create or interact with Google Cloud Agents and Playbooks, you need to provide Google Cloud credentials to the Docker container. The recommended way for local development is using Application Default Credentials (ADC).
Run the following commands in your local terminal:
```bash
gcloud auth application-default login
gcloud config set project <your-project-id>
gcloud auth application-default set-quota-project <your-project-id>
# Verify your configuration
gcloud auth list
gcloud config list project
```
This will create or update ADC on your local machine. The `docker-compose.yml` file is typically configured to mount these local credentials into the backend container, allowing it to authenticate.
> **Windows Users:** The path to ADC might differ on Windows. You may need to adjust the commented-out volume mount paths for `gcloud` credentials in the `docker-compose.yml` file to ensure the backend container can access them.
3. **Build Docker Images (with Agent & Playbook Setup Choice):**
The Docker setup includes a build-time argument `IS_FIRST_DEPLOYMENT` to facilitate initial Google Cloud Vertex AI Conversation agent and playbook creation.
* **Automatic Default Setup (with Sample Playbook):**
If you're running this for the first time and want a quick start with a sample playbook (and have authenticated as per step 2), build the Docker images with the `IS_FIRST_DEPLOYMENT` argument set to `"True"`:
```bash
docker compose build --build-arg IS_FIRST_DEPLOYMENT="True"
```
This configuration will automatically create a default agent and a sample playbook in your Google Cloud project. This sample playbook will provide a basic conversational flow to demonstrate the application's capabilities.
* **Custom Setup or Existing Agent/Playbook:**
If you do not set `IS_FIRST_DEPLOYMENT="True"` during the build (or set it to any other value, or omit it), the application will not attempt to create the default agent and playbook. Instead, you'll need to:
* Configure the backend with environment variables to connect to an existing agent/playbook.
* Provide specific environment variables that instruct the backend on how to create a custom agent/playbook if desired.
Refer to the "Environment Variables" section for backend configuration details. If you are using this path, you can build without the extra argument:
```bash
docker compose build
```
4. **Run the application:**
After building the images (with or without the `IS_FIRST_DEPLOYMENT` arg as per your choice), start the services:
```bash
docker compose up
```
The frontend should typically be available at `http://localhost:4200` (or as configured) and the backend API at `http://localhost:8080`.
### Option 2: Manual Setup (for Development and Customization)
Follow these steps if you prefer to run the frontend and backend services manually on your local machine.
**A. Backend Setup**
1. **Navigate to the `backend/` directory.**
```bash
cd backend
```
2. **Create a virtual environment and install dependencies:**
```bash
# Check if you are already in an environment
pip -V
# If not, create and activate (for Linux/macOS)
python3 -m venv .venv
source .venv/bin/activate
# Install requirements
pip3 install -r requirements.txt
```
> **VS Code Tip:** If VS Code doesn't recognize your virtual environment, press `Ctrl + Shift + P` (or `Cmd + Shift + P` on Mac), type "Python: Select Interpreter", choose "Enter interpreter path...", and then find and select `.venv/bin/python` inside your `backend` directory.
3. **Setup Google Cloud (`gcloud`) credentials:**
Ensure you're authenticated and your project is configured correctly.
```bash
gcloud auth login # Login with your user account
gcloud config set project <your-project-id>
# For services using Application Default Credentials (ADC) locally
gcloud auth application-default login
# Optionally, set a quota project for ADC if not inherited
gcloud auth application-default set-quota-project <your-project-id>
# Verify configuration
gcloud auth list
gcloud config list project
```
4. **Configure Environment Variables:**
Backend configuration is managed via environment variables. Create a `.local.env` file in the `backend/` directory (you can copy from `.local.env.example` if one exists). This file should be added to `.gitignore`.
* **For Mac/Windows (or zsh console on Linux):**
Source the variables directly (from the `backend/` directory):
```bash
. ./.local.env
```
* **For Linux (bash):**
Open `backend/.venv/bin/activate` and append the `export` commands from your `backend/.local.env` file after the `PATH` export section. For example:
```sh
# ... existing activate script content ...
_OLD_VIRTUAL_PATH="$PATH"
PATH="$VIRTUAL_ENV/bin:$PATH"
export PATH
# Quickbot env variables (copied from .local.env)
export ENVIRONMENT="development"
export FRONTEND_URL="http://localhost:4200"
export GCP_PROJECT_ID="<your-project-id>"
export LOCATION_ID="global" # Or your specific region
export AGENT_ID="<your-agent-id>" # Previously DATA_STORE_ID or similar
export PLAYBOOK_ID="<your-playbook-id>" # Previously ENGINE_ID or similar
export BIG_QUERY_DATASET="your_bq_dataset_for_conversation_analytics" # Updated purpose
# ... other necessary variables for agent, playbook, etc. ...
```
Verify the variables are set by running `env` in your activated terminal.
5. **Run the setup script (if applicable):**
This script might perform initial configurations like agent or playbook setup.
```bash
# from the backend/ directory
python3 setup.py
```
6. **Run the backend application:**
```bash
# from the backend/ directory
uvicorn main:app --reload --port 8080
```
**B. Frontend Setup**
(These instructions assume a typical TypeScript/Angular frontend. Adjust as necessary based on your `frontend/README.md`.)
1. **Navigate to the `frontend/` directory.**
```bash
cd frontend
```
2. **Install dependencies:**
```bash
npm install
```
3. **Environment Variables (if applicable):**
The frontend might require its own environment configuration (e.g., via a `.env` file or Angular's `environment.ts` files for API endpoints). Check the `frontend/` directory or its `README.md` for specific instructions.
4. **Run the frontend application:**
```bash
npm start
# Or, for many Angular projects:
# ng serve
```
The application will typically be available at `http://localhost:4200`.
## Project Structure (highlighting important parts)
```text
conversational-app-multi-playbook/
├── backend/ # Python backend (FastAPI/Uvicorn)
│ ├── .venv/ # Python virtual environment (gitignored)
│ ├── .local.env # Local environment variables (gitignored)
│ ├── main.py # Main application file
│ ├── requirements.txt # Backend dependencies
│ ├── setup.py # Backend setup script (e.g., for agent/playbook setup)
│ └── README.md # Backend-specific instructions
├── frontend/ # TypeScript frontend (Angular)
│ ├── node_modules/ # Node.js dependencies (gitignored)
│ ├── src/ # Frontend source code
│ ├── package.json # Frontend dependencies and scripts
│ ├── tsconfig.json # TypeScript configuration
│ └── README.md # Frontend-specific instructions
├── functions/ # Source code for Google Cloud Functions
│ └── create-intent/ # Example: Source for the RAG Cloud Function
│ └── ...
├── docker-compose.yml # Docker Compose configuration for all services
└── README.md # This file: Root project README
```
## Environment Variables
Configuration for both frontend and backend is primarily managed through environment variables.
* **Backend:**
* When running manually, backend environment variables are typically defined in `backend/.local.env`.
* When running with Docker, these variables are usually passed into the backend container via the `docker-compose.yml` file (often referencing a `.env` file at the root or `backend/` directory).
* Key variables include:
* `GCP_PROJECT_ID`: Your Google Cloud Project ID.
* `LOCATION_ID`: The Google Cloud region for your Vertex AI Conversation agent (e.g., `global`, `us-central1`).
* `AGENT_ID`: The ID of your Vertex AI Conversation agent.
* `PLAYBOOK_ID`: (If applicable for your setup) The ID of the specific playbook being used. This might also be configured within the agent itself.
* `ENVIRONMENT`: Application environment (e.g., `development`, `production`).
* `FRONTEND_URL`: URL of the frontend application (e.g., `http://localhost:4200`).
* `BIG_QUERY_DATASET`: (If used) Name of the BigQuery dataset for conversation analytics.
* These variables are especially important if not using the `IS_FIRST_DEPLOYMENT="True"` Docker build arg for automatic setup.
* Consult `backend/README.md` or `backend/.local.env.example` for a complete list.
* **Frontend:**
* Frontend environment variables (e.g., API endpoint URLs) are usually managed within the frontend's build system (e.g., Angular's `environment.ts` files or a `.env` file in the `frontend/` directory).
* Consult `frontend/README.md` for specific details.
## Code Styling & Commit Guidelines
To maintain code quality and consistency across the project:
* **TypeScript (Frontend):** We follow the Angular Coding Style Guide by leveraging Google's TypeScript Style Guide using `gts`. This includes a formatter, linter, and automatic code fixer.
* **Python (Backend):** We adhere to the Google Python Style Guide, using tools like `pylint` and `black` for linting and formatting.
* **Commit Messages:** We suggest following Angular's Commit Message Guidelines to create clear and descriptive commit messages.
### Frontend (TypeScript with `gts`)
(Assumes setup within the `frontend/` directory)
1. **Initialize `gts` (if not already done in the project):**
Navigate to `frontend/` and run:
```bash
npx gts init
```
This will set up `gts` and create necessary configuration files (like `tsconfig.json`). Ensure your `tsconfig.json` (or a related `gts` config file like `.gtsrc`) includes an extension for `gts` defaults, typically:
```json
{
"extends": "./node_modules/gts/tsconfig-google.json"
// ... other configurations
}
```
2. **Check for linting issues:**
(This assumes a `lint` script is defined in `frontend/package.json`, e.g., `"lint": "gts lint"`)
```bash
# from frontend/ directory
npm run lint
```
3. **Fix linting issues automatically (where possible):**
(This assumes a `fix` script is defined in `frontend/package.json`, e.g., `"fix": "gts fix"`)
```bash
# from frontend/ directory
npm run fix
```
### Backend (Python with `pylint` and `black`)
(Assumes setup within the `backend/` directory and its virtual environment activated)
1. **Ensure Dependencies are Installed:**
Add `pylint` and `black` to your `backend/requirements.txt` file if not already present:
```
pylint
black
```
Then install them within your virtual environment:
```bash
# from backend/ directory, with .venv activated
pip install pylint black
# or pip install -r requirements.txt
```
2. **Configure `pylint`:**
It's recommended to have a `.pylintrc` file in your `backend/` directory to configure `pylint` rules. You can generate one if it doesn't exist:
```bash
# from backend/ directory
pylint --generate-rcfile > .pylintrc
```
Customize this file according to your project's needs and the Google Python Style Guide.
3. **Check for linting issues with `pylint`:**
Navigate to the `backend/` directory and run:
```bash
# from backend/ directory
pylint .
# Or specify modules/packages: pylint your_module_name
```
4. **Format code with `black`:**
To automatically format all Python files in the `backend/` directory and its subdirectories:
```bash
# from backend/ directory
python -m black . --line-length=80
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

@@ -0,0 +1,20 @@
# This file specifies files that are *not* uploaded to Google Cloud
# using gcloud. It follows the same syntax as .gitignore, with the addition of
# "#!include" directives (which insert the entries of the given .gitignore-style
# file at that point).
#
# For more information, run:
# $ gcloud topic gcloudignore
#
.gcloudignore
# If you would like to upload your .git directory, .gitignore file or files
# from your .gitignore file, remove the corresponding line
# below:
.git
.gitignore
# Python pycache:
__pycache__/
# Ignored by the build system
/setup.cfg
my_env
@@ -0,0 +1,7 @@
__pycache__/
.idea/
.vscode/
.venv/
env
bin
lib
@@ -0,0 +1,29 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM python:3.11
COPY requirements.txt /tmp
RUN pip install -r /tmp/requirements.txt
RUN mkdir -p /code
WORKDIR /code
COPY . /code/
ENV ENVIRONMENT="development"
ENV BIG_QUERY_DATASET=""
ENV FRONTEND_URL=""
ENV FUNCTION_URL=""
ENV TASK_QUEUE_NAME=""
EXPOSE 8080
ENTRYPOINT ["gunicorn", "main:app", "--workers=4", "--worker-class=uvicorn.workers.UvicornWorker", "--timeout=36000", "--bind=0.0.0.0:8080"]
@@ -0,0 +1,86 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# syntax=docker/dockerfile:1.4
FROM python:3.11 AS builder
# Build arguments
ARG IS_FIRST_DEPLOYMENT="False"
ARG GCLOUD_PROJECT=""
ARG ENVIRONMENT="development"
ARG FRONTEND_URL=""
ARG BIG_QUERY_DATASET=""
# Environment variables for the build process
ENV IS_FIRST_DEPLOYMENT=$IS_FIRST_DEPLOYMENT
ENV GCLOUD_PROJECT=$GCLOUD_PROJECT
ENV ENVIRONMENT=$ENVIRONMENT
ENV FRONTEND_URL=$FRONTEND_URL
ENV BIG_QUERY_DATASET=$BIG_QUERY_DATASET
WORKDIR /code
COPY . .
COPY setup.py ./setup.py
RUN pip install --no-cache-dir -r /code/requirements.txt
RUN --mount=type=secret,id=gcp_credentials,target=/tmp/gcp_adc.json,required=true \
if [ "$IS_FIRST_DEPLOYMENT" = "True" ]; then \
echo "--- Running first deployment setup (IS_FIRST_DEPLOYMENT=True) ---"; \
echo "Using GCLOUD_PROJECT=${GCLOUD_PROJECT} for setup"; \
\
if [ ! -f /tmp/gcp_adc.json ]; then \
echo "CRITICAL ERROR: GCP credentials secret was expected but not mounted to /tmp/gcp_adc.json" >&2; \
exit 1; \
fi; \
echo "DEBUG: GCP credentials secret successfully mounted to /tmp/gcp_adc.json"; \
\
# Set GOOGLE_APPLICATION_CREDENTIALS specifically for this RUN command's execution context
export GOOGLE_APPLICATION_CREDENTIALS=/tmp/gcp_adc.json; \
echo "DEBUG: GOOGLE_APPLICATION_CREDENTIALS for setup is set to: $GOOGLE_APPLICATION_CREDENTIALS"; \
\
echo "Executing setup.py ..."; \
# Now setup.py can make requests to GCP with provided credentials
python /code/setup.py || { echo "setup.py with failed!"; exit 1; }; \
echo "--- First deployment setup finished ---"; \
else \
echo "--- IS_FIRST_DEPLOYMENT is False, skipping first deployment setup. ---"; \
fi
FROM python:3.11-slim AS final
ARG GCLOUD_PROJECT=""
ARG GOOGLE_APPLICATION_CREDENTIALS=""
ARG ENVIRONMENT="development"
ARG FRONTEND_URL=""
ARG BIG_QUERY_DATASET=""
# Environment variables for the build process
ENV GCLOUD_PROJECT=$GCLOUD_PROJECT
ENV GOOGLE_APPLICATION_CREDENTIALS="/root/.config/gcloud/application_default_credentials.json"
ENV ENVIRONMENT=$ENVIRONMENT
ENV FRONTEND_URL=$FRONTEND_URL
ENV BIG_QUERY_DATASET=$BIG_QUERY_DATASET
WORKDIR /app
COPY --from=builder /code/requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir -r /app/requirements.txt
COPY --from=builder /code/main.py /app/main.py
COPY --from=builder /code/src /app/src
EXPOSE 8080
ENTRYPOINT ["gunicorn", "main:app", "--workers=4", "--worker-class=uvicorn.workers.UvicornWorker", "--timeout=36000", "--bind=0.0.0.0:8080"]
@@ -0,0 +1,83 @@
## Setting up
Create a virtual environment on the root of the application and activate it.
```
python3 -m venv .venv
source .venv/bin/activate
pip3 install -r requirements.txt
```
### Running the set up script
```
python3 setup.py
```
### Running the application on local
Create a virtual environment, activate it, install the requirements, set environmental variables from local.env and run the application
```
. ./local.env
uvicorn main:app --reload --port 8080
```
## Code Styling & Commit Guidelines
To maintain code quality and consistency:
* **TypeScript (Frontend):** We follow [Angular Coding Style Guide](https://angular.dev/style-guide) by leveraging the use of [Google's TypeScript Style Guide](https://github.com/google/gts) using `gts`. This includes a formatter, linter, and automatic code fixer.
* **Python (Backend):** We adhere to the [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html), using tools like `pylint` and `black` for linting and formatting.
* **Commit Messages:** We suggest following [Angular's Commit Message Guidelines](https://github.com/angular/angular/blob/main/contributing-docs/commit-message-guidelines.md) to create clear and descriptive commit messages.
#### Frontend (TypeScript with `gts`)
1. **Initialize `gts` (if not already done in the project):**
Navigate to the `frontend/` directory and run:
```bash
npx gts init
```
This will set up `gts` and create necessary configuration files (like `tsconfig.json`). Ensure your `tsconfig.json` (or a related gts config file like `.gtsrc`) includes an extension for `gts` defaults, typically:
```json
{
"extends": "./node_modules/gts/tsconfig-google.json",
// ... other configurations
}
```
2. **Check for linting issues:**
```bash
npm run lint
```
(This assumes a `lint` script is defined in `package.json`, e.g., `"lint": "gts lint"`)
3. **Fix linting issues automatically (where possible):**
```bash
npm run fix
```
(This assumes a `fix` script is defined in `package.json`, e.g., `"fix": "gts fix"`)
#### Backend (Python with `pylint` and `black`)
1. **Ensure Dependencies are Installed:**
Add `pylint` and `black` to your `backend/requirements.txt` file:
```
pylint
black
```
Then install them within your virtual environment:
```bash
pip install pylint black
# or pip install -r requirements.txt
```
2. **Configure `pylint`:**
It's recommended to have a `.pylintrc` file in your `backend/` directory to configure `pylint` rules. You might need to copy a standard one or generate one (`pylint --generate-rcfile > .pylintrc`).
3. **Check for linting issues with `pylint`:**
Navigate to the `backend/` directory and run:
```bash
pylint .
```
(Or specify modules/packages: `pylint your_module_name`)
4. **Format code with `black`:**
To automatically format all Python files in the current directory and subdirectories:
```bash
python -m black . --line-length=80
```
@@ -0,0 +1,14 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
@@ -0,0 +1,2 @@
export ENVIRONMENT=development
export FRONTEND_URL=http://localhost:4200
@@ -0,0 +1,109 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Main FastAPI application entry point for the Quick Bot Backend.
This module initializes the FastAPI application, configures CORS based on
the environment, defines root and version endpoints, includes API routers
(e.g., for search functionality), and provides an endpoint for audio
transcription using Google Cloud Speech-to-Text.
"""
from fastapi import FastAPI, File, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from src.controller.chats import router as chat_router
from src.controller.intents import router as intent_router
from src.controller.models import router as model_router
from google.cloud import speech
from os import getenv
app = FastAPI()
def configure_cors(app):
"""Configures CORS middleware based on the environment."""
environment = getenv("ENVIRONMENT")
allowed_origins = []
if environment == "production":
frontend_url = getenv("FRONTEND_URL")
if not frontend_url:
raise ValueError(
"FRONTEND_URL environment variable not set in production"
)
allowed_origins.append(frontend_url)
elif environment == "development":
allowed_origins.append("*") # Allow all origins in development
else:
raise ValueError(
f"""Invalid ENVIRONMENT: {environment}.
Must be 'production' or 'development'"""
)
app.add_middleware(
CORSMiddleware,
allow_origins=allowed_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Create a route to handle GET requests on root
@app.get("/")
async def root():
return "You are calling Quick Bot Backend"
# Create a route to handle GET requests on /version
@app.get("/api/version")
def version():
return "v0.0.1"
@app.post("/api/audio_chat")
async def audio_chat(audio_file: UploadFile = File(...)):
client = speech.SpeechClient()
audio_content = await audio_file.read()
audio = speech.RecognitionAudio(content=audio_content)
config = speech.RecognitionConfig(
language_code="en-US",
sample_rate_hertz=48000,
model="default",
audio_channel_count=1,
enable_word_confidence=True,
enable_word_time_offsets=True,
)
operation = client.long_running_recognize(config=config, audio=audio)
print("Waiting for operation to complete...")
response = operation.result(timeout=90)
print(response)
text = ""
for result in response.results:
print(f"Transcript: {result.alternatives[0].transcript}")
text = result.alternatives[0].transcript
return text, 200
configure_cors(app)
app.include_router(chat_router)
app.include_router(intent_router)
app.include_router(model_router)
@@ -0,0 +1,399 @@
# This Pylint rcfile contains a best-effort configuration to uphold the
# best-practices and style described in the Google Python style guide:
# https://google.github.io/styleguide/pyguide.html
#
# Its canonical open-source location is:
# https://google.github.io/styleguide/pylintrc
[MAIN]
# Files or directories to be skipped. They should be base names, not paths.
ignore=third_party
# Files or directories matching the regex patterns are skipped. The regex
# matches against base names, not paths.
ignore-patterns=
# Pickle collected data for later comparisons.
persistent=no
# List of plugins (as comma separated values of python modules names) to load,
# usually to register additional checkers.
load-plugins=
# Use multiple processes to speed up Pylint.
jobs=4
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
confidence=
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
# it should appear only once). See also the "--disable" option for examples.
#enable=
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once).You can also use "--disable=all" to
# disable everything first and then reenable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W"
disable=R,
abstract-method,
apply-builtin,
arguments-differ,
attribute-defined-outside-init,
backtick,
bad-option-value,
basestring-builtin,
buffer-builtin,
c-extension-no-member,
consider-using-enumerate,
cmp-builtin,
cmp-method,
coerce-builtin,
coerce-method,
delslice-method,
div-method,
eq-without-hash,
execfile-builtin,
file-builtin,
filter-builtin-not-iterating,
fixme,
getslice-method,
global-statement,
hex-method,
idiv-method,
implicit-str-concat,
import-error,
import-self,
import-star-module-level,
input-builtin,
intern-builtin,
invalid-str-codec,
locally-disabled,
long-builtin,
long-suffix,
map-builtin-not-iterating,
misplaced-comparison-constant,
missing-function-docstring,
metaclass-assignment,
next-method-called,
next-method-defined,
no-absolute-import,
no-init, # added
no-member,
no-name-in-module,
no-self-use,
nonzero-method,
oct-method,
old-division,
old-ne-operator,
old-octal-literal,
old-raise-syntax,
parameter-unpacking,
print-statement,
raising-string,
range-builtin-not-iterating,
raw_input-builtin,
rdiv-method,
reduce-builtin,
relative-import,
reload-builtin,
round-builtin,
setslice-method,
signature-differs,
standarderror-builtin,
suppressed-message,
sys-max-int,
trailing-newlines,
unichr-builtin,
unicode-builtin,
unnecessary-pass,
unpacking-in-except,
useless-else-on-loop,
useless-suppression,
using-cmp-argument,
wrong-import-order,
xrange-builtin,
zip-builtin-not-iterating,
[REPORTS]
# Set the output format. Available formats are text, parseable, colorized, msvs
# (visual studio) and html. You can also give a reporter class, eg
# mypackage.mymodule.MyReporterClass.
output-format=text
# Tells whether to display a full report or only the messages
reports=no
# Python expression which should return a note less than 10 (10 is the highest
# note). You have access to the variables errors warning, statement which
# respectively contain the number of errors / warnings messages and the total
# number of statements analyzed. This is used by the global evaluation report
# (RP0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details
#msg-template=
[BASIC]
# Good variable names which should always be accepted, separated by a comma
good-names=main,_
# Bad variable names which should always be refused, separated by a comma
bad-names=
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=
# Include a hint for the correct naming format with invalid-name
include-naming-hint=no
# List of decorators that produce properties, such as abc.abstractproperty. Add
# to this list to register other decorators that produce valid properties.
property-classes=abc.abstractproperty,cached_property.cached_property,cached_property.threaded_cached_property,cached_property.cached_property_with_ttl,cached_property.threaded_cached_property_with_ttl
# Regular expression matching correct function names
function-rgx=^(?:(?P<exempt>setUp|tearDown|setUpModule|tearDownModule)|(?P<camel_case>_?[A-Z][a-zA-Z0-9]*)|(?P<snake_case>_?[a-z][a-z0-9_]*))$
# Regular expression matching correct variable names
variable-rgx=^[a-z][a-z0-9_]*$
# Regular expression matching correct constant names
const-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$
# Regular expression matching correct attribute names
attr-rgx=^_{0,2}[a-z][a-z0-9_]*$
# Regular expression matching correct argument names
argument-rgx=^[a-z][a-z0-9_]*$
# Regular expression matching correct class attribute names
class-attribute-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$
# Regular expression matching correct inline iteration names
inlinevar-rgx=^[a-z][a-z0-9_]*$
# Regular expression matching correct class names
class-rgx=^_?[A-Z][a-zA-Z0-9]*$
# Regular expression matching correct module names
module-rgx=^(_?[a-z][a-z0-9_]*|__init__)$
# Regular expression matching correct method names
method-rgx=(?x)^(?:(?P<exempt>_[a-z0-9_]+__|runTest|setUp|tearDown|setUpTestCase|tearDownTestCase|setupSelf|tearDownClass|setUpClass|(test|assert)_*[A-Z0-9][a-zA-Z0-9_]*|next)|(?P<camel_case>_{0,2}[A-Z][a-zA-Z0-9_]*)|(?P<snake_case>_{0,2}[a-z][a-z0-9_]*))$
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=(__.*__|main|test.*|.*test|.*Test)$
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=12
[TYPECHECK]
# List of decorators that produce context managers, such as
# contextlib.contextmanager. Add to this list to register other decorators that
# produce valid context managers.
contextmanager-decorators=contextlib.contextmanager,contextlib2.contextmanager
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis. It
# supports qualified module names, as well as Unix pattern matching.
ignored-modules=
# List of class names for which member attributes should not be checked (useful
# for classes with dynamically set attributes). This supports the use of
# qualified names.
ignored-classes=optparse.Values,thread._local,_thread._local
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
generated-members=
[FORMAT]
# Maximum number of characters on a single line.
max-line-length=80
# TODO(https://github.com/pylint-dev/pylint/issues/3352): Direct pylint to exempt
# lines made too long by directives to pytype.
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=(?x)(
^\s*(\#\ )?<?https?://\S+>?$|
^\s*(from\s+\S+\s+)?import\s+.+$)
# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=yes
# Maximum number of lines in a module
max-module-lines=99999
# String used as indentation unit. The internal Google style guide mandates 2
# spaces. Google's externaly-published style guide says 4, consistent with
# PEP 8. Here, we use 2 spaces, for conformity with many open-sourced Google
# projects (like TensorFlow).
indent-string=' '
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=TODO
[STRING]
# This flag controls whether inconsistent-quotes generates a warning when the
# character used as a quote delimiter is used inconsistently within a module.
check-quote-consistency=yes
[VARIABLES]
# Tells whether we should check for unused import in __init__ files.
init-import=no
# A regular expression matching the name of dummy variables (i.e. expectedly
# not used).
dummy-variables-rgx=^\*{0,2}(_$|unused_|dummy_)
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid to define new builtins when possible.
additional-builtins=
# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,_cb
# List of qualified module names which can have objects that can redefine
# builtins.
redefining-builtins-modules=six,six.moves,past.builtins,future.builtins,functools
[LOGGING]
# Logging modules to check that the string format arguments are in logging
# function parameter format
logging-modules=logging,absl.logging,tensorflow.io.logging
[SIMILARITIES]
# Minimum lines number of a similarity.
min-similarity-lines=4
# Ignore comments when computing similarities.
ignore-comments=yes
# Ignore docstrings when computing similarities.
ignore-docstrings=yes
# Ignore imports when computing similarities.
ignore-imports=no
[SPELLING]
# Spelling dictionary name. Available dictionaries: none. To make it working
# install python-enchant package.
spelling-dict=
# List of comma separated words that should not be checked.
spelling-ignore-words=
# A path to a file that contains private dictionary; one word per line.
spelling-private-dict-file=
# Tells whether to store unknown words to indicated private dictionary in
# --spelling-private-dict-file option instead of raising a message.
spelling-store-unknown-words=no
[IMPORTS]
# Deprecated modules which should not be used, separated by a comma
deprecated-modules=regsub,
TERMIOS,
Bastion,
rexec,
sets
# Create a graph of every (i.e. internal and external) dependencies in the
# given file (report RP0402 must not be disabled)
import-graph=
# Create a graph of external dependencies in the given file (report RP0402 must
# not be disabled)
ext-import-graph=
# Create a graph of internal dependencies in the given file (report RP0402 must
# not be disabled)
int-import-graph=
# Force import order to recognize a module as part of the standard
# compatibility libraries.
known-standard-library=
# Force import order to recognize a module as part of a third party library.
known-third-party=enchant, absl
# Analyse import fallback blocks. This can be used to support both Python 2 and
# 3 compatible code, which means that the block might have code that exists
# only in one or another interpreter, leading to false positives when analysed.
analyse-fallback-blocks=no
[CLASSES]
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,
__new__,
setUp
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,
_fields,
_replace,
_source,
_make
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls,
class_
# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=mcs
@@ -0,0 +1,29 @@
gunicorn==20.1.0; python_version > '3.0'
gunicorn==19.10.0; python_version < '3.0'
uvicorn~=0.17.0
fastapi~=0.111.1
fastapi-utilities==0.2.0
cloudpickle==2.2.1
grpcio==1.66.2
urllib3==1.26.16
typing-inspect==0.9.0
typing_extensions==4.12.2
pydantic==2.9.2
requests==2.31.0
langchain==0.3.2
langchain-community==0.3.1
langchain-core==0.3.9
langchain-google-vertexai==2.0.4
google-cloud-aiplatform==1.69.0
google-cloud-bigquery==3.26.0
google-cloud-tasks==2.16.5
google-cloud-logging==3.11.2
google-cloud-speech==2.27.0
pylint
black
pytest
pytest-cov
pytest-watch
@@ -0,0 +1,14 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
@@ -0,0 +1,71 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utility functions for setting up Google BigQuery datasets and tables."""
from typing import List
from google.cloud.bigquery import Client as BigQueryClient, Table, SchemaField
bigquery_client = BigQueryClient()
PROJECT_ID = bigquery_client.project
def create_dataset(dataset_name: str):
"""Deletes an existing BigQuery dataset and recreates it.
If the dataset already exists, it and all its contents
will be deleted first.
Then, a new empty dataset with the specified name is created.
Args:
dataset_name: The name for the BigQuery dataset.
"""
bigquery_client.delete_dataset(
dataset_name, delete_contents=True, not_found_ok=True
)
print(f"{dataset_name} Creating dataset...")
bigquery_client.create_dataset(dataset_name)
def create_table(dataset: str, table_name: str, schema: List[SchemaField]):
"""Creates a BigQuery table within a specified dataset.
Args:
dataset: The name of the dataset where the table will be created.
table_name: The name for the new BigQuery table.
schema: A list of SchemaField objects defining the table's structure.
"""
bigquery_client.create_table(
Table(f"{PROJECT_ID}.{dataset}.{table_name}", schema)
)
def insert_intent(dataset: str, table_name: str, values: str):
"""Inserts a single row into the specified BigQuery table using raw values.
Constructs and executes an INSERT SQL query to add a new record.
Args:
dataset: The name of the BigQuery dataset.
table_name: The name of the target BigQuery table (the intents table).
values: A string representing the row values, formatted as required by
BigQuery INSERT syntax (e.g., "'string_val', 123, TRUE").
The caller is responsible for correct formatting, quoting, and
ensuring the order matches the table schema.
"""
bigquery_client.query(
f"""
INSERT INTO `{dataset}.{table_name}` VALUES({values});
"""
).result()
@@ -0,0 +1,53 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utility functions for setting up Google Cloud Storage (GCS) buckets.
This module provides functions to interact with GCS, specifically for
creating buckets needed by the application. It also defines constants
for bucket configuration like name pattern, content type, and location.
"""
from google.cloud.storage import Client as GCSClient
from scripts.big_query_setup import PROJECT_ID
BUCKET = f"quick-bot-{PROJECT_ID}"
CONTENT_TYPE = "text/plain"
LOCATION = "us-central1"
storage_client = GCSClient()
def create_bucket(bucket_name: str):
"""Creates a Google Cloud Storage bucket if it doesn't already exist.
Checks if a bucket with the specified name exists. If not, it creates
a new bucket in the predefined LOCATION.
Args:
bucket_name: The desired name for the GCS bucket.
Returns:
A google.cloud.storage.bucket.Bucket object representing the
existing or newly created bucket.
"""
storage_bucket = storage_client.bucket(bucket_name)
if not storage_bucket.exists():
print(f"{bucket_name} Bucket does not exist, creating it...")
storage_bucket = storage_client.create_bucket(
bucket_name,
location=LOCATION,
)
return storage_bucket
@@ -0,0 +1,87 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
This script automates the setup process for the application's infrastructure.
It performs the following actions:
1. Creates a Google Cloud Storage (GCS) bucket if it doesn't exist.
2. Creates a BigQuery dataset if it doesn't exist.
3. Creates the necessary BigQuery tables (Chats, Embeddings, Intents) within
the dataset, using schemas defined in the corresponding model classes.
Note: Ensure that the required environment variables
(like GOOGLE_APPLICATION_CREDENTIALS) and configurations (like BUCKET
name and BIG_QUERY_DATASET name) are set before running this script.
"""
from scripts.big_query_setup import create_dataset, create_table, insert_intent
from scripts.gcs_setup import create_bucket, BUCKET
from src.model.chats import Chat
from src.model.embedding import Embedding
from src.model.intent import Intent
from src.repository.big_query import CHATS_TABLE, EMBEDDINGS_TABLE
from src.service.intent import INTENTS_TABLE
BIG_QUERY_DATASET = ""
DEFAULT_INTENTS = [
Intent(
name="Code assistant",
ai_model="gemini-1.5-flash",
ai_temperature=1,
description="The Coding AI Agent is your expert programming assistant",
prompt="""
You are an expert coding agent. You can write, analyze, and debug
code in any programming language.
You can also answer questions about code and software development concepts.
If I provide you with code, please identify the language it is written in.
If I need to execute the code, I will explicitly ask you to do so.
I want you to prioritize generating concise and efficient code.
Please avoid adding unnecessary comments or explanations unless I specifically
request them
""",
questions=[
"Write a Python function to calculate the factorial of a number",
"""What is the difference between a stack and a queue
in data structures?""",
"""Create a simple HTML page with a button that changes
color when clicked.""",
],
status="5",
),
]
print("Setting up GCS... \n")
bucket = create_bucket(BUCKET)
print("\nSuccess!\n")
print("Setting up BigQuery... \n")
create_dataset(BIG_QUERY_DATASET)
create_table(BIG_QUERY_DATASET, CHATS_TABLE, Chat.__schema__())
create_table(BIG_QUERY_DATASET, EMBEDDINGS_TABLE, Embedding.__schema__())
create_table(BIG_QUERY_DATASET, INTENTS_TABLE, Intent.__schema__())
for intent in DEFAULT_INTENTS:
try:
insert_intent(
BIG_QUERY_DATASET, INTENTS_TABLE, intent.to_insert_string()
)
except Exception as e:
print(e)
print("\nSuccess!\n")
@@ -0,0 +1,14 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
@@ -0,0 +1,14 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
@@ -0,0 +1,65 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from uuid import uuid4
from fastapi import APIRouter, BackgroundTasks, Response
from src.model.chats import CreateChatRequest, Chat
from src.service.intent import IntentService
from src.service.intent_matching import IntentMatchingService
from src.service.chats import ChatsService
from src.service.vertex_ai import VertexAIService
router = APIRouter(
prefix="/api/chats",
tags=["chats"],
responses={404: {"description": "Not found"}},
)
@router.post("")
async def chat(
item: CreateChatRequest,
response: Response,
background_tasks: BackgroundTasks,
):
intents = IntentService().get_all()
intent_matching_service = IntentMatchingService(intents)
intent = intent_matching_service.get_intent_from_query(item.text)
suggestedQuestion = intent_matching_service.get_suggested_questions(
item.text, intent
)
model_response = VertexAIService(intents).generate_text_from_model(
item.text,
intent,
)
final_response = Chat(
id=str(uuid4()),
question=item.text,
answer=model_response,
intent=intent.name,
suggested_questions=suggestedQuestion,
)
background_tasks.add_task(
ChatsService().insert_chat,
final_response,
)
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
response.headers["Pragma"] = "no-cache"
response.headers["Expires"] = "0"
return final_response
@@ -0,0 +1,96 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from fastapi import APIRouter, HTTPException
from src.model.http_status import BadRequest
from src.model.intent import CreateIntentRequest, Intent
from src.model.event import IntentCreateEvent
from src.repository.task import TaskRepository
from src.service.index_endpoint import IndexEndpointService
from src.service.intent import IntentService
router = APIRouter(
prefix="/api/intents",
tags=["intents"],
responses={404: {"description": "Not found"}},
)
@router.get("")
async def get_intents():
service = IntentService()
intents = service.get_all()
for intent in intents:
if not intent.is_active():
index_endpoint_service = IndexEndpointService()
if index_endpoint_service.endpoint_has_deployed_indexes(
intent.get_standard_name()
):
intent.status = "5"
service.update(intent.name, intent)
return intents
@router.post("")
async def create_intent(intent: CreateIntentRequest):
intent_service = IntentService()
index_endpoint_service = IndexEndpointService()
task_repository = TaskRepository()
saved_intent = None
index_endpoint = None
try:
saved_intent = intent_service.create(intent.to_intent())
if intent.gcp_bucket:
index_endpoint = index_endpoint_service.create_endpoint(
saved_intent.get_standard_name()
)
task_repository.create(
IntentCreateEvent(
intent_name=intent.name,
index_endpoint_resource=index_endpoint.resource_name,
),
)
except BadRequest as e:
raise HTTPException(status_code=400, detail=e.detail)
except Exception as e:
print(e)
if saved_intent:
intent_service.delete(saved_intent.name)
if index_endpoint:
index_endpoint_service.delete_endpoint(index_endpoint)
return saved_intent
@router.delete("/{intent_name}")
async def delete_intent(intent_name: str):
service = IntentService()
intent = service.get(intent_name)
if intent.gcp_bucket:
index_endpoint_service = IndexEndpointService()
endpoint = index_endpoint_service.get_endpoint(
intent.get_standard_name()
)
index_endpoint_service.delete_endpoint(endpoint)
service.delete(intent_name)
return
@router.put("/{intent_name}")
async def update_intent(intent_name: str, intent: Intent):
service = IntentService()
return service.update(intent_name, intent)
@@ -0,0 +1,29 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from fastapi import APIRouter
from src.service.models import ModelService
router = APIRouter(
prefix="/api/models",
tags=["models"],
responses={404: {"description": "Not found"}},
)
@router.get("")
async def get_models():
service = ModelService()
return service.get_all()
@@ -0,0 +1,14 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
@@ -0,0 +1,54 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from google.cloud.bigquery import SchemaField
from pydantic import BaseModel
from typing import List, Optional
class Chat(BaseModel):
id: str
question: str
answer: str
intent: str
suggested_questions: List[str]
timestamp: Optional[str] = None
@classmethod
def __schema__(cls) -> List[SchemaField]:
return [
SchemaField("id", "STRING", mode="REQUIRED"),
SchemaField("question", "STRING", mode="REQUIRED"),
SchemaField("answer", "STRING", mode="REQUIRED"),
SchemaField("intent", "STRING", mode="REQUIRED"),
SchemaField("suggested_questions", "STRING", mode="REPEATED"),
SchemaField("timestamp", "TIMESTAMP", mode="REQUIRED"),
]
def to_dict(self):
return {
"id": self.id,
"question": self.question,
"answer": self.answer,
"intent": self.intent,
"suggested_questions": self.suggested_questions,
"timestamp": self.timestamp,
}
def to_insert_string(self):
return f'"{self.id}", """{self.question}""", """{self.answer}""", "{self.intent}", {str(self.suggested_questions)}, CURRENT_TIMESTAMP()'
class CreateChatRequest(BaseModel):
text: str
@@ -0,0 +1,47 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from google.cloud.bigquery import SchemaField
from pydantic import BaseModel
from typing import List, Optional
class Embedding(BaseModel):
id: str
text: str
index: str
author: str
timestamp: Optional[str] = None
@classmethod
def __schema__(cls) -> List[SchemaField]:
return [
SchemaField("id", "STRING", mode="REQUIRED"),
SchemaField("text", "STRING", mode="REQUIRED"),
SchemaField("index", "STRING", mode="REQUIRED"),
SchemaField("author", "STRING", mode="REQUIRED"),
SchemaField("timestamp", "TIMESTAMP", mode="REQUIRED"),
]
def to_dict(self):
return {
"id": self.id,
"text": self.text,
"index": self.index,
"author": self.author,
"timestamp": self.timestamp,
}
def to_insert_string(self):
return f'"{self.id}", """{self.text}""", "{self.index}", "{self.author}", CURRENT_TIMESTAMP()'
@@ -0,0 +1,54 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Defines data models for events used within the application.
These models represent specific occurrences or messages, often used for
asynchronous processing via task queues or pub/sub systems.
"""
from pydantic import BaseModel
class IntentCreateEvent(BaseModel):
"""Represents the data required to trigger the processing of a new intent.
This event is typically created after an intent configuration is saved
and is used to initiate background tasks like creating vector search indexes
or processing associated data.
Attributes:
intent_name: The unique name of the intent being processed.
index_endpoint_resource: The full resource name of the Vertex AI
Matching Engine Index Endpoint associated
with this intent.
"""
intent_name: str
index_endpoint_resource: str
def to_dict(self):
"""Serializes the event data into a dictionary format.
Useful for converting the event object into a format suitable for
JSON serialization, often needed for task queue payloads
or API responses.
Returns:
A dictionary representation of the IntentCreateEvent instance.
"""
return {
"intent_name": self.intent_name,
"index_endpoint_resource": self.index_endpoint_resource,
}
@@ -0,0 +1,54 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Defines custom HTTP exception classes for the FastAPI application.
This module provides reusable exception classes that inherit from FastAPI's
HTTPException, allowing for standardized error responses with specific
status codes and detail messages.
"""
from fastapi import HTTPException
class ResourceAlreadyExists(HTTPException):
"""
Custom exception for HTTP 400 Bad Request errors.
Used specifically when an attempt is made to create a resource that
already exists (e.g., creating a duplicate configuration).
Defaults to status code 400.
"""
def __init__(self, detail="Resource already exists"):
"""Initializes the exception with a default detail message."""
super().__init__(status_code=400, detail=detail)
class BadRequest(HTTPException):
"""
Custom exception for general HTTP 400 Bad Request errors.
Can be used for various client-side errors like invalid input,
missing required data, or violating business logic rules before
processing. Defaults to status code 400.
Note: The default detail message "Resource already exists" seems
inconsistent with a general BadRequest. Consider changing it to
a more generic message like "Bad Request" or requiring a specific
detail message upon instantiation.
"""
def __init__(self, detail="Resource already exists"):
super().__init__(status_code=400, detail=detail)
@@ -0,0 +1,107 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from google.cloud.bigquery import SchemaField, Row
from pydantic import BaseModel
from typing import List
class Intent(BaseModel):
name: str
ai_model: str
ai_temperature: float
description: str
prompt: str
questions: List[str]
status: str
gcp_bucket: str = ""
@classmethod
def __schema__(cls) -> List[SchemaField]:
return [
SchemaField("name", "STRING", mode="REQUIRED"),
SchemaField("ai_model", "STRING", mode="REQUIRED"),
SchemaField("ai_temperature", "NUMERIC", mode="REQUIRED"),
SchemaField("description", "STRING", mode="REQUIRED"),
SchemaField("prompt", "STRING", mode="REQUIRED"),
SchemaField("questions", "STRING", mode="REPEATED"),
SchemaField("status", "STRING", mode="REQUIRED"),
SchemaField("gcp_bucket", "STRING", mode="REQUIRED"),
]
@classmethod
def __from_row__(cls, row: Row):
return cls(
name=row["name"],
ai_model=row["ai_model"],
ai_temperature=float(row["ai_temperature"]) if row["ai_temperature"] is not None else 0.0, # Ensure conversion from Decimal/Numeric
description=row["description"],
prompt=row["prompt"],
questions=list(row["questions"]) if row["questions"] is not None else [], # Ensure it's a list
status=row["status"],
gcp_bucket=row["gcp_bucket"],
)
def to_dict(self):
return {
"name": self.name,
"ai_model": self.ai_model,
"description": self.description,
"prompt": self.prompt,
"questions": self.questions,
"status": self.status,
"gcp_bucket": self.gcp_bucket,
}
def to_insert_string(self):
return f'"{self.name}", "{self.ai_model}", {self.ai_temperature},"{self.description}","""{self.prompt}""", {str(self.questions)}, "{self.status}", "{self.gcp_bucket}"'
def is_active(self) -> bool:
return self.status == "5"
def get_standard_name(self) -> str:
return self.name.lower().replace(" ", "-").replace("_", "-")
class CreateIntentRequest(BaseModel):
name: str
gcp_bucket: str = ""
ai_model: str
ai_temperature: float
description: str
prompt: str
questions: List[str]
def to_dict(self):
return {
"name": self.name,
"gcp_bucket": self.gcp_bucket,
"ai_model": self.ai_model,
"ai_temperature": self.ai_temperature,
"description": self.description,
"prompt": self.prompt,
"questions": self.questions,
}
def to_intent(self) -> Intent:
return Intent(
name=self.name,
ai_model=self.ai_model,
ai_temperature=self.ai_temperature,
description=self.description,
prompt=self.prompt,
questions=self.questions,
status="1",
gcp_bucket=self.gcp_bucket,
)
@@ -0,0 +1,14 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
@@ -0,0 +1,325 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Provides a repository class for interacting with Google BigQuery.
This module defines the `BigQueryRepository` class, which encapsulates
common BigQuery operations like running queries, fetching rows by ID,
inserting, updating, and deleting rows. It relies on the 'BIG_QUERY_DATASET'
environment variable to target the correct dataset.
"""
import datetime
from decimal import Decimal
from os import getenv
from typing import Dict, List, Any, Optional, Tuple
from google.cloud.bigquery import (
Client,
QueryJobConfig,
ScalarQueryParameter,
ArrayQueryParameter,
TableReference,
DatasetReference
)
from google.cloud.bigquery.table import RowIterator
from google.cloud.exceptions import GoogleCloudError
_BIG_QUERY_DATASET_ENV = getenv("BIG_QUERY_DATASET")
if not _BIG_QUERY_DATASET_ENV:
raise ValueError(
"The BIG_QUERY_DATASET environment variable is not set. "
"This is required for the BigQueryRepository to function."
)
BIG_QUERY_DATASET: str = _BIG_QUERY_DATASET_ENV
CHATS_TABLE = 'chats'
CHATS_ID_COLUMN = "id"
EMBEDDINGS_TABLE = "embeddings"
EMBEDDINGS_ID_COLUMN = "id"
EMBEDDINGS_TEXT_COLUMN = "text"
EMBEDDINGS_INDEX_COLUMN = "index"
class BigQueryRepository:
"""
A repository class for simplifying interactions with Google BigQuery,
using parameterized queries to prevent SQL injection.
"""
def __init__(self):
"""Initializes the BigQuery client and project ID."""
self.client: Client = Client()
self.project_id: str = self.client.project
self.dataset_id: str = BIG_QUERY_DATASET
def _get_table_ref(self, table_id: str) -> TableReference:
"""Helper to get a TableReference object."""
dataset_ref = DatasetReference(self.project_id, self.dataset_id)
return TableReference(dataset_ref, table_id)
def _validate_column_name(self, column_name: str):
"""Basic validation for column names."""
if not column_name.replace("_", "").isalnum():
raise ValueError(f"Invalid column name: {column_name}")
def _create_scalar_query_parameter(self, name: str, value: Any) -> ScalarQueryParameter:
"""
Creates a ScalarQueryParameter by inferring the BigQuery type from the value.
"""
if isinstance(value, bool):
return ScalarQueryParameter(name, "BOOL", value)
if isinstance(value, int):
return ScalarQueryParameter(name, "INT64", value)
if isinstance(value, float):
return ScalarQueryParameter(name, "FLOAT64", value)
if isinstance(value, str):
return ScalarQueryParameter(name, "STRING", value)
if isinstance(value, datetime.datetime):
return ScalarQueryParameter(name, "TIMESTAMP", value)
if isinstance(value, datetime.date):
return ScalarQueryParameter(name, "DATE", value)
if isinstance(value, Decimal):
return ScalarQueryParameter(name, "NUMERIC", value)
if value is None:
# For None, the type of the column in BQ will ultimately determine how NULL is handled.
# Using STRING type for the parameter is a common safe default.
return ScalarQueryParameter(name, "STRING", None) # Default to STRING for None
# Fallback: attempt to convert other types to string.
try:
str_value = str(value)
return ScalarQueryParameter(name, "STRING", str_value)
except Exception as e:
raise TypeError(
f"Value for param '{name}' of type {type(value)} "
f"could not be converted to a supported BigQuery scalar type: {e}"
)
def run_query(self, query: str, job_config: Optional[QueryJobConfig] = None) -> RowIterator:
"""
Executes a BigQuery SQL query and returns the results.
Args:
query: The SQL query string to execute.
job_config: Optional QueryJobConfig for parameterized queries.
Returns:
A RowIterator object to iterate over the query results.
Raises:
google.cloud.exceptions.GoogleCloudError: If the query fails.
"""
try:
query_job = self.client.query(query, job_config=job_config)
return query_job.result()
except GoogleCloudError as e:
print(f"BigQuery Error: {e}")
print(f"Query: {query}")
if job_config and job_config.query_parameters:
print(f"Parameters: {job_config.query_parameters}")
raise
def get_row_by_id(self, table_id: str, id_column: str, id_value: Any, id_value_type: str = "STRING") -> RowIterator:
"""
Retrieves a single row from a table based on its ID using parameterized query.
Args:
table_id: The ID of the table (without dataset prefix).
id_column: The name of the column containing the ID.
id_value: The specific ID value to search for.
id_value_type: The BigQuery type of the ID value (e.g., "STRING", "INT64").
Returns:
A RowIterator containing the matching row(s).
"""
self._validate_column_name(id_column)
query = f"""
SELECT * FROM `{self.project_id}.{self.dataset_id}.{table_id}`
WHERE `{id_column}` = @id_value;
"""
job_config = QueryJobConfig(
query_parameters=[
ScalarQueryParameter("id_value", id_value_type, id_value)
]
)
return self.run_query(query, job_config=job_config)
def insert_rows_json(self, table_id: str, rows: List[Dict[str, Any]]) -> List[Dict]:
"""
Inserts multiple rows into the specified table using JSON.
This is the recommended method for insertions due to its safety and efficiency.
Args:
table_id: The ID of the table.
rows: A list of dictionaries, where each dictionary represents a row
(column names as keys, values as row data).
Returns:
A list of error dictionaries if any rows failed to insert,
otherwise an empty list.
"""
table_ref = self._get_table_ref(table_id)
errors_from_client = self.client.insert_rows_json(table_ref, rows)
errors_list: List[Dict[str, Any]] = list(errors_from_client) # type: ignore
if errors_list:
print(f"Errors inserting rows into {table_id}: {errors_list}")
return errors_list
def insert_row(self, table_id: str, column_names: List[str], values: Tuple[Any, ...]) -> None:
"""
Inserts a new row into the specified table using a parameterized query.
Prefer `insert_rows_json` for multiple rows or complex data.
Args:
table_id: The ID of the table.
column_names: A list of column names for the insert.
values: A tuple of values corresponding to the column_names.
Raises:
ValueError: If column names are invalid or counts don't match.
TypeError: If a value in `values` cannot be mapped to a BigQuery scalar type.
"""
if not column_names or not values:
raise ValueError("Column names and values cannot be empty.")
if len(column_names) != len(values):
raise ValueError("Number of column names must match number of values.")
for col_name in column_names:
self._validate_column_name(col_name)
cols_str = ", ".join(f"`{col}`" for col in column_names)
placeholders = ", ".join([f"@param{i}" for i in range(len(values))])
query_params: List[ScalarQueryParameter] = []
for i, current_value in enumerate(values):
param_name = f"param{i}"
query_params.append(self._create_scalar_query_parameter(param_name, current_value))
query = f"""
INSERT INTO `{self.project_id}.{self.dataset_id}.{table_id}` ({cols_str})
VALUES ({placeholders});
"""
job_config = QueryJobConfig(query_parameters=query_params)
self.run_query(query, job_config=job_config)
def delete_multiple_rows_by_id(
self, table_id: str, id_column: str, ids: List[Any], id_value_type: str = "STRING"
) -> None:
"""
Deletes multiple rows from a table based on a list of IDs using parameterized query.
Args:
table_id: The ID of the table.
id_column: The name of the column containing the IDs.
ids: A list of ID values to delete.
id_value_type: The BigQuery type of the ID values.
"""
if not ids:
return # Nothing to delete
self._validate_column_name(id_column)
query = f"""
DELETE FROM `{self.project_id}.{self.dataset_id}.{table_id}`
WHERE `{id_column}` IN UNNEST(@ids);
"""
job_config = QueryJobConfig(
query_parameters=[
ArrayQueryParameter("ids", id_value_type, ids)
]
)
self.run_query(query, job_config=job_config)
def update_row_by_id(
self,
table_id: str,
id_column: str,
id_value: Any,
column_values: Dict[str, Any],
id_value_type: str = "STRING",
) -> None:
"""
Updates specific columns of a row identified by its ID using parameterized query.
Args:
table_id: The ID of the table.
id_column: The name of the column containing the ID.
id_value: The specific ID value of the row to update.
column_values: A dictionary where keys are column names and
values are the new values.
id_value_type: The BigQuery type of the ID value.
"""
if not column_values:
return
self._validate_column_name(id_column)
set_clauses = []
query_params_list: List[ScalarQueryParameter] = [
ScalarQueryParameter("id_value", id_value_type, id_value)
]
param_idx = 0
for col, current_value in column_values.items():
self._validate_column_name(col)
update_param_name = f"update_val_{param_idx}"
set_clauses.append(f"`{col}` = @{update_param_name}")
query_params_list.append(self._create_scalar_query_parameter(update_param_name, current_value))
param_idx += 1
if not set_clauses: # Should not happen if column_values is not empty
return
sets_str = ", ".join(set_clauses)
query = f"""
UPDATE `{self.project_id}.{self.dataset_id}.{table_id}`
SET {sets_str}
WHERE `{id_column}` = @id_value;
"""
job_config = QueryJobConfig(query_parameters=query_params_list)
self.run_query(query, job_config=job_config)
def get_all_rows(self, table_id: str) -> RowIterator:
"""
Retrieves all rows from the specified table.
Args:
table_id: The ID of the table.
Returns:
A RowIterator containing all rows in the table.
"""
query = f"SELECT * FROM `{self.project_id}.{self.dataset_id}.{table_id}`"
return self.run_query(query)
def delete_row_by_id(self, table_id: str, id_column: str, id_value: Any, id_value_type: str = "STRING") -> None:
"""
Deletes a single row from a table based on its ID using parameterized query.
Args:
table_id: The ID of the table.
id_column: The name of the column containing the ID.
id_value: The specific ID value of the row to delete.
id_value_type: The BigQuery type of the ID value.
"""
self._validate_column_name(id_column)
query = f"""
DELETE FROM `{self.project_id}.{self.dataset_id}.{table_id}`
WHERE `{id_column}` = @id_value;
"""
job_config = QueryJobConfig(
query_parameters=[
ScalarQueryParameter("id_value", id_value_type, id_value)
]
)
self.run_query(query, job_config=job_config)
@@ -0,0 +1,71 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Provides a repository class for interacting with Google Cloud Storage (GCS).
This module defines constants related to GCS bucket structure
and includes the CloudStorageRepository class, which encapsulates operations
for interacting with GCS objects, such as listing files within a specific path.
"""
from typing import List
from google.cloud.storage import Client, Blob
BUCKET = "quick-bot"
INTENT_FOLDER = "intents"
EMBEDDINGS_FILE = "embeddings.json"
EMBEDDINGS_FOLDER = "embeddings"
CONTENT_TYPE = "text/plain"
class CloudStorageRepository:
"""A repository class for performing operations on Google Cloud Storage.
Provides methods to interact with GCS buckets and objects, such as listing
files within a specified path.
Attributes:
client: An instance of the google.cloud.storage.Client.
"""
def __init__(self):
"""Initializes the CloudStorageRepository with a GCS client."""
self.client = Client()
def list(self, full_path: str) -> List[Blob]:
"""Lists all blobs (files) within a specified GCS path.
Parses the bucket name and prefix from the full GCS path
(e.g., "gs://your-bucket-name/path/to/files/").
Args:
full_path: The full GCS path (starting with "gs://") from which
to list blobs.
Returns:
A list of google.cloud.storage.blob.Blob objects found under
the specified prefix within the bucket. Returns an empty list
if the path is invalid or no blobs are found.
Raises:
ValueError: If the full_path format is invalid (e.g., doesn't
contain enough parts after splitting by '/').
google.cloud.exceptions.NotFound: If the bucket does not exist.
google.api_core.exceptions.GoogleAPICallError: For other API errors.
"""
bucket = full_path.split("/")[2]
prefix = full_path.replace(f"gs://{bucket}/", "")
return list(self.client.list_blobs(bucket, prefix=prefix))
@@ -0,0 +1,120 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Provides a repository class for interacting with Google Cloud Tasks.
Handles the creation of tasks in a specified Google Cloud Tasks queue.
It retrieves necessary configuration like queue name, target URL, and project ID
from environment variables and Google Cloud authentication defaults.
Requires the following environment variables to be set:
- TASK_QUEUE_NAME: The name of the Cloud Tasks queue.
- FUNCTION_URL: The URL of the HTTP target (e.g., a Cloud Function)
that the task will invoke.
"""
from google.cloud.tasks_v2 import CloudTasksClient, HttpMethod
import google.auth
from src.model.event import IntentCreateEvent
from json import dumps
from os import getenv
LOCATION = "us-central1"
INTENT_CREATION_QUEUE = getenv("TASK_QUEUE_NAME")
URL = getenv("FUNCTION_URL")
def get_project_id():
"""Retrieves the default Google Cloud project ID using
application default credentials.
Returns:
The Google Cloud project ID as a string if found, otherwise None.
Raises:
google.auth.exceptions.DefaultCredentialsError: If default credentials
are not found or configured correctly.
"""
try:
_, project_id = google.auth.default()
return project_id
except google.auth.exceptions.DefaultCredentialsError as e:
print(f"Error: {e}")
return None
class TaskRepository:
"""Repository for managing interactions with Google Cloud Tasks.
Currently supports creating tasks to trigger intent creation processing.
Attributes:
client: An instance of the google.cloud.tasks_v2.CloudTasksClient.
project_id: The determined Google Cloud project ID.
"""
def __init__(self):
"""Initializes the TaskRepository.
Creates a CloudTasksClient instance and retrieves the project ID.
Raises:
RuntimeError: If the Google Cloud project ID cannot be determined.
"""
self.client = CloudTasksClient()
def create(self, event: IntentCreateEvent):
"""Creates a Cloud Task to handle an IntentCreateEvent.
Constructs an HTTP task targeting the configured FUNCTION_URL and
enqueues it in the specified INTENT_CREATION_QUEUE. The event data
is sent as the JSON body of the POST request.
Args:
event: The IntentCreateEvent data to be sent in the task payload.
Returns:
The created google.cloud.tasks_v2.types.Task object.
Raises:
google.api_core.exceptions.GoogleAPICallError: If there's an issue
communicating with the Cloud Tasks API (e.g., queue not found,
permission errors).
ValueError: If required configuration (queue, URL) is missing.
(Raised during __init__)
RuntimeError: If the project ID could not be determined.
(Raised during __init__)
"""
# Construct the fully qualified queue name.
project_id = get_project_id()
parent = self.client.queue_path(
project_id, LOCATION, INTENT_CREATION_QUEUE
)
task = {
"http_request": {
"http_method": HttpMethod.POST,
"url": URL,
"headers": {"Content-type": "application/json"},
}
}
converted_payload = dumps(event.to_dict()).encode()
task["http_request"]["body"] = converted_payload
response = self.client.create_task(
request={"parent": parent, "task": task}
)
print(f"Created task {response.name}")
return response
@@ -0,0 +1,14 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
@@ -0,0 +1,68 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Service layer for managing chat interactions.
This module provides the ChatsService class, which handles the logic
for persisting chat records (questions and answers) to the database.
It interacts primarily with the BigQuery repository.
"""
import datetime
from src.repository.big_query import (
BigQueryRepository,
CHATS_TABLE,
)
from src.model.chats import Chat
class ChatsService:
"""Handles business logic related to chat history.
This service interacts with the BigQuery repository to save
chat conversation records.
Attributes:
repository: An instance of BigQueryRepository for database operations.
"""
def __init__(self):
"""Initializes the ChatsService with a BigQueryRepository."""
self.repository = BigQueryRepository()
def insert_chat(self, chat: Chat):
"""Inserts a single chat record into the database.
Args:
chat: The Chat object containing the conversation details to save.
"""
schema_fields = Chat.__schema__()
column_names = [field.name for field in schema_fields]
# Prepare values as a tuple, matching the order of schema_fields
# If chat.timestamp is None, we'll insert the current UTC time.
# BigQueryRepository will handle the Python datetime object.
timestamp_value = chat.timestamp if chat.timestamp else datetime.datetime.now(datetime.timezone.utc)
# Ensure the order here matches the order in Chat.__schema__
values_tuple = (
chat.id,
chat.question,
chat.answer,
chat.intent,
chat.suggested_questions,
timestamp_value
)
self.repository.insert_row(CHATS_TABLE, column_names, values_tuple)
@@ -0,0 +1,73 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Service layer for managing text embeddings.
This module provides the EmbeddingService class, which handles the logic
for creating embedding records in the database. It interacts primarily
with the BigQuery repository.
"""
from src.model.embedding import Embedding
from src.repository.big_query import BigQueryRepository
from typing import List
EMBEDDINGS_TABLE = "embeddings"
INTENTS_TABLE_ID_COLUMN = "id"
class EmbeddingService:
"""Handles business logic related to text embeddings.
This service interacts with the BigQuery repository to persist
embedding data.
Attributes:
repository: An instance of BigQueryRepository for database operations.
"""
def __init__(self):
"""Initializes the EmbeddingService with a BigQueryRepository."""
self.repository = BigQueryRepository()
def create(self, embedding: Embedding) -> Embedding:
"""Creates a single embedding record in the database.
Args:
embedding: The Embedding object to create.
Returns:
The created Embedding object.
"""
schema_fields = Embedding.__schema__()
column_names = [field.name for field in schema_fields]
# Prepare values as a tuple, matching the order of schema_fields
# Ensure the order here matches the order in SearchApplication.__schema__
values_tuple = (embedding.id, embedding.text, embedding.index, embedding.author)
self.repository.insert_row(EMBEDDINGS_TABLE, column_names, values_tuple)
return embedding
def create_all(self, embeddings: List[Embedding]):
"""Creates multiple embedding records in the database.
Iterates through the list and calls the `create` method
for each embedding.
Args:
embeddings: A list of Embedding objects to create.
"""
for embedding in embeddings:
self.create(embedding)
@@ -0,0 +1,65 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import List
from google.cloud.aiplatform import MatchingEngineIndexEndpoint
from src.repository.big_query import BigQueryRepository
from src.service.vertex_ai import EMBEDDINGS_MODEL
BIG_QUERY_ID_COLUMN = "id"
BIG_QUERY_INDEX_COLUMN = "index"
BIG_QUERY_TEXT_COLUMN = "text"
BIG_QUERY_AUTHOR_COLUMN = "author"
BIG_QUERY_TIMESTAMP_COLUMN = "timestamp"
INDEX_DIMENSIONS = 768
INDEX_DISTANCE_MEASURE = "DOT_PRODUCT_DISTANCE"
INDEX_NEIGHBORS_COUNT = 150
class IndexEndpointService:
def __init__(self):
self.bq_repository = BigQueryRepository()
def get_endpoint(self, name: str) -> MatchingEngineIndexEndpoint:
index_endpoints = MatchingEngineIndexEndpoint.list(
filter=f'display_name="{name}"',
)
if index_endpoints:
return index_endpoints[0]
else:
raise Exception(
f"Matching Engine Index Endpoint with name {name} doesn't exist"
)
def create_endpoint(self, name: str) -> MatchingEngineIndexEndpoint:
return MatchingEngineIndexEndpoint.create(
display_name=name,
description=name,
public_endpoint_enabled=True,
)
def delete_endpoint(
self, endpoint: MatchingEngineIndexEndpoint
) -> MatchingEngineIndexEndpoint:
MatchingEngineIndexEndpoint.delete(endpoint, force=True)
def create_embeddings(self, chunk: str) -> List[float]:
return EMBEDDINGS_MODEL.embed_query(chunk)
def endpoint_has_deployed_indexes(self, name: str) -> bool:
index_endpoint = self.get_endpoint(name)
return len(index_endpoint.deployed_indexes) > 0
@@ -0,0 +1,164 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Service layer for managing chatbot intents.
This module provides the IntentService class, which encapsulates the
business logic for creating, retrieving, updating, and deleting intents.
It interacts with BigQuery for persistent storage and Google Cloud Storage
to validate data sources for certain intent types.
"""
from src.model.http_status import BadRequest
from src.model.intent import Intent
from src.repository.big_query import BigQueryRepository
from src.repository.cloud_storage import CloudStorageRepository
from typing import List
INTENTS_TABLE = "intents"
INTENTS_TABLE_ID_COLUMN = "name"
class IntentService:
"""Handles business logic related to chatbot intents.
This service interacts with BigQuery and Cloud Storage repositories
to manage intent data.
Attributes:
repository: An instance of BigQueryRepository for database operations.
gcs_repository: An instance of CloudStorageRepository for GCS
operations.
"""
def __init__(self):
"""Initializes the IntentService with necessary repositories."""
self.repository = BigQueryRepository()
self.gcs_repository = CloudStorageRepository()
def get(self, intent_name: str) -> Intent | None:
"""Retrieves a single intent by its name.
Args:
intent_name: The unique name of the intent to retrieve.
Returns:
An Intent object if found, otherwise None.
"""
intent = None
# Use get_row_by_id for safer, parameterized query
results = self.repository.get_row_by_id(
table_id=INTENTS_TABLE,
id_column=INTENTS_TABLE_ID_COLUMN,
id_value=intent_name,
id_value_type="STRING"
)
# get_row_by_id returns a RowIterator. Expect 0 or 1 row for a unique ID.
row_list = list(results)
if row_list:
intent = Intent.__from_row__(row_list[0])
return intent
def get_all(self) -> List[Intent]:
"""Retrieves all intents from the database.
Returns:
A list of Intent objects. Returns an empty list if no
intents are found.
"""
intents = []
results = self.repository.get_all_rows(INTENTS_TABLE)
for row in list(results):
intent = Intent.__from_row__(row)
intents.append(intent)
return intents
def create(self, intent: Intent) -> Intent:
"""Creates a new intent in the database.
Validates that an intent with the same name doesn't already exist.
If the intent uses a GCS bucket (`gcp_bucket` is set), it verifies
that the bucket path contains data. Sets status to '5' ('Active'
or 'Ready') if no GCS bucket is specified.
Args:
intent: The Intent object to create.
Returns:
The created Intent object.
Raises:
BadRequest: If an intent with the same name already exists, or if
a specified GCS bucket path is empty or invalid.
google.cloud.exceptions.NotFound: If the GCS bucket specified
in the intent does not exist.
google.api_core.exceptions.GoogleAPICallError: For other
GCS API errors.
"""
if self.get(intent.name):
raise BadRequest(detail=f"Intent with name {intent.name} already exists")
if intent.gcp_bucket and not self.gcs_repository.list(intent.gcp_bucket):
raise BadRequest(detail=f"No data found on {intent.gcp_bucket}, please, add your pdf files in the proper location.")
if not intent.gcp_bucket:
intent.status = "5" # Mark as active if no bucket (no indexing needed)
intent_data = intent.to_dict()
column_names = list(intent_data.keys())
values_tuple = tuple(intent_data.values())
# Call the updated insert_row method
self.repository.insert_row(INTENTS_TABLE, column_names, values_tuple)
return intent
def update(self, intent_name: str, intent: Intent) -> None:
"""Updates an existing intent by its name.
Constructs a dictionary of fields to update and passes it to the
repository's update method.
Args:
intent_name: The name of the intent to update.
intent: An Intent object containing the updated values. Note that
only specific fields (ai_model, ai_temperature, prompt,
questions, status) are currently updated by this method.
Returns:
None
"""
# These are the fields intended to be updatable based on the previous implementation.
# Ensure that the 'intent' Pydantic model instance (`intent`) provides these attributes.
column_values_to_update = {
'ai_model': intent.ai_model,
'ai_temperature': intent.ai_temperature,
'prompt': intent.prompt,
'questions': intent.questions,
'status': intent.status,
'gcp_bucket': intent.gcp_bucket,
}
self.repository.update_row_by_id(
table_id=INTENTS_TABLE,
id_column=INTENTS_TABLE_ID_COLUMN,
id_value=intent_name,
column_values=column_values_to_update
)
def delete(self, intent_name: str):
self.repository.delete_row_by_id(
table_id=INTENTS_TABLE,
id_column=INTENTS_TABLE_ID_COLUMN,
id_value=intent_name,
id_value_type="STRING"
)
@@ -0,0 +1,57 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from numpy import argsort
from langchain.utils.math import cosine_similarity
from src.model.intent import Intent
from src.service.vertex_ai import EMBEDDINGS_MODEL
from typing import List
class IntentMatchingService:
def __init__(self, intents: List[Intent]):
self.intents_map = {}
self.questions_embeddings = {}
for intent in intents:
self.intents_map[intent.name] = intent
self.questions_embeddings[intent.name] = (
EMBEDDINGS_MODEL.embed_documents(intent.questions)
)
def get_intent_from_query(self, query: str) -> Intent:
query_embeddings = EMBEDDINGS_MODEL.embed_query(query)
m = 0
intent = None
for intent_name, questions in self.questions_embeddings.items():
similarity = max(
cosine_similarity([query_embeddings], questions)[0]
)
if m < similarity:
m = similarity
intent = self.intents_map[intent_name]
return intent
def get_suggested_questions(self, query: str, intent: Intent) -> List[str]:
questions = intent.questions
query_embeddings = EMBEDDINGS_MODEL.embed_query(query)
similarity = cosine_similarity(
[query_embeddings], self.questions_embeddings[intent.name]
)[0]
suggested_questions = []
for ix in argsort(similarity)[-3:][::-1]:
suggested_questions.append(questions[ix])
return suggested_questions[1:]
@@ -0,0 +1,65 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Service layer for retrieving available AI models.
This module provides the ModelService class, which lists both standard
Google AI models (like Gemini variants) and custom models deployed
within the user's Google Cloud project via Vertex AI Platform.
"""
from google.cloud.aiplatform import Model
GOOGLE_AI_MODELS = [
"gemini-1.5-flash",
"gemini-1.5-flash-8b",
"gemini-1.5-pro",
"gemini-1.0-pro",
]
class ModelService:
"""Provides functionality to list available AI models."""
def get_all(self):
"""Retrieves a list of available AI models, categorized by source.
Fetches custom models deployed on Vertex AI Platform and combines them
with a predefined list of standard Google AI models.
Returns:
A list of dictionaries, where each dictionary represents a model
category ("Gemini" or "Custom") and contains a list of model names
under the "models" key.
Example:
[
{"name": "Gemini", "models": ["gemini-1.5-pro", ...]},
{"name": "Custom", "models": ["my-custom-model-1", ...]}
]
Raises:
google.api_core.exceptions.GoogleAPICallError: If there's an issue
communicating with the Vertex AI API to list custom models.
"""
custom_models = Model.list()
return [
{
"name": "Gemini",
"models": GOOGLE_AI_MODELS,
},
{
"name": "Custom",
"models": [cm.display_name for cm in custom_models],
},
]
@@ -0,0 +1,238 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from langchain_google_vertexai import VertexAIEmbeddings
from vertexai.preview.generative_models import GenerativeModel
from google.cloud import bigquery
from google.cloud.aiplatform import (
MatchingEngineIndexEndpoint,
MatchingEngineIndex,
)
from typing import List, MutableSequence
from src.model.intent import Intent
from src.repository.big_query import BIG_QUERY_DATASET, EMBEDDINGS_TABLE
from google.cloud.aiplatform_v1 import (
FindNeighborsRequest,
IndexDatapoint,
MatchServiceClient,
FindNeighborsResponse,
)
from src.utils.utils import generate_hash, intents_to_json
MATCHING_ENGINE_INDEX_NEIGHBORS = 5
TEXT_EMBEDDING_MODEL = "textembedding-gecko@003"
EMBEDDINGS_MODEL = VertexAIEmbeddings(TEXT_EMBEDDING_MODEL)
INTENTS_HASH = ""
INDEX_ENDPOINTS = {}
INDEX_ENDPOINTS_CLIENTS = {}
class VertexAIService:
def __init__(self, intents: List[Intent]):
global INTENTS_HASH
self.client = bigquery.Client()
new_hash = generate_hash(intents_to_json(intents))
if INTENTS_HASH != new_hash:
for intent in intents:
if not intent.gcp_bucket:
continue
ixs = MatchingEngineIndexEndpoint.list(
filter=f'display_name="{intent.get_standard_name()}"',
)
if len(ixs) == 0:
print(
f"Intent {intent.name} is not yet initialized."
"Please run load_indexes.py to create an index"
"for the intent"
)
continue
INDEX_ENDPOINTS[intent.get_standard_name()] = ixs[0]
# Configure Vector Search client
INDEX_ENDPOINTS_CLIENTS[intent.get_standard_name()] = (
MatchServiceClient(
client_options={
"api_endpoint": ixs[0].public_endpoint_domain_name,
},
)
)
INTENTS_HASH = new_hash
def vector_search_query(
self,
index_endpoint: MatchingEngineIndexEndpoint,
match_service_client: MatchServiceClient,
question: str,
) -> MutableSequence[FindNeighborsResponse.Neighbor]:
# Build FindNeighborsRequest object
datapoint = IndexDatapoint(
feature_vector=EMBEDDINGS_MODEL.embed_query(question)
)
query = FindNeighborsRequest.Query(
datapoint=datapoint, neighbor_count=3
)
return (
match_service_client.find_neighbors(
FindNeighborsRequest(
index_endpoint=index_endpoint.resource_name,
deployed_index_id=index_endpoint.deployed_indexes[0].id,
# Request can have multiple queries
queries=[query],
return_full_datapoint=False,
)
)
.nearest_neighbors[0]
.neighbors
)
def get_index_name(
self, index_endpoint: MatchingEngineIndexEndpoint
) -> str:
return MatchingEngineIndex(
index_endpoint.deployed_indexes[0].index
).display_name
def get_text_results_from_bigquery(
self, chunk_ids: List[str], index_name: str
):
if not chunk_ids:
return []
query = f"""
SELECT text, id
FROM `{BIG_QUERY_DATASET}.{EMBEDDINGS_TABLE}`
WHERE id IN {str(chunk_ids).replace("[", "(").replace("]", ")")}
AND index = "{index_name}";
"""
rows = self.client.query(query).result()
texts = []
for row in rows:
texts.append(row[0])
return texts
def generate_llm_response(
self,
model: GenerativeModel,
prompt: str,
chunked_context,
question: str,
temperature: float,
) -> str:
response_list = []
context = ""
for ix, data in enumerate(chunked_context):
context += f"Context {ix + 1}: {data} \n"
formatted_prompt = f"{prompt} \n {context} Question: {question}"
responses = model.generate_content(
formatted_prompt,
generation_config={
"temperature": temperature,
"max_output_tokens": 8192,
"top_p": 0.95,
},
safety_settings=[],
stream=True,
)
for response in responses:
if response.candidates[0].content.parts:
response_list.append(response.text)
return "".join(response_list)
def generate_out_of_context_response(
self, model: GenerativeModel, question: str
) -> str:
response_list = []
prompt = f"""
You are a friendly conversational bot called. Whenever you are asked a question follow this instructions.
1. Reply that you don't have an answer for the following question given your knowledge and the context provided in the question.
2. Ask the user to provide feedback via de UI or to try and rephrase the question.
3. Apologize and state that you will keep on learning in the future.
Question: {question}
"""
responses = model.generate_content(
prompt,
generation_config={
"temperature": 1,
"max_output_tokens": 8192,
"top_p": 0.95,
},
safety_settings=[],
stream=True,
)
for response in responses:
if response.candidates[0].content.parts:
response_list.append(response.text)
return "".join(response_list)
def generate_text_from_model(
self,
query: str,
intent: Intent,
) -> str:
"""
Given a user query, and an inferred intent
we make a similarity search to VectorSearch,
retrieve the chunked text from bigquery and
finaly generate an LLM response out of it
@param query: The user's query
@param intent: The user's inferred intent
@return LLM response: The LLM generated response
"""
model = GenerativeModel(intent.ai_model)
if intent.gcp_bucket:
index_endpoint = INDEX_ENDPOINTS[intent.get_standard_name()]
match_engine_client = INDEX_ENDPOINTS_CLIENTS[
intent.get_standard_name()
]
similarity_results = self.vector_search_query(
index_endpoint,
match_engine_client,
query,
)
if similarity_results:
context = self.get_text_results_from_bigquery(
[res.datapoint.datapoint_id for res in similarity_results],
self.get_index_name(index_endpoint),
)
return self.generate_llm_response(
model,
intent.prompt,
context,
query,
intent.ai_temperature,
)
else:
return self.generate_out_of_context_response(model, query)
else:
return self.generate_llm_response(
model,
intent.prompt,
[],
query,
1,
)
@@ -0,0 +1,30 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from hashlib import sha256
from json import dumps
from src.model.intent import Intent
from typing import List
def intents_to_json(intents: List[Intent]):
"""Converts BigQuery results to a JSON string."""
dictionaries = [intent.to_dict() for intent in intents]
return dumps(dictionaries, sort_keys=True)
def generate_hash(data):
"""Generates a SHA-256 hash of the input data."""
hash_object = sha256(data.encode())
return hash_object.hexdigest()
@@ -0,0 +1,61 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
services:
backend:
build:
secrets:
- gcp_credentials
context: ./backend
dockerfile: Dockerfile.local
args:
IS_FIRST_DEPLOYMENT: "True"
GCLOUD_PROJECT: my-project-id
ENVIRONMENT: development
FRONTEND_URL: http://localhost:4200
BIG_QUERY_DATASET: bigquery_quickbot_multi_playbook_search
container_name: quickbot-multi-playbook-search-backend
ports:
- "8080:8080"
volumes:
- ./backend:/app
# Mount the gcloud ADC directory. Replace with the correct path for your OS if different. :ro - read-only for better security
- ~/.config/gcloud/:/root/.config/gcloud:ro # Linux/macOS example:
# - %APPDATA%/gcloud:/root/.config/gcloud:ro # Windows example:
environment:
GOOGLE_APPLICATION_CREDENTIALS: "/root/.config/gcloud/application_default_credentials.json"
restart: unless-stopped
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
container_name: quickbot-multi-playbook-search-frontend
ports:
- "4200:8080"
volumes:
- ./frontend:/app
# Use an anonymous volume for node_modules to prevent host node_modules
# from interfering and potentially speed up builds/rebuilds
- /app/node_modules
# To change the env variables in the frontend, change the environment.ts file
restart: unless-stopped
secrets:
gcp_credentials: # This ID must match the one used in services.backend.build.secrets
# Path to your Google Application Default Credentials JSON file on the HOST machine.
# Replace with the correct path for your OS if different.
file: ~/.config/gcloud/application_default_credentials.json
# Windows example (you might need to adjust the target path inside the container if it's a Linux container):
# - %APPDATA%/gcloud/application_default_credentials.json
@@ -0,0 +1,4 @@
.angular
.vscode
dist
node_modules
@@ -0,0 +1,8 @@
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
insert_final_newline = true
@@ -0,0 +1,3 @@
{
"extends": "./node_modules/gts/"
}
@@ -0,0 +1,42 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
# System files
.DS_Store
Thumbs.db
@@ -0,0 +1,33 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
module.exports = {
...require('gts/.prettierrc.json'),
"overrides": [
{
"files": "*.html",
"options": {
"parser": "angular"
}
},
{
"files": "*.scss",
"options": {
"parser": "scss"
}
}
]
}
@@ -0,0 +1,27 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM node:18.17.1 as build
RUN mkdir -p /code
WORKDIR /code
COPY . /code/
RUN npm ci
RUN npm run build:prod
FROM nginx:alpine
COPY --from=build /code/dist/quick-bot-app-frontend /usr/share/nginx/html
COPY --from=build /code/nginx.conf /etc/nginx/conf.d/
EXPOSE 8080
@@ -0,0 +1,87 @@
# QuickBotAppFrontend
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 15.1.3.
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
## Code Styling & Commit Guidelines
To maintain code quality and consistency:
* **TypeScript (Frontend):** We follow [Angular Coding Style Guide](https://angular.dev/style-guide) by leveraging the use of [Google's TypeScript Style Guide](https://github.com/google/gts) using `gts`. This includes a formatter, linter, and automatic code fixer.
* **Python (Backend):** We adhere to the [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html), using tools like `pylint` and `black` for linting and formatting.
* **Commit Messages:** We suggest following [Angular's Commit Message Guidelines](https://github.com/angular/angular/blob/main/contributing-docs/commit-message-guidelines.md) to create clear and descriptive commit messages.
#### Frontend (TypeScript with `gts`)
1. **Initialize `gts` (if not already done in the project):**
Navigate to the `frontend/` directory and run:
```bash
npx gts init
```
This will set up `gts` and create necessary configuration files (like `tsconfig.json`). Ensure your `tsconfig.json` (or a related gts config file like `.gtsrc`) includes an extension for `gts` defaults, typically:
```json
{
"extends": "./node_modules/gts/tsconfig-google.json",
// ... other configurations
}
```
2. **Check for linting issues:**
```bash
npm run lint
```
(This assumes a `lint` script is defined in `package.json`, e.g., `"lint": "gts lint"`)
3. **Fix linting issues automatically (where possible):**
```bash
npm run fix
```
(This assumes a `fix` script is defined in `package.json`, e.g., `"fix": "gts fix"`)
#### Backend (Python with `pylint` and `black`)
1. **Ensure Dependencies are Installed:**
Add `pylint` and `black` to your `backend/requirements.txt` file:
```
pylint
black
```
Then install them within your virtual environment:
```bash
pip install pylint black
# or pip install -r requirements.txt
```
2. **Configure `pylint`:**
It's recommended to have a `.pylintrc` file in your `backend/` directory to configure `pylint` rules. You might need to copy a standard one or generate one (`pylint --generate-rcfile > .pylintrc`).
3. **Check for linting issues with `pylint`:**
Navigate to the `backend/` directory and run:
```bash
pylint .
```
(Or specify modules/packages: `pylint your_module_name`)
4. **Format code with `black`:**
To automatically format all Python files in the current directory and subdirectories:
```bash
python -m black . --line-length=80
```
@@ -0,0 +1,123 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"quick-bot-app-frontend": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/quick-bot-app-frontend",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": [
"zone.js"
],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"@angular/material/prebuilt-themes/indigo-pink.css",
"src/styles.scss",
"node_modules/prismjs/themes/prism-okaidia.css"
],
"scripts": [
"node_modules/marked/marked.min.js",
"node_modules/prismjs/prism.js",
"node_modules/prismjs/components/prism-csharp.min.js",
"node_modules/prismjs/components/prism-css.min.js",
"node_modules/clipboard/dist/clipboard.min.js"
]
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "4mb",
"maximumError": "10mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "20kb",
"maximumError": "50kb"
}
],
"outputHashing": "all"
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"browserTarget": "quick-bot-app-frontend:build:production"
},
"development": {
"browserTarget": "quick-bot-app-frontend:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "quick-bot-app-frontend:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"@angular/material/prebuilt-themes/indigo-pink.css",
"src/styles.scss",
"node_modules/prismjs/themes/prism-okaidia.css"
],
"scripts": [
"node_modules/marked/marked.min.js",
"node_modules/prismjs/prism.js",
"node_modules/prismjs/components/prism-csharp.min.js",
"node_modules/prismjs/components/prism-css.min.js",
"node_modules/clipboard/dist/clipboard.min.js"
]
}
}
}
}
},
"cli": {
"analytics": false
}
}
@@ -0,0 +1,11 @@
server {
listen 8080;
root /usr/share/nginx/html;
index index.html index.htm;
include /etc/nginx/mime.types;
location / {
try_files $uri $uri/ /index.html;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,70 @@
{
"name": "quick-bot-app-frontend",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build:prod": "ng build --configuration production --aot --output-hashing=all",
"build:dev": "ng build --configuration development",
"build:staging": "ng build --configuration staging",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test",
"postinstall": "ngcc",
"post-build": "node ./build/post-build.js",
"lint": "gts lint",
"clean": "gts clean",
"compile": "tsc",
"fix": "gts fix",
"prepare": "npm run compile",
"pretest": "npm run compile",
"posttest": "npm run lint"
},
"private": true,
"dependencies": {
"@angular/animations": "^15.1.0",
"@angular/cdk": "^15.2.9",
"@angular/common": "^15.1.0",
"@angular/compiler": "^15.1.0",
"@angular/core": "^15.1.0",
"@angular/fire": "7.5",
"@angular/flex-layout": "^15.0.0-beta.42",
"@angular/forms": "^15.1.0",
"@angular/material": "^15.2.9",
"@angular/platform-browser": "^15.1.0",
"@angular/platform-browser-dynamic": "^15.1.0",
"@angular/router": "^15.1.0",
"@fortawesome/fontawesome-free": "^6.6.0",
"@ionic/angular": "^7.5.4",
"@ng-idle/core": "^14.0.0",
"clipboard": "^2.0.11",
"ionic-emoji-rating": "^1.0.15",
"marked": "^4.3.0",
"moment": "^2.30.1",
"ngx-markdown": "^15.1.2",
"prismjs": "^1.29.0",
"rxfire": "6.0.3",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"uuid": "^9.0.1",
"zone.js": "~0.12.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^15.1.3",
"@angular/cli": "~15.1.3",
"@angular/compiler-cli": "^15.1.0",
"@types/jasmine": "~4.3.0",
"@types/marked": "^4.3.0",
"@types/uuid": "^9.0.4",
"firebase-tools": "^11.30.0",
"gts": "^5.3.1",
"jasmine-core": "~4.5.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.0.0",
"typescript": "~4.9.4",
"@types/node": "^22.7.5"
}
}
@@ -0,0 +1,40 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {ChatComponent} from './components/main/chat/chat.component';
import {MainComponent} from './components/main/main.component';
import {LoginComponent} from './components/login/login.component';
import {AuthGuard} from './services/login/auth.guard';
import {ManageIntentComponent} from './components/manage-intent/manage-intent.component';
const routes: Routes = [
{path: '', component: MainComponent, canActivate: [AuthGuard]},
{path: 'login', component: LoginComponent},
{path: 'chat', component: ChatComponent, canActivate: [AuthGuard]},
{
path: 'intent-management',
component: ManageIntentComponent,
canActivate: [AuthGuard],
},
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}
@@ -0,0 +1,21 @@
<!--
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<ng-container *ngIf="showHeader">
<app-header></app-header>
</ng-container>
<router-outlet></router-outlet>
<app-footer *ngIf="showHeader"></app-footer>
@@ -0,0 +1,98 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
::ng-deep .tour-step {
background: white;
}
::ng-deep .tour-top{
margin: -1%;
}
::ng-deep .tour-left{
margin-left: -1%;
}
::ng-deep .tour-top-right{
margin-top: -1%;
}
::ng-deep .tour-bottom-right{
margin-top: 1%;
}
::ng-deep .tour-bottom-left{
margin-top: 1%;
}
::ng-deep .tour-bottom{
margin: 1%;
}
::ng-deep ngx-guided-tour .tour-content{
font-size: 17px !important;
padding-bottom: 20px !important;
}
::ng-deep ngx-guided-tour .tour-title{
font-size: 22px !important;
padding-bottom: 0 !important;
}
::ng-deep ngx-guided-tour .tour-step .tour-buttons .skip-button{
font-weight: 500 !important;
font-size: 17px !important;
line-height: 1.7 !important;
}
::ng-deep ngx-guided-tour .tour-step{
border-radius: 20px !important;
}
::ng-deep ngx-guided-tour .tour-step .tour-buttons .back-button{
font-weight: 500 !important;
font-size: 17px !important;
line-height: 1.7 !important;
}
::ng-deep ngx-guided-tour .tour-step .tour-buttons .next-button{
background: #4285F4;
padding-left: 16px !important;
padding-right: 16px !important;
padding-top: 6px !important;
padding-bottom: 6px !important;
font-size: 16px !important;
color: white;
font-weight: 500;
border-radius: 15px !important;
}
::ng-deep ngx-guided-tour .tour-step.tour-top .tour-block{
margin-bottom: 0 !important;
}
::ng-deep ngx-guided-tour .tour-step.tour-bottom-left .tour-block{
margin-top: 0 !important;
}
::ng-deep pre{
background: #485f84 !important;
}
div {
font-family: "Google Sans", sans-serif !important;
}
@@ -0,0 +1,49 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {TestBed} from '@angular/core/testing';
import {RouterTestingModule} from '@angular/router/testing';
import {AppComponent} from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [RouterTestingModule],
declarations: [AppComponent],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it("should have as title 'pac-assist-ui'", () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('pac-assist-ui');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('.content span')?.textContent).toContain(
'pac-assist-ui app is running!'
);
});
});
@@ -0,0 +1,100 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Component, ChangeDetectorRef} from '@angular/core';
import {Router, NavigationEnd, Event as NavigationEvent} from '@angular/router';
import {UserService} from './services/user/user.service';
import {Idle, DEFAULT_INTERRUPTSOURCES} from '@ng-idle/core';
import {MatDialog, MatDialogRef} from '@angular/material/dialog';
import {DialogueBoxComponent} from './dialogue-box/dialogue-box.component';
import {AuthService} from './services/login/auth.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
title = 'pac-assist-ui';
showHeader = true;
userInfo: any;
ref?: MatDialogRef<DialogueBoxComponent>;
constructor(
private router: Router,
private userService: UserService,
private idle: Idle,
cd: ChangeDetectorRef,
public authService: AuthService,
private dialog: MatDialog
) {
this.router.events.subscribe((event: NavigationEvent) => {
if (event instanceof NavigationEnd) {
if (
event.url === '/login' ||
event.url === '/login/e2e' ||
(event.url.includes('login') && event.url.includes('email')) ||
(event.url.includes('login') && event.url.includes('tos')) ||
event.url.includes('reset-password') ||
event.url.includes('support-ticket')
) {
this.showHeader = false;
} else {
this.userInfo = this.userService.getUserDetails();
this.showHeader = true;
// idle for 2 days then show dialog
idle.setIdle(172800);
idle.setInterrupts(DEFAULT_INTERRUPTSOURCES);
idle.onIdleStart.subscribe(() => {
this.showIdleDialog();
});
this.idle.watch();
}
}
});
}
showIdleDialog() {
if (!this.ref && this.showHeader) {
const config = {
title: 'Session Idle',
icon: 'av_timer',
description:
'Session has been inactive for 48 hours, user will be logged out automatically.',
showConfirmBtn: false,
confirmBtnText: 'logout',
cancelCallBackFunction: () => {
this.idle.watch();
},
needsExtraProcessing: true,
extraProcessingFunction: () => {
setTimeout(() => {
this.ref?.close();
this.authService.logout();
}, 20000);
},
};
this.ref = this.dialog.open(DialogueBoxComponent, {
data: config,
disableClose: true,
});
}
this.ref?.afterClosed().subscribe(() => {
this.ref = undefined;
});
}
}
@@ -0,0 +1,167 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {AppRoutingModule} from './app-routing.module';
import {AppComponent} from './app.component';
import {HeaderComponent} from './components/header/header.component';
import {FooterComponent} from './components/footer/footer.component';
import {MainComponent} from './components/main/main.component';
import {ChatComponent} from './components/main/chat/chat.component';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {MatIconModule} from '@angular/material/icon';
import {MatAutocompleteModule} from '@angular/material/autocomplete';
import {MatButtonModule} from '@angular/material/button';
import {MatToolbarModule} from '@angular/material/toolbar';
import {HttpClientModule} from '@angular/common/http';
import {MatInputModule} from '@angular/material/input';
import {NgFor, PathLocationStrategy} from '@angular/common';
import {MatSelectModule} from '@angular/material/select';
import {MatFormFieldModule} from '@angular/material/form-field';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {MatGridListModule} from '@angular/material/grid-list';
import {MatCardModule} from '@angular/material/card';
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
import {ChatbarComponent} from './components/main/chat/chatbar/chatbar.component';
import {MatTooltipModule} from '@angular/material/tooltip';
import {LoginComponent} from './components/login/login.component';
import {initializeApp, provideFirebaseApp} from '@angular/fire/app';
import {
provideAnalytics,
getAnalytics,
ScreenTrackingService,
UserTrackingService,
} from '@angular/fire/analytics';
import {environment} from '../environments/environment';
import {provideAuth, getAuth} from '@angular/fire/auth';
import {MatMenuModule} from '@angular/material/menu';
import {MatDividerModule} from '@angular/material/divider';
import {MatChipsModule} from '@angular/material/chips';
import {LocationStrategy} from '@angular/common';
import {MarkdownModule} from 'ngx-markdown';
import {MatDialogModule} from '@angular/material/dialog';
import {CdkAccordionModule} from '@angular/cdk/accordion';
import {MatExpansionModule} from '@angular/material/expansion';
import {IonicRatingModule} from 'ionic-emoji-rating';
import {MatListModule} from '@angular/material/list';
import {MatTabsModule} from '@angular/material/tabs';
import {MatTableModule} from '@angular/material/table';
import {MatCheckboxModule} from '@angular/material/checkbox';
import {MatSlideToggleModule} from '@angular/material/slide-toggle';
import {MatButtonToggleModule} from '@angular/material/button-toggle';
import {MatSnackBarModule} from '@angular/material/snack-bar';
import {MatSortModule} from '@angular/material/sort';
import {MatPaginatorModule} from '@angular/material/paginator';
import {SuggestionCardComponent} from './components/elements/suggestion-card/suggestion-card.component';
import {MatSidenavModule} from '@angular/material/sidenav';
import {NgIdleModule} from '@ng-idle/core';
import {ClipboardModule} from '@angular/cdk/clipboard';
import 'prismjs';
import 'prismjs/components/prism-typescript.min.js';
import 'prismjs/plugins/line-numbers/prism-line-numbers.js';
import 'prismjs/plugins/line-highlight/prism-line-highlight.js';
import {ToastMessageComponent} from './components/shared/toast-message/toast-message.component';
import {FlexLayoutModule} from '@angular/flex-layout';
import {DialogueBoxComponent} from './dialogue-box/dialogue-box.component';
import {ManageIntentComponent} from './components/manage-intent/manage-intent.component';
import {MatSliderModule} from '@angular/material/slider';
import {MatStepperModule} from '@angular/material/stepper';
import {IntentFormComponent} from './components/manage-intent/intent-form/intent-form.component';
import {CreateIntentFormComponent} from './components/manage-intent/create-intent-form/create-intent-form.component';
@NgModule({
declarations: [
AppComponent,
HeaderComponent,
FooterComponent,
MainComponent,
ChatComponent,
ChatbarComponent,
LoginComponent,
SuggestionCardComponent,
ToastMessageComponent,
DialogueBoxComponent,
ManageIntentComponent,
IntentFormComponent,
CreateIntentFormComponent,
],
imports: [
BrowserModule,
AppRoutingModule,
BrowserAnimationsModule,
MatSnackBarModule,
MatPaginatorModule,
MatSortModule,
MatSliderModule,
MatToolbarModule,
MatButtonModule,
MatIconModule,
HttpClientModule,
FormsModule,
MatFormFieldModule,
MatSelectModule,
NgFor,
MatInputModule,
MatGridListModule,
MatCardModule,
MatTooltipModule,
MatProgressSpinnerModule,
MatMenuModule,
MatDividerModule,
MatChipsModule,
ReactiveFormsModule,
MatMenuModule,
MatDialogModule,
CdkAccordionModule,
IonicRatingModule,
MarkdownModule.forRoot(),
MatExpansionModule,
MatListModule,
MatTabsModule,
MatCheckboxModule,
MatTableModule,
MatSlideToggleModule,
MatButtonToggleModule,
MatSidenavModule,
MatAutocompleteModule,
environment.requiredLogin === 'True'
? [
provideFirebaseApp(() => initializeApp(environment.firebase)),
provideAuth(() => getAuth()),
]
: [],
environment.requiredLogin === 'True'
? [provideAnalytics(() => getAnalytics())]
: [],
FlexLayoutModule,
NgIdleModule.forRoot(),
ClipboardModule,
MatStepperModule,
],
providers: [
{provide: LocationStrategy, useClass: PathLocationStrategy},
environment.requiredLogin === 'True'
? [
ScreenTrackingService, // Automatically track screen views
UserTrackingService, // Automatically track user interactions
]
: [],
],
bootstrap: [AppComponent],
})
export class AppModule {}
@@ -0,0 +1,34 @@
<!--
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="heading-parent-container">
<div class="chips-container">
<mat-chip-listbox>
<mat-chip-row (click)="assignQToChatQuery(question)"
*ngFor="let question of data.suggestedQuestion">
<span class="text-span">
<mat-icon>keyboard_return</mat-icon>
{{question}}
</span>
</mat-chip-row>
</mat-chip-listbox>
</div>
<div class="close-button">
<button mat-icon-button aria-label="Example icon button with a vertical three dot icon" (click)="closeSuggestionCard()">
<mat-icon>close</mat-icon>
</button>
</div>
</div>
@@ -0,0 +1,95 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.mat-mdc-standard-chip {
--mdc-chip-label-text-color: #4285F4;
--mdc-chip-elevated-container-color: #fff;
border: 1px solid #4285F4 !important;
font-family: 'Google Sans', sans-serif !important;
font-size: 1em !important;
mat-icon{
color: #4285F4 !important;
}
}
:host ::ng-deep .mdc-evolution-chip__cell {
justify-content: center;
}
.heading-parent-container{
display: flex;
flex-direction: row;
}
.close-button{
justify-content: flex-end;
display: flex;
width: 6%;
}
.intent-card {
padding: 0px;
font-size: 0.95rem;
}
.intent-heading {
color: rgba(95, 99, 104, 1);
font-size: 0.95rem;
font-style: normal;
font-weight: 400;
line-height: 42px;
font-family: 'Google Sans', sans-serif !important;
width: 20%;
}
.chips-container{
width: 94%;
}
.intent-container {
border-radius: 8px;
background-color: rgba(246, 249, 254, 1);
width: 100%;
box-shadow: none !important;
flex-shrink: 0;
.selected-question {
// margin: 0.5rem 0rem;
&:hover {
background-color: rgba(66, 133, 244, 0.10);
}
}
.question-item {
display: flex;
justify-content: space-between;
font-family: 'Google Sans', sans-serif !important;
color: #161A1D;
font-size: 0.95rem;
line-height: 24px;
flex-wrap: wrap;
}
}
.text-span{
display: flex !important;
gap: 10px !important;
}
@@ -0,0 +1,38 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {SuggestionCardComponent} from './suggestion-card.component';
describe('SuggestionCardComponent', () => {
let component: SuggestionCardComponent;
let fixture: ComponentFixture<SuggestionCardComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [SuggestionCardComponent],
}).compileComponents();
fixture = TestBed.createComponent(SuggestionCardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,36 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Component, Input, Output, EventEmitter} from '@angular/core';
import {SuggestionData} from 'src/app/models/messegeType.model';
@Component({
selector: 'app-suggestion-card',
templateUrl: './suggestion-card.component.html',
styleUrls: ['./suggestion-card.component.scss'],
})
export class SuggestionCardComponent {
@Input() data: SuggestionData;
@Output() raiseQuery = new EventEmitter<string>();
@Output() dismissSuggestionCard = new EventEmitter();
assignQToChatQuery(question: string) {
this.raiseQuery.emit(question);
}
closeSuggestionCard() {
this.dismissSuggestionCard.emit();
}
}
@@ -0,0 +1,23 @@
<!--
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<p>
<mat-toolbar class="container footer" color="primary">
<div class="links-weight">
<span>Powered by <span class="genAI">Vertex AI</span></span>
</div>
</mat-toolbar>
</p>
@@ -0,0 +1,71 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.container {
padding-left: 4rem;
padding-right: 4rem;
}
.footer {
position: fixed;
bottom: 0;
}
.genAI {
background: linear-gradient(86.95deg, #34A853 -65.34%, #FBBC05 -12.75%, #EA4335 25.15%, #1A73E8 103.88%);
-webkit-background-clip: text;
-moz-background-clip: text;
background-clip: text;
color: transparent;
}
:host ::ng-deep .mat-toolbar.mat-primary {
background-color: transparent;
color: #504747;
box-shadow: none;
justify-content: center;
}
.line-spacer {
flex: 1 1 auto;
}
.links-weight {
margin-left: 10px;
margin-right: 10px;
font-size: 14px;
font-weight: 400;
cursor: pointer;
}
.links-bold {
margin-right: 10px;
color: #5f6368;
font-family: 'Google Sans', sans-serif !important;
.blue{
color: #4285f4;
}
.red{
color: #ea4335;
}
.yellow{
color: #fbbc04;
}
.green{
color: #34a853;
}
}
@@ -0,0 +1,38 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {FooterComponent} from './footer.component';
describe('FooterComponent', () => {
let component: FooterComponent;
let fixture: ComponentFixture<FooterComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [FooterComponent],
}).compileComponents();
fixture = TestBed.createComponent(FooterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,24 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Component} from '@angular/core';
@Component({
selector: 'app-footer',
templateUrl: './footer.component.html',
styleUrls: ['./footer.component.scss'],
})
export class FooterComponent {}
@@ -0,0 +1,70 @@
<!--
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<nav class="navbar navbar-expand-lg navbar-light bg-light top-nav fixed-top">
<div class="parent-container">
<div class="left-heading" [matTooltip]="'Home'">
<span class="logo-text gradient-text" (click)="navigate()">{{headerTitle}}</span>
</div>
<div class="right-menu-items">
<div *ngIf="authService.isUserLoggedIn()" class="profile-container" [matMenuTriggerFor]="userAccountMenu">
<div class="circle" *ngIf="_UserService.getUserDetails().photoURL && requiredLogin === 'True'"><img referrerpolicy="no-referrer"
[src]="_UserService.getUserDetails().photoURL" class="user-icon" /> </div>
<div class="circle bypassLoginLogo" *ngIf="requiredLogin !== 'True'"><mat-icon>person</mat-icon></div>
</div>
</div>
</div>
<mat-menu #userAccountMenu class="profile-menu" [overlapTrigger]="false" yPosition="below">
<div class="card-inside-right">
<div class="name">
<span class="sub-title">
{{ requiredLogin === 'True' ? _UserService.getUserDetails().email : 'Guest'}}
</span>
<div *ngIf="requiredLogin === 'True' && _UserService.getUserDetails().photoURL ">
<img [src]="_UserService.getUserDetails().photoURL" class="user-icon container-circle" />
</div>
<div *ngIf="requiredLogin !== 'True'" class="user-icon container-circle bypassMenuContainer">
<mat-icon class="bypassMenuLogo">person</mat-icon>
</div>
</div>
<div class="action-item-container">
<br>
<div class="logout" (click)="goToManageIntentPage()">
<div class="logout-container">
<span class="material-symbols-rounded logo">feedback</span>
<div class="text">
<span class="sub-title">Manage Playbooks</span>
</div>
</div>
</div>
<div class="logout logout-button" (click)="logout()" *ngIf="requiredLogin === 'True'">
<div class="logout-container">
<span class="material-symbols-rounded logo logout-button-text">exit_to_app</span>
<div class="text">
<span class="sub-title logout-button-text">Logout</span>
</div>
</div>
</div>
</div>
</div>
</mat-menu>
</nav>
<div *ngFor="let i of intentsInProgress" class="notification-banner-container blue-border">
<div class="notification-text">
<mat-icon class="blue">info_outline</mat-icon>
<span>New intent is being created! Prepare to be able to chat with me about {{i.name}}</span>
</div>
</div>
@@ -0,0 +1,479 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.container {
margin: auto;
}
.logo-text {
font-family: Google Sans,Helvetica Neue,sans-serif;
font-style:normal;
font-size: 22px;
line-height: 128.7%;
letter-spacing: normal;
color: #2d2c2cbf;
}
.gradient-text {
font-family: Google Sans,Helvetica Neue,sans-serif;
font-style:normal;
font-size: 30px;
line-height: 128.7%;
letter-spacing: normal;
background: linear-gradient(89.9deg, rgba(66, 133, 244, 0.95) 21.17%, #A488F5 44.34%, rgba(234, 67, 53, 0.88) 81.26%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-fill-color: transparent;
}
.links-pointer {
cursor: pointer;
}
.logo-icon {
height: 35px;
width: 125px;
cursor: pointer;
}
// .profile-container {
// margin-left: 1%;
// font-family: 'Google Sans', sans-serif !important;
// .circle {
// display: flex;
// justify-content: center;
// width: 40px;
// height: 40px;
// border: 5px solid rgb(255, 255, 255);
// border-radius: 60%;
// overflow: hidden;
// }
// .circle img {
// width: 100%;
// height: 100%;
// object-fit: cover;
// }
// }
.card-inside-right {
display: flex;
flex-direction: column;
align-items: flex-start;
color: #3E4245;
font-size: 1rem;
font-weight: 400;
font-family: 'Google Sans', sans-serif !important;
border-radius: 5px;
letter-spacing: 0.4px;
.container-circle {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 50px;
padding-top: 4%;
padding-bottom: 4%;
}
.name {
display: flex;
align-items: center;
font-size: 1rem;
color: #3E4245;
font-weight: 500;
padding: 1.3em 0.4em;
flex-direction: column;
gap: 10px;
background: #DDE3EA !important;
width: -webkit-fill-available;;
.sub-title {
color: #3E4245;
}
margin: auto;
}
.company {
display: flex;
align-items: center;
padding: 0.3em 1em;
width: -webkit-fill-available;;
.logo {
padding-right: 2%;
color: #5F6368;
}
.text {
min-width: 100px;
.sub-title {
font-size: 0.85rem;
margin-bottom: 10px;
color: #3E4245;
}
}
margin-bottom: 10px;
}
.logout {
background: #DDE3EA;
border-radius: 20px;
font-size: 1rem;
font-weight: 500;
font-family: 'Google Sans', sans-serif !important;
margin-bottom: 1em;
cursor: pointer;
width: -webkit-fill-available;;
padding: 0.3em 1em;
.logout-container {
display: flex;
align-items: center;
.logo {
padding-right: 4%;
color: #5F6368;
}
.text {
.sub-title {
color: #3E4245;
font-size: 0.85rem;
font-family: 'Google Sans', sans-serif !important;
}
}
}
}
}
//------------css of card end-------------------//
.image {
width: 35px;
height: 35px;
}
.fixed-top {
z-index: 1000;
position: relative;
}
.top-nav {
height: 60px;
background-color: transparent;
// box-shadow: (0px 4px 16px rgba(195, 209, 226, 0.25));
.parent-container {
// width: 1600px;
display: flex;
margin: auto;
justify-content: space-between;
/* border: 1px solid black; */
height: 100%;
align-items: center;
padding: 0% 1.5%;
.left-heading {
display: flex;
align-items: center;
cursor: pointer;
}
.logo-icon {
height: 4.2em;
width: 4em;
cursor: pointer;
background-image: url('../../../assets/images/new-pdc-logo.svg');
background-size: 443px;
background-repeat: round;
border-radius: 28px;
margin-right:1em;
}
.user-icon {
height: 50px;
width: 50px;
cursor: pointer;
border-radius: 28px;
}
.right-menu-items {
display: flex;
align-items: center;
.profile-container {
margin-left: 1%;
font-family: 'Google Sans', sans-serif !important;
.circle {
display: flex;
justify-content: center;
width: 35px;
height: 35px;
border-radius: 60%;
overflow: hidden;
}
.circle img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
}
}
}
.icon-color {
color: #5F6368;
font-size: 24px;
}
::ng-deep .quick-link.mat-mdc-menu-panel.mat-mdc-menu-panel {
width: 420px !important;
display: flex;
flex-direction: column;
background: #F0F4F9;
.mat-mdc-menu-content {
padding: 0.5rem;
}
.ql-container {
width: 100%;
display: flex;
flex-direction: column;
.ql-child-container{
display: flex;
flex-direction: row;
text-wrap: nowrap ;
justify-content: space-between;
align-items: flex-end;
}
a, a:active{
text-decoration: none;
}
.align-link-content{
display: flex;
flex-direction: row;
align-items: baseline;
gap: 10px;
width: 100% !important;
box-shadow: 0px 4px 4px -6px #b9b8b8 !important;
}
.no-box-shadow{
box-shadow: none !important;
}
.icon-container {
display: flex;
alignment-baseline: after-edge;
mat-icon {
color: #161A1D !important;
}
}
}
.ql-header{
margin: 1rem;
margin-left: 1.7rem;
}
.avatar {
width: 40px;
height: 40px; //<--use the size you choose
border-radius: 100%;
text-align: center;
}
div.link-label {
color: #161A1D;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 24px;
letter-spacing: 0.4px;
height: 50px;
margin: 0px 5px;
width: 57%;
}
.avatar span {
line-height: 40px;
font-size: 16px;
}
.link-button {
margin: 1rem;
height: 41px;
width: 100% !important;
}
}
.notification-banner-container {
z-index: 999;
position: relative;
width: inherit;
display: flex;
flex-direction: row;
height: 8%;
background: #F6F9FE;
align-content: center;
flex-wrap: wrap;
padding-left: 1%;
gap: 1%;
margin-bottom: 0.2%;
.amber {
color: #fbc645;
}
.red {
color: red;
}
.blue {
color: #4285F4;
}
}
.red-border {
border-left: 5px solid red;
}
.amber-border {
border-left: 5px solid #fbc645;
}
.blue-border {
border-left: 5px solid #4285F4;
}
.notification-text {
display: flex;
flex-direction: row;
align-items: center;
gap: 15px;
width: 70%;
}
.notification-buttons-container {
width: 26%;
justify-content: flex-end;
display: flex;
flex-direction: row;
gap: 15px;
button {
font-family: 'Google Sans', sans-serif !important;
font-style: normal;
font-weight: 500;
line-height: 23px;
font-size: 14px;
}
.dismiss-button {
letter-spacing: 0.4px;
color: #4285F4;
}
.learn-more-button {
color: #fff !important;
background: #4285F4 !important;
border-radius: 30px;
letter-spacing: unset;
box-shadow: none;
}
}
::ng-deep .profile-menu.mat-mdc-menu-panel.mat-mdc-menu-panel {
max-width: 320px !important;
width: 300px !important;
}
::ng-deep .profile-menu .mat-mdc-menu-content {
padding: 0% !important;
}
.action-item-container {
width: -webkit-fill-available;
padding-inline: 5%;
padding-top: 2%;
padding-bottom: 2%;
}
.logout-button {
background: white !important;
border: 1px solid #4285F4 !important;
}
.logout-button-text {
color: #4285F4 !important;
}
.app-version-chip {
font-size: 9px;
border-radius: -2px;
padding-inline: 8px;
line-height: 16px;
border-radius: 11px;
margin-left: 8px;
color: #474c55;
background: linear-gradient(92.1deg, #bdd1f3 19.16%, rgba(199, 183, 245, 0.9) 79%, rgba(235, 165, 158, 0.9) 135.24%), linear-gradient(272.04deg, rgba(158, 188, 237, 0.7) 14.93%, rgba(158, 188, 237, 0) 104.51%) !important;
}
.bypassLoginLogo {
background: aliceblue;
color: cornflowerblue;
align-items: center;
cursor: pointer;
}
.bypassMenuContainer {
display: flex;
justify-content: center;
background: aliceblue;
width: 42% !important;
height: 40% !important;
border-radius: 65px !important;
}
.bypassMenuLogo {
font-size: 6rem;
height: 6rem;
width: 6rem;
}
@@ -0,0 +1,38 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {HeaderComponent} from './header.component';
describe('HeaderComponent', () => {
let component: HeaderComponent;
let fixture: ComponentFixture<HeaderComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [HeaderComponent],
}).compileComponents();
fixture = TestBed.createComponent(HeaderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,83 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Component} from '@angular/core';
import {DomSanitizer} from '@angular/platform-browser';
import {MatIconRegistry} from '@angular/material/icon';
import {Router} from '@angular/router';
import {UserService} from 'src/app/services/user/user.service';
import {AuthService} from '../../services/login/auth.service';
import {MatDialog} from '@angular/material/dialog';
import {environment} from 'src/environments/environment';
import {IntentDetails, IntentService} from 'src/app/services/intent.service';
const GOOGLE_CLOUD_ICON = `<svg width="694px" height="558px" viewBox="0 0 694 558" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="google-cloud-1" fill-rule="nonzero">
<path d="M440.545766,152.920305 L461.717489,152.920305 L522.033632,92.9270295 L525,67.4718307 C412.772422,-31.0513593 241.450145,-20.4353843 142.396729,91.1914478 C114.856041,122.200508 94.8766816,159.08162 84,199 C90.7179504,196.251996 98.1629517,195.8181 105.171723,197.72724 L225.774927,177.941608 C225.774927,177.941608 231.911237,167.846308 235.081179,168.482688 C288.737536,109.877878 379.037259,103.051256 440.981997,152.920305 L440.545766,152.920305 Z" id="Path" fill="#EA4335"></path>
<path d="M607.866504,200.682375 C594.001378,149.778725 565.57351,104.008948 526.012848,69 L441.426861,153.404341 C477.150633,182.525289 497.497778,226.409746 496.625757,272.440567 L496.625757,287.436115 C538.221135,287.436115 571.910193,321.081832 571.910193,362.558879 C571.910193,404.064932 538.192067,437.681644 496.625757,437.681644 L346.02782,437.681644 L331,452.880226 L331,542.998538 L346.02782,557.994086 L496.625757,557.994086 C582.955785,558.6612 659.548251,502.826713 685.185654,420.568736 C710.764921,338.281754 679.372184,248.946575 607.866504,200.682375 L607.866504,200.682375 Z" id="Path" fill="#4285F4"></path>
<path d="M194.75446,555.998385 L346,555.998385 L346,436.702654 L194.75446,436.702654 C183.982485,436.702654 173.327279,434.43008 163.518651,430 L142.266623,436.47252 L81.3130068,496.134769 L76,517.076966 C110.184236,542.506777 151.900097,556.170985 194.75446,555.998385 L194.75446,555.998385 Z" id="Path" fill="#34A853"></path>
<path d="M195.36137,165 C111.412398,165.496534 37.0600945,219.208571 10.2827643,298.683735 C-16.4945658,378.158899 10.1952567,465.881761 76.7302131,517 L164.383674,429.422857 C126.347031,412.257155 109.458061,367.550553 126.638723,329.547028 C143.819384,291.543502 188.564945,274.669238 226.601588,291.834941 C243.344712,299.412331 256.762546,312.818482 264.346539,329.547028 L352,241.969885 C314.692587,193.270582 256.733377,164.797082 195.36137,165 L195.36137,165 Z" id="Path" fill="#FBBC05"></path>
</g>
</g>
</svg>`;
@Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.scss'],
})
export class HeaderComponent {
headerTitle: string = environment.chatbotName;
intentsInProgress: IntentDetails[] = [];
requiredLogin: string = environment.requiredLogin;
constructor(
iconRegistry: MatIconRegistry,
sanitizer: DomSanitizer,
private router: Router,
public _UserService: UserService,
public authService: AuthService,
public dialog: MatDialog,
private intentsService: IntentService
) {
iconRegistry.addSvgIconLiteral(
'google-cloud-icon',
sanitizer.bypassSecurityTrustHtml(GOOGLE_CLOUD_ICON)
);
this.intentsService.getAllIntent().subscribe(allIntents => {
this.intentsInProgress = allIntents.filter(
i => i.status === '1' || i.status === '3'
);
});
}
navigate() {
this.router.navigateByUrl('/');
}
goToManageIntentPage() {
this.router.navigateByUrl('/intent-management');
}
logout() {
this.authService.logout();
}
openURL(link: string) {
(window as any).open(link, '_blank');
}
}
@@ -0,0 +1,48 @@
<!--
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="login-container">
<div class="left-container">
<div style="display: flex ;flex-direction:column;justify-content: center;align-items: center;">
<div class="parent-container"
style="margin: 0rem;width:90%;text-align: center;font-weight: 500; font-size: 1.5rem;">
<p class="login-app-text">
Quickly setup a <span class="linear-text">Vertex AI</span> powered Chat application
</p>
</div>
</div>
</div>
<div class="right-container">
<div class="login-card">
<p class="heading">{{chatbotName}}</p>
<div class="login-buttons-container">
<div>
<div class="register-text">
Single Sign on with Google Account.
</div>
<button (click)="loginWithGoogle()" class="login-button">
<span class="mat-button-wrapper"> Login in with Google </span>
</button>
</div>
</div>
<div class="spinner">
<mat-spinner [diameter]="50" *ngIf="this.loader"></mat-spinner>
</div>
</div>
</div>
</div>
@@ -0,0 +1,545 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.login-container {
display: flex;
min-height: 100vh;
max-height: 100%;
.spinner{
display: flex;
justify-content: center;
margin: 1em;
}
.linear-text {
background: linear-gradient(89.9deg, rgba(66, 133, 244, 0.95) 21.17%, #A488F5 44.34%, rgba(234, 67, 53, 0.88) 81.26%);
-webkit-background-clip: text;
-moz-background-clip: text;
background-clip: text;
color: transparent;
}
.heading {
font-family: 'Google Sans', sans-serif !important;
font-size: 32px;
font-style: normal;
font-weight: 500;
line-height: 150.7%;
color: rgba(45, 44, 44, 0.7490196078);
}
.left-container {
flex: 0 1 55%;
min-height: 100%;
max-width: 60%;
align-items: center;
background: #fafcfe;
padding: .5em;
overflow: auto;
display: flex;
justify-content: center;
.login-app-text-sub {
color: #161A1D;
font-size: 1rem;
font-style: normal;
font-weight: 400;
line-height: 150.7%;
}
.login-app-text {
// margin-bottom: 2rem;
line-height: 1.5em;
}
.expand-button {
--mdc-fab-container-color: #4285F41A;
--mdc-fab-icon-color: #000000;
border: 1px solid #4285F4;
--mat-mdc-fab-color: #4285F4;
height: 2.2rem;
font-size: 0.8rem;
font-weight: 400;
box-shadow: none;
}
.expand-less-button {
--mdc-fab-container-color: rgba(92, 95, 97, 0.10);
--mdc-fab-icon-color: #000000;
border: 1px solid rgba(92, 95, 97, 0.50);
--mat-mdc-fab-color: rgba(92, 95, 97, 0.50);
margin-bottom: 1em;
height: 2.2rem;
font-size: 0.8rem;
font-weight: 400;
box-shadow: none
}
}
.right-container {
flex: 0 1 45%;
min-height: 100%;
max-width: 80%;
align-items: flex-start;
display: flex;
justify-content: center;
overflow-y: auto;
scrollbar-width: thin;
.login-card {
flex-direction: column;
display: flex;
max-width: 100%;
padding: 3%;
margin: auto;
.logo-icon {
height: 145px;
width: 100%;
// margin-bottom: 2rem;
}
.login-button {
background: #4285F4;
box-sizing: border-box;
border: 1px solid #e0e0e0;
color: #fff;
position: relative;
user-select: none;
cursor: pointer;
outline: none;
-webkit-tap-highlight-color: transparent;
display: inline-block;
font-family: 'Google Sans', sans-serif !important;
font-size: 14px;
font-weight: 500;
white-space: nowrap;
text-decoration: none;
vertical-align: baseline;
text-align: center;
margin: 0;
min-width: 100px;
line-height: 36px;
padding: 0 16px;
border-radius: 4px;
overflow: visible;
transform: translate3d(0, 0, 0);
transition: background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1);
}
.register-text {
display: flex;
flex-direction: column;
align-items: flex-start;
margin: 1em 0em;
font-weight: 400;
color: #161A1D
}
.register-button {
background: white;
box-sizing: border-box;
border: 1px solid #4285F4;
color: #4285F4;
position: relative;
user-select: none;
cursor: pointer;
outline: none;
-webkit-tap-highlight-color: transparent;
display: inline-block;
font-family: 'Google Sans', sans-serif !important;
font-size: 14px;
font-weight: 500;
white-space: nowrap;
text-decoration: none;
vertical-align: baseline;
text-align: center;
margin: 0;
min-width: 64px;
line-height: 36px;
padding: 0 16px;
border-radius: 4px;
overflow: visible;
transform: translate3d(0, 0, 0);
transition: background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1);
}
}
}
.slide {
width: 100%;
min-height: 15rem;
border-radius: 10px;
background-size: cover;
background-position: center;
font-size: 1rem;
font-style: normal;
font-weight: 400;
line-height: 200.7%;
padding-left: 0px;
.feature-heading {
font-weight: 700;
}
}
.slider {
position: relative;
height: 100%;
}
.dotsContainer {
display: flex;
justify-content: center;
align-items: center;
margin-top: 2em;
}
.dot {
margin: 0 3px;
cursor: pointer;
font-size: 18px;
color: #D0D3D7;
}
.active {
color: #1A73E8;
background-color: #D0D3D7;
padding: 2px;
border-radius: 50%;
font-size: 12px;
}
.material-symbols-outlined {
font-variation-settings:
'FILL' 1,
'wght' 400,
'GRAD' 0,
'opsz' 18
}
h6 {
position: relative;
z-index: 1;
overflow: hidden;
text-align: center;
font-size: 1.2em;
font-style: normal;
font-weight: 500;
line-height: 200.7%;
font-family: 'Google Sans', sans-serif !important;
}
h6:before,
h6:after {
position: absolute;
top: 51%;
overflow: hidden;
width: 50%;
height: 1px;
content: '\a0';
background: linear-gradient(86.95deg, #34A853 -65.34%, #FBBC05 -12.75%, #EA4335 25.15%, #1A73E8 103.88%);
}
h6:before {
margin-left: -50%;
text-align: right;
}
}
.login-buttons-container {
padding-inline: 4%;
gap: 12px;
display: flex;
flex-direction: column;
background: #F7F9FF;
padding-top: 4%;
padding-bottom: 4%;
border-radius: 5px;
}
.back-button {
color: #5C5F61 !important;
font-weight: 400 !important;
}
.help-container {
padding-top: 2%;
text-align: center;
}
.blue {
color: #4285F4;
cursor: pointer;
}
mat-form-field {
width: 100%;
}
.user-name-container{
display: flex;
flex-direction: row;
gap: 3%;
}
.error-message {
font-weight: 500;
font-size: 0.8rem;
letter-spacing: -0.005rem;
color: #ed0c0c;
.mat-mdc-form-field-subscript-wrapper {
margin-top: -10px;
}
}
.forgot-password-container {
display: flex;
flex-direction: column;
text-align: center;
padding: 4%;
}
.forgot-password-icon-container {
width: 100%;
height: 100%;
mat-icon{
font-size: 105px;
height: 100%;
width: 100%;
padding: 1%;
padding-top: 7%;
color: #3467bb;
}
}
.forgot-password-heading-container {
font-size: 29px;
font-weight: 600;
font-family: 'Google Sans', sans-serif !important;
line-height: 2.5em;
}
.forgot-password-subtext-container{
display: flex;
flex-direction: column;
text-align: center;
font-size: 19px;
gap: 10px;
line-height: 1.5em;
mat-icon{
color: #4385f4;
}
}
.logo-icon {
height: 170px;
width: 100%;
}
.login-button {
width: 100%;
background: #4285F4;
box-sizing: border-box;
border: 1px solid #e0e0e0;
color: #fff;
position: relative;
user-select: none;
cursor: pointer;
outline: none;
-webkit-tap-highlight-color: transparent;
display: inline-block;
font-family: 'Google Sans', sans-serif !important;
font-size: 14px;
font-weight: 500;
white-space: nowrap;
text-decoration: none;
vertical-align: baseline;
text-align: center;
margin: 0;
min-width: 64px;
line-height: 44px;
padding: 0 16px;
border-radius: 4px;
overflow: visible;
transform: translate3d(0, 0, 0);
transition: background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1);
}
.register-text {
display: flex;
flex-direction: column;
align-items: flex-start;
margin-bottom: 0.3em;
font-weight: 400;
color: #5C5F61
}
.register-button {
width: 100%;
background: white;
box-sizing: border-box;
border: 1px solid #4285F4;
color: #4285F4;
position: relative;
user-select: none;
cursor: pointer;
outline: none;
-webkit-tap-highlight-color: transparent;
display: inline-block;
font-family: 'Google Sans', sans-serif !important;
font-size: 14px;
font-weight: 500;
white-space: nowrap;
text-decoration: none;
vertical-align: baseline;
text-align: center;
margin: 0;
min-width: 64px;
line-height: 44px;
padding: 0 16px;
border-radius: 4px;
overflow: visible;
transform: translate3d(0, 0, 0);
transition: background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1);
}
.send-email-button {
background: #40bd40 !important;
font-weight: 600 !important;
font-size: 16px !important;
}
.close-button {
float: right !important;
}
.forgot-password-link-container {
width: 100%;
text-align: end;
display: flex;
flex-direction: column;
padding-bottom: 4%;
z-index: 100;
position: relative;
}
.button-css {
display: flex;
justify-content: center;
.ok-button{
justify-content: center;
display: flex;
width: 25%;
background: #4285F4;
box-sizing: border-box;
border: 1px solid #e0e0e0;
color: #fff;
position: relative;
user-select: none;
cursor: pointer;
outline: none;
-webkit-tap-highlight-color: transparent;
font-family: 'Google Sans', sans-serif !important;
font-size: 14px;
font-weight: 500;
white-space: nowrap;
text-decoration: none;
vertical-align: baseline;
text-align: center;
margin: 0;
min-width: 64px;
line-height: 44px;
padding: 0 16px;
border-radius: 4px;
overflow: visible;
transform: translate3d(0, 0, 0);
transition: background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1);
}
}
.icon-css {
font-size: 40px;
color: #f3f30aed
}
.text-css {
font-size: 15px;
line-height: 1.45em;
font-family: "Google Sans", sans-serif !important;
color: #645a5a;
}
.logo-container {
display: flex;
flex-direction: row;
justify-content: center;
text-align: center;
mat-icon{
height: 20% !important;
width: 100% !important;
font-size: 70px !important;
color: #4285F4 !important;
}
}
.heading-span {
display: flex;
flex-direction: row;
gap: 8px;
align-items: center;
padding: 2%;
width: 100%;
justify-content: center;
font-size: 18px;
font-weight: 700;
padding-left: 15%;
color: #4285F4 !important;
}
.heading-container {
display: flex;
flex-direction: row;
justify-content: center;
background: aliceblue;
}
.sub-parent-container {
display: flex;
flex-direction: column;
text-align: center;
gap: 15px;
font-style: normal;
line-height: normal;
font-family: 'Google Sans', sans-serif !important;
}
.info {
padding-bottom: 10px !important;
small {
color: #a8a8b5;
}
}
@@ -0,0 +1,38 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {LoginComponent} from './login.component';
describe('LoginComponent', () => {
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [LoginComponent],
}).compileComponents();
fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,92 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Component, NgZone, inject} from '@angular/core';
import {Auth, signInWithPopup, GoogleAuthProvider} from '@angular/fire/auth';
import {Router} from '@angular/router';
import {AuthService} from 'src/app/services/login/auth.service';
import {MatSnackBar} from '@angular/material/snack-bar';
import {ToastMessageComponent} from '../shared/toast-message/toast-message.component';
import {environment} from 'src/environments/environment';
const HOME_ROUTE = '/';
interface LooseObject {
[key: string]: any;
}
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss'],
})
export class LoginComponent {
private readonly auth: Auth = inject(Auth);
private readonly provider: GoogleAuthProvider = new GoogleAuthProvider();
loader = false;
chatbotName: string = environment.chatbotName;
constructor(
private authService: AuthService,
private router: Router,
public ngZone: NgZone,
private _snackBar: MatSnackBar
) {
this.provider.setCustomParameters({
prompt: 'select_account',
});
}
loginWithGoogle() {
this.loader = true;
signInWithPopup(this.auth, this.provider)
.then((result: any) => {
const user = result.user.toJSON();
this.ngZone.run(() => {
this.authService.saveUserSession(user.stsTokenManager.accessToken);
this.redirect(user);
});
})
.catch(error => {
this.loader = false;
if (error.message !== 'Firebase: Error (auth/popup-closed-by-user).') {
this._snackBar.openFromComponent(ToastMessageComponent, {
panelClass: ['red-toast'],
verticalPosition: 'top',
horizontalPosition: 'right',
duration: 5000,
data: {
text: 'Error with SignIn. Please try again later !!!',
icon: 'cross-in-circle-white',
},
});
}
console.error(`Error: ${error}`);
});
}
redirect(user: any) {
const userDetails: LooseObject = {};
userDetails['name'] = user.displayName;
userDetails['email'] = user.email;
userDetails['photoURL'] = user.photoURL;
userDetails['domain'] = user.domain;
(userDetails['uid'] = user.uid),
localStorage.setItem('USER_DETAILS', JSON.stringify(userDetails));
this.loader = false;
this.router.navigate([HOME_ROUTE]);
}
}
@@ -0,0 +1,22 @@
<!--
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="custom-outer-container">
<div class="chat-body-outer">
<app-chatbar></app-chatbar>
</div>
</div>
@@ -0,0 +1,74 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// #chatContainer {
// position: fixed;
// top: 0%;
// bottom: 0%;
// margin: auto;
// width: 90%;
// height: 75%;
// background-color: #f7f9ff;
// right: 0px;
// left: 0px;
// border-radius: 3px;
// display: flex;
// flex-direction: row;
// overflow-y: auto;
// app-sidebar {
// flex-basis: 20%;
// display: block;
// height: 100%;
// border-right: 1px solid #e8e8e8;
// min-width: 20%;
// }
// #chat {
// background-color: #f7f9fa;
// flex-grow: 1;
// }
// }
// .custom-outer-container{
// position: fixed;
// top: 0px;
// bottom: 0px;
// right: 0px;
// left: 0px;
// /* width: 100%; */
// /* height: 100%; */
// /* margin: 0 0; */
// /* padding: 0; */
// margin-top: 4rem;
// background-color: red;
// }
.custom-outer-container {
width: 100%;
height: 100%;
position: absolute;
bottom: 0px;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
.chat-body-outer {
width: 70%;
height: 95%;
}
}
@@ -0,0 +1,38 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {ChatComponent} from './chat.component';
describe('ChatComponent', () => {
let component: ChatComponent;
let fixture: ComponentFixture<ChatComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ChatComponent],
}).compileComponents();
fixture = TestBed.createComponent(ChatComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,31 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Component} from '@angular/core';
import {UserService} from 'src/app/services/user/user.service';
@Component({
selector: 'app-chat',
templateUrl: './chat.component.html',
styleUrls: ['./chat.component.scss'],
})
export class ChatComponent {
userInfo: any;
constructor(private userService: UserService) {
this.userInfo = this.userService.getUserDetails();
}
}
@@ -0,0 +1,121 @@
<!--
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="chat-container">
<div class="chat-body">
<!-- sugesstion along with a response -->
<div class="bot-message" *ngIf="showSuggesstion" class="bot-suggestion-container">
<div class="bot-suggestion-container-child">
<app-suggestion-card [data]="getSuggestionData(this.suggestedQuestionMessage)"
(raiseQuery)="getResponseforSuggestionQuery($event)"
(dismissSuggestionCard)="removeSuggestionElement()"></app-suggestion-card>
</div>
</div>
<!-- Loader -->
<div *ngIf="showLoader">
<div class="loader">
<div class="{{leftContainerClass}}"></div>
<div class="loader-child-container {{rightContainerClass}}">
<div [ngClass]="showLoaderLikeDislikeButtons? 'loader-text-container':''">
<markdown class="variable-binding" [data]="loaderText"></markdown>
</div>
<div *ngIf="showLoaderLikeDislikeButtons" class="like-dislike-button">
</div>
</div>
</div>
</div>
<!-- Chat section for the messages -->
<ng-container *ngFor="let message of conversation;index as i">
<div class="user-message" *ngIf="message.type == 'user'">
<div class="user-message-wrapping-container">
<div class="messageContent">
{{message.body}}
</div>
<div class="user-circle" *ngIf="requiredLogin === 'True' && userService.getUserDetails().photoURL">
<img [src]="userService.getUserDetails().photoURL" class="logo-icon" />
</div>
<div *ngIf="requiredLogin === 'False'" class="user-icon">
<mat-icon class="bypassMenuLogo">person</mat-icon>
</div>
</div>
</div>
<div class="bot-message"
*ngIf="message.type == 'bot' && (!message.suggestedQuestion?.length || 0 > 0)"
id="{{message.botStartTime}}">
<div class="bot-message-wrapping-container">
<div class="bot-message-action">
<div></div>
<!-- <img src="../../../../../assets/images/user-logo.svg" /> -->
</div>
<div class="bot-message-content">
<markdown clipboard class="variable-binding" [data]="message.body"></markdown>
<markdown *ngIf="message.botAnswer" clipboard class="variable-binding"
[data]="getStringData(message.botAnswer)">
</markdown>
<div class="action-content" *ngIf="message.shareable">
<div class="action-message"></div>
<div class="action-butons" *ngIf="message.chat_id">
</div>
</div>
</div>
</div>
<span class="generating-text" *ngIf="message.responseTime">Generated in {{message.responseTime}}s</span>
</div>
</ng-container>
</div>
<div class="chat-footer-outer">
<div [ngStyle]="{'opacity':this.isChatDisabled ? '30%' : '100%' }" class="chat-footer">
<div class="chat-textarea">
<textarea [disabled]="this.isChatDisabled"
[placeholder]="!this.isChatDisabled ? 'Type your question here, make sure to add context' : ''"
[(ngModel)]="chatQuery" (keydown.enter)="submitMessage($event)"
(paste)="removeSuggestionElement();"
(keypress)="removeSuggestionElement();">
</textarea>
</div>
<div class="micIcon">
<button mat-icon-button (click)="startRecording()" matTooltip="Record"
*ngIf="!isRecording"><mat-icon>mic_none</mat-icon>
</button>
<span (click)="stopRecording()" class="recording-container" *ngIf="isRecording">
<span class="blue-dot">&nbsp;</span>
<span class="green-dot">&nbsp;</span>
<span class="yellow-dot">&nbsp;</span>
<button (click)="stopRecording()" style="color: red;" class="reset-button initial" mat-icon-button
matTooltip="Stop Recording" aria-label="record button">
<mat-icon class="material-icons-outlined">stop_circle</mat-icon> </button>
</span>
<button (click)="resetBrowser()" class="reset-button initial" mat-icon-button aria-label="Share icon button">
<span class="material-symbols-rounded send-icon">refresh</span>
</button>
<button mat-icon-button (click)="submitMessage($event)">
<mat-icon class="send-icon">send</mat-icon>
</button>
</div>
</div>
</div>
</div>
@@ -0,0 +1,460 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Component, EventEmitter, Output, OnDestroy} from '@angular/core';
import {ChatService} from 'src/app/services/chat.service';
import {UserService} from 'src/app/services/user/user.service';
import {BroadcastService} from 'src/app/services/broadcast.service';
import {Observable, timeout, ReplaySubject} from 'rxjs';
import {SessionService} from 'src/app/services/user/session.service';
import {Router} from '@angular/router';
import {MatDialog} from '@angular/material/dialog';
import {Chat} from 'src/app/models/chat.model';
import {
animate,
sequence,
state,
style,
transition,
trigger,
} from '@angular/animations';
import {MatSnackBar} from '@angular/material/snack-bar';
import {SpeechToTextService} from 'src/app/services/speech-to-text';
import {Message, SuggestionData} from 'src/app/models/messegeType.model';
import {environment} from 'src/environments/environment';
@Component({
selector: 'app-chatbar',
templateUrl: './chatbar.component.html',
styleUrls: ['./chatbar.component.scss'],
animations: [
trigger('bounce', [
state('true', style({transform: 'translateY(0)', color: '#4285F4'})),
transition('* => true', [
sequence([
style({transform: 'translateY(0)'}),
animate(
'400ms cubic-bezier(0,0,0,1)',
style({transform: 'translateY(-14px)', color: '#4285F4'})
),
animate(
'300ms cubic-bezier(1,0,1,1)',
style({transform: 'translateY(0)', color: '#4285F4'})
),
animate(
'200ms cubic-bezier(0,0,0,1)',
style({transform: 'translateY(-10px)', color: '#4285F4'})
),
animate(
'150ms cubic-bezier(1,0,1,1)',
style({transform: 'translateY(0)', color: '#4285F4'})
),
animate(
'100ms cubic-bezier(0,0,0,1)',
style({transform: 'translateY(-5px)', color: '#4285F4'})
),
animate(
'80ms cubic-bezier(1,0,1,1)',
style({transform: 'translateY(0)', color: '#4285F4'})
),
]),
]),
]),
trigger('delete', [
state('true', style({transform: 'translateY(0)', color: '#ed143d'})),
transition('* => true', [
sequence([
style({transform: 'translateY(0)'}),
animate(
'400ms cubic-bezier(0,0,0,1)',
style({transform: 'translateX(14px)', color: '#ed143d'})
),
animate(
'300ms cubic-bezier(1,0,1,1)',
style({transform: 'translateX(0)', color: '#ed143d'})
),
animate(
'200ms cubic-bezier(0,0,0,1)',
style({transform: 'translateX(-14px)', color: '#ed143d'})
),
animate(
'150ms cubic-bezier(1,0,1,1)',
style({transform: 'translateX(0)', color: '#ed143d'})
),
animate(
'100ms cubic-bezier(0,0,0,1)',
style({transform: 'translateX(7px)', color: '#ed143d'})
),
animate(
'80ms cubic-bezier(1,0,1,1)',
style({transform: 'translateX(0)', color: '#ed143d'})
),
]),
]),
]),
],
})
export class ChatbarComponent implements OnDestroy {
@Output() onSubmit: EventEmitter<any> = new EventEmitter();
isSuggestedQuestion = '';
chatQuery = '';
chatQuery$: Observable<Message>;
showLoader = false;
startTimer = false;
conversation: Message[] = [];
leftContainerClass = '';
rightContainerClass = '';
index = 2;
loaderTextArray: string[] = [
"I am a conversation bot built on Google Cloud's Vertex AI tools.",
'Tool Tip: You can reset the conversation anytime using the reset button next the the text input box.',
'Joke : Why did the robot go on a diet? Because it had too many bytes!',
"Joke : Why can't bicycles stand up by themselves? Because they are two-tired!",
'Joke : How does a computer get drunk? It takes screenshots.',
];
loaderTextTimeout: undefined | ReturnType<typeof setTimeout>;
loaderText = '';
loaderTextIndex = 0;
loaderIndex = 1;
isChatDisabled = false;
botStartTime = 0;
initialQuestion = '';
ticketId = '';
categoryIntent = '';
changeImageInterval: undefined | ReturnType<typeof setTimeout>;
panelOpenState = false;
like = false;
dislike = false;
response: Response | undefined;
loaderSelectedChip = '';
showLoaderLikeDislikeButtons = false;
outOfContextAnswerResponseObject = {
like: false,
dislike: false,
};
showSuggesstion = false;
suggestedQuestionMessage: Message | undefined;
loader_chat_id: any;
questionArray: any[] = [];
/** DEP accordion border colors */
borderColors = ['#4285F4', '#0F9D58', '#F4B400', '#DB4437'];
private readonly destroyed = new ReplaySubject<void>(1);
isRecording = false;
transcribedText = '';
mediaRecorder: MediaRecorder | undefined;
audioChunks: Blob[] = [];
requiredLogin: string = environment.requiredLogin;
constructor(
private router: Router,
public dialog: MatDialog,
private chatService: ChatService,
private sessionService: SessionService,
public userService: UserService,
private broadcastService: BroadcastService,
private _snackBar: MatSnackBar,
private speechToTextService: SpeechToTextService
) {
this.chatQuery$ = this.broadcastService.chatQuery$;
this.chatQuery$.subscribe((value: any) => {
this.conversation.push(value);
});
this.loaderText = this.loaderTextArray[Math.round(Math.random() * 10)];
this.checkIfMessege();
}
setupMediaRecorder(stream: MediaStream) {
this.mediaRecorder = new MediaRecorder(stream);
this.mediaRecorder.ondataavailable = event =>
this.audioChunks.push(event.data);
this.mediaRecorder.onstop = () => this.sendAudioToGCP();
}
async sendAudioToGCP() {
const audioBlob = new Blob(this.audioChunks);
// console.log(audioBlob);
(await this.speechToTextService.transcribeAudio(audioBlob)).subscribe(
(response: any) => {
// console.log(response)
this.chatQuery = response[0];
},
(error: any) => console.error(error)
);
}
ngOnDestroy() {
this.destroyed.next();
this.destroyed.complete();
}
getStringData(obj: any): string {
const str = obj as string;
if (str === '' || str.length === 0) {
return "Sorry! I don't have sufficient information to answer this question at the moment.";
}
return obj as string;
}
getSuggestionData(obj: any): SuggestionData {
return obj as SuggestionData;
}
checkIfMessege() {
if (
this.conversation.length === 1 &&
this.conversation[0].type === 'user'
) {
this.showLoader = true;
this.setTimeoutForLoaderText();
this.setCyclicBackgroundImages();
this.startTimer = true;
this.botStartTime = new Date().getTime();
this.initialQuestion = this.conversation[0].body;
this.pushQuestion(this.initialQuestion);
this.chatService
.postChat(this.conversation[0].body)
.pipe(timeout(90000))
.subscribe({
next: (response: Chat) => this.handleBotResponse(response),
error: err => {
console.error(err);
this.setErrorMessage();
},
});
}
}
pushQuestion(question: string, id?: any) {
this.questionArray.push({question, id});
}
assignId(id: any) {
for (let i = 0; i < this.questionArray.length; i++) {
if (!this.questionArray[i].id) {
this.questionArray[i].id = id;
break;
}
}
}
getQuestion(id: any) {
const questionObj = this.questionArray.find(x => x.id === id);
return questionObj?.question || '';
}
async submitMessage(event: any) {
this.outOfContextAnswerResponseObject = {
like: false,
dislike: false,
};
this.removeSuggestionElement();
this.pushQuestion(this.chatQuery);
this.botStartTime = new Date().getTime();
// keeps the scrollbar to the bottom
const parentElement = document.getElementsByClassName('chat-body');
parentElement[0].scrollTo(0, parentElement[0].scrollHeight);
if (event instanceof KeyboardEvent || event instanceof MouseEvent) {
event.preventDefault();
}
if (!this.chatQuery || this.chatQuery === '') {
return;
}
if (!this.initialQuestion) this.initialQuestion = this.chatQuery;
const singleMessage: Message = {
body: this.chatQuery,
type: 'user',
shareable: false,
};
this.conversation.unshift(singleMessage);
this.chatQuery = '';
this.showLoader = true;
this.setTimeoutForLoaderText();
this.setCyclicBackgroundImages();
this.chatService
.postChat(singleMessage.body)
.pipe(timeout(90000))
.subscribe({
next: (response: Chat) => this.handleBotResponse(response),
error: err => {
console.error(err);
this.setErrorMessage();
},
});
}
setErrorMessage() {
this.clearTimeoutForLoaderText();
this.leftContainerClass = 'left-side-container-error';
this.loaderText = 'Oops something went wrong , please try again.';
this.rightContainerClass = 'right-side-container-error';
this.showLoaderLikeDislikeButtons = false;
}
setAnswerNotFoundText(loaderText: string) {
this.clearTimeoutForLoaderText();
this.showLoader = true;
this.leftContainerClass = 'answer-not-found';
this.rightContainerClass = 'right-side-container-error';
this.loaderText = loaderText + ' Please try asking something else';
this.showLoaderLikeDislikeButtons = true;
}
stopTicketCreationFlow() {
this.isChatDisabled = false;
this.chatQuery = '';
}
setCyclicBackgroundImages() {
if (this.loaderTextIndex === 3) {
this.loaderTextIndex = 0;
}
this.leftContainerClass =
'left-side-container-' + (this.loaderTextIndex + 1);
this.rightContainerClass =
'right-side-container-' + (this.loaderTextIndex + 1);
this.loaderTextIndex++;
}
setTimeoutForLoaderText() {
this.loaderText = this.loaderTextArray[Math.round(Math.random() * 10)];
this.showLoaderLikeDislikeButtons = false;
this.loaderTextTimeout = setInterval(() => {
this.setCyclicLoaderText();
}, 3000);
}
clearTimeoutForLoaderText() {
clearTimeout(this.loaderTextTimeout);
}
setCyclicLoaderText() {
this.loaderText = this.loaderTextArray[Math.round(Math.random() * 10)];
this.loaderIndex++;
}
handleBotResponse(response: Chat) {
const endTime = new Date().getTime();
this.assignId(response?.id);
const singleMesage: Message = {
body: '',
botAnswer: response.answer,
type: 'bot',
responseTime: ((endTime - this.botStartTime) / 1000).toString(),
shareable: true,
categoryIntent: response.intent,
botStartTime: this.botStartTime.toString(),
extras: {
like: false,
dislike: false,
delete: false,
},
chat_id: response.id,
};
this.conversation.unshift(singleMesage);
this.setSuggestedQuestionInChat(response, endTime);
this.showLoader = false;
this.clearTimeoutForLoaderText();
this.isSuggestedQuestion = '';
}
// adds Suggested question as another message
setSuggestedQuestionInChat(response: Chat, endTime: number) {
if (response.suggested_questions?.length || 0 > 0) {
this.showSuggesstion = true;
this.suggestedQuestionMessage = {
body: '',
type: 'bot',
responseTime: ((endTime - this.botStartTime) / 1000).toString(),
shareable: true,
categoryIntent: response.intent,
extras: {
like: false,
dislike: false,
},
suggestedQuestion: response.suggested_questions,
};
setTimeout(() => {
const botResponseElement = document.getElementById(
this.botStartTime.toString()
);
const parentElement = document.getElementsByClassName('chat-body');
const y = botResponseElement?.offsetTop;
if (y) {
parentElement[0].scroll({
top: y - 30,
behavior: 'smooth',
});
}
}, 1000);
}
}
chipControlOnSelect(event: any) {
this.chatQuery = event.target.innerText;
this.submitMessage(event);
}
getResponseforSuggestionQuery(event: any) {
this.isSuggestedQuestion = event;
this.chatQuery = event;
document
.querySelectorAll('.bot-suggestion-container')
.forEach(el => el.remove());
this.submitMessage(event);
}
removeSuggestionElement() {
this.showSuggesstion = false;
document
.querySelectorAll('.bot-suggestion-container')
.forEach(el => el.remove());
}
resetBrowser() {
this.sessionService.createSession();
this.router.navigateByUrl('/');
}
openSnackBar(message: string, color: string) {
this._snackBar.open(message, 'Close', {
panelClass: [color],
horizontalPosition: 'end',
verticalPosition: 'top',
duration: 3000,
});
}
startRecording() {
this.isRecording = true;
this.audioChunks = [];
if (this.mediaRecorder) this.mediaRecorder.start();
}
stopRecording() {
this.isRecording = false;
if (this.mediaRecorder) this.mediaRecorder.stop();
}
}
@@ -0,0 +1,93 @@
<!--
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="parent-container">
<div class="wrapping-container">
<div class="main-div">
<div class="sub-heading">
<div class="name-container">
Hey, {{savedUser.name ? savedUser.name!.split(" ")[0]+'!' : ''}}
</div>
<div class="helper-container">How can I help you today?</div>
</div>
</div>
<div class="chat-footer-outer">
<div class="chat-footer">
<div class="chat-textarea">
<mat-chip-grid class="intent-chip-textarea" [ngClass]="this.intentSelected ? 'intent-chip': 'category-chip'"
style="width: 100%;" #chipGrid aria-label="Enter intent">
<input [matChipInputFor]="chipGrid"
placeholder="Type your question here, make sure to add context or select a card below to get started"
[(ngModel)]="chatQuery" (keydown.enter)="navigate()" />
</mat-chip-grid>
</div>
<div class="micIcon">
<button mat-icon-button (click)="startRecording()" matTooltip="Record"
*ngIf="!isRecording"><mat-icon>mic_none</mat-icon>
</button>
<span class="recording-container" *ngIf="isRecording">
<span class="blue-dot">&nbsp;</span>
<span class="green-dot">&nbsp;</span>
<span class="yellow-dot">&nbsp;</span>
<button (click)="stopRecording()" style="color: red;" class="reset-button initial" mat-icon-button
matTooltip="Stop Recording" aria-label="record button">
<mat-icon class="material-icons-outlined">stop_circle</mat-icon>
</button>
</span>
<button mat-icon-button (click)="navigate()">
<mat-icon>send</mat-icon>
</button>
</div>
</div>
</div>
<div *ngIf="this.intents" class="container main-div margin-div journey-chips">
<div class="cards-outer-container">
<div class="cards-container"
[ngClass]="this.intents.length >= 5 ? 'cards-container-all-categories':'cards-container-few-categories'">
<div
class="intent-container-box {{intent.name}}"
id="{{intent.name}}"
(click)="expandIntentContainer(intent)"
*ngFor="let intent of this.intents">
<div class="card-detail-container">
<div class="card-heading">
{{this.getHumanReadablestring(intent.name)}}
</div>
<div class="{{intent.name}}_close_button_container close-button-container">
<button mat-icon-button class="close-button">
<mat-icon class="close-icon">close</mat-icon>
</button>
</div>
</div>
<div *ngIf="this.chipSelected != intent.name" class="card-content">{{intent.description}}
</div>
<div class="suggested-question-container {{intent.name}}_suggested_questions_container">
<div *ngFor="let question of intent.questions.slice(0,4)">
<mat-chip-row class="suggested-question-chip-row" [highlighted]="this.chipSelected == question" (click)="assignQToChatQuery(question)">
<span class="suggested-question-chip">
<mat-icon class="suggested-question-icon">chat</mat-icon>
<span class="suggested-question-text">{{question}}</span>
</span>
</mat-chip-row>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -0,0 +1,988 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import url("https://fonts.googleapis.com/icon?family=Material+Icons|Material+Icons+Outlined");
:host ::ng-deep mat-form-feild .mat-form-field-appearance-outline .mat-form-field-outline-start {
color: red;
border-radius: 50px !important;
font-size: 10px !important;
}
:host ::ng-deep .mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__leading {
border-radius: 28px 0 0 28px !important;
min-width: 28px !important;
border-color: #babfc4 !important;
}
:host ::ng-deep .mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__trailing {
border-radius: 0 28px 28px 0 !important;
border-color: #babfc4 !important;
}
:host ::ng-deep .mat-form-field-outline-end {
border-radius: 0 28px 28px 0 !important;
}
:host ::ng-deep .mat-mdc-card {
box-shadow: 0px 2px 1px -1px rgb(217 220 224), 0px 1px 1px 0px rgba(0, 0, 0, 0.14), 0px 1px 3px 0px rgb(229 232 234) !important;
}
:host ::ng-deep .mat-mdc-standard-chip.mat-primary.mat-mdc-chip-selected,
.mat-mdc-standard-chip.mat-primary.mat-mdc-chip-highlighted {
--mdc-chip-elevated-container-color: #EDF3FE;
--mdc-chip-elevated-disabled-container-color: #e0e0e0;
--mdc-chip-label-text-color: #4285F4;
;
--mdc-chip-disabled-label-text-color: #4285F4;
;
--mdc-chip-with-icon-icon-color: #4285F4;
;
--mdc-chip-with-icon-disabled-icon-color: #4285F4;
;
--mdc-chip-with-trailing-icon-disabled-trailing-icon-color: #4285F4;
;
--mdc-chip-with-trailing-icon-trailing-icon-color: #4285F4;
;
--mdc-chip-with-icon-selected-icon-color: #4285F4;
;
}
.parent-container {
// background-position: bottom;
// background-image: url("../../../assets/images/footer.png");
// background-repeat: no-repeat;
// background-size: 30%;
// height: calc(100% - 60px);
// overflow: auto;
display: flex;
max-height: calc(100vh - 70px);
height: 100%;
font-family: 'Google Sans', sans-serif !important;
overflow-y: auto;
flex-direction: column;
.img-globe {
// position:absolute;
width: 66%;
}
}
.wrapping-container {
margin-top: 6%;
width: 100%;
padding-bottom: 1.5%;
}
.main-div {
display: flex;
justify-content: center;
align-items: center;
gap: 15px;
font-family: 'Google Sans', sans-serif !important;
}
.text-control {
width: 32%;
}
h1 {
line-height: 50px;
width: 34%;
}
h2 {
font-family: 'Google Sans', sans-serif !important;
}
.header {
-webkit-background-clip: text;
-webkit-text-fill-color: #48b975;
background-clip: text;
font-size: 3rem;
text-align: center;
font-weight: bold;
font-weight: 700;
-webkit-font-smoothing: antialiased;
height: 100%;
font-family: 'Google Sans', sans-serif;
}
.send-button {
color: #babfc4;
}
.logo-icon {
height: 120px;
width: 400px;
margin-bottom: 90px;
margin-left: -20px;
}
.user-info {
display: flex;
justify-content: center;
}
.user-info-column {
display: column;
}
.card-outer-div {
.card-inside-left {
.circle {
width: 100px;
height: 100px;
border: 5px solid rgb(255, 255, 255);
border-radius: 50%;
overflow: hidden;
box-shadow: 0px 2px 1px -1px rgba(0, 0, 0, 0.2), 0px 1px 1px 0px rgba(0, 0, 0, 0.14), 0px 1px 3px 0px rgba(0, 0, 0, 0.12);
}
.circle img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.card-inside-right {
width: 80%;
display: flex;
flex-direction: column;
align-items: center;
color: #747474;
font-size: 0.8rem;
font-weight: 500;
.name {
.title {
font: 1rem;
}
.sub-title {
font-size: 0.9rem;
margin-left: 2%;
}
}
.company {
display: flex;
align-items: center;
.logo {
padding-right: 1%;
}
.text {
width: 100%;
.sub-title {
margin-left: 2%;
}
}
}
}
}
.mat-mdc-standard-chip {
--mdc-chip-label-text-color: #4285F4;
--mdc-chip-elevated-container-color: #fff;
border: 1px solid #4285F4 !important;
font-family: "Google Sans", sans-serif !important;
font-size: 15px !important;
font-weight: 500 !important;
padding-left: 12px !important;
padding-right: 12px !important;
}
.sub-heading {
font-family: 'Google Sans', sans-serif;
width: 66%;
color: #161A1D;
text-align: center;
font-size: 30px;
line-height: 40px;
margin-top: -20px;
text-align: left;
z-index: 999;
}
.genAI {
//background-image: linear-gradient(to left, #4285F4, #DB4437, #F4B400, #0F9D58);
// background-image: linear-gradient(to left, #34A853, #FBBC05, #EA4335, #1A73E8);
background: linear-gradient(86.95deg, #34A853 -65.34%, #FBBC05 -12.75%, #EA4335 25.15%, #1A73E8 103.88%);
-webkit-background-clip: text;
-moz-background-clip: text;
background-clip: text;
color: transparent;
}
.margin-div {
margin-top: -20px;
}
.chat-footer-outer {
display: flex;
align-items: center;
width: 100%;
justify-content: center;
padding-bottom: 4.7%;
padding-top: 1.7%;
.chat-footer {
min-height: 60px;
display: flex;
border: 1px solid #DDE6F6;
box-shadow: 0px 4px 18px rgba(195, 209, 226, 0.25);
border-radius: 30px;
width: 66%;
// box-shadow: 5px 4px 24px rgba(66, 133, 244, 0.15);
background: #F0F4F9;
.chat-textarea {
flex-grow: 1;
display: flex;
align-items: center;
padding-left: 2%;
input {
width: 100%;
height: 45px;
border: 0px;
border-radius: 4px;
resize: none;
outline: none;
padding: 14px;
font-family: 'Google Sans', sans-serif !important;
box-sizing: border-box;
-ms-overflow-style: none;
scrollbar-width: none;
font-size: 0.95rem;
line-height: 1.1rem;
color: #534646;
background: #F0F4F9;
}
input::-webkit-scrollbar {
display: none;
}
}
.micIcon {
display: flex;
align-items: center;
color: #1F1F1F;
margin-top: 2px;
padding-right: 1%;
img {
max-width: 40%;
cursor: pointer;
}
}
}
}
.text-gnosis {
color: var(--White, #FFF);
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 24px;
/* 150% */
letter-spacing: 0.4px;
}
.chip-cancel-button {
color: #002dff;
}
.categories-div {
.mat-mdc-standard-chip {
--mdc-chip-label-text-color: #0F9D58;
--mdc-chip-elevated-container-color: #fff;
border: 1px solid #0F9D58 !important;
font-family: 'Google Sans', sans-serif !important;
// font-weight: 600;
}
}
.intent-chip {
.mat-mdc-standard-chip {
--mdc-chip-label-text-color: #4285F4;
--mdc-chip-elevated-container-color: #fff;
border: 1px solid #4285F4 !important;
font-family: 'Google Sans', sans-serif !important;
// font-weight: 600;
}
.chip-cancel-button {
color: #002dff;
}
}
.category-chip {
.mat-mdc-standard-chip {
--mdc-chip-label-text-color: #0F9D58;
--mdc-chip-elevated-container-color: #fff;
border: 1px solid #0F9D58 !important;
font-family: 'Google Sans', sans-serif !important;
// font-weight: 600;
}
.chip-cancel-button {
color: #007d06;
}
}
.questions-div {
flex-direction: column;
margin-top: 2rem;
margin-bottom: 5rem;
.intent-card {
padding: 0px;
}
.intent-heading {
color: #787978;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: normal;
font-family: 'Google Sans', sans-serif !important;
width: 66%;
}
.intent-container {
border-radius: 8px;
background-color: rgba(246, 249, 254, 1);
width: 66%;
box-shadow: none !important;
flex-shrink: 0;
.selected-question {
// margin: 0.5rem 0rem;
&:hover {
background-color: rgba(66, 133, 244, 0.10);
}
}
.question-item {
display: flex;
justify-content: space-between;
font-family: 'Google Sans', sans-serif !important;
color: #787978;
font-size: 1em;
line-height: 24px;
}
.question-link {
overflow: hidden;
text-overflow: ellipsis;
}
}
}
.intent-chip-textarea ::ng-deep .mdc-evolution-chip-set__chips {
align-items: center !important;
}
.float-button {
position: fixed;
right: 3em;
bottom: 6em;
--mdc-fab-container-color: #4285F4;
}
.name-container {
font-size: 3.5rem;
font-weight: 400;
line-height: 4rem;
font-family: Google Sans, Helvetica Neue, sans-serif;
letter-spacing: -.03em;
background: linear-gradient(89.9deg, rgba(66, 133, 244, 0.95) 0.17%, #A488F5 7.34%, rgba(234, 67, 53, 0.88) 24.26%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-fill-color: transparent;
animation: effect 2s linear;
display: flex;
flex-direction: row;
gap: 1%;
align-items: center;
}
@keyframes effect {
0% {
background: linear-gradient(89.9deg, rgba(240, 216, 214, 0.88) 0.17%, #ebcaec 7.34%, rgba(194, 214, 247, 0.95) 24.26%);
-webkit-background-clip: text;
}
10% {
background: linear-gradient(89.9deg, rgba(227, 237, 253, 0.95) 0.17%, #e5ddfd 7.34%, rgba(186, 210, 248, 0.95) 24.26%);
-webkit-background-clip: text;
}
20% {
background: linear-gradient(89.9deg, rgba(218, 231, 253, 0.95) 0.17%, #dcd1fc 7.34%, rgba(172, 202, 252, 0.95) 24.26%);
-webkit-background-clip: text;
}
30% {
background: linear-gradient(89.9deg, rgba(199, 219, 252, 0.95) 0.17%, #d0c2fa 7.34%, rgba(161, 196, 252, 0.95) 24.26%);
-webkit-background-clip: text;
}
40% {
background: linear-gradient(89.9deg, rgba(169, 201, 253, 0.95) 0.17%, #cdbcff 7.34%, rgba(149, 188, 250, 0.95) 24.26%);
-webkit-background-clip: text;
}
50% {
background: linear-gradient(89.9deg, rgba(200, 220, 253, 0.95) 0.17%, #bba5fc 7.34%, rgba(203, 221, 250, 0.95) 24.26%);
-webkit-background-clip: text;
}
60% {
background: linear-gradient(89.9deg, rgba(227, 237, 253, 0.95) 0.17%, #e5ddfd 7.34%, rgba(186, 210, 248, 0.95) 24.26%);
-webkit-background-clip: text;
}
70% {
background: linear-gradient(89.9deg, rgba(183, 210, 253, 0.95) 0.17%, #e5ddfd 7.34%, rgba(248, 217, 215, 0.88) 24.26%);
-webkit-background-clip: text;
}
80% {
background: linear-gradient(89.9deg, rgba(122, 172, 252, 0.95) 0.17%, #e5ddfd 7.34%, rgba(247, 182, 176, 0.88) 24.26%);
-webkit-background-clip: text;
}
90% {
background: linear-gradient(89.9deg, rgba(94, 148, 236, 0.95) 0.17%, #b49ff1 7.34%, rgba(240, 121, 111, 0.88) 24.26%);
-webkit-background-clip: text;
}
100% {
background: linear-gradient(89.9deg, rgba(66, 133, 244, 0.95) 0.17%, #A488F5 7.34%, rgba(234, 67, 53, 0.88) 24.26%);
-webkit-background-clip: text;
}
}
.helper-container {
white-space: nowrap;
overflow: hidden;
font-size: 3.5rem;
font-weight: 400;
line-height: 4rem;
font-family: Google Sans, Helvetica Neue, sans-serif;
letter-spacing: -.03em;
color: #8f9391;
padding-bottom: 1%;
opacity: 0.5;
animation: typing 2s steps(22), blink .5s step-end infinite alternate;
width: 22ch;
}
@keyframes typing {
from {
width: 0
}
}
@keyframes blink {
50% {
border-color: transparent
}
}
.quick-bot-introduction-container {
width: 100%;
display: flex;
flex-direction: row;
justify-content: center;
.introduction-sub-container {
width: 66%;
justify-content: center;
text-align: center;
display: flex;
flex-direction: column;
padding-top: 2%;
padding-bottom: 2%;
background: radial-gradient(white, #FAFCFF) padding-box, linear-gradient(89.86deg, #34A853 -40.93%, #FBBC05 4.92%, #EA4335 37.97%, #1A73E8 106.62%) border-box;
border-radius: 24px;
border: 1px solid transparent;
}
.logo-image-container {
img {
height: 110px;
width: 110px;
}
}
.logo-text-heading {
font-style: normal;
font-weight: 500;
font-size: 28px;
line-height: 202%;
/* or 65px */
text-align: center;
background: linear-gradient(89.86deg, #34A853 -40.93%, #FBBC05 4.92%, #EA4335 37.97%, #1A73E8 106.62%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-fill-color: transparent;
}
.logo-text-subheading {
font-style: normal;
font-weight: 500;
font-size: 21px;
line-height: 202%;
/* or 48px */
text-align: center;
}
.blue {
color: #4285F4;
}
}
.helpButton {
position: fixed !important;
bottom: 45px !important;
right: 20px !important;
background-color: #4285f4 !important;
color: white !important;
text-decoration: none !important;
border-radius: 60px !important;
height: 41px !important;
display: inline-flex !important;
align-items: center !important;
overflow: hidden !important;
width: auto !important;
max-width: 41px !important;
-webkit-transition: max-width 0.5s !important;
transition: max-width 0.5s !important;
z-index: 1000 !important;
&:hover {
max-width: 300px !important;
}
.text {
white-space: nowrap !important;
padding-right: 15px !important;
font-size: 15px !important;
font-weight: 600 !important;
}
}
::placeholder {
color: #646566;
}
.intent-container-box {
box-sizing: border-box;
width: 239px;
height: 239px;
background: #F0F4F9 !important;
border-radius: 14px;
transition-delay: 2s;
transition: width 8s;
padding: 1%;
cursor: pointer;
}
.selected-intent-box {
box-sizing: border-box;
height: 239px;
border-radius: 14px;
transition-delay: 2s;
transition: width 8s;
cursor: pointer;
width: max-content !important;
max-width: 100%;
display: flex;
flex-direction: column;
gap: 1%;
padding: 1%;
background: linear-gradient(92.1deg, #bdd1f3 19.16%, rgb(199 183 245 / 90%) 79%, rgb(235 165 158 / 90%) 135.24%), linear-gradient(272.04deg, rgba(158, 188, 237, 0.7) 14.93%, rgba(158, 188, 237, 0) 104.51%) !important;
}
.cards-outer-container {
width: 65%;
overflow: auto;
scrollbar-width: none;
}
.cards-outer-container:after {
z-index: -1;
content: '';
width: 10%;
opacity: 0.5 !important;
}
.cards-container-all-categories {
width: max-content;
display: flex;
overflow-x: auto;
gap: 1%;
max-width: 160%;
min-width: 100%;
scrollbar-width: none;
}
.cards-container-few-categories {
width: max-content;
display: flex;
overflow-x: auto;
gap: 1%;
max-width: 160%;
min-width: 100%;
scrollbar-width: none;
justify-content: center;
padding-inline: 5%;
}
.intent-container-box:hover {
background: #DDE3EA !important;
}
.card-detail-container {
width: 100%;
min-width: 250px;
display: flex;
flex-direction: row;
}
.suggested-question-container {
display: none;
}
.selected-intent-suggested-question {
gap: 4%;
display: flex !important;
flex-direction: column;
}
.suggested-question-chip-row {
margin-top: 0.5rem;
.suggested-question-chip {
display: flex;
flex-direction: row;
margin: 0.5rem;
padding: 0.5rem;
width: 100%;
.suggested-question-icon {
padding-right: 0.25rem
}
.suggested-question-text {
margin-left: 0.25rem;
overflow: hidden;
text-overflow: ellipsis;
word-wrap: break-word;
width: 100%;
max-width: 180px;
}
}
}
.close-button-container {
display: none !important;
}
.expand-close-button-container {
display: block !important;
.close-button {
margin-top: -10px !important;
}
}
.card-heading {
padding-top: 2.5%;
font-size: 17px;
color: #1F1F1F;
width: 90%;
}
.card-content {
font-size: 13px;
color: #646566;
padding-top: 4%;
white-space: break-spaces;
text-wrap: balance;
padding-right: 3%;
text-align: left;
width: 85%;
}
::ng-deep .mat-mdc-snack-bar-container {
&.green-snackbar {
--mdc-snackbar-container-color: #0F9D58;
--mat-mdc-snack-bar-button-color: #fff;
--mdc-snackbar-supporting-text-color: #fff;
}
&.red-snackbar {
--mdc-snackbar-container-color: #e9103f;
--mat-mdc-snack-bar-button-color: #fff;
--mdc-snackbar-supporting-text-color: #fff;
}
}
::ng-deep .mat-mdc-dialog-surface {
justify-content: center !important;
display: flex !important;
flex-direction: column !important;
text-align: center !important;
background: #FFFFFF;
border-radius: 20px !important;
box-sizing: border-box;
box-shadow: none !important;
.badge-prompt-parent-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
.badge-greetings-container {
width: 100%;
display: flex;
flex-direction: row;
justify-content: center;
img {
width: 45%;
}
}
.badge-text-container {
font-style: normal;
font-weight: 400;
font-size: 17px;
line-height: 45px;
display: flex;
align-items: center;
color: rgba(92, 95, 97, 0.7);
}
}
}
.blue-dot {
/* Ellipse 1226 */
content: '';
width: 8px;
height: 8px;
left: 1503px;
top: 445px;
display: flow-root;
border-radius: 15px;
/* Blue */
background: #4285F4;
animation: blue-transform 2s infinite;
}
.green-dot {
content: '';
width:8px;
height: 8px;
left: 1522px;
top: 445px;
display: flow-root;
border-radius: 15px;
/* Green */
background: #0F9D58;
animation: green-transform 2s infinite;
}
.yellow-dot {
content: '';
width: 8px;
height: 8px;
left: 1541px;
top: 445px;
display: flow-root;
border-radius: 15px;
/* Yellow */
background: #F4B400;
animation: yellow-transform 2s infinite;
}
.recording-container {
display: flex;
flex-direction: row;
gap: 10px;
cursor: pointer;
align-items: baseline;
}
@keyframes blue-transform {
0% {
transform: translateY(-14px)
}
33% {
transform: translateY(0)
}
66% {
transform: translateY(0)
}
100% {
transform: translateY(-14px)
}
}
@keyframes green-transform {
0% {
transform: translateY(0)
}
33% {
transform: translateY(-14px)
}
66% {
transform: translateY(0)
}
100% {
transform: translateY(0)
}
}
@keyframes yellow-transform {
0% {
transform: translateY(0)
}
33% {
transform: translateY(0)
}
66% {
transform: translateY(-14px)
}
100% {
transform: translateY(0)
}
}
.tooltip-container {
display: flex;
width: 100%;
justify-content: center;
align-items: baseline;
padding-top: 2%;
padding-bottom: 2%;
.tooltip-sub-container {
width: 66%;
display: flex;
align-items: baseline;
justify-content: center;
}
}
.tooltip-icon-container {
display: flex;
flex-direction: row;
// align-self: flex-end;
gap: 10px;
flex-wrap: nowrap;
text-wrap: nowrap;
align-self: center;
color: #5F6368;
.tooltip-label {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
font-size: 16px;
line-height: 20px;
text-align: center;
}
mat-icon {
font-size: 22px !important;
}
}
.tooltip-text-container {
flex-wrap: wrap;
word-wrap: break-word;
justify-content: flex-start;
display: flex;
color: #5F6368;
.tooltip-text {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-size: 16px;
line-height: 20px;
}
}
.dismiss-container {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
font-size: 14px;
line-height: 20px;
/* identical to box height, or 143% */
text-align: center;
/* Blue */
color: #4285F4;
padding-left: 1%;
cursor: pointer;
}
.dismiss-container:hover {
text-decoration: underline;
}
@@ -0,0 +1,38 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {MainComponent} from './main.component';
describe('MainComponent', () => {
let component: MainComponent;
let fixture: ComponentFixture<MainComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [MainComponent],
}).compileComponents();
fixture = TestBed.createComponent(MainComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,312 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {IntentDetails, IntentService} from './../../services/intent.service';
import {Component, TemplateRef, ViewChild} from '@angular/core';
import {Router} from '@angular/router';
import {BroadcastService} from 'src/app/services/broadcast.service';
import {UntypedFormGroup, UntypedFormBuilder} from '@angular/forms';
import {UserService} from 'src/app/services/user/user.service';
import {Message} from 'src/app/models/messegeType.model';
import {SessionService} from 'src/app/services/user/session.service';
import {MatDialog} from '@angular/material/dialog';
import {ReplaySubject} from 'rxjs';
import {MatSnackBar} from '@angular/material/snack-bar';
import {
animate,
sequence,
state,
style,
transition,
trigger,
} from '@angular/animations';
import {SpeechToTextService} from '../../services/speech-to-text';
@Component({
selector: 'app-main',
templateUrl: './main.component.html',
styleUrls: ['./main.component.scss'],
animations: [
trigger('scale', [
state('true', style({transform: 'translateY(0)', color: '#4285F4'})),
transition('* => true', [
sequence([
style({transform: 'translateY(0)'}),
animate(
'450ms cubic-bezier(0,0,0,1)',
style({transform: 'scale(0.8)', color: '#4285F4'})
),
animate(
'400ms cubic-bezier(1,0,1,1)',
style({transform: 'scale(1.2)', color: '#4285F4'})
),
animate(
'350ms cubic-bezier(1,0,1,1)',
style({transform: 'scale(0.8)', color: '#4285F4'})
),
animate(
'250ms cubic-bezier(0,0,0,1)',
style({transform: 'scale(1)', color: '#4285F4'})
),
animate(
'200ms cubic-bezier(0,0,0,1)',
style({transform: 'scale(-1,1)', color: '#4285F4'})
),
animate(
'150ms cubic-bezier(1,0,1,1)',
style({transform: 'scale(-1,1)', color: '#4285F4'})
),
]),
]),
]),
],
})
export class MainComponent {
isRecording = false;
transcribedText = '';
mediaRecorder: MediaRecorder | undefined;
audioChunks: Blob[] = [];
searchForm: UntypedFormGroup;
selectedType = 'chat';
chatQuery = '';
chipSelected = '';
allQuestions: Map<string, string[]> = new Map();
onHover = false;
savedUser;
lastExpandedElement = '';
showTos = false;
showBadge = false;
tooltipTextList: string[] = [];
@ViewChild('userBadgeTemplate', {static: true})
userBadgeTemplate!: TemplateRef<{}>;
intentSelected = false;
intents: IntentDetails[] = [];
dialogRef: any;
private readonly destroyed = new ReplaySubject<void>(1);
toolTipText: string | undefined;
tooltipTextTimeout: undefined | ReturnType<typeof setTimeout>;
constructor(
private router: Router,
private broadcastService: BroadcastService,
private fb: UntypedFormBuilder,
private sessionService: SessionService,
public userService: UserService,
private intentsService: IntentService,
public dialog: MatDialog,
private _snackBar: MatSnackBar,
private speechToTextService: SpeechToTextService
) {
this.intentsService.getAllIntent().subscribe(response => {
this.intents = response.filter(i => i.status === '5');
});
this.searchForm = this.fb.group({
searchTerm: this.fb.control(''),
});
this.savedUser = userService.getUserDetails();
this.sessionService.createSession();
this.setTimeoutForToolTipText();
}
navigate() {
const userMessage: Message = {
body: this.chatQuery,
type: 'user',
shareable: false,
};
this.chatQuery && this.broadcastService.nextChatQuery(userMessage);
this.router.navigateByUrl('/' + this.selectedType);
}
changeSelectedAssistance(assistantType: string) {
this.selectedType = assistantType;
}
chipControlOnSelect(intent: IntentDetails) {
const queryIntent = intent.name;
this.chipSelected = queryIntent;
}
removeIntentSelection() {
this.chipSelected = '';
}
assignQToChatQuery(question: string) {
this.chatQuery = question;
const userMessage: Message = {
body: this.chatQuery,
type: 'user',
shareable: false,
};
this.chatQuery && this.broadcastService.nextChatQuery(userMessage);
this.router.navigateByUrl('/' + this.selectedType);
}
showFullButton() {
this.onHover = true;
}
hideFullButton() {
this.onHover = false;
}
expandIntentContainer(intent: IntentDetails) {
const classNameToFilterElement = intent.name;
this.chipControlOnSelect(intent);
if (this.lastExpandedElement !== '') {
document
.getElementsByClassName(this.lastExpandedElement)[0]
?.classList.add('intent-container-box');
document
.getElementsByClassName(this.lastExpandedElement)[0]
?.classList.remove('selected-intent-box');
document
.getElementsByClassName(
this.lastExpandedElement + '_close_button_container'
)[0]
?.classList.remove('expand-close-button-container');
document
.getElementsByClassName(
this.lastExpandedElement + '_suggested_questions_container'
)[0]
?.classList.remove('selected-intent-suggested-question');
}
if (this.lastExpandedElement === classNameToFilterElement) {
document
.getElementsByClassName(this.lastExpandedElement)[0]
?.classList.remove('selected-intent-box');
document
.getElementsByClassName(
classNameToFilterElement + '_close_button_container'
)[0]
?.classList.remove('expand-close-button-container');
document
.getElementsByClassName(
this.lastExpandedElement + '_suggested_questions_container'
)[0]
?.classList.remove('selected-intent-suggested-question');
this.lastExpandedElement = '';
return;
}
this.lastExpandedElement = classNameToFilterElement;
const elementToExpand = document.getElementsByClassName(
classNameToFilterElement
);
elementToExpand[0]?.classList.remove('intent-container-box');
elementToExpand[0]?.classList.add('selected-intent-box');
const suggestedQuestionElement = document.getElementsByClassName(
classNameToFilterElement + '_suggested_questions_container'
);
suggestedQuestionElement[0]?.classList.add(
'selected-intent-suggested-question'
);
const closeButtonElement = document.getElementsByClassName(
classNameToFilterElement + '_close_button_container'
);
closeButtonElement[0]?.classList.add('expand-close-button-container');
setTimeout(() => {
this.scrollToSelectedElement(classNameToFilterElement);
}, 100);
return;
}
getHumanReadablestring(s: string) {
return s
.replace('_', ' ')
.replace(/(^\w{1})|(\s+\w{1})/g, letter => letter.toUpperCase());
}
scrollToSelectedElement(classNameToFilterElement: string) {
const childElement = document.getElementById(classNameToFilterElement);
childElement?.scrollIntoView();
}
ngOnDestroy() {
this.destroyed.next();
this.destroyed.complete();
}
openSnackBar(message: string, color: string) {
this._snackBar.open(message, 'Close', {
panelClass: [color],
horizontalPosition: 'end',
verticalPosition: 'top',
duration: 3000,
});
}
ngOnInit() {
navigator.mediaDevices
.getUserMedia({audio: true})
.then(stream => this.setupMediaRecorder(stream));
}
setupMediaRecorder(stream: MediaStream) {
this.mediaRecorder = new MediaRecorder(stream);
this.mediaRecorder.ondataavailable = event =>
this.audioChunks.push(event.data);
this.mediaRecorder.onstop = () => this.sendAudioToGCP();
}
startRecording() {
this.isRecording = true;
this.audioChunks = [];
if (this.mediaRecorder) this.mediaRecorder.start();
}
stopRecording() {
this.isRecording = false;
if (this.mediaRecorder) this.mediaRecorder.stop();
}
async sendAudioToGCP() {
const audioBlob = new Blob(this.audioChunks);
// console.log(audioBlob);
(await this.speechToTextService.transcribeAudio(audioBlob)).subscribe(
(response: any) => {
// console.log(response)
this.chatQuery = response[0];
},
(error: any) => console.error(error)
);
}
setTimeoutForToolTipText() {
if (!window.localStorage['showTooltip']) {
this.toolTipText =
this.tooltipTextList[
Math.floor(Math.random() * this.tooltipTextList.length)
];
this.tooltipTextTimeout = setInterval(() => {
this.toolTipText =
this.tooltipTextList[
Math.floor(Math.random() * this.tooltipTextList.length)
];
}, 7000);
}
}
dismissToolTip() {
window.localStorage['showTooltip'] = true;
this.toolTipText = undefined;
clearTimeout(this.tooltipTextTimeout);
}
}
@@ -0,0 +1,185 @@
<!--
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<mat-expansion-panel [expanded]="true" [disabled]="true">
<mat-expansion-panel-header class="panel-heading-container">
<div class="configure-title">
<span>Configure new Playbook</span>
</div>
<div class="intent-buttons-container">
<button
mat-raised-button
class="reset-button"
(click)="discardForm()"
>
<span> Discard </span>
</button>
<button
mat-raised-button
class="reset-button"
(click)="saveForm()"
>
<span>Save</span>
</button>
<div class="spinner-container" *ngIf="this.showSpinner">
<mat-spinner style="width: 60px; height: 60px"></mat-spinner>
</div>
</div>
</mat-expansion-panel-header>
<div [formGroup]="this.intentForm">
<div class="input-field-container">
<mat-checkbox
class="checkbox-margin"
(change)="this.toggleHasExternalDataSource()"
>
Add my own datasource.
</mat-checkbox>
</div>
<div class="input-field-container">
<mat-form-field
class="intent-field"
appearance="outline"
[floatLabel]="'always'"
>
<mat-label>Name</mat-label>
<input
matInput
[formControl]="this.intentForm.controls.name"
placeholder="Intent name"
type="text"
/>
</mat-form-field>
<mat-form-field
class="intent-field"
appearance="outline"
*ngIf="this.hasExternalDataSource"
[floatLabel]="'always'"
>
<mat-label>GCS Path to files</mat-label>
<input
matInput
[formControl]="this.intentForm.controls.gcp_bucket"
placeholder="gs://your-bucket/your-folder"
type="text"
/>
</mat-form-field>
</div>
<div class="input-field-container">
<mat-form-field
class="intent-field"
appearance="outline"
[floatLabel]="'always'"
>
<mat-label>AI Model</mat-label>
<mat-select
[formControl]="this.intentForm.controls.ai_model"
placeholder="AI Model"
>
<mat-optgroup *ngFor="let model of this.models" [label]="model.name">
<mat-option
*ngFor="let model_name of model.models"
[value]="model_name"
>{{ model_name }}</mat-option
>
</mat-optgroup>
</mat-select>
</mat-form-field>
<div class="ai-temperature-field">
<div class="ai-temperature-label-container">
<label>Creativity</label>
<label>{{ this.intentForm.controls.ai_temperature.value }}</label>
</div>
<mat-slider
style="width: 100%"
[max]="2"
[min]="0"
[step]="0.1"
[discrete]="true"
[showTickMarks]="true"
>
<input
matSliderThumb
[formControl]="this.intentForm.controls.ai_temperature"
/>
</mat-slider>
</div>
</div>
<div class="input-field-container">
<mat-form-field
class="prompt-field"
appearance="outline"
[floatLabel]="'always'"
>
<mat-label>Description</mat-label>
<input
matInput
[formControl]="this.intentForm.controls.description"
placeholder="Enter a short description"
type="text"
/>
</mat-form-field>
</div>
<div class="input-field-container">
<mat-form-field
class="prompt-field"
appearance="outline"
[floatLabel]="'always'"
>
<mat-label>Prompt</mat-label>
<textarea
matInput
[formControl]="this.intentForm.controls.prompt"
placeholder="Enter Prompt here"
rows="3"
wrap="soft"
>
</textarea>
</mat-form-field>
</div>
<mat-list>
<mat-list-item
class="questions-field-container"
style="height: 100%"
*ngFor="
let control of this.intentForm.controls.questions.controls;
let i = index
"
>
<mat-form-field
class="search-question-field"
appearance="outline"
[floatLabel]="'always'"
>
<input
matInput
placeholder="Enter a Question"
[formControl]="control"
type="text"
/>
<mat-icon matSuffix (click)="removeQuestion(i)">delete</mat-icon>
</mat-form-field>
</mat-list-item>
<button
mat-button
class="add-question-button"
(click)="addQuestion()"
>
Add Question
</button>
</mat-list>
</div>
</mat-expansion-panel>
@@ -0,0 +1,248 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.panel-heading-container {
font-style: normal;
font-weight: 400;
font-size: 16px;
line-height: 23px;
color: #363e45;
padding-top: 5px;
padding-bottom: 5px;
.configure-title {
display: flex;
align-content: center;
flex-wrap: wrap;
width: 50%;
.inactive-button {
background: rgb(249 249 225) !important;
box-sizing: border-box !important;
border: 1px solid rgba(255, 119, 7, 0.5019607843) !important;
color: rgb(255 119 7 / 93%) !important;
cursor: pointer;
outline: none;
font-family: "Google Sans", sans-serif !important;
font-size: 14px;
font-weight: 500;
margin: 0;
min-width: 64px;
line-height: 36px;
padding: 0px 45px;
height: 60%;
margin-left: 2rem;
border-radius: 48px !important;
display: flex;
justify-content: center;
flex-direction: column;
flex-wrap: wrap;
align-content: center;
align-items: center;
}
.active-button {
background: rgb(226 249 225) !important;
box-sizing: border-box !important;
border: 1px solid rgb(59 153 29 / 84%) !important;
color: rgb(17 193 56 / 93%) !important;
cursor: pointer;
outline: none;
font-family: "Google Sans", sans-serif !important;
font-size: 14px;
font-weight: 500;
margin: 0;
min-width: 64px;
line-height: 36px;
padding: 0px 45px;
height: 60%;
margin-left: 2rem;
border-radius: 48px !important;
display: flex;
justify-content: center;
flex-direction: column;
flex-wrap: wrap;
align-content: center;
align-items: center;
}
}
.intent-buttons-container {
display: flex;
width: 45%;
gap: 2%;
justify-content: flex-end;
padding-top: 1%;
padding-bottom: 1%;
.reset-button {
background: white !important;
box-sizing: border-box !important;
border: 1px solid #4285F4 !important;
color: #4285F4 !important;
cursor: pointer;
outline: none;
font-family: 'Google Sans', sans-serif !important;
font-size: 14px;
font-weight: 500;
margin: 0;
min-width: 64px;
line-height: 36px;
padding: 0px 45px;
height: 100%;
border-radius: 48px !important;
display: flex;
justify-content: center;
flex-direction: column;
flex-wrap: wrap;
align-content: center;
align-items: center;
}
.internal-spinner-container {
height: 31px
}
}
}
.input-field-container {
display: flex;
flex-direction: row;
gap: 2%;
align-items: start;
padding-left: 16px;
padding-right: 16px;
.checkbox-margin {
padding-bottom: 1%;
}
.intent-field {
width: 50%;
.my-icon {
margin-right: 0.5rem;
color: #005cbb;
border-radius: 2rem;
padding: 1rem;
}
}
.prompt-field {
width: 100%;
height: 150px;
}
.ai-temperature-field {
height: 100%;
display: flex;
align-items: center;
width: 50%;
flex-direction: column;
justify-content: center;
.ai-temperature-label-container {
display: flex;
width: 100%;
justify-content: space-between;
}
}
}
.questions-field-container {
height: 100%;
.search-question-field {
width: 100%;
height: 100%;
mat-icon {
color: #363e45;
cursor: pointer;
}
}
}
.add-question-button {
padding-left: 16px;
padding-right: 16px;
color: #4285F4 !important;
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-size: 16px;
line-height: 23px;
}
.image-container {
justify-content: center;
display: flex;
width: 90%;
img {
height: 47%;
width: 47%;
}
}
.delete-confirmation-text {
font-family: 'Google Sans', sans-serif !important;
font-style: normal;
width: 100%;
display: flex;
justify-content: center;
font-size: 20px;
color: #161A1D;
}
.dialog-actions-container {
padding-bottom: 2%;
justify-content: center;
display: flex;
flex-direction: row;
text-align: end;
width: 100%;
padding-right: 2%;
.discard-button {
color: rgba(92, 95, 97, 0.5);
border: 1px solid rgb(92 95 97 / 28%);
border-radius: 40px;
box-shadow: none;
letter-spacing: inherit;
}
.save-button {
background: #4285F4;
color: #fff;
border-radius: 40px;
box-shadow: none;
letter-spacing: inherit;
}
.dialog-delete-button {
border-radius: 30px;
font-family: 'Google Sans', sans-serif !important;
font-style: normal;
font-weight: 500;
font-size: 14px;
line-height: 23px;
letter-spacing: unset;
box-shadow: none;
color: #f70808;
border: 1px solid #f70808;
}
}
@@ -0,0 +1,38 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {CreateIntentFormComponent} from './create-intent-form.component';
describe('CreateIntentFormComponent', () => {
let component: CreateIntentFormComponent;
let fixture: ComponentFixture<CreateIntentFormComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [CreateIntentFormComponent],
}).compileComponents();
fixture = TestBed.createComponent(CreateIntentFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,137 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Component, EventEmitter, Input, Output} from '@angular/core';
import {FormArray, FormControl, FormGroup, Validators} from '@angular/forms';
import {
IntentDetails,
IntentService,
Model,
} from 'src/app/services/intent.service';
import {ToastMessageComponent} from '../../shared/toast-message/toast-message.component';
import {MatSnackBar} from '@angular/material/snack-bar';
@Component({
selector: 'app-create-intent-form',
templateUrl: './create-intent-form.component.html',
styleUrls: ['./create-intent-form.component.scss'],
})
export class CreateIntentFormComponent {
@Input() models: Model[];
@Output() discardFormCreation: EventEmitter<boolean> = new EventEmitter();
hasExternalDataSource = false;
showSpinner = false;
intentForm = new FormGroup({
name: new FormControl<string>('', Validators.required),
gcp_bucket: new FormControl<string>(''),
description: new FormControl<string>('', Validators.required),
prompt: new FormControl<string>('', Validators.required),
ai_model: new FormControl<string>('', Validators.required),
ai_temperature: new FormControl<string>('', Validators.required),
questions: new FormArray<FormControl<string | null>>([
new FormControl<string>(
{value: '', disabled: false},
Validators.required
),
]),
});
constructor(
private snackbar: MatSnackBar,
private service: IntentService
) {}
toggleHasExternalDataSource() {
this.hasExternalDataSource = !this.hasExternalDataSource;
}
removeQuestion(i: number) {
this.intentForm.controls.questions.removeAt(i);
}
addQuestion() {
this.intentForm.controls.questions.push(
new FormControl<string>({value: '', disabled: false}, Validators.required)
);
}
discardForm() {
this.discardFormCreation.emit();
}
saveForm() {
if (
!this.intentForm.valid ||
!this.intentForm.controls.questions.valid ||
(this.hasExternalDataSource &&
this.intentForm.controls.gcp_bucket.value === '')
) {
this.snackbar.openFromComponent(ToastMessageComponent, {
panelClass: ['red-toast'],
verticalPosition: 'top',
horizontalPosition: 'right',
duration: 5000,
data: {
text: 'There is an error on the intent creation form',
icon: 'cross-in-circle-white',
},
});
return;
}
this.showSpinner = true;
const intent: IntentDetails = {
name: this.intentForm.controls.name.value!,
gcp_bucket: this.intentForm.controls.gcp_bucket.value!,
description: this.intentForm.controls.description.value!,
prompt: this.intentForm.controls.prompt.value!,
ai_model: this.intentForm.controls.ai_model.value!,
ai_temperature: this.intentForm.controls.ai_temperature.value!,
questions: this.intentForm.controls.questions.value as string[],
status: '1',
};
this.service.saveIntent(intent).subscribe({
next: () => {
this.showSpinner = false;
this.snackbar.openFromComponent(ToastMessageComponent, {
panelClass: ['green-toast'],
verticalPosition: 'top',
horizontalPosition: 'right',
duration: 5000,
data: {text: 'Intent Saved', icon: 'tick-with-circle'},
});
window.location.reload();
},
error: response => {
const message =
response.error && response.error.detail
? response.error.detail
: 'Error creating intent';
this.showSpinner = false;
this.snackbar.openFromComponent(ToastMessageComponent, {
panelClass: ['red-toast'],
verticalPosition: 'top',
horizontalPosition: 'right',
duration: 5000,
data: {text: message, icon: 'cross-in-circle-white'},
});
},
});
}
}
@@ -0,0 +1,268 @@
<!--
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<mat-expansion-panel>
<mat-expansion-panel-header class="panel-heading-container">
<div class="configure-title">
<span>{{ getHumanReadablestring(this.intent.name) }}</span>
<button
mat-button
class="inactive-button"
disabled="true"
*ngIf="this.intent.status !== '5'"
>
<span>In progress</span>
</button>
<button
mat-button
class="active-button"
disabled="true"
*ngIf="this.intent.status == '5'"
>
<span>Active</span>
</button>
</div>
<div class="intent-buttons-container">
<button
mat-icon-button
class="reset-button"
*ngIf="!this.editMode"
(click)="toggleEditMode()"
>
<mat-icon>edit</mat-icon>
</button>
<button
mat-icon-button
class="reset-button"
*ngIf="!this.editMode"
(click)="showDeleteDialog($event)"
>
<mat-icon>delete</mat-icon>
</button>
<button
mat-raised-button
class="reset-button"
*ngIf="this.editMode"
(click)="toggleEditMode()"
>
<span> Discard Changes </span>
</button>
<button
mat-raised-button
class="reset-button"
*ngIf="this.editMode"
(click)="saveForm()"
>
<span>Save</span>
</button>
<div class="spinner-container" *ngIf="this.showSpinner">
<mat-spinner style="width: 60px; height: 60px"></mat-spinner>
</div>
</div>
</mat-expansion-panel-header>
<div [formGroup]="this.intentForm">
<div class="input-field-container">
<mat-stepper class="stepper" *ngIf="this.intent.status !== '5'">
<mat-step [completed]="this.intent.status >= '1'" [editable]="false">
<ng-template matStepLabel>Index endpoint created</ng-template>
</mat-step>
<mat-step
[errorMessage]="
'There was a problem creating an index with your data'
"
[hasError]="this.intent.status == '2'"
[completed]="this.intent.status >= '3'"
[editable]="false"
>
<ng-template matStepLabel>Index created</ng-template>
</mat-step>
<mat-step
[errorMessage]="'There was a problem deploying your index'"
[hasError]="this.intent.status == '4'"
[completed]="this.intent.status >= '5'"
[editable]="false"
>
<ng-template matStepLabel>Index deployed</ng-template>
</mat-step>
</mat-stepper>
</div>
<div class="input-field-container">
<mat-form-field
class="intent-field"
appearance="outline"
[floatLabel]="'always'"
>
<mat-label>Name</mat-label>
<input
matInput
[formControl]="this.intentForm.controls.name"
placeholder="Intent name"
type="text"
/>
</mat-form-field>
<mat-form-field
*ngIf="this.intent.gcp_bucket"
class="intent-field"
appearance="outline"
[floatLabel]="'always'"
>
<mat-label>GCS Path to files</mat-label>
<input
matInput
[formControl]="this.intentForm.controls.gcp_bucket"
placeholder="gs://your-bucket/your-folder"
type="text"
/>
</mat-form-field>
</div>
<div class="input-field-container">
<mat-form-field
class="intent-field"
appearance="outline"
[floatLabel]="'always'"
>
<mat-label>AI Model</mat-label>
<mat-select
[formControl]="this.intentForm.controls.ai_model"
placeholder="AI Model"
>
<mat-optgroup *ngFor="let model of this.models" [label]="model.name">
<mat-option
*ngFor="let model_name of model.models"
[value]="model_name"
>{{ model_name }}</mat-option
>
</mat-optgroup>
</mat-select>
</mat-form-field>
<div class="ai-temperature-field">
<div class="ai-temperature-label-container">
<label>Creativity</label>
<label>{{ this.intentForm.controls.ai_temperature.value }}</label>
</div>
<mat-slider
style="width: 100%"
[max]="2"
[min]="0"
[step]="0.1"
[discrete]="true"
[showTickMarks]="true"
>
<input
matSliderThumb
[formControl]="this.intentForm.controls.ai_temperature"
/>
</mat-slider>
</div>
</div>
<div class="input-field-container">
<mat-form-field
class="prompt-field"
appearance="outline"
[floatLabel]="'always'"
>
<mat-label>Description</mat-label>
<input
matInput
[formControl]="this.intentForm.controls.description"
placeholder="Enter a short description"
type="text"
/>
</mat-form-field>
</div>
<div class="input-field-container">
<mat-form-field
class="prompt-field"
appearance="outline"
[floatLabel]="'always'"
>
<mat-label>Prompt</mat-label>
<textarea
matInput
[formControl]="this.intentForm.controls.prompt"
placeholder="Enter Prompt here"
rows="3"
wrap="soft"
>
</textarea>
</mat-form-field>
</div>
<mat-list>
<mat-list-item
class="questions-field-container"
style="height: 100%"
*ngFor="
let control of this.intentForm.controls.questions.controls;
let i = index
"
>
<mat-form-field
class="search-question-field"
appearance="outline"
[floatLabel]="'always'"
>
<input
matInput
placeholder="Enter a Question"
[formControl]="control"
type="text"
/>
<mat-icon *ngIf="this.editMode" matSuffix (click)="removeQuestion(i)">delete</mat-icon>
</mat-form-field>
</mat-list-item>
<button
*ngIf="this.editMode"
mat-button
class="add-question-button"
(click)="addQuestion()"
>
Add Question
</button>
</mat-list>
</div>
</mat-expansion-panel>
<ng-template #deleteDialogRef let-data>
<div>
<mat-dialog-content>
<div class="image-container">
<img
src="../../../../assets/images/delete_notification_dialog_image.png"
/>
</div>
<span class="delete-confirmation-text"
>Are you sure you wanna delete this Intent?</span
>
</mat-dialog-content>
<mat-dialog-actions>
<div
class="dialog-actions-container"
>
<button mat-raised-button class="discard-button" mat-dialog-close>
Cancel
</button>
<button
mat-raised-button
class="dialog-delete-button"
(click)="deleteIntent()"
>
Delete
</button>
</div>
</mat-dialog-actions>
</div>
</ng-template>
@@ -0,0 +1,244 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.panel-heading-container {
font-style: normal;
font-weight: 400;
font-size: 16px;
line-height: 23px;
color: #363e45;
padding-top: 5px;
padding-bottom: 5px;
.configure-title {
display: flex;
align-content: center;
flex-wrap: wrap;
width: 50%;
.inactive-button {
background: rgb(249 249 225) !important;
box-sizing: border-box !important;
border: 1px solid rgba(255, 119, 7, 0.5019607843) !important;
color: rgb(255 119 7 / 93%) !important;
cursor: pointer;
outline: none;
font-family: "Google Sans", sans-serif !important;
font-size: 14px;
font-weight: 500;
margin: 0;
min-width: 64px;
line-height: 36px;
padding: 0px 45px;
height: 60%;
margin-left: 2rem;
border-radius: 48px !important;
display: flex;
justify-content: center;
flex-direction: column;
flex-wrap: wrap;
align-content: center;
align-items: center;
}
.active-button {
background: rgb(226 249 225) !important;
box-sizing: border-box !important;
border: 1px solid rgb(59 153 29 / 84%) !important;
color: rgb(17 193 56 / 93%) !important;
cursor: pointer;
outline: none;
font-family: "Google Sans", sans-serif !important;
font-size: 14px;
font-weight: 500;
margin: 0;
min-width: 64px;
line-height: 36px;
padding: 0px 45px;
height: 60%;
margin-left: 2rem;
border-radius: 48px !important;
display: flex;
justify-content: center;
flex-direction: column;
flex-wrap: wrap;
align-content: center;
align-items: center;
}
}
.intent-buttons-container {
display: flex;
width: 45%;
gap: 2%;
justify-content: flex-end;
padding-top: 1%;
padding-bottom: 1%;
.reset-button {
background: white !important;
box-sizing: border-box !important;
border: 1px solid #4285F4 !important;
color: #4285F4 !important;
cursor: pointer;
outline: none;
font-family: 'Google Sans', sans-serif !important;
font-size: 14px;
font-weight: 500;
margin: 0;
min-width: 64px;
line-height: 36px;
padding: 0px 45px;
height: 100%;
border-radius: 48px !important;
display: flex;
justify-content: center;
flex-direction: column;
flex-wrap: wrap;
align-content: center;
align-items: center;
}
.internal-spinner-container {
height: 31px
}
}
}
.input-field-container {
display: flex;
flex-direction: row;
gap: 2%;
align-items: start;
padding-left: 16px;
padding-right: 16px;
.intent-field {
width: 50%;
.my-icon {
margin-right: 0.5rem;
color: #005cbb;
border-radius: 2rem;
padding: 1rem;
}
}
.prompt-field {
width: 100%;
height: 150px;
}
.ai-temperature-field {
height: 100%;
display: flex;
align-items: center;
width: 50%;
flex-direction: column;
justify-content: center;
.ai-temperature-label-container {
display: flex;
width: 100%;
justify-content: space-between;
}
}
}
.questions-field-container {
height: 100%;
.search-question-field {
width: 100%;
height: 100%;
mat-icon {
color: #363e45;
cursor: pointer;
}
}
}
.add-question-button {
padding-left: 16px;
padding-right: 16px;
color: #4285F4 !important;
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-size: 16px;
line-height: 23px;
}
.image-container {
justify-content: center;
display: flex;
width: 90%;
img {
height: 47%;
width: 47%;
}
}
.delete-confirmation-text {
font-family: 'Google Sans', sans-serif !important;
font-style: normal;
width: 100%;
display: flex;
justify-content: center;
font-size: 20px;
color: #161A1D;
}
.dialog-actions-container {
padding-bottom: 2%;
justify-content: center;
display: flex;
flex-direction: row;
text-align: end;
width: 100%;
padding-right: 2%;
.discard-button {
color: rgba(92, 95, 97, 0.5);
border: 1px solid rgb(92 95 97 / 28%);
border-radius: 40px;
box-shadow: none;
letter-spacing: inherit;
}
.save-button {
background: #4285F4;
color: #fff;
border-radius: 40px;
box-shadow: none;
letter-spacing: inherit;
}
.dialog-delete-button {
border-radius: 30px;
font-family: 'Google Sans', sans-serif !important;
font-style: normal;
font-weight: 500;
font-size: 14px;
line-height: 23px;
letter-spacing: unset;
box-shadow: none;
color: #f70808;
border: 1px solid #f70808;
}
}
@@ -0,0 +1,38 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {IntentFormComponent} from './intent-form.component';
describe('IntentFormComponent', () => {
let component: IntentFormComponent;
let fixture: ComponentFixture<IntentFormComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [IntentFormComponent],
}).compileComponents();
fixture = TestBed.createComponent(IntentFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

Some files were not shown because too many files have changed in this diff Show More