diff --git a/.gitignore b/.gitignore index 5d381cc..f22ff58 100644 --- a/.gitignore +++ b/.gitignore @@ -160,3 +160,4 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ +.DS_Store \ No newline at end of file diff --git a/requirements.in b/requirements.in index e6d6efa..5395014 100644 --- a/requirements.in +++ b/requirements.in @@ -3,3 +3,8 @@ langchain-community openai fastapi pydantic-settings +s3fs +aiofiles +duckdb +polars +pyarrow \ No newline at end of file diff --git a/src/hellocomputer/config.py b/src/hellocomputer/config.py index 6e90838..51feb0c 100644 --- a/src/hellocomputer/config.py +++ b/src/hellocomputer/config.py @@ -5,6 +5,7 @@ class Settings(BaseSettings): anyscale_api_key: str = "Awesome API" gcs_access: str = "access" gcs_secret: str = "secret" + gcs_bucketname: str = "bucket" model_config = SettingsConfigDict(env_file=".env") diff --git a/src/hellocomputer/main.py b/src/hellocomputer/main.py index d6c4fda..fd54fc9 100644 --- a/src/hellocomputer/main.py +++ b/src/hellocomputer/main.py @@ -1,15 +1,12 @@ from pathlib import Path from fastapi import FastAPI, status -from fastapi.responses import PlainTextResponse from fastapi.staticfiles import StaticFiles -from langchain_community.chat_models import ChatAnyscale -from langchain_core.messages import HumanMessage, SystemMessage from pydantic import BaseModel import hellocomputer -from .config import settings +from .routers import files, sessions static_path = Path(hellocomputer.__file__).parent / "static" @@ -43,30 +40,8 @@ def get_health() -> HealthCheck: return HealthCheck(status="OK") -@app.get("/greetings", response_class=PlainTextResponse) -async def get_greeting() -> str: - model = "meta-llama/Meta-Llama-3-8B-Instruct" - chat = ChatAnyscale( - model_name=model, - temperature=0.5, - anyscale_api_key=settings.anyscale_api_key, - ) - - messages = [ - SystemMessage(content="You are a helpful AI that shares everything you know."), - HumanMessage( - content="Make a short presentation of yourself " - "as an assistant in Spanish in about 20 words. " - "You're capable of analyzing a file that a user " - "has previously uploaded." - ), - ] - - model_response = await chat.ainvoke(messages) - print(model_response.response_metadata) - return model_response.content - - +app.include_router(sessions.router) +app.include_router(files.router) app.mount( "/", StaticFiles(directory=static_path, html=True, packages=["bootstrap4"]), diff --git a/src/hellocomputer/models.py b/src/hellocomputer/models.py new file mode 100644 index 0000000..fb32d59 --- /dev/null +++ b/src/hellocomputer/models.py @@ -0,0 +1,52 @@ +from enum import StrEnum +from langchain_community.chat_models import ChatAnyscale +from langchain_core.messages import HumanMessage, SystemMessage + + +class AvailableModels(StrEnum): + llama3_8b = "meta-llama/Meta-Llama-3-8B-Instruct" + + +class Chat: + @staticmethod + def raise_no_key(api_key): + if api_key: + return api_key + elif api_key is None: + raise ValueError( + "You need to provide a valid API in the api_key init argument" + ) + else: + raise ValueError("You need to provide a valid API key") + + def __init__( + self, + model: AvailableModels = AvailableModels.llama3_8b, + api_key: str = "", + temperature: float = 0.5, + ): + self.model = model + self.api_key = self.raise_no_key(api_key) + self.messages = [] + self.responses = [] + + model: ChatAnyscale = ChatAnyscale( + model_name=model, temperature=temperature, anyscale_api_key=self.api_key + ) + + async def eval(self, system: str, human: str): + self.messages.append( + [ + SystemMessage(content=system), + HumanMessage(content=human), + ] + ) + + self.responses.append(await self.model.ainvoke(self.messages[-1])) + return self + + def last_response_content(self): + return self.responses[-1].content + + def last_response_metadata(self): + return self.responses[-1].response_metadata diff --git a/src/hellocomputer/routers/__init__.py b/src/hellocomputer/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/hellocomputer/routers/analysis.py b/src/hellocomputer/routers/analysis.py new file mode 100644 index 0000000..739ffae --- /dev/null +++ b/src/hellocomputer/routers/analysis.py @@ -0,0 +1,15 @@ +from fastapi import APIRouter +from fastapi.responses import PlainTextResponse + +from ..config import settings +from ..models import Chat + +router = APIRouter() + + +@router.get("/query", response_class=PlainTextResponse, tags=["queries"]) +async def query(sid: str = "") -> str: + model = Chat(api_key=settings.anyscale_api_key).eval( + system="You're an expert analyst", human="Do some analysis" + ) + return model.last_response_content() diff --git a/src/hellocomputer/routers/files.py b/src/hellocomputer/routers/files.py new file mode 100644 index 0000000..39999e7 --- /dev/null +++ b/src/hellocomputer/routers/files.py @@ -0,0 +1,84 @@ +import aiofiles +import duckdb +import polars as pl +import s3fs +from fastapi import APIRouter, File, UploadFile +from fastapi.responses import JSONResponse + +from ..config import settings + +router = APIRouter() + + +# Configure the S3FS with your Google Cloud Storage credentials +gcs = s3fs.S3FileSystem( + key=settings.gcs_access, + secret=settings.gcs_secret, + client_kwargs={"endpoint_url": "https://storage.googleapis.com"}, +) +bucket_name = settings.gcs_bucketname + + +@router.post("/upload", tags=["files"]) +async def upload_file(file: UploadFile = File(...), sid: str = ""): + async with aiofiles.tempfile.NamedTemporaryFile("wb") as f: + content = await file.read() + await f.write(content) + await f.flush() + + gcs.makedir(f"{settings.gcs_bucketname}/{sid}") + + db = duckdb.connect() + db.install_extension("spatial") + db.install_extension("httpfs") + db.load_extension("httpfs") + db.load_extension("spatial") + + db.sql(f""" + CREATE SECRET ( + TYPE GCS, + KEY_ID '{settings.gcs_access}', + SECRET '{settings.gcs_secret}') + """) + + db.sql(f""" + create table metadata as ( + select + * + from + st_read('{f.name}', + layer='metadata', + open_options=['HEADERS_FORCE', 'FIELD_TYPES=auto'] + ) + )""") + + metadata = db.query("select * from metadata").pl() + sheets = metadata.select(pl.col("Key") == "Sheets") + print(sheets) + + for sheet in sheets.to_dict(): + print(sheet) + + db.sql( + f""" + create table data as ( + select + * + from + st_read('{f.name}', + layer='data', + open_options=['HEADERS_FORCE', 'FIELD_TYPES=auto'] + ) + )""" + ) + + db.sql(f""" + copy + data + to + 'gcs://{settings.gcs_bucketname}/{sid}/data.csv'; + """) + + return JSONResponse( + content={"message": "File uploaded successfully"}, status_code=200 + ) diff --git a/src/hellocomputer/routers/sessions.py b/src/hellocomputer/routers/sessions.py new file mode 100644 index 0000000..0f41958 --- /dev/null +++ b/src/hellocomputer/routers/sessions.py @@ -0,0 +1,19 @@ +from uuid import uuid4 + +from fastapi import APIRouter +from fastapi.responses import PlainTextResponse + +router = APIRouter() + + +@router.get("/new_session") +async def get_new_session() -> str: + return str(uuid4()) + + +@router.get("/greetings", response_class=PlainTextResponse) +async def get_greeting() -> str: + return ( + "Hi! I'm a helpful assistant. Please upload or select a file " + "and I'll try to analyze it following your orders" + ) diff --git a/src/hellocomputer/static/.DS_Store b/src/hellocomputer/static/.DS_Store index 8f0779e..2238bb3 100644 Binary files a/src/hellocomputer/static/.DS_Store and b/src/hellocomputer/static/.DS_Store differ diff --git a/src/hellocomputer/static/index.html b/src/hellocomputer/static/index.html index b0a8d3c..8fbe1bd 100644 --- a/src/hellocomputer/static/index.html +++ b/src/hellocomputer/static/index.html @@ -51,6 +51,11 @@ + + @@ -89,9 +94,6 @@ - diff --git a/src/hellocomputer/static/script.js b/src/hellocomputer/static/script.js index 00082ce..9693814 100644 --- a/src/hellocomputer/static/script.js +++ b/src/hellocomputer/static/script.js @@ -46,11 +46,18 @@ async function fetchResponse(message) { function addAIMessage() { const newMessage = document.createElement('div'); newMessage.classList.add('message', 'bg-white', 'p-2', 'mb-2', 'rounded'); - newMessage.innerHTML = '
'; + newMessage.innerHTML = '
'; chatMessages.prepend(newMessage); // Add new message at the top fetchResponse(newMessage); } +function addAIManualMessage(m) { + const newMessage = document.createElement('div'); + newMessage.classList.add('message', 'bg-white', 'p-2', 'mb-2', 'rounded'); + newMessage.innerHTML = '
' + m + '
'; + chatMessages.prepend(newMessage); // Add new message at the top +} + function addUserMessage() { const messageContent = textarea.value.trim(); if (messageContent) { @@ -82,7 +89,11 @@ document.addEventListener("DOMContentLoaded", function () { async function fetchGreeting() { try { - const response = await fetch('/greetings'); + const session_response = await fetch('/new_session'); + sessionStorage.setItem("helloComputerSession", JSON.parse(await session_response.text())); + + const response = await fetch('/greetings?sid=' + sessionStorage.getItem('helloComputerSession')); + if (!response.ok) { throw new Error('Network response was not ok ' + response.statusText); } @@ -101,3 +112,39 @@ document.addEventListener("DOMContentLoaded", function () { // Call the function to fetch greeting fetchGreeting(); }); + +document.addEventListener("DOMContentLoaded", function () { + const fileInput = document.getElementById('inputGroupFile01'); + const uploadButton = document.getElementById('uploadButton'); + const uploadResultDiv = document.getElementById('uploadResultDiv'); + + uploadButton.addEventListener('click', async function () { + const file = fileInput.files[0]; + + if (!file) { + uploadResultDiv.textContent = 'Please select a file.'; + return; + } + + const formData = new FormData(); + formData.append('file', file); + + try { + const response = await fetch('/upload?sid=' + sessionStorage.getItem('helloComputerSession'), { + method: 'POST', + body: formData + }); + + if (!response.ok) { + throw new Error('Network response was not ok ' + response.statusText); + } + + const data = await response.text(); + uploadResultDiv.textContent = 'Upload successful: ' + JSON.parse(data)['message']; + + addAIManualMessage('File uploaded and processed!'); + } catch (error) { + uploadResultDiv.textContent = 'Error: ' + error.message; + } + }); +});