diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 1ce5c7e..cd919cd 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,49 +3,30 @@ { "name": "FastAPI", "build": { - "context": ".", + "context": ".", "dockerfile": "Dockerfile" }, - - "features": { - "ghcr.io/devcontainers/features/docker-from-docker:1": { - "version": "20.10" - } - }, - "customizations": { - // Configure properties specific to VS Code. - "vscode": { - // Set *default* container specific settings.json values on container create. - "settings": { - "[python]": { - "editor.formatOnType": true, - "editor.formatOnSave": true - } - }, - // Add the IDs of extensions you want installed when the container is created. - "extensions": [ - "ms-python.python", - "GitHub.copilot-nightly", + "vscode": { + "extensions": [ + "ms-python.python", "ms-azuretools.vscode-docker" - ] - } - }, - + ] + } + }, + "features": { + "ghcr.io/devcontainers/features/docker-from-docker:1": { + "version": "20.10" + } + }, "forwardPorts": [ 8000, - 6379 + 6379 ], - - "hostRequirements": { + "hostRequirements": { "memory": "8gb" }, - - "postAttachCommand": "chmod +x .devcontainer/setup.sh && .devcontainer/setup.sh" - - // Use 'postCreateCommand' to run commands after the container is created. - // "postCreateCommand": "pip3 install --user -r requirements.txt", - - // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. - // "remoteUser": "root" + "waitFor": "onCreateCommand", + "updateContentCommand": "python3 -m pip install -r requirements.txt", + "postAttachCommand": "chmod +x .devcontainer/setup.sh && .devcontainer/setup.sh" } \ No newline at end of file diff --git a/.devcontainer/setup.sh b/.devcontainer/setup.sh index 2155d80..71c9bd8 100755 --- a/.devcontainer/setup.sh +++ b/.devcontainer/setup.sh @@ -1,40 +1,13 @@ #!/bin/sh # set -eu -echo "Installing libraries from requirements.txt..." -chmod +x ./requirements.txt && pip install -r ./requirements.txt -echo "Done." -echo +echo "Starting 'redis' container..." # Check if the "redis" container is running if ! docker ps --filter "status=running" --format "{{.Names}}" | grep -q "redis"; then # If the "redis" container is not running, start it using docker-compose docker-compose -f ./docker-compose.yml up -d + echo "Successfully started 'redis' container." else echo "The 'redis' container is already running." fi - -echo -echo "Let's set up your development environment..." -echo -echo "Please enter your OpenAI API key found here: https://platform.openai.com/account/api-keys:" -read -r OPENAI_API_KEY - -# Export the OPENAI_API_KEY environment variable -export OPENAI_API_KEY -export DATASTORE=redis -export BEARER_TOKEN=footoken -export PLUGIN_HOSTNAME=https://$CODESPACE_NAME-8000.$GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN - -echo -echo "Setting host configuration (from ./hostconfig.sh)..." -chmod +x ./hostconfig.sh && ./hostconfig.sh - -echo -echo "Click on GitHub Codespaces PORTS tab. Right click on port 8000, and set Port Visibility to Public. Once Port 8000 if Public, press Enter to continue..." -read -r placeholder_var - -echo "Once your app is running, use the following URL to use this plugin in the OpenAI Plugin store:" -echo $PLUGIN_HOSTNAME -echo -echo "Enter 'footoken' if OpenAI prompts you for a Bearer Token" diff --git a/.gitignore b/.gitignore deleted file mode 100644 index b224641..0000000 --- a/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -__pycache__/ - *.pyc diff --git a/.vscode/json.code-snippets b/.vscode/json.code-snippets deleted file mode 100644 index d04add9..0000000 --- a/.vscode/json.code-snippets +++ /dev/null @@ -1,7 +0,0 @@ -{ - "OpenAPI Manifest": { - "prefix": "manifest-openapi", - "body": ["{\n\t\"schema_version\": \"${1:v1}\",\n\t\"name_for_human\": \"${2}\",\n\t\"name_for_model\": \"${3}\",\n\t\"description_for_human\": \"${4}\",\n\t\"description_for_model\": \"${5}\",\n\t\"auth\": {\n\t\t\"type\": \"${6:none}\"\n\t},\n\t\"api\": {\n\t\t\"type\": \"openapi\",\n\t\t\"url\": \"${7:https://your-app-url.com/openapi.yaml}\",\n\t\t\"is_user_authenticated\": \"${8:false}\"\n\t},\n\t\"logo_url\": \"${9:https://your-app-url.com/logo.png}\",\n\t\"contact_email\": \"${10:example@company.com}\",\n\t\"legal_info_url\": \"${11:https://example.com/legal}\"\n}$0"], - "description": "OpenAI manifest" - } -} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 757d17b..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Python: FastAPI", - "type": "python", - "request": "launch", - "module": "uvicorn", - "args": [ - "main:app" - ], - "jinja": true, - "justMyCode": true - } - ] -} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index f778b8e..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "json.schemas": [ - { - "fileMatch": [ - "ai-plugin.json" - ], - "url": "https://raw.githubusercontent.com/minsa110/ai-plugin-schema/main/ai-plugin-schema.json" - } - ] -} diff --git a/.well-known/ai-plugin.json b/.well-known/ai-plugin.json deleted file mode 100644 index 684971a..0000000 --- a/.well-known/ai-plugin.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "schema_version": "v1", - "name_for_human": "TODO app", - "name_for_model": "TODO_APP", - "description_for_human": "Todo app for managing your tasks", - "description_for_model": "Todo app for managing your tasks", - "auth": { - "type": "user_http", - "authorization_type": "bearer" - }, - "api": { - "type": "openapi", - "url": "https://minsa110-laughing-eureka-979q457rx6q3p954-8000.preview.app.github.dev/.well-known/openapi.yaml", - "is_user_authenticated": "false" - }, - "logo_url": "https://minsa110-laughing-eureka-979q457rx6q3p954-8000.preview.app.github.dev/.well-known/logo.png", - "contact_email": "example@company.com", - "legal_info_url": "https://example.com/legal" -} diff --git a/.well-known/logo.png b/.well-known/logo.png deleted file mode 100644 index 5bffb38..0000000 Binary files a/.well-known/logo.png and /dev/null differ diff --git a/.well-known/openapi.yaml b/.well-known/openapi.yaml index 244e4d7..e779639 100644 --- a/.well-known/openapi.yaml +++ b/.well-known/openapi.yaml @@ -1,10 +1,8 @@ openapi: 3.0.2 info: title: OpenAI plugin for a simple todo app - description: Todo app for managing your tasks on ChatGPT + description: Todo app for managing your tasks version: 1.0.0 - servers: - - url: https://minsa110-laughing-eureka-979q457rx6q3p954-8000.preview.app.github.dev paths: /todos: post: @@ -18,12 +16,6 @@ paths: type: string required: true description: The description of the TODO item - # requestBody: - # required: true - # content: - # application/json: - # schema: - # $ref: "#/components/schemas/TodoItem" responses: "200": description: OK diff --git a/README.md b/README.md index e6a082c..dc577b6 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,13 @@ -# Building an Open AI plugin with Codespaces -☁️ **To deploy your app on Azure using Azure Developer CLI and Container Apps, see the [demo-azd](https://github.com/minsa110/devcontainer-fastapi/tree/demo-azd) branch.** +# GitHub Codespaces ❤️ FastAPI -This is a sample repo for developing OpenAI plugin using the FastAPI framework. There are two main parts of the repo: -- Code to help setup development environment for FastAPI framework -- Code for the FastAPI app +Welcome to your shiny new Codespace running FastAPI! We've got everything fired up and running for you to explore FastAPI. -## 📦 Code to help setup development environment -Create a Codespaces by clicking **<> Code** -> **Codespaces** -> **Create codespaces on {branch}**, and a containerized development environment will be set up for you on the cloud based on the contents of the following files. +You've got a blank canvas to work on from a git perspective as well. There's a single initial commit with what you're seeing right now - where you go from here is up to you! -### **.devcontainer** -The `.devcontainer` folder contains files for defining a containerized development environment, specific to building this FastAPI app. It's set up in a way that makes it easy for you to use with GitHub Codespaces as well: launch a Codespace using this template, and you're ready to start developing! Learn more about devcontainers [here](https://containers.dev/). +Everything you do here is contained within this one codespace. There is no repository on GitHub yet. If and when you’re ready you can click "Publish Branch" and we’ll create your repository and push up your project. If you were just exploring then and have no further need for this code then you can simply delete your codespace and it's gone forever. -### **.vscode** -The `.vscode` folder contains: -- `json.code-snippets` file that helps to quickly write the manifest file for the OpenAI plugin. (✨ Tip: Type `manifest-openai`, press `enter` to accept the template, and `tab` through the fields to quickly generate the manifest) -- `settings.json` file that helps to validate the manifest file (`ai-plugin.json`) against [this schema](https://github.com/minsa110/ai-plugin-schema/blob/main/ai-plugin-schema.json). -- `launch.json` file that helps to customize **Run and Debug**. +To run this application: -## 💻 Code for the FastAPI app -If you have [access](https://code.visualstudio.com/blogs/2023/03/30/vscode-copilot#_getting-started-today) to [GitHub Copilot](https://github.com/features/copilot), try it out to help you write code faster. To test the app, run `uvicorn main:app` in the integrated terminal, or press `F5`, and debug CRUD operations at .../docs. - -- `main.py` is the code for the API plugin. (✨ Tip: Generate the code using Copilot. The following is an example prompt to use in the Copilot [chat view](https://code.visualstudio.com/blogs/2023/03/30/vscode-copilot#_embracing-the-chat-view).) - ```markdown - Write a simple TODO app using FastAPI, that lets the user add TODOs, list their TODOs, list specific TODOs, and delete TODOs, ensuring that the app stores todo_id for each todo item. - - Assume that a docker container is running for Redis, running and accessible at host "redis" and port 6379 as specified in the docker-compose.yml file. Make use of the Redis container for persisting data from the TODO app. - - Include a __main__ section which will run this app using uvicorn. The Python module where I save this code will be called main.py. - - Mount static files in the .well-known directory to the path /.well-known, which should at minimum contain ai-plugin.json that serves (as JSON). - ``` -- `openapi.yaml` is a specification that dictates how to define the schema of the API. -- `ai-plugin.json` is a JSON manifest file that defines relevant metadata for the plugin. Learn more in the [OpenAI docs](https://platform.openai.com/docs/plugins/getting-started/plugin-manifest). - -## 💬 Register the app on ChatGPT -- Go to **PORT** and set visibility of port 8000 to `public` -- Copy the link and paste it on ChatGPT plugin +``` +uvicorn main:app +``` diff --git a/flush_db.py b/flush_db.py deleted file mode 100644 index 9b12b58..0000000 --- a/flush_db.py +++ /dev/null @@ -1,4 +0,0 @@ -import redis - -redis_client = redis.StrictRedis(host='0.0.0.0', port=6379, db=0, decode_responses=True) -redis_client.flushdb() \ No newline at end of file diff --git a/hostconfig.sh b/hostconfig.sh deleted file mode 100755 index 40542cb..0000000 --- a/hostconfig.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/bin/sh -set -eu - -# Check if CODESPACES environment variable is set to true -if [ "$CODESPACES" = "true" ]; then - # If CODESPACES is true and PLUGIN_HOSTNAME is undefined or empty, set PLUGIN_HOSTNAME - if [ -z "$PLUGIN_HOSTNAME" ]; then - # Check if CODESPACE_NAME and GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN are set - if [ -z "$CODESPACE_NAME" ] || [ -z "$GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN" ]; then - echo "CODESPACE_NAME and/or GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN environment variables are not set." - exit 1 - fi - # Set PLUGIN_HOSTNAME to the expanded version of the URL - PLUGIN_HOSTNAME="https://$CODESPACE_NAME-8000.$GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN" - fi -else - # If CODESPACES is not true, check if PLUGIN_HOSTNAME is set - if [ -z "$PLUGIN_HOSTNAME" ]; then - echo "PLUGIN_HOSTNAME environment variable is not set." - exit 1 - fi -fi - -# Input JSON file -json_input_file="./.well-known/ai-plugin.json" - -# Input YAML file -yaml_input_file="./.well-known/openapi.yaml" - -# Create temporary files to store the modified JSON and YAML -temp_json_file=$(mktemp) -temp_yaml_file=$(mktemp) - -# Read the JSON file and perform the substitutions using jq -jq --arg plugin_hostname "$PLUGIN_HOSTNAME" ' - .api.url = ($plugin_hostname + "/.well-known/openapi.yaml") | - .logo_url = ($plugin_hostname + "/.well-known/logo.png") -' "$json_input_file" > "$temp_json_file" - -# Find the line number where the "servers:" key is located in the YAML file -servers_line_number=$(grep -n "servers:" "$yaml_input_file" | cut -d: -f1) - -# Update the YAML file using sed and awk -awk -v line_number="$servers_line_number" -v plugin_hostname="$PLUGIN_HOSTNAME" ' - NR == line_number + 1 { - sub(/url: .*/, "url: " plugin_hostname) - } - { print } -' "$yaml_input_file" > "$temp_yaml_file" - -# Overwrite the original JSON file with the modified contents -mv "$temp_json_file" "$json_input_file" - -# Overwrite the original YAML file with the modified contents -mv "$temp_yaml_file" "$yaml_input_file" - -# Print success messages -echo "$json_input_file has been updated successfully." -echo "$yaml_input_file file has been updated successfully." diff --git a/main.py b/main.py index e5ca87f..09870c7 100644 --- a/main.py +++ b/main.py @@ -1,47 +1,48 @@ -from fastapi import FastAPI, HTTPException -from fastapi.staticfiles import StaticFiles -import os -import redis - -redis_client = redis.StrictRedis(host='0.0.0.0', port=6379, db=0, decode_responses=True) - -app = FastAPI() -app.mount("/.well-known", StaticFiles(directory=".well-known"), name="static") - -# Route to list all TODOs -@app.get("/todos") -def list_todos(): - todos = {} - for key in redis_client.keys(): - if key != 'todo_id': - todos[key] = "["+key+"] "+str(redis_client.get(key)) - return todos - -# Route to list a specific TODO -@app.get("/todos/{todo_id}") -def list_todo(todo_id: int): - todo = redis_client.get(str(todo_id)) - if todo: - return {"todo_id": todo_id, "todo": todo} - else: - raise HTTPException(status_code=404, detail="Todo not found") - -# Route to add a TODO -@app.post("/todos") -def add_todo(todo: str): - # Generate a unique todo_id - todo_id = redis_client.incr('todo_id') - redis_client.set(str(todo_id), todo) - return {"todo_id": todo_id, "todo": todo} - -# Route to delete a TODO -@app.delete("/todos/{todo_id}") -def delete_todo(todo_id: int): - if not redis_client.exists(str(todo_id)): - raise HTTPException(status_code=404, detail="Todo not found") - redis_client.delete(str(todo_id)) - return {"result": "Todo deleted"} - -if __name__ == "__main__": - import uvicorn - uvicorn.run("main:app", host="0.0.0.0", port=8000, log_level="info") +from fastapi import FastAPI, HTTPException +from fastapi.staticfiles import StaticFiles +import os +import redis + +redis_client = redis.StrictRedis(host='0.0.0.0', port=6379, db=0, decode_responses=True) + +app = FastAPI() +app.mount("/.well-known", StaticFiles(directory=".well-known"), name="static") + +# Route to list all TODOs +@app.get("/todos") +def list_todos(): + todos = {} + for key in redis_client.keys(): + if key != 'todo_id': + todos[key] = "["+key+"] "+str(redis_client.get(key)) + return todos + +# Route to list a specific TODO +@app.get("/todos/{todo_id}") +def list_todo(todo_id: int): + todo = redis_client.get(str(todo_id)) + if todo: + return {"todo_id": todo_id, "todo": todo} + else: + raise HTTPException(status_code=404, detail="Todo not found") + +# Route to add a TODO +@app.post("/todos") +def add_todo(todo: str): + # Generate a unique todo_id + todo_id = redis_client.incr('todo_id') + redis_client.set(str(todo_id), todo) + return {"todo_id": todo_id, "todo": todo} + +# Route to delete a TODO +@app.delete("/todos/{todo_id}") +def delete_todo(todo_id: int): + if not redis_client.exists(str(todo_id)): + raise HTTPException(status_code=404, detail="Todo not found") + redis_client.delete(str(todo_id)) + return {"result": "Todo deleted"} + +if __name__ == "__main__": + import uvicorn + uvicorn.run("main:app", host="0.0.0.0", port=8000, log_level="info") + \ No newline at end of file diff --git a/test_main.py b/test_main.py deleted file mode 100644 index b6a682d..0000000 --- a/test_main.py +++ /dev/null @@ -1,46 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from main import app - - -@pytest.fixture -def client(): - return TestClient(app) - - -def test_list_todos_empty(client): - response = client.get("/todos") - assert response.status_code == 200 - assert response.json() == {} - - -def test_list_todo_not_found(client): - response = client.get("/todos/1") - assert response.status_code == 404 - assert response.json() == {"detail": "Todo not found"} - - -def test_add_todo(client): - response = client.post("/todos", params={"todo": "Buy groceries"}) - assert response.status_code == 200 - assert response.json() == {"todo_id": 1, "todo": "Buy groceries"} - - -def test_list_todo(client): - client.post("/todos", params={"todo": "Buy groceries"}) - response = client.get("/todos/1") - assert response.status_code == 200 - assert response.json() == {"todo_id": 1, "todo": "Buy groceries"} - - -def test_delete_todo(client): - response = client.delete("/todos/1") - assert response.status_code == 200 - assert response.json() == {"result": "Todo deleted"} - - -def test_delete_todo_not_found(client): - response = client.delete("/todos/1") - assert response.status_code == 404 - assert response.json() == {"detail": "Todo not found"} \ No newline at end of file