1919from fastapi .responses import Response , StreamingResponse
2020from pydantic import ValidationError
2121from sqlalchemy import and_ , delete , distinct , func , or_ , select
22+ from sqlalchemy .exc import SQLAlchemyError
2223from sqlalchemy .orm import Session , selectinload
2324from starlette import status
2425from 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}" )
15891591def 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 )
24992582def 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