-
-
Notifications
You must be signed in to change notification settings - Fork 137
Add Flask API which wraps the existing LLM based Python functionality #1789
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
5dd2c19
9275393
d73a231
1f00ff5
630084e
6fe44ec
8e60198
69f46cc
7901748
86cf80b
513505e
b54adb1
edbbdd9
3e0fa2b
8147020
f9a2813
e1f4917
24eac79
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,12 +4,23 @@ | |
"cleanUrls": true, | ||
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"] | ||
}, | ||
"functions": { | ||
"predeploy": ["yarn build:functions"], | ||
"source": "functions", | ||
"runtime": "nodejs18", | ||
"runtimeConfig": ".runtimeconfig.json" | ||
}, | ||
"functions": [ | ||
{ | ||
"predeploy": ["yarn build:functions"], | ||
"source": "functions", | ||
"codebase": "maple", | ||
"runtime": "nodejs18", | ||
"runtimeConfig": ".runtimeconfig.json" | ||
}, | ||
{ | ||
"predeploy": [ | ||
". llm/venv/bin/activate && python3 -m pip install -r llm/requirements.txt" | ||
], | ||
"source": "llm", | ||
"codebase": "maple-llm", | ||
"runtime": "python311" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the version running inside |
||
} | ||
], | ||
"firestore": { | ||
"rules": "firestore.rules", | ||
"indexes": "firestore.indexes.json" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
FROM andreysenov/firebase-tools:latest-node-18 | ||
|
||
USER root | ||
RUN apt update && apt install -y curl | ||
RUN apt update && apt install -y curl python3 python3-pip python3-venv | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need python in this container to build the virtual environments, see |
||
|
||
WORKDIR /app | ||
RUN chown -R node:node . | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,11 +4,22 @@ | |
"cleanUrls": true, | ||
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"] | ||
}, | ||
"functions": { | ||
"predeploy": ["yarn build:functions"], | ||
"source": "functions", | ||
"runtime": "nodejs18" | ||
}, | ||
"functions": [ | ||
{ | ||
"predeploy": ["yarn build:functions"], | ||
"source": "functions", | ||
"codebase": "maple", | ||
"runtime": "nodejs18" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does it matter that this doesn't have the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Honestly, I don't think we're even sure that |
||
}, | ||
{ | ||
"predeploy": [ | ||
". llm/venv/bin/activate && python3 -m pip install -r llm/requirements.txt" | ||
], | ||
"source": "llm", | ||
"codebase": "maple-llm", | ||
"runtime": "python311" | ||
} | ||
], | ||
"firestore": { | ||
"rules": "firestore.rules", | ||
"indexes": "firestore.indexes.json" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
venv/ | ||
__pycache__/ | ||
databases/ | ||
.secret.local |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -278,6 +278,7 @@ def set_my_llm_cache(cache_file: Path=LLM_CACHE) -> SQLiteCache: | |
Set an LLM cache, which allows for previously executed completions to be | ||
loaded from disk instead of repeatedly queried. | ||
""" | ||
cache_file.parent.mkdir(exist_ok=True) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was required because the container doesn't have this path and we need it to build the LLM cache |
||
set_llm_cache(SQLiteCache(database_path = cache_file)) | ||
|
||
@dataclass() | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
from flask import Flask, jsonify, abort, request | ||
from llm_functions import get_summary_api_function, get_tags_api_function | ||
import json | ||
from firebase_admin import initialize_app | ||
from firebase_functions import https_fn, options | ||
import os | ||
|
||
initialize_app() | ||
app = Flask(__name__) | ||
|
||
|
||
def is_intersection(keys, required_keys): | ||
return (keys & required_keys) == required_keys | ||
|
||
|
||
def set_openai_api_key(): | ||
match os.environ.get("MAPLE_DEV"): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AFAICT, we use this to deploy dev versus prod. I re-use it here to select an environment. IIUC, we have a lots less money in |
||
case "prod": | ||
if os.environ.get("OPENAI_PROD") != None: | ||
os.environ["OPENAI_API_KEY"] = os.environ["OPENAI_PROD"] | ||
case _: # if "dev" or unspecified, use OPENAI_DEV | ||
if os.environ.get("OPENAI_DEV") != None: | ||
os.environ["OPENAI_API_KEY"] = os.environ["OPENAI_DEV"] | ||
|
||
|
||
@app.route("/summary", methods=["POST"]) | ||
def summary(): | ||
set_openai_api_key() | ||
body = json.loads(request.data) | ||
# We require bill_id, bill_title, bill_text to exist as keys in the POST | ||
if not is_intersection(body.keys(), {"bill_id", "bill_title", "bill_text"}): | ||
abort(404, description="requires bill_id, bill_title, and bill_text") | ||
|
||
summary = get_summary_api_function( | ||
body["bill_id"], body["bill_title"], body["bill_text"] | ||
) | ||
|
||
if summary["status"] in [-1, -2]: | ||
abort(500, description="Unable to generate summary") | ||
Comment on lines
+38
to
+39
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The status field returns either -1 or -2 if it fails... This is probably somewhat unfortunate here because if this API ever changes I will not be able to know about it. I'll add a note to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Or, maybe I should just check that this value is negative? technically the return type is |
||
|
||
return jsonify(summary["summary"]) | ||
|
||
|
||
@app.route("/tags", methods=["POST"]) | ||
def tags(): | ||
set_openai_api_key() | ||
body = json.loads(request.data) | ||
# We require bill_id, bill_title, bill_text to exist as keys in the POST | ||
# Note: & is essentially set intersection | ||
if not is_intersection(body.keys(), {"bill_id", "bill_title", "bill_text"}): | ||
abort(404, description="requires bill_id, bill_title, and bill_text") | ||
|
||
tags = get_tags_api_function(body["bill_id"], body["bill_title"], body["bill_text"]) | ||
|
||
if tags["status"] in [-1, -2]: | ||
abort(500, description="Unable to generate tags") | ||
Comment on lines
+55
to
+56
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably check for negative return type? See above https://github.com/codeforboston/maple/pull/1789/files#r2055086855 |
||
|
||
return jsonify(tags["tags"]) | ||
|
||
|
||
@app.route("/ready", methods=["GET"]) | ||
def ready(): | ||
return "" | ||
Comment on lines
+61
to
+63
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This may not be necessary... |
||
|
||
|
||
@https_fn.on_request( | ||
secrets=["OPENAI_DEV", "OPENAI_PROD"], | ||
timeout_sec=300, | ||
memory=options.MemoryOption.GB_1, | ||
) | ||
def httpsflaskexample(req: https_fn.Request) -> https_fn.Response: | ||
with app.request_context(req.environ): | ||
return app.full_dispatch_request() |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -122,3 +122,74 @@ This project uses OpenAI's API for various language processing tasks. To use the | |
```python | ||
import os | ||
print(os.environ.get('OPENAI_API_KEY')) | ||
|
||
# Running the API | ||
|
||
Set up a virtual environment and run the Flask app | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Conceptually, there are a few ways you can run this application.
"2." is by far the hardest because we directly overlay |
||
|
||
``` | ||
python3 -m venv venv | ||
source venv/bin/activate # .fish if using fish | ||
pip3 install -r requirements.txt | ||
python3 -m flask --app main run | ||
``` | ||
chiroptical marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
## Infrastructure notes | ||
|
||
As of 2025-06-17, the version of `python3` inside the | ||
`infra/Dockerfile.firebase` is 3.11. Therefore, the `firebase.json` files use | ||
the `python311` runtime. | ||
|
||
## Deploying locally | ||
|
||
This is quite tricky due to how we overlay our current source directory to | ||
`/app` inside the container. You'll need to create and install dependencies from | ||
**inside** the container. If you are just working on python related code that | ||
doesn't need to be in Firebase, you **won't** be able to use this environment. | ||
|
||
```shell | ||
# Build the maple-firebase container | ||
yarn dev:update | ||
# Start up bash within the maple-firebase container | ||
docker run -v .:/app -it maple-firebase /bin/bash | ||
# Build the virtual env and install the dependencies matching the container | ||
python3 -m venv llm/venv | ||
source llm/venv/bin/activate | ||
pip3 install -r llm/requirements.txt | ||
``` | ||
|
||
Note: you'll need to set `OPENAI_DEV` and `OPENAI_PROD` in a | ||
`llm/.secret.local` file. Get it with `firebase functions:secrets:access | ||
OPENAI_DEV`. They can be set to the same token. You can see the function URL | ||
in the emulator after running `yarn dev:up`. | ||
|
||
## Deploying to Firebase | ||
|
||
```shell | ||
# not sure if the GOOGLE_APPLICATION_CREDENTIALS is strictly necessary, but I | ||
# had a number of problems with authorization | ||
GOOGLE_APPLICATION_CREDENTIALS=/path/to/application_default_credentials.json \ | ||
firebase deploy --only functions:maple-llm --debug | ||
|
||
# Hit the function in production | ||
curl \ | ||
-X POST \ | ||
-H "Content-Type: application/json" \ | ||
-H "Authorization: Bearer $(gcloud auth print-identity-token)" \ | ||
-d '{"bill_id": "1234","bill_title": "A title","bill_text": "Some bill text"}' \ | ||
https://httpsflaskexample-ke6znoupgq-uc.a.run.app/summary | ||
``` | ||
|
||
## Future work | ||
|
||
Currently, we are just using the built-in Flask server. We should switch to a | ||
production WSGI server, like `gunicorn`. | ||
|
||
The local emulator installation process is quite cumbersome and ideally the | ||
virtual environment was built during container instantiation instead of from | ||
within the Docker container (i.e. the "Deploying locally" docs above). | ||
|
||
The current API is a little wonky because we take `bill_id` **and** `bill_text`. | ||
We could just look up the `bill_text` via the `bill_id` using the Firestore API. | ||
It might make sense to avoid the HTTP wrapper all-together and figure out how | ||
JS <-> Python communication works without an HTTP layer. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: this PR requires an update to
firebase-tools
inpackage.json
!