Skip to content

Commit

Permalink
Merge pull request #47 from radicalxdev/STAGING
Browse files Browse the repository at this point in the history
Update App YML for GAE Deployment
  • Loading branch information
mikhailocampo authored Jun 20, 2024
2 parents b121ba2 + 1600833 commit a9d0436
Show file tree
Hide file tree
Showing 27 changed files with 255 additions and 149 deletions.
Empty file added .gcloudignore
Empty file.
14 changes: 8 additions & 6 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,14 @@ jobs:
credentials_json: ${{ secrets.PRODUCTION_CREDENTIALS_JSON }}

# Dynamically Update app.yaml
- name: Update app.yaml
- name: Update app.yaml with env variables
run: |
echo "" >> app/app.yaml
echo "env_variables:" >> app/app.yaml
echo " ENV_TYPE: '${{ env.ENV_TYPE }}'" >> app/app.yaml
echo " PROJECT_ID: '${{ env.PROJECT_ID }}'" >> app/app.yaml
echo "" >> app.yaml
echo "env_variables:" >> app.yaml
echo " ENV_TYPE: '${{ env.ENV_TYPE }}'" >> app.yaml
echo " PROJECT_ID: '${{ env.PROJECT_ID }}'" >> app.yaml
echo " GOOGLE_API_KEY: '${{ secrets.GOOGLE_API_KEY }}'" >> app.yaml
echo " PYTHONPATH: app/" >> app.yaml
- name: "Set up Cloud SDK"
uses: "google-github-actions/setup-gcloud@v1"
Expand All @@ -42,5 +44,5 @@ jobs:
if: github.ref == 'refs/heads/main'
uses: "google-github-actions/[email protected]"
with:
deliverables: app/app.yaml
deliverables: app.yaml
version: v1
14 changes: 6 additions & 8 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
# backend/Dockerfile
FROM python:3.10.12

WORKDIR /app
WORKDIR /code

COPY app/ /app
COPY app/requirements.txt /code/requirements.txt

RUN pip install --no-cache-dir -r /app/requirements.txt
RUN pip install --no-cache-dir -r /code/requirements.txt

COPY ./app /app
COPY ./app /code/app

# Local development key set
# ENV TYPES: dev, production
# When set to dev, API Key on endpoint requests are just 'dev'
# When set to production, API Key on endpoint requests are the actual API Key

ENV GOOGLE_APPLICATION_CREDENTIALS=/app/local-auth.json
ENV ENV_TYPE="dev"
ENV PROJECT_ID="kai-ai-f63c8"
ENV PYTHONPATH=/code/app

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
CMD ["fastapi", "run", "app/main.py", "--port", "8000"]
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2024 Radical AI

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
27 changes: 8 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,25 +89,11 @@ pip install -r requirements.txt
1. Rename the downloaded JSON key to `local-auth.json`.
2. Move or copy this file to your application's directory, specifically inside the `/app` directory.

### Step 4: Set Environment Variables

1. Open your command line interface.
2. Set the path to the JSON key file by running:
```bash
set GOOGLE_APPLICATION_CREDENTIALS=/app/local-auth.json```
## Set the environment type and project ID:
```bash
set ENV_TYPE="dev"
set PROJECT_ID="Enter your project ID here"
```

```bash
uvicorn main:app --reload
```

### Step 4: Utilize Local Start Script

1. Modify the `local-start.sh` script's environment variable `PROJECT_ID` to match the project ID of your Google Cloud project.
2. Run the script: `./local-start.sh`
3. Navigate to `http://localhost:8000` to view the application.

# Docker Setup Guide

Expand Down Expand Up @@ -153,7 +139,10 @@ The Docker container uses several key environment variables:
`LANGCHAIN_ENDPOINT`
`LANGCHAIN_API_KEY`
`LANGCHAIN_PROJECT`
- Ensure these variables are correctly configured in your Dockerfile or passed as additional parameters to your Docker run command if needed.
- Ensure these variables are correctly configured in your Dockerfile or passed as additional parameters to your Docker run command, as shown in the example below:
```bash
docker run --env ENV_TYPE=dev --env="Enter your project ID here" -p 8000:8000 kai-backend:latest
```
## Accessing the Application
You can access the backend by visiting:
```Bash
Expand Down
6 changes: 6 additions & 0 deletions app.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
runtime: python310
entrypoint: fastapi run app/main.py --port $PORT
instance_class: F2
automatic_scaling:
min_instances: 1
max_instances: 3
2 changes: 2 additions & 0 deletions app/.env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ENV_TYPE=
GOOGLE_API_KEY=
21 changes: 0 additions & 21 deletions app/.gcloudignore

This file was deleted.

12 changes: 6 additions & 6 deletions app/api/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
from typing import Union
from services.schemas import ToolRequest, ChatRequest, Message, ChatResponse, ToolResponse
from utils.auth import key_check
from services.logger import setup_logger
from api.error_utilities import InputValidationError, ErrorResponse
from api.tool_utilities import load_tool_metadata, execute_tool, finalize_inputs
from app.services.schemas import ToolRequest, ChatRequest, Message, ChatResponse, ToolResponse
from app.utils.auth import key_check
from app.services.logger import setup_logger
from app.api.error_utilities import InputValidationError, ErrorResponse
from app.api.tool_utilities import load_tool_metadata, execute_tool, finalize_inputs

logger = setup_logger(__name__)
router = APIRouter()
Expand Down Expand Up @@ -46,7 +46,7 @@ async def submit_tool( data: ToolRequest, _ = Depends(key_check)):

@router.post("/chat", response_model=ChatResponse)
async def chat( request: ChatRequest, _ = Depends(key_check) ):
from features.Kaichat.core import executor as kaichat_executor
from app.features.Kaichat.core import executor as kaichat_executor

user_name = request.user.fullName
chat_messages = request.messages
Expand Down
4 changes: 2 additions & 2 deletions app/api/tests/test_tool_utility.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import json
import pytest
from unittest.mock import patch, MagicMock, mock_open
from services.tool_registry import BaseTool, ToolInput, ToolFile
from app.services.tool_registry import BaseTool, ToolInput, ToolFile
from fastapi import HTTPException
from api.tool_utilities import get_executor_by_name, load_tool_metadata, prepare_input_data, execute_tool
from app.api.tool_utilities import get_executor_by_name, load_tool_metadata, prepare_input_data, execute_tool

# Sample configuration for tools_config
tools_config = {
Expand Down
2 changes: 1 addition & 1 deletion app/api/tests/test_tools.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from fastapi.testclient import TestClient
from main import app
from services.tool_registry import validate_inputs
from app.services.tool_registry import validate_inputs
import pytest
import os

Expand Down
91 changes: 54 additions & 37 deletions app/api/tool_utilities.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import json
import os
from services.logger import setup_logger
from services.tool_registry import ToolFile
from api.error_utilities import VideoTranscriptError, InputValidationError, ToolExecutorError
from app.services.logger import setup_logger
from app.services.tool_registry import ToolFile
from app.api.error_utilities import VideoTranscriptError, InputValidationError, ToolExecutorError
from typing import Dict, Any, List
from fastapi import HTTPException
from pydantic import ValidationError
Expand Down Expand Up @@ -32,9 +32,16 @@ def load_tool_metadata(tool_id):
logger.error(f"No tool configuration found for tool_id: {tool_id}")
raise HTTPException(status_code=404, detail="Tool configuration not found")

# The path to the module needs to be split and only the directory path should be used.
module_dir_path = '/'.join(tool_config['path'].split('.')[:-1]) # This removes the last segment (core)
file_path = os.path.join(os.getcwd(), module_dir_path, tool_config['metadata_file'])
# Ensure the base path is relative to the current file's directory
base_dir = os.path.dirname(os.path.abspath(__file__))
logger.debug(f"Base directory: {base_dir}")

# Construct the directory path
module_dir_path = os.path.join(base_dir, '..', *tool_config['path'].split('.')[:-1]) # Go one level up and then to the path
module_dir_path = os.path.abspath(module_dir_path) # Get absolute path
logger.debug(f"Module directory path: {module_dir_path}")

file_path = os.path.join(module_dir_path, tool_config['metadata_file'])
logger.debug(f"Checking metadata file at: {file_path}")

if not os.path.exists(file_path) or os.path.getsize(file_path) == 0:
Expand All @@ -51,47 +58,57 @@ def prepare_input_data(input_data) -> Dict[str, Any]:
inputs = {input.name: input.value for input in input_data}
return inputs

def validate_inputs(request_data: Dict[str, Any], validate_data: List[Dict[str, str]]) -> bool:
validate_inputs = {input_item['name']: input_item['type'] for input_item in validate_data}

# Check for missing inputs
for validate_input_name, input_type in validate_inputs.items():
def check_missing_inputs(request_data: Dict[str, Any], validate_inputs: Dict[str, str]):
for validate_input_name in validate_inputs:
if validate_input_name not in request_data:
error_message = f"Missing input: `{validate_input_name}`"
logger.error(error_message)
raise InputValidationError(error_message)

# Validate each input in request data against validate definitions
for input_name, input_value in request_data.items():
if input_name not in validate_inputs:
continue # Skip validation for extra inputs not defined in validate
def raise_type_error(input_name: str, input_value: Any, expected_type: str):
error_message = f"Input `{input_name}` must be a {expected_type} but got {type(input_value)}"
logger.error(error_message)
raise InputValidationError(error_message)

expected_type = validate_inputs[input_name]
if expected_type == 'text' and not isinstance(input_value, str):
error_message = f"Input `{input_name}` must be a string but got {type(input_value)}"
def validate_file_input(input_name: str, input_value: Any):
if not isinstance(input_value, list):
error_message = f"Input `{input_name}` must be a list of file dictionaries but got {type(input_value)}"
logger.error(error_message)
raise InputValidationError(error_message)

for file_obj in input_value:
if not isinstance(file_obj, dict):
error_message = f"Each item in the input `{input_name}` must be a dictionary representing a file but got {type(file_obj)}"
logger.error(error_message)
raise InputValidationError(error_message)
elif expected_type == 'number' and not isinstance(input_value, (int, float)):
error_message = f"Input `{input_name}` must be a number but got {type(input_value)}"
try:
ToolFile.model_validate(file_obj, from_attributes=True) # This will raise a validation error if the structure is incorrect
except ValidationError:
error_message = f"Each item in the input `{input_name}` must be a valid ToolFile where a URL is provided"
logger.error(error_message)
raise InputValidationError(error_message)
elif expected_type == 'file':
# Validate file inputs
if not isinstance(input_value, list):
error_message = f"Input `{input_name}` must be a list of file dictionaries but got {type(input_value)}"
logger.error(error_message)
raise InputValidationError(error_message)
for file_obj in input_value:
if not isinstance(file_obj, dict):
error_message = f"Each item in the input `{input_name}` must be a dictionary representing a file but got {type(file_obj)}"
logger.error(error_message)
raise InputValidationError(error_message)
try:
ToolFile.model_validate(file_obj, from_attributes=True) # This will raise a validation error if the structure is incorrect
except ValidationError:
error_message = f"Each item in the input `{input_name}` must be a valid ToolFile where a url is provided"
logger.error(error_message)
raise InputValidationError(error_message)

def validate_input_type(input_name: str, input_value: Any, expected_type: str):
if expected_type == 'text' and not isinstance(input_value, str):
raise_type_error(input_name, input_value, "string")
elif expected_type == 'number' and not isinstance(input_value, (int, float)):
raise_type_error(input_name, input_value, "number")
elif expected_type == 'file':
validate_file_input(input_name, input_value)

def validate_inputs(request_data: Dict[str, Any], validate_data: List[Dict[str, str]]) -> bool:
validate_inputs = {input_item['name']: input_item['type'] for input_item in validate_data}

# Check for missing inputs
check_missing_inputs(request_data, validate_inputs)

# Validate each input in request data against validate definitions
for input_name, input_value in request_data.items():
if input_name not in validate_inputs:
continue # Skip validation for extra inputs not defined in validate_inputs

expected_type = validate_inputs[input_name]
validate_input_type(input_name, input_value, expected_type)

return True

Expand Down
6 changes: 0 additions & 6 deletions app/app.yaml

This file was deleted.

6 changes: 3 additions & 3 deletions app/features/Kaichat/core.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from langchain_google_vertexai import VertexAI
from langchain_google_genai import GoogleGenerativeAI
from langchain.prompts import PromptTemplate
from services.schemas import ChatMessage, Message
from app.services.schemas import ChatMessage, Message
import os

def read_text_file(file_path):
Expand Down Expand Up @@ -40,7 +40,7 @@ def executor(user_name: str, user_query: str, messages: list[Message], k=10):

prompt = build_prompt()

llm = VertexAI(model_name="gemini-1.0-pro")
llm = GoogleGenerativeAI(model="gemini-1.0-pro")

chain = prompt | llm

Expand Down
6 changes: 3 additions & 3 deletions app/features/dynamo/core.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from features.dynamo.tools import summarize_transcript, generate_flashcards
from services.logger import setup_logger
from api.error_utilities import VideoTranscriptError
from app.features.dynamo.tools import summarize_transcript, generate_flashcards
from app.services.logger import setup_logger
from app.api.error_utilities import VideoTranscriptError

logger = setup_logger(__name__)

Expand Down
7 changes: 6 additions & 1 deletion app/features/dynamo/metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,10 @@
"name": "youtube_url",
"type": "string"
}
]
],
"models": {
"Gemini 1.0": "gemini-1.0-pro",
"Gemini 1.5 Flash": "gemini-1.5-flash",
"Gemini 1.5 Pro": "gemini-1.5-pro"
}
}
3 changes: 3 additions & 0 deletions app/features/dynamo/prompt/summarize-prompt.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
You are a video summarizing AI who only summarizes transcript in a concise, readable, and informative format. You can analyze large amounts of text to isolate the key concepts and ideas from the transcript while ignoring tangents. Consider the following video transcript and respond with paragraphs which highlight the core ideas represented. Do not include any headers, markdown, or other page content other than plaintext of the summary.

{full_transcript}
Loading

0 comments on commit a9d0436

Please sign in to comment.