Skip to content

Commit bb689bd

Browse files
authored
Merge pull request #219 from AaronMT/vector-update
llm-cloud-run: Vector RAG Skeleton
2 parents f9eed21 + d1c966c commit bb689bd

21 files changed

Lines changed: 536 additions & 208 deletions
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
name: Deploy to Cloud Run
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
branches: [ "main" ] # change if you deploy from a different branch
7+
8+
permissions:
9+
contents: read
10+
11+
jobs:
12+
deploy:
13+
runs-on: ubuntu-latest
14+
15+
# IMPORTANT: set this to your GitHub Environment name (e.g., "dev", "prod", "vertex-hack")
16+
environment: vertex-hack
17+
18+
env:
19+
# Repo/global (or environment) variables
20+
GCP_PROJECT_ID: ${{ vars.GCP_PROJECT_ID }}
21+
GCP_REGION: ${{ vars.GCP_REGION }}
22+
CLOUD_RUN_SERVICE: ${{ vars.CLOUD_RUN_SERVICE }}
23+
24+
# Image naming (Artifact Registry)
25+
AR_REPO: ${{ vars.AR_REPO }} # e.g. "llm"
26+
AR_IMAGE: ${{ vars.AR_IMAGE }} # e.g. "gemini-llm"
27+
IMAGE_URI: ${{ vars.GCP_REGION }}-docker.p
28+
29+
30+
steps:
31+
- name: Checkout
32+
uses: actions/checkout@v6
33+
34+
- name: Authenticate to Google Cloud (JSON key)
35+
uses: google-github-actions/auth@v3
36+
with:
37+
credentials_json: ${{ secrets.GCP_SA_VERTEX_AI }}
38+
39+
- name: Setup gcloud
40+
uses: google-github-actions/setup-gcloud@v3
41+
with:
42+
project_id: ${{ env.GCP_PROJECT_ID }}
43+
44+
- name: Configure gcloud project
45+
run: gcloud --quiet config set project "${GCP_PROJECT_ID}"
46+
47+
- name: Build container image with Cloud Build
48+
run: |
49+
set -euo pipefail
50+
gcloud builds submit --tag "${IMAGE_URI}"
51+
52+
- name: Deploy to Cloud Run
53+
run: |
54+
set -euo pipefail
55+
56+
# Build env var string from GitHub Environment variables
57+
ENV_VARS="PIPELINE_MODE=${{ vars.PIPELINE_MODE }},ENABLE_BQ_WRITE=${{ vars.ENABLE_BQ_WRITE }},BQ_DATASET=${{ vars.BQ_DATASET }},BQ_NOTES_TABLE=${{ vars.BQ_NOTES_TABLE }},VERTEX_LOCATION=${{ vars.VERTEX_LOCATION }},VERTEX_EMBEDDING_MODEL=${{ vars.VERTEX_EMBEDDING_MODEL }},VERTEX_LLM_MODEL=${{ vars.VERTEX_LLM_MODEL }},LOG_LEVEL=${{ vars.LOG_LEVEL }},DEBUG_RAG=${{ vars.DEBUG_RAG }}"
58+
59+
gcloud run deploy "${CLOUD_RUN_SERVICE}" \
60+
--image "${IMAGE_URI}" \
61+
--region "${GCP_REGION}" \
62+
--platform managed \
63+
--set-env-vars "${ENV_VARS}"
64+
65+
- name: Show service URL
66+
run: |
67+
set -euo pipefail
68+
gcloud run services describe "${CLOUD_RUN_SERVICE}" \
69+
--region "${GCP_REGION}" \
70+
--format="value(status.url)"

‎llm-cloud-run/.dockerignore‎

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
# Vim artifacts
2-
*.sw?
3-
.*.sw?
4-
*~ 
5-
4913
6-
7-
.DS_Store
1+
__pycache__
2+
*.pyc
3+
*.pyo
4+
*.pyd
5+
.Python
6+
.venv
7+
venv
8+
.git
9+
.pytest_cache

‎llm-cloud-run/Dockerfile‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
FROM python:3.13-slim
1+
FROM python:3.11-slim
2+
3+
ENV PYTHONDONTWRITEBYTECODE=1
4+
ENV PYTHONUNBUFFERED=1
25

36
WORKDIR /app
47

58
COPY requirements.txt .
6-
79
RUN pip install --no-cache-dir -r requirements.txt
810

911
COPY . .
1012

11-
EXPOSE 8080
12-
13-
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]
13+
ENV PORT=8080
14+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]

‎llm-cloud-run/common/__init__.py‎

Whitespace-only changes.

‎llm-cloud-run/common/config.py‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import os
2+
from dataclasses import dataclass
3+
4+
5+
def _env(name: str, default: str | None = None) -> str | None:
6+
v = os.getenv(name)
7+
if v is None or v == "":
8+
return default
9+
return v
10+
11+
12+
@dataclass(frozen=True)
13+
class Settings:
14+
# GCP / Vertex
15+
gcp_project: str | None = _env("GCP_PROJECT") or _env("GOOGLE_CLOUD_PROJECT")
16+
gcp_location: str = _env("GCP_LOCATION", "us-central1") or "us-central1"
17+
18+
# BigQuery
19+
bq_project: str | None = _env("BQ_PROJECT") # optional; defaults to gcp_project
20+
bq_dataset: str = _env("BQ_DATASET", "vertex_rag_demo") or "vertex_rag_demo"
21+
bq_notes_table: str = _env("BQ_NOTES_TABLE", "demo_Notes") or "demo_Notes"
22+
bq_embeddings_table: str = _env("BQ_EMBEDDINGS_TABLE", "demo_NoteEmbeddings") or "demo_NoteEmbeddings"
23+
24+
# Column names (override if your schema differs)
25+
notes_id_col: str = _env("NOTES_ID_COL", "note_id") or "note_id"
26+
notes_content_col: str = _env("NOTES_CONTENT_COL", "content") or "content"
27+
notes_source_col: str = _env("NOTES_SOURCE_COL", "source") or "source"
28+
notes_created_col: str = _env("NOTES_CREATED_COL", "created_at") or "created_at"
29+
30+
emb_id_col: str = _env("EMB_ID_COL", "note_id") or "note_id"
31+
emb_vector_col: str = _env("EMB_VECTOR_COL", "embedding") or "embedding" # ARRAY<FLOAT64>
32+
emb_model_col: str = _env("EMB_MODEL_COL", "model") or "model"
33+
emb_updated_col: str = _env("EMB_UPDATED_COL", "updated_at") or "updated_at"
34+
35+
# RAG settings
36+
rag_top_k: int = int(_env("RAG_TOP_K", "3") or "3")
37+
snippet_chars: int = int(_env("RAG_SNIPPET_CHARS", "800") or "800")
38+
39+
# LLM + Embeddings
40+
gemini_model: str = _env("GEMINI_MODEL", "gemini-2.5-flash-lite") or "gemini-2.5-flash-lite"
41+
embedding_model: str = _env("EMBEDDING_MODEL", "text-embedding-004") or "text-embedding-004"
42+
43+
# Optional: cap how many notes to load into memory cache
44+
bq_max_notes: int = int(_env("BQ_MAX_NOTES", "5000") or "5000")
45+
46+
@property
47+
def effective_bq_project(self) -> str | None:
48+
return self.bq_project or self.gcp_project
49+
50+
def require_project(self) -> str:
51+
if not self.gcp_project:
52+
raise RuntimeError("GCP_PROJECT / GOOGLE_CLOUD_PROJECT not set.")
53+
return self.gcp_project
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import logging
2+
import os
3+
4+
5+
def setup_logging() -> None:
6+
level = os.getenv("LOG_LEVEL", "INFO").upper()
7+
logging.basicConfig(
8+
level=getattr(logging, level, logging.INFO),
9+
format="%(asctime)s %(levelname)s %(name)s - %(message)s",
10+
)

‎llm-cloud-run/llm/__init__.py‎

Whitespace-only changes.

‎llm-cloud-run/llm/embeddings.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from __future__ import annotations
2+
3+
from typing import List
4+
from vertexai.language_models import TextEmbeddingModel
5+
6+
from common.config import Settings
7+
from llm.vertex_init import init_vertex
8+
9+
_MODEL: TextEmbeddingModel | None = None
10+
11+
12+
def embed_text(settings: Settings, text: str) -> List[float]:
13+
"""Return an embedding vector for given text using Vertex Embeddings."""
14+
global _MODEL
15+
init_vertex(settings)
16+
if _MODEL is None:
17+
_MODEL = TextEmbeddingModel.from_pretrained(settings.embedding_model)
18+
return _MODEL.get_embeddings([text])[0].values

‎llm-cloud-run/llm/gemini_client.py‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from __future__ import annotations
2+
3+
from vertexai.preview.generative_models import GenerativeModel, Part
4+
5+
from common.config import Settings
6+
from llm.vertex_init import init_vertex
7+
8+
9+
def generate(
10+
settings: Settings,
11+
parts: list[Part] | Part,
12+
*,
13+
temperature: float = 0.3,
14+
max_output_tokens: int = 1024,
15+
) -> str:
16+
"""
17+
Generate text using a Gemini model on Vertex AI.
18+
19+
`parts` can be:
20+
- a single Part (text or image)
21+
- a list of Parts (multimodal prompt)
22+
"""
23+
init_vertex(settings)
24+
25+
model = GenerativeModel(settings.gemini_model)
26+
27+
response = model.generate_content(
28+
parts,
29+
generation_config={
30+
"temperature": temperature,
31+
"max_output_tokens": max_output_tokens,
32+
},
33+
)
34+
35+
# Vertex responses expose `.text` for convenience
36+
return getattr(response, "text", "") or ""

‎llm-cloud-run/llm/vertex_init.py‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import logging
2+
import vertexai
3+
from common.config import Settings
4+
5+
_INITIALIZED = False
6+
7+
8+
def init_vertex(settings: Settings) -> None:
9+
"""Initialize Vertex AI once per process."""
10+
global _INITIALIZED
11+
if _INITIALIZED:
12+
return
13+
14+
project = settings.require_project()
15+
vertexai.init(project=project, location=settings.gcp_location)
16+
logging.getLogger(__name__).info(
17+
"Vertex AI initialized (project=%s, location=%s).",
18+
project,
19+
settings.gcp_location,
20+
)
21+
_INITIALIZED = True

0 commit comments

Comments
 (0)