Skip to content

Commit 9c54a41

Browse files
authored
Merge pull request #1789 from chiroptical/llm-flask-api
Add Flask API which wraps the existing LLM based Python functionality
2 parents e3067aa + 24eac79 commit 9c54a41

File tree

10 files changed

+217
-186
lines changed

10 files changed

+217
-186
lines changed

firebase.json

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,23 @@
44
"cleanUrls": true,
55
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"]
66
},
7-
"functions": {
8-
"predeploy": ["yarn build:functions"],
9-
"source": "functions",
10-
"runtime": "nodejs18",
11-
"runtimeConfig": ".runtimeconfig.json"
12-
},
7+
"functions": [
8+
{
9+
"predeploy": ["yarn build:functions"],
10+
"source": "functions",
11+
"codebase": "maple",
12+
"runtime": "nodejs18",
13+
"runtimeConfig": ".runtimeconfig.json"
14+
},
15+
{
16+
"predeploy": [
17+
". llm/venv/bin/activate && python3 -m pip install -r llm/requirements.txt"
18+
],
19+
"source": "llm",
20+
"codebase": "maple-llm",
21+
"runtime": "python311"
22+
}
23+
],
1324
"firestore": {
1425
"rules": "firestore.rules",
1526
"indexes": "firestore.indexes.json"

infra/Dockerfile.firebase

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
FROM andreysenov/firebase-tools:latest-node-18
22

33
USER root
4-
RUN apt update && apt install -y curl
4+
RUN apt update && apt install -y curl python3 python3-pip python3-venv
55

66
WORKDIR /app
77
RUN chown -R node:node .

infra/firebase.compose.json

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,22 @@
44
"cleanUrls": true,
55
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"]
66
},
7-
"functions": {
8-
"predeploy": ["yarn build:functions"],
9-
"source": "functions",
10-
"runtime": "nodejs18"
11-
},
7+
"functions": [
8+
{
9+
"predeploy": ["yarn build:functions"],
10+
"source": "functions",
11+
"codebase": "maple",
12+
"runtime": "nodejs18"
13+
},
14+
{
15+
"predeploy": [
16+
". llm/venv/bin/activate && python3 -m pip install -r llm/requirements.txt"
17+
],
18+
"source": "llm",
19+
"codebase": "maple-llm",
20+
"runtime": "python311"
21+
}
22+
],
1223
"firestore": {
1324
"rules": "firestore.rules",
1425
"indexes": "firestore.indexes.json"

llm/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
venv/
2+
__pycache__/
3+
databases/
4+
.secret.local

llm/llm_functions.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ def set_my_llm_cache(cache_file: Path=LLM_CACHE) -> SQLiteCache:
278278
Set an LLM cache, which allows for previously executed completions to be
279279
loaded from disk instead of repeatedly queried.
280280
"""
281+
cache_file.parent.mkdir(exist_ok=True)
281282
set_llm_cache(SQLiteCache(database_path = cache_file))
282283

283284
@dataclass()

llm/main.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
from flask import Flask, jsonify, abort, request
2+
from llm_functions import get_summary_api_function, get_tags_api_function
3+
import json
4+
from firebase_admin import initialize_app
5+
from firebase_functions import https_fn, options
6+
import os
7+
8+
initialize_app()
9+
app = Flask(__name__)
10+
11+
12+
def is_intersection(keys, required_keys):
13+
return (keys & required_keys) == required_keys
14+
15+
16+
def set_openai_api_key():
17+
match os.environ.get("MAPLE_DEV"):
18+
case "prod":
19+
if os.environ.get("OPENAI_PROD") != None:
20+
os.environ["OPENAI_API_KEY"] = os.environ["OPENAI_PROD"]
21+
case _: # if "dev" or unspecified, use OPENAI_DEV
22+
if os.environ.get("OPENAI_DEV") != None:
23+
os.environ["OPENAI_API_KEY"] = os.environ["OPENAI_DEV"]
24+
25+
26+
@app.route("/summary", methods=["POST"])
27+
def summary():
28+
set_openai_api_key()
29+
body = json.loads(request.data)
30+
# We require bill_id, bill_title, bill_text to exist as keys in the POST
31+
if not is_intersection(body.keys(), {"bill_id", "bill_title", "bill_text"}):
32+
abort(404, description="requires bill_id, bill_title, and bill_text")
33+
34+
summary = get_summary_api_function(
35+
body["bill_id"], body["bill_title"], body["bill_text"]
36+
)
37+
38+
if summary["status"] in [-1, -2]:
39+
abort(500, description="Unable to generate summary")
40+
41+
return jsonify(summary["summary"])
42+
43+
44+
@app.route("/tags", methods=["POST"])
45+
def tags():
46+
set_openai_api_key()
47+
body = json.loads(request.data)
48+
# We require bill_id, bill_title, bill_text to exist as keys in the POST
49+
# Note: & is essentially set intersection
50+
if not is_intersection(body.keys(), {"bill_id", "bill_title", "bill_text"}):
51+
abort(404, description="requires bill_id, bill_title, and bill_text")
52+
53+
tags = get_tags_api_function(body["bill_id"], body["bill_title"], body["bill_text"])
54+
55+
if tags["status"] in [-1, -2]:
56+
abort(500, description="Unable to generate tags")
57+
58+
return jsonify(tags["tags"])
59+
60+
61+
@app.route("/ready", methods=["GET"])
62+
def ready():
63+
return ""
64+
65+
66+
@https_fn.on_request(
67+
secrets=["OPENAI_DEV", "OPENAI_PROD"],
68+
timeout_sec=300,
69+
memory=options.MemoryOption.GB_1,
70+
)
71+
def httpsflaskexample(req: https_fn.Request) -> https_fn.Response:
72+
with app.request_context(req.environ):
73+
return app.full_dispatch_request()

llm/readme.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,74 @@ This project uses OpenAI's API for various language processing tasks. To use the
122122
```python
123123
import os
124124
print(os.environ.get('OPENAI_API_KEY'))
125+
126+
# Running the API
127+
128+
Set up a virtual environment and run the Flask app
129+
130+
```
131+
python3 -m venv venv
132+
source venv/bin/activate # .fish if using fish
133+
pip3 install -r requirements.txt
134+
python3 -m flask --app main run
135+
```
136+
137+
## Infrastructure notes
138+
139+
As of 2025-06-17, the version of `python3` inside the
140+
`infra/Dockerfile.firebase` is 3.11. Therefore, the `firebase.json` files use
141+
the `python311` runtime.
142+
143+
## Deploying locally
144+
145+
This is quite tricky due to how we overlay our current source directory to
146+
`/app` inside the container. You'll need to create and install dependencies from
147+
**inside** the container. If you are just working on python related code that
148+
doesn't need to be in Firebase, you **won't** be able to use this environment.
149+
150+
```shell
151+
# Build the maple-firebase container
152+
yarn dev:update
153+
# Start up bash within the maple-firebase container
154+
docker run -v .:/app -it maple-firebase /bin/bash
155+
# Build the virtual env and install the dependencies matching the container
156+
python3 -m venv llm/venv
157+
source llm/venv/bin/activate
158+
pip3 install -r llm/requirements.txt
159+
```
160+
161+
Note: you'll need to set `OPENAI_DEV` and `OPENAI_PROD` in a
162+
`llm/.secret.local` file. Get it with `firebase functions:secrets:access
163+
OPENAI_DEV`. They can be set to the same token. You can see the function URL
164+
in the emulator after running `yarn dev:up`.
165+
166+
## Deploying to Firebase
167+
168+
```shell
169+
# not sure if the GOOGLE_APPLICATION_CREDENTIALS is strictly necessary, but I
170+
# had a number of problems with authorization
171+
GOOGLE_APPLICATION_CREDENTIALS=/path/to/application_default_credentials.json \
172+
firebase deploy --only functions:maple-llm --debug
173+
174+
# Hit the function in production
175+
curl \
176+
-X POST \
177+
-H "Content-Type: application/json" \
178+
-H "Authorization: Bearer $(gcloud auth print-identity-token)" \
179+
-d '{"bill_id": "1234","bill_title": "A title","bill_text": "Some bill text"}' \
180+
https://httpsflaskexample-ke6znoupgq-uc.a.run.app/summary
181+
```
182+
183+
## Future work
184+
185+
Currently, we are just using the built-in Flask server. We should switch to a
186+
production WSGI server, like `gunicorn`.
187+
188+
The local emulator installation process is quite cumbersome and ideally the
189+
virtual environment was built during container instantiation instead of from
190+
within the Docker container (i.e. the "Deploying locally" docs above).
191+
192+
The current API is a little wonky because we take `bill_id` **and** `bill_text`.
193+
We could just look up the `bill_text` via the `bill_id` using the Firestore API.
194+
It might make sense to avoid the HTTP wrapper all-together and figure out how
195+
JS <-> Python communication works without an HTTP layer.

0 commit comments

Comments
 (0)