release: freeze Nexus One AI 1.0.0-rc1
This commit is contained in:
1 parent
63acefd890
commit
4f1419ca93
152 files changed
+9445
-828
No files matched your search
@@ -67,6 +67,7 @@ JWT_ALGORITHM = "HS256"
|
||||
JWT_EXPIRE_HRS = 8
|
||||
COOKIE_NAME = "cezen_token"
|
||||
OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434")
|
||||
PROTOTYPE_LICENSE_BYPASS = os.environ.get("CEZEN_PROTOTYPE_BYPASS_LICENSE", "").strip().lower() in {"1", "true", "yes"}
|
||||
SCHEMA_VERSION = 1
|
||||
|
||||
CUSTOMER_STATES = {"loading", "empty", "ready", "degraded", "failed", "restricted", "unavailable", "stale"}
|
||||
@@ -379,6 +380,33 @@ def init_db():
|
||||
finished_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS dataset_generation_jobs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
source_name TEXT NOT NULL,
|
||||
source_path TEXT NOT NULL,
|
||||
model TEXT NOT NULL,
|
||||
format TEXT NOT NULL DEFAULT 'qa',
|
||||
target_count INTEGER NOT NULL DEFAULT 20,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
generated_count INTEGER NOT NULL DEFAULT 0,
|
||||
error_msg TEXT,
|
||||
created_by TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
started_at TEXT,
|
||||
finished_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS dataset_generation_samples (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
job_id INTEGER NOT NULL,
|
||||
instruction TEXT NOT NULL,
|
||||
output TEXT NOT NULL,
|
||||
source_text TEXT NOT NULL DEFAULT '',
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS prompts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
@@ -1171,6 +1199,8 @@ def _feature_enabled(feature_key: str, feasibility: Optional[dict] = None) -> bo
|
||||
|
||||
def require_feature(feature_key: str):
|
||||
def dependency(user: dict = Depends(current_user)):
|
||||
if PROTOTYPE_LICENSE_BYPASS:
|
||||
return user
|
||||
if not _feature_enabled(feature_key):
|
||||
raise HTTPException(status_code=403, detail=f"Feature '{feature_key}' is not licensed or provisioned")
|
||||
return user
|
||||
@@ -1571,12 +1601,14 @@ SERVICES = [
|
||||
{"name": "jupyter", "port": 8888, "label": "JupyterLab"},
|
||||
{"name": "mlflow", "port": 5000, "label": "MLflow"},
|
||||
{"name": "grafana", "port": 3000, "label": "Grafana"},
|
||||
{"name": "k3s", "host": "192.168.122.1", "port": 6443, "label": "K3s"},
|
||||
{"name": "harbor", "host": "192.168.122.1", "port": 30443, "label": "Harbor Registry"},
|
||||
]
|
||||
|
||||
def service_status(svc: dict) -> dict:
|
||||
import socket
|
||||
try:
|
||||
s = socket.create_connection(("127.0.0.1", svc["port"]), timeout=1)
|
||||
s = socket.create_connection((svc.get("host", "127.0.0.1"), svc["port"]), timeout=1)
|
||||
s.close()
|
||||
running = True
|
||||
except OSError:
|
||||
@@ -2634,25 +2666,261 @@ async def rag_query(body: dict, user: dict = Depends(current_user)):
|
||||
DATASETS_DIR = DATA_DIR / "datasets"
|
||||
MODELS_DIR = DATA_DIR / "finetuned"
|
||||
JOBS_LOG_DIR = DATA_DIR / "job_logs"
|
||||
DATASET_SOURCES_DIR = DATA_DIR / "dataset_sources"
|
||||
|
||||
DATASETS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
MODELS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
JOBS_LOG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
DATASET_SOURCES_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def _count_rows(path: Path, orig_name: str) -> int:
|
||||
"""Count data rows in JSONL or CSV file."""
|
||||
try:
|
||||
if orig_name.lower().endswith(".csv"):
|
||||
import csv
|
||||
with open(path, newline="", encoding="utf-8", errors="replace") as f:
|
||||
return max(0, sum(1 for _ in csv.reader(f)) - 1) # exclude header
|
||||
_dataset_generation_tasks: set[asyncio.Task] = set()
|
||||
|
||||
def _extract_dataset_source(path: Path, orig_name: str) -> str:
|
||||
"""Extract plain text from supported dataset source documents."""
|
||||
ext = Path(orig_name).suffix.lower()
|
||||
if ext == ".pdf":
|
||||
import fitz
|
||||
with fitz.open(path) as doc:
|
||||
return "\n\n".join(page.get_text("text") for page in doc)
|
||||
if ext == ".docx":
|
||||
from docx import Document
|
||||
doc = Document(path)
|
||||
return "\n".join(p.text for p in doc.paragraphs)
|
||||
if ext == ".csv":
|
||||
import csv
|
||||
with open(path, newline="", encoding="utf-8", errors="replace") as f:
|
||||
return "\n".join(" | ".join(str(v) for v in row) for row in csv.reader(f))
|
||||
return path.read_text(encoding="utf-8", errors="replace")
|
||||
|
||||
def _chunk_dataset_source(text: str, max_chars: int = 6000) -> list[str]:
|
||||
paragraphs = [" ".join(p.split()) for p in text.splitlines() if p.strip()]
|
||||
chunks, current = [], ""
|
||||
for paragraph in paragraphs:
|
||||
if current and len(current) + len(paragraph) + 1 > max_chars:
|
||||
chunks.append(current)
|
||||
current = ""
|
||||
if len(paragraph) > max_chars:
|
||||
if current:
|
||||
chunks.append(current)
|
||||
current = ""
|
||||
chunks.extend(paragraph[i:i + max_chars] for i in range(0, len(paragraph), max_chars))
|
||||
else:
|
||||
return sum(1 for line in open(path, encoding="utf-8", errors="replace") if line.strip())
|
||||
except Exception:
|
||||
return 0
|
||||
current = f"{current}\n{paragraph}".strip()
|
||||
if current:
|
||||
chunks.append(current)
|
||||
return chunks
|
||||
|
||||
def _parse_generated_samples(raw: str) -> list[dict]:
|
||||
"""Accept a JSON array or an object containing a samples array."""
|
||||
raw = raw.strip()
|
||||
if raw.startswith("```"):
|
||||
raw = raw.split("\n", 1)[-1].rsplit("```", 1)[0].strip()
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
start, end = raw.find("["), raw.rfind("]")
|
||||
if start < 0 or end <= start:
|
||||
return []
|
||||
data = json.loads(raw[start:end + 1])
|
||||
if isinstance(data, dict):
|
||||
data = data.get("samples", [])
|
||||
if not isinstance(data, list):
|
||||
return []
|
||||
valid = []
|
||||
for row in data:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
instruction = str(row.get("instruction") or row.get("question") or row.get("prompt") or "").strip()
|
||||
output = str(row.get("output") or row.get("answer") or row.get("completion") or "").strip()
|
||||
if instruction and output:
|
||||
valid.append({"instruction": instruction[:8000], "output": output[:16000]})
|
||||
return valid
|
||||
|
||||
def _generate_samples_for_chunk(model: str, source: str, count: int, output_format: str) -> list[dict]:
|
||||
format_guidance = {
|
||||
"qa": "Create grounded question and answer pairs.",
|
||||
"instruction": "Create practical instruction and response pairs.",
|
||||
"summary": "Create summarisation instructions and faithful summaries.",
|
||||
}.get(output_format, "Create grounded question and answer pairs.")
|
||||
prompt = f"""You create high-quality supervised fine-tuning data. {format_guidance}
|
||||
Use only facts present in SOURCE. Do not invent facts. Make each example distinct and useful.
|
||||
Return only a JSON array with exactly {count} objects. Each object must have string fields
|
||||
\"instruction\" and \"output\". Do not use markdown fences.
|
||||
|
||||
SOURCE:
|
||||
{source}
|
||||
"""
|
||||
raw = _call_ollama_sync(model, prompt)
|
||||
return _parse_generated_samples(raw)
|
||||
|
||||
async def _run_dataset_generation(job_id: int):
|
||||
db = get_db()
|
||||
job = db.execute("SELECT * FROM dataset_generation_jobs WHERE id=?", (job_id,)).fetchone()
|
||||
if not job:
|
||||
db.close()
|
||||
return
|
||||
db.execute("UPDATE dataset_generation_jobs SET status='running', started_at=? WHERE id=?", (utcnow(), job_id))
|
||||
db.commit()
|
||||
db.close()
|
||||
try:
|
||||
text = await asyncio.to_thread(_extract_dataset_source, Path(job["source_path"]), job["source_name"])
|
||||
chunks = _chunk_dataset_source(text)
|
||||
if not chunks:
|
||||
raise ValueError("No readable text was found in the source document")
|
||||
target = int(job["target_count"])
|
||||
generated, seen = 0, set()
|
||||
for index, chunk in enumerate(chunks):
|
||||
remaining = target - generated
|
||||
if remaining <= 0:
|
||||
break
|
||||
remaining_chunks = max(1, len(chunks) - index)
|
||||
count = min(5, max(1, (remaining + remaining_chunks - 1) // remaining_chunks))
|
||||
rows = await asyncio.to_thread(_generate_samples_for_chunk, job["model"], chunk, count, job["format"])
|
||||
db = get_db()
|
||||
for row in rows:
|
||||
key = row["instruction"].casefold()
|
||||
if key in seen or generated >= target:
|
||||
continue
|
||||
seen.add(key)
|
||||
db.execute(
|
||||
"INSERT INTO dataset_generation_samples (job_id,instruction,output,source_text,status,created_at) VALUES (?,?,?,?,?,?)",
|
||||
(job_id, row["instruction"], row["output"], chunk[:6000], "pending", utcnow())
|
||||
)
|
||||
generated += 1
|
||||
db.execute("UPDATE dataset_generation_jobs SET generated_count=? WHERE id=?", (generated, job_id))
|
||||
db.commit()
|
||||
db.close()
|
||||
db = get_db()
|
||||
db.execute("UPDATE dataset_generation_jobs SET status='completed', generated_count=?, finished_at=? WHERE id=?", (generated, utcnow(), job_id))
|
||||
db.commit()
|
||||
db.close()
|
||||
except Exception as exc:
|
||||
db = get_db()
|
||||
db.execute("UPDATE dataset_generation_jobs SET status='failed', error_msg=?, finished_at=? WHERE id=?", (str(exc)[:1000], utcnow(), job_id))
|
||||
db.commit()
|
||||
db.close()
|
||||
|
||||
@app.post("/api/training/dataset-generation")
|
||||
async def create_dataset_generation(
|
||||
file: UploadFile = File(...), name: str = Form(...), model: str = Form(...),
|
||||
format: str = Form("qa"), target_count: int = Form(20),
|
||||
admin: dict = Depends(admin_only)
|
||||
):
|
||||
allowed_ext = {".pdf", ".docx", ".txt", ".md", ".csv"}
|
||||
orig = Path(file.filename or "source.txt").name
|
||||
ext = Path(orig).suffix.lower()
|
||||
if ext not in allowed_ext:
|
||||
raise HTTPException(status_code=400, detail="Use PDF, DOCX, TXT, Markdown, or CSV source files")
|
||||
if format not in {"qa", "instruction", "summary"}:
|
||||
raise HTTPException(status_code=400, detail="Unsupported dataset format")
|
||||
if not model.strip() or not name.strip():
|
||||
raise HTTPException(status_code=400, detail="Dataset name and local model are required")
|
||||
target_count = max(1, min(int(target_count), 200))
|
||||
data = await file.read()
|
||||
if not data or len(data) > 20 * 1024 * 1024:
|
||||
raise HTTPException(status_code=400, detail="Source file must be between 1 byte and 20 MB")
|
||||
source_path = DATASET_SOURCES_DIR / f"{uuid.uuid4().hex}{ext}"
|
||||
source_path.write_bytes(data)
|
||||
db = get_db()
|
||||
cur = db.execute(
|
||||
"INSERT INTO dataset_generation_jobs (name,source_name,source_path,model,format,target_count,status,created_by,created_at) VALUES (?,?,?,?,?,?,?,?,?)",
|
||||
(name.strip()[:160], orig, str(source_path), model.strip()[:160], format, target_count, "pending", admin["username"], utcnow())
|
||||
)
|
||||
db.commit(); job_id = cur.lastrowid; db.close()
|
||||
task = asyncio.create_task(_run_dataset_generation(job_id))
|
||||
_dataset_generation_tasks.add(task)
|
||||
task.add_done_callback(_dataset_generation_tasks.discard)
|
||||
_audit(None, admin["username"], "dataset_generation_start", f"Started dataset generation: {name} using {model}")
|
||||
return {"job_id": job_id, "status": "pending"}
|
||||
|
||||
@app.get("/api/training/dataset-generation")
|
||||
async def list_dataset_generations(admin: dict = Depends(admin_only)):
|
||||
db = get_db()
|
||||
rows = db.execute("SELECT id,name,source_name,model,format,target_count,generated_count,status,error_msg,created_at,finished_at FROM dataset_generation_jobs ORDER BY id DESC").fetchall()
|
||||
db.close()
|
||||
return {"jobs": [dict(row) for row in rows]}
|
||||
|
||||
@app.get("/api/training/dataset-generation/{job_id}/samples")
|
||||
async def list_generated_samples(job_id: int, admin: dict = Depends(admin_only)):
|
||||
db = get_db()
|
||||
rows = db.execute("SELECT id,instruction,output,source_text,status FROM dataset_generation_samples WHERE job_id=? ORDER BY id", (job_id,)).fetchall()
|
||||
db.close()
|
||||
return {"samples": [dict(row) for row in rows]}
|
||||
|
||||
@app.patch("/api/training/dataset-generation/samples/{sample_id}")
|
||||
async def update_generated_sample(sample_id: int, body: dict, admin: dict = Depends(admin_only)):
|
||||
status_value = body.get("status", "pending")
|
||||
if status_value not in {"pending", "approved", "rejected"}:
|
||||
raise HTTPException(status_code=400, detail="Invalid review status")
|
||||
instruction, output = str(body.get("instruction", "")).strip(), str(body.get("output", "")).strip()
|
||||
if not instruction or not output:
|
||||
raise HTTPException(status_code=400, detail="Instruction and output cannot be empty")
|
||||
db = get_db()
|
||||
cur = db.execute("UPDATE dataset_generation_samples SET instruction=?,output=?,status=? WHERE id=?", (instruction[:8000], output[:16000], status_value, sample_id))
|
||||
db.commit(); db.close()
|
||||
if not cur.rowcount:
|
||||
raise HTTPException(status_code=404, detail="Sample not found")
|
||||
return {"ok": True}
|
||||
|
||||
@app.post("/api/training/dataset-generation/{job_id}/publish")
|
||||
async def publish_generated_dataset(job_id: int, admin: dict = Depends(admin_only)):
|
||||
db = get_db()
|
||||
job = db.execute("SELECT * FROM dataset_generation_jobs WHERE id=?", (job_id,)).fetchone()
|
||||
if not job:
|
||||
db.close(); raise HTTPException(status_code=404, detail="Generation job not found")
|
||||
rows = db.execute("SELECT instruction,output FROM dataset_generation_samples WHERE job_id=? AND status='approved' ORDER BY id", (job_id,)).fetchall()
|
||||
if not rows:
|
||||
db.close(); raise HTTPException(status_code=400, detail="Approve at least one sample before publishing")
|
||||
filename = f"{uuid.uuid4().hex}.jsonl"
|
||||
dest = DATASETS_DIR / filename
|
||||
content = "".join(json.dumps({"instruction": row["instruction"], "output": row["output"]}, ensure_ascii=False) + "\n" for row in rows)
|
||||
dest.write_text(content, encoding="utf-8")
|
||||
orig_name = "".join(c if c.isalnum() or c in "-_" else "_" for c in job["name"]).strip("_") or "generated_dataset"
|
||||
orig_name += ".jsonl"
|
||||
cur = db.execute("INSERT INTO training_datasets (filename,orig_name,file_path,size_bytes,row_count,uploaded_at) VALUES (?,?,?,?,?,?)", (filename, orig_name, str(dest), len(content.encode()), len(rows), utcnow()))
|
||||
db.commit(); dataset_id = cur.lastrowid; db.close()
|
||||
_audit(None, admin["username"], "dataset_generation_publish", f"Published {orig_name} ({len(rows)} approved rows)")
|
||||
return {"dataset_id": dataset_id, "filename": orig_name, "row_count": len(rows)}
|
||||
|
||||
def _validate_training_dataset(path: Path, orig_name: str) -> int:
|
||||
"""Validate a training dataset and return its semantic record count."""
|
||||
import csv
|
||||
|
||||
ext = Path(orig_name).suffix.lower()
|
||||
if ext == ".csv":
|
||||
with open(path, newline="", encoding="utf-8", errors="replace") as f:
|
||||
rows = list(csv.DictReader(f))
|
||||
elif ext == ".json":
|
||||
with open(path, encoding="utf-8", errors="replace") as f:
|
||||
payload = json.load(f)
|
||||
if isinstance(payload, list):
|
||||
rows = payload
|
||||
elif isinstance(payload, dict):
|
||||
rows = next((payload[key] for key in ("samples", "data", "records")
|
||||
if isinstance(payload.get(key), list)), None)
|
||||
if rows is None:
|
||||
raise ValueError("JSON must be an array of training records or contain a samples, data, or records array")
|
||||
else:
|
||||
raise ValueError("JSON must contain an array of training records")
|
||||
else:
|
||||
rows = []
|
||||
with open(path, encoding="utf-8", errors="replace") as f:
|
||||
for line_number, line in enumerate(f, 1):
|
||||
if not line.strip():
|
||||
continue
|
||||
try:
|
||||
rows.append(json.loads(line))
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ValueError(f"Invalid JSON on line {line_number}") from exc
|
||||
|
||||
if not rows:
|
||||
raise ValueError("Dataset has no training records")
|
||||
if not all(isinstance(row, dict) and row for row in rows):
|
||||
raise ValueError("Every training record must be a non-empty object or CSV row")
|
||||
return len(rows)
|
||||
|
||||
@app.post("/api/training/datasets")
|
||||
async def upload_dataset(file: UploadFile = File(...), _licensed: dict = Depends(require_feature("fine_tuning")), admin: dict = Depends(admin_only)):
|
||||
async def upload_dataset(file: UploadFile = File(...), admin: dict = Depends(admin_only)):
|
||||
"""Upload a JSONL or CSV dataset for fine-tuning."""
|
||||
allowed_ext = {".jsonl", ".json", ".csv"}
|
||||
orig = file.filename or "dataset"
|
||||
@@ -2668,7 +2936,11 @@ async def upload_dataset(file: UploadFile = File(...), _licensed: dict = Depends
|
||||
dest = DATASETS_DIR / filename
|
||||
dest.write_bytes(data)
|
||||
|
||||
row_count = _count_rows(dest, orig)
|
||||
try:
|
||||
row_count = _validate_training_dataset(dest, orig)
|
||||
except (ValueError, json.JSONDecodeError) as exc:
|
||||
dest.unlink(missing_ok=True)
|
||||
raise HTTPException(status_code=400, detail=f"Invalid training dataset: {exc}") from exc
|
||||
|
||||
db = get_db()
|
||||
cur = db.execute(
|
||||
@@ -2683,7 +2955,7 @@ async def upload_dataset(file: UploadFile = File(...), _licensed: dict = Depends
|
||||
return {"id": row_id, "filename": orig, "size_bytes": len(data), "row_count": row_count}
|
||||
|
||||
@app.get("/api/training/datasets")
|
||||
async def list_datasets(_licensed: dict = Depends(require_feature("fine_tuning")), admin: dict = Depends(admin_only)):
|
||||
async def list_datasets(admin: dict = Depends(admin_only)):
|
||||
db = get_db()
|
||||
rows = db.execute(
|
||||
"SELECT id, orig_name, size_bytes, row_count, uploaded_at FROM training_datasets ORDER BY uploaded_at DESC"
|
||||
@@ -2692,7 +2964,7 @@ async def list_datasets(_licensed: dict = Depends(require_feature("fine_tuning")
|
||||
return {"datasets": [dict(r) for r in rows]}
|
||||
|
||||
@app.delete("/api/training/datasets/{dataset_id}")
|
||||
async def delete_dataset(dataset_id: int, _licensed: dict = Depends(require_feature("fine_tuning")), admin: dict = Depends(admin_only)):
|
||||
async def delete_dataset(dataset_id: int, admin: dict = Depends(admin_only)):
|
||||
db = get_db()
|
||||
row = db.execute("SELECT * FROM training_datasets WHERE id=?", (dataset_id,)).fetchone()
|
||||
if not row:
|
||||
@@ -2726,6 +2998,11 @@ async def launch_job(body: dict, _licensed: dict = Depends(require_feature("fine
|
||||
if not ds:
|
||||
db.close()
|
||||
raise HTTPException(status_code=404, detail="Dataset not found")
|
||||
try:
|
||||
_validate_training_dataset(Path(ds["file_path"]), ds["orig_name"])
|
||||
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
||||
db.close()
|
||||
raise HTTPException(status_code=400, detail=f"Invalid training dataset: {exc}") from exc
|
||||
|
||||
config = {
|
||||
"epochs": int(body.get("epochs", 3)),
|
||||
@@ -3402,6 +3679,10 @@ async def my_analytics(days: int = 30, user: dict = Depends(current_user)):
|
||||
|
||||
AGENT_STEP_TYPES = {"prompt", "summarise", "extract", "classify", "rag", "format"}
|
||||
|
||||
# Keep strong references to in-flight tasks. asyncio only maintains weak
|
||||
# references, so fire-and-forget tasks can otherwise disappear mid-run.
|
||||
_agent_run_tasks: set = set()
|
||||
|
||||
def _call_ollama_sync(model: str, prompt: str) -> str:
|
||||
import urllib.request as _ur
|
||||
routed = _select_router_model(prompt, model, {"content_type": "text"})
|
||||
@@ -3626,7 +3907,9 @@ async def run_agent(agent_id: int, body: dict, user: dict = Depends(current_user
|
||||
)
|
||||
run_id = cur.lastrowid
|
||||
db.commit(); db.close()
|
||||
asyncio.create_task(_execute_agent(run_id))
|
||||
task = asyncio.create_task(_execute_agent(run_id))
|
||||
_agent_run_tasks.add(task)
|
||||
task.add_done_callback(_agent_run_tasks.discard)
|
||||
return {"run_id": run_id, "status": "pending"}
|
||||
|
||||
|
||||
@@ -4317,6 +4600,7 @@ Respond with ONLY valid JSON, no markdown, no explanation:
|
||||
payload = {
|
||||
"model": judge_model,
|
||||
"messages": [{"role": "user", "content": judge_prompt}],
|
||||
"format": "json",
|
||||
"stream": False,
|
||||
}
|
||||
try:
|
||||
@@ -4595,13 +4879,13 @@ async def get_eval_results(run_id: int, user=Depends(current_user)):
|
||||
|
||||
# ── Scheduled Jobs ────────────────────────────────────────────────────────────
|
||||
|
||||
def _run_scheduled_job_sync(job_id: int):
|
||||
def _run_scheduled_job_sync(job_id: int, force: bool = False):
|
||||
"""Execute a scheduled job (runs in executor thread)."""
|
||||
import asyncio as _asyncio
|
||||
db = get_db()
|
||||
try:
|
||||
job = db.execute("SELECT * FROM scheduled_jobs WHERE id=?", (job_id,)).fetchone()
|
||||
if not job or not job["is_active"]:
|
||||
if not job or (not job["is_active"] and not force):
|
||||
db.close()
|
||||
return
|
||||
|
||||
@@ -4842,7 +5126,8 @@ async def trigger_schedule(job_id: int, user=Depends(current_user)):
|
||||
if not row:
|
||||
raise HTTPException(404, "Job not found")
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_in_executor(None, _run_scheduled_job_sync, job_id)
|
||||
# A manual run is explicit user intent and must also work for paused jobs.
|
||||
loop.run_in_executor(None, _run_scheduled_job_sync, job_id, True)
|
||||
return {"ok": True, "message": "Job triggered"}
|
||||
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ def set_status(status: str):
|
||||
# ── Dataset loading ───────────────────────────────────────────────────────────
|
||||
|
||||
def load_dataset_from_file(path: str):
|
||||
"""Load JSONL or CSV dataset into a list of dicts with 'text' or 'prompt'/'completion' keys."""
|
||||
"""Load JSON, JSONL, or CSV training records as a list of objects."""
|
||||
p = Path(path)
|
||||
rows = []
|
||||
if p.suffix.lower() == ".csv":
|
||||
@@ -82,6 +82,16 @@ def load_dataset_from_file(path: str):
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
rows.append(dict(row))
|
||||
elif p.suffix.lower() == ".json":
|
||||
with open(path, encoding="utf-8", errors="replace") as f:
|
||||
payload = json.load(f)
|
||||
if isinstance(payload, list):
|
||||
rows = payload
|
||||
elif isinstance(payload, dict):
|
||||
for key in ("samples", "data", "records"):
|
||||
if isinstance(payload.get(key), list):
|
||||
rows = payload[key]
|
||||
break
|
||||
else:
|
||||
with open(path, encoding="utf-8", errors="replace") as f:
|
||||
for line in f:
|
||||
@@ -90,7 +100,11 @@ def load_dataset_from_file(path: str):
|
||||
try:
|
||||
rows.append(json.loads(line))
|
||||
except Exception:
|
||||
pass
|
||||
raise ValueError("Dataset contains an invalid JSONL row")
|
||||
if not rows:
|
||||
raise ValueError("Dataset has no training records")
|
||||
if not all(isinstance(row, dict) for row in rows):
|
||||
raise ValueError("Every training record must be a JSON object or CSV row")
|
||||
return rows
|
||||
|
||||
def format_row(row: dict) -> str:
|
||||
|
||||
Reference in new issue
Block a user