-
Couldn't load subscription status.
- Fork 5
Classification: Merging endpoints #391
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c939d3a
1482a0c
c3364d5
ebcd9a0
00b415f
a2ef005
f8e28e9
397807d
ca862cf
375eb5e
14b7db5
38dcf45
8a1b496
9e8d046
dc7a3ce
724497b
3bcd772
b6a7073
1bbd0dd
c47b254
a5323a1
0979fd8
9f0f26c
c04275b
8801b39
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,17 +1,21 @@ | ||
| from typing import Optional | ||
| import logging | ||
| import time | ||
| from uuid import UUID | ||
| from uuid import UUID, uuid4 | ||
| from pathlib import Path | ||
|
|
||
| import openai | ||
| from sqlmodel import Session | ||
| from fastapi import APIRouter, HTTPException, BackgroundTasks | ||
| from fastapi import APIRouter, HTTPException, BackgroundTasks, File, Form, UploadFile | ||
|
|
||
| from app.models import ( | ||
| FineTuningJobCreate, | ||
| FineTuningJobPublic, | ||
| FineTuningUpdate, | ||
| FineTuningStatus, | ||
| Document, | ||
| ModelEvaluationBase, | ||
| ModelEvaluationStatus, | ||
| ) | ||
| from app.core.cloud import get_cloud_storage | ||
| from app.crud.document import DocumentCrud | ||
|
|
@@ -21,10 +25,13 @@ | |
| fetch_by_id, | ||
| update_finetune_job, | ||
| fetch_by_document_id, | ||
| create_model_evaluation, | ||
| fetch_active_model_evals, | ||
| ) | ||
| from app.core.db import engine | ||
| from app.api.deps import CurrentUserOrgProject, SessionDep | ||
| from app.core.finetune.preprocessing import DataPreprocessor | ||
| from app.api.routes.model_evaluation import run_model_evaluation | ||
|
|
||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
@@ -38,16 +45,10 @@ | |
| "running": FineTuningStatus.running, | ||
| "succeeded": FineTuningStatus.completed, | ||
| "failed": FineTuningStatus.failed, | ||
| "cancelled": FineTuningStatus.cancelled, | ||
| } | ||
|
|
||
|
|
||
| def handle_openai_error(e: openai.OpenAIError) -> str: | ||
| """Extract error message from OpenAI error.""" | ||
| if isinstance(e.body, dict) and "message" in e.body: | ||
| return e.body["message"] | ||
| return str(e) | ||
|
|
||
|
|
||
| def process_fine_tuning_job( | ||
| job_id: int, | ||
| ratio: float, | ||
|
|
@@ -179,22 +180,58 @@ def process_fine_tuning_job( | |
| description=load_description("fine_tuning/create.md"), | ||
| response_model=APIResponse, | ||
| ) | ||
| def fine_tune_from_CSV( | ||
| async def fine_tune_from_CSV( | ||
| session: SessionDep, | ||
| current_user: CurrentUserOrgProject, | ||
| request: FineTuningJobCreate, | ||
| background_tasks: BackgroundTasks, | ||
| file: UploadFile = File(..., description="CSV file to use for fine-tuning"), | ||
| base_model: str = Form(...), | ||
| split_ratio: str = Form(...), | ||
| system_prompt: str = Form(...), | ||
| ): | ||
| client = get_openai_client( # Used here only to validate the user's OpenAI key; | ||
| # Parse split ratios | ||
| try: | ||
| split_ratios = [float(r.strip()) for r in split_ratio.split(",")] | ||
| except ValueError as e: | ||
| raise HTTPException(status_code=400, detail=f"Invalid split_ratio format: {e}") | ||
|
|
||
| # Validate file is CSV | ||
| if not file.filename.lower().endswith(".csv") and file.content_type != "text/csv": | ||
| raise HTTPException(status_code=400, detail="File must be a CSV file") | ||
|
|
||
| get_openai_client( # Used here only to validate the user's OpenAI key; | ||
| # the actual client is re-initialized separately inside the background task | ||
| session, | ||
| current_user.organization_id, | ||
| current_user.project_id, | ||
| ) | ||
|
|
||
| # Upload the file to storage and create document | ||
| # ToDo: create a helper function and then use it rather than doing things in router | ||
| storage = get_cloud_storage(session=session, project_id=current_user.project_id) | ||
| document_id = uuid4() | ||
nishika26 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| object_store_url = storage.put(file, Path(str(document_id))) | ||
|
|
||
| # Create document in database | ||
| document_crud = DocumentCrud(session, current_user.project_id) | ||
| document = Document( | ||
| id=document_id, | ||
| fname=file.filename, | ||
| object_store_url=str(object_store_url), | ||
| ) | ||
| created_document = document_crud.update(document) | ||
|
|
||
| # Create FineTuningJobCreate request object | ||
| request = FineTuningJobCreate( | ||
| document_id=created_document.id, | ||
| base_model=base_model, | ||
| split_ratio=split_ratios, | ||
| system_prompt=system_prompt.strip(), | ||
| ) | ||
|
|
||
| results = [] | ||
|
|
||
| for ratio in request.split_ratio: | ||
| for ratio in split_ratios: | ||
| job, created = create_fine_tuning_job( | ||
| session=session, | ||
| request=request, | ||
|
|
@@ -246,7 +283,10 @@ def fine_tune_from_CSV( | |
| response_model=APIResponse[FineTuningJobPublic], | ||
| ) | ||
| def refresh_fine_tune_status( | ||
| fine_tuning_id: int, session: SessionDep, current_user: CurrentUserOrgProject | ||
| fine_tuning_id: int, | ||
| background_tasks: BackgroundTasks, | ||
| session: SessionDep, | ||
| current_user: CurrentUserOrgProject, | ||
| ): | ||
| project_id = current_user.project_id | ||
| job = fetch_by_id(session, fine_tuning_id, project_id) | ||
|
|
@@ -282,13 +322,56 @@ def refresh_fine_tune_status( | |
| error_message=openai_error_msg, | ||
| ) | ||
|
|
||
| # Check if status is changing from running to completed | ||
| is_newly_completed = ( | ||
| job.status == FineTuningStatus.running | ||
| and update_payload.status == FineTuningStatus.completed | ||
| ) | ||
|
|
||
|
Comment on lines
+325
to
+330
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Race: auto-evaluation can be created twice under concurrent refresh calls Two concurrent requests can both observe “no active evals” and create duplicates. Add a DB-level guard (unique constraint/partial index) or a transactional re-check/catch on insert.
I can provide a concrete migration + guarded create if desired. Also applies to: 343-369 🤖 Prompt for AI Agents |
||
| if ( | ||
| job.status != update_payload.status | ||
| or job.fine_tuned_model != update_payload.fine_tuned_model | ||
| or job.error_message != update_payload.error_message | ||
| ): | ||
| job = update_finetune_job(session=session, job=job, update=update_payload) | ||
|
|
||
| # If the job just completed, automatically trigger evaluation | ||
| if is_newly_completed: | ||
| logger.info( | ||
| f"[refresh_fine_tune_status] Fine-tuning job completed, triggering evaluation | " | ||
| f"fine_tuning_id={fine_tuning_id}, project_id={project_id}" | ||
| ) | ||
|
|
||
| # Check if there's already an active evaluation for this job | ||
| active_evaluations = fetch_active_model_evals( | ||
| session, fine_tuning_id, project_id | ||
| ) | ||
|
|
||
| if not active_evaluations: | ||
| # Create a new evaluation | ||
| model_eval = create_model_evaluation( | ||
| session=session, | ||
| request=ModelEvaluationBase(fine_tuning_id=fine_tuning_id), | ||
| project_id=project_id, | ||
| organization_id=current_user.organization_id, | ||
| status=ModelEvaluationStatus.pending, | ||
| ) | ||
|
|
||
| # Queue the evaluation task | ||
| background_tasks.add_task( | ||
| run_model_evaluation, model_eval.id, current_user | ||
| ) | ||
|
|
||
| logger.info( | ||
| f"[refresh_fine_tune_status] Created and queued evaluation | " | ||
| f"eval_id={model_eval.id}, fine_tuning_id={fine_tuning_id}, project_id={project_id}" | ||
| ) | ||
| else: | ||
| logger.info( | ||
| f"[refresh_fine_tune_status] Skipping evaluation creation - active evaluation exists | " | ||
| f"fine_tuning_id={fine_tuning_id}, project_id={project_id}" | ||
| ) | ||
|
|
||
| job = job.model_copy( | ||
| update={ | ||
| "train_data_file_url": storage.get_signed_url(job.train_data_s3_object) | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.