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,6 +1,11 @@
: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;
} }
* { * {
@@ -123,7 +128,6 @@ body {
} }
@keyframes typing-bounce { @keyframes typing-bounce {
0%, 0%,
60%, 60%,
100% { 100% {
@@ -257,12 +261,6 @@ body {
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;

View File

@@ -102,9 +102,20 @@ const connect = () => {
} }
if (payload.type === "report") { if (payload.type === "report") {
console.log("%cSOURCES", 'border: 2px solid red; padding: 1em; color: red; font-size: 14px; font-weight: bold;') console.log(
"%cREPHRASED QUESTION",
"border: 2px solid red; padding: 1em; color: red; font-size: 14px; font-weight: bold;",
);
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(payload.sources);
console.log("%cRE-RANKED SOURCES", 'border: 2px solid red; padding: 1em; color: red; font-size: 14px; font-weight: bold;') console.log(
"%cRE-RANKED SOURCES",
"border: 2px solid red; padding: 1em; color: red; font-size: 14px; font-weight: bold;",
);
console.log(payload.reranked_sources); console.log(payload.reranked_sources);
return; return;
} }

View File

@@ -1,6 +1,5 @@
<!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" />
@@ -13,60 +12,132 @@
<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"
id="settings-toggle"
type="button"
aria-haspopup="dialog"
aria-expanded="false"
aria-controls="settings-drawer"
>
Configurazione
</button>
</div> </div>
<div class="chat__messages" id="messages"></div> <div class="chat__messages" id="messages"></div>
<div class="chat__status" id="status">Connecting…</div> <div class="chat__status" id="status">Connecting…</div>
<form class="chat__footer" id="chat-form"> <form class="chat__footer" id="chat-form">
<input class="chat__input" id="chat-input" type="text" placeholder="Type a message" autocomplete="off" <input
required /> 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" type="submit">Send</button>
<button class="chat__button chat__button--secondary" id="chat-clear" type="button">Clear</button> <button
class="chat__button chat__button--secondary"
id="chat-clear"
type="button"
>
Clear
</button>
</form> </form>
</section> </section>
<aside class="drawer" id="settings-drawer" aria-label="Configurazione" aria-hidden="true"> <aside
class="drawer"
id="settings-drawer"
aria-label="Configurazione"
aria-hidden="true"
>
<div class="drawer__header"> <div class="drawer__header">
<h2 class="drawer__title">Configurazione</h2> <h2 class="drawer__title">Configurazione</h2>
<button class="drawer__close" id="settings-close" type="button" aria-label="Chiudi"></button> <button
class="drawer__close"
id="settings-close"
type="button"
aria-label="Chiudi"
>
</button>
</div> </div>
<form class="drawer__form"> <form class="drawer__form">
<label class="drawer__field"> <label class="drawer__field">
<span class="drawer__label">top_k</span> <span class="drawer__label">top_k</span>
<input class="drawer__range" id="config-top-k" type="range" min="0" max="100" step="1" value="40" /> <input
class="drawer__range"
id="config-top-k"
type="range"
min="0"
max="100"
step="1"
value="40"
/>
<output class="drawer__value">40</output> <output class="drawer__value">40</output>
</label> </label>
<label class="drawer__field"> <label class="drawer__field">
<span class="drawer__label">top_p</span> <span class="drawer__label">top_p</span>
<input class="drawer__range" id="config-top-p" type="range" min="0" max="1" step="0.1" <input
value="0.0" /> 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> <output class="drawer__value">0.0</output>
</label> </label>
<label class="drawer__field"> <label class="drawer__field">
<span class="drawer__label">temperature</span> <span class="drawer__label">temperature</span>
<input class="drawer__range" id="config-temperature" type="range" min="0" max="1.5" step="0.1" <input
value="0.0" /> 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> <output class="drawer__value">0.0</output>
</label> </label>
<label class="drawer__field"> <label class="drawer__field">
<span class="drawer__label">retriever max docs</span> <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" <input
value="40" /> class="drawer__range"
id="config-retriever-max-docs"
type="range"
min="5"
max="100"
step="1"
value="40"
/>
<output class="drawer__value">40</output> <output class="drawer__value">40</output>
</label> </label>
<label class="drawer__field"> <label class="drawer__field">
<span class="drawer__label">reranker max docs</span> <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" <input
value="20" /> class="drawer__range"
id="config-reranker-max-docs"
type="range"
min="5"
max="100"
step="1"
value="20"
/>
<output class="drawer__value">20</output> <output class="drawer__value">20</output>
</label> </label>
<p class="drawer__hint">Solo frontend, nessuna logica applicata.</p> <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> </form>
</aside> </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" },