+This application supports sending questions to Google Gemini and displays the response. The purpose is to demonstrate how to interface with Google Gemini.
+
+Google Gemini is a recently launched family of large language models (LLMs) created by Google DeepMind. It's considered their most capable AI model yet, designed to compete with OpenAI's GPT-4.
+
+
+
+Here's a breakdown of what Gemini offers:
+
+
+
Multimodal: Unlike prior models, Gemini can understand and work with various data types, including text, code, audio, images, and video. This makes it highly versatile.
+
+
Family of models: Gemini comes in three versions: Gemini Nano, Pro, and Ultra. Each caters to different needs, with Ultra being the most powerful and Pro being the free, user-friendly option.
+
+
Applications: Gemini has various applications. Developers can use it to build AI-powered chatbots and apps. It's also integrated with Google services like Gmail and Maps, allowing you to get help with writing, planning, and learning.
+
+
Chatbot: You might also encounter Gemini as the name of Google's AI chatbot, which was formerly called Bard.
+
+
+
+
+
+Overall, Google Gemini is a significant development in AI, offering a powerful and versatile suite of tools for developers and users alike.
+
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Node-Express/views/partials/nav.ejs b/cloud-run-gemini-chat/Application/CloudRun/Node-Express/views/partials/nav.ejs
new file mode 100644
index 0000000..2c5aea8
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Node-Express/views/partials/nav.ejs
@@ -0,0 +1,21 @@
+
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/.dockerignore b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/.dockerignore
new file mode 100644
index 0000000..b0a3e9e
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/.dockerignore
@@ -0,0 +1,14 @@
+.dockerignore
+.gitignore
+.pylintrc
+Dockerfile
+LICENSE
+*.bat
+*.md
+*.sh
+**.swp
+notes.txt
+save/
+tools/
+venv/
+__pycache__
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/.gcloudignore b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/.gcloudignore
new file mode 100644
index 0000000..008e213
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/.gcloudignore
@@ -0,0 +1,14 @@
+__pycache__
+.dockerignore
+.pylintrc
+.gitignore
+.gcloudignore
+*.bat
+*.md
+*.sh
+*.swp
+LICENSE
+notes.txt
+save/
+venv/
+tools/
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/.pylintrc b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/.pylintrc
new file mode 100644
index 0000000..50d9b8c
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/.pylintrc
@@ -0,0 +1,5 @@
+[MESSAGES CONTROL]
+disable=invalid-name, broad-exception-caught
+
+[FORMAT]
+indent-string="\t"
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/BUILD_DOCKER.md b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/BUILD_DOCKER.md
new file mode 100644
index 0000000..a41049b
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/BUILD_DOCKER.md
@@ -0,0 +1,28 @@
+### Build and Run locally with Docker
+TODO: Publish my Docker container build and run tools.
+
+TODO: Publish my Docker tools
+
+### Example command to build the container:
+
+ - **docker build -t gemini-python-django .**
+
+### Example script to run the container (Docker on Windows):
+
+ - Notice some of the options to use a service account inside the container. On my system, I keep secrets, services accounts, etc in a special directory. This command is setup for development and testing.
+
+```
+@if not defined GCP_PROJECT_ID (
+ @echo Please define the environment variable GCP_PROJECT_ID
+ Exit /B 1
+)
+
+docker run -it --rm --name gemini-python-django ^
+-p 8080:8080 ^
+-v %cd%:/work ^
+-v %APPDATA%\gcloud:/root/.config ^
+-v c:/config:/config ^
+-e GOOGLE_APPLICATION_CREDENTIALS=/config/service-account.json ^
+-e GCP_PROJECT_ID=%GCP_PROJECT_ID% ^
+gemini-python-django
+```
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/BUILD_LINUX.md b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/BUILD_LINUX.md
new file mode 100644
index 0000000..c909158
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/BUILD_LINUX.md
@@ -0,0 +1,50 @@
+### Testing on Linux
+
+ - This application is tested with Python 3.12.
+ - From this directory create a Python virtual environment:
+
+ - **python -m venv venv**
+ - **venv/Scripts/activate.sh**
+
+ - Install dependencies
+ - **python -m pip install -r requirements.txt**
+
+ - Set the environment variable **GCP_PROJECT_ID** to the Google Cloud Project.
+ - Example: `export GCP_PROJECT_ID=myproject-123456`.
+
+ - Configure Google Cloud Secrets Manager with your Google Gemini API Key.
+
+ - Run the application
+ - **python manage.py runserver**
+
+ - Launch a web browser and connect to **http://localhost:8080/**
+
+### Linux Tools for Google Cloud
+
+The **tools/linux** directory contains shell scripts to build and deploy to Google Cloud Run:
+
+- **gcp_build.sh** - Builds the container using Google Cloud Build to Google Artifact Registry.
+- **gcp_deploy.sh** - Deploys the container from Google Artifact Registry to Google Cloud Run.
+- **gcp_check_build_upload.sh** - Output a list of files that will be upload to Google Cloud Buil. Run this command to make sure only required files are uploaded. Runs the command **gcloud meta list-files-for-upload**.
+
+Review both files and make any desired changes to the region, location, repository, etc. The changes must match in both files.
+
+ REGION=us-central1
+ SERVICE_NAME=gemini-python-django-v0
+ IMAGE_NAME=gemini-python-django-v0
+ LOCATION=us-central1
+ REPOSITORY=gemini-project
+
+### Build and Deploy from Linux or WSL
+1. OPTIONAL. From this directory execute `source ./add_tools.sh`. This adds the **tools/linux** directory to the PATH. The alternate is to specify the build tool using the syntax **tools/windows/TOOLNAME**.
+
+ - Example: **./tools/linux/gcp_build.sh**
+2. Set the environment variable **GCP_PROJECT_ID** to the Google Cloud Project.
+
+ - Example: `export GCP_PROJECT_ID=myproject-123456`.
+3. Verify the list of files to be included in the container image:
+ - gcp_check_build_upload.sh**
+4. To build the container execute:
+ - **gcp_build.sh**
+5. To deploy the container to Cloud Run execute:
+ - **gcp_deploy.sh**
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/BUILD_WINDOWS.md b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/BUILD_WINDOWS.md
new file mode 100644
index 0000000..baedd15
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/BUILD_WINDOWS.md
@@ -0,0 +1,50 @@
+### Testing on Windows
+
+ - This application is tested with Python 3.12.
+ - From this directory create a Python virtual environment:
+
+ - **python -m venv venv**
+ - **venv\Scripts\activate.bat**
+
+ - Install dependencies
+ - **python -m pip install -r requirements.txt**
+
+ - Set the environment variable **GCP_PROJECT_ID** to the Google Cloud Project.
+ - Example: `set GCP_PROJECT_ID=myproject-123456`.
+
+ - Configure Google Cloud Secrets Manager with your Google Gemini API Key.
+
+ - Run the application
+ - **python manage.py runserver**
+
+ - Launch a web browser and connect to **http://localhost:8080/**
+
+### Windows Tools for Google Cloud
+
+The **tools\windows** directory contains batch files to build and deploy to Google Cloud Run:
+
+- **gcp_build.bat** - Builds the container using Google Cloud Build to Google Artifact Registry.
+- **gcp_deploy.bat** - Deploys the container from Google Artifact Registry to Google Cloud Run.
+- **gcp_check_build_upload.bat** - Output a list of files that will be upload to Google Cloud Buil. Run this command to make sure only required files are uploaded. Runs the command **gcloud meta list-files-for-upload**.
+
+Review both files and make any desired changes to the region, location, repository, etc. The changes must match in both files.
+
+ @set REGION=us-central1
+ @set SERVICE_NAME=gemini-python-django-v0
+ @set IMAGE_NAME=gemini-python-django-v0
+ @set LOCATION=us-central1
+ @set REPOSITORY=gemini-project
+
+### Build and Deploy from Windows
+1. OPTIONAL. From this directory execute `add_tools.bat`. This adds the **tools\windows** directory to the PATH. The alternate is to specify the build tool using the syntax **tools\windows\TOOLNAME**.
+
+ - Example: **.\tools\windows\gcp_build.bat**
+2. Set the environment variable **GCP_PROJECT_ID** to the Google Cloud Project.
+
+ - Example: `set GCP_PROJECT_ID=myproject-123456`.
+3. Verify the list of files to be included in the container image:
+ - gcp_check_build_upload.bat**
+4. To build the container execute:
+ - **gcp_build.bat**
+5. To deploy the container to Cloud Run execute:
+ - **gcp_deploy.bat**
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/Dockerfile b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/Dockerfile
new file mode 100644
index 0000000..67f4250
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/Dockerfile
@@ -0,0 +1,46 @@
+# Use the official Python 3 image.
+# https://hub.docker.com/_/python
+#
+# python:3 builds a 1060 MB image - 342 MB in Google Container Registry
+# FROM python:3
+#
+# python:3-slim builds a 172 MB image - 60 MB in Google Container Registry
+# FROM python:3-slim
+#
+# python:3-alpine builds a 97 MB image - 32 MB in Google Container Registry
+FROM python:3-alpine
+
+# RUN apt-get update -y
+# RUN apt-get install -y python-pip
+
+# COPY db.sqlite3 /tmp
+
+# Create and change to the app directory.
+WORKDIR /app
+
+COPY . .
+
+# FIX
+# RUN chmod 444 requirements.txt
+
+RUN adduser app -D app
+
+# Run the application as a non-root user.
+USER app
+
+# Fix warning message: WARNING: The script is installed in '/home/app/.local/bin' which is not on PATH.
+ENV PATH=${PATH}:/home/app/.local/bin
+
+RUN python -m pip install --no-cache-dir -r requirements.txt
+
+# Service must listen to $PORT environment variable.
+# This default value facilitates local development.
+ENV PORT 8080
+
+# ENV PYTHONDONTWRITEBYTECODE 1
+ENV PYTHONUNBUFFERED 1
+
+# Run the web service on container startup.
+ENTRYPOINT ["python", "manage.py"]
+# CMD ["runserver", "0.0.0.0:8080"]
+CMD ["runserver"]
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/LICENSE b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/LICENSE
new file mode 100644
index 0000000..f1c7291
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 John J. Hanley
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/README.md b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/README.md
new file mode 100644
index 0000000..75192b2
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/README.md
@@ -0,0 +1,31 @@
+# Gemini Python Django Application
+
+This section contains source code for the Python Django application.
+
+ - Python 3.12 + Django
+
+## Build and Deploy to Google Cloud Run
+
+### Requirements
+
+#### Google Cloud CLI
+
+ - [Google Cloud CLI](https://cloud.google.com/cli). Tested with version 470 (2024-04-26).
+
+#### Google Gemini API Key
+
+ - [Google Gemini API Key](https://aistudio.google.com/app/prompts/new_chat/).
+
+#### Google Cloud Secrets Manager
+
+ - The application reads the Google Gemini API Key in Google Secrets Manager. The secret name is **GEMINI_API_KEY**.
+ - TODO: Publish my tool to create and rotate the secret.
+
+#### Google Cloud Run Permissions
+ - The service account attached to Google Cloud reads the secret from Google Secrets Manager. Add the role **Secret Manager Secret Accessor** to the project's IAM for the service account.
+ - TODO: Pubish my tools to modify IAM permissions
+
+Operating System Specific Instructions:
+ - [Docker](BUILD_DOCKER.md)
+ - [Linux](BUILD_LINUX.md)
+ - [Windows](BUILD_WINDOWS.md)
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/add_tools.bat b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/add_tools.bat
new file mode 100644
index 0000000..8cfd8f3
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/add_tools.bat
@@ -0,0 +1,5 @@
+set TOOLS=%cd%\tools\windows
+
+@set PATH=%TOOLS%;%PATH%
+
+@echo Added tools to the path
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/add_tools.sh b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/add_tools.sh
new file mode 100644
index 0000000..9f5b9fa
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/add_tools.sh
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+TOOLS=`pwd`/tools/linux
+
+export PATH=$PATH:$TOOLS
+
+echo Added tools to the path
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/__init__.py b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/asgi.py b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/asgi.py
new file mode 100644
index 0000000..698e849
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/asgi.py
@@ -0,0 +1,16 @@
+"""
+ASGI config for geminiapp project.
+
+It exposes the ASGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/
+"""
+
+import os
+
+from django.core.asgi import get_asgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'geminiapp.settings')
+
+application = get_asgi_application()
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/gcp_gemini.py b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/gcp_gemini.py
new file mode 100644
index 0000000..081a25e
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/gcp_gemini.py
@@ -0,0 +1,144 @@
+'''
+This module implements the Google Gemini REST API
+ https://ai.google.dev/tutorials/rest_quickstart
+'''
+
+import json
+import requests
+
+#-------------------------------------------------------------------------------
+# Markdown to HTML converter
+#-------------------------------------------------------------------------------
+
+import markdown
+
+#-------------------------------------------------------------------------------
+# This module implements utilities for the Google Gemini REST API
+#-------------------------------------------------------------------------------
+
+from .gcp_gemini_utils import get_gemini_model_endpoint, get_gemini_response_text
+
+def ask_gemini(api_key, model, question):
+ ''' Interact with Google Gemini '''
+
+ #---------------------------------------------------------------------
+ # Google Gemini REST API Documentation
+ # https://ai.google.dev/api/rest
+ #---------------------------------------------------------------------
+
+ #---------------------------------------------------------------------
+ # Validate api_key
+ #---------------------------------------------------------------------
+
+ if api_key is None or len(api_key) == 0:
+ return "Internal Error: Missing API Key"
+
+ #---------------------------------------------------------------------
+ # Verify question parameter
+ #
+ # TODO: Validate question
+ # Only the string length for zero is checked at this time.
+ # What is the maximum length supported. Gemini specifies token
+ # which is about 4 characters.
+ # https://ai.google.dev/models/gemini
+ # Gemini 1.0 Pro input token limit is 30,720.
+ # Gemini 1.5 Pro input token limit is 1,048,576.
+ #---------------------------------------------------------------------
+
+ if question is None or len(question) == 0:
+ return "Please enter a question"
+
+ #---------------------------------------------------------------------
+ # Get the Gemini Model to use
+ #---------------------------------------------------------------------
+
+ url = get_gemini_model_endpoint(model)
+
+ headers = {
+ "x-goog-api-key": api_key,
+ "Content-type": "application/json"
+ }
+
+ #---------------------------------------------------------------------
+ # Format the JSON request
+ #---------------------------------------------------------------------
+
+ data = {
+ "contents": [
+ {
+ "parts":[
+ {
+ "text": question
+ }
+ ]
+ }
+ ]
+ }
+
+ #---------------------------------------------------------------------
+ # Issue the HTTP POST request
+ #---------------------------------------------------------------------
+
+ try:
+ # print(json.dumps(data, indent=4)) # debug
+
+ response = requests.post(url, headers=headers, json=data, timeout=120)
+
+ # print(response.status_code) # debug
+ # print(response.content) # debug
+
+ if response.status_code == 404:
+ print(response.content) # print error message
+ msg = "**Error: The Gemini model is not available for your project or does not exist**"
+ return markdown.markdown(msg)
+
+ if response.status_code >= 400:
+ print(response.content) # print error message
+ msg = "**Error: Request to Gemini failed**"
+ return markdown.markdown(msg)
+
+ #---------------------------------------------------------------------
+ # Process the output
+ # TODO: This needs better handling.
+ # Gemini returns various formats that are not yet documented.
+ # An important item is to process the key "finishReason".
+ # Normal requests return "STOP", but I have seen "SAFETY"
+ # which means the request was rejected. See the link:
+ # https://ai.google.dev/api/rest/v1/GenerateContentResponse#FinishReason
+ #
+ # https://ai.google.dev/api/rest/v1/Content
+ #---------------------------------------------------------------------
+
+ resp = response.json()
+
+ # print(json.dumps(resp, indent=4)) # debug
+
+ reason = resp["candidates"][0]["finishReason"]
+
+ # print("Reason:", reason) # debug
+
+ if reason == "STOP":
+ # Good status
+ text = get_gemini_response_text(resp)
+ elif reason == "MAX_TOKENS":
+ # Error
+ text = "**Gemini Error: The maximum number of tokens as specified in the request was reached.**"
+ elif reason == "RECITATION":
+ # Error
+ text = "**Gemini Error: The candidate content was flagged for recitation reasons.**"
+ elif reason == "SAFETY":
+ # Error
+ text = "**Gemini Error: The candidate content was flagged for safety reasons.**"
+ else:
+ # Error
+ text = f"**Gemini Error: Gemini refused the question for {reason}**"
+
+ #---------------------------------------------------------------------
+ # Gemini returns Markdown, convert to HTML
+ #---------------------------------------------------------------------
+
+ html = markdown.markdown(text, extensions=['tables'])
+ return html.replace('
', '
')
+ except Exception as e:
+ print(e) # print error message
+ return None
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/gcp_gemini_utils.py b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/gcp_gemini_utils.py
new file mode 100644
index 0000000..640a9fc
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/gcp_gemini_utils.py
@@ -0,0 +1,48 @@
+'''
+This module implements utilities for the Google Gemini REST API
+ https://ai.google.dev/tutorials/rest_quickstart
+'''
+
+def get_gemini_model_endpoint(model):
+ ''' Determine which Gemini model to use and return the REST URL '''
+
+ host = "https://generativelanguage.googleapis.com"
+
+ #---------------------------------------------------------------------
+ # Validate model
+ #---------------------------------------------------------------------
+
+ if model is None or len(model) == 0:
+ # Default to model (Gemini 1.0 Pro)
+ model = "gemini_1_0_pro_latest"
+
+ #---------------------------------------------------------------------
+ # Select the Gemini LLM model to use
+ #---------------------------------------------------------------------
+
+ if model == "gemini_1_0_pro_latest":
+ path = "/v1beta/models/gemini-1.0-pro-latest:generateContent"
+ elif model == "gemini_1_0_ultra_latest":
+ path = "/v1beta/models/gemini-1.0-ultra-latest:generateContent"
+ elif model == "gemini_1_5_pro_latest":
+ path = "/v1beta/models/gemini-1.5-pro-latest:generateContent"
+ else:
+ # Default model (Gemini 1.0 Pro)
+ path = "/v1beta/models/gemini-pro:generateContent"
+
+ return host + path
+
+def get_gemini_response_text(resp):
+ ''' Process the Gemini response and return the content text '''
+
+ text = ''
+
+ candidates = resp["candidates"]
+
+ for candidate in candidates:
+ parts = candidate["content"]["parts"]
+
+ for part in parts:
+ text += part.get("text", "")
+
+ return text
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/gcp_secrets.py b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/gcp_secrets.py
new file mode 100644
index 0000000..8fa7872
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/gcp_secrets.py
@@ -0,0 +1,55 @@
+'''
+This module fetches the Gemini API Key stored in Google Secrets Manager
+ https://cloud.google.com/python/docs/reference/secretmanager/latest
+'''
+
+# Google Secrets Manager imports
+from google.cloud import secretmanager_v1
+
+def init_secrets(project_id, secret_name):
+ ''' Fetch the GEMINI_API_KEY stored in Google Secrets Manager '''
+
+ try:
+ #---------------------------------------------------------------------
+ # Secrets Manager reports an error if byte strings are used
+ #---------------------------------------------------------------------
+
+ if isinstance(project_id, bytes):
+ project_id = project_id.decode('utf-8')
+
+ if isinstance(secret_name, bytes):
+ secret_name = secret_name.decode('utf-8')
+
+ #---------------------------------------------------------------------
+ # Initialize the Secrets Manager Client
+ #---------------------------------------------------------------------
+
+ client = secretmanager_v1.SecretManagerServiceClient()
+
+ #---------------------------------------------------------------------
+ # Format the secret name
+ # In app.py, the secret name is set by SECRET_NAME
+ #---------------------------------------------------------------------
+
+ name = f"projects/{project_id}/secrets/{secret_name}/versions/latest"
+
+ #---------------------------------------------------------------------
+ # Build the client request
+ #---------------------------------------------------------------------
+
+ req = secretmanager_v1.AccessSecretVersionRequest(
+ name=name
+ )
+
+ #---------------------------------------------------------------------
+ # Fetch the secret
+ #---------------------------------------------------------------------
+
+ response = client.access_secret_version(request=req)
+
+ api_key = response.payload.data.decode('utf-8')
+
+ return api_key
+ except Exception as e:
+ print(e) # print error message
+ return None
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/gcp_utils.py b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/gcp_utils.py
new file mode 100644
index 0000000..e2b39d2
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/gcp_utils.py
@@ -0,0 +1,56 @@
+'''
+This module implements various Google Cloud Project functions
+'''
+
+import requests
+
+def get_project_id():
+ '''
+ This function reads the Google Cloud Project ID from the Metadata service
+ '''
+
+ try:
+ url = "http://metadata.google.internal/computeMetadata/v1/project/project-id"
+ # url = "http://metadata.goog/v1/project/project-id"
+
+ headers = {
+ "Metadata-Flavor": "Google"
+ }
+
+ response = requests.get(url, headers=headers, timeout=0.5)
+
+ if response.status_code >= 400:
+ print(response.content) # print error message
+ return None
+
+ return response.content.decode("utf-8")
+ except Exception as e:
+ print(e) # print error message
+ return None
+
+def get_client_ip():
+ '''
+ Returns the client IP address.
+ That IP might be IPv4 or IPv6 depending on how the client connected.
+
+ if "x-forwarded-for" in request.headers:
+ return request.headers.getlist("x-forwarded-for")[0].rpartition(" ")[-1]
+
+ return request.remote_addr
+'''
+ return 'FIX IP ADDRESS'
+
+def get_host():
+ '''
+ Returns the host header
+
+ On Google Cloud Run the HTTP Host header cannot be forged.
+ 1) The host header is used by the GFE to know which Cloud Run instance to forware to.
+ 2) The GFE only forwards HTTPS requets. That means the host header must match
+ one of the managed certificates.
+
+ if "host" in request.headers:
+ return request.headers.get("host", "localhost")
+ '''
+
+ return "localhost"
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/settings.py b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/settings.py
new file mode 100644
index 0000000..1dbe6b2
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/settings.py
@@ -0,0 +1,137 @@
+"""
+Django settings for geminiapp project.
+
+Generated by 'django-admin startproject' using Django 5.0.3.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/5.0/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/5.0/ref/settings/
+"""
+
+from pathlib import Path
+
+# Build paths inside the project like this: BASE_DIR / 'subdir'.
+BASE_DIR = Path(__file__).resolve().parent.parent
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+# Generate key with:
+# python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
+SECRET_KEY = '#(j6i_m3zvm9s6lnl$%###_t18nsbaz6)!@y)-37cj$rhx6za5'
+
+# SECURITY WARNING: don't run with debug turned on in production!
+# TODO: Create a method to configure debug
+DEBUG = True
+DEBUG = False
+
+# TODO: FIX allowed hosts
+ALLOWED_HOSTS = ["*"]
+
+# TODO: FIX - how to get this to work? I modified manage.py instead
+# RUNSERVERPLUS_SERVER_ADDRESS_PORT = '0.0.0.0:8080'
+
+# Application definition
+
+INSTALLED_APPS = [
+ 'django.contrib.admin',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+ 'django_extensions',
+ 'geminiapp'
+]
+
+MIDDLEWARE = [
+ 'django.middleware.security.SecurityMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.common.CommonMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+]
+
+ROOT_URLCONF = 'geminiapp.urls'
+
+TEMPLATES = [
+ {
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ 'DIRS': [],
+ 'APP_DIRS': True,
+ 'OPTIONS': {
+ 'context_processors': [
+ 'django.template.context_processors.debug',
+ 'django.template.context_processors.request',
+ 'django.contrib.auth.context_processors.auth',
+ 'django.contrib.messages.context_processors.messages',
+ ],
+ },
+ },
+]
+
+WSGI_APPLICATION = 'geminiapp.wsgi.application'
+
+
+# Database
+# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
+
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+# 'NAME': BASE_DIR / 'db.sqlite3',
+ 'NAME': '/tmp/db.sqlite3',
+ }
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+ {
+ 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+ },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/5.0/topics/i18n/
+
+LANGUAGE_CODE = 'en-us'
+
+TIME_ZONE = 'UTC'
+
+USE_I18N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/5.0/howto/static-files/
+
+STATIC_URL = 'static/'
+STATIC_ROOT = './static'
+
+# Default primary key field type
+# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
+
+DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
+
+USE_X_FORWARDED_HOST = True
+SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/static/app.js b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/static/app.js
new file mode 100644
index 0000000..6ba069a
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/static/app.js
@@ -0,0 +1,102 @@
+// Toggle the Submit button to a Loading... button, or vice versa
+function toggleSubmitButton() {
+ const submitButton = document.querySelector('#input-form button[type=submit]');
+
+ // Flip the value true->false or false->true
+ submitButton.disabled = !submitButton.disabled;
+
+ // Flip the button's text back to "Waiting..."" or "Submit"
+ const submitButtonText = submitButton.querySelector('.submit-button-text');
+ if(submitButtonText.innerHTML === 'Waiting...') {
+ submitButtonText.innerHTML = 'Submit';
+ } else {
+ submitButtonText.innerHTML = 'Waiting...';
+ }
+
+ // Show or Hide the loading spinner
+ const submitButtonSpinner = submitButton.querySelector('.submit-button-spinner')
+ submitButtonSpinner.hidden = !submitButtonSpinner.hidden;
+}
+
+// Process the user's form input
+function processFormInput(form) {
+ // Get values from the form
+ // TODO: FIX CSRF token
+ // const token = form.csrf_token.value.trim();
+ const token = ''
+ const topic = form.topic.value.trim();
+ const model = form.model.value.trim();
+
+ // Update the Submit button to indicate we're done loading
+ toggleSubmitButton();
+
+ // Clear the output of any existing content
+ document.querySelector('#output').innerHTML = '';
+
+ // Send the question
+ send(token, topic, model);
+}
+
+// Source:
+// https://django.readthedocs.io/en/latest/howto/csrf.html#including-the-csrf-token-in-an-unprotected-view
+function getCookie(name) {
+ let cookieValue = null;
+ if (document.cookie && document.cookie !== '') {
+ const cookies = document.cookie.split(';');
+ for (let i = 0; i < cookies.length; i++) {
+ const cookie = cookies[i].trim();
+ // Does this cookie string begin with the name we want?
+ if (cookie.substring(0, name.length + 1) === (name + '=')) {
+ cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
+ break;
+ }
+ }
+ }
+ return cookieValue;
+}
+
+function send(token, topic, model) {
+ const csrftoken = getCookie('csrftoken');
+
+ fetch("ask", {
+ method: "POST",
+ headers: {
+ 'X-CSRFToken': csrftoken,
+ 'Content-type': "application/json"
+ },
+ body: JSON.stringify({ token: token, text: topic, model: model })
+ })
+ .then(response => {
+ return response.json();
+ })
+ .then(data => {
+ document.querySelector('#output').innerHTML = data["text"];
+ toggleSubmitButton();
+ })
+ .catch(error => {
+ console.log(error)
+ toggleSubmitButton();
+ })
+}
+
+function main() {
+ // Wait for the user to submit the form
+ document.querySelector('#input-form').onsubmit = function(e) {
+ // Stop the form from submitting, we'll handle it in the browser with JS
+ e.preventDefault();
+
+ // Process the data in the form, passing the form to the function
+ processFormInput(e.target)
+ };
+
+ // Update the character count when the user enters any text in the topic textarea
+ document.querySelector('#topic').oninput = function(e) {
+ // Get the current length
+ const length = e.target.value.length;
+ // Update the badge text
+ document.querySelector('#topic-badge').innerText = `${length} characters`;
+ }
+}
+
+// Wait for the DOM to be ready before we start
+addEventListener('DOMContentLoaded', main);
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/static/favicon.ico b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/static/favicon.ico
new file mode 100644
index 0000000..98f74ce
Binary files /dev/null and b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/static/favicon.ico differ
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/templates/about.html b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/templates/about.html
new file mode 100644
index 0000000..5fc9fbb
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/templates/about.html
@@ -0,0 +1,45 @@
+{% load static %}
+
+
+
+
+
+
+ Gemini AI
+
+
+
+
+
+
+
+{% include 'nav.html' %}
+
+
+This application supports sending questions to Google Gemini and displays the response. The purpose is to demonstrate how to interface with Google Gemini.
+
+Google Gemini is a recently launched family of large language models (LLMs) created by Google DeepMind. It's considered their most capable AI model yet, designed to compete with OpenAI's GPT-4.
+
+
+
+Here's a breakdown of what Gemini offers:
+
+
+
Multimodal: Unlike prior models, Gemini can understand and work with various data types, including text, code, audio, images, and video. This makes it highly versatile.
+
+
Family of models: Gemini comes in three versions: Gemini Nano, Pro, and Ultra. Each caters to different needs, with Ultra being the most powerful and Pro being the free, user-friendly option.
+
+
Applications: Gemini has various applications. Developers can use it to build AI-powered chatbots and apps. It's also integrated with Google services like Gmail and Maps, allowing you to get help with writing, planning, and learning.
+
+
Chatbot: You might also encounter Gemini as the name of Google's AI chatbot, which was formerly called Bard.
+
+
+
+
+
+Overall, Google Gemini is a significant development in AI, offering a powerful and versatile suite of tools for developers and users alike.
+
+ This AI model's knowledge cutoff is April 2023.
+
+
+ Enter a question for Artificial Intelligence to answer.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% include 'footer.html' %}
+
+
+
+
+
+
+
+
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/templates/nav.html b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/templates/nav.html
new file mode 100644
index 0000000..3a3545b
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/templates/nav.html
@@ -0,0 +1,22 @@
+
+
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/urls.py b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/urls.py
new file mode 100644
index 0000000..01674ee
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/urls.py
@@ -0,0 +1,32 @@
+"""
+URL configuration for geminiapp project.
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+ https://docs.djangoproject.com/en/5.0/topics/http/urls/
+Examples:
+Function views
+ 1. Add an import: from my_app import views
+ 2. Add a URL to urlpatterns: path('', views.home, name='home')
+Class-based views
+ 1. Add an import: from other_app.views import Home
+ 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
+Including another URLconf
+ 1. Import the include() function: from django.urls import include, path
+ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
+"""
+from django.contrib import admin
+from django.urls import path
+
+from . import views
+
+urlpatterns = [
+# path('admin/', admin.site.urls),
+ path('', views.index, name='index'),
+ path('about', views.about, name='about'),
+ path('gemini', views.gemini, name='gemini'),
+ path('ask', views.ask, name='ask'),
+ path('favicon.ico', views.favicon, name='favicon'),
+ path('static/favicon.ico', views.favicon, name='favicon'),
+ path('app.js', views.app_js, name='app_js'),
+ path('static/app.js', views.app_js, name='app_js'),
+]
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/views.py b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/views.py
new file mode 100644
index 0000000..19b7a0b
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/views.py
@@ -0,0 +1,121 @@
+'''
+This application implements the Google Gemini REST API
+'''
+
+import os
+import json
+
+from django.http import HttpResponse, JsonResponse
+from django.views.decorators.csrf import csrf_exempt, csrf_protect, ensure_csrf_cookie
+from django.template import loader
+
+#-------------------------------------------------------------------------------
+# This local module fetches the Gemini API Key stored in Google Secrets Manager
+#-------------------------------------------------------------------------------
+
+from .gcp_secrets import init_secrets
+
+#-------------------------------------------------------------------------------
+# This module implements the Google Gemini REST API
+#-------------------------------------------------------------------------------
+
+from .gcp_gemini import ask_gemini
+
+#-------------------------------------------------------------------------------
+# This module implements various Google Cloud Project functions
+#-------------------------------------------------------------------------------
+
+from .gcp_utils import get_project_id # , get_client_ip, get_host
+
+#-------------------------------------------------------------------------------
+# Set the Project ID for Secrets Manager when running locally.
+# In Cloud Run the Project ID will be read from the Metadata service
+#-------------------------------------------------------------------------------
+
+gcp_project_id = os.environ.get("GCP_PROJECT_ID", None)
+
+#-------------------------------------------------------------------------------
+# Google Cloud Secret Manager secret name
+#-------------------------------------------------------------------------------
+
+SECRET_NAME = "GEMINI_API_KEY"
+
+#-------------------------------------------------------------------------------
+# The Gemini API Key read from Google Secrets Manager
+#-------------------------------------------------------------------------------
+
+gemini_api_key = None
+
+#-------------------------------------------------------------------------------
+# Utils
+#-------------------------------------------------------------------------------
+
+def create_response(msg):
+ ''' Accepts a string (msg) and returns JSON that the web browser app.js expects '''
+ resp = { "text": msg }
+ return resp
+
+@ensure_csrf_cookie
+def index(request, *args, **kwargs):
+ ''' This view serves the home (index) page '''
+ ''' TODO: FIX code to handle Client IP in template '''
+ template = loader.get_template('index.html')
+ return HttpResponse(template.render())
+
+def app_js(request, *args, **kwargs):
+ ''' This view serves the app.js file '''
+ with open("geminiapp/static/app.js", 'rb') as f:
+ data = f.read()
+
+ return HttpResponse(data, headers={'Content-Type': 'text/javascript'})
+
+def favicon(request, *args, **kwargs):
+ ''' This view serves the favicon.ico image '''
+ with open("geminiapp/static/favicon.ico", 'rb') as f:
+ data = f.read()
+
+ return HttpResponse(data, headers={'Content-Type': 'image/vnd.microsoft.icon'})
+
+def about(request, *args, **kwargs):
+ ''' This view serves the about page '''
+ ''' TODO: FIX code to handle Client IP in template '''
+ template = loader.get_template('about.html')
+ return HttpResponse(template.render())
+
+def gemini(request, *args, **kwargs):
+ ''' This view serves the gemini page '''
+ ''' TODO: FIX code to handle Client IP in template '''
+ template = loader.get_template('gemini.html')
+ return HttpResponse(template.render())
+
+# @csrf_exempt # use when testing to turn off CSRF
+@csrf_protect
+def ask(request, *args, **kwargs):
+ ''' Endpoint that accepts a JSON POST request '''
+ data = json.loads(request.body)
+
+ model = data["model"]
+ question = data["text"]
+
+ answer = ask_gemini(gemini_api_key, model, question)
+
+ return JsonResponse(create_response(answer))
+
+#-------------------------------------------------------------------------------
+# BEGIN - Initialize app
+#-------------------------------------------------------------------------------
+
+if gcp_project_id is None:
+ project_id = get_project_id()
+ if project_id is not None:
+ gcp_project_id = project_id
+if gcp_project_id is None:
+ print("Error: Cannot set the Project ID")
+
+if gemini_api_key is None:
+ gemini_api_key = init_secrets(gcp_project_id, SECRET_NAME)
+if gemini_api_key is None:
+ print("Error: Cannot fetch Gemini API Key")
+
+# print(f"Project ID: Key {gcp_project_id}") # debug
+# print(f"Gemini API Key: {gemini_api_key}") # debug
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/wsgi.py b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/wsgi.py
new file mode 100644
index 0000000..604a918
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for geminiapp project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'geminiapp.settings')
+
+application = get_wsgi_application()
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/manage.py b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/manage.py
new file mode 100644
index 0000000..b35fd65
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/manage.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python
+"""Django's command-line utility for administrative tasks."""
+import os
+import sys
+from django.core.management.commands.runserver import Command as runserver
+import shutil
+
+def main():
+ """Run administrative tasks."""
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'geminiapp.settings')
+ try:
+ from django.core.management import execute_from_command_line
+ except ImportError as exc:
+ raise ImportError(
+ "Couldn't import Django. Are you sure it's installed and "
+ "available on your PYTHONPATH environment variable? Did you "
+ "forget to activate a virtual environment?"
+ ) from exc
+ execute_from_command_line(sys.argv)
+
+try:
+ path = os.getcwd() + '/db.sqlite3'
+
+ # print("Copy database") # debug
+ # print(f"Path: {path}") # debug
+
+ shutil.copyfile(path, '/tmp/db.sqlite3')
+except Exception as e:
+ print(e) # print error message
+
+if __name__ == '__main__':
+ port = int(os.environ.get("PORT", 8080))
+ runserver.default_addr = "0.0.0.0"
+ runserver.default_addr_ipv6 = "::"
+ runserver.default_port = port
+ main()
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/requirements.txt b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/requirements.txt
new file mode 100644
index 0000000..7ddfed7
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/requirements.txt
@@ -0,0 +1,4 @@
+Django>=5.0.3
+django-extensions>=3.2.3
+google-cloud-secret-manager>=2.19
+markdown
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/tools/linux/gcp_build.sh b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/tools/linux/gcp_build.sh
new file mode 100644
index 0000000..e06837b
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/tools/linux/gcp_build.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+
+if [[ -z "${GCP_PROJECT_ID}" ]];
+then
+ echo Please define the environment variable GCP_PROJECT_ID
+ exit 1
+fi
+
+# GCP_PROJECT_ID=
+REGION=us-central1
+SERVICE_NAME=gemini-python-django-v0
+IMAGE_NAME=gemini-python-django-v0
+LOCATION=us-central1
+REPOSITORY=gemini-project
+
+echo Building image $LOCATION-docker.pkg.dev/$GCP_PROJECT_ID/$REPOSITORY/$IMAGE_NAME
+
+gcloud builds submit \
+--tag $LOCATION-docker.pkg.dev/$GCP_PROJECT_ID/$REPOSITORY/$IMAGE_NAME
+RET=$?
+
+if [ $RET -eq 0 ];
+then
+ exit 0
+fi
+
+echo "***************************************************************"
+echo "Build Failed Build Failed Build Failed Build Failed"
+echo "***************************************************************"
+
+exit 0
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/tools/linux/gcp_check_build_upload.sh b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/tools/linux/gcp_check_build_upload.sh
new file mode 100644
index 0000000..5a209c9
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/tools/linux/gcp_check_build_upload.sh
@@ -0,0 +1 @@
+gcloud meta list-files-for-upload
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/tools/linux/gcp_deploy.sh b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/tools/linux/gcp_deploy.sh
new file mode 100644
index 0000000..60cd1d6
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/tools/linux/gcp_deploy.sh
@@ -0,0 +1,36 @@
+#!/bin/bash
+
+if [[ -z "${GCP_PROJECT_ID}" ]];
+then
+ echo Please define the environment variable GCP_PROJECT_ID
+ exit 1
+fi
+
+# GCP_PROJECT_ID=
+REGION=us-central1
+SERVICE_NAME=gemini-python-django-v0
+IMAGE_NAME=gemini-python-django-v0
+LOCATION=us-central1
+REPOSITORY=gemini-project
+
+echo Deploying image $LOCATION-docker.pkg.dev/$GCP_PROJECT_ID/$REPOSITORY/$IMAGE_NAME
+
+gcloud run deploy $SERVICE_NAME \
+--region $REGION \
+--image $LOCATION-docker.pkg.dev/$GCP_PROJECT_ID/$REPOSITORY/$IMAGE_NAME \
+--execution-environment=gen1 \
+--memory=256Mi \
+--allow-unauthenticated \
+--platform managed
+RET=$?
+
+if [ $RET -eq 0 ];
+then
+ exit 0
+fi
+
+echo "***************************************************************"
+echo "Deplopy Failed Deplopy Failed Deplopy Failed Deplopy Failed"
+echo "***************************************************************"
+
+exit 0
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/tools/windows/gcp_build.bat b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/tools/windows/gcp_build.bat
new file mode 100644
index 0000000..4b2ceea
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/tools/windows/gcp_build.bat
@@ -0,0 +1,29 @@
+@if not defined GCP_PROJECT_ID (
+ @echo Please define the environment variable GCP_PROJECT_ID
+ Exit /B 1
+)
+
+:: @set GCP_PROJECT_ID=
+@set REGION=us-central1
+@set SERVICE_NAME=gemini-python-django-v0
+@set IMAGE_NAME=gemini-python-django-v0
+@set LOCATION=us-central1
+@set REPOSITORY=gemini-project
+
+:: Items that must be done before deploying to Google Cloud Run
+del \tmp\db.sqlite3
+python manage.py migrate
+copy \tmp\db.sqlite3
+
+call gcloud builds submit ^
+--tag %LOCATION%-docker.pkg.dev/%GCP_PROJECT_ID%/%REPOSITORY%/%IMAGE_NAME%
+@if errorlevel 1 goto err_out
+
+goto end
+
+:err_out
+@echo ***************************************************************
+@echo Build Failed Build Failed Build Failed Build Failed
+@echo ***************************************************************
+
+:end
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/tools/windows/gcp_check_build_upload.bat b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/tools/windows/gcp_check_build_upload.bat
new file mode 100644
index 0000000..5a209c9
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/tools/windows/gcp_check_build_upload.bat
@@ -0,0 +1 @@
+gcloud meta list-files-for-upload
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/tools/windows/gcp_deploy.bat b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/tools/windows/gcp_deploy.bat
new file mode 100644
index 0000000..3c4238c
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/tools/windows/gcp_deploy.bat
@@ -0,0 +1,29 @@
+@if not defined GCP_PROJECT_ID (
+ @echo Please define the environment variable GCP_PROJECT_ID
+ Exit /B 1
+)
+
+:: @set GCP_PROJECT_ID=
+@set REGION=us-central1
+@set SERVICE_NAME=gemini-python-django-v0
+@set IMAGE_NAME=gemini-python-django-v0
+@set LOCATION=us-central1
+@set REPOSITORY=gemini-project
+
+call gcloud run deploy %SERVICE_NAME% ^
+--region %REGION% ^
+--image %LOCATION%-docker.pkg.dev/%GCP_PROJECT_ID%/%REPOSITORY%/%IMAGE_NAME% ^
+--execution-environment=gen1 ^
+--memory=256Mi ^
+--allow-unauthenticated ^
+--platform managed
+@if errorlevel 1 goto err_out
+
+goto end
+
+:err_out
+@echo ***************************************************************
+@echo Deplopy Failed Deplopy Failed Deplopy Failed Deplopy Failed
+@echo ***************************************************************
+
+:end
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/.dockerignore b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/.dockerignore
new file mode 100644
index 0000000..a9506e6
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/.dockerignore
@@ -0,0 +1,15 @@
+.dockerignore
+.gitignore
+.pylintrc
+Dockerfile
+LICENSE
+*.bat
+*.md
+*.sh
+**.swp
+notes.txt
+save/
+tools/
+venv/
+__pycache__
+x
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/.gcloudignore b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/.gcloudignore
new file mode 100644
index 0000000..008e213
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/.gcloudignore
@@ -0,0 +1,14 @@
+__pycache__
+.dockerignore
+.pylintrc
+.gitignore
+.gcloudignore
+*.bat
+*.md
+*.sh
+*.swp
+LICENSE
+notes.txt
+save/
+venv/
+tools/
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/.pylintrc b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/.pylintrc
new file mode 100644
index 0000000..50d9b8c
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/.pylintrc
@@ -0,0 +1,5 @@
+[MESSAGES CONTROL]
+disable=invalid-name, broad-exception-caught
+
+[FORMAT]
+indent-string="\t"
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/BUILD_DOCKER.md b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/BUILD_DOCKER.md
new file mode 100644
index 0000000..fca2040
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/BUILD_DOCKER.md
@@ -0,0 +1,29 @@
+### Build and Run locally with Docker
+TODO: Publish my Docker container build and run tools.
+
+TODO: Publish my Docker tools
+
+### Example command to build the container:
+
+ - **docker build -t gemini-python-flask .**
+
+### Example script to run the container (Docker on Windows):
+
+ - Notice some of the options to use a service account inside the container. On my system, I keep secrets, services accounts, etc in a special directory. This command is setup for development and testing.
+
+```
+@if not defined GCP_PROJECT_ID (
+ @echo Please define the environment variable GCP_PROJECT_ID
+ Exit /B 1
+)
+
+docker run -it --rm --name gemini-python-flask ^
+-p 8080:8080 ^
+-v %cd%:/work ^
+-v %APPDATA%\gcloud:/root/.config ^
+-v c:/config:/config ^
+-e GOOGLE_APPLICATION_CREDENTIALS=/config/service-account.json ^
+-e GCP_PROJECT_ID=%GCP_PROJECT_ID% ^
+-e FLASK_DEBUG=True ^
+gemini-python-flask
+```
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/BUILD_LINUX.md b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/BUILD_LINUX.md
new file mode 100644
index 0000000..2adcf96
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/BUILD_LINUX.md
@@ -0,0 +1,51 @@
+### Testing on Linux
+
+ - This application is tested with Python 3.12.
+ - From this directory create a Python virtual environment:
+
+ - **python -m venv venv**
+ - **venv/Scripts/activate.sh**
+
+ - Install dependencies
+ - **python -m pip install -r requirements.txt**
+
+ - Set the environment variable **GCP_PROJECT_ID** to the Google Cloud Project.
+ - Example: `export GCP_PROJECT_ID=myproject-123456`.
+
+ - Configure Google Cloud Secrets Manager with your Google Gemini API Key.
+
+ - Run the application
+ - **export FLASK_DEBUG=True**
+ - **python app.py**
+
+ - Launch a web browser and connect to **http://localhost:8080/**
+
+### Linux Tools for Google Cloud
+
+The **tools/linux** directory contains shell scripts to build and deploy to Google Cloud Run:
+
+- **gcp_build.sh** - Builds the container using Google Cloud Build to Google Artifact Registry.
+- **gcp_deploy.sh** - Deploys the container from Google Artifact Registry to Google Cloud Run.
+- **gcp_check_build_upload.sh** - Output a list of files that will be upload to Google Cloud Buil. Run this command to make sure only required files are uploaded. Runs the command **gcloud meta list-files-for-upload**.
+
+Review both files and make any desired changes to the region, location, repository, etc. The changes must match in both files.
+
+ REGION=us-central1
+ SERVICE_NAME=gemini-python-flask-v0
+ IMAGE_NAME=gemini-python-flask-v0
+ LOCATION=us-central1
+ REPOSITORY=gemini-project
+
+### Build and Deploy from Linux or WSL
+1. OPTIONAL. From this directory execute `source ./add_tools.sh`. This adds the **tools/linux** directory to the PATH. The alternate is to specify the build tool using the syntax **tools/windows/TOOLNAME**.
+
+ - Example: **./tools/linux/gcp_build.sh**
+2. Set the environment variable **GCP_PROJECT_ID** to the Google Cloud Project.
+
+ - Example: `export GCP_PROJECT_ID=myproject-123456`.
+3. Verify the list of files to be included in the container image:
+ - gcp_check_build_upload.sh**
+4. To build the container execute:
+ - **gcp_build.sh**
+5. To deploy the container to Cloud Run execute:
+ - **gcp_deploy.sh**
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/BUILD_WINDOWS.md b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/BUILD_WINDOWS.md
new file mode 100644
index 0000000..80ab5aa
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/BUILD_WINDOWS.md
@@ -0,0 +1,51 @@
+### Testing on Windows
+
+ - This application is tested with Python 3.12.
+ - From this directory create a Python virtual environment:
+
+ - **python -m venv venv**
+ - **venv\Scripts\activate.bat**
+
+ - Install dependencies
+ - **python -m pip install -r requirements.txt**
+
+ - Set the environment variable **GCP_PROJECT_ID** to the Google Cloud Project.
+ - Example: `set GCP_PROJECT_ID=myproject-123456`.
+
+ - Configure Google Cloud Secrets Manager with your Google Gemini API Key.
+
+ - Run the application
+ - **set FLASK_DEBUG=True**
+ - **python app.py**
+
+ - Launch a web browser and connect to **http://localhost:8080/**
+
+### Windows Tools for Google Cloud
+
+The **tools\windows** directory contains batch files to build and deploy to Google Cloud Run:
+
+- **gcp_build.bat** - Builds the container using Google Cloud Build to Google Artifact Registry.
+- **gcp_deploy.bat** - Deploys the container from Google Artifact Registry to Google Cloud Run.
+- **gcp_check_build_upload.bat** - Output a list of files that will be upload to Google Cloud Buil. Run this command to make sure only required files are uploaded. Runs the command **gcloud meta list-files-for-upload**.
+
+Review both files and make any desired changes to the region, location, repository, etc. The changes must match in both files.
+
+ @set REGION=us-central1
+ @set SERVICE_NAME=gemini-python-flask-v0
+ @set IMAGE_NAME=gemini-python-flask-v0
+ @set LOCATION=us-central1
+ @set REPOSITORY=gemini-project
+
+### Build and Deploy from Windows
+1. OPTIONAL. From this directory execute `add_tools.bat`. This adds the **tools\windows** directory to the PATH. The alternate is to specify the build tool using the syntax **tools\windows\TOOLNAME**.
+
+ - Example: **.\tools\windows\gcp_build.bat**
+2. Set the environment variable **GCP_PROJECT_ID** to the Google Cloud Project.
+
+ - Example: `set GCP_PROJECT_ID=myproject-123456`.
+3. Verify the list of files to be included in the container image:
+ - gcp_check_build_upload.bat**
+4. To build the container execute:
+ - **gcp_build.bat**
+5. To deploy the container to Cloud Run execute:
+ - **gcp_deploy.bat**
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/Dockerfile b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/Dockerfile
new file mode 100644
index 0000000..5968b2f
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/Dockerfile
@@ -0,0 +1,41 @@
+# Use the official Python 3 image.
+# https://hub.docker.com/_/python
+#
+# python:3 builds a 1060 MB image - 342 MB in Google Container Registry
+# FROM python:3
+#
+# python:3-slim builds a 172 MB image - 60 MB in Google Container Registry
+# FROM python:3-slim
+#
+# python:3-alpine builds a 97 MB image - 32 MB in Google Container Registry
+FROM python:3-alpine
+
+# RUN apt-get update -y
+# RUN apt-get install -y python-pip
+
+# Create and change to the app directory.
+WORKDIR /app
+
+COPY . .
+
+RUN chmod 444 app.py requirements.txt
+
+RUN adduser app -D app
+
+# Run the application as a non-root user.
+USER app
+
+# Fix warning message: WARNING: The script is installed in '/home/app/.local/bin' which is not on PATH.
+ENV PATH=${PATH}:/home/app/.local/bin
+
+RUN python -m pip install --no-cache-dir -r requirements.txt
+
+# Service must listen to $PORT environment variable.
+# This default value facilitates local development.
+ENV PORT 8080
+
+ENV PYTHONUNBUFFERED True
+
+# Run the web service on container startup.
+# CMD [ "python", "app.py" ]
+CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 app:app
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/LICENSE b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/LICENSE
new file mode 100644
index 0000000..f1c7291
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 John J. Hanley
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/README.md b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/README.md
new file mode 100644
index 0000000..9db947a
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/README.md
@@ -0,0 +1,31 @@
+# Gemini Python Flask Application
+
+This section contains source code for the Python Flask application.
+
+ - Python 3.12 + Flask
+
+## Build and Deploy to Google Cloud Run
+
+### Requirements
+
+#### Google Cloud CLI
+
+ - [Google Cloud CLI](https://cloud.google.com/cli). Tested with version 470 (2024-04-26).
+
+#### Google Gemini API Key
+
+ - [Google Gemini API Key](https://aistudio.google.com/app/prompts/new_chat/).
+
+#### Google Cloud Secrets Manager
+
+ - The application reads the Google Gemini API Key in Google Secrets Manager. The secret name is **GEMINI_API_KEY**.
+ - TODO: Publish my tool to create and rotate the secret.
+
+#### Google Cloud Run Permissions
+ - The service account attached to Google Cloud reads the secret from Google Secrets Manager. Add the role **Secret Manager Secret Accessor** to the project's IAM for the service account.
+ - TODO: Pubish my tools to modify IAM permissions
+
+Operating System Specific Instructions:
+ - [Docker](BUILD_DOCKER.md)
+ - [Linux](BUILD_LINUX.md)
+ - [Windows](BUILD_WINDOWS.md)
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/add_tools.bat b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/add_tools.bat
new file mode 100644
index 0000000..8cfd8f3
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/add_tools.bat
@@ -0,0 +1,5 @@
+set TOOLS=%cd%\tools\windows
+
+@set PATH=%TOOLS%;%PATH%
+
+@echo Added tools to the path
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/add_tools.sh b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/add_tools.sh
new file mode 100755
index 0000000..9f5b9fa
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/add_tools.sh
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+TOOLS=`pwd`/tools/linux
+
+export PATH=$PATH:$TOOLS
+
+echo Added tools to the path
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/app.py b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/app.py
new file mode 100644
index 0000000..7c22fbf
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/app.py
@@ -0,0 +1,162 @@
+'''
+This application implements the Google Gemini REST API
+'''
+
+import json
+import os
+
+# Flask imports
+from flask import Flask, request, Response, send_from_directory, render_template
+from flask_wtf.csrf import CSRFProtect
+
+#-------------------------------------------------------------------------------
+# This local module fetches the Gemini API Key stored in Google Secrets Manager
+#-------------------------------------------------------------------------------
+
+from gcp_secrets import init_secrets
+
+#-------------------------------------------------------------------------------
+# This module implements the Google Gemini REST API
+#-------------------------------------------------------------------------------
+
+from gcp_gemini import ask_gemini
+
+#-------------------------------------------------------------------------------
+# This module implements various Google Cloud Project functions
+#-------------------------------------------------------------------------------
+
+from gcp_utils import get_project_id, get_client_ip
+
+#-------------------------------------------------------------------------------
+# Set the Project ID for Secrets Manager when running locally.
+# In Cloud Run the Project ID will be read from the Metadata service
+#-------------------------------------------------------------------------------
+
+gcp_project_id = os.environ.get("GCP_PROJECT_ID", None)
+
+#-------------------------------------------------------------------------------
+# Google Cloud Secret Manager secret name
+#-------------------------------------------------------------------------------
+
+SECRET_NAME = "GEMINI_API_KEY"
+
+#-------------------------------------------------------------------------------
+# The Gemini API Key read from Google Secrets Manager
+#-------------------------------------------------------------------------------
+
+gemini_api_key = 'AIzaSyBmdRFzwspedvRUsH34tTjZbGcNpfLVkko'
+
+#-------------------------------------------------------------------------------
+# Create the Flask application
+#-------------------------------------------------------------------------------
+
+app = Flask(__name__)
+
+#-------------------------------------------------------------------------------
+# TODO: Update with a different value
+#-------------------------------------------------------------------------------
+
+app.config["SECRET_KEY"] = "9b37d4693c274cc553524218819305e3fa739a12ed84dc7b43d882d6c3320004"
+
+#-------------------------------------------------------------------------------
+# CSRF - Cross Site Request Forgery
+#-------------------------------------------------------------------------------
+
+# app.config['WTF_CSRF_ENABLED'] = False
+csrf = CSRFProtect(app)
+
+#-------------------------------------------------------------------------------
+# Utils
+#-------------------------------------------------------------------------------
+
+def create_response(msg):
+ ''' Accepts a string (msg) and returns JSON that the web browser app.js expects '''
+ resp = { "text": msg }
+ return json.dumps(resp)
+
+#-------------------------------------------------------------------------------
+# Routes
+#-------------------------------------------------------------------------------
+
+@app.after_request
+def after_request(response):
+ '''
+ Flask does not log the correct client IP address on Cloud Run.
+ Flask logs the proxy (GFE) address when running on Cloud Run.
+ This function parses the HTTP request headers to determine the correct address.
+ '''
+
+ # print(f"Host: {get_host()}")
+ print(f"Client IP: {get_client_ip()}")
+ return response
+
+@app.route("/", methods = ["GET"])
+def home():
+ ''' Return the website home page '''
+ return render_template("index.html", client_ip=get_client_ip())
+
+@app.route("/about", methods = ["GET"])
+def about():
+ ''' Return the website about page '''
+ return render_template("about.html", client_ip=get_client_ip())
+
+@app.route("/gemini", methods = ["GET"])
+def gemini():
+ ''' Return the website gemini page '''
+ return render_template("gemini.html", client_ip=get_client_ip())
+
+@app.route("/app.js", methods = ["GET"])
+def app_js():
+ ''' Return the file app.js '''
+ return send_from_directory("static", "app.js")
+
+@app.route("/favicon.ico", methods = ["GET"])
+def fav_ico():
+ ''' Return the file favicon.ico '''
+ return send_from_directory("static", "favicon.ico", mimetype='image/vnd.microsoft.icon')
+
+# @csrf.exempt
+@app.route("/ask", methods = ["POST"])
+def ask():
+ ''' Endpoint that accepts a JSON POST request '''
+ data = request.get_json()
+
+ model = data.get("model")
+ question = data.get("text")
+
+ answer = ask_gemini(gemini_api_key, model, question)
+
+ return Response(create_response(answer), mimetype='application/json')
+
+#-------------------------------------------------------------------------------
+# BEGIN - Initialize app
+#-------------------------------------------------------------------------------
+
+if gcp_project_id is None:
+ project_id = get_project_id()
+ if project_id is not None:
+ gcp_project_id = project_id
+if gcp_project_id is None:
+ print("Error: Cannot set the Project ID")
+
+if gemini_api_key is None:
+ gemini_api_key = init_secrets(gcp_project_id, SECRET_NAME)
+if gemini_api_key is None:
+ print("Error: Cannot fetch Gemini API Key")
+
+#-------------------------------------------------------------------------------
+# This section only runs if started by Python
+# Does not run under gunicorn
+#-------------------------------------------------------------------------------
+
+if __name__ == "__main__":
+ debugFlag = os.environ.get("FLASK_DEBUG", False)
+
+ if debugFlag == "False":
+ debugFlag = False
+ print('Debug disable')
+ elif debugFlag == "True":
+ debugFlag = True
+ print('Debug enabled')
+
+ app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 8080)))
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/gcp_gemini.py b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/gcp_gemini.py
new file mode 100644
index 0000000..6aae4ca
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/gcp_gemini.py
@@ -0,0 +1,144 @@
+'''
+This module implements the Google Gemini REST API
+ https://ai.google.dev/tutorials/rest_quickstart
+'''
+
+import json
+import requests
+
+#-------------------------------------------------------------------------------
+# Markdown to HTML converter
+#-------------------------------------------------------------------------------
+
+import markdown
+
+#-------------------------------------------------------------------------------
+# This module implements utilities for the Google Gemini REST API
+#-------------------------------------------------------------------------------
+
+from gcp_gemini_utils import get_gemini_model_endpoint, get_gemini_response_text
+
+def ask_gemini(api_key, model, question):
+ ''' Interact with Google Gemini '''
+
+ #---------------------------------------------------------------------
+ # Google Gemini REST API Documentation
+ # https://ai.google.dev/api/rest
+ #---------------------------------------------------------------------
+
+ #---------------------------------------------------------------------
+ # Validate api_key
+ #---------------------------------------------------------------------
+
+ if api_key is None or len(api_key) == 0:
+ return "Internal Error: Missing API Key"
+
+ #---------------------------------------------------------------------
+ # Verify question parameter
+ #
+ # TODO: Validate question
+ # Only the string length for zero is checked at this time.
+ # What is the maximum length supported. Gemini specifies token
+ # which is about 4 characters.
+ # https://ai.google.dev/models/gemini
+ # Gemini 1.0 Pro input token limit is 30,720.
+ # Gemini 1.5 Pro input token limit is 1,048,576.
+ #---------------------------------------------------------------------
+
+ if question is None or len(question) == 0:
+ return "Please enter a question"
+
+ #---------------------------------------------------------------------
+ # Get the Gemini Model to use
+ #---------------------------------------------------------------------
+
+ url = get_gemini_model_endpoint(model)
+
+ headers = {
+ "x-goog-api-key": api_key,
+ "Content-type": "application/json"
+ }
+
+ #---------------------------------------------------------------------
+ # Format the JSON request
+ #---------------------------------------------------------------------
+
+ data = {
+ "contents": [
+ {
+ "parts":[
+ {
+ "text": question
+ }
+ ]
+ }
+ ]
+ }
+
+ #---------------------------------------------------------------------
+ # Issue the HTTP POST request
+ #---------------------------------------------------------------------
+
+ try:
+ # print(json.dumps(data, indent=4)) # debug
+
+ response = requests.post(url, headers=headers, json=data, timeout=120)
+
+ # print(response.status_code) # debug
+ # print(response.content) # debug
+
+ if response.status_code == 404:
+ print(response.content) # print error message
+ msg = "**Error: The Gemini model is not available for your project or does not exist**"
+ return markdown.markdown(msg)
+
+ if response.status_code >= 400:
+ print(response.content) # print error message
+ msg = "**Error: Request to Gemini failed**"
+ return markdown.markdown(msg)
+
+ #---------------------------------------------------------------------
+ # Process the output
+ # TODO: This needs better handling.
+ # Gemini returns various formats that are not yet documented.
+ # An important item is to process the key "finishReason".
+ # Normal requests return "STOP", but I have seen "SAFETY"
+ # which means the request was rejected. See the link:
+ # https://ai.google.dev/api/rest/v1/GenerateContentResponse#FinishReason
+ #
+ # https://ai.google.dev/api/rest/v1/Content
+ #---------------------------------------------------------------------
+
+ resp = response.json()
+
+ # print(json.dumps(resp, indent=4)) # debug
+
+ reason = resp["candidates"][0]["finishReason"]
+
+ # print("Reason:", reason) # debug
+
+ if reason == "STOP":
+ # Good status
+ text = get_gemini_response_text(resp)
+ elif reason == "MAX_TOKENS":
+ # Error
+ text = "**Gemini Error: The maximum number of tokens as specified in the request was reached.**"
+ elif reason == "RECITATION":
+ # Error
+ text = "**Gemini Error: The candidate content was flagged for recitation reasons.**"
+ elif reason == "SAFETY":
+ # Error
+ text = "**Gemini Error: The candidate content was flagged for safety reasons.**"
+ else:
+ # Error
+ text = f"**Gemini Error: Gemini refused the question for {reason}**"
+
+ #---------------------------------------------------------------------
+ # Gemini returns Markdown, convert to HTML
+ #---------------------------------------------------------------------
+
+ html = markdown.markdown(text, extensions=['tables'])
+ return html.replace('
', '
')
+ except Exception as e:
+ print(e) # print error message
+ return None
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/gcp_gemini_utils.py b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/gcp_gemini_utils.py
new file mode 100644
index 0000000..640a9fc
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/gcp_gemini_utils.py
@@ -0,0 +1,48 @@
+'''
+This module implements utilities for the Google Gemini REST API
+ https://ai.google.dev/tutorials/rest_quickstart
+'''
+
+def get_gemini_model_endpoint(model):
+ ''' Determine which Gemini model to use and return the REST URL '''
+
+ host = "https://generativelanguage.googleapis.com"
+
+ #---------------------------------------------------------------------
+ # Validate model
+ #---------------------------------------------------------------------
+
+ if model is None or len(model) == 0:
+ # Default to model (Gemini 1.0 Pro)
+ model = "gemini_1_0_pro_latest"
+
+ #---------------------------------------------------------------------
+ # Select the Gemini LLM model to use
+ #---------------------------------------------------------------------
+
+ if model == "gemini_1_0_pro_latest":
+ path = "/v1beta/models/gemini-1.0-pro-latest:generateContent"
+ elif model == "gemini_1_0_ultra_latest":
+ path = "/v1beta/models/gemini-1.0-ultra-latest:generateContent"
+ elif model == "gemini_1_5_pro_latest":
+ path = "/v1beta/models/gemini-1.5-pro-latest:generateContent"
+ else:
+ # Default model (Gemini 1.0 Pro)
+ path = "/v1beta/models/gemini-pro:generateContent"
+
+ return host + path
+
+def get_gemini_response_text(resp):
+ ''' Process the Gemini response and return the content text '''
+
+ text = ''
+
+ candidates = resp["candidates"]
+
+ for candidate in candidates:
+ parts = candidate["content"]["parts"]
+
+ for part in parts:
+ text += part.get("text", "")
+
+ return text
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/gcp_secrets.py b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/gcp_secrets.py
new file mode 100644
index 0000000..8fa7872
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/gcp_secrets.py
@@ -0,0 +1,55 @@
+'''
+This module fetches the Gemini API Key stored in Google Secrets Manager
+ https://cloud.google.com/python/docs/reference/secretmanager/latest
+'''
+
+# Google Secrets Manager imports
+from google.cloud import secretmanager_v1
+
+def init_secrets(project_id, secret_name):
+ ''' Fetch the GEMINI_API_KEY stored in Google Secrets Manager '''
+
+ try:
+ #---------------------------------------------------------------------
+ # Secrets Manager reports an error if byte strings are used
+ #---------------------------------------------------------------------
+
+ if isinstance(project_id, bytes):
+ project_id = project_id.decode('utf-8')
+
+ if isinstance(secret_name, bytes):
+ secret_name = secret_name.decode('utf-8')
+
+ #---------------------------------------------------------------------
+ # Initialize the Secrets Manager Client
+ #---------------------------------------------------------------------
+
+ client = secretmanager_v1.SecretManagerServiceClient()
+
+ #---------------------------------------------------------------------
+ # Format the secret name
+ # In app.py, the secret name is set by SECRET_NAME
+ #---------------------------------------------------------------------
+
+ name = f"projects/{project_id}/secrets/{secret_name}/versions/latest"
+
+ #---------------------------------------------------------------------
+ # Build the client request
+ #---------------------------------------------------------------------
+
+ req = secretmanager_v1.AccessSecretVersionRequest(
+ name=name
+ )
+
+ #---------------------------------------------------------------------
+ # Fetch the secret
+ #---------------------------------------------------------------------
+
+ response = client.access_secret_version(request=req)
+
+ api_key = response.payload.data.decode('utf-8')
+
+ return api_key
+ except Exception as e:
+ print(e) # print error message
+ return None
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/gcp_utils.py b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/gcp_utils.py
new file mode 100644
index 0000000..d552036
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/gcp_utils.py
@@ -0,0 +1,56 @@
+'''
+This module implements various Google Cloud Project functions
+'''
+# Flask imports
+from flask import request
+
+import requests
+
+def get_project_id():
+ '''
+ This function reads the Google Cloud Project ID from the Metadata service
+ '''
+
+ try:
+ url = "http://metadata.google.internal/computeMetadata/v1/project/project-id"
+ # url = "http://metadata.goog/v1/project/project-id"
+
+ headers = {
+ "Metadata-Flavor": "Google"
+ }
+
+ response = requests.get(url, headers=headers, timeout=0.5)
+
+ if response.status_code >= 400:
+ print(response.content) # print error message
+ return None
+
+ return response.content.decode("utf-8")
+ except Exception as e:
+ print(e) # print error message
+ return None
+
+def get_client_ip():
+ '''
+ Returns the client IP address.
+ That IP might be IPv4 or IPv6 depending on how the client connected.
+ '''
+
+ if "x-forwarded-for" in request.headers:
+ return request.headers.getlist("x-forwarded-for")[0].rpartition(" ")[-1]
+
+ return request.remote_addr
+
+def get_host():
+ '''
+ Returns the host header
+
+ On Google Cloud Run the HTTP Host header cannot be forged.
+ 1) The host header is used by the GFE to know which Cloud Run instance to forware to.
+ 2) The GFE only forwards HTTPS requets. That means the host header must match
+ one of the managed certificates.
+ '''
+ if "host" in request.headers:
+ return request.headers.get("host", "localhost")
+
+ return "localhost"
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/requirements.txt b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/requirements.txt
new file mode 100644
index 0000000..766dfc4
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/requirements.txt
@@ -0,0 +1,5 @@
+Flask>=3.0
+Flask-WTF>=1.2
+google-cloud-secret-manager>=2.19
+markdown>=3.6
+gunicorn>=21.2
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/static/app.js b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/static/app.js
new file mode 100644
index 0000000..18fe53c
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/static/app.js
@@ -0,0 +1,80 @@
+// Toggle the Submit button to a Loading... button, or vice versa
+function toggleSubmitButton() {
+ const submitButton = document.querySelector('#input-form button[type=submit]');
+
+ // Flip the value true->false or false->true
+ submitButton.disabled = !submitButton.disabled;
+
+ // Flip the button's text back to "Waiting..."" or "Submit"
+ const submitButtonText = submitButton.querySelector('.submit-button-text');
+ if(submitButtonText.innerHTML === 'Waiting...') {
+ submitButtonText.innerHTML = 'Submit';
+ } else {
+ submitButtonText.innerHTML = 'Waiting...';
+ }
+
+ // Show or Hide the loading spinner
+ const submitButtonSpinner = submitButton.querySelector('.submit-button-spinner')
+ submitButtonSpinner.hidden = !submitButtonSpinner.hidden;
+}
+
+// Process the user's form input
+function processFormInput(form) {
+ // Get values from the form
+ const token = form.csrf_token.value.trim();
+ const topic = form.topic.value.trim();
+ const model = form.model.value.trim();
+
+ // Update the Submit button to indicate we're done loading
+ toggleSubmitButton();
+
+ // Clear the output of any existing content
+ document.querySelector('#output').innerHTML = '';
+
+ // Send the question
+ send(token, topic, model);
+}
+
+function send(token, topic, model) {
+ fetch("ask", {
+ method: "POST",
+ headers: {
+ 'X-CSRFToken': token,
+ 'Content-type': "application/json"
+ },
+ body: JSON.stringify({ token: token, text: topic, model: model })
+ })
+ .then(response => {
+ return response.json();
+ })
+ .then(data => {
+ document.querySelector('#output').innerHTML = data["text"];
+ toggleSubmitButton();
+ })
+ .catch(error => {
+ console.log(error)
+ toggleSubmitButton();
+ })
+}
+
+function main() {
+ // Wait for the user to submit the form
+ document.querySelector('#input-form').onsubmit = function(e) {
+ // Stop the form from submitting, we'll handle it in the browser with JS
+ e.preventDefault();
+
+ // Process the data in the form, passing the form to the function
+ processFormInput(e.target)
+ };
+
+ // Update the character count when the user enters any text in the topic textarea
+ document.querySelector('#topic').oninput = function(e) {
+ // Get the current length
+ const length = e.target.value.length;
+ // Update the badge text
+ document.querySelector('#topic-badge').innerText = `${length} characters`;
+ }
+}
+
+// Wait for the DOM to be ready before we start
+addEventListener('DOMContentLoaded', main);
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/static/favicon.ico b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/static/favicon.ico
new file mode 100644
index 0000000..98f74ce
Binary files /dev/null and b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/static/favicon.ico differ
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/templates/about.html b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/templates/about.html
new file mode 100644
index 0000000..27cc093
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/templates/about.html
@@ -0,0 +1,41 @@
+
+
+
+
+
+ Gemini AI
+
+
+
+
+
+{% include 'nav.html' %}
+
+
+This application supports sending questions to Google Gemini and displays the response. The purpose is to demonstrate how to interface with Google Gemini.
+
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/templates/gemini.html b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/templates/gemini.html
new file mode 100644
index 0000000..fdff232
--- /dev/null
+++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/templates/gemini.html
@@ -0,0 +1,52 @@
+
+
+
+
+
+ Gemini AI
+
+
+
+
+
+{% include 'nav.html' %}
+
+
Google Gemini
+
+
+Google Gemini is a recently launched family of large language models (LLMs) created by Google DeepMind. It's considered their most capable AI model yet, designed to compete with OpenAI's GPT-4.
+
+
+
+Here's a breakdown of what Gemini offers:
+
+
+
Multimodal: Unlike prior models, Gemini can understand and work with various data types, including text, code, audio, images, and video. This makes it highly versatile.
+
+
Family of models: Gemini comes in three versions: Gemini Nano, Pro, and Ultra. Each caters to different needs, with Ultra being the most powerful and Pro being the free, user-friendly option.
+
+
Applications: Gemini has various applications. Developers can use it to build AI-powered chatbots and apps. It's also integrated with Google services like Gmail and Maps, allowing you to get help with writing, planning, and learning.
+
+
Chatbot: You might also encounter Gemini as the name of Google's AI chatbot, which was formerly called Bard.
+
+
+
+
+
+Overall, Google Gemini is a significant development in AI, offering a powerful and versatile suite of tools for developers and users alike.
+