Skip to content

Commit 89bcae3

Browse files
committed
Move tasks and make them experiment scoped
1 parent bd89ad5 commit 89bcae3

9 files changed

Lines changed: 468 additions & 138 deletions

File tree

api/api.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,10 @@ async def lifespan(app: FastAPI):
123123
# One-time migration: legacy workspace/jobs -> workspace/experiments/<exp_id>/jobs
124124
# Runs in the background so it doesn't delay the API startup.
125125
from transformerlab.services.migrate_jobs_to_experiment_dirs import start_jobs_migration_worker
126+
from transformerlab.services.migrate_tasks_to_experiment_dirs import start_tasks_migration_worker
126127

127128
await start_jobs_migration_worker()
129+
await start_tasks_migration_worker()
128130

129131
# Start background sweep status updater after all startup steps succeed.
130132
await start_sweep_status_worker()
@@ -152,11 +154,13 @@ async def lifespan(app: FastAPI):
152154
# Do the following at API Shutdown:
153155
if is_leader():
154156
from transformerlab.services.migrate_jobs_to_experiment_dirs import stop_jobs_migration_worker
157+
from transformerlab.services.migrate_tasks_to_experiment_dirs import stop_tasks_migration_worker
155158

156159
await stop_sweep_status_worker()
157160
await stop_remote_job_status_worker()
158161
await stop_notification_worker()
159162
await stop_remote_job_queue_worker()
163+
await stop_tasks_migration_worker()
160164
await stop_jobs_migration_worker()
161165
from transformerlab.services.process_registry import get_registry
162166

api/transformerlab/routers/experiment/task.py

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ async def task_get_all(experimentId: str):
117117
tags=["tasks:{experimentId}"],
118118
)
119119
async def task_get_by_id(experimentId: str, task_id: str):
120-
task = await task_service.task_get_by_id(task_id)
120+
task = await task_service.task_get_by_id(task_id, experiment_id=experimentId)
121121
if task is None:
122122
return {"message": "NOT FOUND"}
123123
return task
@@ -153,7 +153,7 @@ async def task_get_by_type_in_experiment(experimentId: str, type: str):
153153
response_model=TaskFilesResponse,
154154
summary="List files associated with a task template (GitHub + local mounts)",
155155
)
156-
async def task_list_files(task_id: str) -> TaskFilesResponse:
156+
async def task_list_files(experimentId: str, task_id: str) -> TaskFilesResponse:
157157
"""
158158
Return a lightweight list of files associated with a task template.
159159
@@ -163,7 +163,7 @@ async def task_list_files(task_id: str) -> TaskFilesResponse:
163163
readily available.
164164
- If file_mounts is set, it will be returned as-is as a list of local paths.
165165
"""
166-
task = await task_service.task_get_by_id(task_id)
166+
task = await task_service.task_get_by_id(task_id, experiment_id=experimentId)
167167
if not task:
168168
raise HTTPException(status_code=404, detail="Task not found")
169169

@@ -207,7 +207,7 @@ async def task_list_files(task_id: str) -> TaskFilesResponse:
207207
# Always list files from the canonical per-task directory.
208208
# This directory contains at minimum task.yaml and may include uploaded files.
209209
try:
210-
task_dir = await task_service.get_task_dir(task_id)
210+
task_dir = await task_service.get_task_dir(task_id, experiment_id=experimentId)
211211
if await storage.exists(task_dir):
212212
entries = await storage.ls(task_dir)
213213
# Build a set of basenames already in local_files for dedup.
@@ -353,7 +353,7 @@ async def task_update_file(experimentId: str, task_id: str, file_path: str, requ
353353
.py, .md, .yaml/.yml, .json, .txt). Request bodies are decoded as UTF-8 and
354354
written in text mode. Binary uploads should use the file-upload endpoint.
355355
"""
356-
task = await task_service.task_get_by_id(task_id)
356+
task = await task_service.task_get_by_id(task_id, experiment_id=experimentId)
357357
if not task:
358358
raise HTTPException(status_code=404, detail="Task not found")
359359

@@ -376,7 +376,7 @@ async def task_update_file(experimentId: str, task_id: str, file_path: str, requ
376376
await f.write(body)
377377

378378
if not task.get("file_mounts"):
379-
await task_service.update_task(task_id, {"file_mounts": True})
379+
await task_service.update_task(task_id, {"file_mounts": True}, experiment_id=experimentId)
380380

381381
await cache.invalidate(f"tasks:{experimentId}")
382382
return {"message": "OK"}
@@ -387,7 +387,7 @@ async def task_update_file(experimentId: str, task_id: str, file_path: str, requ
387387
summary="Delete a file from a task's local workspace directory",
388388
)
389389
async def task_delete_file(experimentId: str, task_id: str, file_path: str):
390-
task = await task_service.task_get_by_id(task_id)
390+
task = await task_service.task_get_by_id(task_id, experiment_id=experimentId)
391391
if not task:
392392
raise HTTPException(status_code=404, detail="Task not found")
393393

@@ -421,7 +421,7 @@ async def task_upload_file(
421421
task_id: str,
422422
files: list[UploadFile] = File(...),
423423
):
424-
task = await task_service.task_get_by_id(task_id)
424+
task = await task_service.task_get_by_id(task_id, experiment_id=experimentId)
425425
if not task:
426426
raise HTTPException(status_code=404, detail="Task not found")
427427

@@ -450,7 +450,7 @@ async def task_upload_file(
450450
raise HTTPException(status_code=400, detail="No valid files uploaded")
451451

452452
if not task.get("file_mounts"):
453-
await task_service.update_task(task_id, {"file_mounts": True})
453+
await task_service.update_task(task_id, {"file_mounts": True}, experiment_id=experimentId)
454454

455455
await cache.invalidate(f"tasks:{experimentId}")
456456
return {"status": "success", "files": saved_files}
@@ -460,14 +460,14 @@ async def task_upload_file(
460460
"/{task_id}/github_file/{file_path:path}",
461461
summary="Serve a file from the task's associated GitHub repository for preview",
462462
)
463-
async def task_get_github_file(task_id: str, file_path: str):
463+
async def task_get_github_file(experimentId: str, task_id: str, file_path: str):
464464
"""
465465
Serve a file from the GitHub repository configured on the task (github_repo_url).
466466
467467
This endpoint uses the same GitHub PAT resolution logic as other GitHub helpers
468468
and is intended for lightweight previews in the UI.
469469
"""
470-
task = await task_service.task_get_by_id(task_id)
470+
task = await task_service.task_get_by_id(task_id, experiment_id=experimentId)
471471
if not task:
472472
raise HTTPException(status_code=404, detail="Task not found")
473473

@@ -583,7 +583,7 @@ async def update_task(experimentId: str, task_id: str, new_task: dict = Body()):
583583
if "name" in new_task:
584584
new_task["name"] = secure_filename(new_task["name"])
585585

586-
success = await task_service.update_task(task_id, new_task)
586+
success = await task_service.update_task(task_id, new_task, experiment_id=experimentId)
587587
if success:
588588
# Best-effort invalidation: task detail + this experiment's task lists.
589589
await cache.invalidate(f"tasks:{experimentId}")
@@ -594,7 +594,7 @@ async def update_task(experimentId: str, task_id: str, new_task: dict = Body()):
594594

595595
@router.get("/{task_id}/delete", summary="Deletes a task")
596596
async def delete_task(experimentId: str, task_id: str):
597-
success = await task_service.delete_task(task_id)
597+
success = await task_service.delete_task(task_id, experiment_id=experimentId)
598598
if success:
599599
# Best-effort invalidation: task detail + this experiment's task lists.
600600
await cache.invalidate(f"tasks:{experimentId}")
@@ -899,23 +899,23 @@ async def create_task(
899899

900900
@router.get("/{task_id}/yaml", summary="Get task.yaml from the task directory")
901901
async def get_task_yaml(experimentId: str, task_id: str):
902-
task = await task_service.task_get_by_id(task_id)
902+
task = await task_service.task_get_by_id(task_id, experiment_id=experimentId)
903903
if task is None:
904904
raise HTTPException(status_code=404, detail="Task not found")
905-
content = await task_service.read_task_yaml(task_id)
905+
content = await task_service.read_task_yaml(task_id, experiment_id=experimentId)
906906
return PlainTextResponse(content, media_type="text/plain")
907907

908908

909909
@router.put("/{task_id}/yaml", summary="Save task.yaml and sync index.json")
910910
async def update_task_yaml(experimentId: str, task_id: str, request: Request):
911911
body = (await request.body()).decode("utf-8")
912-
task = await task_service.task_get_by_id(task_id)
912+
task = await task_service.task_get_by_id(task_id, experiment_id=experimentId)
913913
if task is None:
914914
raise HTTPException(status_code=404, detail="Task not found")
915-
await task_service.write_task_yaml(task_id, body)
915+
await task_service.write_task_yaml(task_id, body, experiment_id=experimentId)
916916
task_data = _parse_yaml_to_task_data(body)
917917
task_data.pop("id", None)
918-
success = await task_service.update_task_from_yaml(task_id, task_data)
918+
success = await task_service.update_task_from_yaml(task_id, task_data, experiment_id=experimentId)
919919
if not success:
920920
raise HTTPException(status_code=404, detail="Task not found")
921921
await cache.invalidate(f"tasks:{experimentId}")
@@ -1120,7 +1120,7 @@ async def import_task_from_gallery(
11201120
await storage.makedirs(task_dir_path, exist_ok=True)
11211121
dest_subdir = storage.join(task_dir_path, os.path.basename(local_task_dir.rstrip("/")))
11221122
await storage.copy_dir(local_task_dir, dest_subdir)
1123-
await task_service.update_task(task_id, {"file_mounts": True})
1123+
await task_service.update_task(task_id, {"file_mounts": True}, experiment_id=experimentId)
11241124

11251125
return {"status": "success", "message": f"Interactive task '{task_name}' imported successfully", "id": task_id}
11261126

@@ -1532,7 +1532,7 @@ async def import_task_from_team_gallery(
15321532
await f.write(json.dumps(data, indent=2))
15331533
else:
15341534
# No index.json in the copied directory; write at least minimal metadata.
1535-
await task_service.update_task(task_id, {"id": task_id})
1535+
await task_service.update_task(task_id, {"id": task_id}, experiment_id=experimentId)
15361536
except Exception as e:
15371537
raise HTTPException(
15381538
status_code=500, detail=f"Failed to overwrite id in index.json for imported team task: {e}"
@@ -1708,7 +1708,7 @@ async def export_task_to_team_gallery(
17081708
Export a task into the team-specific gallery stored in workspace_dir.
17091709
Tasks store all fields directly (not nested in config).
17101710
"""
1711-
task = await task_service.task_get_by_id(request.task_id)
1711+
task = await task_service.task_get_by_id(request.task_id, experiment_id=request.experiment_id)
17121712
if not task:
17131713
raise HTTPException(status_code=404, detail="Task not found")
17141714

api/transformerlab/services/compute_provider/launch_template.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,13 @@
4242
from transformerlab.shared.models.models import ProviderType
4343
from transformerlab.shared.secret_utils import load_team_secrets, replace_secrets_in_dict, replace_secret_placeholders
4444
from lab import storage
45-
from lab.dirs import get_job_dir, get_local_provider_job_dir, get_task_dir, get_workspace_dir
45+
from lab.dirs import (
46+
get_experiment_task_dir,
47+
get_job_dir,
48+
get_local_provider_job_dir,
49+
get_task_dir,
50+
get_workspace_dir,
51+
)
4652
from lab.job_status import JobStatus
4753
from lab.storage import STORAGE_PROVIDER
4854
from werkzeug.utils import secure_filename
@@ -274,7 +280,7 @@ async def launch_template_on_provider(
274280
# This handles GitHub-sourced interactive tasks where the CLI/TUI doesn't
275281
# send these fields and relies on the backend to resolve them from the task.
276282
if not request.github_repo_url and request.task_id:
277-
task_data = await task_service.task_get_by_id(request.task_id)
283+
task_data = await task_service.task_get_by_id(request.task_id, experiment_id=request.experiment_id)
278284
if task_data:
279285
request.github_repo_url = task_data.get("github_repo_url", "") or ""
280286
request.github_repo_dir = task_data.get("github_repo_dir", "") or ""
@@ -436,7 +442,7 @@ async def launch_template_on_provider(
436442
# This handles GitHub-sourced interactive tasks where the command/setup
437443
# are in task.yaml and were stored in the task at import time.
438444
if not base_command.strip() and request.task_id:
439-
fallback_task = await task_service.task_get_by_id(request.task_id)
445+
fallback_task = await task_service.task_get_by_id(request.task_id, experiment_id=request.experiment_id)
440446
if fallback_task:
441447
base_command = fallback_task.get("run", "") or fallback_task.get("command", "")
442448
# Also pick up setup from the task if not already added
@@ -557,8 +563,11 @@ async def launch_template_on_provider(
557563
# for metadata and overwriting it with the task's index.json would break
558564
# job status tracking.
559565
if request.task_id:
560-
task_dir_root = await get_task_dir()
561-
task_src = storage.join(task_dir_root, secure_filename(str(request.task_id)))
566+
task_src = await get_experiment_task_dir(request.experiment_id, request.task_id)
567+
if not await storage.isdir(task_src):
568+
# Legacy fallback for tasks that have not been migrated yet.
569+
task_dir_root = await get_task_dir()
570+
task_src = storage.join(task_dir_root, secure_filename(str(request.task_id)))
562571
if await storage.isdir(task_src):
563572
workspace_job_dir = await get_job_dir(job_id, request.experiment_id)
564573
await copy_task_files_to_dir(task_src, workspace_job_dir)

0 commit comments

Comments
 (0)