Compare commits

...

4 commits

Author SHA1 Message Date
Guillem Borrell d642bb0a39 Add about
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2024-06-14 21:58:14 +02:00
Guillem Borrell 62c54dc3e6 List sessions 2024-06-12 22:22:26 +02:00
Guillem Borrell 833a446899 Save session when file is uploaded 2024-06-12 12:45:09 +02:00
Guillem Borrell a994eacd2d Get available sessions 2024-06-12 11:04:28 +02:00
13 changed files with 164 additions and 33 deletions

View file

@ -2,7 +2,7 @@ import json
from pathlib import Path from pathlib import Path
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.responses import HTMLResponse, RedirectResponse, FileResponse from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from starlette.middleware.sessions import SessionMiddleware from starlette.middleware.sessions import SessionMiddleware
from starlette.requests import Request from starlette.requests import Request

View file

@ -1,9 +1,9 @@
from fastapi import APIRouter from fastapi import APIRouter
from fastapi.responses import PlainTextResponse from fastapi.responses import PlainTextResponse
from hellocomputer.sessions import SessionDB
from hellocomputer.db import StorageEngines from hellocomputer.db import StorageEngines
from hellocomputer.extraction import extract_code_block from hellocomputer.extraction import extract_code_block
from hellocomputer.sessions import SessionDB
from ..config import settings from ..config import settings
from ..models import Chat from ..models import Chat

View file

@ -2,9 +2,10 @@ from authlib.integrations.starlette_client import OAuth, OAuthError
from fastapi import APIRouter from fastapi import APIRouter
from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.responses import HTMLResponse, RedirectResponse
from starlette.requests import Request from starlette.requests import Request
from hellocomputer.config import settings from hellocomputer.config import settings
from hellocomputer.users import UserDB
from hellocomputer.db import StorageEngines from hellocomputer.db import StorageEngines
from hellocomputer.users import UserDB
router = APIRouter() router = APIRouter()

View file

@ -3,10 +3,12 @@ import aiofiles
# import s3fs # import s3fs
from fastapi import APIRouter, File, UploadFile from fastapi import APIRouter, File, UploadFile
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from starlette.requests import Request
from ..sessions import SessionDB
from ..config import settings from ..config import settings
from ..db import StorageEngines from ..db import StorageEngines
from ..sessions import SessionDB
from ..users import OwnershipDB
router = APIRouter() router = APIRouter()
@ -21,7 +23,7 @@ router = APIRouter()
@router.post("/upload", tags=["files"]) @router.post("/upload", tags=["files"])
async def upload_file(file: UploadFile = File(...), sid: str = ""): async def upload_file(request: Request, file: UploadFile = File(...), sid: str = ""):
async with aiofiles.tempfile.NamedTemporaryFile("wb") as f: async with aiofiles.tempfile.NamedTemporaryFile("wb") as f:
content = await file.read() content = await file.read()
await f.write(content) await f.write(content)
@ -39,6 +41,13 @@ async def upload_file(file: UploadFile = File(...), sid: str = ""):
.dump() .dump()
) )
OwnershipDB(
StorageEngines.gcs,
gcs_access=settings.gcs_access,
gcs_secret=settings.gcs_secret,
bucket=settings.gcs_bucketname,
).set_ownersip(request.session.get("user").get("email"), sid)
return JSONResponse( return JSONResponse(
content={"message": "File uploaded successfully"}, status_code=200 content={"message": "File uploaded successfully"}, status_code=200
) )

View file

@ -3,8 +3,11 @@ from uuid import uuid4
from fastapi import APIRouter from fastapi import APIRouter
from fastapi.responses import PlainTextResponse from fastapi.responses import PlainTextResponse
from starlette.requests import Request from starlette.requests import Request
from hellocomputer.users import OwnershipDB from typing import List
from hellocomputer.db import StorageEngines from hellocomputer.db import StorageEngines
from hellocomputer.users import OwnershipDB
from ..config import settings from ..config import settings
# Scheme for the Authorization header # Scheme for the Authorization header
@ -13,17 +16,8 @@ router = APIRouter()
@router.get("/new_session") @router.get("/new_session")
async def get_new_session(request: Request) -> str: async def get_new_session() -> str:
user_email = request.session.get("user").get("email") return str(uuid4())
ownership = OwnershipDB(
StorageEngines.gcs,
gcs_access=settings.gcs_access,
gcs_secret=settings.gcs_secret,
bucket=settings.gcs_bucketname,
)
sid = str(uuid4())
return ownership.set_ownersip(user_email, sid)
@router.get("/greetings", response_class=PlainTextResponse) @router.get("/greetings", response_class=PlainTextResponse)
@ -32,3 +26,15 @@ async def get_greeting() -> str:
"Hi! I'm a helpful assistant. Please upload or select a file " "Hi! I'm a helpful assistant. Please upload or select a file "
"and I'll try to analyze it following your orders" "and I'll try to analyze it following your orders"
) )
@router.get("/sessions")
async def get_sessions(request: Request) -> List[str]:
user_email = request.session.get("user").get("email")
ownership = OwnershipDB(
StorageEngines.gcs,
gcs_access=settings.gcs_access,
gcs_secret=settings.gcs_secret,
bucket=settings.gcs_bucketname,
)
return ownership.sessions(user_email)

View file

@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hola, computer!</title>
<link rel="icon" type="image/x-icon" href="/app/img/favicon.ico">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&display=swap" rel="stylesheet">
<style>
.login-container {
max-width: 400px;
margin: 0 auto;
padding: 50px 0;
}
.logo {
display: block;
margin: 0 auto 20px auto;
}
.techie-font {
font-family: 'Share Tech Mono', monospace;
font-size: 24px;
text-align: center;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="container">
<div class="login-container text-center">
<h1 class="h3 mb-3 fw-normal techie-font">Hola, computer!</h1>
<img src="/app/img/assistant.webp" alt="Logo" class="logo img-fluid">
<p class="techie-font">
Hola, computer! is a web assistant that allows you to query excel files using natural language. It may
not be as powerful as Excel, but it has an efficient query backend that can process your data faster
and more efficiently than Excel.
</p>
<a href="/"><button class="btn btn-secondary w-100">Back</button></a>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View file

@ -63,9 +63,8 @@
<label for="datasetLabel" class="form-label">Sesson name</label> <label for="datasetLabel" class="form-label">Sesson name</label>
<input type="text" class="form-control" id="datasetLabel" <input type="text" class="form-control" id="datasetLabel"
aria-describedby="labelHelp"> aria-describedby="labelHelp">
<div id="labelHelp" class="form-text">You'll be able to recover this file in the <div id="labelHelp" class="form-text">
future You'll be able to recover this file in the future with this name
with this name
</div> </div>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@ -86,8 +85,9 @@
</div> </div>
<!-- Button trigger modal --> <!-- Button trigger modal -->
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#staticBackdrop"> <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#staticBackdrop"
Or load an existing session id="loadSessionsButton">
Load a session
</button> </button>
<!-- Modal --> <!-- Modal -->
@ -96,15 +96,17 @@
<div class="modal-dialog modal-dialog-scrollable"> <div class="modal-dialog modal-dialog-scrollable">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="staticBackdropLabel">Current sessions</h5> <h5 class="modal-title" id="staticBackdropLabel">Available sessions</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" <button type="button" class="btn-close" data-bs-dismiss="modal"
aria-label="Close"></button> aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body" id="userSessions">
<p>Current sessions</p> <ul id="userSessions">
</ul>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"
id="sessionCloseButton">Close</button>
</div> </div>
</div> </div>
</div> </div>

View file

@ -35,6 +35,8 @@
<h1 class="h3 mb-3 fw-normal techie-font">Hola, computer!</h1> <h1 class="h3 mb-3 fw-normal techie-font">Hola, computer!</h1>
<img src="/app/img/assistant.webp" alt="Logo" class="logo img-fluid"> <img src="/app/img/assistant.webp" alt="Logo" class="logo img-fluid">
<a href="/login"><button type="submit" class="btn btn-primary w-100">Login</button></a> <a href="/login"><button type="submit" class="btn btn-primary w-100">Login</button></a>
<p></p>
<a href="/app/about.html"><button class="btn btn-secondary w-100">About</button></a>
</div> </div>
</div> </div>

View file

@ -13,6 +13,7 @@ $('#menu-toggle').click(function (e) {
toggleMenuArrow(document.getElementById('menu-toggle')); toggleMenuArrow(document.getElementById('menu-toggle'));
}); });
// Hide sidebar on mobile devices
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
console.log('Width: ' + window.innerWidth + ' Height: ' + window.innerHeight); console.log('Width: ' + window.innerWidth + ' Height: ' + window.innerHeight);
if ((window.innerWidth <= 800) && (window.innerHeight <= 600)) { if ((window.innerWidth <= 800) && (window.innerHeight <= 600)) {
@ -27,6 +28,7 @@ const textarea = document.getElementById('chatTextarea');
const sendButton = document.getElementById('sendButton'); const sendButton = document.getElementById('sendButton');
const chatMessages = document.querySelector('.chat-messages'); const chatMessages = document.querySelector('.chat-messages');
// Auto resize textarea
textarea.addEventListener('input', function () { textarea.addEventListener('input', function () {
this.style.height = 'auto'; this.style.height = 'auto';
this.style.height = (this.scrollHeight <= 150 ? this.scrollHeight : 150) + 'px'; this.style.height = (this.scrollHeight <= 150 ? this.scrollHeight : 150) + 'px';
@ -53,6 +55,7 @@ async function fetchResponse(message, newMessage) {
} }
} }
// Function to add AI message
function addAIMessage(messageContent) { function addAIMessage(messageContent) {
const newMessage = document.createElement('div'); const newMessage = document.createElement('div');
newMessage.classList.add('message', 'bg-white', 'p-2', 'mb-2', 'rounded'); newMessage.classList.add('message', 'bg-white', 'p-2', 'mb-2', 'rounded');
@ -123,6 +126,7 @@ document.addEventListener("DOMContentLoaded", function () {
fetchGreeting(); fetchGreeting();
}); });
// Function upload the data file
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
const fileInput = document.getElementById('inputGroupFile01'); const fileInput = document.getElementById('inputGroupFile01');
const uploadButton = document.getElementById('uploadButton'); const uploadButton = document.getElementById('uploadButton');
@ -158,3 +162,32 @@ document.addEventListener("DOMContentLoaded", function () {
} }
}); });
}); });
// Function to get the user sessions
document.addEventListener("DOMContentLoaded", function () {
const sessionsButton = document.getElementById('loadSessionsButton');
const sessions = document.getElementById('userSessions');
sessionsButton.addEventListener('click', async function fetchSessions() {
try {
const response = await fetch('/sessions');
if (!response.ok) {
throw new Error('Network response was not ok ' + response.statusText);
}
const data = JSON.parse(await response.text());
sessions.innerHTML = '';
data.forEach(item => {
const listItem = document.createElement('li');
const button = document.createElement('button');
button.textContent = item;
button.addEventListener("click", function () { alert(`You clicked on ${item}`); });
listItem.appendChild(button);
sessions.appendChild(listItem);
});
} catch (error) {
sessions.innerHTML = 'Error: ' + error.message;
}
}
);
}
);

View file

@ -1,11 +1,12 @@
import json import json
import os import os
from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import List
from uuid import UUID, uuid4 from uuid import UUID, uuid4
import duckdb import duckdb
import polars as pl import polars as pl
from datetime import datetime
from .db import DDB, StorageEngines from .db import DDB, StorageEngines
@ -79,7 +80,7 @@ class OwnershipDB(DDB):
'{sid}' as sid, '{sid}' as sid,
'{now}' as timestamp '{now}' as timestamp
) )
TO '{self.path_prefix}/{record_id}.csv' (FORMAT JSON)""" TO '{self.path_prefix}/{record_id}.csv'"""
try: try:
self.db.sql(query) self.db.sql(query)
@ -88,3 +89,25 @@ class OwnershipDB(DDB):
self.db.sql(query) self.db.sql(query)
return sid return sid
def sessions(self, user_email: str) -> List[str]:
try:
return (
self.db.sql(f"""
SELECT
sid
FROM
'{self.path_prefix}/*.csv'
WHERE
email = '{user_email}
ORDER BY
timestamp ASC
LIMIT 10'
""")
.pl()
.to_series()
.to_list()
)
# If the table does not exist
except duckdb.duckdb.IOException:
return []

View file

@ -1,8 +1,8 @@
from pathlib import Path from pathlib import Path
import hellocomputer import hellocomputer
from hellocomputer.sessions import SessionDB
from hellocomputer.db import StorageEngines from hellocomputer.db import StorageEngines
from hellocomputer.sessions import SessionDB
TEST_STORAGE = StorageEngines.local TEST_STORAGE = StorageEngines.local
TEST_XLS_PATH = ( TEST_XLS_PATH = (

View file

@ -2,11 +2,11 @@ from pathlib import Path
import hellocomputer import hellocomputer
import pytest import pytest
from hellocomputer.sessions import SessionDB
from hellocomputer.config import settings from hellocomputer.config import settings
from hellocomputer.db import StorageEngines from hellocomputer.db import StorageEngines
from hellocomputer.extraction import extract_code_block from hellocomputer.extraction import extract_code_block
from hellocomputer.models import Chat from hellocomputer.models import Chat
from hellocomputer.sessions import SessionDB
TEST_XLS_PATH = ( TEST_XLS_PATH = (
Path(hellocomputer.__file__).parents[2] Path(hellocomputer.__file__).parents[2]

View file

@ -2,7 +2,7 @@ from pathlib import Path
import hellocomputer import hellocomputer
from hellocomputer.db import StorageEngines from hellocomputer.db import StorageEngines
from hellocomputer.users import UserDB, OwnershipDB from hellocomputer.users import OwnershipDB, UserDB
TEST_STORAGE = StorageEngines.local TEST_STORAGE = StorageEngines.local
TEST_OUTPUT_FOLDER = Path(hellocomputer.__file__).parents[2] / "test" / "output" TEST_OUTPUT_FOLDER = Path(hellocomputer.__file__).parents[2] / "test" / "output"
@ -28,7 +28,13 @@ def test_user_exists():
def test_assign_owner(): def test_assign_owner():
assert ( assert (
OwnershipDB(storage_engine=TEST_STORAGE, path=TEST_OUTPUT_FOLDER).set_ownersip( OwnershipDB(storage_engine=TEST_STORAGE, path=TEST_OUTPUT_FOLDER).set_ownersip(
"something.something@something", "1234", "test" "something.something@something", "testsession", "test"
) )
== "1234" == "testsession"
) )
def test_get_sessions():
assert OwnershipDB(storage_engine=TEST_STORAGE, path=TEST_OUTPUT_FOLDER).sessions(
"something.something@something"
) == ["testsession"]