This commit is contained in:
Matteo Rosati
2026-02-18 15:39:01 +01:00
parent d4e9643afc
commit b64e97c9d0
12 changed files with 507 additions and 360 deletions

1
.gitignore vendored
View File

@@ -6,3 +6,4 @@ credentials.json
.zed .zed
.DS_STORE .DS_STORE
*.sqlite3 *.sqlite3
*.db

21
app.py
View File

@@ -5,6 +5,7 @@ from fastapi.templating import Jinja2Templates
from fastapi import Request from fastapi import Request
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from db import DB
from chain import RagChain from chain import RagChain
from pprint import pprint from pprint import pprint
@@ -16,7 +17,18 @@ app.mount("/static", StaticFiles(directory="static"), name="static")
@app.get("/") @app.get("/")
def home(request: Request): def home(request: Request):
return templates.TemplateResponse("index.html", {"request": request}) db = DB()
prompts = db.get_prompts()
return templates.TemplateResponse("index.html", {"request": request, "prompts": prompts})
@app.get("/prova")
def prova(request: Request):
cursor = DB()
prompts = cursor.get_prompts()
return prompts
@app.websocket("/ws") @app.websocket("/ws")
@@ -40,8 +52,10 @@ async def websocket_endpoint(websocket: WebSocket):
top_k=int(config.get("top_k", 40)), top_k=int(config.get("top_k", 40)),
top_p=float(config.get("top_p", 0.0)), top_p=float(config.get("top_p", 0.0)),
temperature=float(config.get("temperature", 0.0)), temperature=float(config.get("temperature", 0.0)),
retriever_max_docs=int(config.get("retriever_max_docs", 40)), retriever_max_docs=int(
reranker_max_results=int(config.get("reranker_max_results", 20)), config.get("retriever_max_docs", 40)),
reranker_max_results=int(
config.get("reranker_max_results", 20)),
) )
async for chunk in rag_chain.stream(message): async for chunk in rag_chain.stream(message):
@@ -52,6 +66,7 @@ async def websocket_endpoint(websocket: WebSocket):
"type": "report", "type": "report",
"sources": rag_chain.getSources(), "sources": rag_chain.getSources(),
"reranked_sources": rag_chain.getRankedSources(), "reranked_sources": rag_chain.getRankedSources(),
"rephrased_question": rag_chain.getRephrasedQuestion(),
} }
) )
finally: finally:

View File

@@ -45,6 +45,7 @@ class RagChain:
self._retriever_sources: list[dict] = [] self._retriever_sources: list[dict] = []
self._reranked_sources: list[dict] = [] self._reranked_sources: list[dict] = []
self._rephrased_question: str = ""
self._llm = ChatGoogleGenerativeAI( self._llm = ChatGoogleGenerativeAI(
model=MODEL, model=MODEL,
@@ -80,7 +81,7 @@ class RagChain:
| question_rewrite_prompt | question_rewrite_prompt
| self._llm | self._llm
| StrOutputParser() | StrOutputParser()
| RunnableLambda(self._log_rewritten_question) | RunnableLambda(self._log_rephrased_question)
) )
rag_chain = ( rag_chain = (
@@ -95,8 +96,9 @@ class RagChain:
self._full_chain = question_rewrite_chain | rag_chain self._full_chain = question_rewrite_chain | rag_chain
def _log_rewritten_question(self, rewritten_question: str) -> str: def _log_rephrased_question(self, rephrased_question: str) -> str:
return rewritten_question self._rephrased_question = rephrased_question
return rephrased_question
def _format_docs(self, question: str) -> str: def _format_docs(self, question: str) -> str:
retrieved_docs = self._base_retriever.invoke(question) retrieved_docs = self._base_retriever.invoke(question)
@@ -150,5 +152,8 @@ class RagChain:
def getRankedSources(self) -> list[dict]: def getRankedSources(self) -> list[dict]:
return list(self._reranked_sources) return list(self._reranked_sources)
def getRephrasedQuestion(self) -> str:
return self._rephrased_question
def stream(self, message: str): def stream(self, message: str):
return self._full_chain.astream(message) return self._full_chain.astream(message)

15
db.py Normal file
View File

@@ -0,0 +1,15 @@
from typing import List
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from models.orm import Prompt
class DB:
def __init__(self, db: str = "sqlite:///example.db"):
self.engine = create_engine(
db, connect_args={"check_same_thread": False})
def get_prompts(self) -> List[Prompt]:
with Session(self.engine) as session:
return session.query(Prompt).all()

17
models/orm.py Normal file
View File

@@ -0,0 +1,17 @@
from sqlalchemy import create_engine, Column, String, Text, Integer
from sqlalchemy.orm import declarative_base
Base = declarative_base()
class Prompt(Base):
__tablename__ = "prompts"
id = Column(Integer, primary_key=True)
name = Column(String, nullable=False)
text = Column(Text, nullable=False)
if __name__ == "__main__":
engine = create_engine("sqlite:///example.db")
Base.metadata.create_all(engine)

6
models/validation.py Normal file
View File

@@ -0,0 +1,6 @@
from pydantic import BaseModel
class Prompt(BaseModel):
name: str
text: str

View File

@@ -16,6 +16,8 @@ dependencies = [
"python-dotenv>=1.2.1", "python-dotenv>=1.2.1",
"fastapi>=0.129.0", "fastapi>=0.129.0",
"fastapi[standard]", "fastapi[standard]",
"pydantic>=2.12.5",
"sqlalchemy>=2.0.46",
] ]
[dependency-groups] [dependency-groups]

View File

@@ -75,7 +75,6 @@ mdurl==0.1.2
multidict==6.7.1 multidict==6.7.1
mypy-extensions==1.1.0 mypy-extensions==1.1.0
nodeenv==1.10.0 nodeenv==1.10.0
nuitka==4.0.1
numexpr==2.14.1 numexpr==2.14.1
numpy==2.4.2 numpy==2.4.2
openai==2.21.0 openai==2.21.0

View File

@@ -1,274 +1,272 @@
:root { :root {
color-scheme: light; color-scheme: light;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
sans-serif;
} }
* { * {
box-sizing: border-box; box-sizing: border-box;
} }
html, html,
body { body {
height: 100%; height: 100%;
} }
body { body {
margin: 0; margin: 0;
padding: 0; padding: 0;
background: #f7f8fa; background: #f7f8fa;
color: #1f2937; color: #1f2937;
} }
.app { .app {
display: flex; display: flex;
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
overflow: hidden; overflow: hidden;
} }
.chat { .chat {
flex: 1; flex: 1;
height: 100%; height: 100%;
margin: 0; margin: 0;
background: #ffffff; background: #ffffff;
border: 1px solid #e5e7eb; border: 1px solid #e5e7eb;
border-radius: 0; border-radius: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.chat__header { .chat__header {
padding: 16px 20px; padding: 16px 20px;
border-bottom: 1px solid #e5e7eb; border-bottom: 1px solid #e5e7eb;
font-weight: 600; font-weight: 600;
background: #f9fafb; background: #f9fafb;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 12px; gap: 12px;
} }
.chat__settings { .chat__settings {
padding: 6px 12px; padding: 6px 12px;
border-radius: 999px; border-radius: 999px;
border: 1px solid #d1d5db; border: 1px solid #d1d5db;
background: #ffffff; background: #ffffff;
color: #1f2937; color: #1f2937;
font-weight: 600; font-weight: 600;
font-size: 13px; font-size: 13px;
cursor: pointer; cursor: pointer;
} }
.chat__settings:hover { .chat__settings:hover {
background: #f3f4f6; background: #f3f4f6;
} }
.chat__messages { .chat__messages {
flex: 1; flex: 1;
padding: 16px 20px; padding: 16px 20px;
overflow-y: auto; overflow-y: auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
} }
.message { .message {
max-width: 70%; max-width: 70%;
padding: 10px 12px; padding: 10px 12px;
border-radius: 12px; border-radius: 12px;
line-height: 1.4; line-height: 1.4;
font-size: 14px; font-size: 14px;
word-wrap: break-word; word-wrap: break-word;
white-space: pre-wrap; white-space: pre-wrap;
} }
.message--out { .message--out {
align-self: flex-end; align-self: flex-end;
background: #2563eb; background: #2563eb;
color: #ffffff; color: #ffffff;
} }
.message--in { .message--in {
align-self: flex-start; align-self: flex-start;
background: #f3f4f6; background: #f3f4f6;
color: #111827; color: #111827;
} }
/* Typing / loading indicator */ /* Typing / loading indicator */
.message--loading { .message--loading {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 5px; gap: 5px;
padding: 12px 16px; padding: 12px 16px;
} }
.typing-dot { .typing-dot {
width: 8px; width: 8px;
height: 8px; height: 8px;
border-radius: 50%; border-radius: 50%;
background: #9ca3af; background: #9ca3af;
animation: typing-bounce 1.2s infinite ease-in-out; animation: typing-bounce 1.2s infinite ease-in-out;
} }
.typing-dot:nth-child(1) { .typing-dot:nth-child(1) {
animation-delay: 0s; animation-delay: 0s;
} }
.typing-dot:nth-child(2) { .typing-dot:nth-child(2) {
animation-delay: 0.2s; animation-delay: 0.2s;
} }
.typing-dot:nth-child(3) { .typing-dot:nth-child(3) {
animation-delay: 0.4s; animation-delay: 0.4s;
} }
@keyframes typing-bounce { @keyframes typing-bounce {
0%,
60%,
100% {
transform: translateY(0);
background: #9ca3af;
}
0%, 30% {
60%, transform: translateY(-6px);
100% { background: #6b7280;
transform: translateY(0); }
background: #9ca3af;
}
30% {
transform: translateY(-6px);
background: #6b7280;
}
} }
.chat__footer { .chat__footer {
padding: 16px 20px; padding: 16px 20px;
border-top: 1px solid #e5e7eb; border-top: 1px solid #e5e7eb;
display: flex; display: flex;
gap: 12px; gap: 12px;
background: #f9fafb; background: #f9fafb;
} }
.chat__input { .chat__input {
flex: 1; flex: 1;
padding: 10px 12px; padding: 10px 12px;
border-radius: 8px; border-radius: 8px;
border: 1px solid #d1d5db; border: 1px solid #d1d5db;
background: #ffffff; background: #ffffff;
color: #111827; color: #111827;
} }
.chat__input::placeholder { .chat__input::placeholder {
color: #9ca3af; color: #9ca3af;
} }
.chat__button { .chat__button {
padding: 10px 16px; padding: 10px 16px;
border-radius: 8px; border-radius: 8px;
border: none; border: none;
background: #2563eb; background: #2563eb;
color: #ffffff; color: #ffffff;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
} }
.chat__button:disabled { .chat__button:disabled {
background: #cbd5f5; background: #cbd5f5;
color: #ffffff; color: #ffffff;
cursor: not-allowed; cursor: not-allowed;
opacity: 0.75; opacity: 0.75;
} }
.chat__button--secondary { .chat__button--secondary {
background: #e5e7eb; background: #e5e7eb;
color: #1f2937; color: #1f2937;
} }
.chat__button--secondary:hover { .chat__button--secondary:hover {
background: #d1d5db; background: #d1d5db;
} }
.chat__status { .chat__status {
padding: 8px 20px 0; padding: 8px 20px 0;
font-size: 12px; font-size: 12px;
color: #6b7280; color: #6b7280;
} }
.drawer { .drawer {
width: 320px; width: 320px;
max-width: 100%; max-width: 100%;
height: 100%; height: 100%;
border-left: 1px solid #e5e7eb; border-left: 1px solid #e5e7eb;
background: #ffffff; background: #ffffff;
padding: 20px; padding: 20px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: 16px;
transform: translateX(100%); transform: translateX(100%);
transition: transform 0.25s ease; transition: transform 0.25s ease;
} }
.drawer--open { .drawer--open {
transform: translateX(0%); transform: translateX(0%);
} }
.drawer__header { .drawer__header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 12px; gap: 12px;
} }
.drawer__title { .drawer__title {
margin: 0; margin: 0;
font-size: 18px; font-size: 18px;
} }
.drawer__close { .drawer__close {
border: 1px solid #d1d5db; border: 1px solid #d1d5db;
background: #ffffff; background: #ffffff;
border-radius: 8px; border-radius: 8px;
padding: 4px 8px; padding: 4px 8px;
cursor: pointer; cursor: pointer;
} }
.drawer__form { .drawer__form {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: 16px;
} }
.drawer__field { .drawer__field {
display: grid; display: grid;
grid-template-columns: 1fr auto; grid-template-columns: 1fr auto;
grid-template-rows: auto auto; grid-template-rows: auto auto;
gap: 6px 12px; gap: 6px 12px;
align-items: center; align-items: center;
font-size: 13px; font-size: 13px;
color: #1f2937; color: #1f2937;
} }
.drawer__label { .drawer__label {
font-weight: 600; font-weight: 600;
} }
.drawer__range { .drawer__range {
grid-column: 1 / -1; grid-column: 1 / -1;
width: 100%; width: 100%;
} }
.drawer__value { .drawer__value {
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
}
.drawer__hint {
margin: 0;
font-size: 12px;
color: #6b7280;
} }
@media (max-width: 900px) { @media (max-width: 900px) {
.drawer { .drawer {
position: absolute; position: absolute;
right: 0; right: 0;
top: 0; top: 0;
height: 100vh; height: 100vh;
box-shadow: -12px 0 24px rgba(15, 23, 42, 0.08); box-shadow: -12px 0 24px rgba(15, 23, 42, 0.08);
} }
} }

View File

@@ -8,14 +8,14 @@ const settingsToggle = document.getElementById("settings-toggle");
const settingsDrawer = document.getElementById("settings-drawer"); const settingsDrawer = document.getElementById("settings-drawer");
const settingsClose = document.getElementById("settings-close"); const settingsClose = document.getElementById("settings-close");
const rangeInputs = settingsDrawer const rangeInputs = settingsDrawer
? Array.from(settingsDrawer.querySelectorAll("input[type='range']")) ? Array.from(settingsDrawer.querySelectorAll("input[type='range']"))
: []; : [];
const configInputs = { const configInputs = {
topK: document.getElementById("config-top-k"), topK: document.getElementById("config-top-k"),
topP: document.getElementById("config-top-p"), topP: document.getElementById("config-top-p"),
temperature: document.getElementById("config-temperature"), temperature: document.getElementById("config-temperature"),
retrieverMaxDocs: document.getElementById("config-retriever-max-docs"), retrieverMaxDocs: document.getElementById("config-retriever-max-docs"),
rerankerMaxDocs: document.getElementById("config-reranker-max-docs"), rerankerMaxDocs: document.getElementById("config-reranker-max-docs"),
}; };
const scheme = window.location.protocol === "https:" ? "wss" : "ws"; const scheme = window.location.protocol === "https:" ? "wss" : "ws";
@@ -26,178 +26,189 @@ let loadingBubble = null;
let isAwaitingResponse = false; let isAwaitingResponse = false;
const updateSendButtonState = () => { const updateSendButtonState = () => {
if (!sendButtonEl) return; if (!sendButtonEl) return;
sendButtonEl.disabled = isAwaitingResponse; sendButtonEl.disabled = isAwaitingResponse;
}; };
const addMessage = (text, direction) => { const addMessage = (text, direction) => {
const bubble = document.createElement("div"); const bubble = document.createElement("div");
bubble.className = `message message--${direction}`; bubble.className = `message message--${direction}`;
bubble.textContent = text; bubble.textContent = text;
messagesEl.appendChild(bubble); messagesEl.appendChild(bubble);
messagesEl.scrollTop = messagesEl.scrollHeight; messagesEl.scrollTop = messagesEl.scrollHeight;
}; };
const showLoadingBubble = () => { const showLoadingBubble = () => {
if (loadingBubble) return; if (loadingBubble) return;
loadingBubble = document.createElement("div"); loadingBubble = document.createElement("div");
loadingBubble.className = "message message--in message--loading"; loadingBubble.className = "message message--in message--loading";
loadingBubble.innerHTML = loadingBubble.innerHTML =
'<span class="typing-dot"></span><span class="typing-dot"></span><span class="typing-dot"></span>'; '<span class="typing-dot"></span><span class="typing-dot"></span><span class="typing-dot"></span>';
messagesEl.appendChild(loadingBubble); messagesEl.appendChild(loadingBubble);
messagesEl.scrollTop = messagesEl.scrollHeight; messagesEl.scrollTop = messagesEl.scrollHeight;
}; };
const removeLoadingBubble = () => { const removeLoadingBubble = () => {
if (!loadingBubble) return; if (!loadingBubble) return;
loadingBubble.remove(); loadingBubble.remove();
loadingBubble = null; loadingBubble = null;
}; };
const clearMessages = () => { const clearMessages = () => {
if (!messagesEl) return; if (!messagesEl) return;
messagesEl.innerHTML = ""; messagesEl.innerHTML = "";
streamingBubble = null; streamingBubble = null;
loadingBubble = null; loadingBubble = null;
}; };
const connect = () => { const connect = () => {
socket = new WebSocket(socketUrl); socket = new WebSocket(socketUrl);
socket.addEventListener("open", () => { socket.addEventListener("open", () => {
statusEl.textContent = "Connected"; statusEl.textContent = "Connected";
}); });
socket.addEventListener("message", (event) => { socket.addEventListener("message", (event) => {
let payload; let payload;
try { try {
payload = JSON.parse(event.data); payload = JSON.parse(event.data);
} catch (error) { } catch (error) {
addMessage(event.data, "in"); addMessage(event.data, "in");
return; return;
} }
if (payload.type === "start") { if (payload.type === "start") {
// Keep the loading bubble visible until the first chunk arrives. // Keep the loading bubble visible until the first chunk arrives.
// Just prepare the streaming bubble but don't append it yet. // Just prepare the streaming bubble but don't append it yet.
streamingBubble = document.createElement("div"); streamingBubble = document.createElement("div");
streamingBubble.className = "message message--in"; streamingBubble.className = "message message--in";
streamingBubble.textContent = ""; streamingBubble.textContent = "";
return; return;
} }
if (payload.type === "chunk") { if (payload.type === "chunk") {
if (!streamingBubble) { if (!streamingBubble) {
streamingBubble = document.createElement("div"); streamingBubble = document.createElement("div");
streamingBubble.className = "message message--in"; streamingBubble.className = "message message--in";
} }
// First chunk: swap loading bubble for the streaming bubble. // First chunk: swap loading bubble for the streaming bubble.
if (!streamingBubble.isConnected) { if (!streamingBubble.isConnected) {
removeLoadingBubble();
messagesEl.appendChild(streamingBubble);
}
streamingBubble.textContent += payload.data ?? "";
messagesEl.scrollTop = messagesEl.scrollHeight;
return;
}
if (payload.type === "report") {
console.log("%cSOURCES", 'border: 2px solid red; padding: 1em; color: red; font-size: 14px; font-weight: bold;')
console.log(payload.sources);
console.log("%cRE-RANKED SOURCES", 'border: 2px solid red; padding: 1em; color: red; font-size: 14px; font-weight: bold;')
console.log(payload.reranked_sources);
return;
}
if (payload.type === "end") {
// Safety net: remove loading bubble if no chunks were ever received.
removeLoadingBubble();
streamingBubble = null;
isAwaitingResponse = false;
updateSendButtonState();
return;
}
});
socket.addEventListener("close", () => {
statusEl.textContent = "Disconnected";
removeLoadingBubble(); removeLoadingBubble();
isAwaitingResponse = false; messagesEl.appendChild(streamingBubble);
updateSendButtonState(); }
}); streamingBubble.textContent += payload.data ?? "";
messagesEl.scrollTop = messagesEl.scrollHeight;
return;
}
socket.addEventListener("error", () => { if (payload.type === "report") {
statusEl.textContent = "Connection error"; console.log(
removeLoadingBubble(); "%cREPHRASED QUESTION",
isAwaitingResponse = false; "border: 2px solid red; padding: 1em; color: red; font-size: 14px; font-weight: bold;",
updateSendButtonState(); );
}); console.log(payload.rephrased_question);
console.log(
"%cSOURCES",
"border: 2px solid red; padding: 1em; color: red; font-size: 14px; font-weight: bold;",
);
console.log(payload.sources);
console.log(
"%cRE-RANKED SOURCES",
"border: 2px solid red; padding: 1em; color: red; font-size: 14px; font-weight: bold;",
);
console.log(payload.reranked_sources);
return;
}
if (payload.type === "end") {
// Safety net: remove loading bubble if no chunks were ever received.
removeLoadingBubble();
streamingBubble = null;
isAwaitingResponse = false;
updateSendButtonState();
return;
}
});
socket.addEventListener("close", () => {
statusEl.textContent = "Disconnected";
removeLoadingBubble();
isAwaitingResponse = false;
updateSendButtonState();
});
socket.addEventListener("error", () => {
statusEl.textContent = "Connection error";
removeLoadingBubble();
isAwaitingResponse = false;
updateSendButtonState();
});
}; };
const updateDrawerValue = (input) => { const updateDrawerValue = (input) => {
const output = input.parentElement?.querySelector(".drawer__value"); const output = input.parentElement?.querySelector(".drawer__value");
if (!output) return; if (!output) return;
output.textContent = input.value; output.textContent = input.value;
}; };
rangeInputs.forEach((input) => { rangeInputs.forEach((input) => {
updateDrawerValue(input); updateDrawerValue(input);
input.addEventListener("input", () => updateDrawerValue(input)); input.addEventListener("input", () => updateDrawerValue(input));
}); });
const closeDrawer = () => { const closeDrawer = () => {
settingsDrawer?.classList.remove("drawer--open"); settingsDrawer?.classList.remove("drawer--open");
if (settingsDrawer) settingsDrawer.setAttribute("aria-hidden", "true"); if (settingsDrawer) settingsDrawer.setAttribute("aria-hidden", "true");
if (settingsToggle) settingsToggle.setAttribute("aria-expanded", "false"); if (settingsToggle) settingsToggle.setAttribute("aria-expanded", "false");
}; };
const openDrawer = () => { const openDrawer = () => {
settingsDrawer?.classList.add("drawer--open"); settingsDrawer?.classList.add("drawer--open");
if (settingsDrawer) settingsDrawer.setAttribute("aria-hidden", "false"); if (settingsDrawer) settingsDrawer.setAttribute("aria-hidden", "false");
if (settingsToggle) settingsToggle.setAttribute("aria-expanded", "true"); if (settingsToggle) settingsToggle.setAttribute("aria-expanded", "true");
}; };
settingsToggle?.addEventListener("click", () => { settingsToggle?.addEventListener("click", () => {
if (settingsDrawer?.classList.contains("drawer--open")) { if (settingsDrawer?.classList.contains("drawer--open")) {
closeDrawer(); closeDrawer();
} else { } else {
openDrawer(); openDrawer();
} }
}); });
settingsClose?.addEventListener("click", closeDrawer); settingsClose?.addEventListener("click", closeDrawer);
clearEl?.addEventListener("click", () => { clearEl?.addEventListener("click", () => {
clearMessages(); clearMessages();
inputEl?.focus(); inputEl?.focus();
updateSendButtonState(); updateSendButtonState();
}); });
inputEl?.addEventListener("input", updateSendButtonState); inputEl?.addEventListener("input", updateSendButtonState);
formEl.addEventListener("submit", (event) => { formEl.addEventListener("submit", (event) => {
event.preventDefault(); event.preventDefault();
const text = inputEl.value.trim(); const text = inputEl.value.trim();
if (!text) return; if (!text) return;
if (!socket || socket.readyState !== WebSocket.OPEN) { if (!socket || socket.readyState !== WebSocket.OPEN) {
addMessage("Not connected.", "in"); addMessage("Not connected.", "in");
return; return;
} }
if (isAwaitingResponse) return; if (isAwaitingResponse) return;
const config = { const config = {
top_k: Number(configInputs.topK?.value ?? 0), top_k: Number(configInputs.topK?.value ?? 0),
top_p: Number(configInputs.topP?.value ?? 0), top_p: Number(configInputs.topP?.value ?? 0),
temperature: Number(configInputs.temperature?.value ?? 0), temperature: Number(configInputs.temperature?.value ?? 0),
retriever_max_docs: Number(configInputs.retrieverMaxDocs?.value ?? 0), retriever_max_docs: Number(configInputs.retrieverMaxDocs?.value ?? 0),
reranker_max_results: Number(configInputs.rerankerMaxDocs?.value ?? 0), reranker_max_results: Number(configInputs.rerankerMaxDocs?.value ?? 0),
}; };
addMessage(text, "out"); addMessage(text, "out");
socket.send(JSON.stringify({ message: text, config })); socket.send(JSON.stringify({ message: text, config }));
inputEl.value = ""; inputEl.value = "";
inputEl.focus(); inputEl.focus();
isAwaitingResponse = true; isAwaitingResponse = true;
updateSendButtonState(); updateSendButtonState();
showLoadingBubble(); showLoadingBubble();
}); });
connect(); connect();

View File

@@ -1,72 +1,143 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head>
<head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AKERN Assistant</title> <title>AKERN Assistant</title>
<link rel="stylesheet" href="/static/css/chat.css" /> <link rel="stylesheet" href="/static/css/chat.css" />
</head> </head>
<body> <body>
<div class="app" aria-label="Chat application"> <div class="app" aria-label="Chat application">
<section class="chat" aria-label="Chat"> <section class="chat" aria-label="Chat">
<div class="chat__header"> <div class="chat__header">
<span>AKERN Assistant</span> <span>AKERN Assistant</span>
<button class="chat__settings" id="settings-toggle" type="button" aria-haspopup="dialog" <button
aria-expanded="false" aria-controls="settings-drawer">Configurazione</button> class="chat__settings"
</div> id="settings-toggle"
<div class="chat__messages" id="messages"></div> type="button"
<div class="chat__status" id="status">Connecting…</div> aria-haspopup="dialog"
<form class="chat__footer" id="chat-form"> aria-expanded="false"
<input class="chat__input" id="chat-input" type="text" placeholder="Type a message" autocomplete="off" aria-controls="settings-drawer"
required /> >
<button class="chat__button" type="submit">Send</button> Configurazione
<button class="chat__button chat__button--secondary" id="chat-clear" type="button">Clear</button> </button>
</form> </div>
</section> <div class="chat__messages" id="messages"></div>
<div class="chat__status" id="status">Connecting…</div>
<form class="chat__footer" id="chat-form">
<input
class="chat__input"
id="chat-input"
type="text"
placeholder="Type a message"
autocomplete="off"
required
/>
<button class="chat__button" type="submit">Send</button>
<button
class="chat__button chat__button--secondary"
id="chat-clear"
type="button"
>
Clear
</button>
</form>
</section>
<aside class="drawer" id="settings-drawer" aria-label="Configurazione" aria-hidden="true"> <aside
<div class="drawer__header"> class="drawer"
<h2 class="drawer__title">Configurazione</h2> id="settings-drawer"
<button class="drawer__close" id="settings-close" type="button" aria-label="Chiudi"></button> aria-label="Configurazione"
</div> aria-hidden="true"
<form class="drawer__form"> >
<label class="drawer__field"> <div class="drawer__header">
<span class="drawer__label">top_k</span> <h2 class="drawer__title">Configurazione</h2>
<input class="drawer__range" id="config-top-k" type="range" min="0" max="100" step="1" value="40" /> <button
<output class="drawer__value">40</output> class="drawer__close"
</label> id="settings-close"
<label class="drawer__field"> type="button"
<span class="drawer__label">top_p</span> aria-label="Chiudi"
<input class="drawer__range" id="config-top-p" type="range" min="0" max="1" step="0.1" >
value="0.0" />
<output class="drawer__value">0.0</output> </button>
</label> </div>
<label class="drawer__field"> <form class="drawer__form">
<span class="drawer__label">temperature</span> <label class="drawer__field">
<input class="drawer__range" id="config-temperature" type="range" min="0" max="1.5" step="0.1" <span class="drawer__label">top_k</span>
value="0.0" /> <input
<output class="drawer__value">0.0</output> class="drawer__range"
</label> id="config-top-k"
<label class="drawer__field"> type="range"
<span class="drawer__label">retriever max docs</span> min="0"
<input class="drawer__range" id="config-retriever-max-docs" type="range" min="5" max="100" step="1" max="100"
value="40" /> step="1"
<output class="drawer__value">40</output> value="40"
</label> />
<label class="drawer__field"> <output class="drawer__value">40</output>
<span class="drawer__label">reranker max docs</span> </label>
<input class="drawer__range" id="config-reranker-max-docs" type="range" min="5" max="100" step="1" <label class="drawer__field">
value="20" /> <span class="drawer__label">top_p</span>
<output class="drawer__value">20</output> <input
</label> class="drawer__range"
<p class="drawer__hint">Solo frontend, nessuna logica applicata.</p> id="config-top-p"
</form> type="range"
</aside> min="0"
max="1"
step="0.1"
value="0.0"
/>
<output class="drawer__value">0.0</output>
</label>
<label class="drawer__field">
<span class="drawer__label">temperature</span>
<input
class="drawer__range"
id="config-temperature"
type="range"
min="0"
max="1.5"
step="0.1"
value="0.0"
/>
<output class="drawer__value">0.0</output>
</label>
<label class="drawer__field">
<span class="drawer__label">retriever max docs</span>
<input
class="drawer__range"
id="config-retriever-max-docs"
type="range"
min="5"
max="100"
step="1"
value="40"
/>
<output class="drawer__value">40</output>
</label>
<label class="drawer__field">
<span class="drawer__label">reranker max docs</span>
<input
class="drawer__range"
id="config-reranker-max-docs"
type="range"
min="5"
max="100"
step="1"
value="20"
/>
<output class="drawer__value">20</output>
</label>
<label for="drawer__field">
<span class="drawer__label">Prompts</span>
<!-- TODO -->
<!-- Here i need a select -->
<!-- {% for prompt in prompts %} {{ prompt.name }} {{ prompt.id }} {% endfor %} -->
</label>
</form>
</aside>
</div> </div>
<script src="/static/js/chat.js" defer></script> <script src="/static/js/chat.js" defer></script>
</body> </body>
</html> </html>

7
uv.lock generated
View File

@@ -109,7 +109,9 @@ dependencies = [
{ name = "langchain-google-genai" }, { name = "langchain-google-genai" },
{ name = "langchain-google-vertexai" }, { name = "langchain-google-vertexai" },
{ name = "langchain-openai" }, { name = "langchain-openai" },
{ name = "pydantic" },
{ name = "python-dotenv" }, { name = "python-dotenv" },
{ name = "sqlalchemy" },
] ]
[package.dev-dependencies] [package.dev-dependencies]
@@ -129,7 +131,9 @@ requires-dist = [
{ name = "langchain-google-genai", specifier = ">=4.2.0" }, { name = "langchain-google-genai", specifier = ">=4.2.0" },
{ name = "langchain-google-vertexai", specifier = ">=3.2.0" }, { name = "langchain-google-vertexai", specifier = ">=3.2.0" },
{ name = "langchain-openai", specifier = ">=1.1.9" }, { name = "langchain-openai", specifier = ">=1.1.9" },
{ name = "pydantic", specifier = ">=2.12.5" },
{ name = "python-dotenv", specifier = ">=1.2.1" }, { name = "python-dotenv", specifier = ">=1.2.1" },
{ name = "sqlalchemy", specifier = ">=2.0.46" },
] ]
[package.metadata.requires-dev] [package.metadata.requires-dev]
@@ -920,6 +924,7 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/ab/d26750f2b7242c2b90ea2ad71de70cfcd73a948a49513188a0fc0d6fc15a/greenlet-3.3.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:7ab327905cabb0622adca5971e488064e35115430cec2c35a50fd36e72a315b3", size = 275205, upload-time = "2026-01-23T15:30:24.556Z" }, { url = "https://files.pythonhosted.org/packages/ec/ab/d26750f2b7242c2b90ea2ad71de70cfcd73a948a49513188a0fc0d6fc15a/greenlet-3.3.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:7ab327905cabb0622adca5971e488064e35115430cec2c35a50fd36e72a315b3", size = 275205, upload-time = "2026-01-23T15:30:24.556Z" },
{ url = "https://files.pythonhosted.org/packages/10/d3/be7d19e8fad7c5a78eeefb2d896a08cd4643e1e90c605c4be3b46264998f/greenlet-3.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65be2f026ca6a176f88fb935ee23c18333ccea97048076aef4db1ef5bc0713ac", size = 599284, upload-time = "2026-01-23T16:00:58.584Z" }, { url = "https://files.pythonhosted.org/packages/10/d3/be7d19e8fad7c5a78eeefb2d896a08cd4643e1e90c605c4be3b46264998f/greenlet-3.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65be2f026ca6a176f88fb935ee23c18333ccea97048076aef4db1ef5bc0713ac", size = 599284, upload-time = "2026-01-23T16:00:58.584Z" },
{ url = "https://files.pythonhosted.org/packages/ae/21/fe703aaa056fdb0f17e5afd4b5c80195bbdab701208918938bd15b00d39b/greenlet-3.3.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a3ae05b3d225b4155bda56b072ceb09d05e974bc74be6c3fc15463cf69f33fd", size = 610274, upload-time = "2026-01-23T16:05:29.312Z" }, { url = "https://files.pythonhosted.org/packages/ae/21/fe703aaa056fdb0f17e5afd4b5c80195bbdab701208918938bd15b00d39b/greenlet-3.3.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a3ae05b3d225b4155bda56b072ceb09d05e974bc74be6c3fc15463cf69f33fd", size = 610274, upload-time = "2026-01-23T16:05:29.312Z" },
{ url = "https://files.pythonhosted.org/packages/06/00/95df0b6a935103c0452dad2203f5be8377e551b8466a29650c4c5a5af6cc/greenlet-3.3.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:12184c61e5d64268a160226fb4818af4df02cfead8379d7f8b99a56c3a54ff3e", size = 624375, upload-time = "2026-01-23T16:15:55.915Z" },
{ url = "https://files.pythonhosted.org/packages/cb/86/5c6ab23bb3c28c21ed6bebad006515cfe08b04613eb105ca0041fecca852/greenlet-3.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6423481193bbbe871313de5fd06a082f2649e7ce6e08015d2a76c1e9186ca5b3", size = 612904, upload-time = "2026-01-23T15:32:52.317Z" }, { url = "https://files.pythonhosted.org/packages/cb/86/5c6ab23bb3c28c21ed6bebad006515cfe08b04613eb105ca0041fecca852/greenlet-3.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6423481193bbbe871313de5fd06a082f2649e7ce6e08015d2a76c1e9186ca5b3", size = 612904, upload-time = "2026-01-23T15:32:52.317Z" },
{ url = "https://files.pythonhosted.org/packages/c2/f3/7949994264e22639e40718c2daf6f6df5169bf48fb038c008a489ec53a50/greenlet-3.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:33a956fe78bbbda82bfc95e128d61129b32d66bcf0a20a1f0c08aa4839ffa951", size = 1567316, upload-time = "2026-01-23T16:04:23.316Z" }, { url = "https://files.pythonhosted.org/packages/c2/f3/7949994264e22639e40718c2daf6f6df5169bf48fb038c008a489ec53a50/greenlet-3.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:33a956fe78bbbda82bfc95e128d61129b32d66bcf0a20a1f0c08aa4839ffa951", size = 1567316, upload-time = "2026-01-23T16:04:23.316Z" },
{ url = "https://files.pythonhosted.org/packages/8d/6e/d73c94d13b6465e9f7cd6231c68abde838bb22408596c05d9059830b7872/greenlet-3.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b065d3284be43728dd280f6f9a13990b56470b81be20375a207cdc814a983f2", size = 1636549, upload-time = "2026-01-23T15:33:48.643Z" }, { url = "https://files.pythonhosted.org/packages/8d/6e/d73c94d13b6465e9f7cd6231c68abde838bb22408596c05d9059830b7872/greenlet-3.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b065d3284be43728dd280f6f9a13990b56470b81be20375a207cdc814a983f2", size = 1636549, upload-time = "2026-01-23T15:33:48.643Z" },
@@ -928,6 +933,7 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ae/fb/011c7c717213182caf78084a9bea51c8590b0afda98001f69d9f853a495b/greenlet-3.3.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:bd59acd8529b372775cd0fcbc5f420ae20681c5b045ce25bd453ed8455ab99b5", size = 275737, upload-time = "2026-01-23T15:32:16.889Z" }, { url = "https://files.pythonhosted.org/packages/ae/fb/011c7c717213182caf78084a9bea51c8590b0afda98001f69d9f853a495b/greenlet-3.3.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:bd59acd8529b372775cd0fcbc5f420ae20681c5b045ce25bd453ed8455ab99b5", size = 275737, upload-time = "2026-01-23T15:32:16.889Z" },
{ url = "https://files.pythonhosted.org/packages/41/2e/a3a417d620363fdbb08a48b1dd582956a46a61bf8fd27ee8164f9dfe87c2/greenlet-3.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b31c05dd84ef6871dd47120386aed35323c944d86c3d91a17c4b8d23df62f15b", size = 646422, upload-time = "2026-01-23T16:01:00.354Z" }, { url = "https://files.pythonhosted.org/packages/41/2e/a3a417d620363fdbb08a48b1dd582956a46a61bf8fd27ee8164f9dfe87c2/greenlet-3.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b31c05dd84ef6871dd47120386aed35323c944d86c3d91a17c4b8d23df62f15b", size = 646422, upload-time = "2026-01-23T16:01:00.354Z" },
{ url = "https://files.pythonhosted.org/packages/b4/09/c6c4a0db47defafd2d6bab8ddfe47ad19963b4e30f5bed84d75328059f8c/greenlet-3.3.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02925a0bfffc41e542c70aa14c7eda3593e4d7e274bfcccca1827e6c0875902e", size = 658219, upload-time = "2026-01-23T16:05:30.956Z" }, { url = "https://files.pythonhosted.org/packages/b4/09/c6c4a0db47defafd2d6bab8ddfe47ad19963b4e30f5bed84d75328059f8c/greenlet-3.3.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02925a0bfffc41e542c70aa14c7eda3593e4d7e274bfcccca1827e6c0875902e", size = 658219, upload-time = "2026-01-23T16:05:30.956Z" },
{ url = "https://files.pythonhosted.org/packages/e2/89/b95f2ddcc5f3c2bc09c8ee8d77be312df7f9e7175703ab780f2014a0e781/greenlet-3.3.1-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3e0f3878ca3a3ff63ab4ea478585942b53df66ddde327b59ecb191b19dbbd62d", size = 671455, upload-time = "2026-01-23T16:15:57.232Z" },
{ url = "https://files.pythonhosted.org/packages/80/38/9d42d60dffb04b45f03dbab9430898352dba277758640751dc5cc316c521/greenlet-3.3.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34a729e2e4e4ffe9ae2408d5ecaf12f944853f40ad724929b7585bca808a9d6f", size = 660237, upload-time = "2026-01-23T15:32:53.967Z" }, { url = "https://files.pythonhosted.org/packages/80/38/9d42d60dffb04b45f03dbab9430898352dba277758640751dc5cc316c521/greenlet-3.3.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34a729e2e4e4ffe9ae2408d5ecaf12f944853f40ad724929b7585bca808a9d6f", size = 660237, upload-time = "2026-01-23T15:32:53.967Z" },
{ url = "https://files.pythonhosted.org/packages/96/61/373c30b7197f9e756e4c81ae90a8d55dc3598c17673f91f4d31c3c689c3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aec9ab04e82918e623415947921dea15851b152b822661cce3f8e4393c3df683", size = 1615261, upload-time = "2026-01-23T16:04:25.066Z" }, { url = "https://files.pythonhosted.org/packages/96/61/373c30b7197f9e756e4c81ae90a8d55dc3598c17673f91f4d31c3c689c3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aec9ab04e82918e623415947921dea15851b152b822661cce3f8e4393c3df683", size = 1615261, upload-time = "2026-01-23T16:04:25.066Z" },
{ url = "https://files.pythonhosted.org/packages/fd/d3/ca534310343f5945316f9451e953dcd89b36fe7a19de652a1dc5a0eeef3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71c767cf281a80d02b6c1bdc41c9468e1f5a494fb11bc8688c360524e273d7b1", size = 1683719, upload-time = "2026-01-23T15:33:50.61Z" }, { url = "https://files.pythonhosted.org/packages/fd/d3/ca534310343f5945316f9451e953dcd89b36fe7a19de652a1dc5a0eeef3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71c767cf281a80d02b6c1bdc41c9468e1f5a494fb11bc8688c360524e273d7b1", size = 1683719, upload-time = "2026-01-23T15:33:50.61Z" },
@@ -936,6 +942,7 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/28/24/cbbec49bacdcc9ec652a81d3efef7b59f326697e7edf6ed775a5e08e54c2/greenlet-3.3.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:3e63252943c921b90abb035ebe9de832c436401d9c45f262d80e2d06cc659242", size = 282706, upload-time = "2026-01-23T15:33:05.525Z" }, { url = "https://files.pythonhosted.org/packages/28/24/cbbec49bacdcc9ec652a81d3efef7b59f326697e7edf6ed775a5e08e54c2/greenlet-3.3.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:3e63252943c921b90abb035ebe9de832c436401d9c45f262d80e2d06cc659242", size = 282706, upload-time = "2026-01-23T15:33:05.525Z" },
{ url = "https://files.pythonhosted.org/packages/86/2e/4f2b9323c144c4fe8842a4e0d92121465485c3c2c5b9e9b30a52e80f523f/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76e39058e68eb125de10c92524573924e827927df5d3891fbc97bd55764a8774", size = 651209, upload-time = "2026-01-23T16:01:01.517Z" }, { url = "https://files.pythonhosted.org/packages/86/2e/4f2b9323c144c4fe8842a4e0d92121465485c3c2c5b9e9b30a52e80f523f/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76e39058e68eb125de10c92524573924e827927df5d3891fbc97bd55764a8774", size = 651209, upload-time = "2026-01-23T16:01:01.517Z" },
{ url = "https://files.pythonhosted.org/packages/d9/87/50ca60e515f5bb55a2fbc5f0c9b5b156de7d2fc51a0a69abc9d23914a237/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9f9d5e7a9310b7a2f416dd13d2e3fd8b42d803968ea580b7c0f322ccb389b97", size = 654300, upload-time = "2026-01-23T16:05:32.199Z" }, { url = "https://files.pythonhosted.org/packages/d9/87/50ca60e515f5bb55a2fbc5f0c9b5b156de7d2fc51a0a69abc9d23914a237/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9f9d5e7a9310b7a2f416dd13d2e3fd8b42d803968ea580b7c0f322ccb389b97", size = 654300, upload-time = "2026-01-23T16:05:32.199Z" },
{ url = "https://files.pythonhosted.org/packages/7c/25/c51a63f3f463171e09cb586eb64db0861eb06667ab01a7968371a24c4f3b/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b9721549a95db96689458a1e0ae32412ca18776ed004463df3a9299c1b257ab", size = 662574, upload-time = "2026-01-23T16:15:58.364Z" },
{ url = "https://files.pythonhosted.org/packages/1d/94/74310866dfa2b73dd08659a3d18762f83985ad3281901ba0ee9a815194fb/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92497c78adf3ac703b57f1e3813c2d874f27f71a178f9ea5887855da413cd6d2", size = 653842, upload-time = "2026-01-23T15:32:55.671Z" }, { url = "https://files.pythonhosted.org/packages/1d/94/74310866dfa2b73dd08659a3d18762f83985ad3281901ba0ee9a815194fb/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92497c78adf3ac703b57f1e3813c2d874f27f71a178f9ea5887855da413cd6d2", size = 653842, upload-time = "2026-01-23T15:32:55.671Z" },
{ url = "https://files.pythonhosted.org/packages/97/43/8bf0ffa3d498eeee4c58c212a3905dd6146c01c8dc0b0a046481ca29b18c/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ed6b402bc74d6557a705e197d47f9063733091ed6357b3de33619d8a8d93ac53", size = 1614917, upload-time = "2026-01-23T16:04:26.276Z" }, { url = "https://files.pythonhosted.org/packages/97/43/8bf0ffa3d498eeee4c58c212a3905dd6146c01c8dc0b0a046481ca29b18c/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ed6b402bc74d6557a705e197d47f9063733091ed6357b3de33619d8a8d93ac53", size = 1614917, upload-time = "2026-01-23T16:04:26.276Z" },
{ url = "https://files.pythonhosted.org/packages/89/90/a3be7a5f378fc6e84abe4dcfb2ba32b07786861172e502388b4c90000d1b/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:59913f1e5ada20fde795ba906916aea25d442abcc0593fba7e26c92b7ad76249", size = 1676092, upload-time = "2026-01-23T15:33:52.176Z" }, { url = "https://files.pythonhosted.org/packages/89/90/a3be7a5f378fc6e84abe4dcfb2ba32b07786861172e502388b4c90000d1b/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:59913f1e5ada20fde795ba906916aea25d442abcc0593fba7e26c92b7ad76249", size = 1676092, upload-time = "2026-01-23T15:33:52.176Z" },