Skip to content

Commit

Permalink
feat: movie search sample app with Pinecone and PostgreSQL backend (#261
Browse files Browse the repository at this point in the history
)
  • Loading branch information
gotochkin authored Oct 28, 2024
1 parent cdfb8bb commit 80c0f95
Show file tree
Hide file tree
Showing 9 changed files with 1,072 additions and 0 deletions.
14 changes: 14 additions & 0 deletions infrastructure/movie-search-app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# Environment
.env
.venv

# Jupyter Notebook
.ipynb_checkpoints

# Misc
.python-version
1 change: 1 addition & 0 deletions infrastructure/movie-search-app/Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 movie_search:me
169 changes: 169 additions & 0 deletions infrastructure/movie-search-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@

# Sample app for genai embeddings using Pinecone or PostgreSQL compaible database
## Description
- The demo shows a sample moview seach chat assistant using either Pinecone or PostgreSQL compatible database as a backend.
- In both cases the Google AI studio is used for conversations and embedding generation.

### Architecture
- The application can be deployed on a VM or any other environment supporting Python 3.11
- It connects to a Pinecone environment using Pinecone API token
- It uses Google AI Studio to generate responses (using model gemini-1.5-flash) or to generate embeddings (model textetext-embedding-004)

## Requirements
- Platform to deploy the application supporting Python 3.11
- Token in Google AI studio (you can get it from [here](https://ai.google.dev/gemini-api/docs/api-key))
- Token for Pinecone API (optional)
- Project in Google Cloud with enabled APIs for all components.


## Deployment for Pinecone Backend

The dataset with movies and how to deploy it to the Pinecone environment is not discussed here.

### Prepare Virtual machine
- Enable the required APIs in Google Cloud
```
gcloud services enable compute.googleapis.com
```
- Create a GCE VM in a Google Cloud project
- Connect to the VM ussing SSH
- Clone the software
```
git clone https://github.com/GoogleCloudPlatform/devrel-demos.git
```
- Prepare Python 3.11
```
sudo apt install -y python3.11-venv git
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
```
### Run the application
- Change directory
```
cd devrel-demos/infrastructure/movie-search-app
```
- Install dependencies
```
pip install -r requirements.txt
```
- Set environment variables (Pinecone index name)
```
export PINECONE_INDEX_NAME=netflix-index-01
export PORT=8080
```
- Start the application from command line
```
gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 movie_search:me
```
- Connect to the chat using the VM host:port to get the application interface

### Work with application
- Click at the bottom of the app to choose backend.
- Put Google AI API token and Pinecone API token at the top (you need both to use the Pinecone backend).
- Select Pinecone as a backend and confirm the choice.
- Post your question in the input window at the bottom and click the arrow.

Ask sample questions about the movies

### You can deploy your application to Cloud Run
Optionally you can deploy the application to Cloud Run.

## Deployment with AlloyDB Backend
You will need AlloyDB database as a backend for the application.

Assuming all the actions are performed in the same Google Cloud project.
### Enable all required APIs usng gcloud command
```
gcloud services enable alloydb.googleapis.com \
compute.googleapis.com \
cloudresourcemanager.googleapis.com \
servicenetworking.googleapis.com \
vpcaccess.googleapis.com \
aiplatform.googleapis.com \
cloudbuild.googleapis.com \
artifactregistry.googleapis.com \
run.googleapis.com \
iam.googleapis.com
```

### Create AlloyDB cluster
Please follow instruction in the documentation to create an AlloyDB cluster and primary instance in the same project where the application is going to be deployed.

Here is the [link to the documentation for AlloyDB](https://cloud.google.com/alloydb/docs/quickstart/create-and-connect)

### Create a database in AlloyDB
Create a database with the name movies and the user movies_owner. You can choose your own names for the database and the user. The application takes it from environment variables. Optionally you can modify the application to use secret manager in Google Cloud as more secured approach.

### Migrate data from Pinecone to AlloyDB
- Move the data from Pinecone to AlloyDB

### Enable virtual environment for Python
You can use either your laptop or a virtual machnie for deployment. Using a VM deployed in the same Google Cloud project simplifies deployeent and network configuration. On a Debian Linux you can enable it in the shell using the following command:
```
sudo apt-get update
sudo apt install python3.11-venv git postgresql-client
python3 -m venv venv
source venv/bin/activate
```

### Clone the software
Clone the software using git:
```
git clone https://github.com/gotochkin/devrel-demos.git
```
### Run the application
- Change directory
```
cd devrel-demos/infrastructure/movie-search-app
```
- Install dependencies
```
pip install -r requirements.txt
```
- Set environment variables (Pinecone index name)
```
export PINECONE_INDEX_NAME=netflix-index-01
export PORT=8080
export DB_USER=movies_owner
export DB_PASS=DatabasePassword
export DB_NAME=movies
export INSTANCE_HOST=ALLOYDB_IP
export DB_PORT=5432
```
- Start the application from command line
```
gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 movie_search:me
```
- Connect to the chat using the VM host:port to get the application interface

### Deploy the applicaion to Cloud Run
Create a service account cymbal-store-identity and grant role VertexAI User to the account - optional now since we are not using Vertex AI as of now.
Build and deploy application to the Cloud Run service.

```
gcloud alpha run deploy movie-search-app \
--source=./ \
--no-allow-unauthenticated \
--service-account movie-search-identity \
--region us-central1 \
--network=default \
--set-env-vars=DB_USER=cymbaldb_owner,DB_PASS=StrongPassword,DB_NAME=cymbaldb,INSTANCE_HOST=127.0.0.1,DB_PORT=5432 \
--quiet
```
### Work with application
- Click at the bottom of the app to choose backend.
- Put Google AI API token and Pinecone API token at the top (you need both to use the Pinecone backend).
- Select Pinecone as a backend and confirm the choice.
- Post your question in the input window at the bottom and click the arrow.

Ask sample questions about the movies

# TO DO
- Add support for other models and providers

# License
Apache License Version 2.0;
Copyright 2024 Google LLC


124 changes: 124 additions & 0 deletions infrastructure/movie-search-app/connect_tcp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Copyright 2022 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.

# TODO (https://github.com/GoogleCloudPlatform/python-docs-samples/issues/8253): remove old region tags
# [START cloud_sql_postgres_sqlalchemy_connect_tcp]
# [START cloud_sql_postgres_sqlalchemy_sslcerts]
# [START cloud_sql_postgres_sqlalchemy_connect_tcp_sslcerts]
import os
import ssl
import logging

import sqlalchemy


def connect_tcp_socket() -> sqlalchemy.engine.base.Engine:
"""Initializes a TCP connection pool for a PostgreSQL instance of Postgres."""
# Note: Saving credentials in environment variables is convenient, but not
# secure - consider a more secure solution such as
# Cloud Secret Manager (https://cloud.google.com/secret-manager) to help
# keep secrets safe.
if os.environ.get("INSTANCE_HOST"):
db_host = os.environ[
"INSTANCE_HOST"
] # e.g. '127.0.0.1' ('172.17.0.1' if deployed to GAE Flex)
else:
db_host = "127.0.0.1"
logging.warning(("INSTANCE_HOST is not set using default: %s", db_host))
if os.environ.get("DB_PORT"):
db_port = os.environ["DB_PORT"] # e.g. '5432'
else:
db_port = "5432"
logging.warning(("DB_PORT is not set using default: %s", db_port))
if os.environ.get("DB_USER"):
db_user = os.environ["DB_USER"] # e.g. 'my-db-user'
else:
db_user = "movies_owner"
logging.warning(("DB_USER is not set using default: %s", db_user))
if os.environ.get("DB_PASS"):
db_pass = os.environ["DB_PASS"] # e.g. 'my-db-password'
else:
db_pass = "password"
logging.warning(("DB_PASS is not set using default: %s", db_pass))
if os.environ.get("DB_NAME"):
db_name = os.environ["DB_NAME"] # e.g. 'my-database'
else:
db_name = "movies"
logging.warning(("DB_NAME is not set using default: %s", db_name))


# [END cloud_sql_postgres_sqlalchemy_connect_tcp]
connect_args = {}
# For deployments that connect directly to a PostgreSQL instance without
# using the PostgreSQL Proxy, configuring SSL certificates will ensure the
# connection is encrypted.
if os.environ.get("DB_ROOT_CERT"):
db_root_cert = os.environ["DB_ROOT_CERT"] # e.g. '/path/to/my/server-ca.pem'
db_cert = os.environ["DB_CERT"] # e.g. '/path/to/my/client-cert.pem'
db_key = os.environ["DB_KEY"] # e.g. '/path/to/my/client-key.pem'

ssl_context = ssl.SSLContext()
ssl_context.verify_mode = ssl.CERT_REQUIRED
ssl_context.load_verify_locations(db_root_cert)
ssl_context.load_cert_chain(db_cert, db_key)
connect_args["ssl_context"] = ssl_context

# [START cloud_sql_postgres_sqlalchemy_connect_tcp]
pool = sqlalchemy.create_engine(
# Equivalent URL:
# postgresql+pg8000://<db_user>:<db_pass>@<db_host>:<db_port>/<db_name>
sqlalchemy.engine.url.URL.create(
drivername="postgresql+pg8000",
username=db_user,
password=db_pass,
host=db_host,
port=db_port,
database=db_name,
),
# [END cloud_sql_postgres_sqlalchemy_connect_tcp]
connect_args=connect_args,
# [START cloud_sql_postgres_sqlalchemy_connect_tcp]
# [START_EXCLUDE]
# [START cloud_sql_postgres_sqlalchemy_limit]
# Pool size is the maximum number of permanent connections to keep.
pool_size=5,
# Temporarily exceeds the set pool_size if no connections are available.
max_overflow=2,
# The total number of concurrent connections for your application will be
# a total of pool_size and max_overflow.
# [END cloud_sql_postgres_sqlalchemy_limit]
# [START cloud_sql_postgres_sqlalchemy_backoff]
# SQLAlchemy automatically uses delays between failed connection attempts,
# but provides no arguments for configuration.
# [END cloud_sql_postgres_sqlalchemy_backoff]
# [START cloud_sql_postgres_sqlalchemy_timeout]
# 'pool_timeout' is the maximum number of seconds to wait when retrieving a
# new connection from the pool. After the specified amount of time, an
# exception will be thrown.
pool_timeout=30, # 30 seconds
# [END cloud_sql_postgres_sqlalchemy_timeout]
# [START cloud_sql_postgres_sqlalchemy_lifetime]
# 'pool_recycle' is the maximum number of seconds a connection can persist.
# Connections that live longer than the specified amount of time will be
# re-established
pool_recycle=1800, # 30 minutes
# [END cloud_sql_postgres_sqlalchemy_lifetime]
# [END_EXCLUDE]
)
return pool


# [END cloud_sql_postgres_sqlalchemy_connect_tcp_sslcerts]
# [END cloud_sql_postgres_sqlalchemy_sslcerts]
# [END cloud_sql_postgres_sqlalchemy_connect_tcp]
51 changes: 51 additions & 0 deletions infrastructure/movie-search-app/data_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Copyright 2024 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 mesop as me
from dataclasses import dataclass, field
from enum import Enum
from typing import Literal

Role = Literal["user", "model"]

# Data Model
@dataclass(kw_only=True)
class ChatMessage:
role: Role = "user"
content: str = ""
in_progress: bool = False

class Models(Enum):
GEMINI_1_5_FLASH = "PostgreSQL"
PINECONE = "Pinecone"

@dataclass
class Conversation:
model: str = ""
messages: list[ChatMessage] = field(default_factory=list)

@me.stateclass
class State:
is_model_picker_dialog_open: bool = False
input: str = ""
conversations: list[Conversation] = field(default_factory=list)
models: list[str] = field(default_factory=list)
gemini_api_key: str = ""
pinecone_api_key: str = ""
location: str = ""
in_progress: bool = False

@me.stateclass
class ModelDialogState:
selected_models: list[str] = field(default_factory=list)
Loading

0 comments on commit 80c0f95

Please sign in to comment.