Spaces:
Running
Running
github-actions[bot] commited on
Commit ·
46e5b37
1
Parent(s): 5250342
Sync from GitHub 214f9ed998ff8d82e81656fab8d69dcd637cd425
Browse files- app.py +40 -2
- auth/__init__.py +0 -0
- data/__init__.py +0 -0
- data/crud.py +3 -4
- frontend/app.py +149 -42
- requirements.txt +0 -4
- src/artifacts/podcast_generator.py +12 -11
- src/artifacts/quiz_generator.py +6 -5
- src/artifacts/report_generator.py +4 -3
- src/ingestion/service.py +13 -4
- utils/llm_client.py +5 -6
app.py
CHANGED
|
@@ -197,7 +197,7 @@ MAX_HISTORY_MESSAGES = 8
|
|
| 197 |
MAX_HISTORY_CHARS_PER_MESSAGE = 1000
|
| 198 |
MAX_UPLOAD_FILENAME_LENGTH = 255
|
| 199 |
SAFE_FILENAME_RE = re.compile(r"[^A-Za-z0-9._-]+")
|
| 200 |
-
UPLOADS_ROOT = Path("
|
| 201 |
|
| 202 |
|
| 203 |
def _build_conversation_history(
|
|
@@ -287,13 +287,51 @@ def _remove_tree_within_root(root: Path, target: Path) -> None:
|
|
| 287 |
target_resolved = target.resolve()
|
| 288 |
if target_resolved == root_resolved or root_resolved not in target_resolved.parents:
|
| 289 |
raise RuntimeError(f"Refusing to delete path outside root: {target_resolved}")
|
| 290 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 291 |
|
| 292 |
|
| 293 |
def _cleanup_notebook_storage(owner_user_id: int, notebook_id: int) -> None:
|
| 294 |
storage_base = Path(os.getenv("STORAGE_BASE_DIR", "data"))
|
| 295 |
notebook_root = storage_base / "users" / str(owner_user_id) / "notebooks"
|
| 296 |
notebook_path = notebook_root / str(notebook_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
_remove_tree_within_root(notebook_root, notebook_path)
|
| 298 |
|
| 299 |
upload_path = UPLOADS_ROOT / f"notebook_{notebook_id}"
|
|
|
|
| 197 |
MAX_HISTORY_CHARS_PER_MESSAGE = 1000
|
| 198 |
MAX_UPLOAD_FILENAME_LENGTH = 255
|
| 199 |
SAFE_FILENAME_RE = re.compile(r"[^A-Za-z0-9._-]+")
|
| 200 |
+
UPLOADS_ROOT = Path(os.getenv("STORAGE_BASE_DIR", "data")) / "uploads"
|
| 201 |
|
| 202 |
|
| 203 |
def _build_conversation_history(
|
|
|
|
| 287 |
target_resolved = target.resolve()
|
| 288 |
if target_resolved == root_resolved or root_resolved not in target_resolved.parents:
|
| 289 |
raise RuntimeError(f"Refusing to delete path outside root: {target_resolved}")
|
| 290 |
+
|
| 291 |
+
def _onerror(_func: Any, path: str, _exc_info: Any) -> None:
|
| 292 |
+
"""Handle Windows locked / read-only files by forcing writable."""
|
| 293 |
+
try:
|
| 294 |
+
os.chmod(path, 0o777)
|
| 295 |
+
os.remove(path)
|
| 296 |
+
except OSError:
|
| 297 |
+
pass
|
| 298 |
+
|
| 299 |
+
# Try up to 3 times to handle file locks (e.g. ChromaDB on Windows)
|
| 300 |
+
import gc
|
| 301 |
+
for attempt in range(3):
|
| 302 |
+
try:
|
| 303 |
+
shutil.rmtree(target_resolved, onerror=_onerror)
|
| 304 |
+
return
|
| 305 |
+
except OSError:
|
| 306 |
+
if attempt == 2:
|
| 307 |
+
raise
|
| 308 |
+
gc.collect()
|
| 309 |
+
import time
|
| 310 |
+
time.sleep(0.5)
|
| 311 |
|
| 312 |
|
| 313 |
def _cleanup_notebook_storage(owner_user_id: int, notebook_id: int) -> None:
|
| 314 |
storage_base = Path(os.getenv("STORAGE_BASE_DIR", "data"))
|
| 315 |
notebook_root = storage_base / "users" / str(owner_user_id) / "notebooks"
|
| 316 |
notebook_path = notebook_root / str(notebook_id)
|
| 317 |
+
|
| 318 |
+
# Release any ChromaDB connections to this notebook before deleting
|
| 319 |
+
chroma_dir = notebook_path / "chroma"
|
| 320 |
+
if chroma_dir.exists():
|
| 321 |
+
try:
|
| 322 |
+
from src.ingestion.vectorstore import ChromaAdapter
|
| 323 |
+
store = ChromaAdapter(persist_directory=str(chroma_dir))
|
| 324 |
+
collection_name = f"user_{owner_user_id}_notebook_{notebook_id}"
|
| 325 |
+
try:
|
| 326 |
+
store._client.delete_collection(collection_name)
|
| 327 |
+
except Exception:
|
| 328 |
+
pass
|
| 329 |
+
del store
|
| 330 |
+
import gc
|
| 331 |
+
gc.collect()
|
| 332 |
+
except Exception:
|
| 333 |
+
pass
|
| 334 |
+
|
| 335 |
_remove_tree_within_root(notebook_root, notebook_path)
|
| 336 |
|
| 337 |
upload_path = UPLOADS_ROOT / f"notebook_{notebook_id}"
|
auth/__init__.py
ADDED
|
File without changes
|
data/__init__.py
ADDED
|
File without changes
|
data/crud.py
CHANGED
|
@@ -1,11 +1,10 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
from collections import defaultdict
|
| 4 |
-
from datetime import datetime
|
| 5 |
-
from data.models import Artifact
|
| 6 |
from sqlalchemy.orm import Session
|
| 7 |
|
| 8 |
-
from data.models import ChatThread, Message, MessageCitation, Notebook, Source, User
|
| 9 |
|
| 10 |
|
| 11 |
def get_or_create_user(
|
|
@@ -296,7 +295,7 @@ def update_artifact(
|
|
| 296 |
merged.update(metadata)
|
| 297 |
artifact.artifact_metadata = merged
|
| 298 |
if status == "ready":
|
| 299 |
-
artifact.generated_at = datetime.
|
| 300 |
|
| 301 |
db.commit()
|
| 302 |
db.refresh(artifact)
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
from collections import defaultdict
|
| 4 |
+
from datetime import datetime, timezone
|
|
|
|
| 5 |
from sqlalchemy.orm import Session
|
| 6 |
|
| 7 |
+
from data.models import Artifact, ChatThread, Message, MessageCitation, Notebook, Source, User
|
| 8 |
|
| 9 |
|
| 10 |
def get_or_create_user(
|
|
|
|
| 295 |
merged.update(metadata)
|
| 296 |
artifact.artifact_metadata = merged
|
| 297 |
if status == "ready":
|
| 298 |
+
artifact.generated_at = datetime.now(timezone.utc)
|
| 299 |
|
| 300 |
db.commit()
|
| 301 |
db.refresh(artifact)
|
frontend/app.py
CHANGED
|
@@ -161,6 +161,7 @@ def inject_theme() -> None:
|
|
| 161 |
<style>
|
| 162 |
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;700&family=IBM+Plex+Sans:wght@400;500;600&display=swap');
|
| 163 |
|
|
|
|
| 164 |
:root {
|
| 165 |
--ink: #162525;
|
| 166 |
--ink-muted: #4f6362;
|
|
@@ -176,13 +177,89 @@ def inject_theme() -> None:
|
|
| 176 |
--error-border: #d2675a;
|
| 177 |
--pending-soft: #eceeef;
|
| 178 |
--pending-border: #9aa7ad;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
}
|
| 180 |
|
| 181 |
.stApp {
|
| 182 |
background:
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
color: var(--ink);
|
| 187 |
font-family: "IBM Plex Sans", sans-serif;
|
| 188 |
}
|
|
@@ -195,10 +272,11 @@ def inject_theme() -> None:
|
|
| 195 |
|
| 196 |
p, li, label, [data-testid="stMarkdownContainer"] p {
|
| 197 |
line-height: 1.5;
|
|
|
|
| 198 |
}
|
| 199 |
|
| 200 |
[data-testid="stSidebar"] {
|
| 201 |
-
background:
|
| 202 |
}
|
| 203 |
|
| 204 |
[data-testid="stSidebar"] h1,
|
|
@@ -209,7 +287,7 @@ def inject_theme() -> None:
|
|
| 209 |
[data-testid="stSidebar"] p,
|
| 210 |
[data-testid="stSidebar"] small,
|
| 211 |
[data-testid="stSidebar"] span {
|
| 212 |
-
color:
|
| 213 |
font-family: "IBM Plex Sans", sans-serif;
|
| 214 |
}
|
| 215 |
|
|
@@ -217,13 +295,13 @@ def inject_theme() -> None:
|
|
| 217 |
[data-testid="stSidebar"] .stTextArea textarea,
|
| 218 |
[data-testid="stSidebar"] div[data-baseweb="select"] > div,
|
| 219 |
[data-testid="stSidebar"] .stNumberInput input {
|
| 220 |
-
background:
|
| 221 |
-
color:
|
| 222 |
-
border: 1px solid
|
| 223 |
}
|
| 224 |
|
| 225 |
[data-testid="stSidebar"] div[data-baseweb="select"] * {
|
| 226 |
-
color:
|
| 227 |
}
|
| 228 |
|
| 229 |
h1, h2, h3, h4 {
|
|
@@ -233,8 +311,8 @@ def inject_theme() -> None:
|
|
| 233 |
}
|
| 234 |
|
| 235 |
.app-hero {
|
| 236 |
-
background:
|
| 237 |
-
border: 1px solid
|
| 238 |
border-radius: 18px;
|
| 239 |
padding: 18px 22px;
|
| 240 |
margin-bottom: 14px;
|
|
@@ -247,9 +325,9 @@ def inject_theme() -> None:
|
|
| 247 |
font-size: 0.75rem;
|
| 248 |
letter-spacing: 0.08em;
|
| 249 |
text-transform: uppercase;
|
| 250 |
-
color:
|
| 251 |
-
background:
|
| 252 |
-
border: 1px solid
|
| 253 |
border-radius: 999px;
|
| 254 |
padding: 3px 10px;
|
| 255 |
margin-bottom: 8px;
|
|
@@ -260,6 +338,7 @@ def inject_theme() -> None:
|
|
| 260 |
font-family: "Space Grotesk", sans-serif;
|
| 261 |
font-size: clamp(1.45rem, 2.4vw, 2rem);
|
| 262 |
line-height: 1.2;
|
|
|
|
| 263 |
}
|
| 264 |
|
| 265 |
.hero-sub {
|
|
@@ -269,10 +348,11 @@ def inject_theme() -> None:
|
|
| 269 |
}
|
| 270 |
|
| 271 |
.soft-card {
|
| 272 |
-
background:
|
| 273 |
border: 1px solid var(--card-border);
|
| 274 |
border-radius: 14px;
|
| 275 |
padding: 12px 14px;
|
|
|
|
| 276 |
}
|
| 277 |
|
| 278 |
.metric-card {
|
|
@@ -307,22 +387,22 @@ def inject_theme() -> None:
|
|
| 307 |
.status-ready {
|
| 308 |
background: var(--ok-soft);
|
| 309 |
border-color: var(--ok-border);
|
| 310 |
-
color:
|
| 311 |
}
|
| 312 |
.status-failed {
|
| 313 |
background: var(--error-soft);
|
| 314 |
border-color: var(--error-border);
|
| 315 |
-
color:
|
| 316 |
}
|
| 317 |
.status-processing {
|
| 318 |
background: var(--warn-soft);
|
| 319 |
border-color: var(--warn-border);
|
| 320 |
-
color:
|
| 321 |
}
|
| 322 |
.status-pending {
|
| 323 |
background: var(--pending-soft);
|
| 324 |
border-color: var(--pending-border);
|
| 325 |
-
color:
|
| 326 |
}
|
| 327 |
|
| 328 |
[data-testid="stText"] {
|
|
@@ -338,18 +418,18 @@ def inject_theme() -> None:
|
|
| 338 |
div.stFormSubmitButton > button,
|
| 339 |
div.stDownloadButton > button {
|
| 340 |
border-radius: 11px;
|
| 341 |
-
border: 1px solid
|
| 342 |
-
background:
|
| 343 |
-
color:
|
| 344 |
font-weight: 600;
|
| 345 |
padding: 0.38rem 0.95rem;
|
| 346 |
transition: transform 120ms ease, box-shadow 120ms ease, filter 120ms ease;
|
| 347 |
-
box-shadow: 0 3px 10px
|
| 348 |
}
|
| 349 |
div.stButton > button *,
|
| 350 |
div.stFormSubmitButton > button *,
|
| 351 |
div.stDownloadButton > button * {
|
| 352 |
-
color:
|
| 353 |
}
|
| 354 |
|
| 355 |
div.stButton > button:hover,
|
|
@@ -357,7 +437,7 @@ def inject_theme() -> None:
|
|
| 357 |
div.stDownloadButton > button:hover {
|
| 358 |
transform: translateY(-1px);
|
| 359 |
filter: brightness(1.03);
|
| 360 |
-
box-shadow: 0 5px 14px
|
| 361 |
}
|
| 362 |
|
| 363 |
div.stButton > button:focus,
|
|
@@ -370,44 +450,71 @@ def inject_theme() -> None:
|
|
| 370 |
[data-testid="stSidebar"] div.stButton > button,
|
| 371 |
[data-testid="stSidebar"] div.stFormSubmitButton > button,
|
| 372 |
[data-testid="stSidebar"] div.stDownloadButton > button {
|
| 373 |
-
background:
|
| 374 |
-
color:
|
| 375 |
-
border: 1px solid
|
| 376 |
box-shadow: none;
|
| 377 |
}
|
| 378 |
[data-testid="stSidebar"] div.stButton > button *,
|
| 379 |
[data-testid="stSidebar"] div.stFormSubmitButton > button *,
|
| 380 |
[data-testid="stSidebar"] div.stDownloadButton > button * {
|
| 381 |
-
color:
|
| 382 |
}
|
| 383 |
|
| 384 |
[data-testid="stSidebar"] div.stButton > button:hover,
|
| 385 |
[data-testid="stSidebar"] div.stFormSubmitButton > button:hover,
|
| 386 |
[data-testid="stSidebar"] div.stDownloadButton > button:hover {
|
| 387 |
-
background:
|
| 388 |
}
|
| 389 |
|
| 390 |
/* Streamlit base button variants (primary/secondary/tertiary) */
|
| 391 |
button[data-testid^="stBaseButton"] {
|
| 392 |
-
color:
|
| 393 |
}
|
| 394 |
[data-testid="stSidebar"] button[data-testid^="stBaseButton"] {
|
| 395 |
-
color:
|
| 396 |
}
|
| 397 |
button[data-testid^="stBaseButton"] * {
|
| 398 |
color: inherit !important;
|
| 399 |
}
|
| 400 |
|
| 401 |
-
/* Keep form controls readable across
|
| 402 |
.stTextInput input,
|
| 403 |
.stTextArea textarea,
|
| 404 |
.stNumberInput input,
|
| 405 |
div[data-baseweb="select"] > div {
|
| 406 |
-
color:
|
| 407 |
}
|
| 408 |
.stTextInput input::placeholder,
|
| 409 |
.stTextArea textarea::placeholder {
|
| 410 |
-
color:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 411 |
}
|
| 412 |
|
| 413 |
[data-testid="stDataFrame"] {
|
|
@@ -646,7 +753,7 @@ if page == "Notebooks":
|
|
| 646 |
with refresh_col:
|
| 647 |
st.write("")
|
| 648 |
st.write("")
|
| 649 |
-
if st.button("Refresh notebooks",
|
| 650 |
st.rerun()
|
| 651 |
|
| 652 |
if submitted:
|
|
@@ -680,7 +787,7 @@ if page == "Notebooks":
|
|
| 680 |
|
| 681 |
if notebooks:
|
| 682 |
st.write("Your notebooks")
|
| 683 |
-
st.dataframe(notebooks,
|
| 684 |
|
| 685 |
notebook_options = {
|
| 686 |
f"{n['id']} - {n['title']}": n
|
|
@@ -825,10 +932,10 @@ if page == "Notebooks":
|
|
| 825 |
if ok:
|
| 826 |
st.success("File uploaded and source added.")
|
| 827 |
st.json(create_result)
|
|
|
|
| 828 |
else:
|
| 829 |
st.error("Failed to upload file source.")
|
| 830 |
st.code(str(create_result))
|
| 831 |
-
st.rerun()
|
| 832 |
|
| 833 |
if source_type == "url" and not source_url.strip():
|
| 834 |
st.error("Please provide a URL when source type is 'url'.")
|
|
@@ -878,7 +985,7 @@ if page == "Notebooks":
|
|
| 878 |
with source_metrics_3:
|
| 879 |
render_metric_card("In Flight", processing_sources)
|
| 880 |
st.write("Sources")
|
| 881 |
-
st.dataframe(sources,
|
| 882 |
else:
|
| 883 |
st.info("No sources yet for this notebook.")
|
| 884 |
else:
|
|
@@ -952,7 +1059,7 @@ if page == "Notebooks":
|
|
| 952 |
st.markdown(str(content))
|
| 953 |
if role == "assistant" and isinstance(citations, list) and citations:
|
| 954 |
with st.expander("Citations", expanded=False):
|
| 955 |
-
st.dataframe(citations,
|
| 956 |
else:
|
| 957 |
st.error("Failed to fetch messages.")
|
| 958 |
st.code(str(message_result))
|
|
@@ -978,7 +1085,7 @@ if page == "Notebooks":
|
|
| 978 |
citations = chat_result.get("citations", [])
|
| 979 |
if citations:
|
| 980 |
st.write("Citations")
|
| 981 |
-
st.dataframe(citations,
|
| 982 |
st.rerun()
|
| 983 |
else:
|
| 984 |
st.error("Chat request failed.")
|
|
@@ -1143,7 +1250,7 @@ if page == "Notebooks":
|
|
| 1143 |
with artifact_metric_4:
|
| 1144 |
render_metric_card("Failed", failed_count)
|
| 1145 |
|
| 1146 |
-
st.dataframe(artifacts,
|
| 1147 |
selected_artifact = choose_artifact_for_notebook(
|
| 1148 |
int(selected_notebook_id),
|
| 1149 |
artifacts,
|
|
|
|
| 161 |
<style>
|
| 162 |
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;700&family=IBM+Plex+Sans:wght@400;500;600&display=swap');
|
| 163 |
|
| 164 |
+
/* ── Light-mode tokens (default) ── */
|
| 165 |
:root {
|
| 166 |
--ink: #162525;
|
| 167 |
--ink-muted: #4f6362;
|
|
|
|
| 177 |
--error-border: #d2675a;
|
| 178 |
--pending-soft: #eceeef;
|
| 179 |
--pending-border: #9aa7ad;
|
| 180 |
+
--app-bg: linear-gradient(180deg, #f8faf8 0%, #f1f5f2 100%);
|
| 181 |
+
--app-bg-overlay-1: radial-gradient(circle at 15% -5%, #fff5e6 0%, rgba(255, 245, 230, 0) 42%);
|
| 182 |
+
--app-bg-overlay-2: radial-gradient(circle at 88% 8%, #dff2ee 0%, rgba(223, 242, 238, 0) 44%);
|
| 183 |
+
--hero-bg: linear-gradient(125deg, rgba(20, 111, 103, 0.13) 0%, rgba(255, 245, 228, 0.95) 56%, rgba(205, 231, 226, 0.9) 100%);
|
| 184 |
+
--hero-border: #c9d9cf;
|
| 185 |
+
--hero-kicker-bg: rgba(255, 255, 255, 0.7);
|
| 186 |
+
--hero-kicker-color: #0f625b;
|
| 187 |
+
--hero-kicker-border: rgba(15, 98, 91, 0.22);
|
| 188 |
+
--soft-card-bg: rgba(255, 255, 255, 0.82);
|
| 189 |
+
--sidebar-bg: linear-gradient(180deg, #173436 0%, #11292b 100%);
|
| 190 |
+
--sidebar-text: #e9f1ef;
|
| 191 |
+
--sidebar-input-bg: rgba(233, 241, 239, 0.1);
|
| 192 |
+
--sidebar-input-border: rgba(178, 209, 202, 0.35);
|
| 193 |
+
--sidebar-btn-bg: linear-gradient(135deg, #ecf8f4 0%, #d8efe9 100%);
|
| 194 |
+
--sidebar-btn-color: #123f3b;
|
| 195 |
+
--sidebar-btn-border: #90c6bb;
|
| 196 |
+
--sidebar-btn-hover-bg: linear-gradient(135deg, #f5fffc 0%, #e3f8f3 100%);
|
| 197 |
+
--btn-bg: linear-gradient(135deg, #167269 0%, #0f5f57 100%);
|
| 198 |
+
--btn-color: #f4fffd;
|
| 199 |
+
--btn-border: #0f5f57;
|
| 200 |
+
--btn-shadow: rgba(15, 95, 87, 0.22);
|
| 201 |
+
--input-color: #162525;
|
| 202 |
+
--input-placeholder: #5a6a69;
|
| 203 |
+
--select-dropdown-color: #0f2a27;
|
| 204 |
+
--status-ready-color: #1c6b4d;
|
| 205 |
+
--status-failed-color: #8d3329;
|
| 206 |
+
--status-processing-color: #7d581a;
|
| 207 |
+
--status-pending-color: #485a63;
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
/* ── Dark-mode tokens ── */
|
| 211 |
+
@media (prefers-color-scheme: dark) {
|
| 212 |
+
:root {
|
| 213 |
+
--ink: #e4eceb;
|
| 214 |
+
--ink-muted: #9bb0ae;
|
| 215 |
+
--card: #1e2d2d;
|
| 216 |
+
--card-border: #3a4e4b;
|
| 217 |
+
--accent: #3dd4c6;
|
| 218 |
+
--accent-soft: #1a3d39;
|
| 219 |
+
--warn-soft: #3d3223;
|
| 220 |
+
--warn-border: #b89040;
|
| 221 |
+
--ok-soft: #1a3d2e;
|
| 222 |
+
--ok-border: #3d9a6a;
|
| 223 |
+
--error-soft: #3d2220;
|
| 224 |
+
--error-border: #c25a4e;
|
| 225 |
+
--pending-soft: #2a3235;
|
| 226 |
+
--pending-border: #6b7b82;
|
| 227 |
+
--app-bg: linear-gradient(180deg, #141e1e 0%, #0f1817 100%);
|
| 228 |
+
--app-bg-overlay-1: radial-gradient(circle at 15% -5%, rgba(50, 40, 20, 0.4) 0%, rgba(50, 40, 20, 0) 42%);
|
| 229 |
+
--app-bg-overlay-2: radial-gradient(circle at 88% 8%, rgba(20, 60, 55, 0.3) 0%, rgba(20, 60, 55, 0) 44%);
|
| 230 |
+
--hero-bg: linear-gradient(125deg, rgba(20, 111, 103, 0.2) 0%, rgba(30, 45, 45, 0.95) 56%, rgba(25, 60, 55, 0.9) 100%);
|
| 231 |
+
--hero-border: #2a3e3c;
|
| 232 |
+
--hero-kicker-bg: rgba(30, 45, 45, 0.7);
|
| 233 |
+
--hero-kicker-color: #5ce0d3;
|
| 234 |
+
--hero-kicker-border: rgba(60, 180, 165, 0.3);
|
| 235 |
+
--soft-card-bg: rgba(30, 45, 45, 0.7);
|
| 236 |
+
--sidebar-bg: linear-gradient(180deg, #0e1f20 0%, #0a1617 100%);
|
| 237 |
+
--sidebar-text: #c8dad6;
|
| 238 |
+
--sidebar-input-bg: rgba(200, 218, 214, 0.08);
|
| 239 |
+
--sidebar-input-border: rgba(120, 170, 160, 0.25);
|
| 240 |
+
--sidebar-btn-bg: linear-gradient(135deg, #1a3332 0%, #162c2b 100%);
|
| 241 |
+
--sidebar-btn-color: #b0d8cf;
|
| 242 |
+
--sidebar-btn-border: #3a6e64;
|
| 243 |
+
--sidebar-btn-hover-bg: linear-gradient(135deg, #1f3d3b 0%, #1a3634 100%);
|
| 244 |
+
--btn-bg: linear-gradient(135deg, #1a8a7e 0%, #167a70 100%);
|
| 245 |
+
--btn-color: #e8fff9;
|
| 246 |
+
--btn-border: #1a8a7e;
|
| 247 |
+
--btn-shadow: rgba(26, 138, 126, 0.3);
|
| 248 |
+
--input-color: #e0ece9;
|
| 249 |
+
--input-placeholder: #7a9490;
|
| 250 |
+
--select-dropdown-color: #e0ece9;
|
| 251 |
+
--status-ready-color: #6ee6b7;
|
| 252 |
+
--status-failed-color: #f49b8f;
|
| 253 |
+
--status-processing-color: #f0c56d;
|
| 254 |
+
--status-pending-color: #9bb0b5;
|
| 255 |
+
}
|
| 256 |
}
|
| 257 |
|
| 258 |
.stApp {
|
| 259 |
background:
|
| 260 |
+
var(--app-bg-overlay-1),
|
| 261 |
+
var(--app-bg-overlay-2),
|
| 262 |
+
var(--app-bg);
|
| 263 |
color: var(--ink);
|
| 264 |
font-family: "IBM Plex Sans", sans-serif;
|
| 265 |
}
|
|
|
|
| 272 |
|
| 273 |
p, li, label, [data-testid="stMarkdownContainer"] p {
|
| 274 |
line-height: 1.5;
|
| 275 |
+
color: var(--ink);
|
| 276 |
}
|
| 277 |
|
| 278 |
[data-testid="stSidebar"] {
|
| 279 |
+
background: var(--sidebar-bg);
|
| 280 |
}
|
| 281 |
|
| 282 |
[data-testid="stSidebar"] h1,
|
|
|
|
| 287 |
[data-testid="stSidebar"] p,
|
| 288 |
[data-testid="stSidebar"] small,
|
| 289 |
[data-testid="stSidebar"] span {
|
| 290 |
+
color: var(--sidebar-text);
|
| 291 |
font-family: "IBM Plex Sans", sans-serif;
|
| 292 |
}
|
| 293 |
|
|
|
|
| 295 |
[data-testid="stSidebar"] .stTextArea textarea,
|
| 296 |
[data-testid="stSidebar"] div[data-baseweb="select"] > div,
|
| 297 |
[data-testid="stSidebar"] .stNumberInput input {
|
| 298 |
+
background: var(--sidebar-input-bg);
|
| 299 |
+
color: var(--sidebar-text);
|
| 300 |
+
border: 1px solid var(--sidebar-input-border);
|
| 301 |
}
|
| 302 |
|
| 303 |
[data-testid="stSidebar"] div[data-baseweb="select"] * {
|
| 304 |
+
color: var(--select-dropdown-color);
|
| 305 |
}
|
| 306 |
|
| 307 |
h1, h2, h3, h4 {
|
|
|
|
| 311 |
}
|
| 312 |
|
| 313 |
.app-hero {
|
| 314 |
+
background: var(--hero-bg);
|
| 315 |
+
border: 1px solid var(--hero-border);
|
| 316 |
border-radius: 18px;
|
| 317 |
padding: 18px 22px;
|
| 318 |
margin-bottom: 14px;
|
|
|
|
| 325 |
font-size: 0.75rem;
|
| 326 |
letter-spacing: 0.08em;
|
| 327 |
text-transform: uppercase;
|
| 328 |
+
color: var(--hero-kicker-color);
|
| 329 |
+
background: var(--hero-kicker-bg);
|
| 330 |
+
border: 1px solid var(--hero-kicker-border);
|
| 331 |
border-radius: 999px;
|
| 332 |
padding: 3px 10px;
|
| 333 |
margin-bottom: 8px;
|
|
|
|
| 338 |
font-family: "Space Grotesk", sans-serif;
|
| 339 |
font-size: clamp(1.45rem, 2.4vw, 2rem);
|
| 340 |
line-height: 1.2;
|
| 341 |
+
color: var(--ink);
|
| 342 |
}
|
| 343 |
|
| 344 |
.hero-sub {
|
|
|
|
| 348 |
}
|
| 349 |
|
| 350 |
.soft-card {
|
| 351 |
+
background: var(--soft-card-bg);
|
| 352 |
border: 1px solid var(--card-border);
|
| 353 |
border-radius: 14px;
|
| 354 |
padding: 12px 14px;
|
| 355 |
+
color: var(--ink);
|
| 356 |
}
|
| 357 |
|
| 358 |
.metric-card {
|
|
|
|
| 387 |
.status-ready {
|
| 388 |
background: var(--ok-soft);
|
| 389 |
border-color: var(--ok-border);
|
| 390 |
+
color: var(--status-ready-color);
|
| 391 |
}
|
| 392 |
.status-failed {
|
| 393 |
background: var(--error-soft);
|
| 394 |
border-color: var(--error-border);
|
| 395 |
+
color: var(--status-failed-color);
|
| 396 |
}
|
| 397 |
.status-processing {
|
| 398 |
background: var(--warn-soft);
|
| 399 |
border-color: var(--warn-border);
|
| 400 |
+
color: var(--status-processing-color);
|
| 401 |
}
|
| 402 |
.status-pending {
|
| 403 |
background: var(--pending-soft);
|
| 404 |
border-color: var(--pending-border);
|
| 405 |
+
color: var(--status-pending-color);
|
| 406 |
}
|
| 407 |
|
| 408 |
[data-testid="stText"] {
|
|
|
|
| 418 |
div.stFormSubmitButton > button,
|
| 419 |
div.stDownloadButton > button {
|
| 420 |
border-radius: 11px;
|
| 421 |
+
border: 1px solid var(--btn-border);
|
| 422 |
+
background: var(--btn-bg);
|
| 423 |
+
color: var(--btn-color) !important;
|
| 424 |
font-weight: 600;
|
| 425 |
padding: 0.38rem 0.95rem;
|
| 426 |
transition: transform 120ms ease, box-shadow 120ms ease, filter 120ms ease;
|
| 427 |
+
box-shadow: 0 3px 10px var(--btn-shadow);
|
| 428 |
}
|
| 429 |
div.stButton > button *,
|
| 430 |
div.stFormSubmitButton > button *,
|
| 431 |
div.stDownloadButton > button * {
|
| 432 |
+
color: var(--btn-color) !important;
|
| 433 |
}
|
| 434 |
|
| 435 |
div.stButton > button:hover,
|
|
|
|
| 437 |
div.stDownloadButton > button:hover {
|
| 438 |
transform: translateY(-1px);
|
| 439 |
filter: brightness(1.03);
|
| 440 |
+
box-shadow: 0 5px 14px var(--btn-shadow);
|
| 441 |
}
|
| 442 |
|
| 443 |
div.stButton > button:focus,
|
|
|
|
| 450 |
[data-testid="stSidebar"] div.stButton > button,
|
| 451 |
[data-testid="stSidebar"] div.stFormSubmitButton > button,
|
| 452 |
[data-testid="stSidebar"] div.stDownloadButton > button {
|
| 453 |
+
background: var(--sidebar-btn-bg);
|
| 454 |
+
color: var(--sidebar-btn-color) !important;
|
| 455 |
+
border: 1px solid var(--sidebar-btn-border);
|
| 456 |
box-shadow: none;
|
| 457 |
}
|
| 458 |
[data-testid="stSidebar"] div.stButton > button *,
|
| 459 |
[data-testid="stSidebar"] div.stFormSubmitButton > button *,
|
| 460 |
[data-testid="stSidebar"] div.stDownloadButton > button * {
|
| 461 |
+
color: var(--sidebar-btn-color) !important;
|
| 462 |
}
|
| 463 |
|
| 464 |
[data-testid="stSidebar"] div.stButton > button:hover,
|
| 465 |
[data-testid="stSidebar"] div.stFormSubmitButton > button:hover,
|
| 466 |
[data-testid="stSidebar"] div.stDownloadButton > button:hover {
|
| 467 |
+
background: var(--sidebar-btn-hover-bg);
|
| 468 |
}
|
| 469 |
|
| 470 |
/* Streamlit base button variants (primary/secondary/tertiary) */
|
| 471 |
button[data-testid^="stBaseButton"] {
|
| 472 |
+
color: var(--ink) !important;
|
| 473 |
}
|
| 474 |
[data-testid="stSidebar"] button[data-testid^="stBaseButton"] {
|
| 475 |
+
color: var(--sidebar-btn-color) !important;
|
| 476 |
}
|
| 477 |
button[data-testid^="stBaseButton"] * {
|
| 478 |
color: inherit !important;
|
| 479 |
}
|
| 480 |
|
| 481 |
+
/* Keep form controls readable across light/dark */
|
| 482 |
.stTextInput input,
|
| 483 |
.stTextArea textarea,
|
| 484 |
.stNumberInput input,
|
| 485 |
div[data-baseweb="select"] > div {
|
| 486 |
+
color: var(--input-color) !important;
|
| 487 |
}
|
| 488 |
.stTextInput input::placeholder,
|
| 489 |
.stTextArea textarea::placeholder {
|
| 490 |
+
color: var(--input-placeholder) !important;
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
/* Chat message text */
|
| 494 |
+
[data-testid="stChatMessage"] p,
|
| 495 |
+
[data-testid="stChatMessage"] li,
|
| 496 |
+
[data-testid="stChatMessage"] span {
|
| 497 |
+
color: var(--ink) !important;
|
| 498 |
+
}
|
| 499 |
+
|
| 500 |
+
/* Expander text */
|
| 501 |
+
[data-testid="stExpander"] summary span {
|
| 502 |
+
color: var(--ink) !important;
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
/* Selectbox display text */
|
| 506 |
+
div[data-baseweb="select"] span {
|
| 507 |
+
color: var(--input-color) !important;
|
| 508 |
+
}
|
| 509 |
+
|
| 510 |
+
/* Tabs / segmented control labels */
|
| 511 |
+
[data-testid="stTabs"] button p {
|
| 512 |
+
color: var(--ink) !important;
|
| 513 |
+
}
|
| 514 |
+
|
| 515 |
+
/* Caption */
|
| 516 |
+
.stCaption, [data-testid="stCaptionContainer"] {
|
| 517 |
+
color: var(--ink-muted) !important;
|
| 518 |
}
|
| 519 |
|
| 520 |
[data-testid="stDataFrame"] {
|
|
|
|
| 753 |
with refresh_col:
|
| 754 |
st.write("")
|
| 755 |
st.write("")
|
| 756 |
+
if st.button("Refresh notebooks", width="stretch"):
|
| 757 |
st.rerun()
|
| 758 |
|
| 759 |
if submitted:
|
|
|
|
| 787 |
|
| 788 |
if notebooks:
|
| 789 |
st.write("Your notebooks")
|
| 790 |
+
st.dataframe(notebooks, width="stretch", hide_index=True)
|
| 791 |
|
| 792 |
notebook_options = {
|
| 793 |
f"{n['id']} - {n['title']}": n
|
|
|
|
| 932 |
if ok:
|
| 933 |
st.success("File uploaded and source added.")
|
| 934 |
st.json(create_result)
|
| 935 |
+
st.rerun()
|
| 936 |
else:
|
| 937 |
st.error("Failed to upload file source.")
|
| 938 |
st.code(str(create_result))
|
|
|
|
| 939 |
|
| 940 |
if source_type == "url" and not source_url.strip():
|
| 941 |
st.error("Please provide a URL when source type is 'url'.")
|
|
|
|
| 985 |
with source_metrics_3:
|
| 986 |
render_metric_card("In Flight", processing_sources)
|
| 987 |
st.write("Sources")
|
| 988 |
+
st.dataframe(sources, width="stretch", hide_index=True)
|
| 989 |
else:
|
| 990 |
st.info("No sources yet for this notebook.")
|
| 991 |
else:
|
|
|
|
| 1059 |
st.markdown(str(content))
|
| 1060 |
if role == "assistant" and isinstance(citations, list) and citations:
|
| 1061 |
with st.expander("Citations", expanded=False):
|
| 1062 |
+
st.dataframe(citations, width="stretch", hide_index=True)
|
| 1063 |
else:
|
| 1064 |
st.error("Failed to fetch messages.")
|
| 1065 |
st.code(str(message_result))
|
|
|
|
| 1085 |
citations = chat_result.get("citations", [])
|
| 1086 |
if citations:
|
| 1087 |
st.write("Citations")
|
| 1088 |
+
st.dataframe(citations, width="stretch")
|
| 1089 |
st.rerun()
|
| 1090 |
else:
|
| 1091 |
st.error("Chat request failed.")
|
|
|
|
| 1250 |
with artifact_metric_4:
|
| 1251 |
render_metric_card("Failed", failed_count)
|
| 1252 |
|
| 1253 |
+
st.dataframe(artifacts, width="stretch", hide_index=True)
|
| 1254 |
selected_artifact = choose_artifact_for_notebook(
|
| 1255 |
int(selected_notebook_id),
|
| 1256 |
artifacts,
|
requirements.txt
CHANGED
|
@@ -7,12 +7,9 @@ python-multipart==0.0.20
|
|
| 7 |
itsdangerous==2.2.0
|
| 8 |
httpx==0.28.1
|
| 9 |
openai>=1.0.0
|
| 10 |
-
faiss-cpu==1.10.0
|
| 11 |
-
pypdf==5.3.0
|
| 12 |
requests==2.32.3
|
| 13 |
streamlit==1.42.2
|
| 14 |
chromadb==0.5.20
|
| 15 |
-
langchain-text-splitters==0.3.5
|
| 16 |
sentence-transformers==3.4.1
|
| 17 |
python-pptx==1.0.2
|
| 18 |
groq==0.19.0
|
|
@@ -20,7 +17,6 @@ transformers
|
|
| 20 |
pymupdf
|
| 21 |
beautifulsoup4
|
| 22 |
readability-lxml
|
| 23 |
-
sqlmodel
|
| 24 |
nltk
|
| 25 |
tqdm
|
| 26 |
pytest
|
|
|
|
| 7 |
itsdangerous==2.2.0
|
| 8 |
httpx==0.28.1
|
| 9 |
openai>=1.0.0
|
|
|
|
|
|
|
| 10 |
requests==2.32.3
|
| 11 |
streamlit==1.42.2
|
| 12 |
chromadb==0.5.20
|
|
|
|
| 13 |
sentence-transformers==3.4.1
|
| 14 |
python-pptx==1.0.2
|
| 15 |
groq==0.19.0
|
|
|
|
| 17 |
pymupdf
|
| 18 |
beautifulsoup4
|
| 19 |
readability-lxml
|
|
|
|
| 20 |
nltk
|
| 21 |
tqdm
|
| 22 |
pytest
|
src/artifacts/podcast_generator.py
CHANGED
|
@@ -6,7 +6,7 @@ from __future__ import annotations
|
|
| 6 |
import json
|
| 7 |
import os
|
| 8 |
import re
|
| 9 |
-
from datetime import datetime
|
| 10 |
from pathlib import Path
|
| 11 |
from typing import Any, Dict, List, Optional
|
| 12 |
|
|
@@ -166,7 +166,7 @@ class PodcastGenerator:
|
|
| 166 |
"num_segments": len(script),
|
| 167 |
"topic_focus": topic_focus,
|
| 168 |
"tts_errors": self._last_tts_errors[:20],
|
| 169 |
-
"generated_at": datetime.
|
| 170 |
},
|
| 171 |
}
|
| 172 |
|
|
@@ -190,7 +190,7 @@ class PodcastGenerator:
|
|
| 190 |
"llm_model": self.model,
|
| 191 |
"num_segments": len(script),
|
| 192 |
"topic_focus": topic_focus,
|
| 193 |
-
"generated_at": datetime.
|
| 194 |
},
|
| 195 |
}
|
| 196 |
|
|
@@ -206,7 +206,7 @@ class PodcastGenerator:
|
|
| 206 |
"llm_model": self.model,
|
| 207 |
"num_segments": len(script),
|
| 208 |
"topic_focus": topic_focus,
|
| 209 |
-
"generated_at": datetime.
|
| 210 |
},
|
| 211 |
}
|
| 212 |
|
|
@@ -455,7 +455,8 @@ IMPORTANT:
|
|
| 455 |
) -> List[str]:
|
| 456 |
"""Synthesize each script segment to audio."""
|
| 457 |
|
| 458 |
-
|
|
|
|
| 459 |
audio_dir.mkdir(parents=True, exist_ok=True)
|
| 460 |
|
| 461 |
voice_maps: Dict[str, Dict[str, str]] = {
|
|
@@ -532,8 +533,9 @@ IMPORTANT:
|
|
| 532 |
print(f" ⚠️ Error processing segment {i}: {e}")
|
| 533 |
continue
|
| 534 |
|
| 535 |
-
|
| 536 |
-
|
|
|
|
| 537 |
final_path = str(output_dir / f"podcast_{timestamp}.mp3")
|
| 538 |
|
| 539 |
combined.export(final_path, format="mp3")
|
|
@@ -550,12 +552,11 @@ IMPORTANT:
|
|
| 550 |
notebook_id: str,
|
| 551 |
) -> str:
|
| 552 |
"""Save podcast transcript Markdown to file."""
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
)
|
| 556 |
transcript_dir.mkdir(parents=True, exist_ok=True)
|
| 557 |
|
| 558 |
-
timestamp = datetime.
|
| 559 |
filename = f"transcript_{timestamp}.md"
|
| 560 |
filepath = transcript_dir / filename
|
| 561 |
|
|
|
|
| 6 |
import json
|
| 7 |
import os
|
| 8 |
import re
|
| 9 |
+
from datetime import datetime, timezone
|
| 10 |
from pathlib import Path
|
| 11 |
from typing import Any, Dict, List, Optional
|
| 12 |
|
|
|
|
| 166 |
"num_segments": len(script),
|
| 167 |
"topic_focus": topic_focus,
|
| 168 |
"tts_errors": self._last_tts_errors[:20],
|
| 169 |
+
"generated_at": datetime.now(timezone.utc).isoformat(),
|
| 170 |
},
|
| 171 |
}
|
| 172 |
|
|
|
|
| 190 |
"llm_model": self.model,
|
| 191 |
"num_segments": len(script),
|
| 192 |
"topic_focus": topic_focus,
|
| 193 |
+
"generated_at": datetime.now(timezone.utc).isoformat(),
|
| 194 |
},
|
| 195 |
}
|
| 196 |
|
|
|
|
| 206 |
"llm_model": self.model,
|
| 207 |
"num_segments": len(script),
|
| 208 |
"topic_focus": topic_focus,
|
| 209 |
+
"generated_at": datetime.now(timezone.utc).isoformat(),
|
| 210 |
},
|
| 211 |
}
|
| 212 |
|
|
|
|
| 455 |
) -> List[str]:
|
| 456 |
"""Synthesize each script segment to audio."""
|
| 457 |
|
| 458 |
+
storage_base = os.getenv("STORAGE_BASE_DIR", "data")
|
| 459 |
+
audio_dir = Path(storage_base) / "users" / user_id / "notebooks" / notebook_id / "artifacts" / "podcasts"
|
| 460 |
audio_dir.mkdir(parents=True, exist_ok=True)
|
| 461 |
|
| 462 |
voice_maps: Dict[str, Dict[str, str]] = {
|
|
|
|
| 533 |
print(f" ⚠️ Error processing segment {i}: {e}")
|
| 534 |
continue
|
| 535 |
|
| 536 |
+
storage_base = os.getenv("STORAGE_BASE_DIR", "data")
|
| 537 |
+
output_dir = Path(storage_base) / "users" / user_id / "notebooks" / notebook_id / "artifacts" / "podcasts"
|
| 538 |
+
timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
|
| 539 |
final_path = str(output_dir / f"podcast_{timestamp}.mp3")
|
| 540 |
|
| 541 |
combined.export(final_path, format="mp3")
|
|
|
|
| 552 |
notebook_id: str,
|
| 553 |
) -> str:
|
| 554 |
"""Save podcast transcript Markdown to file."""
|
| 555 |
+
storage_base = os.getenv("STORAGE_BASE_DIR", "data")
|
| 556 |
+
transcript_dir = Path(storage_base) / "users" / user_id / "notebooks" / notebook_id / "artifacts" / "podcasts"
|
|
|
|
| 557 |
transcript_dir.mkdir(parents=True, exist_ok=True)
|
| 558 |
|
| 559 |
+
timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
|
| 560 |
filename = f"transcript_{timestamp}.md"
|
| 561 |
filepath = transcript_dir / filename
|
| 562 |
|
src/artifacts/quiz_generator.py
CHANGED
|
@@ -6,7 +6,7 @@ from __future__ import annotations
|
|
| 6 |
import json
|
| 7 |
import os
|
| 8 |
import re
|
| 9 |
-
from datetime import datetime
|
| 10 |
from pathlib import Path
|
| 11 |
from typing import Any, Dict, List, Optional
|
| 12 |
|
|
@@ -135,7 +135,7 @@ class QuizGenerator:
|
|
| 135 |
"topic_focus": topic_focus,
|
| 136 |
"llm_provider": self.llm_provider,
|
| 137 |
"llm_model": self.model,
|
| 138 |
-
"generated_at": datetime.
|
| 139 |
},
|
| 140 |
}
|
| 141 |
|
|
@@ -149,7 +149,7 @@ class QuizGenerator:
|
|
| 149 |
"topic_focus": topic_focus,
|
| 150 |
"llm_provider": self.llm_provider,
|
| 151 |
"llm_model": self.model,
|
| 152 |
-
"generated_at": datetime.
|
| 153 |
},
|
| 154 |
}
|
| 155 |
|
|
@@ -466,10 +466,11 @@ Requirements:
|
|
| 466 |
|
| 467 |
def save_quiz(self, quiz_markdown: str, user_id: str, notebook_id: str) -> str:
|
| 468 |
"""Save generated quiz Markdown to file."""
|
| 469 |
-
|
|
|
|
| 470 |
quiz_dir.mkdir(parents=True, exist_ok=True)
|
| 471 |
|
| 472 |
-
timestamp = datetime.
|
| 473 |
filename = f"quiz_{timestamp}.md"
|
| 474 |
filepath = quiz_dir / filename
|
| 475 |
|
|
|
|
| 6 |
import json
|
| 7 |
import os
|
| 8 |
import re
|
| 9 |
+
from datetime import datetime, timezone
|
| 10 |
from pathlib import Path
|
| 11 |
from typing import Any, Dict, List, Optional
|
| 12 |
|
|
|
|
| 135 |
"topic_focus": topic_focus,
|
| 136 |
"llm_provider": self.llm_provider,
|
| 137 |
"llm_model": self.model,
|
| 138 |
+
"generated_at": datetime.now(timezone.utc).isoformat(),
|
| 139 |
},
|
| 140 |
}
|
| 141 |
|
|
|
|
| 149 |
"topic_focus": topic_focus,
|
| 150 |
"llm_provider": self.llm_provider,
|
| 151 |
"llm_model": self.model,
|
| 152 |
+
"generated_at": datetime.now(timezone.utc).isoformat(),
|
| 153 |
},
|
| 154 |
}
|
| 155 |
|
|
|
|
| 466 |
|
| 467 |
def save_quiz(self, quiz_markdown: str, user_id: str, notebook_id: str) -> str:
|
| 468 |
"""Save generated quiz Markdown to file."""
|
| 469 |
+
storage_base = os.getenv("STORAGE_BASE_DIR", "data")
|
| 470 |
+
quiz_dir = Path(storage_base) / "users" / user_id / "notebooks" / notebook_id / "artifacts" / "quizzes"
|
| 471 |
quiz_dir.mkdir(parents=True, exist_ok=True)
|
| 472 |
|
| 473 |
+
timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
|
| 474 |
filename = f"quiz_{timestamp}.md"
|
| 475 |
filepath = quiz_dir / filename
|
| 476 |
|
src/artifacts/report_generator.py
CHANGED
|
@@ -4,7 +4,7 @@ Report generator using RAG context from ingested notebook content.
|
|
| 4 |
from __future__ import annotations
|
| 5 |
|
| 6 |
import os
|
| 7 |
-
from datetime import datetime
|
| 8 |
from pathlib import Path
|
| 9 |
from typing import Optional
|
| 10 |
|
|
@@ -205,10 +205,11 @@ Requirements:
|
|
| 205 |
return str(body.get("response", "")).strip()
|
| 206 |
|
| 207 |
def save_report(self, markdown_text: str, user_id: str, notebook_id: str) -> str:
|
| 208 |
-
|
|
|
|
| 209 |
report_dir.mkdir(parents=True, exist_ok=True)
|
| 210 |
|
| 211 |
-
timestamp = datetime.
|
| 212 |
path = report_dir / f"report_{timestamp}.md"
|
| 213 |
path.write_text(markdown_text, encoding="utf-8")
|
| 214 |
return str(path)
|
|
|
|
| 4 |
from __future__ import annotations
|
| 5 |
|
| 6 |
import os
|
| 7 |
+
from datetime import datetime, timezone
|
| 8 |
from pathlib import Path
|
| 9 |
from typing import Optional
|
| 10 |
|
|
|
|
| 205 |
return str(body.get("response", "")).strip()
|
| 206 |
|
| 207 |
def save_report(self, markdown_text: str, user_id: str, notebook_id: str) -> str:
|
| 208 |
+
storage_base = os.getenv("STORAGE_BASE_DIR", "data")
|
| 209 |
+
report_dir = Path(storage_base) / "users" / user_id / "notebooks" / notebook_id / "artifacts" / "reports"
|
| 210 |
report_dir.mkdir(parents=True, exist_ok=True)
|
| 211 |
|
| 212 |
+
timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
|
| 213 |
path = report_dir / f"report_{timestamp}.md"
|
| 214 |
path.write_text(markdown_text, encoding="utf-8")
|
| 215 |
return str(path)
|
src/ingestion/service.py
CHANGED
|
@@ -22,6 +22,18 @@ def _coerce_user_notebook_ids(owner_user_id: int, notebook_id: int) -> tuple[str
|
|
| 22 |
return str(owner_user_id), str(notebook_id)
|
| 23 |
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
def _extract_source_text(source: Source) -> str:
|
| 26 |
if source.type == "url" and source.url:
|
| 27 |
return extract_text_from_url(source.url).get("text", "")
|
|
@@ -97,10 +109,7 @@ def query_notebook_chunks(
|
|
| 97 |
query_text=query_text,
|
| 98 |
top_k=top_k,
|
| 99 |
source_id=(str(source_id) if source_id is not None else None),
|
| 100 |
-
query_embedding=
|
| 101 |
-
model_name=os.getenv("EMBEDDING_MODEL", "all-MiniLM-L6-v2"),
|
| 102 |
-
provider=os.getenv("EMBEDDING_PROVIDER", "local"),
|
| 103 |
-
).embed_texts([query_text], batch_size=1)[0],
|
| 104 |
)
|
| 105 |
return [
|
| 106 |
{
|
|
|
|
| 22 |
return str(owner_user_id), str(notebook_id)
|
| 23 |
|
| 24 |
|
| 25 |
+
_embedding_adapter_cache: dict[tuple[str, str], EmbeddingAdapter] = {}
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def _get_embedding_adapter() -> EmbeddingAdapter:
|
| 29 |
+
model = os.getenv("EMBEDDING_MODEL", "all-MiniLM-L6-v2")
|
| 30 |
+
provider = os.getenv("EMBEDDING_PROVIDER", "local")
|
| 31 |
+
key = (model, provider)
|
| 32 |
+
if key not in _embedding_adapter_cache:
|
| 33 |
+
_embedding_adapter_cache[key] = EmbeddingAdapter(model_name=model, provider=provider)
|
| 34 |
+
return _embedding_adapter_cache[key]
|
| 35 |
+
|
| 36 |
+
|
| 37 |
def _extract_source_text(source: Source) -> str:
|
| 38 |
if source.type == "url" and source.url:
|
| 39 |
return extract_text_from_url(source.url).get("text", "")
|
|
|
|
| 109 |
query_text=query_text,
|
| 110 |
top_k=top_k,
|
| 111 |
source_id=(str(source_id) if source_id is not None else None),
|
| 112 |
+
query_embedding=_get_embedding_adapter().embed_texts([query_text], batch_size=1)[0],
|
|
|
|
|
|
|
|
|
|
| 113 |
)
|
| 114 |
return [
|
| 115 |
{
|
utils/llm_client.py
CHANGED
|
@@ -7,24 +7,23 @@ from groq import Groq
|
|
| 7 |
|
| 8 |
load_dotenv()
|
| 9 |
|
| 10 |
-
_GROQ_API_KEY = os.getenv("GROQ_API_KEY")
|
| 11 |
-
_GROQ_MODEL = os.getenv("GROQ_MODEL", "llama-3.1-8b-instant")
|
| 12 |
-
|
| 13 |
|
| 14 |
class LLMConfigError(RuntimeError):
|
| 15 |
pass
|
| 16 |
|
| 17 |
|
| 18 |
def _get_client() -> Groq:
|
| 19 |
-
|
|
|
|
| 20 |
raise LLMConfigError("GROQ_API_KEY is not set.")
|
| 21 |
-
return Groq(api_key=
|
| 22 |
|
| 23 |
|
| 24 |
def generate_chat_completion(system_prompt: str, user_prompt: str) -> str:
|
| 25 |
client = _get_client()
|
|
|
|
| 26 |
response = client.chat.completions.create(
|
| 27 |
-
model=
|
| 28 |
messages=[
|
| 29 |
{"role": "system", "content": system_prompt},
|
| 30 |
{"role": "user", "content": user_prompt},
|
|
|
|
| 7 |
|
| 8 |
load_dotenv()
|
| 9 |
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
class LLMConfigError(RuntimeError):
|
| 12 |
pass
|
| 13 |
|
| 14 |
|
| 15 |
def _get_client() -> Groq:
|
| 16 |
+
api_key = os.getenv("GROQ_API_KEY")
|
| 17 |
+
if not api_key:
|
| 18 |
raise LLMConfigError("GROQ_API_KEY is not set.")
|
| 19 |
+
return Groq(api_key=api_key)
|
| 20 |
|
| 21 |
|
| 22 |
def generate_chat_completion(system_prompt: str, user_prompt: str) -> str:
|
| 23 |
client = _get_client()
|
| 24 |
+
model = os.getenv("GROQ_MODEL", "llama-3.1-8b-instant")
|
| 25 |
response = client.chat.completions.create(
|
| 26 |
+
model=model,
|
| 27 |
messages=[
|
| 28 |
{"role": "system", "content": system_prompt},
|
| 29 |
{"role": "user", "content": user_prompt},
|