Skip to content

Llm flask api #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

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions firebase.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,20 @@
"cleanUrls": true,
Copy link
Collaborator Author

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 in package.json!

"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"
},
{
"predeploy": [". llm/venv/bin/activate && python3 -m pip install -r llm/requirements.txt"],
"source": "llm",
"codebase": "maple-llm",
"runtime": "python311"
Copy link
Collaborator Author

@chiroptical chiroptical Jun 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the version running inside Dockerfile.firebase, and this is actually required to build the virtual environment see llm/readme.md for more information.

}
],
"firestore": {
"rules": "firestore.rules",
"indexes": "firestore.indexes.json"
Expand Down
2 changes: 1 addition & 1 deletion infra/Dockerfile.firebase
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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need python in this container to build the virtual environments, see readme.md


WORKDIR /app
RUN chown -R node:node .
Expand Down
19 changes: 14 additions & 5 deletions infra/firebase.compose.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,20 @@
"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"
},
{
"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"
Expand Down
3 changes: 3 additions & 0 deletions llm/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
venv/
__pycache__/
databases/
1 change: 1 addition & 0 deletions llm/llm_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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()
Expand Down
73 changes: 73 additions & 0 deletions llm/main.py
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"):
Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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 lost less dollars in OPENAI_DEV so it seems like a safe default.

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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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 get_summary_api_function but we don't really have types.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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 int


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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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()
52 changes: 52 additions & 0 deletions llm/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,55 @@ 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
Copy link
Collaborator Author

@chiroptical chiroptical Jun 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Conceptually, there are a few ways you can run this application.

  1. Locally with no dependencies on firebase, that is the first set of notes
  2. Deployed as a function with firebase locally, that is the second set of notes
  3. Deploying to firebase directly

"2." is by far the hardest because we directly overlay /app into the Docker.firebase container in docker-compose.yaml. So, we need the virtual env and python version when running inside the container to match! Therefore, either we stop overlaying /app or we build the virtual environment from inside the container. We chose the latter because it is a simpler change.


```
python3 -m venv venv
source venv/bin/activate # .fish if using fish
pip3 install -r requirements.txt
python3 -m flask --app main run
```

## 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
```

## 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
```
177 changes: 5 additions & 172 deletions llm/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,183 +1,16 @@
absl-py==2.1.0
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minimized and simplified the requirements file.

aiohttp==3.9.5
aiosignal==1.3.1
altair==5.3.0
annotated-types==0.7.0
anyio==4.4.0
asgiref==3.8.1
asttokens==2.4.1
attrs==23.2.0
backoff==2.2.1
bcrypt==4.1.3
blinker==1.8.2
blis==0.7.11
build==1.2.1
cachetools==5.3.3
catalogue==2.0.10
certifi==2024.6.2
charset-normalizer==3.3.2
chroma-hnswlib==0.7.3
Flask==3.1.0
chromadb==0.5.0
click==8.1.7
cloudpathlib==0.18.1
coloredlogs==15.0.1
confection==0.1.5
cymem==2.0.8
dataclasses-json==0.6.6
decorator==5.1.1
Deprecated==1.2.14
distro==1.9.0
dnspython==2.6.1
email_validator==2.1.1
executing==2.0.1
fastapi==0.111.0
fastapi-cli==0.0.4
filelock==3.14.0
flatbuffers==24.3.25
frozenlist==1.4.1
fsspec==2024.6.0
git-filter-repo==2.45.0
gitdb==4.0.11
GitPython==3.1.43
google-auth==2.29.0
googleapis-common-protos==1.63.1
grpcio==1.64.1
h11==0.14.0
httpcore==1.0.5
httptools==0.6.1
httpx==0.27.0
huggingface-hub==0.25.2
humanfriendly==10.0
idna==3.7
importlib_metadata==7.1.0
importlib_resources==6.4.0
ipdb==0.13.13
ipython==8.25.0
jedi==0.19.1
Jinja2==3.1.4
joblib==1.4.2
jsonpatch==1.33
jsonpointer==2.4
jsonschema==4.22.0
jsonschema-specifications==2023.12.1
kubernetes==29.0.0
langchain==0.2.1
firebase-admin==6.7.0
firebase-functions==0.4.2
langchain-community==0.2.1
langchain-core==0.2.3
langchain-openai==0.1.8
langchain-text-splitters==0.2.0
langcodes==3.4.0
langsmith==0.1.69
language_data==1.2.0
marisa-trie==1.2.0
markdown-it-py==3.0.0
MarkupSafe==2.1.5
marshmallow==3.21.2
matplotlib-inline==0.1.7
mdurl==0.1.2
mmh3==4.1.0
monotonic==1.6
mpmath==1.3.0
multidict==6.0.5
murmurhash==1.0.10
mypy-extensions==1.0.0
networkx==3.3
nltk==3.8.1
langchain==0.2.1
numpy==1.26.4
oauthlib==3.2.2
onnxruntime==1.18.0
openai==1.31.0
opentelemetry-api==1.25.0
opentelemetry-exporter-otlp-proto-common==1.25.0
opentelemetry-exporter-otlp-proto-grpc==1.25.0
opentelemetry-instrumentation==0.46b0
opentelemetry-instrumentation-asgi==0.46b0
opentelemetry-instrumentation-fastapi==0.46b0
opentelemetry-proto==1.25.0
opentelemetry-sdk==1.25.0
opentelemetry-semantic-conventions==0.46b0
opentelemetry-util-http==0.46b0
orjson==3.10.3
overrides==7.7.0
packaging==23.2
pandas==2.2.2
parso==0.8.4
pexpect==4.9.0
pillow==10.3.0
posthog==3.5.0
preshed==3.0.9
prompt_toolkit==3.0.45
protobuf==4.25.3
ptyprocess==0.7.0
pure-eval==0.2.2
pyarrow==16.1.0
pyasn1==0.6.0
pyasn1_modules==0.4.0
pydantic==2.7.3
pydantic_core==2.18.4
pydeck==0.9.1
Pygments==2.18.0
PyPika==0.48.9
pyproject_hooks==1.1.0
python-dateutil==2.9.0.post0
python-dotenv==1.0.1
python-multipart==0.0.9
pytz==2024.1
PyYAML==6.0.1
redis==5.0.7
referencing==0.35.1
regex==2024.5.15
pytest==8.3.5
requests==2.32.3
requests-oauthlib==2.0.0
rich==13.7.1
rouge_score==0.1.2
rpds-py==0.18.1
rsa==4.9
safetensors==0.4.3
scikit-learn==1.5.0
scipy==1.13.1
sentence-transformers==3.0.0
setuptools==70.0.0
shellingham==1.5.4
six==1.16.0
smart-open==7.0.4
smmap==5.0.1
sniffio==1.3.1
spacy==3.7.5
spacy-legacy==3.0.12
spacy-loggers==1.0.5
SQLAlchemy==2.0.30
srsly==2.4.8
stack-data==0.6.3
starlette==0.37.2
streamlit==1.35.0
sympy==1.12.1
tenacity==8.3.0
thinc==8.2.5
threadpoolctl==3.5.0
tiktoken==0.7.0
tokenizers==0.20.1
toml==0.10.2
toolz==0.12.1
torch==2.4.1
tornado==6.4
tqdm==4.66.4
traitlets==5.14.3
transformers==4.45.2
typer==0.12.3
typing-inspect==0.9.0
typing_extensions==4.12.1
tzdata==2024.1
ujson==5.10.0
urllib3==2.2.1
uvicorn==0.30.1
uvloop==0.19.0
wasabi==1.1.3
watchfiles==0.22.0
wcwidth==0.2.13
weasel==0.4.1
websocket-client==1.8.0
websockets==12.0
wrapt==1.16.0
yarl==1.9.4
zipp==3.19.1
Loading