Skip to content

Commit 6935a33

Browse files
committed
Design adjustments, change password
Signed-off-by: Fabian Wolf <fabian@fawolf.de>
1 parent c9c1994 commit 6935a33

31 files changed

+3195
-2793
lines changed

backend/src/main.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,7 @@ async def root():
4646
return {"message": "Hello, hello!"}
4747

4848

49-
@app.get("/version")
50-
async def version():
51-
return {"version": __version__, "description": "LLMAIx (v2) backend API"}
52-
53-
5449
@app.get("/api/v1/version")
50+
@app.get("/version")
5551
async def version_api():
5652
return {"version": __version__, "description": "LLMAIx (v2) backend API"}

backend/src/models/project.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,8 @@ class TrialStatus(str, enum.Enum):
330330
class Trial(Base):
331331
__tablename__ = "trials"
332332
id: Mapped[int] = mapped_column(primary_key=True)
333+
name: Mapped[str] = mapped_column(String(100), nullable=True)
334+
description: Mapped[str] = mapped_column(String(512), nullable=True)
333335
project_id: Mapped[int] = mapped_column(ForeignKey("projects.id"), nullable=False)
334336
schema_id: Mapped[int] = mapped_column(ForeignKey("schemas.id"), nullable=False)
335337
prompt_id: Mapped[str] = mapped_column(ForeignKey("prompts.id"), nullable=False)

backend/src/routers/v1/endpoints/auth.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import asyncio
12
from datetime import timedelta
3+
import time
24

35
from fastapi import APIRouter, Depends, HTTPException, status
46
from fastapi.security import OAuth2PasswordRequestForm
@@ -11,21 +13,30 @@
1113

1214
router = APIRouter()
1315

16+
@router.get("/settings")
17+
def get_settings():
18+
return {
19+
"require_invitation": settings.REQUIRE_INVITATION
20+
}
21+
1422

1523
@router.post("/login", response_model=schemas.Token)
24+
@router.post("/api/v1/login", response_model=schemas.Token)
1625
def login(
1726
db: Session = Depends(get_db),
1827
form_data: OAuth2PasswordRequestForm = Depends(),
1928
) -> schemas.Token:
2029
"""OAuth2 compatible token login, get an access token for future requests."""
2130
user = db.query(models.User).filter(models.User.email == form_data.username).first()
22-
if not user or not verify_password(form_data.password, str(user.hashed_password)):
31+
if not user or not verify_password(form_data.password, str(user.hashed_password)):#
32+
time.sleep(0.5)
2333
raise HTTPException(
2434
status_code=status.HTTP_401_UNAUTHORIZED,
2535
detail="Incorrect email or password",
2636
headers={"WWW-Authenticate": "Bearer"},
2737
)
2838
elif not user.is_active:
39+
time.sleep(0.5)
2940
raise HTTPException(
3041
status_code=status.HTTP_401_UNAUTHORIZED, detail="Inactive user"
3142
)

backend/src/routers/v1/endpoints/projects.py

Lines changed: 116 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from fastapi.responses import Response, StreamingResponse
2020
from pydantic import ValidationError
2121
from sqlalchemy import and_, delete, distinct, func, or_, select
22+
from sqlalchemy.exc import SQLAlchemyError
2223
from sqlalchemy.orm import Session, selectinload
2324
from starlette import status
2425
from thefuzz import fuzz
@@ -1585,6 +1586,7 @@ def get_document(
15851586
return schemas.Document.model_validate(document)
15861587

15871588

1589+
15881590
@router.delete("/{project_id}/document/{document_id}")
15891591
def delete_document(
15901592
*,
@@ -1593,7 +1595,7 @@ def delete_document(
15931595
current_user: models.User = Depends(get_current_user),
15941596
db: Session = Depends(get_db),
15951597
) -> dict:
1596-
"""Delete a specific document."""
1598+
"""Delete a specific document, only if not used in any trial, trial result, or evaluation metric."""
15971599
check_project_access(project_id, current_user, db, "write")
15981600

15991601
document = db.execute(
@@ -1605,14 +1607,48 @@ def delete_document(
16051607
if not document:
16061608
raise HTTPException(status_code=404, detail="Document not found")
16071609

1608-
# Check if document is part of any document sets
1610+
# --- NEW: Check for usage in any trial (document_ids is a JSON list) ---
1611+
trials_with_doc = db.execute(
1612+
select(models.Trial)
1613+
.where(
1614+
models.Trial.project_id == project_id,
1615+
models.Trial.document_ids.contains([document_id]),
1616+
)
1617+
).scalars().first()
1618+
if trials_with_doc:
1619+
raise HTTPException(
1620+
status_code=400,
1621+
detail=f"Document is referenced in trial '{trials_with_doc.name or trials_with_doc.id}'. Remove from trial(s) first."
1622+
)
1623+
1624+
# --- Check if document is used in any trial results ---
1625+
trial_result = db.execute(
1626+
select(models.TrialResult).where(models.TrialResult.document_id == document_id)
1627+
).scalar_one_or_none()
1628+
if trial_result:
1629+
raise HTTPException(
1630+
status_code=400,
1631+
detail="Document is referenced in a trial result. Remove results/trials first."
1632+
)
1633+
1634+
# --- (Optional) Check if document is used in any evaluation metric ---
1635+
metric = db.execute(
1636+
select(models.EvaluationMetric).where(models.EvaluationMetric.document_id == document_id)
1637+
).scalar_one_or_none()
1638+
if metric:
1639+
raise HTTPException(
1640+
status_code=400,
1641+
detail="Document is referenced in evaluation metrics. Remove related evaluation/trial first."
1642+
)
1643+
1644+
# --- Existing check: Document sets ---
16091645
if document.document_sets:
16101646
raise HTTPException(
16111647
status_code=400,
16121648
detail=f"Document is part of {len(document.document_sets)} document sets. Remove from sets first.",
16131649
)
16141650

1615-
# Delete preprocessed file if exists and not used by other documents
1651+
# --- Preprocessed file deletion logic as before ---
16161652
if document.preprocessed_file_id:
16171653
other_docs_using_file = db.execute(
16181654
select(models.Document).where(
@@ -1622,16 +1658,13 @@ def delete_document(
16221658
).scalar_one_or_none()
16231659

16241660
if not other_docs_using_file:
1625-
# Safe to delete preprocessed file
16261661
preprocessed_file = db.get(models.File, document.preprocessed_file_id)
16271662
if preprocessed_file:
16281663
try:
16291664
from ....dependencies import remove_file
1630-
16311665
remove_file(preprocessed_file.file_uuid)
16321666
db.delete(preprocessed_file)
16331667
except Exception as e:
1634-
# Log error but continue
16351668
print(f"Error deleting preprocessed file: {e}")
16361669

16371670
db.delete(document)
@@ -2428,8 +2461,10 @@ def create_trial(
24282461

24292462
# 4. Create trial object
24302463
trial_db = models.Trial(
2464+
name=trial.name,
2465+
description=trial.description,
24312466
schema_id=trial.schema_id,
2432-
prompt_id=trial.prompt_id,
2467+
prompt_id=str(trial.prompt_id),
24332468
project_id=project_id,
24342469
llm_model=llm_model,
24352470
api_key=api_key,
@@ -2440,7 +2475,6 @@ def create_trial(
24402475
else None,
24412476
bypass_celery=trial.bypass_celery,
24422477
advanced_options=trial.advanced_options or {},
2443-
# Add other trial fields as needed...
24442478
)
24452479

24462480
db.add(trial_db)
@@ -2495,13 +2529,63 @@ def create_trial(
24952529
return schemas.Trial.model_validate(trial_db)
24962530

24972531

2532+
@router.patch("/{project_id}/trial/{trial_id}", response_model=schemas.Trial)
2533+
def update_trial(
2534+
project_id: int,
2535+
trial_id: int,
2536+
trial_update: schemas.TrialUpdate,
2537+
current_user: models.User = Depends(get_current_user),
2538+
db: Session = Depends(get_db),
2539+
):
2540+
# 1. Project existence and permission check
2541+
project = db.execute(
2542+
select(models.Project).where(models.Project.id == project_id)
2543+
).scalar_one_or_none()
2544+
if not project:
2545+
raise HTTPException(status_code=404, detail="Project not found")
2546+
if current_user.role != "admin" and project.owner_id != current_user.id:
2547+
raise HTTPException(
2548+
status_code=403,
2549+
detail="Not authorized to update trials for this project"
2550+
)
2551+
2552+
# 2. Trial existence check
2553+
trial = db.execute(
2554+
select(models.Trial).where(
2555+
models.Trial.project_id == project_id,
2556+
models.Trial.id == trial_id
2557+
)
2558+
).scalar_one_or_none()
2559+
if not trial:
2560+
raise HTTPException(status_code=404, detail="Trial not found")
2561+
2562+
# 3. Update allowed fields
2563+
updated = False
2564+
if trial_update.name is not None:
2565+
trial.name = trial_update.name
2566+
updated = True
2567+
if trial_update.description is not None:
2568+
trial.description = trial_update.description
2569+
updated = True
2570+
2571+
if not updated:
2572+
raise HTTPException(
2573+
status_code=400, detail="No updatable fields provided"
2574+
)
2575+
2576+
db.commit()
2577+
db.refresh(trial)
2578+
return schemas.Trial.model_validate(trial)
2579+
2580+
24982581
@router.delete("/{project_id}/trial/{trial_id}", response_model=schemas.Trial)
24992582
def delete_trial(
25002583
project_id: int,
25012584
trial_id: int,
25022585
current_user: models.User = Depends(get_current_user),
25032586
db: Session = Depends(get_db),
25042587
) -> schemas.Trial:
2588+
# Project and permission checks
25052589
project: models.Project | None = db.execute(
25062590
select(models.Project).where(models.Project.id == project_id)
25072591
).scalar_one_or_none()
@@ -2512,32 +2596,41 @@ def delete_trial(
25122596
status_code=403, detail="Not authorized to delete trials for this project"
25132597
)
25142598

2599+
# Fetch the trial
25152600
trial: models.Trial | None = db.execute(
25162601
select(models.Trial).where(
25172602
models.Trial.project_id == project_id, models.Trial.id == trial_id
25182603
)
25192604
).scalar_one_or_none()
2520-
25212605
if not trial:
25222606
raise HTTPException(status_code=404, detail="Trial not found")
25232607

2524-
# Check if the trial has results
2525-
results = (
2526-
db.execute(
2608+
# --- SERIALIZE before delete ---
2609+
trial_data = schemas.Trial.model_validate(trial)
2610+
2611+
try:
2612+
# Delete all evaluations (and their metrics, via cascade)
2613+
evaluations = db.execute(
2614+
select(models.Evaluation).where(models.Evaluation.trial_id == trial_id)
2615+
).scalars().all()
2616+
for evaluation in evaluations:
2617+
db.delete(evaluation)
2618+
2619+
# Delete all trial results for this trial
2620+
results = db.execute(
25272621
select(models.TrialResult).where(models.TrialResult.trial_id == trial_id)
2528-
)
2529-
.scalars()
2530-
.all()
2531-
)
2532-
if results:
2533-
raise HTTPException(
2534-
status_code=400, detail="Cannot delete trial with existing results"
2535-
)
2622+
).scalars().all()
2623+
for result in results:
2624+
db.delete(result)
25362625

2537-
db.delete(trial)
2538-
db.commit()
2626+
# Delete the trial itself
2627+
db.delete(trial)
2628+
db.commit()
2629+
except SQLAlchemyError as e:
2630+
db.rollback()
2631+
raise HTTPException(status_code=500, detail=f"Database error during deletion: {e}")
25392632

2540-
return schemas.Trial.model_validate(trial)
2633+
return trial_data
25412634

25422635

25432636
@router.get("/{project_id}/trial", response_model=list[schemas.Trial])

0 commit comments

Comments
 (0)