Spaces:
Sleeping
Sleeping
| """Production-ready standalone Maris human training Space.""" | |
| from __future__ import annotations | |
| import hashlib | |
| import html | |
| import json | |
| import logging | |
| import os | |
| import secrets | |
| import subprocess | |
| import sys | |
| import tempfile | |
| from datetime import UTC, datetime | |
| from pathlib import Path | |
| from threading import Lock | |
| from typing import Any, Literal | |
| from fastapi import FastAPI, Header, HTTPException | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.responses import HTMLResponse | |
| from pydantic import BaseModel, ConfigDict, Field, field_validator | |
| REPO_ROOT = Path(__file__).resolve().parent.parent | |
| CORE_PYTHON_DIR = REPO_ROOT / "core-python" | |
| if str(CORE_PYTHON_DIR) not in sys.path: | |
| sys.path.insert(0, str(CORE_PYTHON_DIR)) | |
| from huggingface_human_training_space.studio_store import ( # noqa: E402 | |
| archive_draft, | |
| get_artifact, | |
| get_draft, | |
| get_run, | |
| index_artifacts, | |
| list_artifacts, | |
| list_drafts, | |
| list_runs, | |
| save_draft, | |
| save_run, | |
| update_run, | |
| ) | |
| from maris_core.training.human_training import ( # noqa: E402 | |
| HumanTrainingExecuteRequest, | |
| HumanTrainingRequest, | |
| build_human_training_launch_spec, | |
| load_human_training_manifest, | |
| publish_human_training_artifacts, | |
| stage_human_training_artifacts, | |
| ) | |
| from maris_core.training.space_ui import ( # noqa: E402 | |
| SpaceTrainingRequest, | |
| build_space_training_command, | |
| build_space_training_env, | |
| has_completed_training_artifacts, | |
| list_space_model_choices, | |
| parse_training_progress, | |
| resolve_output_dir, | |
| tail_log, | |
| terminate_process_tree, | |
| ) | |
| from maris_core.utils.env import get_env_any_or_default, get_hf_token # noqa: E402 | |
| logger = logging.getLogger(__name__) | |
| ROLE_GUIDES: dict[str, dict[str, object]] = { | |
| "owner": { | |
| "label": "Owner", | |
| "headline": "Vada kvalitāti, riskus un gala lēmumus pirms publicēšanas.", | |
| "responsibilities": [ | |
| "Apstiprina mērķi, kvalitātes robežas un release kritērijus.", | |
| "Izvērtē riskus un pieņem gala lēmumu par publicēšanu.", | |
| "Seko līdzi vai treniņa rezultāts atbilst biznesa vajadzībām.", | |
| ], | |
| "workflow": [ | |
| "Definē success metrics un ko komanda grib iemācīt modelim.", | |
| "Pārskata staging preview un dataset kvalitātes reportus.", | |
| "Dod gala atļauju publicēšanai un treniņa startam.", | |
| ], | |
| "examples": [ | |
| "Apstiprini, vai šis datasets ir gatavs produkcijas treniņam.", | |
| "Parādi galvenos riskus pirms publish + train.", | |
| "Sagatavo īsu owner-ready kopsavilkumu par progresu.", | |
| ], | |
| }, | |
| "secretary": { | |
| "label": "Secretary", | |
| "headline": "Sakārto ievadi, dokumentē lēmumus un uztur procesu disciplinētu.", | |
| "responsibilities": [ | |
| "Savāc prasības, piezīmes un stakeholder feedback vienā formātā.", | |
| "Normalizē instrukcijas un pārbauda vai dokumentācija ir pilna.", | |
| "Uztur checklists, onboarding materiālus un darba secību.", | |
| ], | |
| "workflow": [ | |
| "Pārvērš neformālu ievadi strukturētos treniņa blokos.", | |
| "Pārbauda, vai nekas svarīgs nav izlaists staging etapā.", | |
| "Fiksē nākamos soļus komandai pēc preview un pēc train run.", | |
| ], | |
| "examples": [ | |
| "Sakārto sapulces piezīmes profesionālā treniņa ievadē.", | |
| "Izveido checklist pirms human training publicēšanas.", | |
| "Apvieno feedback vienā skaidrā dokumentētā paketē.", | |
| ], | |
| }, | |
| "trainee": { | |
| "label": "Trainee", | |
| "headline": "Veido piemērus, preference pairs un eval scenārijus praktiskam progresam.", | |
| "responsibilities": [ | |
| "Raksta kvalitatīvus conversation, preference un eval piemērus.", | |
| "Atzīmē neskaidros vai konfliktējošos gadījumus pārskatam.", | |
| "Pārbauda vai atbildes ir skaidras, konsekventas un profesionālas.", | |
| ], | |
| "workflow": [ | |
| "Izveido konkrētus scenārijus ar reālu lietošanas kontekstu.", | |
| "Salīdzina chosen un rejected atbildes ar skaidru pamatojumu.", | |
| "Pievieno eval piemērus, kas pārbauda svarīgāko kvalitāti.", | |
| ], | |
| "examples": [ | |
| "Izveido 5 klientu atbalsta scenārijus ar pareizajām atbildēm.", | |
| "Salīdzini labu un sliktu atbildi vienam jautājumam.", | |
| "Pievieno eval piemēru skaidrai latviešu valodai.", | |
| ], | |
| }, | |
| "user": { | |
| "label": "User", | |
| "headline": "Sniedz reālos scenārijus un atgriezenisko saiti par rezultāta lietojamību.", | |
| "responsibilities": [ | |
| "Apraksta vajadzību, kontekstu un vēlamo iznākumu.", | |
| "Novērtē, vai atbildes palīdz sasniegt mērķi praksē.", | |
| "Norāda, kas ir neskaidrs, lieks vai neprofesionāls.", | |
| ], | |
| "workflow": [ | |
| "Iesniedz skaidru problēmu un gaidīto rezultātu.", | |
| "Dod reālus piemērus ar savu kontekstu.", | |
| "Apstiprina, kas jāuztur un kas jāuzlabo nākamajā iterācijā.", | |
| ], | |
| "examples": [ | |
| "Man vajag profesionālu atbildi klientam latviski un angliski.", | |
| "Šis rezultāts ir pārāk garš — saīsini līdz 5 punktiem.", | |
| "Dod piemēru, kā pareizi strukturēt onboarding instrukciju.", | |
| ], | |
| }, | |
| } | |
| PLATFORM_SECTIONS = { | |
| "workflow": [ | |
| { | |
| "title": "1. Ievade", | |
| "summary": "Savāc profila faktus, preferences, instrukcijas un reālus piemērus.", | |
| }, | |
| { | |
| "title": "2. Staging preview", | |
| "summary": "Pārskati manifestu, dataset kvalitāti un publicēšanas gatavību.", | |
| }, | |
| { | |
| "title": "3. Publish + train", | |
| "summary": "Publicē artefaktus dataset repozitorijā un palaid treniņu tikai apstiprinātai versijai.", | |
| }, | |
| { | |
| "title": "4. Rezultāts", | |
| "summary": "Komanda saņem skaidru statusu, logus un nākamos soļus.", | |
| }, | |
| ], | |
| "documentation": [ | |
| { | |
| "title": "Onboarding guide", | |
| "summary": "Paskaidro, kā katra loma sāk darbu bez liekiem pieņēmumiem.", | |
| }, | |
| { | |
| "title": "Role playbook", | |
| "summary": "Nosaka, ko dara owner, secretary, trainee un user katrā posmā.", | |
| }, | |
| { | |
| "title": "Quality checklist", | |
| "summary": "Palīdz pārbaudīt datu kvalitāti, saprotamību un publicēšanas gatavību.", | |
| }, | |
| { | |
| "title": "Example library", | |
| "summary": "Dod gatavus conversation, preference un eval piemēru modeļus.", | |
| }, | |
| ], | |
| } | |
| STUDIO_TEMPLATES: dict[str, dict[str, object]] = { | |
| "customer-support-lv": { | |
| "label": "Klientu atbalsts LV", | |
| "summary": "Profesionālas, īsas un mierīgas atbildes klientu apkalpošanai latviešu valodā.", | |
| "payload": { | |
| "profile_facts": [ | |
| "Asistents strādā kā Maris AI klientu atbalsta speciālists.", | |
| "Primārā valoda ir latviešu valoda, bet vajadzības gadījumā var dot īsu EN kopsavilkumu.", | |
| "Atbildēs nedrīkst solīt to, ko komanda nevar izpildīt praksē.", | |
| ], | |
| "profile_preferences": [ | |
| "Sāc ar tiešu atbildi un tad dod 2-4 skaidrus soļus.", | |
| "Nesodi klientu un neizmanto pasīvi agresīvu toni.", | |
| "Ja pietrūkst informācijas, uzdod vienu precizējošu jautājumu.", | |
| ], | |
| "response_instructions": [ | |
| "Prioritāte ir skaidrība, profesionāls tonis un droša informācija.", | |
| "Ja ir kļūda vai incidents, skaidri pasaki, ko komanda dara tālāk.", | |
| "Ja atbilde ir gara, beigās iedod īsu kopsavilkumu.", | |
| ], | |
| "conversation_examples": [ | |
| { | |
| "user": "Mums klients raksta, ka sistēma nestrādā kopš rīta. Kā atbildēt?", | |
| "assistant": "Atvainojamies par traucējumiem. Šobrīd pārbaudām incidentu un jau strādājam pie risinājuma. Lūdzu atsūti ietekmēto lietotāju vai konta piemēru, lai varam prioritizēt pārbaudi. Tiklīdz būs konkrēts atjauninājums, paziņosim nekavējoties.", | |
| } | |
| ], | |
| "preference_pairs": [ | |
| { | |
| "prompt": "Atbildi klientam par kavētu piegādi.", | |
| "chosen": "Paldies, ka uzrakstīji. Redzam, ka piegāde kavējas, un jau pārbaudām statusu ar partneri. Atgriezīsimies ar precīzu atjauninājumu tuvākajā laikā.", | |
| "rejected": "Tas nav mūsu ziņā, gaidiet.", | |
| } | |
| ], | |
| "eval_examples": [ | |
| { | |
| "prompt": "Klients ir neapmierināts ar lēnu atbalstu.", | |
| "completion": "Atbildei jābūt empātiskai, konkrētai un ar skaidru nākamo soli bez aizsargājoša toņa.", | |
| } | |
| ], | |
| }, | |
| }, | |
| "onboarding-coach": { | |
| "label": "Onboarding coach", | |
| "summary": "Skaidra iekšējā onboarding un rokasgrāmatu rakstīšana jaunajiem komandas biedriem.", | |
| "payload": { | |
| "profile_facts": [ | |
| "Asistents palīdz strukturēt onboarding saturu jaunajiem darbiniekiem.", | |
| "Mērķis ir no haotiskām piezīmēm izveidot skaidru darbību secību.", | |
| "Svarīga ir konsekventa terminoloģija un vienāds dokumentu stils.", | |
| ], | |
| "profile_preferences": [ | |
| "Atbildi strukturē ar sadaļām, checklistēm un īsiem paskaidrojumiem.", | |
| "Izcel riskus, piekļuves atkarības un kas jāzina pirmajā dienā.", | |
| "Nelieto neskaidrus saīsinājumus bez paskaidrojuma.", | |
| ], | |
| "response_instructions": [ | |
| "Pirms soļiem uzraksti mērķi un sagaidāmo rezultātu.", | |
| "Ja informācija ir nepilna, norādi tieši, kas vēl jāsaņem.", | |
| "Izvairies no marketinga valodas; prioritāte ir praktiska lietojamība.", | |
| ], | |
| "conversation_examples": [ | |
| { | |
| "user": "No šīm piezīmēm izveido saprotamu onboarding plānu jaunam operatoram.", | |
| "assistant": "Zemāk ir strukturēts onboarding plāns ar pirmās dienas uzdevumiem, nepieciešamajām piekļuvēm, kvalitātes checkpointiem un jautājumiem, kas jāprecizē pirms starta.", | |
| } | |
| ], | |
| "preference_pairs": [ | |
| { | |
| "prompt": "Sakārto onboarding instrukciju.", | |
| "chosen": "Instrukcija sākas ar mērķi, pēc tam dod secīgu checklisti, atbildīgās lomas un sagaidāmo rezultātu.", | |
| "rejected": "Te ir dažas idejas, paskaties pats, kas der.", | |
| } | |
| ], | |
| "eval_examples": [ | |
| { | |
| "prompt": "Kā jāizskatās labai onboarding instrukcijai?", | |
| "completion": "Tai jābūt secīgai, auditējamai un saprotamai arī cilvēkam bez iepriekšēja konteksta.", | |
| } | |
| ], | |
| }, | |
| }, | |
| "executive-briefing": { | |
| "label": "Executive briefing", | |
| "summary": "Owner/secretary režīms īsiem vadības kopsavilkumiem ar riskiem, lēmumiem un nākamajiem soļiem.", | |
| "payload": { | |
| "profile_facts": [ | |
| "Asistents gatavo vadības līmeņa kopsavilkumus par progresu un riskiem.", | |
| "Galvenais lasītājs ir owner vai lēmumu pieņēmējs ar mazu laiku.", | |
| "Svarīgākais ir signāls, nevis lieka detaļu pārslodze.", | |
| ], | |
| "profile_preferences": [ | |
| "Sāc ar statusu vienā teikumā.", | |
| "Dod 3-5 punktus par riskiem, progresu un vajadzīgo lēmumu.", | |
| "Ja iespējams, pievieno skaidru recommendation.", | |
| ], | |
| "response_instructions": [ | |
| "Nekopē neapstrādātus logus vai garus citātus.", | |
| "Ja nav datu lēmumam, pasaki, kas pietrūkst.", | |
| "Saglabā profesionālu, mierīgu un precīzu toni.", | |
| ], | |
| "conversation_examples": [ | |
| { | |
| "user": "Sagatavo owner-ready kopsavilkumu par human training progresu.", | |
| "assistant": "Statuss: staging artefakti ir gatavi pārskatam, publicēšanas tokens ir pieejams, un pirms treniņa starta jāapstiprina divi kvalitātes riski.", | |
| } | |
| ], | |
| "preference_pairs": [ | |
| { | |
| "prompt": "Apraksti statusu vadībai.", | |
| "chosen": "Statuss ir īss, ar skaidru recommendation un konkrētu nākamo soli.", | |
| "rejected": "Te ir viss pilnais konteksts 20 rindkopās bez prioritātēm.", | |
| } | |
| ], | |
| "eval_examples": [ | |
| { | |
| "prompt": "Kādam jābūt owner-ready update?", | |
| "completion": "Īsam, prioritizētam un orientētam uz lēmumu vai risku, nevis uz detalizētu iekšējo darba dienasgrāmatu.", | |
| } | |
| ], | |
| }, | |
| }, | |
| } | |
| LOGO_URL = "https://github.com/user-attachments/assets/347ded6a-40dc-4991-9cc7-4207cffdf452" | |
| PERSISTENT_DIR = Path( | |
| get_env_any_or_default("MARIS_PERSISTENT_DIR", "HF_PERSISTENT_DIR", default="/data") | |
| ) | |
| USERS_FILE = PERSISTENT_DIR / "human-training-users.json" | |
| USER_STORE_FALLBACK_DIRNAME = "maris-human-training-space" | |
| TRAIN_SCRIPT = str(REPO_ROOT / "huggingface" / "train-hf.sh") | |
| LOG_DIR = Path( | |
| get_env_any_or_default( | |
| "MARIS_HUMAN_TRAINING_LOG_DIR", | |
| "HF_SPACE_LOG_DIR", | |
| default=f"{PERSISTENT_DIR}/human-training-space-logs", | |
| ) | |
| ) | |
| DEFAULT_DATASET_REPO = get_env_any_or_default( | |
| "MARIS_MEMORY_REPO", | |
| "MARIS_DATASET_REPO", | |
| "HF_DATASET_REPO", | |
| default="MarisUK/maris-ai-lv-memory", | |
| ) | |
| DEFAULT_HUB_MODEL_ID = get_env_any_or_default( | |
| "MARIS_HUMAN_TRAINING_MODEL_REPO", | |
| "MARIS_MODEL_REPO", | |
| "HF_MODEL_REPO", | |
| default="MarisUK/maris-ai-lv", | |
| ) | |
| DEFAULT_OUTPUT_SUBDIR = get_env_any_or_default( | |
| "MARIS_HUMAN_TRAINING_OUTPUT_SUBDIR", | |
| default="maris-ai-lv", | |
| ) | |
| AUTH_REQUIRED = get_env_any_or_default( | |
| "MARIS_HUMAN_TRAINING_REQUIRE_AUTH", | |
| "MARIS_HUMAN_TRAINING_REQUIRE_LOGIN", | |
| default="false", | |
| ).strip().lower() in {"1", "true", "yes", "on"} | |
| DEFAULT_PRIVATE_ROLE = ( | |
| get_env_any_or_default( | |
| "MARIS_HUMAN_TRAINING_PRIVATE_ROLE", | |
| default="owner", | |
| ) | |
| .strip() | |
| .lower() | |
| ) | |
| if DEFAULT_PRIVATE_ROLE not in ROLE_GUIDES: | |
| logger.warning("Unknown private role '%s'; falling back to owner.", DEFAULT_PRIVATE_ROLE) | |
| DEFAULT_PRIVATE_ROLE = "owner" | |
| APP = FastAPI(title="Maris AI Human Training Space", version="2.0.0") | |
| APP.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| SESSION_LOCK = Lock() | |
| USER_LOCK = Lock() | |
| STATE_LOCK = Lock() | |
| SESSION_STORE: dict[str, str] = {} | |
| TRAINING_STATE: dict[str, Any] = { | |
| "process": None, | |
| "log_path": "", | |
| "log_handle": None, | |
| "started_at": None, | |
| "finished_at": None, | |
| "request": None, | |
| "stop_requested": False, | |
| } | |
| PRIVATE_SPACE_REGISTERED_AT = datetime.now(UTC).replace(microsecond=0).isoformat() | |
| PRIVATE_SPACE_TOKEN = "private-space" | |
| class RegisterRequest(BaseModel): | |
| model_config = ConfigDict(str_strip_whitespace=True) | |
| full_name: str = Field(min_length=2, max_length=120) | |
| email: str = Field(min_length=5, max_length=160) | |
| password: str = Field(min_length=8, max_length=256) | |
| role: Literal["owner", "secretary", "trainee", "user"] | |
| def validate_email(cls, value: str) -> str: | |
| normalized = value.strip().lower() | |
| if "@" not in normalized or normalized.startswith("@") or normalized.endswith("@"): | |
| raise ValueError("Norādi derīgu e-pastu.") | |
| return normalized | |
| class LoginRequest(BaseModel): | |
| model_config = ConfigDict(str_strip_whitespace=True) | |
| email: str = Field(min_length=5, max_length=160) | |
| password: str = Field(min_length=8, max_length=256) | |
| def validate_email(cls, value: str) -> str: | |
| normalized = value.strip().lower() | |
| if "@" not in normalized or normalized.startswith("@") or normalized.endswith("@"): | |
| raise ValueError("Norādi derīgu e-pastu.") | |
| return normalized | |
| class SessionResponse(BaseModel): | |
| token: str | |
| user: dict[str, str] | |
| role_guide: dict[str, object] | |
| platform: dict[str, object] | |
| class StudioDraftSaveRequest(BaseModel): | |
| model_config = ConfigDict(str_strip_whitespace=True) | |
| name: str = Field(min_length=2, max_length=120) | |
| payload: HumanTrainingRequest | |
| draft_id: str = "" | |
| def _timestamp() -> str: | |
| return datetime.now(UTC).replace(microsecond=0).isoformat() | |
| def _env_flag(*names: str, default: bool = False) -> bool: | |
| return get_env_any_or_default( | |
| *names, default="true" if default else "false" | |
| ).strip().lower() in { | |
| "1", | |
| "true", | |
| "yes", | |
| "on", | |
| } | |
| def _env_int(*names: str, default: int) -> int: | |
| value = get_env_any_or_default(*names, default=str(default)).strip() | |
| try: | |
| return int(value) | |
| except ValueError as exc: | |
| raise RuntimeError( | |
| f"Nederīga vesela skaitļa vērtība env laukam {'/'.join(names)}: {value}" | |
| ) from exc | |
| def _has_publish_token() -> bool: | |
| return bool(get_hf_token()) | |
| def _resolve_user_store_path() -> Path: | |
| try: | |
| USERS_FILE.parent.mkdir(parents=True, exist_ok=True) | |
| except PermissionError: | |
| fallback_root = Path(tempfile.gettempdir()) / USER_STORE_FALLBACK_DIRNAME | |
| try: | |
| fallback_root.mkdir(parents=True, exist_ok=True) | |
| except PermissionError as exc: | |
| raise HTTPException( | |
| status_code=500, | |
| detail="Lietotāju glabātuve nav pieejama ne primārajā, ne rezerves vietā.", | |
| ) from exc | |
| return fallback_root / USERS_FILE.name | |
| return USERS_FILE | |
| def _ensure_user_store() -> Path: | |
| users_file = _resolve_user_store_path() | |
| if not users_file.exists(): | |
| users_file.write_text("{}\n", encoding="utf-8") | |
| return users_file | |
| def _load_users() -> dict[str, dict[str, str]]: | |
| users_file = _ensure_user_store() | |
| try: | |
| payload = json.loads(users_file.read_text(encoding="utf-8")) | |
| except json.JSONDecodeError as exc: | |
| raise HTTPException(status_code=500, detail="Lietotāju glabātuve nav nolasāma.") from exc | |
| return payload if isinstance(payload, dict) else {} | |
| def _save_users(users: dict[str, dict[str, str]]) -> None: | |
| users_file = _ensure_user_store() | |
| users_file.write_text(json.dumps(users, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") | |
| def _hash_password(password: str, salt_hex: str | None = None) -> tuple[str, str]: | |
| salt = bytes.fromhex(salt_hex) if salt_hex else secrets.token_bytes(16) | |
| digest = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, 120000) | |
| return digest.hex(), salt.hex() | |
| def _verify_password(password: str, *, password_hash: str, password_salt: str) -> bool: | |
| calculated_hash, _ = _hash_password(password, password_salt) | |
| return secrets.compare_digest(calculated_hash, password_hash) | |
| def _public_user(record: dict[str, str]) -> dict[str, str]: | |
| return { | |
| "full_name": record["full_name"], | |
| "email": record["email"], | |
| "role": record["role"], | |
| "registered_at": record["registered_at"], | |
| } | |
| def _build_session_response(record: dict[str, str], token: str) -> SessionResponse: | |
| role = record["role"] | |
| return SessionResponse( | |
| token=token, | |
| user=_public_user(record), | |
| role_guide=ROLE_GUIDES[role], | |
| platform={ | |
| "workflow": PLATFORM_SECTIONS["workflow"], | |
| "documentation": PLATFORM_SECTIONS["documentation"], | |
| "examples": ROLE_GUIDES[role]["examples"], | |
| }, | |
| ) | |
| def _build_private_space_session() -> SessionResponse: | |
| return _build_session_response( | |
| { | |
| "full_name": "Private Space Team", | |
| "email": "private-space@maris.ai", | |
| "role": DEFAULT_PRIVATE_ROLE, | |
| "registered_at": PRIVATE_SPACE_REGISTERED_AT, | |
| }, | |
| token=PRIVATE_SPACE_TOKEN, | |
| ) | |
| def _create_session(email: str) -> str: | |
| token = secrets.token_urlsafe(24) | |
| with SESSION_LOCK: | |
| SESSION_STORE[token] = email | |
| return token | |
| def _require_user(session_token: str | None) -> dict[str, str]: | |
| if not AUTH_REQUIRED: | |
| return _build_private_space_session().user | |
| if not session_token: | |
| raise HTTPException( | |
| status_code=401, detail="Pieslēdzies platformai, lai izmantotu human.training rīkus." | |
| ) | |
| with SESSION_LOCK: | |
| email = SESSION_STORE.get(session_token) | |
| if not email: | |
| raise HTTPException(status_code=401, detail="Sesija nav derīga. Pieslēdzies vēlreiz.") | |
| record = _load_users().get(email) | |
| if record is None: | |
| raise HTTPException(status_code=401, detail="Lietotāja ieraksts nav atrasts.") | |
| return record | |
| def _user_email(record: dict[str, str] | None) -> str: | |
| if record and record.get("email"): | |
| return str(record["email"]).strip().lower() | |
| return "private-space@maris.ai" | |
| def _request_payload_dict(request: Any) -> dict[str, Any]: | |
| if hasattr(request, "model_dump"): | |
| return request.model_dump() | |
| return dict(request) | |
| def _close_log_handle_unlocked() -> None: | |
| handle = TRAINING_STATE.get("log_handle") | |
| if handle is not None: | |
| handle.close() | |
| TRAINING_STATE["log_handle"] = None | |
| def _write_log_line_unlocked(message: str) -> None: | |
| handle = TRAINING_STATE.get("log_handle") | |
| if handle is None: | |
| return | |
| handle.write(message + "\n") | |
| handle.flush() | |
| def _sync_training_state_unlocked() -> None: | |
| process = TRAINING_STATE.get("process") | |
| if process is None or process.poll() is None: | |
| return | |
| TRAINING_STATE["process"] = None | |
| TRAINING_STATE["finished_at"] = TRAINING_STATE.get("finished_at") or _timestamp() | |
| _close_log_handle_unlocked() | |
| def _save_huggingface_repo_text_file( | |
| *, | |
| repo_id: str, | |
| repo_type: str, | |
| path_in_repo: str, | |
| content: str, | |
| commit_message: str, | |
| ) -> dict[str, Any]: | |
| token = get_hf_token() | |
| if not token: | |
| raise RuntimeError("Hugging Face token nav iestatīts publicēšanai.") | |
| try: | |
| from huggingface_hub import HfApi | |
| except ImportError as exc: # pragma: no cover | |
| raise RuntimeError("huggingface_hub nav pieejams Space vidē.") from exc | |
| payload = content.encode("utf-8") | |
| api = HfApi(token=token) | |
| try: | |
| api.upload_file( | |
| path_or_fileobj=payload, | |
| path_in_repo=path_in_repo, | |
| repo_id=repo_id, | |
| repo_type=repo_type, | |
| commit_message=commit_message, | |
| ) | |
| except Exception as exc: # noqa: BLE001 | |
| detail = str(exc).strip() or type(exc).__name__ | |
| raise RuntimeError( | |
| f"Neizdevās saglabāt failu Hugging Face repozitorijā: {detail}." | |
| ) from exc | |
| return { | |
| "repo_id": repo_id, | |
| "repo_type": repo_type, | |
| "path": path_in_repo, | |
| "size_bytes": len(payload), | |
| "commit_message": commit_message, | |
| "saved": True, | |
| } | |
| def _training_defaults() -> dict[str, Any]: | |
| return { | |
| "dataset_repo": DEFAULT_DATASET_REPO, | |
| "hub_model_id": DEFAULT_HUB_MODEL_ID, | |
| "model_preset": "balanced", | |
| "model_name": "", | |
| "num_epochs": 3, | |
| "all_branches": False, | |
| "push_to_hub": True, | |
| "output_subdir": DEFAULT_OUTPUT_SUBDIR, | |
| "continue_from_latest_artifact": True, | |
| "continue_model_path": DEFAULT_OUTPUT_SUBDIR, | |
| } | |
| def _training_runtime_payload() -> dict[str, Any]: | |
| model_choices = [ | |
| { | |
| "id": preset_id, | |
| "label": f"{preset_id} — {config['model_name']}", | |
| **config, | |
| } | |
| for preset_id, config in list_space_model_choices().items() | |
| ] | |
| return { | |
| "model_choices": model_choices, | |
| "has_publish_token": _has_publish_token(), | |
| "defaults": _training_defaults(), | |
| } | |
| def _auto_training_request() -> SpaceTrainingRequest: | |
| defaults = _training_defaults() | |
| output_subdir = get_env_any_or_default( | |
| "MARIS_HUMAN_TRAINING_AUTO_TRAIN_OUTPUT_SUBDIR", | |
| "MARIS_SPACE_AUTO_TRAIN_OUTPUT_SUBDIR", | |
| default=str(defaults["output_subdir"]), | |
| ).strip() | |
| continue_model_path = get_env_any_or_default( | |
| "MARIS_HUMAN_TRAINING_AUTO_TRAIN_CONTINUE_MODEL_PATH", | |
| "MARIS_SPACE_AUTO_TRAIN_CONTINUE_MODEL_PATH", | |
| "MARIS_TRAIN_CONTINUE_MODEL_PATH", | |
| "HF_TRAIN_CONTINUE_MODEL_PATH", | |
| default="", | |
| ).strip() | |
| model_name = get_env_any_or_default( | |
| "MARIS_HUMAN_TRAINING_AUTO_TRAIN_MODEL_NAME", | |
| "MARIS_SPACE_AUTO_TRAIN_MODEL_NAME", | |
| "MARIS_TRAIN_BASE_MODEL", | |
| "HF_TRAIN_BASE_MODEL", | |
| default="", | |
| ).strip() | |
| model_preset = get_env_any_or_default( | |
| "MARIS_HUMAN_TRAINING_AUTO_TRAIN_MODEL_PRESET", | |
| "MARIS_SPACE_AUTO_TRAIN_MODEL_PRESET", | |
| "MARIS_TRAIN_MODEL_PRESET", | |
| "HF_TRAIN_MODEL_PRESET", | |
| default=str(defaults["model_preset"]), | |
| ).strip() | |
| return SpaceTrainingRequest( | |
| dataset_repo=get_env_any_or_default( | |
| "MARIS_HUMAN_TRAINING_AUTO_TRAIN_DATASET_REPO", | |
| "MARIS_MEMORY_REPO", | |
| "MARIS_DATASET_REPO", | |
| "HF_DATASET_REPO", | |
| default=str(defaults["dataset_repo"]), | |
| ), | |
| model_repo=get_env_any_or_default( | |
| "MARIS_HUMAN_TRAINING_AUTO_TRAIN_MODEL_REPO", | |
| "MARIS_HUMAN_TRAINING_MODEL_REPO", | |
| "MARIS_MODEL_REPO", | |
| "HF_MODEL_REPO", | |
| default=str(defaults["hub_model_id"]), | |
| ), | |
| model_preset="" if model_name else model_preset, | |
| model_name=model_name, | |
| num_epochs=_env_int( | |
| "MARIS_HUMAN_TRAINING_AUTO_TRAIN_NUM_EPOCHS", | |
| "MARIS_TRAIN_NUM_EPOCHS", | |
| "HF_TRAIN_NUM_EPOCHS", | |
| default=int(defaults["num_epochs"]), | |
| ), | |
| all_branches=_env_flag( | |
| "MARIS_HUMAN_TRAINING_AUTO_TRAIN_ALL_BRANCHES", | |
| "MARIS_SPACE_AUTO_TRAIN_ALL_BRANCHES", | |
| default=bool(defaults["all_branches"]), | |
| ), | |
| push_to_hub=_env_flag( | |
| "MARIS_HUMAN_TRAINING_AUTO_TRAIN_PUSH_TO_HUB", | |
| "MARIS_SPACE_AUTO_TRAIN_PUSH_TO_HUB", | |
| "MARIS_TRAIN_PUBLISH", | |
| "HF_TRAIN_PUSH_TO_HUB", | |
| default=bool(defaults["push_to_hub"]), | |
| ), | |
| output_subdir=output_subdir, | |
| continue_from_latest_artifact=_env_flag( | |
| "MARIS_HUMAN_TRAINING_AUTO_TRAIN_CONTINUE_FROM_LATEST", | |
| "MARIS_SPACE_AUTO_TRAIN_CONTINUE_FROM_LATEST", | |
| "MARIS_TRAIN_CONTINUE_FROM_LATEST", | |
| "HF_TRAIN_CONTINUE_FROM_LATEST", | |
| default=bool(defaults["continue_from_latest_artifact"]), | |
| ), | |
| continue_model_path=continue_model_path, | |
| ) | |
| def _maybe_start_automatic_training() -> None: | |
| if not _env_flag( | |
| "MARIS_HUMAN_TRAINING_AUTO_TRAIN", | |
| "MARIS_SPACE_AUTO_TRAIN", | |
| "HF_SPACE_AUTO_TRAIN", | |
| ): | |
| return | |
| try: | |
| request = _auto_training_request() | |
| output_dir = resolve_output_dir(str(PERSISTENT_DIR), request.output_subdir) | |
| force_start = _env_flag( | |
| "MARIS_HUMAN_TRAINING_AUTO_TRAIN_FORCE", | |
| "MARIS_SPACE_AUTO_TRAIN_FORCE", | |
| "HF_SPACE_AUTO_TRAIN_FORCE", | |
| default=False, | |
| ) | |
| if not force_start and has_completed_training_artifacts(output_dir): | |
| logger.info( | |
| "Izlaižu human training Space auto-startu, jo output jau satur pabeigta skrējiena artefaktus: %s", | |
| output_dir, | |
| ) | |
| return | |
| result = _start_training_process(request) | |
| logger.info( | |
| "Human training Space automātiskais treniņš palaists: pid=%s log=%s", | |
| result["pid"], | |
| result["log_path"], | |
| ) | |
| except Exception: # noqa: BLE001 | |
| logger.exception("Neizdevās automātiski palaist human training Space treniņu starta laikā.") | |
| def _startup_auto_training() -> None: | |
| _maybe_start_automatic_training() | |
| def _start_training_process(request: Any, *, run_id: str | None = None) -> dict[str, Any]: | |
| if bool(getattr(request, "push_to_hub", False)) and not _has_publish_token(): | |
| raise HTTPException( | |
| status_code=400, | |
| detail="Hugging Face token nav iestatīts, bet publicēšana uz Hub ir ieslēgta.", | |
| ) | |
| if not Path(TRAIN_SCRIPT).is_file(): | |
| raise HTTPException( | |
| status_code=500, detail="Treniņa skripts nav atrasts Space bundle vidē." | |
| ) | |
| LOG_DIR.mkdir(parents=True, exist_ok=True) | |
| request_payload = _request_payload_dict(request) | |
| if run_id: | |
| request_payload["run_id"] = run_id | |
| with STATE_LOCK: | |
| _sync_training_state_unlocked() | |
| process = TRAINING_STATE.get("process") | |
| if process is not None: | |
| raise HTTPException(status_code=409, detail="Treniņš jau darbojas.") | |
| _close_log_handle_unlocked() | |
| started_at = _timestamp() | |
| log_path = LOG_DIR / f"training-{started_at.replace(':', '-').replace('+00:00', 'Z')}.log" | |
| log_handle = log_path.open("a", encoding="utf-8") | |
| TRAINING_STATE.update( | |
| { | |
| "process": None, | |
| "log_path": str(log_path), | |
| "log_handle": log_handle, | |
| "started_at": started_at, | |
| "finished_at": None, | |
| "request": request_payload, | |
| "stop_requested": False, | |
| } | |
| ) | |
| _write_log_line_unlocked("Starting training request") | |
| _write_log_line_unlocked(json.dumps(request_payload, ensure_ascii=False)) | |
| env = build_space_training_env(os.environ.copy(), request, str(PERSISTENT_DIR)) | |
| command = build_space_training_command(TRAIN_SCRIPT, request) | |
| process = subprocess.Popen( # noqa: S603 | |
| command, | |
| cwd=str(REPO_ROOT), | |
| env=env, | |
| stdout=log_handle, | |
| stderr=subprocess.STDOUT, | |
| start_new_session=True, | |
| ) | |
| TRAINING_STATE["process"] = process | |
| return { | |
| "message": "Treniņš palaists human.training Space vidē.", | |
| "log_path": str(log_path), | |
| "pid": process.pid, | |
| } | |
| def _load_training_status() -> dict[str, Any]: | |
| with STATE_LOCK: | |
| _sync_training_state_unlocked() | |
| process = TRAINING_STATE.get("process") | |
| running = process is not None and process.poll() is None | |
| exit_code = None if process is None else process.poll() | |
| log_path = str(TRAINING_STATE.get("log_path") or "") | |
| request = TRAINING_STATE.get("request") | |
| started_at = TRAINING_STATE.get("started_at") | |
| finished_at = TRAINING_STATE.get("finished_at") | |
| requested_stop = bool(TRAINING_STATE.get("stop_requested")) | |
| log_text = tail_log(log_path, max_chars=32000) if log_path else "" | |
| progress = parse_training_progress( | |
| log_text, request=request, running=running, exit_code=exit_code | |
| ) | |
| run_id = "" | |
| if isinstance(request, dict): | |
| run_id = str(request.get("run_id") or "") | |
| if run_id and not running and exit_code is not None: | |
| update_run( | |
| PERSISTENT_DIR, | |
| run_id=run_id, | |
| status="completed" if exit_code == 0 else "failed", | |
| finished_at=finished_at or _timestamp(), | |
| exit_code=exit_code, | |
| ) | |
| return { | |
| "running": running, | |
| "exit_code": exit_code, | |
| "log_path": log_path, | |
| "log_tail": tail_log(log_path) if log_path else "", | |
| "request": request, | |
| "started_at": started_at, | |
| "finished_at": finished_at, | |
| "requested_stop": requested_stop, | |
| "can_stop": running, | |
| "has_publish_token": _has_publish_token(), | |
| "progress": progress, | |
| } | |
| def _render_cards(items: list[dict[str, str]], *, kind: str) -> str: | |
| cards: list[str] = [] | |
| for item in items: | |
| title = html.escape(item["title"]) | |
| summary = html.escape(item["summary"]) | |
| cards.append( | |
| "\n".join( | |
| [ | |
| f'<article class="info-card {kind}-card">', | |
| f' <div class="eyebrow">{kind}</div>', | |
| f" <h3>{title}</h3>", | |
| f" <p>{summary}</p>", | |
| "</article>", | |
| ] | |
| ) | |
| ) | |
| return "\n".join(cards) | |
| def _render_role_cards() -> str: | |
| cards: list[str] = [] | |
| for role_id, guide in ROLE_GUIDES.items(): | |
| label = html.escape(str(guide["label"])) | |
| headline = html.escape(str(guide["headline"])) | |
| safe_role_id = html.escape(role_id) | |
| responsibilities = "".join( | |
| f"<li>{html.escape(str(item))}</li>" for item in guide["responsibilities"] | |
| ) | |
| cards.append( | |
| f""" | |
| <article class="role-card" data-role="{safe_role_id}"> | |
| <div class="role-head"> | |
| <span class="role-tag">{label}</span> | |
| <span class="role-code">{safe_role_id}</span> | |
| </div> | |
| <h3>{label}</h3> | |
| <p>{headline}</p> | |
| <ul>{responsibilities}</ul> | |
| </article> | |
| """ | |
| ) | |
| return "\n".join(cards) | |
| def _render_list_items(items: list[str]) -> str: | |
| if not items: | |
| return "<li>Vēl nav datu.</li>" | |
| return "".join(f"<li>{html.escape(str(item))}</li>" for item in items) | |
| def _initial_workspace_payload() -> dict[str, object]: | |
| if not AUTH_REQUIRED: | |
| session = _build_private_space_session().model_dump() | |
| return { | |
| "active_class": "active", | |
| "title": f"{session['role_guide']['label']} workspace", | |
| "summary": str(session["role_guide"]["headline"]), | |
| "responsibilities": list(session["role_guide"]["responsibilities"]), | |
| "workflow": list(session["role_guide"]["workflow"]), | |
| "examples": list(session["platform"]["examples"]), | |
| "docs": [str(item["title"]) for item in session["platform"]["documentation"]], | |
| "session": session, | |
| } | |
| return { | |
| "active_class": "", | |
| "title": "Sveicināti platformā", | |
| "summary": "Izvēlies lomu vai izmanto privāto pieeju, lai sāktu darbu.", | |
| "responsibilities": [], | |
| "workflow": [], | |
| "examples": [], | |
| "docs": [], | |
| "session": None, | |
| } | |
| def _render_template_options() -> str: | |
| return "\n".join( | |
| f'<option value="{html.escape(template_id)}">{html.escape(str(template["label"]))}</option>' | |
| for template_id, template in STUDIO_TEMPLATES.items() | |
| ) | |
| def _access_panel() -> str: | |
| if not AUTH_REQUIRED: | |
| private_guide = ROLE_GUIDES[DEFAULT_PRIVATE_ROLE] | |
| private_label = html.escape(str(private_guide["label"])) | |
| return f""" | |
| <aside class="hero-panel side-panel" id="auth-shell"> | |
| <div class="eyebrow">Private Space</div> | |
| <h2>Workspace ir atvērts uzreiz</h2> | |
| <p class="muted">Šis Space ir paredzēts kontrolētai komandai, tāpēc darba vide ielādējas automātiski ar <strong>{private_label}</strong> profilu.</p> | |
| <div class="callout"> | |
| <strong>Kas ir gatavs:</strong> | |
| <ul> | |
| <li>staging preview ar profesionālu human training formu;</li> | |
| <li>artefaktu publicēšana dataset repozitorijā;</li> | |
| <li>reāls publish + train process ar dzīvo statusu un logiem.</li> | |
| </ul> | |
| </div> | |
| <div id="auth-status" class="status success">Privātais Space ir gatavs darbam.</div> | |
| </aside> | |
| """ | |
| role_options = "\n".join( | |
| f'<option value="{role_id}">{guide["label"]}</option>' | |
| for role_id, guide in ROLE_GUIDES.items() | |
| ) | |
| return f""" | |
| <aside class="hero-panel side-panel" id="auth-shell"> | |
| <div class="eyebrow">Secure access</div> | |
| <h2>Login vai Register</h2> | |
| <p class="muted">Ja privātais auto-access ir izslēgts, komanda ieiet Space ar sesiju un lomu profilu.</p> | |
| <div class="auth-grid"> | |
| <form id="login-form" class="stacked-form"> | |
| <h3>Login</h3> | |
| <label>E-pasts<input id="login-email" type="email" placeholder="komanda@maris.ai" required /></label> | |
| <label>Parole<input id="login-password" type="password" placeholder="Vismaz 8 simboli" required /></label> | |
| <button type="submit" class="primary-button">Login</button> | |
| </form> | |
| <form id="register-form" class="stacked-form"> | |
| <h3>Register</h3> | |
| <label>Vārds un uzvārds<input id="register-full-name" type="text" placeholder="Māris Ozols" required /></label> | |
| <label>E-pasts<input id="register-email" type="email" placeholder="komanda@maris.ai" required /></label> | |
| <label>Parole<input id="register-password" type="password" placeholder="Vismaz 8 simboli" required /></label> | |
| <label>Loma<select id="register-role">{role_options}</select></label> | |
| <button type="submit" class="primary-button">Register</button> | |
| </form> | |
| </div> | |
| <div id="auth-status" class="status">Izvēlies login vai register un turpini ar savu lomu.</div> | |
| </aside> | |
| """ | |
| def _runtime_payload() -> dict[str, Any]: | |
| return { | |
| "roles": ROLE_GUIDES, | |
| "workflow": PLATFORM_SECTIONS["workflow"], | |
| "documentation": PLATFORM_SECTIONS["documentation"], | |
| "templates": STUDIO_TEMPLATES, | |
| "studio_features": ["saved-drafts", "run-history", "artifact-browser"], | |
| "training": _training_runtime_payload(), | |
| "auth_required": AUTH_REQUIRED, | |
| "private_session": _build_private_space_session().model_dump() | |
| if not AUTH_REQUIRED | |
| else None, | |
| } | |
| def _render_index() -> str: | |
| workspace_bootstrap = _initial_workspace_payload() | |
| runtime_json = json.dumps(_runtime_payload(), ensure_ascii=False).replace("</", "<\\/") | |
| workspace_active_class = html.escape(str(workspace_bootstrap["active_class"])) | |
| workspace_title = html.escape(str(workspace_bootstrap["title"])) | |
| workspace_summary = html.escape(str(workspace_bootstrap["summary"])) | |
| default_dataset_repo_html = html.escape(DEFAULT_DATASET_REPO) | |
| default_hub_model_id_html = html.escape(DEFAULT_HUB_MODEL_ID) | |
| persistent_dir_html = html.escape(str(PERSISTENT_DIR)) | |
| conversation_placeholder = json.dumps( | |
| [ | |
| { | |
| "user": "Kas man ir svarīgi?", | |
| "assistant": "Tev svarīgas ir profesionālas, īsas atbildes latviešu valodā.", | |
| } | |
| ], | |
| ensure_ascii=False, | |
| ) | |
| preference_placeholder = json.dumps( | |
| [ | |
| { | |
| "prompt": "Apraksti manu profilu.", | |
| "chosen": "Tu vēlies strukturētas profesionālas atbildes latviski.", | |
| "rejected": "Es neko nezinu par tavām preferencēm.", | |
| } | |
| ], | |
| ensure_ascii=False, | |
| ) | |
| eval_placeholder = json.dumps( | |
| [ | |
| { | |
| "prompt": "Kā tu atbildēsi turpmāk?", | |
| "completion": "Profesionāli, skaidri un latviski.", | |
| } | |
| ], | |
| ensure_ascii=False, | |
| ) | |
| template_options = _render_template_options() | |
| return f"""<!doctype html> | |
| <html lang="lv"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <title>Maris AI Human Training</title> | |
| <style> | |
| :root {{ | |
| color-scheme: dark; | |
| --bg: #07101d; | |
| --panel: rgba(10, 20, 36, 0.92); | |
| --panel-border: rgba(148, 163, 184, 0.14); | |
| --text: #e8eef8; | |
| --muted: #9cb0c8; | |
| --accent: #4f8cff; | |
| --accent-strong: #2155d6; | |
| --success: #38d39f; | |
| --danger: #f87171; | |
| }} | |
| * {{ box-sizing: border-box; }} | |
| body {{ | |
| margin: 0; | |
| min-height: 100vh; | |
| background: | |
| radial-gradient(circle at top left, rgba(79, 140, 255, 0.22), transparent 28%), | |
| radial-gradient(circle at top right, rgba(56, 211, 159, 0.12), transparent 22%), | |
| var(--bg); | |
| color: var(--text); | |
| font-family: Inter, Arial, sans-serif; | |
| }} | |
| code {{ color: #d8e6ff; }} | |
| .page {{ max-width: 1360px; margin: 0 auto; padding: 28px 20px 56px; }} | |
| .hero {{ display: grid; grid-template-columns: 1.2fr 0.9fr; gap: 20px; align-items: stretch; }} | |
| .hero-panel, .section-panel, .workspace-panel, .role-card, .info-card {{ | |
| background: var(--panel); | |
| border: 1px solid var(--panel-border); | |
| border-radius: 24px; | |
| box-shadow: 0 24px 70px rgba(2, 6, 23, 0.34); | |
| }} | |
| .hero-panel, .section-panel, .workspace-panel {{ padding: 28px; }} | |
| .section {{ margin-top: 22px; }} | |
| .hero-copy .brand {{ display: flex; gap: 16px; align-items: center; }} | |
| .hero-copy img {{ width: 72px; height: 72px; border-radius: 20px; }} | |
| .hero-copy h1 {{ margin: 0; font-size: clamp(2rem, 4vw, 3.4rem); line-height: 1.05; }} | |
| .hero-copy p {{ margin: 12px 0 0; color: var(--muted); line-height: 1.7; }} | |
| .badge-row {{ display: flex; flex-wrap: wrap; gap: 10px; margin-top: 20px; }} | |
| .badge, .eyebrow, .role-tag {{ | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 8px; | |
| border-radius: 999px; | |
| padding: 8px 12px; | |
| font-weight: 700; | |
| font-size: 0.82rem; | |
| letter-spacing: 0.02em; | |
| background: rgba(79, 140, 255, 0.14); | |
| color: #d6e6ff; | |
| }} | |
| .eyebrow {{ text-transform: uppercase; }} | |
| .side-panel h2, .section h2, .workspace-panel h3 {{ margin: 12px 0; }} | |
| .muted, .role-card p, .role-card li, .info-card p, .workspace-panel p, .workspace-panel li {{ color: var(--muted); line-height: 1.65; }} | |
| .callout {{ margin-top: 18px; padding: 16px; border-radius: 18px; background: rgba(79, 140, 255, 0.08); border: 1px solid rgba(79, 140, 255, 0.18); }} | |
| .callout ul, .role-card ul, .workspace-panel ul {{ margin: 10px 0 0; padding-left: 18px; }} | |
| .role-grid, .info-grid, .workspace-grid, .form-grid {{ display: grid; gap: 18px; }} | |
| .role-grid, .info-grid {{ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }} | |
| .workspace-grid {{ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }} | |
| .role-card, .info-card {{ padding: 22px; }} | |
| .role-head {{ display: flex; justify-content: space-between; gap: 12px; align-items: center; }} | |
| .role-code {{ color: #9cb0c8; font-size: 0.92rem; text-transform: lowercase; font-family: ui-monospace, monospace; }} | |
| .auth-grid {{ display: grid; gap: 16px; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }} | |
| .stacked-form, .form-grid {{ display: grid; gap: 14px; }} | |
| label {{ display: grid; gap: 8px; font-size: 0.94rem; color: #dce7f9; }} | |
| input, select, textarea, button {{ font: inherit; }} | |
| input, select, textarea {{ | |
| width: 100%; | |
| border-radius: 16px; | |
| border: 1px solid rgba(148, 163, 184, 0.22); | |
| padding: 13px 14px; | |
| background: rgba(8, 15, 27, 0.85); | |
| color: var(--text); | |
| }} | |
| textarea {{ min-height: 120px; resize: vertical; }} | |
| .checkbox-group {{ display: grid; gap: 10px; align-content: start; }} | |
| .checkbox-label {{ display: flex; align-items: center; gap: 10px; }} | |
| .checkbox-label input {{ width: auto; }} | |
| .button-row {{ display: flex; flex-wrap: wrap; gap: 12px; }} | |
| .toolbar-row {{ display: flex; flex-wrap: wrap; gap: 12px; align-items: end; }} | |
| .toolbar-row > label {{ flex: 1 1 260px; }} | |
| .split-grid {{ display: grid; gap: 18px; grid-template-columns: 1.2fr 0.8fr; }} | |
| .primary-button, .secondary-button, .danger-button {{ | |
| border: 0; | |
| border-radius: 16px; | |
| padding: 13px 18px; | |
| font-weight: 800; | |
| color: white; | |
| cursor: pointer; | |
| }} | |
| .primary-button {{ background: linear-gradient(135deg, var(--accent-strong), var(--accent)); }} | |
| .secondary-button {{ background: rgba(79, 140, 255, 0.16); border: 1px solid rgba(79, 140, 255, 0.2); }} | |
| .danger-button {{ background: rgba(248, 113, 113, 0.16); border: 1px solid rgba(248, 113, 113, 0.26); }} | |
| .small-button {{ padding: 10px 14px; font-size: 0.9rem; }} | |
| .status {{ min-height: 24px; font-size: 0.94rem; color: var(--muted); }} | |
| .status.success {{ color: var(--success); }} | |
| .status.error {{ color: var(--danger); }} | |
| #workspace-shell {{ display: none; }} | |
| #workspace-shell.active {{ display: block; }} | |
| .metric-grid {{ display: grid; gap: 14px; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); margin: 16px 0; }} | |
| .metric-card {{ | |
| border-radius: 18px; | |
| border: 1px solid rgba(148, 163, 184, 0.14); | |
| background: rgba(4, 10, 19, 0.76); | |
| padding: 16px; | |
| }} | |
| .metric-card strong {{ display: block; font-size: 1.6rem; margin-top: 10px; }} | |
| .chip-list {{ display: flex; flex-wrap: wrap; gap: 10px; margin-top: 14px; }} | |
| .chip {{ | |
| border-radius: 999px; | |
| border: 1px solid rgba(79, 140, 255, 0.18); | |
| padding: 8px 12px; | |
| background: rgba(79, 140, 255, 0.1); | |
| color: #dce7f9; | |
| font-size: 0.88rem; | |
| }} | |
| .artifact-list {{ display: grid; gap: 10px; margin: 16px 0 0; padding: 0; list-style: none; }} | |
| .artifact-list li {{ | |
| border-radius: 16px; | |
| border: 1px solid rgba(148, 163, 184, 0.14); | |
| background: rgba(4, 10, 19, 0.76); | |
| padding: 14px 16px; | |
| }} | |
| .artifact-list strong {{ display: block; margin-bottom: 4px; }} | |
| .item-list {{ display: grid; gap: 10px; padding: 0; list-style: none; margin: 16px 0 0; }} | |
| .item-list li {{ | |
| border-radius: 16px; | |
| border: 1px solid rgba(148, 163, 184, 0.14); | |
| background: rgba(4, 10, 19, 0.76); | |
| padding: 14px 16px; | |
| }} | |
| .item-list button {{ | |
| width: auto; | |
| }} | |
| .item-meta {{ display: block; margin-top: 6px; color: var(--muted); font-size: 0.88rem; }} | |
| pre {{ | |
| margin: 0; | |
| min-height: 220px; | |
| max-height: 460px; | |
| overflow: auto; | |
| white-space: pre-wrap; | |
| word-break: break-word; | |
| border-radius: 18px; | |
| border: 1px solid rgba(148, 163, 184, 0.14); | |
| background: rgba(4, 10, 19, 0.92); | |
| padding: 18px; | |
| color: #dce7f9; | |
| }} | |
| @media (max-width: 980px) {{ | |
| .hero {{ grid-template-columns: 1fr; }} | |
| .split-grid {{ grid-template-columns: 1fr; }} | |
| }} | |
| </style> | |
| </head> | |
| <body> | |
| <main class="page"> | |
| <section class="hero"> | |
| <div class="hero-panel hero-copy"> | |
| <div class="brand"> | |
| <img src="{LOGO_URL}" alt="Maris AI logo" /> | |
| <div> | |
| <h1>Maris AI Human Training</h1> | |
| <p>Profesionāli pārbūvēta atsevišķa platforma priekš <strong>MarisUK/maris.ai.human.training</strong> ar skaidru onboarding, preview, publish un reālu train izpildi.</p> | |
| </div> | |
| </div> | |
| <div class="badge-row"> | |
| <span class="badge">Dedicated Space</span> | |
| <span class="badge">Private Space</span> | |
| <span class="badge">Professional workflow</span> | |
| <span class="badge">Publish + train</span> | |
| </div> | |
| <p>Šis Space ir būvēts tā, lai komanda vienā vietā sagatavotu human training ievadi, pārskatītu artefaktu manifestu, publicētu to dataset repozitorijā un droši palaistu treniņu ar dzīvu statusu.</p> | |
| </div> | |
| {_access_panel()} | |
| </section> | |
| <section class="section"> | |
| <div class="section-panel"> | |
| <div class="eyebrow">Roles</div> | |
| <h2>Lomas un atbildības</h2> | |
| <p class="muted">Owner, secretary, trainee un user katrs redz saprotamu atbildības zonu jau no pirmās dienas.</p> | |
| <div class="role-grid">{_render_role_cards()}</div> | |
| </div> | |
| </section> | |
| <section class="section"> | |
| <div class="section-panel"> | |
| <div class="eyebrow">Documentation</div> | |
| <h2>Platformas dokumentācija</h2> | |
| <p class="muted">Praktiski materiāli, lai darbs būtu vienots, auditējams un profesionāls.</p> | |
| <div class="info-grid">{_render_cards(PLATFORM_SECTIONS["documentation"], kind="doc")}</div> | |
| </div> | |
| </section> | |
| <section class="section"> | |
| <div class="section-panel"> | |
| <div class="eyebrow">Workflow</div> | |
| <h2>Darba plūsma</h2> | |
| <p class="muted">Vienkārša secība no ievades līdz apstiprinātam treniņa startam.</p> | |
| <div class="info-grid">{_render_cards(PLATFORM_SECTIONS["workflow"], kind="workflow")}</div> | |
| </div> | |
| </section> | |
| <section class="section {workspace_active_class}" id="workspace-shell"> | |
| <div class="workspace-grid"> | |
| <article class="workspace-panel"> | |
| <div class="eyebrow">Role dashboard</div> | |
| <h3 id="workspace-title">{workspace_title}</h3> | |
| <p id="workspace-summary">{workspace_summary}</p> | |
| <ul id="workspace-responsibilities">{_render_list_items(workspace_bootstrap["responsibilities"])}</ul> | |
| </article> | |
| <article class="workspace-panel"> | |
| <div class="eyebrow">What to do next</div> | |
| <h3>Nākamie soļi</h3> | |
| <ul id="workspace-workflow">{_render_list_items(workspace_bootstrap["workflow"])}</ul> | |
| </article> | |
| <article class="workspace-panel"> | |
| <div class="eyebrow">Examples</div> | |
| <h3>Lomas piemēri</h3> | |
| <ul id="workspace-examples">{_render_list_items(workspace_bootstrap["examples"])}</ul> | |
| </article> | |
| <article class="workspace-panel"> | |
| <div class="eyebrow">Documentation</div> | |
| <h3>Kas jāizlasa</h3> | |
| <ul id="workspace-docs">{_render_list_items(workspace_bootstrap["docs"])}</ul> | |
| </article> | |
| </div> | |
| <div class="workspace-grid" style="margin-top:18px;"> | |
| <article class="workspace-panel"> | |
| <div class="eyebrow">Studio mode</div> | |
| <h3>Ātra starta vadība</h3> | |
| <p class="muted">Izvēlies gatavu studijas šablonu, lai forma uzreiz piepildās ar profesionālu sākuma saturu.</p> | |
| <div class="toolbar-row"> | |
| <label>Studio template<select id="human-template-select">{template_options}</select></label> | |
| <button type="button" class="secondary-button" id="human-apply-template-button">Apply template</button> | |
| </div> | |
| <div class="chip-list"> | |
| <span class="chip" id="human-default-dataset-chip">Dataset: {default_dataset_repo_html}</span> | |
| <span class="chip" id="human-default-model-chip">Model: {default_hub_model_id_html}</span> | |
| <span class="chip">Persistent dir: {persistent_dir_html}</span> | |
| </div> | |
| <ul id="human-studio-checklist" style="margin-top:16px;"> | |
| <li>Izvēlies vai pielāgo studio template.</li> | |
| <li>Pārskati dataset/model repo un output path.</li> | |
| <li>Build preview, tad publicē artefaktus un tikai pēc tam startē treniņu.</li> | |
| </ul> | |
| </article> | |
| <article class="workspace-panel"> | |
| <div class="eyebrow">Review summary</div> | |
| <h3>Staging pārskats</h3> | |
| <p class="muted" id="human-summary-status">Kad izveidosi preview, te parādīsies galvenie kvalitātes signāli un artefaktu kopa.</p> | |
| <div class="metric-grid" id="human-summary-metrics"> | |
| <div class="metric-card"><div class="eyebrow">Train</div><strong>0</strong><span>ieraksti preview</span></div> | |
| <div class="metric-card"><div class="eyebrow">Eval</div><strong>0</strong><span>eval piemēri</span></div> | |
| <div class="metric-card"><div class="eyebrow">Prefs</div><strong>0</strong><span>preference pāri</span></div> | |
| <div class="metric-card"><div class="eyebrow">Duplicates</div><strong>0</strong><span>izņemtie dublikāti</span></div> | |
| </div> | |
| <ul class="artifact-list" id="human-published-artifacts"> | |
| <li><strong>Artefakti vēl nav publicēti.</strong><span>Pēc publish šeit redzēsi katra artefakta repo ceļu.</span></li> | |
| </ul> | |
| </article> | |
| <article class="workspace-panel"> | |
| <div class="eyebrow">Human training builder</div> | |
| <h3>Profesionāla advanced konfigurācija</h3> | |
| <p class="muted">Sagatavo kvalitātīvu preview, publicē artefaktus un tikai tad palaid treniņu.</p> | |
| <form id="human-training-form" class="form-grid"> | |
| <div class="toolbar-row"> | |
| <label>Draft name<input id="human-draft-name" type="text" value="Studio draft" /></label> | |
| <button type="button" class="secondary-button small-button" id="human-save-draft-button">Save draft</button> | |
| <button type="button" class="secondary-button small-button" id="human-refresh-studio-button">Refresh studio</button> | |
| </div> | |
| <div class="workspace-grid"> | |
| <label>Dataset repo<input id="human_dataset_repo" type="text" /></label> | |
| <label>Hub model ID<input id="human_hub_model_id" type="text" /></label> | |
| <label>Model preset<select id="human_model_preset"></select></label> | |
| <label>Model name<input id="human_model_name" type="text" placeholder="Piemēram, meta-llama/Llama-3.2-3B-Instruct" /></label> | |
| <label>Output subdir<input id="human_output_subdir" type="text" /></label> | |
| <label>Continue model path<input id="human_continue_model_path" type="text" /></label> | |
| <label>Num epochs<input id="human_num_epochs" type="number" min="1" max="100" /></label> | |
| <div class="checkbox-group"> | |
| <label class="checkbox-label"><input id="human_continue_from_latest_artefact" type="checkbox" /> continue_from_latest_artefact</label> | |
| <label class="checkbox-label"><input id="human_push_to_hub" type="checkbox" /> push_to_hub</label> | |
| <label class="checkbox-label"><input id="human_all_branches" type="checkbox" /> all_branches</label> | |
| </div> | |
| </div> | |
| <div class="workspace-grid"> | |
| <label>Profile facts<textarea id="human_profile_facts" placeholder="Viena rinda = viens fakts"></textarea></label> | |
| <label>Profile preferences<textarea id="human_profile_preferences" placeholder="Viena rinda = viena preference"></textarea></label> | |
| <label>Response instructions<textarea id="human_response_instructions" placeholder="Viena rinda = viena instrukcija"></textarea></label> | |
| </div> | |
| <div class="workspace-grid"> | |
| <label>Conversation examples (JSON array)<textarea id="human_conversation_examples">{conversation_placeholder}</textarea></label> | |
| <label>Preference pairs (JSON array)<textarea id="human_preference_pairs">{preference_placeholder}</textarea></label> | |
| <label>Eval examples (JSON array)<textarea id="human_eval_examples">{eval_placeholder}</textarea></label> | |
| </div> | |
| <div class="button-row"> | |
| <button type="submit" class="primary-button" id="human-build-button">Build preview</button> | |
| <button type="button" class="secondary-button" id="human-publish-button">Publish artifacts</button> | |
| <button type="button" class="secondary-button" id="human-execute-button">Publish + Train</button> | |
| <button type="button" class="danger-button" id="human-stop-button">Stop training</button> | |
| </div> | |
| </form> | |
| <p id="human-training-status-line" class="status">Gatavs staging priekšskatam.</p> | |
| <p id="human-training-meta-line" class="muted">Collect → review → publish → train</p> | |
| </article> | |
| <article class="workspace-panel"> | |
| <div class="eyebrow">Preview & live status</div> | |
| <h3>Manifests un treniņa statuss</h3> | |
| <p class="muted" id="human-token-state">Hub publish token: {"pieejams" if _has_publish_token() else "nav iestatīts"}</p> | |
| <pre id="human-training-preview">Artefaktu manifests parādīsies šeit.</pre> | |
| <h3 style="margin-top:18px;">Treniņa logs</h3> | |
| <pre id="training-log-output">Kad startēsi treniņu, te parādīsies dzīvais loga izgriezums.</pre> | |
| </article> | |
| </div> | |
| <div class="workspace-grid" style="margin-top:18px;"> | |
| <article class="workspace-panel"> | |
| <div class="eyebrow">Saved drafts</div> | |
| <h3>Saglabātie drafts</h3> | |
| <p class="muted">Saglabā darba versijas, ielādē tās atpakaļ formā un turpini no pēdējā stāvokļa.</p> | |
| <ul id="studio-draft-list" class="item-list"> | |
| <li><strong>Drafts vēl nav.</strong><span class="item-meta">Saglabā pirmo draft, lai sāktu versiju vēsturi.</span></li> | |
| </ul> | |
| </article> | |
| <article class="workspace-panel"> | |
| <div class="eyebrow">Run history</div> | |
| <h3>Run history</h3> | |
| <p class="muted">Redzi staged, published, running un pabeigtos run ierakstus vienā vietā.</p> | |
| <ul id="studio-run-list" class="item-list"> | |
| <li><strong>Run history vēl nav.</strong><span class="item-meta">Build preview izveidos pirmo run ierakstu.</span></li> | |
| </ul> | |
| </article> | |
| <article class="workspace-panel"> | |
| <div class="eyebrow">Artifact browser</div> | |
| <h3>Artifact browser</h3> | |
| <p class="muted">Pārlūko staged un publicētos artefaktus ar sample preview un repo ceļiem.</p> | |
| <ul id="studio-artifact-list" class="item-list"> | |
| <li><strong>Artefakti vēl nav indeksēti.</strong><span class="item-meta">Build preview vai publish pievienos artefaktu browser ierakstus.</span></li> | |
| </ul> | |
| <h3 style="margin-top:18px;">Artifact preview</h3> | |
| <pre id="studio-artifact-preview">Izvēlies artefaktu no saraksta, lai redzētu preview.</pre> | |
| </article> | |
| </div> | |
| </section> | |
| </main> | |
| <script> | |
| const runtime = {runtime_json}; | |
| const authStatus = document.getElementById("auth-status"); | |
| const workspaceShell = document.getElementById("workspace-shell"); | |
| const preview = document.getElementById("human-training-preview"); | |
| const logOutput = document.getElementById("training-log-output"); | |
| const publishButton = document.getElementById("human-publish-button"); | |
| const executeButton = document.getElementById("human-execute-button"); | |
| const stopButton = document.getElementById("human-stop-button"); | |
| const saveDraftButton = document.getElementById("human-save-draft-button"); | |
| const refreshStudioButton = document.getElementById("human-refresh-studio-button"); | |
| const applyTemplateButton = document.getElementById("human-apply-template-button"); | |
| const templateSelect = document.getElementById("human-template-select"); | |
| const draftNameInput = document.getElementById("human-draft-name"); | |
| const humanStatusLine = document.getElementById("human-training-status-line"); | |
| const humanMetaLine = document.getElementById("human-training-meta-line"); | |
| const tokenState = document.getElementById("human-token-state"); | |
| const summaryStatus = document.getElementById("human-summary-status"); | |
| const summaryMetrics = document.getElementById("human-summary-metrics"); | |
| const publishedArtifacts = document.getElementById("human-published-artifacts"); | |
| const draftList = document.getElementById("studio-draft-list"); | |
| const runList = document.getElementById("studio-run-list"); | |
| const artifactList = document.getElementById("studio-artifact-list"); | |
| const artifactPreview = document.getElementById("studio-artifact-preview"); | |
| let sessionToken = ""; | |
| let activeDraftId = ""; | |
| let activeRunId = ""; | |
| let activeManifest = null; | |
| let pollHandle = null; | |
| function sessionHeaders() {{ | |
| return sessionToken ? {{ "X-Session-Token": sessionToken }} : {{}}; | |
| }} | |
| async function requestJson(url, options = {{}}) {{ | |
| const response = await fetch(url, options); | |
| let body = {{}}; | |
| try {{ | |
| body = await response.json(); | |
| }} catch (_error) {{ | |
| body = {{}}; | |
| }} | |
| if (!response.ok) {{ | |
| throw new Error(body.detail || body.message || `Request failed: ${{response.status}}`); | |
| }} | |
| return body; | |
| }} | |
| function updateStatus(element, message, tone = "neutral") {{ | |
| element.textContent = message; | |
| element.className = `status${{tone === "neutral" ? "" : ` ${{tone}}`}}`; | |
| }} | |
| function fillList(targetId, items) {{ | |
| const target = document.getElementById(targetId); | |
| target.innerHTML = ""; | |
| for (const item of items || []) {{ | |
| const li = document.createElement("li"); | |
| li.textContent = typeof item === "string" ? item : item.title || item.summary || ""; | |
| target.appendChild(li); | |
| }} | |
| }} | |
| function setTextAreaJson(id, value) {{ | |
| const element = document.getElementById(id); | |
| if (!element) return; | |
| element.value = JSON.stringify(value || [], null, 2); | |
| }} | |
| function renderPublishedArtifacts(items = []) {{ | |
| publishedArtifacts.innerHTML = ""; | |
| if (!items.length) {{ | |
| publishedArtifacts.innerHTML = "<li><strong>Artefakti vēl nav publicēti.</strong><span>Pēc publish šeit redzēsi katra artefakta repo ceļu.</span></li>"; | |
| return; | |
| }} | |
| for (const item of items) {{ | |
| const li = document.createElement("li"); | |
| const artifact = item.artifact || "artifact"; | |
| const path = item.path || item.path_in_repo || "—"; | |
| const strong = document.createElement("strong"); | |
| strong.textContent = artifact; | |
| const span = document.createElement("span"); | |
| span.textContent = path; | |
| li.appendChild(strong); | |
| li.appendChild(span); | |
| publishedArtifacts.appendChild(li); | |
| }} | |
| }} | |
| function loadPayloadIntoForm(payload) {{ | |
| if (!payload) return; | |
| setDefault("human_dataset_repo", payload.dataset_repo || runtime.training.defaults?.dataset_repo || ""); | |
| setDefault("human_hub_model_id", payload.hub_model_id || payload.model_repo || runtime.training.defaults?.hub_model_id || ""); | |
| setDefault("human_model_preset", payload.model_preset || runtime.training.defaults?.model_preset || ""); | |
| setDefault("human_model_name", payload.model_name || ""); | |
| setDefault("human_num_epochs", payload.num_epochs || runtime.training.defaults?.num_epochs || 3); | |
| setDefault("human_output_subdir", payload.output_subdir || runtime.training.defaults?.output_subdir || ""); | |
| setDefault("human_continue_model_path", payload.continue_model_path || runtime.training.defaults?.continue_model_path || ""); | |
| setDefault("human_continue_from_latest_artefact", Boolean(payload.continue_from_latest_artifact)); | |
| setDefault("human_push_to_hub", payload.push_to_hub !== false); | |
| setDefault("human_all_branches", Boolean(payload.all_branches)); | |
| setDefault("human_profile_facts", (payload.profile_facts || []).join("\\n")); | |
| setDefault("human_profile_preferences", (payload.profile_preferences || []).join("\\n")); | |
| setDefault("human_response_instructions", (payload.response_instructions || []).join("\\n")); | |
| setTextAreaJson("human_conversation_examples", payload.conversation_examples || []); | |
| setTextAreaJson("human_preference_pairs", payload.preference_pairs || []); | |
| setTextAreaJson("human_eval_examples", payload.eval_examples || []); | |
| }} | |
| function renderDrafts(items = []) {{ | |
| draftList.innerHTML = ""; | |
| if (!items.length) {{ | |
| draftList.innerHTML = '<li><strong>Drafts vēl nav.</strong><span class="item-meta">Saglabā pirmo draft, lai sāktu versiju vēsturi.</span></li>'; | |
| return; | |
| }} | |
| for (const item of items) {{ | |
| const li = document.createElement("li"); | |
| const strong = document.createElement("strong"); | |
| strong.textContent = item.name; | |
| const meta = document.createElement("span"); | |
| meta.className = "item-meta"; | |
| meta.textContent = "updated: " + item.updated_at + " · id: " + item.draft_id; | |
| const actions = document.createElement("div"); | |
| actions.className = "button-row"; | |
| actions.style.marginTop = "10px"; | |
| const loadButton = document.createElement("button"); | |
| loadButton.type = "button"; | |
| loadButton.className = "secondary-button small-button"; | |
| loadButton.dataset.draftLoad = item.draft_id; | |
| loadButton.textContent = "Load"; | |
| const archiveButton = document.createElement("button"); | |
| archiveButton.type = "button"; | |
| archiveButton.className = "danger-button small-button"; | |
| archiveButton.dataset.draftArchive = item.draft_id; | |
| archiveButton.textContent = "Archive"; | |
| actions.appendChild(loadButton); | |
| actions.appendChild(archiveButton); | |
| li.appendChild(strong); | |
| li.appendChild(meta); | |
| li.appendChild(actions); | |
| draftList.appendChild(li); | |
| }} | |
| }} | |
| function renderRuns(items = []) {{ | |
| runList.innerHTML = ""; | |
| if (!items.length) {{ | |
| runList.innerHTML = '<li><strong>Run history vēl nav.</strong><span class="item-meta">Build preview izveidos pirmo run ierakstu.</span></li>'; | |
| return; | |
| }} | |
| for (const item of items) {{ | |
| const li = document.createElement("li"); | |
| const strong = document.createElement("strong"); | |
| strong.textContent = item.run_id; | |
| const meta = document.createElement("span"); | |
| meta.className = "item-meta"; | |
| meta.textContent = "status: " + item.status + " · artifacts: " + (item.artifact_count || 0) + " · updated: " + item.updated_at; | |
| const actions = document.createElement("div"); | |
| actions.className = "button-row"; | |
| actions.style.marginTop = "10px"; | |
| const openButton = document.createElement("button"); | |
| openButton.type = "button"; | |
| openButton.className = "secondary-button small-button"; | |
| openButton.dataset.runLoad = item.run_id; | |
| openButton.textContent = "Open"; | |
| const artifactsButton = document.createElement("button"); | |
| artifactsButton.type = "button"; | |
| artifactsButton.className = "secondary-button small-button"; | |
| artifactsButton.dataset.runArtifacts = item.run_id; | |
| artifactsButton.textContent = "Artifacts"; | |
| actions.appendChild(openButton); | |
| actions.appendChild(artifactsButton); | |
| li.appendChild(strong); | |
| li.appendChild(meta); | |
| li.appendChild(actions); | |
| runList.appendChild(li); | |
| }} | |
| }} | |
| function renderArtifactList(items = []) {{ | |
| artifactList.innerHTML = ""; | |
| if (!items.length) {{ | |
| artifactList.innerHTML = '<li><strong>Artefakti vēl nav indeksēti.</strong><span class="item-meta">Build preview vai publish pievienos artefaktu browser ierakstus.</span></li>'; | |
| return; | |
| }} | |
| for (const item of items) {{ | |
| const li = document.createElement("li"); | |
| const strong = document.createElement("strong"); | |
| strong.textContent = item.artifact_name; | |
| const meta = document.createElement("span"); | |
| meta.className = "item-meta"; | |
| meta.textContent = "run: " + item.run_id + " · records: " + (item.record_count || 0) + " · " + (item.published ? "published" : "staged"); | |
| const actions = document.createElement("div"); | |
| actions.className = "button-row"; | |
| actions.style.marginTop = "10px"; | |
| const previewButton = document.createElement("button"); | |
| previewButton.type = "button"; | |
| previewButton.className = "secondary-button small-button"; | |
| previewButton.dataset.artifactOpen = item.artifact_id; | |
| previewButton.textContent = "Preview"; | |
| actions.appendChild(previewButton); | |
| li.appendChild(strong); | |
| li.appendChild(meta); | |
| li.appendChild(actions); | |
| artifactList.appendChild(li); | |
| }} | |
| }} | |
| async function refreshDrafts() {{ | |
| const body = await requestJson("/api/studio/drafts", {{ headers: sessionHeaders() }}); | |
| renderDrafts(body.drafts || []); | |
| }} | |
| async function refreshRuns() {{ | |
| const body = await requestJson("/api/studio/runs", {{ headers: sessionHeaders() }}); | |
| renderRuns(body.runs || []); | |
| }} | |
| async function refreshArtifacts(runId = "") {{ | |
| const suffix = runId ? `?run_id=${{encodeURIComponent(runId)}}` : ""; | |
| const body = await requestJson(`/api/studio/artifacts${{suffix}}`, {{ headers: sessionHeaders() }}); | |
| renderArtifactList(body.artifacts || []); | |
| }} | |
| async function refreshStudio() {{ | |
| if (runtime.auth_required && !sessionToken) return; | |
| await Promise.all([refreshDrafts(), refreshRuns(), refreshArtifacts(activeRunId)]); | |
| }} | |
| async function loadDraft(draftId) {{ | |
| const body = await requestJson(`/api/studio/drafts/${{encodeURIComponent(draftId)}}`, {{ headers: sessionHeaders() }}); | |
| activeDraftId = body.draft.draft_id; | |
| draftNameInput.value = body.draft.name || "Studio draft"; | |
| loadPayloadIntoForm(body.draft.payload || {{}}); | |
| humanStatusLine.textContent = `Ielādēts draft: ${{body.draft.name}}`; | |
| humanMetaLine.textContent = `Draft ID: ${{body.draft.draft_id}}`; | |
| }} | |
| async function openRun(runId) {{ | |
| const body = await requestJson(`/api/studio/runs/${{encodeURIComponent(runId)}}`, {{ headers: sessionHeaders() }}); | |
| activeRunId = body.run.run_id; | |
| activeManifest = body.run.manifest || null; | |
| if (activeManifest) {{ | |
| renderManifestSummary(activeManifest); | |
| preview.textContent = JSON.stringify(activeManifest, null, 2); | |
| const artifactBody = await requestJson(`/api/studio/artifacts?run_id=${{encodeURIComponent(runId)}}`, {{ headers: sessionHeaders() }}); | |
| renderPublishedArtifacts((artifactBody.artifacts || []).map((item) => ({{ | |
| artifact: item.artifact_name, | |
| path: item.repo_path || item.local_path || "—", | |
| }}))); | |
| }} | |
| humanStatusLine.textContent = `Atvērts run: ${{runId}}`; | |
| humanMetaLine.textContent = `Statuss: ${{body.run.status}}`; | |
| }} | |
| async function openArtifact(artifactId) {{ | |
| const body = await requestJson(`/api/studio/artifacts/${{encodeURIComponent(artifactId)}}`, {{ headers: sessionHeaders() }}); | |
| artifactPreview.textContent = JSON.stringify(body.artifact, null, 2); | |
| }} | |
| function renderManifestSummary(manifest) {{ | |
| activeManifest = manifest || null; | |
| if (!manifest) {{ | |
| summaryStatus.textContent = "Kad izveidosi preview, te parādīsies galvenie kvalitātes signāli un artefaktu kopa."; | |
| summaryMetrics.innerHTML = ` | |
| <div class="metric-card"><div class="eyebrow">Train</div><strong>0</strong><span>ieraksti preview</span></div> | |
| <div class="metric-card"><div class="eyebrow">Eval</div><strong>0</strong><span>eval piemēri</span></div> | |
| <div class="metric-card"><div class="eyebrow">Prefs</div><strong>0</strong><span>preference pāri</span></div> | |
| <div class="metric-card"><div class="eyebrow">Duplicates</div><strong>0</strong><span>izņemtie dublikāti</span></div> | |
| `; | |
| return; | |
| }} | |
| const input = manifest.input_summary || {{}}; | |
| const quality = manifest.quality_report || {{}}; | |
| const trainCount = manifest.artifacts?.train_dataset?.record_count || quality.train_kept_records || 0; | |
| const evalCount = manifest.artifacts?.eval_dataset?.record_count || input.eval_examples || 0; | |
| const prefCount = manifest.artifacts?.preference_dataset?.record_count || input.preference_pairs || 0; | |
| const duplicates = quality.duplicates_removed || 0; | |
| summaryStatus.textContent = `Run ${{manifest.run_id}} ir sagatavots. Review ready: ${{manifest.ready_for_review ? "jā" : "nē"}} · Training ready: ${{manifest.ready_for_training ? "jā" : "nē"}}`; | |
| summaryMetrics.innerHTML = ` | |
| <div class="metric-card"><div class="eyebrow">Train</div><strong>${{trainCount}}</strong><span>ieraksti preview</span></div> | |
| <div class="metric-card"><div class="eyebrow">Eval</div><strong>${{evalCount}}</strong><span>eval piemēri</span></div> | |
| <div class="metric-card"><div class="eyebrow">Prefs</div><strong>${{prefCount}}</strong><span>preference pāri</span></div> | |
| <div class="metric-card"><div class="eyebrow">Duplicates</div><strong>${{duplicates}}</strong><span>izņemtie dublikāti</span></div> | |
| `; | |
| }} | |
| function applyTemplate(templateId) {{ | |
| const template = runtime.templates?.[templateId]; | |
| if (!template || !template.payload) return; | |
| const payload = template.payload; | |
| setDefault("human_profile_facts", (payload.profile_facts || []).join("\\n")); | |
| setDefault("human_profile_preferences", (payload.profile_preferences || []).join("\\n")); | |
| setDefault("human_response_instructions", (payload.response_instructions || []).join("\\n")); | |
| setTextAreaJson("human_conversation_examples", payload.conversation_examples || []); | |
| setTextAreaJson("human_preference_pairs", payload.preference_pairs || []); | |
| setTextAreaJson("human_eval_examples", payload.eval_examples || []); | |
| activeRunId = ""; | |
| renderPublishedArtifacts([]); | |
| humanStatusLine.textContent = `Pielietots studio template: ${{template.label}}`; | |
| humanMetaLine.textContent = template.summary || "Template pielietots."; | |
| }} | |
| function renderWorkspace(session) {{ | |
| const guide = session.role_guide || runtime.roles[session.user.role]; | |
| document.getElementById("workspace-title").textContent = `${{guide.label}} workspace`; | |
| document.getElementById("workspace-summary").textContent = guide.headline; | |
| fillList("workspace-responsibilities", guide.responsibilities || []); | |
| fillList("workspace-workflow", guide.workflow || []); | |
| fillList("workspace-examples", session.platform?.examples || guide.examples || []); | |
| fillList("workspace-docs", (session.platform?.documentation || []).map((item) => item.title)); | |
| workspaceShell.classList.add("active"); | |
| }} | |
| function setDefault(id, value) {{ | |
| const element = document.getElementById(id); | |
| if (!element) return; | |
| if (element.type === "checkbox") {{ | |
| element.checked = Boolean(value); | |
| }} else if (value !== undefined && value !== null) {{ | |
| element.value = value; | |
| }} | |
| }} | |
| function configureTrainingForm() {{ | |
| const defaults = runtime.training.defaults || {{}}; | |
| const choices = runtime.training.model_choices || []; | |
| const preset = document.getElementById("human_model_preset"); | |
| preset.innerHTML = ""; | |
| for (const choice of choices) {{ | |
| const option = document.createElement("option"); | |
| option.value = choice.id; | |
| option.textContent = choice.label; | |
| preset.appendChild(option); | |
| }} | |
| setDefault("human_dataset_repo", defaults.dataset_repo); | |
| setDefault("human_hub_model_id", defaults.hub_model_id); | |
| setDefault("human_model_preset", defaults.model_preset); | |
| setDefault("human_model_name", defaults.model_name); | |
| setDefault("human_num_epochs", defaults.num_epochs); | |
| setDefault("human_output_subdir", defaults.output_subdir); | |
| setDefault("human_continue_model_path", defaults.continue_model_path); | |
| setDefault("human_continue_from_latest_artefact", defaults.continue_from_latest_artifact); | |
| setDefault("human_push_to_hub", defaults.push_to_hub); | |
| setDefault("human_all_branches", defaults.all_branches); | |
| tokenState.textContent = `Hub publish token: ${{runtime.training.has_publish_token ? "pieejams" : "nav iestatīts"}}`; | |
| publishButton.disabled = !runtime.training.has_publish_token; | |
| executeButton.disabled = !runtime.training.has_publish_token; | |
| stopButton.disabled = true; | |
| if (templateSelect && templateSelect.value) {{ | |
| applyTemplate(templateSelect.value); | |
| }} | |
| }} | |
| function readLines(id) {{ | |
| return document.getElementById(id).value.split("\n").map((item) => item.trim()).filter(Boolean); | |
| }} | |
| function parseJsonArray(id, label) {{ | |
| const raw = document.getElementById(id).value.trim(); | |
| if (!raw) return []; | |
| let parsed; | |
| try {{ | |
| parsed = JSON.parse(raw); | |
| }} catch (_error) {{ | |
| throw new Error(`${{label}} jābūt derīgam JSON masīvam.`); | |
| }} | |
| if (!Array.isArray(parsed)) {{ | |
| throw new Error(`${{label}} jābūt JSON masīvam.`); | |
| }} | |
| return parsed; | |
| }} | |
| function collectPayload() {{ | |
| return {{ | |
| dataset_repo: document.getElementById("human_dataset_repo").value, | |
| hub_model_id: document.getElementById("human_hub_model_id").value, | |
| model_preset: document.getElementById("human_model_preset").value, | |
| model_name: document.getElementById("human_model_name").value, | |
| num_epochs: Number(document.getElementById("human_num_epochs").value), | |
| output_subdir: document.getElementById("human_output_subdir").value, | |
| continue_model_path: document.getElementById("human_continue_model_path").value, | |
| continue_from_latest_artifact: document.getElementById("human_continue_from_latest_artefact").checked, | |
| all_branches: document.getElementById("human_all_branches").checked, | |
| push_to_hub: document.getElementById("human_push_to_hub").checked, | |
| profile_facts: readLines("human_profile_facts"), | |
| profile_preferences: readLines("human_profile_preferences"), | |
| response_instructions: readLines("human_response_instructions"), | |
| conversation_examples: parseJsonArray("human_conversation_examples", "Conversation examples"), | |
| preference_pairs: parseJsonArray("human_preference_pairs", "Preference pairs"), | |
| eval_examples: parseJsonArray("human_eval_examples", "Eval examples"), | |
| }}; | |
| }} | |
| async function buildPreview() {{ | |
| humanStatusLine.textContent = "Veidoju staging artefaktus…"; | |
| humanMetaLine.textContent = "Validēju ievadi un sagatavoju manifestu pārskatam."; | |
| const suffix = activeDraftId ? `?draft_id=${{encodeURIComponent(activeDraftId)}}` : ""; | |
| const body = await requestJson(`/api/human-training/build${{suffix}}`, {{ | |
| method: "POST", | |
| headers: {{ "Content-Type": "application/json", ...sessionHeaders() }}, | |
| body: JSON.stringify(collectPayload()), | |
| }}); | |
| activeRunId = body.run_id; | |
| renderManifestSummary(body.manifest); | |
| renderPublishedArtifacts([]); | |
| preview.textContent = JSON.stringify(body.manifest, null, 2); | |
| humanStatusLine.textContent = "Artefakti sagatavoti pārskatam."; | |
| humanMetaLine.textContent = `Run ID: ${{body.run_id}} · Vari publicēt artefaktus un startēt treniņu.`; | |
| await refreshStudio(); | |
| return body; | |
| }} | |
| async function refreshTrainingStatus() {{ | |
| if (runtime.auth_required && !sessionToken) return; | |
| try {{ | |
| const body = await requestJson("/api/training/status", {{ headers: sessionHeaders() }}); | |
| const progress = body.progress || {{}}; | |
| tokenState.textContent = `Hub publish token: ${{body.has_publish_token ? "pieejams" : "nav iestatīts"}}`; | |
| humanMetaLine.textContent = [ | |
| progress.label || "Gaida treniņa startu", | |
| body.started_at ? `started: ${{body.started_at}}` : null, | |
| body.finished_at ? `finished: ${{body.finished_at}}` : null, | |
| body.request?.hub_model_id ? `hub_model_id: ${{body.request.hub_model_id}}` : null, | |
| ].filter(Boolean).join(" · "); | |
| if (body.running) {{ | |
| humanStatusLine.textContent = `Treniņš darbojas · ${{progress.percent || 0}}%`; | |
| }} else if (body.exit_code === 0) {{ | |
| humanStatusLine.textContent = "Treniņš pabeigts veiksmīgi."; | |
| }} else if (body.exit_code !== null && body.exit_code !== undefined) {{ | |
| humanStatusLine.textContent = `Treniņš beidzās ar kļūdu (exit ${{body.exit_code}}).`; | |
| }} | |
| logOutput.textContent = body.log_tail || "Logu vēl nav."; | |
| stopButton.disabled = !body.can_stop; | |
| publishButton.disabled = body.running || !body.has_publish_token; | |
| executeButton.disabled = body.running || !body.has_publish_token; | |
| await refreshRuns(); | |
| }} catch (error) {{ | |
| humanMetaLine.textContent = error.message; | |
| }} | |
| }} | |
| function startPolling() {{ | |
| if (pollHandle) clearInterval(pollHandle); | |
| pollHandle = setInterval(refreshTrainingStatus, 2000); | |
| refreshTrainingStatus(); | |
| }} | |
| const loginForm = document.getElementById("login-form"); | |
| const registerForm = document.getElementById("register-form"); | |
| if (loginForm) {{ | |
| loginForm.addEventListener("submit", async (event) => {{ | |
| event.preventDefault(); | |
| updateStatus(authStatus, "Notiek ielogošanās..."); | |
| try {{ | |
| const body = await requestJson("/api/auth/login", {{ | |
| method: "POST", | |
| headers: {{ "Content-Type": "application/json" }}, | |
| body: JSON.stringify({{ | |
| email: document.getElementById("login-email").value, | |
| password: document.getElementById("login-password").value, | |
| }}), | |
| }}); | |
| sessionToken = body.token; | |
| renderWorkspace(body); | |
| configureTrainingForm(); | |
| updateStatus(authStatus, "Sesija izveidota.", "success"); | |
| startPolling(); | |
| }} catch (error) {{ | |
| updateStatus(authStatus, error.message, "error"); | |
| }} | |
| }}); | |
| }} | |
| if (registerForm) {{ | |
| registerForm.addEventListener("submit", async (event) => {{ | |
| event.preventDefault(); | |
| updateStatus(authStatus, "Veidojam kontu..."); | |
| try {{ | |
| const body = await requestJson("/api/auth/register", {{ | |
| method: "POST", | |
| headers: {{ "Content-Type": "application/json" }}, | |
| body: JSON.stringify({{ | |
| full_name: document.getElementById("register-full-name").value, | |
| email: document.getElementById("register-email").value, | |
| password: document.getElementById("register-password").value, | |
| role: document.getElementById("register-role").value, | |
| }}), | |
| }}); | |
| sessionToken = body.token; | |
| renderWorkspace(body); | |
| configureTrainingForm(); | |
| updateStatus(authStatus, "Konts izveidots un sesija aktīva.", "success"); | |
| startPolling(); | |
| }} catch (error) {{ | |
| updateStatus(authStatus, error.message, "error"); | |
| }} | |
| }}); | |
| }} | |
| if (!runtime.auth_required && runtime.private_session) {{ | |
| renderWorkspace(runtime.private_session); | |
| updateStatus(authStatus, "Privātais Space ir atvērts bez login.", "success"); | |
| startPolling(); | |
| }} | |
| configureTrainingForm(); | |
| renderManifestSummary(null); | |
| renderPublishedArtifacts([]); | |
| refreshStudio(); | |
| document.getElementById("human-training-form").addEventListener("submit", async (event) => {{ | |
| event.preventDefault(); | |
| try {{ | |
| await buildPreview(); | |
| }} catch (error) {{ | |
| humanStatusLine.textContent = "Neizdevās sagatavot artefaktus."; | |
| humanMetaLine.textContent = error.message; | |
| preview.textContent = error.message; | |
| }} | |
| }}); | |
| applyTemplateButton.addEventListener("click", () => {{ | |
| applyTemplate(templateSelect.value); | |
| }}); | |
| saveDraftButton.addEventListener("click", async () => {{ | |
| try {{ | |
| const body = await requestJson("/api/studio/drafts", {{ | |
| method: "POST", | |
| headers: {{ "Content-Type": "application/json", ...sessionHeaders() }}, | |
| body: JSON.stringify({{ | |
| draft_id: activeDraftId, | |
| name: draftNameInput.value || "Studio draft", | |
| payload: collectPayload(), | |
| }}), | |
| }}); | |
| activeDraftId = body.draft.draft_id; | |
| draftNameInput.value = body.draft.name; | |
| humanStatusLine.textContent = "Draft saglabāts."; | |
| humanMetaLine.textContent = `Draft ID: ${{body.draft.draft_id}}`; | |
| await refreshDrafts(); | |
| }} catch (error) {{ | |
| humanStatusLine.textContent = "Neizdevās saglabāt draft."; | |
| humanMetaLine.textContent = error.message; | |
| }} | |
| }}); | |
| refreshStudioButton.addEventListener("click", async () => {{ | |
| try {{ | |
| await refreshStudio(); | |
| humanMetaLine.textContent = "Studio dati atjaunoti."; | |
| }} catch (error) {{ | |
| humanMetaLine.textContent = error.message; | |
| }} | |
| }}); | |
| draftList.addEventListener("click", async (event) => {{ | |
| const target = event.target; | |
| if (!(target instanceof HTMLElement)) return; | |
| const loadId = target.getAttribute("data-draft-load"); | |
| const archiveId = target.getAttribute("data-draft-archive"); | |
| try {{ | |
| if (loadId) {{ | |
| await loadDraft(loadId); | |
| return; | |
| }} | |
| if (archiveId) {{ | |
| await requestJson(`/api/studio/drafts/${{encodeURIComponent(archiveId)}}`, {{ | |
| method: "DELETE", | |
| headers: sessionHeaders(), | |
| }}); | |
| if (activeDraftId === archiveId) activeDraftId = ""; | |
| await refreshDrafts(); | |
| }} | |
| }} catch (error) {{ | |
| humanMetaLine.textContent = error.message; | |
| }} | |
| }}); | |
| runList.addEventListener("click", async (event) => {{ | |
| const target = event.target; | |
| if (!(target instanceof HTMLElement)) return; | |
| const runId = target.getAttribute("data-run-load") || target.getAttribute("data-run-artifacts"); | |
| if (!runId) return; | |
| try {{ | |
| if (target.getAttribute("data-run-load")) {{ | |
| await openRun(runId); | |
| }} else {{ | |
| activeRunId = runId; | |
| await refreshArtifacts(runId); | |
| humanMetaLine.textContent = `Filtrēti artefakti run: ${{runId}}`; | |
| }} | |
| }} catch (error) {{ | |
| humanMetaLine.textContent = error.message; | |
| }} | |
| }}); | |
| artifactList.addEventListener("click", async (event) => {{ | |
| const target = event.target; | |
| if (!(target instanceof HTMLElement)) return; | |
| const artifactId = target.getAttribute("data-artifact-open"); | |
| if (!artifactId) return; | |
| try {{ | |
| await openArtifact(artifactId); | |
| }} catch (error) {{ | |
| artifactPreview.textContent = error.message; | |
| }} | |
| }}); | |
| publishButton.addEventListener("click", async () => {{ | |
| try {{ | |
| if (!activeRunId) await buildPreview(); | |
| humanStatusLine.textContent = "Publicēju artefaktus dataset repozitorijā…"; | |
| const body = await requestJson("/api/human-training/execute", {{ | |
| method: "POST", | |
| headers: {{ "Content-Type": "application/json", ...sessionHeaders() }}, | |
| body: JSON.stringify({{ run_id: activeRunId, publish_artifacts: true, start_training: false }}), | |
| }}); | |
| preview.textContent = JSON.stringify(body, null, 2); | |
| renderPublishedArtifacts(body.published || []); | |
| humanStatusLine.textContent = "Artefakti publicēti."; | |
| humanMetaLine.textContent = `${{body.published.length}} artefakti publicēti bez treniņa starta.`; | |
| await refreshStudio(); | |
| }} catch (error) {{ | |
| humanStatusLine.textContent = "Artefaktu publicēšana neizdevās."; | |
| humanMetaLine.textContent = error.message; | |
| preview.textContent = error.message; | |
| }} | |
| }}); | |
| executeButton.addEventListener("click", async () => {{ | |
| try {{ | |
| if (!activeRunId) await buildPreview(); | |
| humanStatusLine.textContent = "Publicēju artefaktus un startēju treniņu…"; | |
| const body = await requestJson("/api/human-training/execute", {{ | |
| method: "POST", | |
| headers: {{ "Content-Type": "application/json", ...sessionHeaders() }}, | |
| body: JSON.stringify({{ run_id: activeRunId, publish_artifacts: true, start_training: true }}), | |
| }}); | |
| preview.textContent = JSON.stringify(body, null, 2); | |
| renderPublishedArtifacts(body.published || []); | |
| humanStatusLine.textContent = "Artefakti publicēti un treniņš palaists."; | |
| humanMetaLine.textContent = `${{body.published.length}} artefakti publicēti · PID: ${{body.training?.pid || "—"}}`; | |
| await refreshStudio(); | |
| startPolling(); | |
| }} catch (error) {{ | |
| humanStatusLine.textContent = "Human training izpilde neizdevās."; | |
| humanMetaLine.textContent = error.message; | |
| preview.textContent = error.message; | |
| }} | |
| }}); | |
| stopButton.addEventListener("click", async () => {{ | |
| try {{ | |
| const body = await requestJson("/api/training/stop", {{ | |
| method: "POST", | |
| headers: {{ "Content-Type": "application/json", ...sessionHeaders() }}, | |
| }}); | |
| humanMetaLine.textContent = body.message; | |
| await refreshTrainingStatus(); | |
| }} catch (error) {{ | |
| humanMetaLine.textContent = error.message; | |
| }} | |
| }}); | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| def index() -> HTMLResponse: | |
| return HTMLResponse(_render_index()) | |
| def health() -> dict[str, str]: | |
| return {"service": "maris-human-training-space", "status": "ok"} | |
| def runtime() -> dict[str, object]: | |
| return { | |
| "service": "maris-human-training-space", | |
| "roles": ROLE_GUIDES, | |
| "workflow": PLATFORM_SECTIONS["workflow"], | |
| "documentation": PLATFORM_SECTIONS["documentation"], | |
| "templates": STUDIO_TEMPLATES, | |
| "studio_features": ["saved-drafts", "run-history", "artifact-browser"], | |
| "training": _training_runtime_payload(), | |
| "auth_required": AUTH_REQUIRED, | |
| "private_session": _build_private_space_session().model_dump() | |
| if not AUTH_REQUIRED | |
| else None, | |
| } | |
| def register(payload: RegisterRequest) -> SessionResponse: | |
| with USER_LOCK: | |
| users = _load_users() | |
| if payload.email in users: | |
| raise HTTPException(status_code=409, detail="Konts ar šo e-pastu jau eksistē.") | |
| password_hash, password_salt = _hash_password(payload.password) | |
| users[payload.email] = { | |
| "full_name": payload.full_name, | |
| "email": payload.email, | |
| "role": payload.role, | |
| "password_hash": password_hash, | |
| "password_salt": password_salt, | |
| "registered_at": _timestamp(), | |
| } | |
| _save_users(users) | |
| token = _create_session(payload.email) | |
| return _build_session_response(users[payload.email], token) | |
| def login(payload: LoginRequest) -> SessionResponse: | |
| with USER_LOCK: | |
| users = _load_users() | |
| record = users.get(payload.email) | |
| if record is None or not _verify_password( | |
| payload.password, | |
| password_hash=record["password_hash"], | |
| password_salt=record["password_salt"], | |
| ): | |
| raise HTTPException(status_code=401, detail="Nepareizs e-pasts vai parole.") | |
| token = _create_session(payload.email) | |
| return _build_session_response(record, token) | |
| def studio_drafts( | |
| x_session_token: str | None = Header(default=None, alias="X-Session-Token"), | |
| ) -> dict[str, Any]: | |
| user = _require_user(x_session_token) | |
| return {"drafts": list_drafts(PERSISTENT_DIR, user_email=_user_email(user))} | |
| def studio_save_draft( | |
| request: StudioDraftSaveRequest, | |
| x_session_token: str | None = Header(default=None, alias="X-Session-Token"), | |
| ) -> dict[str, Any]: | |
| user = _require_user(x_session_token) | |
| draft = save_draft( | |
| PERSISTENT_DIR, | |
| user_email=_user_email(user), | |
| name=request.name, | |
| payload=request.payload.model_dump(), | |
| draft_id=request.draft_id or None, | |
| ) | |
| return {"message": "Draft saglabāts.", "draft": draft} | |
| def studio_get_draft( | |
| draft_id: str, | |
| x_session_token: str | None = Header(default=None, alias="X-Session-Token"), | |
| ) -> dict[str, Any]: | |
| user = _require_user(x_session_token) | |
| draft = get_draft(PERSISTENT_DIR, draft_id) | |
| if not draft or draft.get("owner_email") != _user_email(user) or draft.get("archived"): | |
| raise HTTPException(status_code=404, detail="Draft nav atrasts.") | |
| return {"draft": draft} | |
| def studio_archive_draft( | |
| draft_id: str, | |
| x_session_token: str | None = Header(default=None, alias="X-Session-Token"), | |
| ) -> dict[str, Any]: | |
| user = _require_user(x_session_token) | |
| draft = archive_draft(PERSISTENT_DIR, draft_id=draft_id, user_email=_user_email(user)) | |
| if not draft: | |
| raise HTTPException(status_code=404, detail="Draft nav atrasts.") | |
| return {"message": "Draft arhivēts.", "draft": draft} | |
| def studio_runs( | |
| x_session_token: str | None = Header(default=None, alias="X-Session-Token"), | |
| ) -> dict[str, Any]: | |
| user = _require_user(x_session_token) | |
| return {"runs": list_runs(PERSISTENT_DIR, user_email=_user_email(user))} | |
| def studio_run( | |
| run_id: str, | |
| x_session_token: str | None = Header(default=None, alias="X-Session-Token"), | |
| ) -> dict[str, Any]: | |
| user = _require_user(x_session_token) | |
| run = get_run(PERSISTENT_DIR, run_id) | |
| if not run or run.get("owner_email") != _user_email(user): | |
| raise HTTPException(status_code=404, detail="Run nav atrasts.") | |
| return {"run": run} | |
| def studio_artifacts( | |
| x_session_token: str | None = Header(default=None, alias="X-Session-Token"), | |
| run_id: str | None = None, | |
| ) -> dict[str, Any]: | |
| user = _require_user(x_session_token) | |
| return { | |
| "artifacts": list_artifacts( | |
| PERSISTENT_DIR, | |
| user_email=_user_email(user), | |
| run_id=run_id, | |
| ) | |
| } | |
| def studio_artifact( | |
| artifact_id: str, | |
| x_session_token: str | None = Header(default=None, alias="X-Session-Token"), | |
| ) -> dict[str, Any]: | |
| user = _require_user(x_session_token) | |
| artifact = get_artifact(PERSISTENT_DIR, artifact_id) | |
| if not artifact or artifact.get("owner_email") != _user_email(user): | |
| raise HTTPException(status_code=404, detail="Artefakts nav atrasts.") | |
| return {"artifact": artifact} | |
| def build_human_training( | |
| request: HumanTrainingRequest, | |
| x_session_token: str | None = Header(default=None, alias="X-Session-Token"), | |
| draft_id: str | None = None, | |
| ) -> dict[str, Any]: | |
| user = _require_user(x_session_token) | |
| manifest = stage_human_training_artifacts(request, persistent_dir=str(PERSISTENT_DIR)) | |
| run_record = save_run( | |
| PERSISTENT_DIR, | |
| manifest=manifest, | |
| user_email=_user_email(user), | |
| draft_id=draft_id, | |
| ) | |
| artifacts = index_artifacts( | |
| PERSISTENT_DIR, | |
| manifest=manifest, | |
| user_email=_user_email(user), | |
| ) | |
| return { | |
| "message": "Human training artefakti sagatavoti staging pārskatam.", | |
| "run_id": manifest["run_id"], | |
| "manifest": manifest, | |
| "run": run_record, | |
| "artifacts": artifacts, | |
| } | |
| def execute_human_training( | |
| request: HumanTrainingExecuteRequest, | |
| x_session_token: str | None = Header(default=None, alias="X-Session-Token"), | |
| ) -> dict[str, Any]: | |
| user = _require_user(x_session_token) | |
| if request.publish_artifacts and not _has_publish_token(): | |
| raise HTTPException( | |
| status_code=400, | |
| detail="Hugging Face token nav iestatīts, tāpēc nevar publicēt human training artefaktus.", | |
| ) | |
| try: | |
| manifest = load_human_training_manifest(str(PERSISTENT_DIR), request.run_id) | |
| except FileNotFoundError as exc: | |
| raise HTTPException(status_code=404, detail=str(exc)) from exc | |
| try: | |
| published = ( | |
| publish_human_training_artifacts(manifest, save_file=_save_huggingface_repo_text_file) | |
| if request.publish_artifacts | |
| else [] | |
| ) | |
| except RuntimeError as exc: | |
| raise HTTPException(status_code=502, detail=str(exc)) from exc | |
| training_result = None | |
| if request.publish_artifacts: | |
| index_artifacts( | |
| PERSISTENT_DIR, | |
| manifest=manifest, | |
| user_email=_user_email(user), | |
| published=published, | |
| ) | |
| update_run( | |
| PERSISTENT_DIR, | |
| run_id=request.run_id, | |
| status="published" if not request.start_training else "starting", | |
| published_at=_timestamp(), | |
| ) | |
| if request.start_training: | |
| training_result = _start_training_process( | |
| build_human_training_launch_spec(manifest), | |
| run_id=request.run_id, | |
| ) | |
| update_run( | |
| PERSISTENT_DIR, | |
| run_id=request.run_id, | |
| status="running", | |
| training_started_at=_timestamp(), | |
| ) | |
| return { | |
| "message": "Human training staging izpildīts.", | |
| "run_id": manifest["run_id"], | |
| "published": published, | |
| "training": training_result, | |
| } | |
| def training_status( | |
| x_session_token: str | None = Header(default=None, alias="X-Session-Token"), | |
| ) -> dict[str, Any]: | |
| _require_user(x_session_token) | |
| return _load_training_status() | |
| def stop_training( | |
| x_session_token: str | None = Header(default=None, alias="X-Session-Token"), | |
| ) -> dict[str, Any]: | |
| _require_user(x_session_token) | |
| with STATE_LOCK: | |
| _sync_training_state_unlocked() | |
| process = TRAINING_STATE.get("process") | |
| if process is None or process.poll() is not None: | |
| raise HTTPException(status_code=409, detail="Nav aktīva treniņa, ko apturēt.") | |
| TRAINING_STATE["stop_requested"] = True | |
| _write_log_line_unlocked("Stop requested by user") | |
| exit_code = terminate_process_tree(process) | |
| with STATE_LOCK: | |
| _write_log_line_unlocked("Training stopped by user") | |
| run_id = "" | |
| request = TRAINING_STATE.get("request") | |
| if isinstance(request, dict): | |
| run_id = str(request.get("run_id") or "") | |
| TRAINING_STATE["process"] = None | |
| TRAINING_STATE["finished_at"] = _timestamp() | |
| _close_log_handle_unlocked() | |
| if run_id: | |
| update_run( | |
| PERSISTENT_DIR, | |
| run_id=run_id, | |
| status="stopped", | |
| finished_at=_timestamp(), | |
| exit_code=exit_code, | |
| ) | |
| return { | |
| "message": "Treniņa apturēšanas signāls nosūtīts.", | |
| "exit_code": exit_code, | |
| } | |
| app = APP | |