diff --git a/helm-chart/templates/shoppingassistantservice.yaml b/helm-chart/templates/shoppingassistantservice.yaml new file mode 100644 index 00000000000..f75150180de --- /dev/null +++ b/helm-chart/templates/shoppingassistantservice.yaml @@ -0,0 +1,135 @@ +{{- if .Values.shoppingAssistantService.create }} +{{- if .Values.serviceAccounts.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ .Values.shoppingAssistantService.name }} + namespace: {{ .Release.Namespace }} + {{- if not .Values.serviceAccounts.annotationsOnlyForCartservice }} + {{- with .Values.serviceAccounts.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- end }} + +--- +{{- end }} + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Values.shoppingAssistantService.name }} + namespace: {{ .Release.Namespace }} + labels: + app: {{ .Values.shoppingAssistantService.name }} +spec: + selector: + matchLabels: + app: {{ .Values.shoppingAssistantService.name }} + template: + metadata: + labels: + app: {{ .Values.shoppingAssistantService.name }} + spec: + {{- if .Values.serviceAccounts.create }} + serviceAccountName: {{ .Values.shoppingAssistantService.name }} + {{- else }} + serviceAccountName: default + {{- end }} + terminationGracePeriodSeconds: 5 + {{- if .Values.securityContext.enable }} + securityContext: + fsGroup: 1000 + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + {{- if .Values.seccompProfile.enable }} + seccompProfile: + type: {{ .Values.seccompProfile.type }} + {{- end }} + {{- end }} + containers: + - name: server + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + privileged: false + image: {{ .Values.images.repository }}/{{ .Values.shoppingAssistantService.name }}:{{ .Values.images.tag | default .Chart.AppVersion }} + ports: + - containerPort: 8080 + env: + - name: PORT + value: "8080" + - name: GOOGLE_APPLICATION_CREDENTIALS + value: "/var/secrets/google/key.json" + - name: ALLOYDB_CLUSTER_NAME + value: {{ .Values.shoppingAssistantService.alloydbClusterName | quote }} + - name: ALLOYDB_INSTANCE_NAME + value: {{ .Values.shoppingAssistantService.alloydbInstanceName | quote }} + - name: ALLOYDB_DATABASE_NAME + value: {{ .Values.shoppingAssistantService.alloydbDatabaseName | quote }} + - name: ALLOYDB_TABLE_NAME + value: {{ .Values.shoppingAssistantService.alloydbTableName | quote }} + - name: ALLOYDB_SECRET_NAME + value: {{ .Values.shoppingAssistantService.alloydbSecretName | quote }} + - name: PROJECT_ID + value: {{ .Values.shoppingAssistantService.projectId | quote }} + - name: REGION + value: {{ .Values.shoppingAssistantService.region | quote }} + {{- if .Values.opentelemetryCollector.create }} + - name: COLLECTOR_SERVICE_ADDR + value: "{{ .Values.opentelemetryCollector.name }}:4317" + - name: OTEL_SERVICE_NAME + value: "{{ .Values.shoppingAssistantService.name }}" + {{- end }} + {{- if .Values.googleCloudOperations.tracing }} + - name: ENABLE_TRACING + value: "1" + {{- end }} + {{- if not .Values.googleCloudOperations.profiler }} + - name: DISABLE_PROFILER + value: "1" + {{- end }} + volumeMounts: + - name: gcp-key + mountPath: /var/secrets/google + readOnly: true + volumes: + - name: gcp-key + secret: + secretName: {{ .Values.shoppingAssistantService.name }}-gcp-key + resources: + {{- toYaml .Values.shoppingAssistantService.resources | nindent 10 }} + +--- + +apiVersion: v1 +kind: Service +metadata: + name: {{ .Values.shoppingAssistantService.name }} + namespace: {{ .Release.Namespace }} + labels: + app: {{ .Values.shoppingAssistantService.name }} +spec: + type: ClusterIP + selector: + app: {{ .Values.shoppingAssistantService.name }} + ports: + - name: http + port: 80 + targetPort: 8080 + + +# Make sure to create a secret like this one with the GCP credentials +# apiVersion: v1 +# kind: Secret +# metadata: +# name: {{ .Values.shoppingAssistantService.name }}-gcp-key +# namespace: {{ .Release.Namespace }} +# type: Opaque +# data: +# key.json: "" + +{{ end -}} diff --git a/helm-chart/values.yaml b/helm-chart/values.yaml index 7f403b1045c..c0aa204cc69 100644 --- a/helm-chart/values.yaml +++ b/helm-chart/values.yaml @@ -17,7 +17,7 @@ # Declare variables to be passed into your templates. images: - repository: us-central1-docker.pkg.dev/google-samples/microservices-demo + repository: ghcr.io/riptideslabs/microservice-demo # Overrides the image tag whose default is the chart appVersion. tag: "" @@ -213,8 +213,20 @@ cartDatabase: endpointPort: "" certificate: "" -# @TODO: This service is not currently available in Helm. -# https://github.com/GoogleCloudPlatform/microservices-demo/tree/main/kustomize/components/shopping-assistant shoppingAssistantService: - create: false + create: true name: shoppingassistantservice + alloydbClusterName: microservices-demo + alloydbInstanceName: microservices-demo-primary + alloydbDatabaseName: shoppingassistant + alloydbTableName: catalog_items + projectId: "" + region: europe-west1 + resources: + requests: + cpu: 100m + memory: 64Mi + limits: + cpu: 200m + memory: 128Mi + diff --git a/src/shoppingassistantservice/requirements.in b/src/shoppingassistantservice/requirements.in index 056c30b61b5..85b949d8f70 100644 --- a/src/shoppingassistantservice/requirements.in +++ b/src/shoppingassistantservice/requirements.in @@ -4,3 +4,4 @@ langchain==0.3.21 pillow==11.1.0 langchain-google-alloydb-pg==0.10.0 google-cloud-secret-manager==2.23.2 +langgraph==0.5.2 diff --git a/src/shoppingassistantservice/requirements.txt b/src/shoppingassistantservice/requirements.txt index 49d3c355334..27d4076847e 100644 --- a/src/shoppingassistantservice/requirements.txt +++ b/src/shoppingassistantservice/requirements.txt @@ -1,16 +1,16 @@ # -# This file is autogenerated by pip-compile with Python 3.11 +# This file is autogenerated by pip-compile with Python 3.12 # by the following command: # -# pip-compile --output-file=requirements.txt requirements.in +# pip-compile requirements.in # aiofiles==24.1.0 # via google-cloud-alloydb-connector aiohappyeyeballs==2.6.1 # via aiohttp -aiohttp==3.11.14 +aiohttp==3.12.14 # via google-cloud-alloydb-connector -aiosignal==1.3.2 +aiosignal==1.4.0 # via aiohttp annotated-types==0.7.0 # via pydantic @@ -24,50 +24,55 @@ blinker==1.9.0 # via flask cachetools==5.5.2 # via google-auth -certifi==2025.1.31 +certifi==2025.7.14 # via # httpcore # httpx # requests cffi==1.17.1 # via cryptography -charset-normalizer==3.4.1 +charset-normalizer==3.4.2 # via requests -click==8.1.8 +click==8.2.1 # via flask -cryptography==44.0.2 +cryptography==45.0.5 # via google-cloud-alloydb-connector filetype==1.2.0 # via langchain-google-genai flask==3.1.0 # via -r requirements.in -frozenlist==1.5.0 +frozenlist==1.7.0 # via # aiohttp # aiosignal -google-ai-generativelanguage==0.6.17 +google-ai-generativelanguage==0.6.18 # via langchain-google-genai -google-api-core[grpc]==2.24.2 +google-api-core[grpc]==2.25.1 # via # google-ai-generativelanguage + # google-cloud-alloydb + # google-cloud-alloydb-connector # google-cloud-core # google-cloud-secret-manager # google-cloud-storage -google-auth==2.38.0 +google-auth==2.40.3 # via # google-ai-generativelanguage # google-api-core + # google-cloud-alloydb # google-cloud-alloydb-connector # google-cloud-core # google-cloud-secret-manager # google-cloud-storage -google-cloud-alloydb-connector[asyncpg]==1.7.0 +google-cloud-alloydb==0.4.8 + # via google-cloud-alloydb-connector +google-cloud-alloydb-connector[asyncpg]==1.9.0 # via langchain-google-alloydb-pg google-cloud-core==2.4.3 # via google-cloud-storage google-cloud-secret-manager==2.23.2 # via -r requirements.in -google-cloud-storage==3.1.0 +google-cloud-storage==3.2.0 # via langchain-google-alloydb-pg google-crc32c==1.7.1 # via @@ -75,29 +80,33 @@ google-crc32c==1.7.1 # google-resumable-media google-resumable-media==2.7.2 # via google-cloud-storage -googleapis-common-protos[grpc]==1.69.2 +googleapis-common-protos[grpc]==1.70.0 # via # google-api-core # grpc-google-iam-v1 # grpcio-status -greenlet==3.1.1 +greenlet==3.2.3 # via sqlalchemy grpc-google-iam-v1==0.14.2 - # via google-cloud-secret-manager -grpcio==1.71.0 + # via + # google-cloud-alloydb + # google-cloud-secret-manager +grpcio==1.73.1 # via # google-api-core # googleapis-common-protos # grpc-google-iam-v1 # grpcio-status -grpcio-status==1.71.0 +grpcio-status==1.73.1 # via google-api-core -h11==0.14.0 +h11==0.16.0 # via httpcore -httpcore==1.0.7 +httpcore==1.0.9 # via httpx httpx==0.28.1 - # via langsmith + # via + # langgraph-sdk + # langsmith idna==3.10 # via # anyio @@ -114,19 +123,32 @@ jsonpointer==3.0.0 # via jsonpatch langchain==0.3.21 # via -r requirements.in -langchain-core==0.3.49 +langchain-core==0.3.69 # via # langchain # langchain-google-alloydb-pg # langchain-google-genai # langchain-text-splitters + # langgraph + # langgraph-checkpoint + # langgraph-prebuilt langchain-google-alloydb-pg==0.10.0 # via -r requirements.in langchain-google-genai==2.1.1 # via -r requirements.in -langchain-text-splitters==0.3.7 +langchain-text-splitters==0.3.8 # via langchain -langsmith==0.3.19 +langgraph==0.5.2 + # via -r requirements.in +langgraph-checkpoint==2.1.0 + # via + # langgraph + # langgraph-prebuilt +langgraph-prebuilt==0.5.2 + # via langgraph +langgraph-sdk==0.1.73 + # via langgraph +langsmith==0.3.45 # via # langchain # langchain-core @@ -134,25 +156,29 @@ markupsafe==3.0.2 # via # jinja2 # werkzeug -multidict==6.2.0 +multidict==6.6.3 # via # aiohttp # yarl -numpy==2.2.4 +numpy==2.3.1 # via # langchain-google-alloydb-pg # pgvector -orjson==3.10.16 - # via langsmith -packaging==24.2 +orjson==3.11.0 + # via + # langgraph-sdk + # langsmith +ormsgpack==1.10.0 + # via langgraph-checkpoint +packaging==25.0 # via # langchain-core # langsmith -pgvector==0.4.0 +pgvector==0.4.1 # via langchain-google-alloydb-pg pillow==11.1.0 # via -r requirements.in -propcache==0.3.1 +propcache==0.3.2 # via # aiohttp # yarl @@ -160,11 +186,13 @@ proto-plus==1.26.1 # via # google-ai-generativelanguage # google-api-core + # google-cloud-alloydb # google-cloud-secret-manager -protobuf==5.29.4 +protobuf==6.31.1 # via # google-ai-generativelanguage # google-api-core + # google-cloud-alloydb # google-cloud-alloydb-connector # google-cloud-secret-manager # googleapis-common-protos @@ -179,19 +207,20 @@ pyasn1-modules==0.4.2 # via google-auth pycparser==2.22 # via cffi -pydantic==2.11.0 +pydantic==2.11.7 # via # langchain # langchain-core # langchain-google-genai + # langgraph # langsmith -pydantic-core==2.33.0 +pydantic-core==2.33.2 # via pydantic pyyaml==6.0.2 # via # langchain # langchain-core -requests==2.32.3 +requests==2.32.4 # via # google-api-core # google-cloud-alloydb-connector @@ -201,31 +230,34 @@ requests==2.32.3 # requests-toolbelt requests-toolbelt==1.0.0 # via langsmith -rsa==4.9 +rsa==4.9.1 # via google-auth sniffio==1.3.1 # via anyio -sqlalchemy[asyncio]==2.0.40 +sqlalchemy[asyncio]==2.0.41 # via # langchain # langchain-google-alloydb-pg -tenacity==9.0.0 +tenacity==9.1.2 # via langchain-core -typing-extensions==4.13.0 +typing-extensions==4.14.1 # via + # aiosignal # anyio # langchain-core # pydantic # pydantic-core # sqlalchemy # typing-inspection -typing-inspection==0.4.0 +typing-inspection==0.4.1 # via pydantic -urllib3==2.3.0 +urllib3==2.5.0 # via requests werkzeug==3.1.3 # via flask -yarl==1.18.3 +xxhash==3.5.0 + # via langgraph +yarl==1.20.1 # via aiohttp zstandard==0.23.0 # via langsmith diff --git a/src/shoppingassistantservice/shoppingassistantservice.py b/src/shoppingassistantservice/shoppingassistantservice.py index 94fd8c2b486..b7068d9fe57 100755 --- a/src/shoppingassistantservice/shoppingassistantservice.py +++ b/src/shoppingassistantservice/shoppingassistantservice.py @@ -30,13 +30,16 @@ ALLOYDB_TABLE_NAME = os.environ["ALLOYDB_TABLE_NAME"] ALLOYDB_CLUSTER_NAME = os.environ["ALLOYDB_CLUSTER_NAME"] ALLOYDB_INSTANCE_NAME = os.environ["ALLOYDB_INSTANCE_NAME"] -ALLOYDB_SECRET_NAME = os.environ["ALLOYDB_SECRET_NAME"] -secret_manager_client = secretmanager_v1.SecretManagerServiceClient() -secret_name = secret_manager_client.secret_version_path(project=PROJECT_ID, secret=ALLOYDB_SECRET_NAME, secret_version="latest") -secret_request = secretmanager_v1.AccessSecretVersionRequest(name=secret_name) -secret_response = secret_manager_client.access_secret_version(request=secret_request) -PGPASSWORD = secret_response.payload.data.decode("UTF-8").strip() +# Commented these out to use AlloyDB IAM authentication instead of password + +# ALLOYDB_SECRET_NAME = os.environ["ALLOYDB_SECRET_NAME"] + +# secret_manager_client = secretmanager_v1.SecretManagerServiceClient() +# secret_name = secret_manager_client.secret_version_path(project=PROJECT_ID, secret=ALLOYDB_SECRET_NAME, secret_version="latest") +# secret_request = secretmanager_v1.AccessSecretVersionRequest(name=secret_name) +# secret_response = secret_manager_client.access_secret_version(request=secret_request) +# PGPASSWORD = secret_response.payload.data.decode("UTF-8").strip() engine = AlloyDBEngine.from_instance( project_id=PROJECT_ID, @@ -44,8 +47,8 @@ cluster=ALLOYDB_CLUSTER_NAME, instance=ALLOYDB_INSTANCE_NAME, database=ALLOYDB_DATABASE_NAME, - user="postgres", - password=PGPASSWORD + # user="postgres", + # password=PGPASSWORD ) # Create a synchronous connection to our vectorstore @@ -68,28 +71,28 @@ def talkToGemini(): prompt = request.json['message'] prompt = unquote(prompt) - # Step 1 – Get a room description from Gemini-vision-pro - llm_vision = ChatGoogleGenerativeAI(model="gemini-1.5-flash") - message = HumanMessage( - content=[ - { - "type": "text", - "text": "You are a professional interior designer, give me a detailed decsription of the style of the room in this image", - }, - {"type": "image_url", "image_url": request.json['image']}, - ] - ) - response = llm_vision.invoke([message]) - print("Description step:") - print(response) - description_response = response.content + # # Step 1 – Get a room description from Gemini-vision-pro + # llm_vision = ChatGoogleGenerativeAI(model="gemini-1.5-flash") + # message = HumanMessage( + # content=[ + # { + # "type": "text", + # "text": "You are a professional interior designer, give me a detailed decsription of the style of the room in this image", + # }, + # {"type": "image_url", "image_url": request.json['image']}, + # ] + # ) + # response = llm_vision.invoke([message]) + # print("Description step:") + # print(response) + # description_response = response.content # Step 2 – Similarity search with the description & user prompt - vector_search_prompt = f""" This is the user's request: {prompt} Find the most relevant items for that prompt, while matching style of the room described here: {description_response} """ + vector_search_prompt = f""" This is the user's request: {prompt} Find the most relevant items for that prompt.""" print(vector_search_prompt) docs = vectorstore.similarity_search(vector_search_prompt) - print(f"Vector search: {description_response}") + # print(f"Vector search: {description_response}") print(f"Retrieved documents: {len(docs)}") #Prepare relevant documents for inclusion in final prompt relevant_docs = "" @@ -101,8 +104,8 @@ def talkToGemini(): # Step 3 – Tie it all together by augmenting our call to Gemini-pro llm = ChatGoogleGenerativeAI(model="gemini-1.5-flash") design_prompt = ( - f" You are an interior designer that works for Online Boutique. You are tasked with providing recommendations to a customer on what they should add to a given room from our catalog. This is the description of the room: \n" - f"{description_response} Here are a list of products that are relevant to it: {relevant_docs} Specifically, this is what the customer has asked for, see if you can accommodate it: {prompt} Start by repeating a brief description of the room's design to the customer, then provide your recommendations. Do your best to pick the most relevant item out of the list of products provided, but if none of them seem relevant, then say that instead of inventing a new product. At the end of the response, add a list of the IDs of the relevant products in the following format for the top 3 results: [], [], [] ") + f" You are a shopping assistant for Online Boutique. You are tasked with providing recommendations to a customer on what they should buy based on their request. \n" + f"Here is a list of products that are relevant to it: {relevant_docs} Specifically, this is what the customer has asked for, see if you can accommodate it: {prompt} Do your best to pick the most relevant item out of the list of products provided, but if none of them seem relevant, then say that instead of inventing a new product. At the end of the response, add a list of the IDs of the relevant products in the following format for the top 3 results: [], [], [] ") print("Final design prompt: ") print(design_prompt) design_response = llm.invoke(