Skip to content
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

hotfix: Revert FastAPI change #162

Merged
merged 1 commit into from
Dec 27, 2024
Merged
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
1 change: 0 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
local.settings.json
.env
.venv
32 changes: 16 additions & 16 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,20 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
run: pipx install uv
- name: Install poetry
run: pipx install poetry
- uses: actions/setup-python@v5
with:
python-version-file: pyproject.toml
cache: pip
cache: poetry
- name: Install dependencies
run: |
uv sync
uv run python -m spacy download en_core_web_sm
poetry install
poetry run python -m spacy download en_core_web_sm
- name: Run tests
id: run-tests
run: >
uv run pytest . \
poetry run pytest . \
--junitxml=pytest.xml \
--cov-report=term-missing:skip-covered \
--cov-report=xml:coverage.xml \
Expand All @@ -49,27 +49,27 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
run: pipx install uv
- name: Install poetry
run: pipx install poetry
- uses: actions/setup-python@v5
with:
python-version-file: pyproject.toml
cache: pip
cache: poetry
- name: Install dependencies
run: |
uv sync
uv run ruff check .
poetry install
poetry run ruff check .

mypy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
run: pipx install uv
- name: Install poetry
run: pipx install poetry
- uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: pip
cache: poetry
- run: |
uv sync
uv run mypy .
poetry install
poetry run mypy .
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
.idea
~*
.ruff_cache
.vscode
Expand Down
25 changes: 25 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,37 @@
fail_fast: false

repos:
- repo: https://github.com/python-poetry/poetry
rev: 1.8.0
hooks:
- id: poetry-check

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.0
hooks:
- id: ruff
- id: ruff-format

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.9.0
hooks:
- id: mypy
args: [--ignore-missing-imports, --strict]
additional_dependencies:
- anthropic
- azure-functions
- cmi-docx
- instructor
- openai
- pycap
- pydantic
- pydantic_settings
- pytest
- spacy
- types-aiofiles
- types-python-dateutil
- types-pytz
- types-requests
- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
rev: v2.12.0
hooks:
Expand Down
19 changes: 8 additions & 11 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
# Dockerfile for building the ctk-functions container which runs Azure Functions
# using Python 3.11.
# Dockerfile for building the ctk-functions container which runs Azure Functions using Python 3.11.
#
# Stages:
# 1. `unzipper`: Downloads and unzips Azure Blob Signatures.
# 2. `mcr.microsoft.com/azure-functions/python:4-python3.11`: Final image that will
# run on Azure.
# 2. `mcr.microsoft.com/azure-functions/python:4-python3.11`: Final image that will run on Azure.
#
# Build Arguments:
# - `AZURE_BLOB_SIGNATURES_CONNECTION_STRING`: Connection string that lets the
# container download the signatures file.
# - `AZURE_BLOB_SIGNATURES_CONNECTION_STRING`: Connection string that lets the container download the signatures file.
FROM alpine:latest as unzipper

WORKDIR /files
Expand All @@ -34,8 +31,8 @@ RUN cd /home/site/wwwroot && \
apt-get install -y openjdk-17-jre && \
update-alternatives --config java && \
update-alternatives --config javac && \
pip install uv && \
uv sync && \
uv run python -c 'import spacy; spacy.load("en_core_web_sm")' && \
uv run python -c \
'import language_tool_python; language_tool_python.LanguageTool ("en-US")'
pip install poetry && \
poetry config virtualenvs.create false && \
poetry install --only main --no-interaction --no-ansi && \
poetry run python -c 'import spacy; spacy.load("en_core_web_sm")' && \
poetry run python -c 'import language_tool_python; language_tool_python.LanguageTool("en-US")'
6 changes: 3 additions & 3 deletions azure-pipelines.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ steps:
addToPath: true

- script: |
python -m pip install uv
uv sync
python -m pip install poetry
poetry install
displayName: Install dependencies

- script: uv run python tests/integration/azure_test_intakes.py
- script: poetry run python tests/integration/azure_test_intakes.py
displayName: Run test_intake.py
env:
SURVEY_IDS: $(SURVEY_IDS)
Expand Down
24 changes: 24 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
services:
functions:
build: .
ports:
- 8080:80
env_file:
- .env
develop:
watch:
- action: sync+restart
path: ./src/ctk_functions
target: /home/site/wwwroot/src/ctk_functions
- action: sync+restart
path: ./function_app.py
target: /home/site/wwwroot/function_app.py
- action: sync+restart
path: ./host.json
target: /home/site/wwwroot/host.json
- action: rebuild
path: pyproject.toml
- action: rebuild
path: Dockerfile
- action: rebuild
path: docker-compose.yaml
205 changes: 201 additions & 4 deletions function_app.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,204 @@
"""Entrypoint for Azure Functions."""
"""Entrypoint for the Azure Functions app."""

import azure.functions as func
import http
import json
import typing

from ctk_functions import app as fastapi_app
from azure import functions

app = func.AsgiFunctionApp(app=fastapi_app.app, http_auth_level=func.AuthLevel.FUNCTION)
from ctk_functions import config
from ctk_functions.functions.file_conversion import (
controller as file_conversion_controller,
)
from ctk_functions.functions.intake import controller as intake_controller
from ctk_functions.functions.language_tool import controller as language_tool_controller
from ctk_functions.functions.llm import controller as llm_controller
from ctk_functions.microservices import llm

logger = config.get_logger()

app = functions.FunctionApp()


@app.function_name(name="llm")
@app.route(route="llm", auth_level=functions.AuthLevel.FUNCTION, methods=["POST"])
async def llm_endpoint(req: functions.HttpRequest) -> functions.HttpResponse:
"""Runs a large language model.

The request body should contain a JSON object with the following keys:
- system_prompt: The system prompt.
- user_prompt: The user prompt.
- model: The model name. See ctk_functions.microservices.llm.VALID_LLM_MODELS.

Args:
req: The HTTP request object.

Returns:
The HTTP response containing the output text.
"""
body_dict = json.loads(req.get_body().decode("utf-8"))
system_prompt = body_dict.get("system_prompt")
user_prompt = body_dict.get("user_prompt")
model = body_dict.get("model")

if not system_prompt or not user_prompt or not model:
return functions.HttpResponse(
"Please provide a system prompt, a user prompt, and a model name.",
status_code=http.HTTPStatus.BAD_REQUEST,
)
if model not in typing.get_args(llm.VALID_LLM_MODELS):
return functions.HttpResponse(
"Invalid model, valid models are: "
+ ", ".join(typing.get_args(llm.VALID_LLM_MODELS))
+ ".",
status_code=http.HTTPStatus.BAD_REQUEST,
)

logger.info("Running LLM with model: %s", model)
text = await llm_controller.run_llm(model, system_prompt, user_prompt)
return functions.HttpResponse(
body=text,
status_code=http.HTTPStatus.OK,
)


@app.function_name(name="IntakeReport")
@app.route(
route="intake-report/{survey_id}",
auth_level=functions.AuthLevel.FUNCTION,
methods=["GET"],
)
async def get_intake_report(req: functions.HttpRequest) -> functions.HttpResponse:
"""Generates an intake report for a survey.

The survey ID should be provided in the URL path. The model should be provided as
an "X-Model" header. See ctk_functions.microservices.llm.VALID_LLM_MODELS for valid
models.

Args:
req: The HTTP request object.

Returns:
The HTTP response containing the .docx file.
"""
survey_id = req.route_params.get("survey_id")
model = req.headers.get("X-Model")

if not survey_id or not model:
return functions.HttpResponse(
"Please provide a survey ID and model name.",
status_code=http.HTTPStatus.BAD_REQUEST,
)
if model not in typing.get_args(llm.VALID_LLM_MODELS):
return functions.HttpResponse(
"Invalid model, valid models are: "
+ ", ".join(typing.get_args(llm.VALID_LLM_MODELS))
+ ".",
status_code=http.HTTPStatus.BAD_REQUEST,
)

logger.info(
"Generating intake report for identifier %s with model %s.",
survey_id,
model,
)
return await intake_controller.get_intake_report(survey_id, model)


@app.function_name(name="MarkdownToDocx")
@app.route(
route="markdown2docx",
auth_level=functions.AuthLevel.FUNCTION,
methods=["POST"],
)
async def markdown2docx(req: functions.HttpRequest) -> functions.HttpResponse:
"""Converts a Markdown document to a .docx file.

The request body should contain a JSON object with the following keys:
- markdown: The Markdown document.
- formatting: Formatting options, must abide by cmi_docx.ParagraphStyle arguments.

Args:
req: The HTTP request object.

Returns:
The HTTP response containing the .docx file.
"""
body_dict = json.loads(req.get_body().decode("utf-8"))
formatting = json.loads(body_dict.get("formatting", "{}"))

markdown = body_dict.get("markdown", None)
if not markdown:
return functions.HttpResponse(
"Please provide a Markdown document.",
status_code=http.HTTPStatus.BAD_REQUEST,
)
logger.info("Converting Markdown to .docx")
docx_bytes = file_conversion_controller.markdown2docx(markdown, formatting)
return functions.HttpResponse(
body=docx_bytes,
status_code=http.HTTPStatus.OK,
mimetype="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
)


@app.function_name(name="LanguageTool")
@app.route(
route="language-tool",
auth_level=functions.AuthLevel.FUNCTION,
methods=["POST"],
)
async def language_tool(req: functions.HttpRequest) -> functions.HttpResponse:
"""Runs the LanguageTool grammar checker.

The request body should contain a JSON object with the following keys:
- text: The text to correct.
- rules: The rules to enable c.f. LanguageTool for a list of rules.

Args:
req: The HTTP request object.

Returns:
The HTTP response containing the output text.
"""
body_dict = json.loads(req.get_body().decode("utf-8"))
text = body_dict.get("text")
rules = json.loads(body_dict.get("rules", "[]"))

if not rules:
return functions.HttpResponse(
"Please provide some rules.",
status_code=http.HTTPStatus.BAD_REQUEST,
)

if not text:
return functions.HttpResponse(
"Please provide some text.",
status_code=http.HTTPStatus.BAD_REQUEST,
)

logger.info("Running LanguageTool")
corrected_text = language_tool_controller.language_tool(text, rules)
return functions.HttpResponse(
body=corrected_text,
status_code=http.HTTPStatus.OK,
)


@app.function_name(name="Health")
@app.route(route="health", auth_level=functions.AuthLevel.FUNCTION, methods=["GET"])
async def health(req: functions.HttpRequest) -> functions.HttpResponse: # noqa: ARG001
"""Health check endpoint.

This endpoint takes no arguments and always returns a 200 OK response.

Args:
req: The HTTP request object.

Returns:
The HTTP response indicating the health of the app.
"""
return functions.HttpResponse(
body="Healthy",
status_code=http.HTTPStatus.OK,
)
Loading
Loading