functions/main.py (166 lines of code) (raw):
# 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.
# Welcome to Cloud Functions for Firebase for Python!
# Deploy with `firebase deploy`
# The Cloud Functions for Firebase SDK to create Cloud Functions and set up triggers.
import os
import re
from firebase_functions import firestore_fn, https_fn, options
# The Firebase Admin SDK to access Cloud Firestore.
from firebase_admin import initialize_app, firestore
import markdown
from flask import jsonify
import google.ai.generativelanguage as glm
import google.generativeai as genai
from bs4 import BeautifulSoup
from google.oauth2 import service_account
app = initialize_app()
# Used to securely store your API key
GOOGLE_API_KEY=os.getenv('GOOGLE_API_KEY')
# Select your Gemini API endpoint.
SERVICE_ACCOUNT_FILE_NAME = 'service_account_key.json'
AQA_MODEL = "models/aqa"
PRODUCT_NAME = "Angular"
ANSWER_STYLE = "VERBOSE" # or ABSTRACTIVE, EXTRACTIVE
CORPUS_NAME = "corpora/angular-dev" # TODO: change this to your DocsAgent Corpus Name!
LOG_LEVEL = "VERBOSE"
@https_fn.on_request(cors=options.CorsOptions(cors_origins="*", cors_methods=["get", "post"]))
def generate_aqa_answer(req: https_fn.Request) -> https_fn.Response:
# Grab the text parameter.
prompt = req.args.get("text")
if prompt is None:
return https_fn.Response("No text parameter provided", status=400)
credentials = service_account.Credentials.from_service_account_file(SERVICE_ACCOUNT_FILE_NAME)
scoped_credentials = credentials.with_scopes(
['https://www.googleapis.com/auth/cloud-platform', 'https://www.googleapis.com/auth/generative-language.retriever'])
generative_service_client = glm.GenerativeServiceClient(credentials=scoped_credentials)
# Prepare parameters for the AQA model
content = glm.Content(parts=[glm.Part(text=prompt)])
retriever_config = glm.SemanticRetrieverConfig(
source=CORPUS_NAME, query=content
)
# Create a request to the AQA model
req = glm.GenerateAnswerRequest(
model=AQA_MODEL,
contents=[content],
semantic_retriever=retriever_config,
answer_style=ANSWER_STYLE,
)
try:
aqa_response = generative_service_client.generate_answer(req)
answer = aqa_response.answer.content.parts[0].text
print(aqa_response)
except:
print('Generate AQA answer - in the exception')
try:
answer = convert_to_html(answer)
print(answer)
except:
print('Make HTML - in the exception')
try:
resource_url = get_url(aqa_response.answer.grounding_attributions[0].source_id.semantic_retriever_chunk.chunk)
except:
print('Resouce URL attempt - in the exception')
questions = get_genai_follow_up_questions(prompt, aqa_response.answer.grounding_attributions)
if (aqa_response.answerable_probability < .1):
answer = "Sorry, that question isn't answered on Angular.dev. Please try again!"
resource_url = ''
return jsonify({
'answer': answer,
'probability': aqa_response.answerable_probability,
'url': resource_url,
'questions': questions
})
def get_url(chunk_resource_name: str) -> str:
credentials = service_account.Credentials.from_service_account_file(SERVICE_ACCOUNT_FILE_NAME)
scoped_credentials = credentials.with_scopes(
['https://www.googleapis.com/auth/cloud-platform', 'https://www.googleapis.com/auth/generative-language.retriever'])
retriever_service_client = glm.RetrieverServiceClient(credentials=scoped_credentials)
url = "Reference URL"
try:
# Get the metadata from the first attributed passages for the source
get_chunk_response = retriever_service_client.get_chunk(
name=chunk_resource_name
)
metadata = get_chunk_response.custom_metadata
for m in metadata:
if m.key == "url":
url = m.string_value
except:
url = "URL unknown"
url = url.replace('/overview', '')
url = url.replace('/reference', '')
url = url.replace('/best-practices', '')
url = url.replace('/introduction/what-is-angular', '/overview')
url = url.replace('_', '-')
return url
def convert_to_html(answer):
prompt = "Read the answer below. Convert the answer into valid HTML, with no markdown wrapper. The title should be an <h4>."
response = call_genai_generate_content(prompt + answer)
return response
def get_genai_follow_up_questions(prompt, grounding_attributions):
context = "Given a developer just asked " + prompt
for item in grounding_attributions:
context = add_custom_instruction_to_context(
context, item.content.parts[0].text
)
new_condition = "Read the context below and answer the user's question at the end."
new_context_with_instruction = add_custom_instruction_to_context(
new_condition, context
)
new_question = (
"What are 3 questions developers might ask after reading the context above?"
)
new_response = markdown.markdown(
ask_model_with_context(
new_context_with_instruction, new_question
)
)
related_questions = parse_related_questions_response_to_list(new_response)
return related_questions
# Add custom instruction as a prefix to the context
def add_custom_instruction_to_context(condition, context):
new_context = ""
new_context += condition + "\n\n" + context
return new_context
# Use this method for talking to a PaLM text model
def ask_model_with_context(context, question):
new_prompt = f"{context}\n\nQuestion: {question}"
response = call_genai_generate_content(new_prompt)
return response
# Parse a response containing a list of related questions from the language model
# and convert it into an HTML-based list.
def parse_related_questions_response_to_list(response):
soup = BeautifulSoup(response, "html.parser")
questions = []
for item in soup.find_all("li"):
# In case there are code tags, remove the tag and just replace with plain text
if item.find("code"):
text = item.find("code").text
# item.code.replace_with(text)
questions += [text]
# In case there are <p> tags within the <li> strip <p>
if item.find("p"):
text = item.find("p").text
# link = soup.new_tag(
# "a",
# # href=url_for("chatui.question", ask=urllib.parse.quote_plus(text)),
# )
# link.string = text
# item.string = ""
# item.append(link)
questions += [text]
if item.string is not None:
# link = soup.new_tag(
# "a",
# # href=url_for(
# # "chatui.question", ask=urllib.parse.quote_plus(item.string)
# # ),
# )
# link.string = item.string
# item.string = ""
# item.append(link)
questions += [item.string]
return questions
# Print the prompt on the terminal for debugging
def print_the_prompt(prompt):
print("#########################################")
print("# PROMPT #")
print("#########################################")
print(prompt)
print("#########################################")
print("# END OF PROMPT #")
print("#########################################")
print("\n")
@https_fn.on_request(cors=options.CorsOptions(cors_origins="*", cors_methods=["get", "post"]))
def generate_genai_answer(req: https_fn.Request) -> https_fn.Response:
# Grab the text parameter.
prompt = req.args.get("text")
if prompt is None:
return https_fn.Response("No text parameter provided", status=400)
response = call_genai_generate_content(prompt)
return jsonify({
'answer': response
})
@https_fn.on_request(cors=options.CorsOptions(cors_origins="*", cors_methods=["get", "post"]))
def hello(req: https_fn.Request) -> https_fn.Response:
return https_fn.Response("Hello world!")
return jsonify({
'answer': 'hello'
})
def call_genai_generate_content(prompt) -> str:
# Print the prompt for debugging if the log level is VERBOSE.
if LOG_LEVEL == "VERBOSE":
print_the_prompt(prompt)
try:
genai.configure(api_key=GOOGLE_API_KEY)
model = genai.GenerativeModel('gemini-pro')
response = model.generate_content(prompt)
except:
print("Failed to call the model!")
if response.text is None:
print("Block reason: " + str(response.filters))
print("Safety feedback: " + str(response.safety_feedback))
return response.text