Spaces:
Sleeping
Sleeping
GitHub Actions deploy 9ccdda2e1f1a3b66eb560f07afdd07994be52372
Browse files- core-python/.env.example +3 -0
- core-python/maris_core/__main__.py +1 -0
- core-python/maris_core/autonomous/agent.py +88 -7
- core-python/maris_core/autonomous/executor.py +3 -1
- core-python/maris_core/autonomous/planner.py +37 -8
- core-python/maris_core/autonomous/session_store.py +3 -1
- core-python/maris_core/code/generate_code.py +94 -27
- core-python/maris_core/data/quality.py +8 -10
- core-python/maris_core/data/validator.py +8 -2
- core-python/maris_core/orchestrator/routing.py +6 -2
- core-python/maris_core/space_agent.py +28 -15
- core-python/maris_core/text/benchmark.py +3 -8
- core-python/maris_core/text/evals.py +15 -5
- core-python/maris_core/text/generate.py +6 -2
- core-python/maris_core/training/config.py +62 -2
- core-python/maris_core/training/hf_compat.py +9 -3
- core-python/maris_core/training/human_training.py +19 -11
- core-python/maris_core/training/space_ui.py +3 -0
- core-python/maris_core/training/train.py +176 -15
- core-python/scripts/export_to_hf.py +20 -1
- core-python/tests/test_autonomous.py +46 -0
- core-python/tests/test_autonomous_runtime.py +9 -2
- core-python/tests/test_code.py +16 -6
- core-python/tests/test_huggingface_human_training_space_studio.py +3 -1
- core-python/tests/test_huggingface_sync.py +384 -0
- core-python/tests/test_human_training.py +8 -2
- core-python/tests/test_space_agent.py +17 -8
- core-python/tests/test_space_ui.py +14 -0
- core-python/tests/test_text.py +12 -4
- core-python/tests/test_text_benchmark.py +3 -1
- core-python/tests/test_training_hf_compat.py +19 -7
- core-python/tests/test_training_pipeline.py +247 -5
- huggingface_human_training_space/app.py +1 -1
core-python/.env.example
CHANGED
|
@@ -7,6 +7,9 @@ MARIS_REPO_TOKEN=your_maris_repo_token_here
|
|
| 7 |
# HUGGING_FACE_HUB_TOKEN=your_huggingface_token_here
|
| 8 |
# HUGGINGFACEHUB_API_TOKEN=your_huggingface_token_here
|
| 9 |
MARIS_MEMORY_REPO=MarisUK/maris-ai-memory
|
|
|
|
|
|
|
|
|
|
| 10 |
MARIS_MODEL_REPO=MarisUK/maris-ai-master
|
| 11 |
MARIS_AGENT_SPACE_REPO=MarisUK/maris.ai.agent
|
| 12 |
MARIS_HUMAN_TRAINING_SPACE_REPO=MarisUK/maris.ai.human.training
|
|
|
|
| 7 |
# HUGGING_FACE_HUB_TOKEN=your_huggingface_token_here
|
| 8 |
# HUGGINGFACEHUB_API_TOKEN=your_huggingface_token_here
|
| 9 |
MARIS_MEMORY_REPO=MarisUK/maris-ai-memory
|
| 10 |
+
MARIS_GLOBAL_MEMORY_REPO=MarisUK/maris-ai-memory
|
| 11 |
+
MARIS_EVAL_DATASET_REPO=MarisUK/maris-ai-evals
|
| 12 |
+
MARIS_BENCHMARK_DATASET_REPO=MarisUK/maris-ai-benchmark
|
| 13 |
MARIS_MODEL_REPO=MarisUK/maris-ai-master
|
| 14 |
MARIS_AGENT_SPACE_REPO=MarisUK/maris.ai.agent
|
| 15 |
MARIS_HUMAN_TRAINING_SPACE_REPO=MarisUK/maris.ai.human.training
|
core-python/maris_core/__main__.py
CHANGED
|
@@ -25,6 +25,7 @@ async def lifespan(_: FastAPI):
|
|
| 25 |
warm_text_model_runtime()
|
| 26 |
yield
|
| 27 |
|
|
|
|
| 28 |
app = FastAPI(
|
| 29 |
title="Maris AI Core Python",
|
| 30 |
description="MI kodols: teksts, attēli, audio, video, kods, aģents",
|
|
|
|
| 25 |
warm_text_model_runtime()
|
| 26 |
yield
|
| 27 |
|
| 28 |
+
|
| 29 |
app = FastAPI(
|
| 30 |
title="Maris AI Core Python",
|
| 31 |
description="MI kodols: teksts, attēli, audio, video, kods, aģents",
|
core-python/maris_core/autonomous/agent.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
|
|
|
| 5 |
import logging
|
| 6 |
import uuid
|
| 7 |
from datetime import UTC, datetime
|
|
@@ -24,9 +25,13 @@ CODE_GENERATION_KEYWORDS = ("kod", "api", "script", "python", "rust")
|
|
| 24 |
WEB_RESEARCH_KEYWORDS = ("meklē", "research", "search", "salīdzini")
|
| 25 |
WEB_AUTOMATION_KEYWORDS = ("browser", "pārlūk", "form", "klikš", "scrape", "web")
|
| 26 |
VALIDATION_KEYWORDS = ("test", "verify", "pārbaud")
|
|
|
|
| 27 |
|
| 28 |
# Karstais runtime cache virs persistenta session store.
|
| 29 |
_sessions: dict[str, dict[str, Any]] = {}
|
|
|
|
|
|
|
|
|
|
| 30 |
planner = Planner()
|
| 31 |
_AUTONOMOUS_AGENT_ROLES = [
|
| 32 |
{
|
|
@@ -298,14 +303,88 @@ async def _load_session(session_id: str) -> dict[str, Any]:
|
|
| 298 |
restored = await session_store.load_session(session_id)
|
| 299 |
if restored is None:
|
| 300 |
return {}
|
| 301 |
-
restored.setdefault(
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
|
|
|
|
|
|
|
|
|
| 305 |
_sessions[session_id] = restored
|
| 306 |
return restored
|
| 307 |
|
| 308 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 309 |
def _infer_tool(description: str) -> str:
|
| 310 |
lowered = description.lower()
|
| 311 |
if any(token in lowered for token in CODE_GENERATION_KEYWORDS):
|
|
@@ -663,7 +742,9 @@ async def _advance_session(session_id: str) -> None:
|
|
| 663 |
ready_task["result"] = f"Mēģinājums {ready_task['attempts']} neizdevās: {exc}"
|
| 664 |
ready_task["failure_class"] = exc.failure_class
|
| 665 |
ready_task.setdefault("metrics", {})["failure_class"] = exc.failure_class
|
| 666 |
-
session.setdefault("telemetry", {}).setdefault("failure_classes", []).append(
|
|
|
|
|
|
|
| 667 |
_append_event(
|
| 668 |
session,
|
| 669 |
event_type="task.failed_attempt",
|
|
@@ -847,6 +928,7 @@ async def start_session(req: StartRequest) -> SessionResponse:
|
|
| 847 |
)
|
| 848 |
await _persist_session(req.session_id, session)
|
| 849 |
await _advance_session(req.session_id)
|
|
|
|
| 850 |
|
| 851 |
return _build_session_response(req.session_id, session)
|
| 852 |
|
|
@@ -856,6 +938,5 @@ async def get_status(req: StatusRequest) -> SessionResponse:
|
|
| 856 |
"""Atgriež sesijas statusu."""
|
| 857 |
session = await _load_session(req.session_id)
|
| 858 |
if session:
|
| 859 |
-
await
|
| 860 |
-
session = await _load_session(req.session_id)
|
| 861 |
return _build_session_response(req.session_id, session)
|
|
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
+
import asyncio
|
| 6 |
import logging
|
| 7 |
import uuid
|
| 8 |
from datetime import UTC, datetime
|
|
|
|
| 25 |
WEB_RESEARCH_KEYWORDS = ("meklē", "research", "search", "salīdzini")
|
| 26 |
WEB_AUTOMATION_KEYWORDS = ("browser", "pārlūk", "form", "klikš", "scrape", "web")
|
| 27 |
VALIDATION_KEYWORDS = ("test", "verify", "pārbaud")
|
| 28 |
+
AUTONOMOUS_BACKGROUND_LOOP_DELAY_SECONDS = 0.05
|
| 29 |
|
| 30 |
# Karstais runtime cache virs persistenta session store.
|
| 31 |
_sessions: dict[str, dict[str, Any]] = {}
|
| 32 |
+
_session_runners: dict[str, asyncio.Task[None]] = {}
|
| 33 |
+
_session_runner_lock: asyncio.Lock | None = None
|
| 34 |
+
_session_runner_lock_loop: asyncio.AbstractEventLoop | None = None
|
| 35 |
planner = Planner()
|
| 36 |
_AUTONOMOUS_AGENT_ROLES = [
|
| 37 |
{
|
|
|
|
| 303 |
restored = await session_store.load_session(session_id)
|
| 304 |
if restored is None:
|
| 305 |
return {}
|
| 306 |
+
restored.setdefault(
|
| 307 |
+
"_checkpoint_keys",
|
| 308 |
+
{
|
| 309 |
+
(checkpoint.get("label", ""), checkpoint.get("task_id", "") or "")
|
| 310 |
+
for checkpoint in restored.get("checkpoints", [])
|
| 311 |
+
},
|
| 312 |
+
)
|
| 313 |
_sessions[session_id] = restored
|
| 314 |
return restored
|
| 315 |
|
| 316 |
|
| 317 |
+
async def _run_session_until_terminal(session_id: str) -> None:
|
| 318 |
+
try:
|
| 319 |
+
while True:
|
| 320 |
+
session = await _load_session(session_id)
|
| 321 |
+
if not session or session.get("status") in {"completed", "failed"}:
|
| 322 |
+
return
|
| 323 |
+
|
| 324 |
+
await _advance_session(session_id)
|
| 325 |
+
|
| 326 |
+
session = await _load_session(session_id)
|
| 327 |
+
if not session or session.get("status") in {"completed", "failed"}:
|
| 328 |
+
return
|
| 329 |
+
|
| 330 |
+
await asyncio.sleep(AUTONOMOUS_BACKGROUND_LOOP_DELAY_SECONDS)
|
| 331 |
+
except asyncio.CancelledError:
|
| 332 |
+
raise
|
| 333 |
+
except Exception as exc: # noqa: BLE001
|
| 334 |
+
logger.exception("Autonomous background runner neizdevās sesijai %s: %s", session_id, exc)
|
| 335 |
+
session = await _load_session(session_id)
|
| 336 |
+
if session and session.get("status") not in {"completed", "failed"}:
|
| 337 |
+
session["status"] = "failed"
|
| 338 |
+
_set_agent_role_status(session, "reviewer", "attention")
|
| 339 |
+
_append_event(
|
| 340 |
+
session,
|
| 341 |
+
event_type="session.runtime_failed",
|
| 342 |
+
title="Autonomous runtime failed",
|
| 343 |
+
detail=f"Fona izpildītājs apstājās ar kļūdu: {exc}",
|
| 344 |
+
agent_role="operator",
|
| 345 |
+
level="warning",
|
| 346 |
+
interruptible=True,
|
| 347 |
+
)
|
| 348 |
+
_ensure_checkpoint(
|
| 349 |
+
session,
|
| 350 |
+
label="Runtime failure checkpoint",
|
| 351 |
+
summary="Sesija apstājās fona runtime kļūdas dēļ un ir atjaunojama no checkpointa.",
|
| 352 |
+
status="recoverable",
|
| 353 |
+
)
|
| 354 |
+
await _persist_session(session_id, session)
|
| 355 |
+
finally:
|
| 356 |
+
current_task = asyncio.current_task()
|
| 357 |
+
async with _get_session_runner_lock():
|
| 358 |
+
if current_task is not None and _session_runners.get(session_id) is current_task:
|
| 359 |
+
_session_runners.pop(session_id, None)
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
def _get_session_runner_lock() -> asyncio.Lock:
|
| 363 |
+
global _session_runner_lock # noqa: PLW0603
|
| 364 |
+
global _session_runner_lock_loop # noqa: PLW0603
|
| 365 |
+
|
| 366 |
+
loop = asyncio.get_running_loop()
|
| 367 |
+
if _session_runner_lock is None or _session_runner_lock_loop is not loop:
|
| 368 |
+
_session_runner_lock = asyncio.Lock()
|
| 369 |
+
_session_runner_lock_loop = loop
|
| 370 |
+
return _session_runner_lock
|
| 371 |
+
|
| 372 |
+
|
| 373 |
+
async def _ensure_session_runner(session_id: str) -> None:
|
| 374 |
+
session = await _load_session(session_id)
|
| 375 |
+
if not session or session.get("status") in {"completed", "failed"}:
|
| 376 |
+
return
|
| 377 |
+
|
| 378 |
+
async with _get_session_runner_lock():
|
| 379 |
+
existing = _session_runners.get(session_id)
|
| 380 |
+
if existing is not None and not existing.done():
|
| 381 |
+
return
|
| 382 |
+
_session_runners[session_id] = asyncio.create_task(
|
| 383 |
+
_run_session_until_terminal(session_id),
|
| 384 |
+
name=f"maris-autonomous-{session_id}",
|
| 385 |
+
)
|
| 386 |
+
|
| 387 |
+
|
| 388 |
def _infer_tool(description: str) -> str:
|
| 389 |
lowered = description.lower()
|
| 390 |
if any(token in lowered for token in CODE_GENERATION_KEYWORDS):
|
|
|
|
| 742 |
ready_task["result"] = f"Mēģinājums {ready_task['attempts']} neizdevās: {exc}"
|
| 743 |
ready_task["failure_class"] = exc.failure_class
|
| 744 |
ready_task.setdefault("metrics", {})["failure_class"] = exc.failure_class
|
| 745 |
+
session.setdefault("telemetry", {}).setdefault("failure_classes", []).append(
|
| 746 |
+
exc.failure_class
|
| 747 |
+
)
|
| 748 |
_append_event(
|
| 749 |
session,
|
| 750 |
event_type="task.failed_attempt",
|
|
|
|
| 928 |
)
|
| 929 |
await _persist_session(req.session_id, session)
|
| 930 |
await _advance_session(req.session_id)
|
| 931 |
+
await _ensure_session_runner(req.session_id)
|
| 932 |
|
| 933 |
return _build_session_response(req.session_id, session)
|
| 934 |
|
|
|
|
| 938 |
"""Atgriež sesijas statusu."""
|
| 939 |
session = await _load_session(req.session_id)
|
| 940 |
if session:
|
| 941 |
+
await _ensure_session_runner(req.session_id)
|
|
|
|
| 942 |
return _build_session_response(req.session_id, session)
|
core-python/maris_core/autonomous/executor.py
CHANGED
|
@@ -258,7 +258,9 @@ class AutonomousTaskExecutor:
|
|
| 258 |
) -> TaskExecutionResult:
|
| 259 |
del task, goal, persona_id, session_id
|
| 260 |
dependency_tasks = [
|
| 261 |
-
candidate
|
|
|
|
|
|
|
| 262 |
]
|
| 263 |
if not dependency_tasks:
|
| 264 |
raise TaskExecutionError(
|
|
|
|
| 258 |
) -> TaskExecutionResult:
|
| 259 |
del task, goal, persona_id, session_id
|
| 260 |
dependency_tasks = [
|
| 261 |
+
candidate
|
| 262 |
+
for candidate in tasks
|
| 263 |
+
if candidate["status"] == "completed" and candidate.get("result")
|
| 264 |
]
|
| 265 |
if not dependency_tasks:
|
| 266 |
raise TaskExecutionError(
|
core-python/maris_core/autonomous/planner.py
CHANGED
|
@@ -2,7 +2,6 @@
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
-
import re
|
| 6 |
from typing import Any
|
| 7 |
|
| 8 |
from maris_core.memory_context import MemoryMatch
|
|
@@ -11,7 +10,8 @@ _CODE_KEYWORDS = ("kod", "api", "script", "python", "rust", "refactor", "fix")
|
|
| 11 |
_BROWSER_KEYWORDS = ("browser", "web", "pārlūk", "klikš", "scrape", "form", "http://", "https://")
|
| 12 |
_VALIDATION_KEYWORDS = ("test", "verify", "pārbaud", "validate", "review")
|
| 13 |
_RESEARCH_KEYWORDS = ("research", "meklē", "salīdzini", "izpēti")
|
| 14 |
-
|
|
|
|
| 15 |
|
| 16 |
|
| 17 |
class Planner:
|
|
@@ -37,11 +37,7 @@ class Planner:
|
|
| 37 |
if not normalized_goal:
|
| 38 |
return []
|
| 39 |
|
| 40 |
-
chunks
|
| 41 |
-
for chunk in _SEPARATOR_PATTERN.split(normalized_goal):
|
| 42 |
-
cleaned = chunk.strip(" -")
|
| 43 |
-
if cleaned:
|
| 44 |
-
chunks.append(cleaned)
|
| 45 |
candidate_steps = chunks[: max_steps - 1] if chunks else []
|
| 46 |
if not candidate_steps:
|
| 47 |
candidate_steps.append(normalized_goal)
|
|
@@ -69,7 +65,8 @@ class Planner:
|
|
| 69 |
"depends_on_steps": [index - 1] if index > 1 else [],
|
| 70 |
"execution_policy": "sequential",
|
| 71 |
"risk_level": risk_level,
|
| 72 |
-
"approval_required": tool
|
|
|
|
| 73 |
"max_attempts": 1 if tool == "validation" else 2,
|
| 74 |
"observability_tags": ["autonomous", tool, f"risk:{risk_level}"],
|
| 75 |
}
|
|
@@ -87,3 +84,35 @@ class Planner:
|
|
| 87 |
if any(keyword in lowered for keyword in _RESEARCH_KEYWORDS):
|
| 88 |
return "web_research"
|
| 89 |
return "reasoning"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
|
|
|
| 5 |
from typing import Any
|
| 6 |
|
| 7 |
from maris_core.memory_context import MemoryMatch
|
|
|
|
| 10 |
_BROWSER_KEYWORDS = ("browser", "web", "pārlūk", "klikš", "scrape", "form", "http://", "https://")
|
| 11 |
_VALIDATION_KEYWORDS = ("test", "verify", "pārbaud", "validate", "review")
|
| 12 |
_RESEARCH_KEYWORDS = ("research", "meklē", "salīdzini", "izpēti")
|
| 13 |
+
_PUNCTUATION_SEPARATORS = {",", ";", "->"}
|
| 14 |
+
_WORD_SEPARATORS = {"and", "un", "then"}
|
| 15 |
|
| 16 |
|
| 17 |
class Planner:
|
|
|
|
| 37 |
if not normalized_goal:
|
| 38 |
return []
|
| 39 |
|
| 40 |
+
chunks = _split_goal_chunks(normalized_goal)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
candidate_steps = chunks[: max_steps - 1] if chunks else []
|
| 42 |
if not candidate_steps:
|
| 43 |
candidate_steps.append(normalized_goal)
|
|
|
|
| 65 |
"depends_on_steps": [index - 1] if index > 1 else [],
|
| 66 |
"execution_policy": "sequential",
|
| 67 |
"risk_level": risk_level,
|
| 68 |
+
"approval_required": tool
|
| 69 |
+
in {"browser_automation", "code_generation", "validation"},
|
| 70 |
"max_attempts": 1 if tool == "validation" else 2,
|
| 71 |
"observability_tags": ["autonomous", tool, f"risk:{risk_level}"],
|
| 72 |
}
|
|
|
|
| 84 |
if any(keyword in lowered for keyword in _RESEARCH_KEYWORDS):
|
| 85 |
return "web_research"
|
| 86 |
return "reasoning"
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def _split_goal_chunks(goal: str) -> list[str]:
|
| 90 |
+
normalized = goal.replace("->", " -> ").replace(",", " , ").replace(";", " ; ")
|
| 91 |
+
tokens = normalized.split()
|
| 92 |
+
chunks: list[str] = []
|
| 93 |
+
current_tokens: list[str] = []
|
| 94 |
+
index = 0
|
| 95 |
+
|
| 96 |
+
while index < len(tokens):
|
| 97 |
+
token = tokens[index]
|
| 98 |
+
lowered = token.lower()
|
| 99 |
+
next_lowered = tokens[index + 1].lower() if index + 1 < len(tokens) else ""
|
| 100 |
+
is_separator = (
|
| 101 |
+
token in _PUNCTUATION_SEPARATORS
|
| 102 |
+
or lowered in _WORD_SEPARATORS
|
| 103 |
+
or (lowered == "un" and next_lowered == "tad")
|
| 104 |
+
)
|
| 105 |
+
if is_separator:
|
| 106 |
+
if current_tokens:
|
| 107 |
+
chunks.append(" ".join(current_tokens).strip(" -"))
|
| 108 |
+
current_tokens = []
|
| 109 |
+
index += 2 if lowered == "un" and next_lowered == "tad" else 1
|
| 110 |
+
continue
|
| 111 |
+
|
| 112 |
+
current_tokens.append(token)
|
| 113 |
+
index += 1
|
| 114 |
+
|
| 115 |
+
if current_tokens:
|
| 116 |
+
chunks.append(" ".join(current_tokens).strip(" -"))
|
| 117 |
+
|
| 118 |
+
return [chunk for chunk in chunks if chunk]
|
core-python/maris_core/autonomous/session_store.py
CHANGED
|
@@ -139,7 +139,9 @@ class AutonomousSessionStore:
|
|
| 139 |
try:
|
| 140 |
await asyncio.to_thread(_write)
|
| 141 |
except Exception as exc: # noqa: BLE001
|
| 142 |
-
logger.warning(
|
|
|
|
|
|
|
| 143 |
|
| 144 |
async def load_audit_records(self, session_id: str) -> list[dict[str, Any]]:
|
| 145 |
def _read() -> list[dict[str, Any]]:
|
|
|
|
| 139 |
try:
|
| 140 |
await asyncio.to_thread(_write)
|
| 141 |
except Exception as exc: # noqa: BLE001
|
| 142 |
+
logger.warning(
|
| 143 |
+
"Neizdevās pierakstīt autonomous audit trail sesijai %s: %s", session_id, exc
|
| 144 |
+
)
|
| 145 |
|
| 146 |
async def load_audit_records(self, session_id: str) -> list[dict[str, Any]]:
|
| 147 |
def _read() -> list[dict[str, Any]]:
|
core-python/maris_core/code/generate_code.py
CHANGED
|
@@ -207,16 +207,36 @@ def _detect_stack(prompt: str, requested_language: str, repo_path: Path | None)
|
|
| 207 |
if repo_path is not None:
|
| 208 |
package_json = _read_json(repo_path / "package.json")
|
| 209 |
dependencies = {
|
| 210 |
-
**(
|
| 211 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
}
|
| 213 |
-
if (
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
return "nextjs"
|
| 215 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
return "react"
|
| 217 |
if (repo_path / "Cargo.toml").exists() or (repo_path / "src/main.rs").exists():
|
| 218 |
return "rust"
|
| 219 |
-
if (
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
return "python"
|
| 221 |
|
| 222 |
normalized_prompt = f" {prompt.lower()} "
|
|
@@ -270,7 +290,11 @@ def _stack_scaffold_templates(detected_stack: str) -> dict[str, str]:
|
|
| 270 |
"private": True,
|
| 271 |
"scripts": {"dev": "next dev", "build": "next build", "start": "next start"},
|
| 272 |
"dependencies": {"next": "15.0.0", "react": "18.3.1", "react-dom": "18.3.1"},
|
| 273 |
-
"devDependencies": {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
}
|
| 275 |
return {
|
| 276 |
"package.json": json.dumps(package_json, ensure_ascii=False, indent=2) + "\n",
|
|
@@ -299,7 +323,7 @@ def _stack_scaffold_templates(detected_stack: str) -> dict[str, str]:
|
|
| 299 |
+ "\n",
|
| 300 |
"next-env.d.ts": '/// <reference types="next" />\n/// <reference types="next/image-types/global" />\n',
|
| 301 |
"next.config.mjs": "/** @type {import('next').NextConfig} */\nconst nextConfig = {};\n\nexport default nextConfig;\n",
|
| 302 |
-
"app/layout.tsx":
|
| 303 |
"app/page.tsx": "export default function HomePage() {\n return <main>Maris Next.js app</main>;\n}\n",
|
| 304 |
}
|
| 305 |
if detected_stack == "react":
|
|
@@ -344,21 +368,21 @@ def _stack_scaffold_templates(detected_stack: str) -> dict[str, str]:
|
|
| 344 |
)
|
| 345 |
+ "\n",
|
| 346 |
"vite.config.ts": "import { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\n\nexport default defineConfig({\n plugins: [react()],\n});\n",
|
| 347 |
-
"index.html":
|
| 348 |
"src/main.tsx": "import React from 'react';\nimport ReactDOM from 'react-dom/client';\nimport App from './App';\n\nReactDOM.createRoot(document.getElementById('root')!).render(\n <React.StrictMode>\n <App />\n </React.StrictMode>,\n);\n",
|
| 349 |
"src/App.tsx": "export default function App() {\n return <main>Maris React app</main>;\n}\n",
|
| 350 |
}
|
| 351 |
if detected_stack == "rust":
|
| 352 |
return {
|
| 353 |
-
"Cargo.toml":
|
| 354 |
"src/main.rs": 'fn main() {\n println!("Hello from Maris Rust app");\n}\n',
|
| 355 |
}
|
| 356 |
if detected_stack == "web":
|
| 357 |
return {
|
| 358 |
-
"index.html":
|
| 359 |
}
|
| 360 |
return {
|
| 361 |
-
"pyproject.toml":
|
| 362 |
"src/main.py": "def main() -> None:\n print('Hello from Maris Python app')\n\n\nif __name__ == '__main__':\n main()\n",
|
| 363 |
}
|
| 364 |
|
|
@@ -367,7 +391,9 @@ def _normalize_files(files: list[ProjectFile]) -> list[ProjectFile]:
|
|
| 367 |
normalized: dict[str, ProjectFile] = {}
|
| 368 |
for file in files:
|
| 369 |
path = _sanitize_relative_path(file.path)
|
| 370 |
-
normalized[path] = ProjectFile(
|
|
|
|
|
|
|
| 371 |
return list(normalized.values())
|
| 372 |
|
| 373 |
|
|
@@ -387,17 +413,28 @@ def _ensure_stack_scaffold(
|
|
| 387 |
if len(normalized_files) == 1:
|
| 388 |
only_file = normalized_files[0]
|
| 389 |
if only_file.path != resolved_entrypoint:
|
| 390 |
-
normalized_files[0] = ProjectFile(
|
|
|
|
|
|
|
| 391 |
return normalized_files, resolved_entrypoint
|
| 392 |
|
| 393 |
templates = _stack_scaffold_templates(detected_stack)
|
| 394 |
file_map = {file.path: file for file in normalized_files}
|
| 395 |
-
fallback_paths = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 396 |
if len(file_map) == 1:
|
| 397 |
only_path, only_file = next(iter(file_map.items()))
|
| 398 |
if only_path in fallback_paths and only_path != resolved_entrypoint:
|
| 399 |
file_map.pop(only_path)
|
| 400 |
-
file_map[resolved_entrypoint] = ProjectFile(
|
|
|
|
|
|
|
| 401 |
|
| 402 |
for path, content in templates.items():
|
| 403 |
if path not in file_map:
|
|
@@ -434,9 +471,15 @@ def _extract_project_files(
|
|
| 434 |
)
|
| 435 |
entrypoint = payload.get("entrypoint") or payload.get("primary_file")
|
| 436 |
explanation = str(payload.get("explanation") or payload.get("summary") or "").strip()
|
| 437 |
-
return
|
| 438 |
-
|
| 439 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 440 |
|
| 441 |
code, explanation = _extract_code_block(text, language)
|
| 442 |
if not code.strip():
|
|
@@ -476,10 +519,30 @@ def _repo_context_candidates(repo_path: Path, detected_stack: str, prompt: str =
|
|
| 476 |
candidates = _extract_repo_relative_hints(prompt, repo_path) + ["README.md"]
|
| 477 |
candidates.extend(
|
| 478 |
{
|
| 479 |
-
"nextjs": [
|
| 480 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 481 |
"rust": ["Cargo.toml", "src/main.rs", "src/lib.rs"],
|
| 482 |
-
"python": [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 483 |
"web": ["index.html"],
|
| 484 |
}.get(detected_stack, [])
|
| 485 |
)
|
|
@@ -492,7 +555,9 @@ def _repo_context_candidates(repo_path: Path, detected_stack: str, prompt: str =
|
|
| 492 |
return existing
|
| 493 |
|
| 494 |
|
| 495 |
-
def _build_repo_context(
|
|
|
|
|
|
|
| 496 |
if repo_path is None or not repo_path.exists():
|
| 497 |
return None
|
| 498 |
files = _repo_context_candidates(repo_path, detected_stack, prompt)
|
|
@@ -504,12 +569,14 @@ def _build_repo_context(repo_path: Path | None, detected_stack: str, prompt: str
|
|
| 504 |
content = (repo_path / relative_path).read_text(encoding="utf-8")
|
| 505 |
except OSError:
|
| 506 |
continue
|
| 507 |
-
excerpts.append(
|
| 508 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 509 |
)
|
| 510 |
-
return RepoContext(repo_path=str(repo_path), files=files) if not excerpts else RepoContext(
|
| 511 |
-
repo_path=str(repo_path),
|
| 512 |
-
files=["\n\n".join(excerpts)],
|
| 513 |
)
|
| 514 |
|
| 515 |
|
|
|
|
| 207 |
if repo_path is not None:
|
| 208 |
package_json = _read_json(repo_path / "package.json")
|
| 209 |
dependencies = {
|
| 210 |
+
**(
|
| 211 |
+
{str(k): v for k, v in package_json.get("dependencies", {}).items()}
|
| 212 |
+
if isinstance(package_json.get("dependencies"), dict)
|
| 213 |
+
else {}
|
| 214 |
+
),
|
| 215 |
+
**(
|
| 216 |
+
{str(k): v for k, v in package_json.get("devDependencies", {}).items()}
|
| 217 |
+
if isinstance(package_json.get("devDependencies"), dict)
|
| 218 |
+
else {}
|
| 219 |
+
),
|
| 220 |
}
|
| 221 |
+
if (
|
| 222 |
+
(repo_path / "next.config.js").exists()
|
| 223 |
+
or (repo_path / "next.config.mjs").exists()
|
| 224 |
+
or "next" in dependencies
|
| 225 |
+
):
|
| 226 |
return "nextjs"
|
| 227 |
+
if (
|
| 228 |
+
"react" in dependencies
|
| 229 |
+
or (repo_path / "src/App.tsx").exists()
|
| 230 |
+
or (repo_path / "src/main.tsx").exists()
|
| 231 |
+
):
|
| 232 |
return "react"
|
| 233 |
if (repo_path / "Cargo.toml").exists() or (repo_path / "src/main.rs").exists():
|
| 234 |
return "rust"
|
| 235 |
+
if (
|
| 236 |
+
(repo_path / "pyproject.toml").exists()
|
| 237 |
+
or (repo_path / "requirements.txt").exists()
|
| 238 |
+
or (repo_path / "src/main.py").exists()
|
| 239 |
+
):
|
| 240 |
return "python"
|
| 241 |
|
| 242 |
normalized_prompt = f" {prompt.lower()} "
|
|
|
|
| 290 |
"private": True,
|
| 291 |
"scripts": {"dev": "next dev", "build": "next build", "start": "next start"},
|
| 292 |
"dependencies": {"next": "15.0.0", "react": "18.3.1", "react-dom": "18.3.1"},
|
| 293 |
+
"devDependencies": {
|
| 294 |
+
"typescript": "5.6.3",
|
| 295 |
+
"@types/react": "18.3.3",
|
| 296 |
+
"@types/node": "22.7.4",
|
| 297 |
+
},
|
| 298 |
}
|
| 299 |
return {
|
| 300 |
"package.json": json.dumps(package_json, ensure_ascii=False, indent=2) + "\n",
|
|
|
|
| 323 |
+ "\n",
|
| 324 |
"next-env.d.ts": '/// <reference types="next" />\n/// <reference types="next/image-types/global" />\n',
|
| 325 |
"next.config.mjs": "/** @type {import('next').NextConfig} */\nconst nextConfig = {};\n\nexport default nextConfig;\n",
|
| 326 |
+
"app/layout.tsx": 'export default function RootLayout({ children }: { children: React.ReactNode }) {\n return (\n <html lang="en">\n <body>{children}</body>\n </html>\n );\n}\n',
|
| 327 |
"app/page.tsx": "export default function HomePage() {\n return <main>Maris Next.js app</main>;\n}\n",
|
| 328 |
}
|
| 329 |
if detected_stack == "react":
|
|
|
|
| 368 |
)
|
| 369 |
+ "\n",
|
| 370 |
"vite.config.ts": "import { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\n\nexport default defineConfig({\n plugins: [react()],\n});\n",
|
| 371 |
+
"index.html": '<!doctype html>\n<html lang="en">\n <head>\n <meta charset="UTF-8" />\n <meta name="viewport" content="width=device-width, initial-scale=1.0" />\n <title>Maris React App</title>\n </head>\n <body>\n <div id="root"></div>\n <script type="module" src="/src/main.tsx"></script>\n </body>\n</html>\n',
|
| 372 |
"src/main.tsx": "import React from 'react';\nimport ReactDOM from 'react-dom/client';\nimport App from './App';\n\nReactDOM.createRoot(document.getElementById('root')!).render(\n <React.StrictMode>\n <App />\n </React.StrictMode>,\n);\n",
|
| 373 |
"src/App.tsx": "export default function App() {\n return <main>Maris React app</main>;\n}\n",
|
| 374 |
}
|
| 375 |
if detected_stack == "rust":
|
| 376 |
return {
|
| 377 |
+
"Cargo.toml": '[package]\nname = "maris-rust-app"\nversion = "0.1.0"\nedition = "2021"\n\n[dependencies]\n',
|
| 378 |
"src/main.rs": 'fn main() {\n println!("Hello from Maris Rust app");\n}\n',
|
| 379 |
}
|
| 380 |
if detected_stack == "web":
|
| 381 |
return {
|
| 382 |
+
"index.html": '<!doctype html>\n<html lang="en">\n <head>\n <meta charset="UTF-8" />\n <meta name="viewport" content="width=device-width, initial-scale=1.0" />\n <title>Maris Web App</title>\n </head>\n <body>\n <main>Maris web artifact</main>\n </body>\n</html>\n',
|
| 383 |
}
|
| 384 |
return {
|
| 385 |
+
"pyproject.toml": '[project]\nname = "maris-python-app"\nversion = "0.1.0"\ndescription = "Generated by Maris AI"\nrequires-python = ">=3.11"\n\n[project.scripts]\nmaris-app = "src.main:main"\n',
|
| 386 |
"src/main.py": "def main() -> None:\n print('Hello from Maris Python app')\n\n\nif __name__ == '__main__':\n main()\n",
|
| 387 |
}
|
| 388 |
|
|
|
|
| 391 |
normalized: dict[str, ProjectFile] = {}
|
| 392 |
for file in files:
|
| 393 |
path = _sanitize_relative_path(file.path)
|
| 394 |
+
normalized[path] = ProjectFile(
|
| 395 |
+
path=path, content=file.content, absolute_path=file.absolute_path
|
| 396 |
+
)
|
| 397 |
return list(normalized.values())
|
| 398 |
|
| 399 |
|
|
|
|
| 413 |
if len(normalized_files) == 1:
|
| 414 |
only_file = normalized_files[0]
|
| 415 |
if only_file.path != resolved_entrypoint:
|
| 416 |
+
normalized_files[0] = ProjectFile(
|
| 417 |
+
path=resolved_entrypoint, content=only_file.content
|
| 418 |
+
)
|
| 419 |
return normalized_files, resolved_entrypoint
|
| 420 |
|
| 421 |
templates = _stack_scaffold_templates(detected_stack)
|
| 422 |
file_map = {file.path: file for file in normalized_files}
|
| 423 |
+
fallback_paths = {
|
| 424 |
+
"main.py",
|
| 425 |
+
"src/main.py",
|
| 426 |
+
"src/main.rs",
|
| 427 |
+
"src/App.tsx",
|
| 428 |
+
"app/page.tsx",
|
| 429 |
+
"index.html",
|
| 430 |
+
}
|
| 431 |
if len(file_map) == 1:
|
| 432 |
only_path, only_file = next(iter(file_map.items()))
|
| 433 |
if only_path in fallback_paths and only_path != resolved_entrypoint:
|
| 434 |
file_map.pop(only_path)
|
| 435 |
+
file_map[resolved_entrypoint] = ProjectFile(
|
| 436 |
+
path=resolved_entrypoint, content=only_file.content
|
| 437 |
+
)
|
| 438 |
|
| 439 |
for path, content in templates.items():
|
| 440 |
if path not in file_map:
|
|
|
|
| 471 |
)
|
| 472 |
entrypoint = payload.get("entrypoint") or payload.get("primary_file")
|
| 473 |
explanation = str(payload.get("explanation") or payload.get("summary") or "").strip()
|
| 474 |
+
return (
|
| 475 |
+
files,
|
| 476 |
+
(
|
| 477 |
+
_sanitize_relative_path(str(entrypoint))
|
| 478 |
+
if isinstance(entrypoint, str) and entrypoint
|
| 479 |
+
else None
|
| 480 |
+
),
|
| 481 |
+
explanation,
|
| 482 |
+
)
|
| 483 |
|
| 484 |
code, explanation = _extract_code_block(text, language)
|
| 485 |
if not code.strip():
|
|
|
|
| 519 |
candidates = _extract_repo_relative_hints(prompt, repo_path) + ["README.md"]
|
| 520 |
candidates.extend(
|
| 521 |
{
|
| 522 |
+
"nextjs": [
|
| 523 |
+
"package.json",
|
| 524 |
+
"tsconfig.json",
|
| 525 |
+
"next.config.mjs",
|
| 526 |
+
"app/layout.tsx",
|
| 527 |
+
"app/page.tsx",
|
| 528 |
+
"pages/index.tsx",
|
| 529 |
+
],
|
| 530 |
+
"react": [
|
| 531 |
+
"package.json",
|
| 532 |
+
"tsconfig.json",
|
| 533 |
+
"vite.config.ts",
|
| 534 |
+
"index.html",
|
| 535 |
+
"src/main.tsx",
|
| 536 |
+
"src/App.tsx",
|
| 537 |
+
],
|
| 538 |
"rust": ["Cargo.toml", "src/main.rs", "src/lib.rs"],
|
| 539 |
+
"python": [
|
| 540 |
+
"pyproject.toml",
|
| 541 |
+
"requirements.txt",
|
| 542 |
+
"src/main.py",
|
| 543 |
+
"main.py",
|
| 544 |
+
"app/main.py",
|
| 545 |
+
],
|
| 546 |
"web": ["index.html"],
|
| 547 |
}.get(detected_stack, [])
|
| 548 |
)
|
|
|
|
| 555 |
return existing
|
| 556 |
|
| 557 |
|
| 558 |
+
def _build_repo_context(
|
| 559 |
+
repo_path: Path | None, detected_stack: str, prompt: str = ""
|
| 560 |
+
) -> RepoContext | None:
|
| 561 |
if repo_path is None or not repo_path.exists():
|
| 562 |
return None
|
| 563 |
files = _repo_context_candidates(repo_path, detected_stack, prompt)
|
|
|
|
| 569 |
content = (repo_path / relative_path).read_text(encoding="utf-8")
|
| 570 |
except OSError:
|
| 571 |
continue
|
| 572 |
+
excerpts.append(f"[FILE {relative_path}]\n{content[:_MAX_REPO_CONTEXT_CHARS].strip()}")
|
| 573 |
+
return (
|
| 574 |
+
RepoContext(repo_path=str(repo_path), files=files)
|
| 575 |
+
if not excerpts
|
| 576 |
+
else RepoContext(
|
| 577 |
+
repo_path=str(repo_path),
|
| 578 |
+
files=["\n\n".join(excerpts)],
|
| 579 |
)
|
|
|
|
|
|
|
|
|
|
| 580 |
)
|
| 581 |
|
| 582 |
|
core-python/maris_core/data/quality.py
CHANGED
|
@@ -261,12 +261,9 @@ def _looks_like_prompt_echo(
|
|
| 261 |
if prompt_normalized == completion_normalized:
|
| 262 |
return True
|
| 263 |
if (
|
| 264 |
-
(
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
)
|
| 268 |
-
and len(completion_normalized) <= int(len(prompt_normalized) * 1.2)
|
| 269 |
-
):
|
| 270 |
return True
|
| 271 |
prompt_tokens = prompt_normalized.split()
|
| 272 |
completion_tokens = completion_normalized.split()
|
|
@@ -277,14 +274,15 @@ def _looks_like_prompt_echo(
|
|
| 277 |
if completion_overlap < 0.8:
|
| 278 |
return False
|
| 279 |
similarity = SequenceMatcher(a=prompt_normalized, b=completion_normalized).ratio()
|
| 280 |
-
return (
|
| 281 |
-
|
| 282 |
-
and len(completion_normalized) <= int(len(prompt_normalized) * 1.2)
|
| 283 |
)
|
| 284 |
|
| 285 |
|
| 286 |
def _has_repeated_line_noise(value: str, max_repeated_line_fraction: float) -> bool:
|
| 287 |
-
lines = [
|
|
|
|
|
|
|
| 288 |
if len(lines) < 3:
|
| 289 |
return False
|
| 290 |
repeated_counts = Counter(line for line in lines if len(line) >= _MIN_REPEATED_SEGMENT_LENGTH)
|
|
|
|
| 261 |
if prompt_normalized == completion_normalized:
|
| 262 |
return True
|
| 263 |
if (
|
| 264 |
+
completion_normalized.startswith(prompt_normalized)
|
| 265 |
+
or prompt_normalized.startswith(completion_normalized)
|
| 266 |
+
) and len(completion_normalized) <= int(len(prompt_normalized) * 1.2):
|
|
|
|
|
|
|
|
|
|
| 267 |
return True
|
| 268 |
prompt_tokens = prompt_normalized.split()
|
| 269 |
completion_tokens = completion_normalized.split()
|
|
|
|
| 274 |
if completion_overlap < 0.8:
|
| 275 |
return False
|
| 276 |
similarity = SequenceMatcher(a=prompt_normalized, b=completion_normalized).ratio()
|
| 277 |
+
return similarity >= max_prompt_echo_similarity and len(completion_normalized) <= int(
|
| 278 |
+
len(prompt_normalized) * 1.2
|
|
|
|
| 279 |
)
|
| 280 |
|
| 281 |
|
| 282 |
def _has_repeated_line_noise(value: str, max_repeated_line_fraction: float) -> bool:
|
| 283 |
+
lines = [
|
| 284 |
+
line.strip().casefold() for line in _REPEATED_SEGMENT_SPLIT_RE.split(value) if line.strip()
|
| 285 |
+
]
|
| 286 |
if len(lines) < 3:
|
| 287 |
return False
|
| 288 |
repeated_counts = Counter(line for line in lines if len(line) >= _MIN_REPEATED_SEGMENT_LENGTH)
|
core-python/maris_core/data/validator.py
CHANGED
|
@@ -201,7 +201,11 @@ def _is_non_empty_string(value: Any) -> bool:
|
|
| 201 |
|
| 202 |
|
| 203 |
def _is_non_empty_string_list(value: Any) -> bool:
|
| 204 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
|
| 206 |
|
| 207 |
def _is_iso8601_timestamp(value: str) -> bool:
|
|
@@ -237,7 +241,9 @@ def _resolve_profile(root: Path, profile: str) -> str:
|
|
| 237 |
normalized = profile.strip().lower()
|
| 238 |
if normalized not in _VALIDATION_PROFILES:
|
| 239 |
allowed = ", ".join(sorted(_VALIDATION_PROFILES))
|
| 240 |
-
raise DatasetValidationError(
|
|
|
|
|
|
|
| 241 |
if normalized != "auto":
|
| 242 |
return normalized
|
| 243 |
return "eval" if root.name == "eval-data" else "bootstrap"
|
|
|
|
| 201 |
|
| 202 |
|
| 203 |
def _is_non_empty_string_list(value: Any) -> bool:
|
| 204 |
+
return (
|
| 205 |
+
isinstance(value, list)
|
| 206 |
+
and bool(value)
|
| 207 |
+
and all(_is_non_empty_string(item) for item in value)
|
| 208 |
+
)
|
| 209 |
|
| 210 |
|
| 211 |
def _is_iso8601_timestamp(value: str) -> bool:
|
|
|
|
| 241 |
normalized = profile.strip().lower()
|
| 242 |
if normalized not in _VALIDATION_PROFILES:
|
| 243 |
allowed = ", ".join(sorted(_VALIDATION_PROFILES))
|
| 244 |
+
raise DatasetValidationError(
|
| 245 |
+
[f"Neatbalstīts validācijas profils '{profile}'. Atļautie: {allowed}."]
|
| 246 |
+
)
|
| 247 |
if normalized != "auto":
|
| 248 |
return normalized
|
| 249 |
return "eval" if root.name == "eval-data" else "bootstrap"
|
core-python/maris_core/orchestrator/routing.py
CHANGED
|
@@ -51,7 +51,9 @@ _CODE_BUILDABLE_PATTERN = re.compile(
|
|
| 51 |
r"(kalkulator|calculator|dashboard|landing page|web app|cli|service|widget|\bapp\b)",
|
| 52 |
flags=re.IGNORECASE,
|
| 53 |
)
|
| 54 |
-
_CODE_FILE_PATTERN = re.compile(
|
|
|
|
|
|
|
| 55 |
_AUTONOMOUS_INTENT_PATTERN = re.compile(
|
| 56 |
r"(autonom|roadmap|plān|workflow|darba plūsm|izpildi|veic uzdevumu|labojum|uzlabojum|sadal[iī]|prioritiz|rollout|incident response|postmortem|migration plan|delivery plan)",
|
| 57 |
flags=re.IGNORECASE,
|
|
@@ -437,7 +439,9 @@ def _looks_like_code_request(message: str) -> bool:
|
|
| 437 |
normalized = message.strip().lower()
|
| 438 |
if _CODE_FILE_PATTERN.search(normalized):
|
| 439 |
return True
|
| 440 |
-
if _EXPLANATION_INTENT_PATTERN.search(normalized) and not _CODE_ACTION_PATTERN.search(
|
|
|
|
|
|
|
| 441 |
return False
|
| 442 |
if _CODE_SUBJECT_PATTERN.search(normalized):
|
| 443 |
return True
|
|
|
|
| 51 |
r"(kalkulator|calculator|dashboard|landing page|web app|cli|service|widget|\bapp\b)",
|
| 52 |
flags=re.IGNORECASE,
|
| 53 |
)
|
| 54 |
+
_CODE_FILE_PATTERN = re.compile(
|
| 55 |
+
r"\b[\w./-]+\.(py|ts|tsx|js|jsx|rs|sql|toml|json|yaml|yml)\b", re.IGNORECASE
|
| 56 |
+
)
|
| 57 |
_AUTONOMOUS_INTENT_PATTERN = re.compile(
|
| 58 |
r"(autonom|roadmap|plān|workflow|darba plūsm|izpildi|veic uzdevumu|labojum|uzlabojum|sadal[iī]|prioritiz|rollout|incident response|postmortem|migration plan|delivery plan)",
|
| 59 |
flags=re.IGNORECASE,
|
|
|
|
| 439 |
normalized = message.strip().lower()
|
| 440 |
if _CODE_FILE_PATTERN.search(normalized):
|
| 441 |
return True
|
| 442 |
+
if _EXPLANATION_INTENT_PATTERN.search(normalized) and not _CODE_ACTION_PATTERN.search(
|
| 443 |
+
normalized
|
| 444 |
+
):
|
| 445 |
return False
|
| 446 |
if _CODE_SUBJECT_PATTERN.search(normalized):
|
| 447 |
return True
|
core-python/maris_core/space_agent.py
CHANGED
|
@@ -31,6 +31,7 @@ logger = logging.getLogger(__name__)
|
|
| 31 |
class SpaceAgentCancelledError(Exception):
|
| 32 |
"""Raised when a Space agent task is cancelled by the caller."""
|
| 33 |
|
|
|
|
| 34 |
SPACE_AGENT_MODEL_DEFAULT = "MarisUK/maris-ai-master"
|
| 35 |
SPACE_AGENT_SPACE_REPO_DEFAULT = "MarisUK/maris.ai.agent"
|
| 36 |
SPACE_AGENT_DATASET_REPO_DEFAULT = "MarisUK/maris-ai-lv-memory"
|
|
@@ -87,14 +88,14 @@ SPACE_AGENT_CAPABILITIES = (
|
|
| 87 |
"title": "Coding copilot",
|
| 88 |
"description": "Dod profesionālus ieteikumus par promptiem, skriptiem, workflow un tehniskām izmaiņām, izmantojot Qwen coder modeli.",
|
| 89 |
},
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
{
|
| 99 |
"title": "Validation runner",
|
| 100 |
"description": "Var palaist droši ierobežotas build, lint un test komandas izolētā draft darba telpā.",
|
|
@@ -103,10 +104,10 @@ SPACE_AGENT_CAPABILITIES = (
|
|
| 103 |
"title": "Command presets",
|
| 104 |
"description": "Var atgriezt gatavu validācijas komandu katalogu Python, frontend, Rust un Hugging Face darba plūsmām.",
|
| 105 |
},
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
{
|
| 111 |
"title": "Persona system",
|
| 112 |
"description": "Var atgriezt aktīvo Maris persona katalogu ar režīmiem, kuri pielāgo komunikācijas stilu.",
|
|
@@ -121,7 +122,13 @@ SPACE_AGENT_WORKSPACE_COMMAND_PRESETS = (
|
|
| 121 |
"id": "python-space-tests",
|
| 122 |
"label": "Space agent tests",
|
| 123 |
"description": "Pārbauda Space agent un app fokusētos testus.",
|
| 124 |
-
"command": [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
"cwd": "core-python",
|
| 126 |
},
|
| 127 |
{
|
|
@@ -854,7 +861,11 @@ def execute_space_agent_tool(
|
|
| 854 |
"error_type": "WorkspaceCommandUnavailable",
|
| 855 |
}
|
| 856 |
result = command_runner(tool_call.arguments)
|
| 857 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
| 858 |
raise ValueError(f"Unsupported tool call: {tool_call.name}")
|
| 859 |
|
| 860 |
|
|
@@ -1186,7 +1197,9 @@ def _workspace_file_state(target_path: Path) -> tuple[str | None, str]:
|
|
| 1186 |
return previous, "update"
|
| 1187 |
|
| 1188 |
|
| 1189 |
-
def _try_read_existing_hf_repo_text(
|
|
|
|
|
|
|
| 1190 |
try:
|
| 1191 |
local_path = Path(
|
| 1192 |
_download_hf_repo_file(repo_id=repo_id, repo_type=repo_type, path_in_repo=path_in_repo)
|
|
|
|
| 31 |
class SpaceAgentCancelledError(Exception):
|
| 32 |
"""Raised when a Space agent task is cancelled by the caller."""
|
| 33 |
|
| 34 |
+
|
| 35 |
SPACE_AGENT_MODEL_DEFAULT = "MarisUK/maris-ai-master"
|
| 36 |
SPACE_AGENT_SPACE_REPO_DEFAULT = "MarisUK/maris.ai.agent"
|
| 37 |
SPACE_AGENT_DATASET_REPO_DEFAULT = "MarisUK/maris-ai-lv-memory"
|
|
|
|
| 88 |
"title": "Coding copilot",
|
| 89 |
"description": "Dod profesionālus ieteikumus par promptiem, skriptiem, workflow un tehniskām izmaiņām, izmantojot Qwen coder modeli.",
|
| 90 |
},
|
| 91 |
+
{
|
| 92 |
+
"title": "Workspace access",
|
| 93 |
+
"description": "Var nolasīt, labot un sagatavot teksta failu izmaiņas izolētā Maris draft darba telpā.",
|
| 94 |
+
},
|
| 95 |
+
{
|
| 96 |
+
"title": "Hugging Face operator",
|
| 97 |
+
"description": "Var pārlūkot tavus HF repozitorijus, nolasīt failus un saglabāt izmaiņas ar commit ziņām.",
|
| 98 |
+
},
|
| 99 |
{
|
| 100 |
"title": "Validation runner",
|
| 101 |
"description": "Var palaist droši ierobežotas build, lint un test komandas izolētā draft darba telpā.",
|
|
|
|
| 104 |
"title": "Command presets",
|
| 105 |
"description": "Var atgriezt gatavu validācijas komandu katalogu Python, frontend, Rust un Hugging Face darba plūsmām.",
|
| 106 |
},
|
| 107 |
+
{
|
| 108 |
+
"title": "Browser automation",
|
| 109 |
+
"description": "Var izskaidrot Playwright browser automation endpointus, sesiju limitus un drošos URL režīmus.",
|
| 110 |
+
},
|
| 111 |
{
|
| 112 |
"title": "Persona system",
|
| 113 |
"description": "Var atgriezt aktīvo Maris persona katalogu ar režīmiem, kuri pielāgo komunikācijas stilu.",
|
|
|
|
| 122 |
"id": "python-space-tests",
|
| 123 |
"label": "Space agent tests",
|
| 124 |
"description": "Pārbauda Space agent un app fokusētos testus.",
|
| 125 |
+
"command": [
|
| 126 |
+
"python",
|
| 127 |
+
"-m",
|
| 128 |
+
"pytest",
|
| 129 |
+
"tests/test_space_agent.py",
|
| 130 |
+
"tests/test_huggingface_space_app.py",
|
| 131 |
+
],
|
| 132 |
"cwd": "core-python",
|
| 133 |
},
|
| 134 |
{
|
|
|
|
| 861 |
"error_type": "WorkspaceCommandUnavailable",
|
| 862 |
}
|
| 863 |
result = command_runner(tool_call.arguments)
|
| 864 |
+
return (
|
| 865 |
+
result
|
| 866 |
+
if isinstance(result, dict)
|
| 867 |
+
else {"ok": False, "error": "Nederīgs komandas rezultāts."}
|
| 868 |
+
)
|
| 869 |
raise ValueError(f"Unsupported tool call: {tool_call.name}")
|
| 870 |
|
| 871 |
|
|
|
|
| 1197 |
return previous, "update"
|
| 1198 |
|
| 1199 |
|
| 1200 |
+
def _try_read_existing_hf_repo_text(
|
| 1201 |
+
*, repo_id: str, repo_type: str, path_in_repo: str
|
| 1202 |
+
) -> str | None:
|
| 1203 |
try:
|
| 1204 |
local_path = Path(
|
| 1205 |
_download_hf_repo_file(repo_id=repo_id, repo_type=repo_type, path_in_repo=path_in_repo)
|
core-python/maris_core/text/benchmark.py
CHANGED
|
@@ -265,14 +265,10 @@ def summarize_chat_benchmark(results: list[ChatBenchmarkResult]) -> dict[str, An
|
|
| 265 |
else 1.0,
|
| 266 |
"judge_overall": judge_summary["overall"],
|
| 267 |
"judge_task_completion": judge_summary["dimension_scores"]["task_completion"],
|
| 268 |
-
"judge_instruction_following": judge_summary["dimension_scores"][
|
| 269 |
-
"instruction_following"
|
| 270 |
-
],
|
| 271 |
"judge_grounding": judge_summary["dimension_scores"]["grounding"],
|
| 272 |
"judge_safety": judge_summary["dimension_scores"]["safety"],
|
| 273 |
-
"judge_multi_turn_continuity": judge_summary["dimension_scores"][
|
| 274 |
-
"multi_turn_continuity"
|
| 275 |
-
],
|
| 276 |
"judge_code_quality": judge_summary["dimension_scores"]["code_quality"],
|
| 277 |
"judge_regression_risk": judge_summary["dimension_scores"]["regression_risk"],
|
| 278 |
"memory_retrieval_pass_rate": round(len(memory_passed) / len(memory_results), 3)
|
|
@@ -885,8 +881,7 @@ def _is_hallucination_incident(result: ChatBenchmarkResult) -> bool:
|
|
| 885 |
if judge is None:
|
| 886 |
return False
|
| 887 |
return (
|
| 888 |
-
result.category in {"grounding", "factuality", "multimodal"}
|
| 889 |
-
and not judge.grounding.passed
|
| 890 |
)
|
| 891 |
|
| 892 |
|
|
|
|
| 265 |
else 1.0,
|
| 266 |
"judge_overall": judge_summary["overall"],
|
| 267 |
"judge_task_completion": judge_summary["dimension_scores"]["task_completion"],
|
| 268 |
+
"judge_instruction_following": judge_summary["dimension_scores"]["instruction_following"],
|
|
|
|
|
|
|
| 269 |
"judge_grounding": judge_summary["dimension_scores"]["grounding"],
|
| 270 |
"judge_safety": judge_summary["dimension_scores"]["safety"],
|
| 271 |
+
"judge_multi_turn_continuity": judge_summary["dimension_scores"]["multi_turn_continuity"],
|
|
|
|
|
|
|
| 272 |
"judge_code_quality": judge_summary["dimension_scores"]["code_quality"],
|
| 273 |
"judge_regression_risk": judge_summary["dimension_scores"]["regression_risk"],
|
| 274 |
"memory_retrieval_pass_rate": round(len(memory_passed) / len(memory_results), 3)
|
|
|
|
| 881 |
if judge is None:
|
| 882 |
return False
|
| 883 |
return (
|
| 884 |
+
result.category in {"grounding", "factuality", "multimodal"} and not judge.grounding.passed
|
|
|
|
| 885 |
)
|
| 886 |
|
| 887 |
|
core-python/maris_core/text/evals.py
CHANGED
|
@@ -328,10 +328,16 @@ def _evaluate_rubric_judge(
|
|
| 328 |
if case.history_turns > 0:
|
| 329 |
if any(marker in lowered for marker in _CONTEXT_CONTINUITY_MARKERS):
|
| 330 |
instruction_following += 0.15
|
| 331 |
-
elif case.expected_terms and not any(
|
|
|
|
|
|
|
| 332 |
instruction_following -= 0.15
|
| 333 |
-
instruction_following_reasons.append(
|
| 334 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 335 |
instruction_following -= 0.1
|
| 336 |
instruction_following_reasons.append("neskaidram pieprasījumam pietrūkst precizējoša soļa")
|
| 337 |
if any(term.lower() in lowered for term in case.forbidden_terms):
|
|
@@ -369,7 +375,9 @@ def _evaluate_rubric_judge(
|
|
| 369 |
if case.expects_code or case.category == "coding":
|
| 370 |
code_quality = coding
|
| 371 |
if code_quality < 0.7:
|
| 372 |
-
code_quality_reasons.append(
|
|
|
|
|
|
|
| 373 |
else:
|
| 374 |
code_quality = 1.0
|
| 375 |
|
|
@@ -384,7 +392,9 @@ def _evaluate_rubric_judge(
|
|
| 384 |
if case.expects_code and "```" in response:
|
| 385 |
regression_risk += 0.05
|
| 386 |
if regression_risk < 0.7:
|
| 387 |
-
regression_reasons.append(
|
|
|
|
|
|
|
| 388 |
else:
|
| 389 |
regression_risk = 1.0
|
| 390 |
|
|
|
|
| 328 |
if case.history_turns > 0:
|
| 329 |
if any(marker in lowered for marker in _CONTEXT_CONTINUITY_MARKERS):
|
| 330 |
instruction_following += 0.15
|
| 331 |
+
elif case.expected_terms and not any(
|
| 332 |
+
term.lower() in lowered for term in case.expected_terms
|
| 333 |
+
):
|
| 334 |
instruction_following -= 0.15
|
| 335 |
+
instruction_following_reasons.append(
|
| 336 |
+
"follow-up atbilde neparāda iepriekšējā konteksta turpinājumu"
|
| 337 |
+
)
|
| 338 |
+
if case.category == "helpfulness" and not any(
|
| 339 |
+
marker in lowered for marker in _CLARIFICATION_MARKERS
|
| 340 |
+
):
|
| 341 |
instruction_following -= 0.1
|
| 342 |
instruction_following_reasons.append("neskaidram pieprasījumam pietrūkst precizējoša soļa")
|
| 343 |
if any(term.lower() in lowered for term in case.forbidden_terms):
|
|
|
|
| 375 |
if case.expects_code or case.category == "coding":
|
| 376 |
code_quality = coding
|
| 377 |
if code_quality < 0.7:
|
| 378 |
+
code_quality_reasons.append(
|
| 379 |
+
"koda kvalitātes, validācijas vai testu signāli ir par vāju"
|
| 380 |
+
)
|
| 381 |
else:
|
| 382 |
code_quality = 1.0
|
| 383 |
|
|
|
|
| 392 |
if case.expects_code and "```" in response:
|
| 393 |
regression_risk += 0.05
|
| 394 |
if regression_risk < 0.7:
|
| 395 |
+
regression_reasons.append(
|
| 396 |
+
"pietrūkst regresiju, rollback vai verifikācijas drošības signālu"
|
| 397 |
+
)
|
| 398 |
else:
|
| 399 |
regression_risk = 1.0
|
| 400 |
|
core-python/maris_core/text/generate.py
CHANGED
|
@@ -543,7 +543,9 @@ def complete_with_hf_fallback(
|
|
| 543 |
max_tokens=max_new_tokens,
|
| 544 |
temperature=temperature,
|
| 545 |
)
|
| 546 |
-
response_text = _sanitize_response_text(
|
|
|
|
|
|
|
| 547 |
if response_text:
|
| 548 |
return resolved_model, response_text
|
| 549 |
except AttributeError:
|
|
@@ -564,7 +566,9 @@ def complete_with_hf_fallback(
|
|
| 564 |
except retryable_hf_errors as exc:
|
| 565 |
logger.warning("HF fallback text_generation failed for model %s: %s", resolved_model, exc)
|
| 566 |
return None
|
| 567 |
-
response_text = _sanitize_response_text(
|
|
|
|
|
|
|
| 568 |
if not response_text:
|
| 569 |
return None
|
| 570 |
return resolved_model, response_text
|
|
|
|
| 543 |
max_tokens=max_new_tokens,
|
| 544 |
temperature=temperature,
|
| 545 |
)
|
| 546 |
+
response_text = _sanitize_response_text(
|
| 547 |
+
_extract_inference_response_text(raw_response), messages
|
| 548 |
+
)
|
| 549 |
if response_text:
|
| 550 |
return resolved_model, response_text
|
| 551 |
except AttributeError:
|
|
|
|
| 566 |
except retryable_hf_errors as exc:
|
| 567 |
logger.warning("HF fallback text_generation failed for model %s: %s", resolved_model, exc)
|
| 568 |
return None
|
| 569 |
+
response_text = _sanitize_response_text(
|
| 570 |
+
_extract_inference_response_text(raw_response), messages
|
| 571 |
+
)
|
| 572 |
if not response_text:
|
| 573 |
return None
|
| 574 |
return resolved_model, response_text
|
core-python/maris_core/training/config.py
CHANGED
|
@@ -23,6 +23,17 @@ DEFAULT_MUSIC_MODEL_REPO = "MarisUK/maris-ai-music"
|
|
| 23 |
DEFAULT_TTS_MODEL_REPO = "MarisUK/maris-tts-runtime"
|
| 24 |
DEFAULT_STT_MODEL_REPO = "MarisUK/maris-stt-runtime"
|
| 25 |
DEFAULT_VIDEO_MODEL_REPO = "MarisUK/maris-ai-video"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
AVAILABLE_TRAINING_BASE_MODELS: dict[str, dict[str, str]] = {
|
| 27 |
"balanced": {
|
| 28 |
"model_name": DEFAULT_TRAINING_BASE_MODEL,
|
|
@@ -177,6 +188,21 @@ def _parse_list(value: Any) -> list[str]:
|
|
| 177 |
return [item.strip() for item in str(value).split(",") if item.strip()]
|
| 178 |
|
| 179 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
@dataclass(slots=True)
|
| 181 |
class TrainingConfig:
|
| 182 |
"""Pilna apmācības konfigurācija vienam Maris treniņa skrējienam."""
|
|
@@ -194,8 +220,10 @@ class TrainingConfig:
|
|
| 194 |
qlora_quant_type: str = "nf4"
|
| 195 |
qlora_use_double_quant: bool = True
|
| 196 |
qlora_compute_dtype: str = "float16"
|
| 197 |
-
dataset_repo: str =
|
|
|
|
| 198 |
eval_dataset_repo: str = ""
|
|
|
|
| 199 |
output_dir: str = "./output/model"
|
| 200 |
hub_model_id: str = DEFAULT_MASTER_MODEL_REPO
|
| 201 |
text_model_id: str = DEFAULT_TEXT_MODEL_REPO
|
|
@@ -545,7 +573,9 @@ def load_training_config(
|
|
| 545 |
"HF_TRAIN_QLORA_COMPUTE_DTYPE",
|
| 546 |
),
|
| 547 |
"dataset_repo": get_env_any("MARIS_MEMORY_REPO", "MARIS_DATASET_REPO", "HF_DATASET_REPO"),
|
|
|
|
| 548 |
"eval_dataset_repo": get_env_any("MARIS_EVAL_DATASET_REPO", "HF_EVAL_DATASET_REPO"),
|
|
|
|
| 549 |
"output_dir": get_env_any("MARIS_TRAIN_OUTPUT_DIR", "HF_TRAIN_OUTPUT_DIR"),
|
| 550 |
"hub_model_id": get_env_any("MARIS_MODEL_REPO", "HF_MODEL_REPO"),
|
| 551 |
"text_model_id": get_env_any("TEXT_MODEL", default=DEFAULT_TEXT_MODEL_REPO),
|
|
@@ -803,6 +833,7 @@ def load_training_config(
|
|
| 803 |
"MARIS_MEMORY_REPO/MARIS_DATASET_REPO/HF_DATASET_REPO/dataset_repo",
|
| 804 |
label="dataset repozitorijs",
|
| 805 |
),
|
|
|
|
| 806 |
eval_dataset_repo=(
|
| 807 |
validate_maris_repo(
|
| 808 |
str(merged["eval_dataset_repo"]),
|
|
@@ -812,6 +843,7 @@ def load_training_config(
|
|
| 812 |
if merged.get("eval_dataset_repo") not in (None, "")
|
| 813 |
else ""
|
| 814 |
),
|
|
|
|
| 815 |
output_dir=str(merged["output_dir"]),
|
| 816 |
hub_model_id=validate_maris_model(
|
| 817 |
str(merged["hub_model_id"]),
|
|
@@ -882,7 +914,9 @@ def load_training_config(
|
|
| 882 |
save_safetensors=_parse_bool(merged.get("save_safetensors"), default=True),
|
| 883 |
lr_scheduler_type=str(merged["lr_scheduler_type"]),
|
| 884 |
benchmark_dataset_path=str(merged.get("benchmark_dataset_path", "") or ""),
|
| 885 |
-
benchmark_name=str(
|
|
|
|
|
|
|
| 886 |
benchmark_levels=_parse_list(merged.get("benchmark_levels")) or ["local", "ci", "release"],
|
| 887 |
benchmark_min_overall=float(merged.get("benchmark_min_overall", 0.7)),
|
| 888 |
benchmark_gate_enabled=_parse_bool(merged.get("benchmark_gate_enabled"), default=False),
|
|
@@ -948,6 +982,32 @@ def load_training_config(
|
|
| 948 |
),
|
| 949 |
continue_model_path=str(merged.get("continue_model_path", "") or ""),
|
| 950 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 951 |
if config.fp16 and config.bf16:
|
| 952 |
raise ValueError("Maris training konfigurācijā nevar vienlaikus ieslēgt fp16 un bf16.")
|
| 953 |
if config.adapter_type not in {"full", "lora", "qlora", "specialist_model"}:
|
|
|
|
| 23 |
DEFAULT_TTS_MODEL_REPO = "MarisUK/maris-tts-runtime"
|
| 24 |
DEFAULT_STT_MODEL_REPO = "MarisUK/maris-stt-runtime"
|
| 25 |
DEFAULT_VIDEO_MODEL_REPO = "MarisUK/maris-ai-video"
|
| 26 |
+
DEFAULT_PRIMARY_TRAINING_DATASET_REPO = "MarisUK/maris-ai-lv-memory"
|
| 27 |
+
DEFAULT_TRAINING_DATASET_REPOS: list[str] = [
|
| 28 |
+
"MarisUK/maris-ai-memory",
|
| 29 |
+
DEFAULT_PRIMARY_TRAINING_DATASET_REPO,
|
| 30 |
+
"MarisUK/maris-ai-evals",
|
| 31 |
+
"MarisUK/maris-ai-benchmark",
|
| 32 |
+
]
|
| 33 |
+
DEFAULT_EVAL_DATASET_REPOS: list[str] = [
|
| 34 |
+
"MarisUK/maris-ai-evals",
|
| 35 |
+
"MarisUK/maris-ai-benchmark",
|
| 36 |
+
]
|
| 37 |
AVAILABLE_TRAINING_BASE_MODELS: dict[str, dict[str, str]] = {
|
| 38 |
"balanced": {
|
| 39 |
"model_name": DEFAULT_TRAINING_BASE_MODEL,
|
|
|
|
| 188 |
return [item.strip() for item in str(value).split(",") if item.strip()]
|
| 189 |
|
| 190 |
|
| 191 |
+
def _parse_repo_list(value: Any, *, default: list[str] | None = None) -> list[str]:
|
| 192 |
+
if value in (None, ""):
|
| 193 |
+
return list(default or [])
|
| 194 |
+
parsed = (
|
| 195 |
+
json.loads(value) if isinstance(value, str) and value.lstrip().startswith("[") else value
|
| 196 |
+
)
|
| 197 |
+
raw_items = parsed if isinstance(parsed, list) else EXTRA_MODEL_SPLIT_RE.split(str(parsed))
|
| 198 |
+
normalized: list[str] = []
|
| 199 |
+
for item in raw_items:
|
| 200 |
+
candidate = str(item or "").strip()
|
| 201 |
+
if candidate and candidate not in normalized:
|
| 202 |
+
normalized.append(candidate)
|
| 203 |
+
return normalized
|
| 204 |
+
|
| 205 |
+
|
| 206 |
@dataclass(slots=True)
|
| 207 |
class TrainingConfig:
|
| 208 |
"""Pilna apmācības konfigurācija vienam Maris treniņa skrējienam."""
|
|
|
|
| 220 |
qlora_quant_type: str = "nf4"
|
| 221 |
qlora_use_double_quant: bool = True
|
| 222 |
qlora_compute_dtype: str = "float16"
|
| 223 |
+
dataset_repo: str = DEFAULT_PRIMARY_TRAINING_DATASET_REPO
|
| 224 |
+
dataset_repos: list[str] = field(default_factory=list)
|
| 225 |
eval_dataset_repo: str = ""
|
| 226 |
+
eval_dataset_repos: list[str] = field(default_factory=list)
|
| 227 |
output_dir: str = "./output/model"
|
| 228 |
hub_model_id: str = DEFAULT_MASTER_MODEL_REPO
|
| 229 |
text_model_id: str = DEFAULT_TEXT_MODEL_REPO
|
|
|
|
| 573 |
"HF_TRAIN_QLORA_COMPUTE_DTYPE",
|
| 574 |
),
|
| 575 |
"dataset_repo": get_env_any("MARIS_MEMORY_REPO", "MARIS_DATASET_REPO", "HF_DATASET_REPO"),
|
| 576 |
+
"dataset_repos": get_env_any("MARIS_DATASET_REPOS", "HF_DATASET_REPOS"),
|
| 577 |
"eval_dataset_repo": get_env_any("MARIS_EVAL_DATASET_REPO", "HF_EVAL_DATASET_REPO"),
|
| 578 |
+
"eval_dataset_repos": get_env_any("MARIS_EVAL_DATASET_REPOS", "HF_EVAL_DATASET_REPOS"),
|
| 579 |
"output_dir": get_env_any("MARIS_TRAIN_OUTPUT_DIR", "HF_TRAIN_OUTPUT_DIR"),
|
| 580 |
"hub_model_id": get_env_any("MARIS_MODEL_REPO", "HF_MODEL_REPO"),
|
| 581 |
"text_model_id": get_env_any("TEXT_MODEL", default=DEFAULT_TEXT_MODEL_REPO),
|
|
|
|
| 833 |
"MARIS_MEMORY_REPO/MARIS_DATASET_REPO/HF_DATASET_REPO/dataset_repo",
|
| 834 |
label="dataset repozitorijs",
|
| 835 |
),
|
| 836 |
+
dataset_repos=[],
|
| 837 |
eval_dataset_repo=(
|
| 838 |
validate_maris_repo(
|
| 839 |
str(merged["eval_dataset_repo"]),
|
|
|
|
| 843 |
if merged.get("eval_dataset_repo") not in (None, "")
|
| 844 |
else ""
|
| 845 |
),
|
| 846 |
+
eval_dataset_repos=[],
|
| 847 |
output_dir=str(merged["output_dir"]),
|
| 848 |
hub_model_id=validate_maris_model(
|
| 849 |
str(merged["hub_model_id"]),
|
|
|
|
| 914 |
save_safetensors=_parse_bool(merged.get("save_safetensors"), default=True),
|
| 915 |
lr_scheduler_type=str(merged["lr_scheduler_type"]),
|
| 916 |
benchmark_dataset_path=str(merged.get("benchmark_dataset_path", "") or ""),
|
| 917 |
+
benchmark_name=str(
|
| 918 |
+
merged.get("benchmark_name", DEFAULT_BENCHMARK_NAME) or DEFAULT_BENCHMARK_NAME
|
| 919 |
+
),
|
| 920 |
benchmark_levels=_parse_list(merged.get("benchmark_levels")) or ["local", "ci", "release"],
|
| 921 |
benchmark_min_overall=float(merged.get("benchmark_min_overall", 0.7)),
|
| 922 |
benchmark_gate_enabled=_parse_bool(merged.get("benchmark_gate_enabled"), default=False),
|
|
|
|
| 982 |
),
|
| 983 |
continue_model_path=str(merged.get("continue_model_path", "") or ""),
|
| 984 |
)
|
| 985 |
+
config.dataset_repos = [
|
| 986 |
+
validate_maris_repo(
|
| 987 |
+
repo_id,
|
| 988 |
+
"MARIS_DATASET_REPOS/HF_DATASET_REPOS/dataset_repos",
|
| 989 |
+
label="dataset repozitorijs",
|
| 990 |
+
)
|
| 991 |
+
for repo_id in _parse_repo_list(
|
| 992 |
+
merged.get("dataset_repos"),
|
| 993 |
+
default=[config.dataset_repo],
|
| 994 |
+
)
|
| 995 |
+
]
|
| 996 |
+
if config.dataset_repo not in config.dataset_repos:
|
| 997 |
+
config.dataset_repos.insert(0, config.dataset_repo)
|
| 998 |
+
config.eval_dataset_repos = [
|
| 999 |
+
validate_maris_repo(
|
| 1000 |
+
repo_id,
|
| 1001 |
+
"MARIS_EVAL_DATASET_REPOS/HF_EVAL_DATASET_REPOS/eval_dataset_repos",
|
| 1002 |
+
label="eval dataset repozitorijs",
|
| 1003 |
+
)
|
| 1004 |
+
for repo_id in _parse_repo_list(
|
| 1005 |
+
merged.get("eval_dataset_repos"),
|
| 1006 |
+
default=[config.eval_dataset_repo] if config.eval_dataset_repo else [],
|
| 1007 |
+
)
|
| 1008 |
+
]
|
| 1009 |
+
if config.eval_dataset_repo and config.eval_dataset_repo not in config.eval_dataset_repos:
|
| 1010 |
+
config.eval_dataset_repos.insert(0, config.eval_dataset_repo)
|
| 1011 |
if config.fp16 and config.bf16:
|
| 1012 |
raise ValueError("Maris training konfigurācijā nevar vienlaikus ieslēgt fp16 un bf16.")
|
| 1013 |
if config.adapter_type not in {"full", "lora", "qlora", "specialist_model"}:
|
core-python/maris_core/training/hf_compat.py
CHANGED
|
@@ -99,7 +99,9 @@ def _build_restore_entries(output_dir: Path) -> dict[str, dict[str, str]]:
|
|
| 99 |
if payload is None:
|
| 100 |
continue
|
| 101 |
existing_restore_fields: dict[str, Any] = {}
|
| 102 |
-
existing_entry =
|
|
|
|
|
|
|
| 103 |
if isinstance(existing_entry, dict) and isinstance(existing_entry.get("payload"), str):
|
| 104 |
existing_restore_fields = _decode_restore_payload(existing_entry["payload"])
|
| 105 |
sanitized_fields = _SANITIZED_COMPATIBILITY_FIELDS.get(artifact_name, {})
|
|
@@ -165,7 +167,9 @@ def has_maris_compatibility_artifact(model_dir: Path) -> bool:
|
|
| 165 |
)
|
| 166 |
|
| 167 |
|
| 168 |
-
def _restore_compatibility_artifact(
|
|
|
|
|
|
|
| 169 |
restored = dict(payload)
|
| 170 |
restored.update(restore_fields)
|
| 171 |
return restored
|
|
@@ -214,7 +218,9 @@ def _resolve_repo_snapshot(model_name_or_path: str) -> Path | None:
|
|
| 214 |
model_name_or_path,
|
| 215 |
)
|
| 216 |
try:
|
| 217 |
-
logger.info(
|
|
|
|
|
|
|
| 218 |
snapshot_dir = snapshot_download(repo_id=model_name_or_path, repo_type="model", token=token)
|
| 219 |
except Exception as exc: # noqa: BLE001
|
| 220 |
raise RuntimeError(
|
|
|
|
| 99 |
if payload is None:
|
| 100 |
continue
|
| 101 |
existing_restore_fields: dict[str, Any] = {}
|
| 102 |
+
existing_entry = (
|
| 103 |
+
existing_artifacts.get(artifact_name) if isinstance(existing_artifacts, dict) else None
|
| 104 |
+
)
|
| 105 |
if isinstance(existing_entry, dict) and isinstance(existing_entry.get("payload"), str):
|
| 106 |
existing_restore_fields = _decode_restore_payload(existing_entry["payload"])
|
| 107 |
sanitized_fields = _SANITIZED_COMPATIBILITY_FIELDS.get(artifact_name, {})
|
|
|
|
| 167 |
)
|
| 168 |
|
| 169 |
|
| 170 |
+
def _restore_compatibility_artifact(
|
| 171 |
+
payload: dict[str, Any], restore_fields: dict[str, Any]
|
| 172 |
+
) -> dict[str, Any]:
|
| 173 |
restored = dict(payload)
|
| 174 |
restored.update(restore_fields)
|
| 175 |
return restored
|
|
|
|
| 218 |
model_name_or_path,
|
| 219 |
)
|
| 220 |
try:
|
| 221 |
+
logger.info(
|
| 222 |
+
"Lejupielādē runtime modeli compatibility restore vajadzībām: %s", model_name_or_path
|
| 223 |
+
)
|
| 224 |
snapshot_dir = snapshot_download(repo_id=model_name_or_path, repo_type="model", token=token)
|
| 225 |
except Exception as exc: # noqa: BLE001
|
| 226 |
raise RuntimeError(
|
core-python/maris_core/training/human_training.py
CHANGED
|
@@ -206,7 +206,9 @@ class HumanTrainingExecuteRequest(BaseModel):
|
|
| 206 |
@model_validator(mode="after")
|
| 207 |
def validate_execution_flags(self) -> HumanTrainingExecuteRequest:
|
| 208 |
if self.start_training and not self.publish_artifacts:
|
| 209 |
-
raise ValueError(
|
|
|
|
|
|
|
| 210 |
return self
|
| 211 |
|
| 212 |
|
|
@@ -239,7 +241,9 @@ def resolve_human_training_stage_dir(persistent_dir: str, run_id: str) -> Path:
|
|
| 239 |
root = Path(persistent_dir).expanduser().resolve()
|
| 240 |
target = (root / HUMAN_TRAINING_STAGE_DIRNAME / run_id).resolve()
|
| 241 |
if os.path.commonpath([str(root), str(target)]) != str(root):
|
| 242 |
-
raise ValueError(
|
|
|
|
|
|
|
| 243 |
return target
|
| 244 |
|
| 245 |
|
|
@@ -262,20 +266,22 @@ def stage_human_training_artifacts(
|
|
| 262 |
split_name="train",
|
| 263 |
config=quality_config,
|
| 264 |
)
|
| 265 |
-
filtered_eval, eval_report =
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 270 |
quality_report = build_dataset_quality_report(
|
| 271 |
config=quality_config,
|
| 272 |
train_report=train_report,
|
| 273 |
eval_report=eval_report,
|
| 274 |
).to_dict()
|
| 275 |
|
| 276 |
-
preference_examples = [
|
| 277 |
-
PreferenceExample(**item) for item in preference_dataset["preferences"]
|
| 278 |
-
]
|
| 279 |
preference_summary = (
|
| 280 |
summarize_preference_dataset(preference_examples) if preference_examples else None
|
| 281 |
)
|
|
@@ -438,7 +444,9 @@ def _build_train_records(request: HumanTrainingRequest) -> list[dict[str, Any]]:
|
|
| 438 |
def _build_profile_record(request: HumanTrainingRequest) -> dict[str, Any] | None:
|
| 439 |
sections: list[str] = []
|
| 440 |
if request.profile_facts:
|
| 441 |
-
sections.append(
|
|
|
|
|
|
|
| 442 |
if request.profile_preferences:
|
| 443 |
sections.append(
|
| 444 |
"Lietotāja preferences:\n"
|
|
|
|
| 206 |
@model_validator(mode="after")
|
| 207 |
def validate_execution_flags(self) -> HumanTrainingExecuteRequest:
|
| 208 |
if self.start_training and not self.publish_artifacts:
|
| 209 |
+
raise ValueError(
|
| 210 |
+
"Lai sāktu treniņu, artefakti vispirms jāpublicē dataset repozitorijā."
|
| 211 |
+
)
|
| 212 |
return self
|
| 213 |
|
| 214 |
|
|
|
|
| 241 |
root = Path(persistent_dir).expanduser().resolve()
|
| 242 |
target = (root / HUMAN_TRAINING_STAGE_DIRNAME / run_id).resolve()
|
| 243 |
if os.path.commonpath([str(root), str(target)]) != str(root):
|
| 244 |
+
raise ValueError(
|
| 245 |
+
"Human training staging direktorijai jāatrodas persistent storage ietvaros."
|
| 246 |
+
)
|
| 247 |
return target
|
| 248 |
|
| 249 |
|
|
|
|
| 266 |
split_name="train",
|
| 267 |
config=quality_config,
|
| 268 |
)
|
| 269 |
+
filtered_eval, eval_report = (
|
| 270 |
+
apply_quality_gate_to_records(
|
| 271 |
+
eval_records,
|
| 272 |
+
split_name="eval",
|
| 273 |
+
config=quality_config,
|
| 274 |
+
)
|
| 275 |
+
if eval_records
|
| 276 |
+
else ([], None)
|
| 277 |
+
)
|
| 278 |
quality_report = build_dataset_quality_report(
|
| 279 |
config=quality_config,
|
| 280 |
train_report=train_report,
|
| 281 |
eval_report=eval_report,
|
| 282 |
).to_dict()
|
| 283 |
|
| 284 |
+
preference_examples = [PreferenceExample(**item) for item in preference_dataset["preferences"]]
|
|
|
|
|
|
|
| 285 |
preference_summary = (
|
| 286 |
summarize_preference_dataset(preference_examples) if preference_examples else None
|
| 287 |
)
|
|
|
|
| 444 |
def _build_profile_record(request: HumanTrainingRequest) -> dict[str, Any] | None:
|
| 445 |
sections: list[str] = []
|
| 446 |
if request.profile_facts:
|
| 447 |
+
sections.append(
|
| 448 |
+
"Fakti par lietotāju:\n" + "\n".join(f"- {item}" for item in request.profile_facts)
|
| 449 |
+
)
|
| 450 |
if request.profile_preferences:
|
| 451 |
sections.append(
|
| 452 |
"Lietotāja preferences:\n"
|
core-python/maris_core/training/space_ui.py
CHANGED
|
@@ -380,6 +380,9 @@ def parse_training_progress(
|
|
| 380 |
elif stage == "benchmarking":
|
| 381 |
label = structured_label or "Palaiž benchmark un release gate pārbaudes"
|
| 382 |
percent = 94
|
|
|
|
|
|
|
|
|
|
| 383 |
elif any(token in lower_log for token in ("uploading", "pushing", "export_to_hf")):
|
| 384 |
stage = "publishing"
|
| 385 |
label = "Publicē modeli origin repozitorijā"
|
|
|
|
| 380 |
elif stage == "benchmarking":
|
| 381 |
label = structured_label or "Palaiž benchmark un release gate pārbaudes"
|
| 382 |
percent = 94
|
| 383 |
+
elif stage == "preparing":
|
| 384 |
+
label = structured_label or "Sagatavo datus, modeli un cache"
|
| 385 |
+
percent = 20
|
| 386 |
elif any(token in lower_log for token in ("uploading", "pushing", "export_to_hf")):
|
| 387 |
stage = "publishing"
|
| 388 |
label = "Publicē modeli origin repozitorijā"
|
core-python/maris_core/training/train.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import asyncio
|
|
|
|
| 6 |
import inspect
|
| 7 |
import json
|
| 8 |
import logging
|
|
@@ -143,6 +144,7 @@ LOCAL_TRAINING_ARTIFACT_FILES = (
|
|
| 143 |
"model.safetensors",
|
| 144 |
"pytorch_model.bin",
|
| 145 |
)
|
|
|
|
| 146 |
REPO_ROOT = Path(__file__).resolve().parents[3]
|
| 147 |
|
| 148 |
|
|
@@ -332,6 +334,80 @@ def _get_split(dataset: Any, *names: str) -> Any:
|
|
| 332 |
return None
|
| 333 |
|
| 334 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 335 |
def _prepare_train_eval_splits(
|
| 336 |
dataset: Any,
|
| 337 |
config: TrainingConfig,
|
|
@@ -930,19 +1006,36 @@ def _is_local_training_artifact_dir(path: Path) -> bool:
|
|
| 930 |
)
|
| 931 |
|
| 932 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 933 |
def _resolve_training_model_source(config: TrainingConfig) -> str:
|
| 934 |
"""Resolve the effective source model, preferring local persistent artifacts when enabled."""
|
| 935 |
if not config.continue_from_latest_artifact:
|
| 936 |
return config.model_name
|
| 937 |
|
| 938 |
-
candidates: list[Path] = []
|
| 939 |
if config.continue_model_path:
|
| 940 |
-
candidates.append(Path(config.continue_model_path))
|
| 941 |
output_dir = Path(config.output_dir)
|
| 942 |
-
candidates.append(output_dir)
|
|
|
|
| 943 |
|
| 944 |
seen: set[Path] = set()
|
| 945 |
-
for candidate in candidates:
|
| 946 |
try:
|
| 947 |
resolved = candidate.expanduser().resolve()
|
| 948 |
except OSError:
|
|
@@ -951,6 +1044,20 @@ def _resolve_training_model_source(config: TrainingConfig) -> str:
|
|
| 951 |
continue
|
| 952 |
seen.add(resolved)
|
| 953 |
if _is_local_training_artifact_dir(resolved):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 954 |
logger.info("Turpinu treniņu no lokālā artefakta: %s", resolved)
|
| 955 |
return str(resolved)
|
| 956 |
|
|
@@ -1129,6 +1236,7 @@ def _build_training_artifact_config(config: TrainingConfig) -> dict[str, Any]:
|
|
| 1129 |
payload = config.to_dict().copy()
|
| 1130 |
payload.pop("model_name", None)
|
| 1131 |
payload["model_preset"] = config.model_preset or "custom"
|
|
|
|
| 1132 |
payload["maris_origin"] = MARIS_ORIGIN_NAME
|
| 1133 |
payload["maris_framework"] = MARIS_FRAMEWORK_NAME
|
| 1134 |
payload["maris_model_id"] = config.hub_model_id
|
|
@@ -1141,7 +1249,9 @@ def _build_artifact_identity(config: TrainingConfig) -> dict[str, Any]:
|
|
| 1141 |
"maris_origin": MARIS_ORIGIN_NAME,
|
| 1142 |
"maris_framework": MARIS_FRAMEWORK_NAME,
|
| 1143 |
"dataset_repo": config.dataset_repo,
|
| 1144 |
-
"
|
|
|
|
|
|
|
| 1145 |
"branch_name": config.branch_name,
|
| 1146 |
"branch_focus": config.branch_focus,
|
| 1147 |
}
|
|
@@ -1227,7 +1337,9 @@ def _build_model_card(
|
|
| 1227 |
"## Training Data",
|
| 1228 |
"",
|
| 1229 |
f"- Dataset repo: `{config.dataset_repo}`",
|
| 1230 |
-
f"-
|
|
|
|
|
|
|
| 1231 |
f"- Train examples: `{train_examples}`",
|
| 1232 |
f"- Eval examples: `{eval_examples}`",
|
| 1233 |
f"- Branch focus: `{config.branch_focus}`",
|
|
@@ -1283,7 +1395,9 @@ def _write_training_provenance(
|
|
| 1283 |
{
|
| 1284 |
"trained_at": trained_at,
|
| 1285 |
"dataset_repo": config.dataset_repo,
|
| 1286 |
-
"
|
|
|
|
|
|
|
| 1287 |
"branch_name": config.branch_name,
|
| 1288 |
"branch_focus": config.branch_focus,
|
| 1289 |
"adapter_type": config.adapter_type,
|
|
@@ -1666,7 +1780,9 @@ def _apply_branch_runtime_defaults(config: TrainingConfig) -> TrainingConfig:
|
|
| 1666 |
return config
|
| 1667 |
if not benchmark_dataset_path and config.benchmark_gate_enabled:
|
| 1668 |
benchmark_dataset_path = _resolve_branch_benchmark_dataset_path(config, config.branch_name)
|
| 1669 |
-
if config.benchmark_gate_enabled and (
|
|
|
|
|
|
|
| 1670 |
benchmark_name = _resolve_branch_benchmark_name(config, config.branch_name)
|
| 1671 |
if not preference_dataset_path:
|
| 1672 |
preference_dataset_path = _resolve_branch_preference_dataset_path(
|
|
@@ -2568,9 +2684,19 @@ def train_with_config(config: TrainingConfig) -> dict[str, float]:
|
|
| 2568 |
"""Apmāca modeli pēc pilnas konfigurācijas."""
|
| 2569 |
config = _apply_branch_runtime_defaults(_normalize_training_runtime_config(config))
|
| 2570 |
_ensure_runtime_home_dir()
|
|
|
|
|
|
|
| 2571 |
try:
|
| 2572 |
-
|
| 2573 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2574 |
except HFDatasetError as exc:
|
| 2575 |
logger.error("Apmācība apturēta: %s", exc)
|
| 2576 |
raise SystemExit(str(exc)) from None
|
|
@@ -2584,22 +2710,39 @@ def train_with_config(config: TrainingConfig) -> dict[str, float]:
|
|
| 2584 |
Trainer,
|
| 2585 |
TrainingArguments,
|
| 2586 |
)
|
|
|
|
| 2587 |
try:
|
| 2588 |
from transformers import TrainerCallback # type: ignore
|
| 2589 |
except ImportError: # pragma: no cover - fallback for lightweight test doubles
|
|
|
|
| 2590 |
class TrainerCallback: # type: ignore[no-redef]
|
| 2591 |
pass
|
| 2592 |
|
| 2593 |
training_model_source = _resolve_training_model_source(config)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2594 |
logger.info("Ielādē modeli: %s", training_model_source)
|
| 2595 |
tokenizer = _load_tokenizer(training_model_source, config)
|
| 2596 |
_configure_tokenizer(tokenizer, config)
|
| 2597 |
model = _prepare_training_model(training_model_source, tokenizer, config)
|
| 2598 |
|
| 2599 |
train_split, eval_split = _prepare_train_eval_splits(dataset, config)
|
| 2600 |
-
if
|
| 2601 |
-
|
| 2602 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2603 |
eval_split = _select_eval_split(
|
| 2604 |
eval_source_dataset,
|
| 2605 |
config,
|
|
@@ -2648,6 +2791,12 @@ def train_with_config(config: TrainingConfig) -> dict[str, float]:
|
|
| 2648 |
expand_weights=False,
|
| 2649 |
benchmark_feedback=benchmark_feedback,
|
| 2650 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2651 |
train_dataset = _tokenize_dataset(train_split, tokenizer, config.max_seq_length)
|
| 2652 |
eval_dataset = (
|
| 2653 |
_tokenize_dataset(eval_split, tokenizer, config.max_seq_length)
|
|
@@ -2655,6 +2804,12 @@ def train_with_config(config: TrainingConfig) -> dict[str, float]:
|
|
| 2655 |
else None
|
| 2656 |
)
|
| 2657 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2658 |
training_args = _build_training_arguments(
|
| 2659 |
TrainingArguments,
|
| 2660 |
output_dir=config.output_dir,
|
|
@@ -2908,8 +3063,10 @@ def evaluate_with_config(
|
|
| 2908 |
"""Novērtē modeli ar to pašu datu pipeline, ko izmanto apmācībai."""
|
| 2909 |
config = _apply_branch_runtime_defaults(config)
|
| 2910 |
_ensure_runtime_home_dir()
|
|
|
|
|
|
|
| 2911 |
try:
|
| 2912 |
-
dataset =
|
| 2913 |
except HFDatasetError as exc:
|
| 2914 |
logger.error("Novērtēšana apturēta: %s", exc)
|
| 2915 |
raise SystemExit(str(exc)) from None
|
|
@@ -2925,7 +3082,7 @@ def evaluate_with_config(
|
|
| 2925 |
_configure_tokenizer(tokenizer, config)
|
| 2926 |
model = _load_model(resolved_model, config)
|
| 2927 |
|
| 2928 |
-
if
|
| 2929 |
reference_split = _select_eval_split(dataset, config, allow_train_fallback=True)
|
| 2930 |
else:
|
| 2931 |
train_split, eval_split = _prepare_train_eval_splits(dataset, config)
|
|
@@ -2997,7 +3154,9 @@ def train(
|
|
| 2997 |
config_path: str | None = None,
|
| 2998 |
model_name: str | None = None,
|
| 2999 |
dataset_repo: str | None = None,
|
|
|
|
| 3000 |
eval_dataset_repo: str | None = None,
|
|
|
|
| 3001 |
benchmark_dataset_path: str | None = None,
|
| 3002 |
benchmark_feedback_path: str | None = None,
|
| 3003 |
preference_dataset_path: str | None = None,
|
|
@@ -3040,7 +3199,9 @@ def train(
|
|
| 3040 |
overrides={
|
| 3041 |
"model_name": model_name,
|
| 3042 |
"dataset_repo": dataset_repo,
|
|
|
|
| 3043 |
"eval_dataset_repo": eval_dataset_repo,
|
|
|
|
| 3044 |
"benchmark_dataset_path": benchmark_dataset_path,
|
| 3045 |
"benchmark_feedback_path": benchmark_feedback_path,
|
| 3046 |
"preference_dataset_path": preference_dataset_path,
|
|
|
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import asyncio
|
| 6 |
+
import hashlib
|
| 7 |
import inspect
|
| 8 |
import json
|
| 9 |
import logging
|
|
|
|
| 144 |
"model.safetensors",
|
| 145 |
"pytorch_model.bin",
|
| 146 |
)
|
| 147 |
+
MODEL_SOURCE_FINGERPRINT_KEY = "model_source_fingerprint"
|
| 148 |
REPO_ROOT = Path(__file__).resolve().parents[3]
|
| 149 |
|
| 150 |
|
|
|
|
| 334 |
return None
|
| 335 |
|
| 336 |
|
| 337 |
+
def _dataset_split_names(dataset: Any) -> list[str]:
|
| 338 |
+
if isinstance(dataset, dict):
|
| 339 |
+
return [str(name) for name in dataset]
|
| 340 |
+
keys = getattr(dataset, "keys", None)
|
| 341 |
+
if callable(keys):
|
| 342 |
+
try:
|
| 343 |
+
return [str(name) for name in keys()]
|
| 344 |
+
except Exception: # noqa: BLE001
|
| 345 |
+
pass
|
| 346 |
+
return [
|
| 347 |
+
name
|
| 348 |
+
for name in ("train", "validation", "eval", "test")
|
| 349 |
+
if _get_split(dataset, name) is not None
|
| 350 |
+
]
|
| 351 |
+
|
| 352 |
+
|
| 353 |
+
def _merge_dataset_splits(splits: list[Any]) -> Any:
|
| 354 |
+
merged_splits = [split for split in splits if split is not None]
|
| 355 |
+
if not merged_splits:
|
| 356 |
+
return None
|
| 357 |
+
if len(merged_splits) == 1:
|
| 358 |
+
return merged_splits[0]
|
| 359 |
+
if all(type(split).__module__.startswith("datasets") for split in merged_splits):
|
| 360 |
+
try:
|
| 361 |
+
from datasets import concatenate_datasets # type: ignore
|
| 362 |
+
|
| 363 |
+
return concatenate_datasets(merged_splits)
|
| 364 |
+
except Exception as exc: # noqa: BLE001
|
| 365 |
+
logger.warning(
|
| 366 |
+
"HF split concatenation neizdevās; pārslēdzamies uz ierakstu līmeņa merge: %s",
|
| 367 |
+
exc,
|
| 368 |
+
)
|
| 369 |
+
records: list[dict[str, Any]] = []
|
| 370 |
+
for split in merged_splits:
|
| 371 |
+
records.extend(_materialize_split_records(split))
|
| 372 |
+
return _rebuild_split_like(merged_splits[0], records)
|
| 373 |
+
|
| 374 |
+
|
| 375 |
+
def _load_combined_hf_dataset(repo_ids: list[str]) -> Any:
|
| 376 |
+
datasets_by_repo = [(repo_id, load_hf_dataset(repo_id)) for repo_id in repo_ids]
|
| 377 |
+
split_names: list[str] = []
|
| 378 |
+
for _, dataset in datasets_by_repo:
|
| 379 |
+
for split_name in _dataset_split_names(dataset):
|
| 380 |
+
if split_name not in split_names:
|
| 381 |
+
split_names.append(split_name)
|
| 382 |
+
if not split_names:
|
| 383 |
+
raise ValueError("Neviens no norādītajiem dataset repo nesatur pieejamus splitus.")
|
| 384 |
+
return {
|
| 385 |
+
split_name: _merge_dataset_splits(
|
| 386 |
+
[_get_split(dataset, split_name) for _, dataset in datasets_by_repo]
|
| 387 |
+
)
|
| 388 |
+
for split_name in split_names
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
|
| 392 |
+
def _resolve_training_dataset_repos(config: TrainingConfig) -> list[str]:
|
| 393 |
+
return list(config.dataset_repos or [config.dataset_repo])
|
| 394 |
+
|
| 395 |
+
|
| 396 |
+
def _resolve_eval_dataset_repos(config: TrainingConfig) -> list[str]:
|
| 397 |
+
if config.eval_dataset_repos:
|
| 398 |
+
return list(config.eval_dataset_repos)
|
| 399 |
+
if config.eval_dataset_repo:
|
| 400 |
+
return [config.eval_dataset_repo]
|
| 401 |
+
return []
|
| 402 |
+
|
| 403 |
+
|
| 404 |
+
def _resolve_primary_eval_dataset_repo(config: TrainingConfig) -> str:
|
| 405 |
+
eval_dataset_repos = _resolve_eval_dataset_repos(config)
|
| 406 |
+
if eval_dataset_repos:
|
| 407 |
+
return eval_dataset_repos[0]
|
| 408 |
+
return config.eval_dataset_repo or config.dataset_repo
|
| 409 |
+
|
| 410 |
+
|
| 411 |
def _prepare_train_eval_splits(
|
| 412 |
dataset: Any,
|
| 413 |
config: TrainingConfig,
|
|
|
|
| 1006 |
)
|
| 1007 |
|
| 1008 |
|
| 1009 |
+
def _build_model_source_fingerprint(model_name: str) -> str:
|
| 1010 |
+
normalized = model_name.strip().casefold().encode("utf-8")
|
| 1011 |
+
return hashlib.sha256(normalized).hexdigest()
|
| 1012 |
+
|
| 1013 |
+
|
| 1014 |
+
def _load_local_model_source_fingerprint(path: Path) -> str | None:
|
| 1015 |
+
training_config = _load_json_if_exists(path / "training-config.json")
|
| 1016 |
+
if not isinstance(training_config, dict):
|
| 1017 |
+
return None
|
| 1018 |
+
fingerprint = training_config.get(MODEL_SOURCE_FINGERPRINT_KEY)
|
| 1019 |
+
if not isinstance(fingerprint, str):
|
| 1020 |
+
return None
|
| 1021 |
+
normalized = fingerprint.strip().casefold()
|
| 1022 |
+
return normalized or None
|
| 1023 |
+
|
| 1024 |
+
|
| 1025 |
def _resolve_training_model_source(config: TrainingConfig) -> str:
|
| 1026 |
"""Resolve the effective source model, preferring local persistent artifacts when enabled."""
|
| 1027 |
if not config.continue_from_latest_artifact:
|
| 1028 |
return config.model_name
|
| 1029 |
|
| 1030 |
+
candidates: list[tuple[Path, bool]] = []
|
| 1031 |
if config.continue_model_path:
|
| 1032 |
+
candidates.append((Path(config.continue_model_path), True))
|
| 1033 |
output_dir = Path(config.output_dir)
|
| 1034 |
+
candidates.append((output_dir, False))
|
| 1035 |
+
expected_fingerprint = _build_model_source_fingerprint(config.model_name)
|
| 1036 |
|
| 1037 |
seen: set[Path] = set()
|
| 1038 |
+
for candidate, explicit_candidate in candidates:
|
| 1039 |
try:
|
| 1040 |
resolved = candidate.expanduser().resolve()
|
| 1041 |
except OSError:
|
|
|
|
| 1044 |
continue
|
| 1045 |
seen.add(resolved)
|
| 1046 |
if _is_local_training_artifact_dir(resolved):
|
| 1047 |
+
if not explicit_candidate:
|
| 1048 |
+
saved_fingerprint = _load_local_model_source_fingerprint(resolved)
|
| 1049 |
+
if saved_fingerprint != expected_fingerprint:
|
| 1050 |
+
if saved_fingerprint:
|
| 1051 |
+
logger.info(
|
| 1052 |
+
"Izlaižu auto-continue no %s, jo saglabātā modeļa bāze neatbilst izvēlētajam modelim.",
|
| 1053 |
+
resolved,
|
| 1054 |
+
)
|
| 1055 |
+
else:
|
| 1056 |
+
logger.info(
|
| 1057 |
+
"Izlaižu auto-continue no %s, jo trūkst modeļa saderības metadata.",
|
| 1058 |
+
resolved,
|
| 1059 |
+
)
|
| 1060 |
+
continue
|
| 1061 |
logger.info("Turpinu treniņu no lokālā artefakta: %s", resolved)
|
| 1062 |
return str(resolved)
|
| 1063 |
|
|
|
|
| 1236 |
payload = config.to_dict().copy()
|
| 1237 |
payload.pop("model_name", None)
|
| 1238 |
payload["model_preset"] = config.model_preset or "custom"
|
| 1239 |
+
payload[MODEL_SOURCE_FINGERPRINT_KEY] = _build_model_source_fingerprint(config.model_name)
|
| 1240 |
payload["maris_origin"] = MARIS_ORIGIN_NAME
|
| 1241 |
payload["maris_framework"] = MARIS_FRAMEWORK_NAME
|
| 1242 |
payload["maris_model_id"] = config.hub_model_id
|
|
|
|
| 1249 |
"maris_origin": MARIS_ORIGIN_NAME,
|
| 1250 |
"maris_framework": MARIS_FRAMEWORK_NAME,
|
| 1251 |
"dataset_repo": config.dataset_repo,
|
| 1252 |
+
"dataset_repos": _resolve_training_dataset_repos(config),
|
| 1253 |
+
"eval_dataset_repo": _resolve_primary_eval_dataset_repo(config),
|
| 1254 |
+
"eval_dataset_repos": _resolve_eval_dataset_repos(config),
|
| 1255 |
"branch_name": config.branch_name,
|
| 1256 |
"branch_focus": config.branch_focus,
|
| 1257 |
}
|
|
|
|
| 1337 |
"## Training Data",
|
| 1338 |
"",
|
| 1339 |
f"- Dataset repo: `{config.dataset_repo}`",
|
| 1340 |
+
f"- Dataset repos: `{', '.join(_resolve_training_dataset_repos(config))}`",
|
| 1341 |
+
f"- Eval dataset repo: `{_resolve_primary_eval_dataset_repo(config)}`",
|
| 1342 |
+
f"- Eval dataset repos: `{', '.join(_resolve_eval_dataset_repos(config) or [_resolve_primary_eval_dataset_repo(config)])}`",
|
| 1343 |
f"- Train examples: `{train_examples}`",
|
| 1344 |
f"- Eval examples: `{eval_examples}`",
|
| 1345 |
f"- Branch focus: `{config.branch_focus}`",
|
|
|
|
| 1395 |
{
|
| 1396 |
"trained_at": trained_at,
|
| 1397 |
"dataset_repo": config.dataset_repo,
|
| 1398 |
+
"dataset_repos": _resolve_training_dataset_repos(config),
|
| 1399 |
+
"eval_dataset_repo": _resolve_primary_eval_dataset_repo(config),
|
| 1400 |
+
"eval_dataset_repos": _resolve_eval_dataset_repos(config),
|
| 1401 |
"branch_name": config.branch_name,
|
| 1402 |
"branch_focus": config.branch_focus,
|
| 1403 |
"adapter_type": config.adapter_type,
|
|
|
|
| 1780 |
return config
|
| 1781 |
if not benchmark_dataset_path and config.benchmark_gate_enabled:
|
| 1782 |
benchmark_dataset_path = _resolve_branch_benchmark_dataset_path(config, config.branch_name)
|
| 1783 |
+
if config.benchmark_gate_enabled and (
|
| 1784 |
+
not benchmark_name or benchmark_name == DEFAULT_BENCHMARK_NAME
|
| 1785 |
+
):
|
| 1786 |
benchmark_name = _resolve_branch_benchmark_name(config, config.branch_name)
|
| 1787 |
if not preference_dataset_path:
|
| 1788 |
preference_dataset_path = _resolve_branch_preference_dataset_path(
|
|
|
|
| 2684 |
"""Apmāca modeli pēc pilnas konfigurācijas."""
|
| 2685 |
config = _apply_branch_runtime_defaults(_normalize_training_runtime_config(config))
|
| 2686 |
_ensure_runtime_home_dir()
|
| 2687 |
+
training_dataset_repos = _resolve_training_dataset_repos(config)
|
| 2688 |
+
eval_dataset_repos = _resolve_eval_dataset_repos(config)
|
| 2689 |
try:
|
| 2690 |
+
_emit_training_progress_event(
|
| 2691 |
+
"prepare_dataset",
|
| 2692 |
+
stage="preparing",
|
| 2693 |
+
label="Ielādē training datasetu",
|
| 2694 |
+
total_epochs=config.num_epochs,
|
| 2695 |
+
dataset_repo=config.dataset_repo,
|
| 2696 |
+
dataset_repos=training_dataset_repos,
|
| 2697 |
+
)
|
| 2698 |
+
logger.info("Ielādē training datasetus: %s", ", ".join(training_dataset_repos))
|
| 2699 |
+
dataset = _load_combined_hf_dataset(training_dataset_repos)
|
| 2700 |
except HFDatasetError as exc:
|
| 2701 |
logger.error("Apmācība apturēta: %s", exc)
|
| 2702 |
raise SystemExit(str(exc)) from None
|
|
|
|
| 2710 |
Trainer,
|
| 2711 |
TrainingArguments,
|
| 2712 |
)
|
| 2713 |
+
|
| 2714 |
try:
|
| 2715 |
from transformers import TrainerCallback # type: ignore
|
| 2716 |
except ImportError: # pragma: no cover - fallback for lightweight test doubles
|
| 2717 |
+
|
| 2718 |
class TrainerCallback: # type: ignore[no-redef]
|
| 2719 |
pass
|
| 2720 |
|
| 2721 |
training_model_source = _resolve_training_model_source(config)
|
| 2722 |
+
_emit_training_progress_event(
|
| 2723 |
+
"prepare_model",
|
| 2724 |
+
stage="preparing",
|
| 2725 |
+
label="Ielādē tokenizeri un modeli",
|
| 2726 |
+
total_epochs=config.num_epochs,
|
| 2727 |
+
model_source=training_model_source,
|
| 2728 |
+
)
|
| 2729 |
logger.info("Ielādē modeli: %s", training_model_source)
|
| 2730 |
tokenizer = _load_tokenizer(training_model_source, config)
|
| 2731 |
_configure_tokenizer(tokenizer, config)
|
| 2732 |
model = _prepare_training_model(training_model_source, tokenizer, config)
|
| 2733 |
|
| 2734 |
train_split, eval_split = _prepare_train_eval_splits(dataset, config)
|
| 2735 |
+
if eval_dataset_repos:
|
| 2736 |
+
_emit_training_progress_event(
|
| 2737 |
+
"prepare_eval_dataset",
|
| 2738 |
+
stage="preparing",
|
| 2739 |
+
label="Ielādē eval datasetu",
|
| 2740 |
+
total_epochs=config.num_epochs,
|
| 2741 |
+
dataset_repo=config.eval_dataset_repo or eval_dataset_repos[0],
|
| 2742 |
+
dataset_repos=eval_dataset_repos,
|
| 2743 |
+
)
|
| 2744 |
+
logger.info("Ielādē atsevišķus eval datasetus: %s", ", ".join(eval_dataset_repos))
|
| 2745 |
+
eval_source_dataset = _load_combined_hf_dataset(eval_dataset_repos)
|
| 2746 |
eval_split = _select_eval_split(
|
| 2747 |
eval_source_dataset,
|
| 2748 |
config,
|
|
|
|
| 2791 |
expand_weights=False,
|
| 2792 |
benchmark_feedback=benchmark_feedback,
|
| 2793 |
)
|
| 2794 |
+
_emit_training_progress_event(
|
| 2795 |
+
"prepare_tokenization",
|
| 2796 |
+
stage="preparing",
|
| 2797 |
+
label="Tokenizē treniņa un eval datus",
|
| 2798 |
+
total_epochs=config.num_epochs,
|
| 2799 |
+
)
|
| 2800 |
train_dataset = _tokenize_dataset(train_split, tokenizer, config.max_seq_length)
|
| 2801 |
eval_dataset = (
|
| 2802 |
_tokenize_dataset(eval_split, tokenizer, config.max_seq_length)
|
|
|
|
| 2804 |
else None
|
| 2805 |
)
|
| 2806 |
|
| 2807 |
+
_emit_training_progress_event(
|
| 2808 |
+
"prepare_runtime",
|
| 2809 |
+
stage="preparing",
|
| 2810 |
+
label="Sagatavo treneri un runtime",
|
| 2811 |
+
total_epochs=config.num_epochs,
|
| 2812 |
+
)
|
| 2813 |
training_args = _build_training_arguments(
|
| 2814 |
TrainingArguments,
|
| 2815 |
output_dir=config.output_dir,
|
|
|
|
| 3063 |
"""Novērtē modeli ar to pašu datu pipeline, ko izmanto apmācībai."""
|
| 3064 |
config = _apply_branch_runtime_defaults(config)
|
| 3065 |
_ensure_runtime_home_dir()
|
| 3066 |
+
eval_dataset_repos = _resolve_eval_dataset_repos(config)
|
| 3067 |
+
dataset_repos = eval_dataset_repos or _resolve_training_dataset_repos(config)
|
| 3068 |
try:
|
| 3069 |
+
dataset = _load_combined_hf_dataset(dataset_repos)
|
| 3070 |
except HFDatasetError as exc:
|
| 3071 |
logger.error("Novērtēšana apturēta: %s", exc)
|
| 3072 |
raise SystemExit(str(exc)) from None
|
|
|
|
| 3082 |
_configure_tokenizer(tokenizer, config)
|
| 3083 |
model = _load_model(resolved_model, config)
|
| 3084 |
|
| 3085 |
+
if eval_dataset_repos:
|
| 3086 |
reference_split = _select_eval_split(dataset, config, allow_train_fallback=True)
|
| 3087 |
else:
|
| 3088 |
train_split, eval_split = _prepare_train_eval_splits(dataset, config)
|
|
|
|
| 3154 |
config_path: str | None = None,
|
| 3155 |
model_name: str | None = None,
|
| 3156 |
dataset_repo: str | None = None,
|
| 3157 |
+
dataset_repos: str | list[str] | None = None,
|
| 3158 |
eval_dataset_repo: str | None = None,
|
| 3159 |
+
eval_dataset_repos: str | list[str] | None = None,
|
| 3160 |
benchmark_dataset_path: str | None = None,
|
| 3161 |
benchmark_feedback_path: str | None = None,
|
| 3162 |
preference_dataset_path: str | None = None,
|
|
|
|
| 3199 |
overrides={
|
| 3200 |
"model_name": model_name,
|
| 3201 |
"dataset_repo": dataset_repo,
|
| 3202 |
+
"dataset_repos": dataset_repos,
|
| 3203 |
"eval_dataset_repo": eval_dataset_repo,
|
| 3204 |
+
"eval_dataset_repos": eval_dataset_repos,
|
| 3205 |
"benchmark_dataset_path": benchmark_dataset_path,
|
| 3206 |
"benchmark_feedback_path": benchmark_feedback_path,
|
| 3207 |
"preference_dataset_path": preference_dataset_path,
|
core-python/scripts/export_to_hf.py
CHANGED
|
@@ -13,6 +13,14 @@ from maris_core.utils.env import validate_maris_model
|
|
| 13 |
logger = logging.getLogger(__name__)
|
| 14 |
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
def _upload_folder(api: object, folder_path: str, repo_id: str, commit_message: str) -> None:
|
| 17 |
logger.info("Eksportē modeli: %s -> %s", folder_path, repo_id)
|
| 18 |
api.create_repo(repo_id=repo_id, repo_type="model", exist_ok=True)
|
|
@@ -36,7 +44,18 @@ def _branch_suite_export_targets(
|
|
| 36 |
|
| 37 |
suite = json.loads(manifest_path.read_text(encoding="utf-8"))
|
| 38 |
branch_exports = {
|
| 39 |
-
"master":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
"image": os.getenv("IMAGE_MODEL", "MarisUK/maris-ai-image"),
|
| 41 |
"music": os.getenv("MUSIC_MODEL", "MarisUK/maris-ai-music"),
|
| 42 |
"tts": os.getenv("TTS_MODEL", "MarisUK/maris-tts-runtime"),
|
|
|
|
| 13 |
logger = logging.getLogger(__name__)
|
| 14 |
|
| 15 |
|
| 16 |
+
def _getenv_any(*names: str, default: str) -> str:
|
| 17 |
+
for name in names:
|
| 18 |
+
value = os.getenv(name, "").strip()
|
| 19 |
+
if value:
|
| 20 |
+
return value
|
| 21 |
+
return default
|
| 22 |
+
|
| 23 |
+
|
| 24 |
def _upload_folder(api: object, folder_path: str, repo_id: str, commit_message: str) -> None:
|
| 25 |
logger.info("Eksportē modeli: %s -> %s", folder_path, repo_id)
|
| 26 |
api.create_repo(repo_id=repo_id, repo_type="model", exist_ok=True)
|
|
|
|
| 44 |
|
| 45 |
suite = json.loads(manifest_path.read_text(encoding="utf-8"))
|
| 46 |
branch_exports = {
|
| 47 |
+
"master": _getenv_any(
|
| 48 |
+
"MARIS_TEXT_MODEL_REPO",
|
| 49 |
+
"HF_TEXT_MODEL_REPO",
|
| 50 |
+
"TEXT_MODEL",
|
| 51 |
+
default="MarisUK/maris-ai-lv",
|
| 52 |
+
),
|
| 53 |
+
"coder": _getenv_any(
|
| 54 |
+
"MARIS_CODEX_MODEL_REPO",
|
| 55 |
+
"HF_CODEX_MODEL_REPO",
|
| 56 |
+
"CODEX_MODEL",
|
| 57 |
+
default="MarisUK/maris-ai-codex",
|
| 58 |
+
),
|
| 59 |
"image": os.getenv("IMAGE_MODEL", "MarisUK/maris-ai-image"),
|
| 60 |
"music": os.getenv("MUSIC_MODEL", "MarisUK/maris-ai-music"),
|
| 61 |
"tts": os.getenv("TTS_MODEL", "MarisUK/maris-tts-runtime"),
|
core-python/tests/test_autonomous.py
CHANGED
|
@@ -1,12 +1,27 @@
|
|
| 1 |
"""Tests for stronger autonomous planning execution."""
|
| 2 |
|
|
|
|
| 3 |
from unittest.mock import patch
|
| 4 |
|
| 5 |
import pytest
|
| 6 |
|
| 7 |
from maris_core.autonomous.agent import StartRequest, StatusRequest, get_status, start_session
|
|
|
|
| 8 |
from maris_core.memory_context import ConversationMemoryStore
|
| 9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
@pytest.mark.asyncio
|
| 12 |
async def test_autonomous_session_executes_dependent_tasks_over_status_polls() -> None:
|
|
@@ -35,6 +50,7 @@ async def test_autonomous_task_retries_before_failing() -> None:
|
|
| 35 |
) as execute_task_mock,
|
| 36 |
):
|
| 37 |
started = await start_session(StartRequest(session_id="session-2", goal="Debugot servisu"))
|
|
|
|
| 38 |
retried = await get_status(StatusRequest(session_id="session-2"))
|
| 39 |
|
| 40 |
assert execute_task_mock.await_count == 2
|
|
@@ -47,6 +63,36 @@ async def test_autonomous_task_retries_before_failing() -> None:
|
|
| 47 |
assert any(event.event_type == "task.failed_attempt" for event in retried.events)
|
| 48 |
|
| 49 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
@pytest.mark.asyncio
|
| 51 |
async def test_autonomous_session_preserves_selected_persona() -> None:
|
| 52 |
captured_messages: list[dict[str, str]] = []
|
|
|
|
| 1 |
"""Tests for stronger autonomous planning execution."""
|
| 2 |
|
| 3 |
+
import asyncio
|
| 4 |
from unittest.mock import patch
|
| 5 |
|
| 6 |
import pytest
|
| 7 |
|
| 8 |
from maris_core.autonomous.agent import StartRequest, StatusRequest, get_status, start_session
|
| 9 |
+
from maris_core.autonomous.planner import Planner
|
| 10 |
from maris_core.memory_context import ConversationMemoryStore
|
| 11 |
|
| 12 |
+
BACKGROUND_TASK_SETTLE_SECONDS = 0.3
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def test_planner_decompose_splits_goal_without_regex_backtracking() -> None:
|
| 16 |
+
planner = Planner()
|
| 17 |
+
|
| 18 |
+
steps = planner.decompose("Ievāc datus un tad salīdzini rezultātus -> pārbaudi kopsavilkumu")
|
| 19 |
+
|
| 20 |
+
assert [step["action"] for step in steps[1:3]] == [
|
| 21 |
+
"Ievāc datus",
|
| 22 |
+
"salīdzini rezultātus",
|
| 23 |
+
]
|
| 24 |
+
|
| 25 |
|
| 26 |
@pytest.mark.asyncio
|
| 27 |
async def test_autonomous_session_executes_dependent_tasks_over_status_polls() -> None:
|
|
|
|
| 50 |
) as execute_task_mock,
|
| 51 |
):
|
| 52 |
started = await start_session(StartRequest(session_id="session-2", goal="Debugot servisu"))
|
| 53 |
+
await asyncio.sleep(BACKGROUND_TASK_SETTLE_SECONDS)
|
| 54 |
retried = await get_status(StatusRequest(session_id="session-2"))
|
| 55 |
|
| 56 |
assert execute_task_mock.await_count == 2
|
|
|
|
| 63 |
assert any(event.event_type == "task.failed_attempt" for event in retried.events)
|
| 64 |
|
| 65 |
|
| 66 |
+
@pytest.mark.asyncio
|
| 67 |
+
async def test_autonomous_session_keeps_running_without_status_polls() -> None:
|
| 68 |
+
async def fake_execute_task(
|
| 69 |
+
task: dict[str, object],
|
| 70 |
+
goal: str,
|
| 71 |
+
tasks: list[dict[str, object]],
|
| 72 |
+
*,
|
| 73 |
+
persona_id: str | None = None,
|
| 74 |
+
) -> dict[str, object]:
|
| 75 |
+
del goal, tasks, persona_id
|
| 76 |
+
return {"summary": f"Pabeigts: {task['description']}", "artifacts": {}, "metrics": {}}
|
| 77 |
+
|
| 78 |
+
with (
|
| 79 |
+
patch("maris_core.autonomous.agent.get_pipeline", return_value=None),
|
| 80 |
+
patch("maris_core.autonomous.agent._execute_task", side_effect=fake_execute_task),
|
| 81 |
+
):
|
| 82 |
+
started = await start_session(
|
| 83 |
+
StartRequest(session_id="session-auto", goal="Uztaisīt plānu")
|
| 84 |
+
)
|
| 85 |
+
assert started.tasks[0].status == "completed"
|
| 86 |
+
|
| 87 |
+
await asyncio.sleep(BACKGROUND_TASK_SETTLE_SECONDS)
|
| 88 |
+
current = await get_status(StatusRequest(session_id="session-auto"))
|
| 89 |
+
|
| 90 |
+
assert current.status == "completed"
|
| 91 |
+
assert current.progress_percent == 100
|
| 92 |
+
assert all(task.status == "completed" for task in current.tasks)
|
| 93 |
+
assert any(event.event_type == "session.completed" for event in current.events)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
@pytest.mark.asyncio
|
| 97 |
async def test_autonomous_session_preserves_selected_persona() -> None:
|
| 98 |
captured_messages: list[dict[str, str]] = []
|
core-python/tests/test_autonomous_runtime.py
CHANGED
|
@@ -27,7 +27,9 @@ async def test_autonomous_session_recovers_from_persistent_store(tmp_path: Path)
|
|
| 27 |
patch("maris_core.autonomous.agent.session_store", store),
|
| 28 |
patch("maris_core.autonomous.agent.get_pipeline", return_value=None),
|
| 29 |
):
|
| 30 |
-
started = await start_session(
|
|
|
|
|
|
|
| 31 |
_sessions.clear()
|
| 32 |
recovered = await get_status(StatusRequest(session_id="persisted-session"))
|
| 33 |
|
|
@@ -43,7 +45,12 @@ async def test_autonomous_session_recovers_from_persistent_store(tmp_path: Path)
|
|
| 43 |
@pytest.mark.asyncio
|
| 44 |
async def test_executor_runs_reasoning_with_runtime_generate() -> None:
|
| 45 |
executor = AutonomousTaskExecutor()
|
| 46 |
-
task = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
fake_response = SimpleNamespace(
|
| 49 |
response="Konkrēts darba rezultāts",
|
|
|
|
| 27 |
patch("maris_core.autonomous.agent.session_store", store),
|
| 28 |
patch("maris_core.autonomous.agent.get_pipeline", return_value=None),
|
| 29 |
):
|
| 30 |
+
started = await start_session(
|
| 31 |
+
StartRequest(session_id="persisted-session", goal="Uztaisīt plānu")
|
| 32 |
+
)
|
| 33 |
_sessions.clear()
|
| 34 |
recovered = await get_status(StatusRequest(session_id="persisted-session"))
|
| 35 |
|
|
|
|
| 45 |
@pytest.mark.asyncio
|
| 46 |
async def test_executor_runs_reasoning_with_runtime_generate() -> None:
|
| 47 |
executor = AutonomousTaskExecutor()
|
| 48 |
+
task = {
|
| 49 |
+
"id": "task-1",
|
| 50 |
+
"description": "Nosaki prioritātes",
|
| 51 |
+
"tool": "reasoning",
|
| 52 |
+
"depends_on": [],
|
| 53 |
+
}
|
| 54 |
|
| 55 |
fake_response = SimpleNamespace(
|
| 56 |
response="Konkrēts darba rezultāts",
|
core-python/tests/test_code.py
CHANGED
|
@@ -81,7 +81,9 @@ async def test_generate_code_requires_text_model() -> None:
|
|
| 81 |
|
| 82 |
|
| 83 |
@pytest.mark.asyncio
|
| 84 |
-
async def test_generate_code_uses_requested_hf_fallback_model_when_text_runtime_is_unavailable() ->
|
|
|
|
|
|
|
| 85 |
class FakeClient:
|
| 86 |
def chat_completion(
|
| 87 |
self,
|
|
@@ -109,7 +111,9 @@ async def test_generate_code_uses_requested_hf_fallback_model_when_text_runtime_
|
|
| 109 |
with (
|
| 110 |
patch("maris_core.code.generate_code.get_pipeline", return_value=None),
|
| 111 |
patch("maris_core.text.generate.create_hf_inference_client", return_value=FakeClient()),
|
| 112 |
-
patch.dict(
|
|
|
|
|
|
|
| 113 |
patch(
|
| 114 |
"maris_core.utils.hf_integration.HFIntegration.save_generation",
|
| 115 |
new_callable=AsyncMock,
|
|
@@ -199,7 +203,9 @@ async def test_generate_code_uses_stronger_engineering_system_prompt() -> None:
|
|
| 199 |
|
| 200 |
|
| 201 |
@pytest.mark.asyncio
|
| 202 |
-
async def test_generate_code_materializes_workspace_artifacts_from_structured_payload(
|
|
|
|
|
|
|
| 203 |
def fake_pipeline(
|
| 204 |
messages: list[dict[str, Any]], *, max_new_tokens: int, temperature: float
|
| 205 |
) -> list[dict[str, list[dict[str, str]]]]:
|
|
@@ -244,8 +250,10 @@ async def test_generate_code_materializes_workspace_artifacts_from_structured_pa
|
|
| 244 |
assert [file.path for file in response.files] == ["index.html", "assets/app.js"]
|
| 245 |
assert response.files[0].absolute_path is not None
|
| 246 |
assert bundle_path.exists()
|
| 247 |
-
assert
|
| 248 |
-
|
|
|
|
|
|
|
| 249 |
)
|
| 250 |
|
| 251 |
|
|
@@ -302,7 +310,9 @@ async def test_generate_code_uses_repo_aware_entrypoint_for_existing_project(tmp
|
|
| 302 |
"[project]\nname = 'demo'\nversion = '0.1.0'\n",
|
| 303 |
encoding="utf-8",
|
| 304 |
)
|
| 305 |
-
(project_root / "src/main.py").write_text(
|
|
|
|
|
|
|
| 306 |
|
| 307 |
captured_messages: list[dict[str, Any]] = []
|
| 308 |
|
|
|
|
| 81 |
|
| 82 |
|
| 83 |
@pytest.mark.asyncio
|
| 84 |
+
async def test_generate_code_uses_requested_hf_fallback_model_when_text_runtime_is_unavailable() -> (
|
| 85 |
+
None
|
| 86 |
+
):
|
| 87 |
class FakeClient:
|
| 88 |
def chat_completion(
|
| 89 |
self,
|
|
|
|
| 111 |
with (
|
| 112 |
patch("maris_core.code.generate_code.get_pipeline", return_value=None),
|
| 113 |
patch("maris_core.text.generate.create_hf_inference_client", return_value=FakeClient()),
|
| 114 |
+
patch.dict(
|
| 115 |
+
sys.modules, {"huggingface_hub": fake_hf_module, "huggingface_hub.utils": fake_hf_utils}
|
| 116 |
+
),
|
| 117 |
patch(
|
| 118 |
"maris_core.utils.hf_integration.HFIntegration.save_generation",
|
| 119 |
new_callable=AsyncMock,
|
|
|
|
| 203 |
|
| 204 |
|
| 205 |
@pytest.mark.asyncio
|
| 206 |
+
async def test_generate_code_materializes_workspace_artifacts_from_structured_payload(
|
| 207 |
+
tmp_path,
|
| 208 |
+
) -> None:
|
| 209 |
def fake_pipeline(
|
| 210 |
messages: list[dict[str, Any]], *, max_new_tokens: int, temperature: float
|
| 211 |
) -> list[dict[str, list[dict[str, str]]]]:
|
|
|
|
| 250 |
assert [file.path for file in response.files] == ["index.html", "assets/app.js"]
|
| 251 |
assert response.files[0].absolute_path is not None
|
| 252 |
assert bundle_path.exists()
|
| 253 |
+
assert (
|
| 254 |
+
Path(response.files[0].absolute_path or "")
|
| 255 |
+
.read_text(encoding="utf-8")
|
| 256 |
+
.startswith("<!doctype html>")
|
| 257 |
)
|
| 258 |
|
| 259 |
|
|
|
|
| 310 |
"[project]\nname = 'demo'\nversion = '0.1.0'\n",
|
| 311 |
encoding="utf-8",
|
| 312 |
)
|
| 313 |
+
(project_root / "src/main.py").write_text(
|
| 314 |
+
"def main() -> None:\n print('old')\n", encoding="utf-8"
|
| 315 |
+
)
|
| 316 |
|
| 317 |
captured_messages: list[dict[str, Any]] = []
|
| 318 |
|
core-python/tests/test_huggingface_human_training_space_studio.py
CHANGED
|
@@ -89,7 +89,9 @@ def test_build_populates_run_history_and_artifact_browser(monkeypatch, tmp_path:
|
|
| 89 |
)
|
| 90 |
draft_id = draft_response.json()["draft"]["draft_id"]
|
| 91 |
|
| 92 |
-
build_response = client.post(
|
|
|
|
|
|
|
| 93 |
|
| 94 |
assert build_response.status_code == 200
|
| 95 |
body = build_response.json()
|
|
|
|
| 89 |
)
|
| 90 |
draft_id = draft_response.json()["draft"]["draft_id"]
|
| 91 |
|
| 92 |
+
build_response = client.post(
|
| 93 |
+
f"/api/human-training/build?draft_id={draft_id}", json=_sample_payload()
|
| 94 |
+
)
|
| 95 |
|
| 96 |
assert build_response.status_code == 200
|
| 97 |
body = build_response.json()
|
core-python/tests/test_huggingface_sync.py
CHANGED
|
@@ -148,6 +148,126 @@ def test_space_uploads_pass_space_sdk(
|
|
| 148 |
assert payload["argv"][4:] == ["token", "", sdk_value]
|
| 149 |
|
| 150 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
def test_upload_dataset_also_publishes_optional_eval_repo(tmp_path: Path) -> None:
|
| 152 |
repo_root = tmp_path / "repo"
|
| 153 |
(repo_root / "huggingface").mkdir(parents=True)
|
|
@@ -275,6 +395,165 @@ def test_upload_dataset_also_publishes_optional_eval_repo(tmp_path: Path) -> Non
|
|
| 275 |
]
|
| 276 |
|
| 277 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
def test_upload_dataset_uses_bundled_eval_data_by_default(tmp_path: Path) -> None:
|
| 279 |
repo_root = tmp_path / "repo"
|
| 280 |
(repo_root / "huggingface").mkdir(parents=True)
|
|
@@ -371,6 +650,111 @@ def test_upload_dataset_uses_bundled_eval_data_by_default(tmp_path: Path) -> Non
|
|
| 371 |
]
|
| 372 |
|
| 373 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
@pytest.mark.parametrize("command", ["upload-eval-dataset", "upload-evals-data"])
|
| 375 |
def test_upload_eval_dataset_only_publishes_eval_repo(tmp_path: Path, command: str) -> None:
|
| 376 |
repo_root = tmp_path / "repo"
|
|
|
|
| 148 |
assert payload["argv"][4:] == ["token", "", sdk_value]
|
| 149 |
|
| 150 |
|
| 151 |
+
def test_upload_model_publishes_branch_suite_text_and_codex_repos(tmp_path: Path) -> None:
|
| 152 |
+
repo_root = tmp_path / "repo"
|
| 153 |
+
model_root = repo_root / "core-python" / "output" / "model"
|
| 154 |
+
(repo_root / "huggingface").mkdir(parents=True)
|
| 155 |
+
(repo_root / "core-python" / "output").mkdir(parents=True)
|
| 156 |
+
(model_root / "master").mkdir(parents=True)
|
| 157 |
+
(model_root / "coder").mkdir(parents=True)
|
| 158 |
+
(repo_root / "huggingface" / "sync.sh").write_text(
|
| 159 |
+
SYNC_SCRIPT_PATH.read_text(encoding="utf-8"),
|
| 160 |
+
encoding="utf-8",
|
| 161 |
+
)
|
| 162 |
+
(repo_root / "huggingface" / "model-card.md").write_text(
|
| 163 |
+
"# Model card\n",
|
| 164 |
+
encoding="utf-8",
|
| 165 |
+
)
|
| 166 |
+
(model_root / "config.json").write_text("{}", encoding="utf-8")
|
| 167 |
+
(model_root / "master" / "config.json").write_text("{}", encoding="utf-8")
|
| 168 |
+
(model_root / "coder" / "config.json").write_text("{}", encoding="utf-8")
|
| 169 |
+
(model_root / "branch-suite.json").write_text(
|
| 170 |
+
json.dumps(
|
| 171 |
+
{
|
| 172 |
+
"branches": {
|
| 173 |
+
"master": {"output_dir": "core-python/output/model/master"},
|
| 174 |
+
"coder": {"output_dir": "core-python/output/model/coder"},
|
| 175 |
+
}
|
| 176 |
+
}
|
| 177 |
+
),
|
| 178 |
+
encoding="utf-8",
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
fake_bin = tmp_path / "bin"
|
| 182 |
+
fake_bin.mkdir()
|
| 183 |
+
log_path = tmp_path / "python-log.jsonl"
|
| 184 |
+
python_wrapper = fake_bin / "python3"
|
| 185 |
+
python_wrapper.write_text(
|
| 186 |
+
"\n".join(
|
| 187 |
+
[
|
| 188 |
+
f"#!{sys.executable}",
|
| 189 |
+
"import json, os, sys",
|
| 190 |
+
"log_path = os.environ['HF_TEST_LOG']",
|
| 191 |
+
"entry = {'argv': sys.argv[1:], 'stdin': sys.stdin.read()}",
|
| 192 |
+
"with open(log_path, 'a', encoding='utf-8') as handle:",
|
| 193 |
+
" handle.write(json.dumps(entry) + '\\n')",
|
| 194 |
+
"if len(sys.argv) > 1 and sys.argv[1] == '-c':",
|
| 195 |
+
" raise SystemExit(0)",
|
| 196 |
+
"if len(sys.argv) > 1 and sys.argv[1] == '-':",
|
| 197 |
+
" if len(sys.argv) >= 6 and sys.argv[2].endswith('branch-suite.json'):",
|
| 198 |
+
" manifest_path = sys.argv[2]",
|
| 199 |
+
" repo_root = sys.argv[3]",
|
| 200 |
+
" text_repo = sys.argv[4]",
|
| 201 |
+
" codex_repo = sys.argv[5]",
|
| 202 |
+
" payload = json.load(open(manifest_path, encoding='utf-8'))",
|
| 203 |
+
" branches = payload.get('branches', {})",
|
| 204 |
+
" for branch_name, repo_id in (('master', text_repo), ('coder', codex_repo)):",
|
| 205 |
+
" output_dir = branches.get(branch_name, {}).get('output_dir', '')",
|
| 206 |
+
" if output_dir and repo_id:",
|
| 207 |
+
" print(f'{branch_name}\\t{repo_root}/{output_dir}\\t{repo_id}')",
|
| 208 |
+
" raise SystemExit(0)",
|
| 209 |
+
"raise SystemExit(f'unexpected python3 invocation: {sys.argv!r}')",
|
| 210 |
+
"",
|
| 211 |
+
]
|
| 212 |
+
),
|
| 213 |
+
encoding="utf-8",
|
| 214 |
+
)
|
| 215 |
+
python_wrapper.chmod(python_wrapper.stat().st_mode | stat.S_IEXEC)
|
| 216 |
+
|
| 217 |
+
env = os.environ.copy()
|
| 218 |
+
env.update(
|
| 219 |
+
{
|
| 220 |
+
"PATH": f"{fake_bin}:{env['PATH']}",
|
| 221 |
+
"HF_TEST_LOG": str(log_path),
|
| 222 |
+
"MARIS_REPO_TOKEN": "token",
|
| 223 |
+
"HF_MODEL_REPO": "MarisUK/maris-ai-master",
|
| 224 |
+
"HF_TEXT_MODEL_REPO": "MarisUK/maris-ai-lv",
|
| 225 |
+
"HF_CODEX_MODEL_REPO": "MarisUK/maris-ai-codex",
|
| 226 |
+
"HF_LOCAL_MODEL_DIR": str(model_root),
|
| 227 |
+
}
|
| 228 |
+
)
|
| 229 |
+
|
| 230 |
+
subprocess.run(
|
| 231 |
+
["bash", str(repo_root / "huggingface" / "sync.sh"), "upload-model"],
|
| 232 |
+
cwd=repo_root,
|
| 233 |
+
env=env,
|
| 234 |
+
check=True,
|
| 235 |
+
capture_output=True,
|
| 236 |
+
text=True,
|
| 237 |
+
)
|
| 238 |
+
|
| 239 |
+
payloads = [
|
| 240 |
+
json.loads(line)
|
| 241 |
+
for line in log_path.read_text(encoding="utf-8").splitlines()
|
| 242 |
+
if line.strip()
|
| 243 |
+
]
|
| 244 |
+
workspace_uploads = [
|
| 245 |
+
payload
|
| 246 |
+
for payload in payloads
|
| 247 |
+
if payload["argv"] and payload["argv"][0] == "-" and len(payload["argv"]) >= 5 and payload["argv"][2] == "model"
|
| 248 |
+
]
|
| 249 |
+
card_uploads = [
|
| 250 |
+
payload
|
| 251 |
+
for payload in payloads
|
| 252 |
+
if payload["argv"]
|
| 253 |
+
and payload["argv"][0] == "-"
|
| 254 |
+
and len(payload["argv"]) == 5
|
| 255 |
+
and payload["argv"][2] == str(repo_root / "huggingface" / "model-card.md")
|
| 256 |
+
]
|
| 257 |
+
|
| 258 |
+
assert [(payload["argv"][1], payload["argv"][4]) for payload in workspace_uploads] == [
|
| 259 |
+
("MarisUK/maris-ai-master", "Maris AI model sync"),
|
| 260 |
+
("MarisUK/maris-ai-lv", "Maris AI model sync (master)"),
|
| 261 |
+
("MarisUK/maris-ai-codex", "Maris AI model sync (coder)"),
|
| 262 |
+
]
|
| 263 |
+
assert all(payload["argv"][3].startswith("/tmp/") for payload in workspace_uploads)
|
| 264 |
+
assert [(payload["argv"][1], payload["argv"][3], payload["argv"][4]) for payload in card_uploads] == [
|
| 265 |
+
("MarisUK/maris-ai-master", "Maris AI model sync", "token"),
|
| 266 |
+
("MarisUK/maris-ai-lv", "Maris AI model sync (master)", "token"),
|
| 267 |
+
("MarisUK/maris-ai-codex", "Maris AI model sync (coder)", "token"),
|
| 268 |
+
]
|
| 269 |
+
|
| 270 |
+
|
| 271 |
def test_upload_dataset_also_publishes_optional_eval_repo(tmp_path: Path) -> None:
|
| 272 |
repo_root = tmp_path / "repo"
|
| 273 |
(repo_root / "huggingface").mkdir(parents=True)
|
|
|
|
| 395 |
]
|
| 396 |
|
| 397 |
|
| 398 |
+
def test_upload_dataset_can_publish_global_memory_eval_and_benchmark_repos(
|
| 399 |
+
tmp_path: Path,
|
| 400 |
+
) -> None:
|
| 401 |
+
repo_root = tmp_path / "repo"
|
| 402 |
+
(repo_root / "huggingface").mkdir(parents=True)
|
| 403 |
+
(repo_root / "core-python" / "scripts").mkdir(parents=True)
|
| 404 |
+
for dataset_dir in ("data", "eval-data", "benchmark-data"):
|
| 405 |
+
(repo_root / dataset_dir / "conversation").mkdir(parents=True)
|
| 406 |
+
(repo_root / "huggingface" / "sync.sh").write_text(
|
| 407 |
+
SYNC_SCRIPT_PATH.read_text(encoding="utf-8"),
|
| 408 |
+
encoding="utf-8",
|
| 409 |
+
)
|
| 410 |
+
(repo_root / "huggingface" / "dataset-card.md").write_text(
|
| 411 |
+
"# Dataset card\n",
|
| 412 |
+
encoding="utf-8",
|
| 413 |
+
)
|
| 414 |
+
(repo_root / "huggingface" / "global-memory-dataset-card.md").write_text(
|
| 415 |
+
"# Global memory dataset card\n",
|
| 416 |
+
encoding="utf-8",
|
| 417 |
+
)
|
| 418 |
+
(repo_root / "huggingface" / "eval-dataset-card.md").write_text(
|
| 419 |
+
"# Eval dataset card\n",
|
| 420 |
+
encoding="utf-8",
|
| 421 |
+
)
|
| 422 |
+
(repo_root / "huggingface" / "benchmark-dataset-card.md").write_text(
|
| 423 |
+
"# Benchmark dataset card\n",
|
| 424 |
+
encoding="utf-8",
|
| 425 |
+
)
|
| 426 |
+
(repo_root / "core-python" / "scripts" / "validate_datasets.py").write_text(
|
| 427 |
+
"print('ok')\n",
|
| 428 |
+
encoding="utf-8",
|
| 429 |
+
)
|
| 430 |
+
(repo_root / "data" / "conversation" / "sample.jsonl").write_text(
|
| 431 |
+
'{"timestamp":"2026-04-06T00:02:00Z","type":"conversation","session_id":"main-1","user":"u","assistant":"a","language":"lv","source":"test"}\n',
|
| 432 |
+
encoding="utf-8",
|
| 433 |
+
)
|
| 434 |
+
eval_record = (
|
| 435 |
+
'{"timestamp":"2026-04-06T00:03:00Z","type":"conversation","session_id":"eval-1",'
|
| 436 |
+
'"user":"u","assistant":"a","language":"lv","source":"test","task_id":"eval-1",'
|
| 437 |
+
'"benchmark_version":"maris-evals-v1","suite":"sanity","difficulty":"easy",'
|
| 438 |
+
'"evaluation_mode":"reference-review","risk_level":"low","expected_behavior":["ok"],'
|
| 439 |
+
'"scoring_hints":["ok"],"reference_answer":"a","acceptance_criteria":["ok"]}\n'
|
| 440 |
+
)
|
| 441 |
+
(repo_root / "eval-data" / "conversation" / "sample.jsonl").write_text(
|
| 442 |
+
eval_record,
|
| 443 |
+
encoding="utf-8",
|
| 444 |
+
)
|
| 445 |
+
benchmark_record = (
|
| 446 |
+
'{"timestamp":"2026-04-06T00:04:00Z","type":"conversation","session_id":"bench-1",'
|
| 447 |
+
'"user":"u","assistant":"a","language":"en","source":"test","task_id":"bench-1",'
|
| 448 |
+
'"benchmark_version":"maris-benchmark-v1","suite":"release","difficulty":"medium",'
|
| 449 |
+
'"evaluation_mode":"reference-review","risk_level":"high","expected_behavior":["ok"],'
|
| 450 |
+
'"scoring_hints":["ok"],"reference_answer":"a","acceptance_criteria":["ok"]}\n'
|
| 451 |
+
)
|
| 452 |
+
(repo_root / "benchmark-data" / "conversation" / "sample.jsonl").write_text(
|
| 453 |
+
benchmark_record,
|
| 454 |
+
encoding="utf-8",
|
| 455 |
+
)
|
| 456 |
+
|
| 457 |
+
fake_bin = tmp_path / "bin"
|
| 458 |
+
fake_bin.mkdir()
|
| 459 |
+
log_path = tmp_path / "python-log.jsonl"
|
| 460 |
+
python_wrapper = fake_bin / "python3"
|
| 461 |
+
python_wrapper.write_text(
|
| 462 |
+
"\n".join(
|
| 463 |
+
[
|
| 464 |
+
f"#!{sys.executable}",
|
| 465 |
+
"import json, os, sys",
|
| 466 |
+
"log_path = os.environ['HF_TEST_LOG']",
|
| 467 |
+
"entry = {'argv': sys.argv[1:], 'stdin': sys.stdin.read()}",
|
| 468 |
+
"with open(log_path, 'a', encoding='utf-8') as handle:",
|
| 469 |
+
" handle.write(json.dumps(entry) + '\\n')",
|
| 470 |
+
"if len(sys.argv) > 1 and sys.argv[1] == '-c':",
|
| 471 |
+
" raise SystemExit(0)",
|
| 472 |
+
"if len(sys.argv) > 1 and sys.argv[1].endswith('validate_datasets.py'):",
|
| 473 |
+
" raise SystemExit(0)",
|
| 474 |
+
"if len(sys.argv) > 1 and sys.argv[1] == '-':",
|
| 475 |
+
" raise SystemExit(0)",
|
| 476 |
+
"raise SystemExit(f'unexpected python3 invocation: {sys.argv!r}')",
|
| 477 |
+
"",
|
| 478 |
+
]
|
| 479 |
+
),
|
| 480 |
+
encoding="utf-8",
|
| 481 |
+
)
|
| 482 |
+
python_wrapper.chmod(python_wrapper.stat().st_mode | stat.S_IEXEC)
|
| 483 |
+
|
| 484 |
+
env = os.environ.copy()
|
| 485 |
+
env.update(
|
| 486 |
+
{
|
| 487 |
+
"PATH": f"{fake_bin}:{env['PATH']}",
|
| 488 |
+
"HF_TEST_LOG": str(log_path),
|
| 489 |
+
"MARIS_REPO_TOKEN": "token",
|
| 490 |
+
"HF_DATASET_REPO": "MarisUK/maris-ai-lv-memory",
|
| 491 |
+
"HF_GLOBAL_MEMORY_REPO": "MarisUK/maris-ai-memory",
|
| 492 |
+
"HF_EVAL_DATASET_REPO": "MarisUK/maris-ai-evals",
|
| 493 |
+
"HF_BENCHMARK_DATASET_REPO": "MarisUK/maris-ai-benchmark",
|
| 494 |
+
"HF_LOCAL_DATASET_DIR": str(repo_root / "data"),
|
| 495 |
+
"HF_LOCAL_GLOBAL_DATASET_DIR": str(repo_root / "data"),
|
| 496 |
+
"HF_LOCAL_EVAL_DATASET_DIR": str(repo_root / "eval-data"),
|
| 497 |
+
"HF_LOCAL_BENCHMARK_DATASET_DIR": str(repo_root / "benchmark-data"),
|
| 498 |
+
}
|
| 499 |
+
)
|
| 500 |
+
|
| 501 |
+
subprocess.run(
|
| 502 |
+
["bash", str(repo_root / "huggingface" / "sync.sh"), "upload-dataset"],
|
| 503 |
+
cwd=repo_root,
|
| 504 |
+
env=env,
|
| 505 |
+
check=True,
|
| 506 |
+
capture_output=True,
|
| 507 |
+
text=True,
|
| 508 |
+
)
|
| 509 |
+
|
| 510 |
+
payloads = [
|
| 511 |
+
json.loads(line)
|
| 512 |
+
for line in log_path.read_text(encoding="utf-8").splitlines()
|
| 513 |
+
if line.strip()
|
| 514 |
+
]
|
| 515 |
+
upload_payloads = [
|
| 516 |
+
payload for payload in payloads if payload["argv"] and payload["argv"][0] == "-"
|
| 517 |
+
]
|
| 518 |
+
validate_payloads = [
|
| 519 |
+
payload
|
| 520 |
+
for payload in payloads
|
| 521 |
+
if payload["argv"] and payload["argv"][0].endswith("validate_datasets.py")
|
| 522 |
+
]
|
| 523 |
+
|
| 524 |
+
assert [payload["argv"][1:] for payload in validate_payloads] == [
|
| 525 |
+
["--profile", "bootstrap", str(repo_root / "data")],
|
| 526 |
+
["--profile", "bootstrap", str(repo_root / "data")],
|
| 527 |
+
["--profile", "eval", str(repo_root / "eval-data")],
|
| 528 |
+
["--profile", "eval", str(repo_root / "benchmark-data")],
|
| 529 |
+
]
|
| 530 |
+
assert len(upload_payloads) == 8
|
| 531 |
+
assert upload_payloads[2]["argv"][1:5] == [
|
| 532 |
+
"MarisUK/maris-ai-memory",
|
| 533 |
+
"dataset",
|
| 534 |
+
upload_payloads[2]["argv"][3],
|
| 535 |
+
"Maris AI global memory dataset sync",
|
| 536 |
+
]
|
| 537 |
+
assert upload_payloads[3]["argv"][1:5] == [
|
| 538 |
+
"MarisUK/maris-ai-memory",
|
| 539 |
+
str(repo_root / "huggingface" / "global-memory-dataset-card.md"),
|
| 540 |
+
"Maris AI global memory dataset sync",
|
| 541 |
+
"token",
|
| 542 |
+
]
|
| 543 |
+
assert upload_payloads[6]["argv"][1:5] == [
|
| 544 |
+
"MarisUK/maris-ai-benchmark",
|
| 545 |
+
"dataset",
|
| 546 |
+
upload_payloads[6]["argv"][3],
|
| 547 |
+
"Maris AI benchmark dataset sync",
|
| 548 |
+
]
|
| 549 |
+
assert upload_payloads[7]["argv"][1:5] == [
|
| 550 |
+
"MarisUK/maris-ai-benchmark",
|
| 551 |
+
str(repo_root / "huggingface" / "benchmark-dataset-card.md"),
|
| 552 |
+
"Maris AI benchmark dataset sync",
|
| 553 |
+
"token",
|
| 554 |
+
]
|
| 555 |
+
|
| 556 |
+
|
| 557 |
def test_upload_dataset_uses_bundled_eval_data_by_default(tmp_path: Path) -> None:
|
| 558 |
repo_root = tmp_path / "repo"
|
| 559 |
(repo_root / "huggingface").mkdir(parents=True)
|
|
|
|
| 650 |
]
|
| 651 |
|
| 652 |
|
| 653 |
+
@pytest.mark.parametrize("command", ["upload-benchmark-dataset", "upload-benchmark-data"])
|
| 654 |
+
def test_upload_benchmark_dataset_only_publishes_benchmark_repo(
|
| 655 |
+
tmp_path: Path, command: str
|
| 656 |
+
) -> None:
|
| 657 |
+
repo_root = tmp_path / "repo"
|
| 658 |
+
(repo_root / "huggingface").mkdir(parents=True)
|
| 659 |
+
(repo_root / "core-python" / "scripts").mkdir(parents=True)
|
| 660 |
+
(repo_root / "benchmark-data" / "conversation").mkdir(parents=True)
|
| 661 |
+
(repo_root / "huggingface" / "sync.sh").write_text(
|
| 662 |
+
SYNC_SCRIPT_PATH.read_text(encoding="utf-8"),
|
| 663 |
+
encoding="utf-8",
|
| 664 |
+
)
|
| 665 |
+
(repo_root / "huggingface" / "benchmark-dataset-card.md").write_text(
|
| 666 |
+
"# Benchmark dataset card\n",
|
| 667 |
+
encoding="utf-8",
|
| 668 |
+
)
|
| 669 |
+
(repo_root / "core-python" / "scripts" / "validate_datasets.py").write_text(
|
| 670 |
+
"print('ok')\n",
|
| 671 |
+
encoding="utf-8",
|
| 672 |
+
)
|
| 673 |
+
(repo_root / "benchmark-data" / "conversation" / "sample.jsonl").write_text(
|
| 674 |
+
'{"timestamp":"2026-04-06T00:04:00Z","type":"conversation","session_id":"bench-1","user":"u","assistant":"a","language":"en","source":"test","task_id":"bench-1","benchmark_version":"maris-benchmark-v1","suite":"release","difficulty":"medium","evaluation_mode":"reference-review","risk_level":"high","expected_behavior":["ok"],"scoring_hints":["ok"],"reference_answer":"a","acceptance_criteria":["ok"]}\n',
|
| 675 |
+
encoding="utf-8",
|
| 676 |
+
)
|
| 677 |
+
|
| 678 |
+
fake_bin = tmp_path / "bin"
|
| 679 |
+
fake_bin.mkdir()
|
| 680 |
+
log_path = tmp_path / "python-log.jsonl"
|
| 681 |
+
python_wrapper = fake_bin / "python3"
|
| 682 |
+
python_wrapper.write_text(
|
| 683 |
+
"\n".join(
|
| 684 |
+
[
|
| 685 |
+
f"#!{sys.executable}",
|
| 686 |
+
"import json, os, sys",
|
| 687 |
+
"log_path = os.environ['HF_TEST_LOG']",
|
| 688 |
+
"entry = {'argv': sys.argv[1:], 'stdin': sys.stdin.read()}",
|
| 689 |
+
"with open(log_path, 'a', encoding='utf-8') as handle:",
|
| 690 |
+
" handle.write(json.dumps(entry) + '\\n')",
|
| 691 |
+
"if len(sys.argv) > 1 and sys.argv[1] == '-c':",
|
| 692 |
+
" raise SystemExit(0)",
|
| 693 |
+
"if len(sys.argv) > 1 and sys.argv[1].endswith('validate_datasets.py'):",
|
| 694 |
+
" raise SystemExit(0)",
|
| 695 |
+
"if len(sys.argv) > 1 and sys.argv[1] == '-':",
|
| 696 |
+
" raise SystemExit(0)",
|
| 697 |
+
"raise SystemExit(f'unexpected python3 invocation: {sys.argv!r}')",
|
| 698 |
+
"",
|
| 699 |
+
]
|
| 700 |
+
),
|
| 701 |
+
encoding="utf-8",
|
| 702 |
+
)
|
| 703 |
+
python_wrapper.chmod(python_wrapper.stat().st_mode | stat.S_IEXEC)
|
| 704 |
+
|
| 705 |
+
env = os.environ.copy()
|
| 706 |
+
env.update(
|
| 707 |
+
{
|
| 708 |
+
"PATH": f"{fake_bin}:{env['PATH']}",
|
| 709 |
+
"HF_TEST_LOG": str(log_path),
|
| 710 |
+
"MARIS_REPO_TOKEN": "token",
|
| 711 |
+
"HF_DATASET_REPO": "MarisUK/maris-ai-lv-memory",
|
| 712 |
+
"HF_BENCHMARK_DATASET_REPO": "MarisUK/maris-ai-benchmark",
|
| 713 |
+
"HF_LOCAL_BENCHMARK_DATASET_DIR": str(repo_root / "benchmark-data"),
|
| 714 |
+
}
|
| 715 |
+
)
|
| 716 |
+
|
| 717 |
+
subprocess.run(
|
| 718 |
+
["bash", str(repo_root / "huggingface" / "sync.sh"), command],
|
| 719 |
+
cwd=repo_root,
|
| 720 |
+
env=env,
|
| 721 |
+
check=True,
|
| 722 |
+
capture_output=True,
|
| 723 |
+
text=True,
|
| 724 |
+
)
|
| 725 |
+
|
| 726 |
+
payloads = [
|
| 727 |
+
json.loads(line)
|
| 728 |
+
for line in log_path.read_text(encoding="utf-8").splitlines()
|
| 729 |
+
if line.strip()
|
| 730 |
+
]
|
| 731 |
+
upload_payloads = [
|
| 732 |
+
payload for payload in payloads if payload["argv"] and payload["argv"][0] == "-"
|
| 733 |
+
]
|
| 734 |
+
validate_payloads = [
|
| 735 |
+
payload
|
| 736 |
+
for payload in payloads
|
| 737 |
+
if payload["argv"] and payload["argv"][0].endswith("validate_datasets.py")
|
| 738 |
+
]
|
| 739 |
+
|
| 740 |
+
assert [payload["argv"][1:] for payload in validate_payloads] == [
|
| 741 |
+
["--profile", "eval", str(repo_root / "benchmark-data")]
|
| 742 |
+
]
|
| 743 |
+
assert len(upload_payloads) == 2
|
| 744 |
+
assert upload_payloads[0]["argv"][1:5] == [
|
| 745 |
+
"MarisUK/maris-ai-benchmark",
|
| 746 |
+
"dataset",
|
| 747 |
+
upload_payloads[0]["argv"][3],
|
| 748 |
+
"Maris AI benchmark dataset sync",
|
| 749 |
+
]
|
| 750 |
+
assert upload_payloads[1]["argv"][1:5] == [
|
| 751 |
+
"MarisUK/maris-ai-benchmark",
|
| 752 |
+
str(repo_root / "huggingface" / "benchmark-dataset-card.md"),
|
| 753 |
+
"Maris AI benchmark dataset sync",
|
| 754 |
+
"token",
|
| 755 |
+
]
|
| 756 |
+
|
| 757 |
+
|
| 758 |
@pytest.mark.parametrize("command", ["upload-eval-dataset", "upload-evals-data"])
|
| 759 |
def test_upload_eval_dataset_only_publishes_eval_repo(tmp_path: Path, command: str) -> None:
|
| 760 |
repo_root = tmp_path / "repo"
|
core-python/tests/test_human_training.py
CHANGED
|
@@ -39,8 +39,14 @@ def test_stage_human_training_artifacts_builds_staging_manifest(tmp_path: Path)
|
|
| 39 |
profile_preferences=["Atbildi latviski."],
|
| 40 |
response_instructions=["Ja iespējams, dod strukturētu kopsavilkumu."],
|
| 41 |
conversation_examples=[
|
| 42 |
-
{
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
],
|
| 45 |
preference_pairs=[
|
| 46 |
{
|
|
|
|
| 39 |
profile_preferences=["Atbildi latviski."],
|
| 40 |
response_instructions=["Ja iespējams, dod strukturētu kopsavilkumu."],
|
| 41 |
conversation_examples=[
|
| 42 |
+
{
|
| 43 |
+
"user": "Kas ir mana valodas preference?",
|
| 44 |
+
"assistant": "Tu dod priekšroku latviešu valodai.",
|
| 45 |
+
},
|
| 46 |
+
{
|
| 47 |
+
"user": "Kas ir mana valodas preference?",
|
| 48 |
+
"assistant": "Tu dod priekšroku latviešu valodai.",
|
| 49 |
+
},
|
| 50 |
],
|
| 51 |
preference_pairs=[
|
| 52 |
{
|
core-python/tests/test_space_agent.py
CHANGED
|
@@ -236,7 +236,9 @@ def test_execute_space_agent_tool_returns_model_dataset_playbook() -> None:
|
|
| 236 |
assert "latest_agent_principles" in result
|
| 237 |
assert "recommended_loop" in result
|
| 238 |
assert "validate_dataset" in result["repo_commands"]
|
| 239 |
-
assert any(
|
|
|
|
|
|
|
| 240 |
|
| 241 |
|
| 242 |
def test_execute_space_agent_tool_returns_browser_capabilities() -> None:
|
|
@@ -253,7 +255,9 @@ def test_execute_space_agent_tool_returns_workspace_command_catalog() -> None:
|
|
| 253 |
|
| 254 |
assert result["presets"]
|
| 255 |
assert any(group["category"] == "python" for group in result["presets"])
|
| 256 |
-
assert any(
|
|
|
|
|
|
|
| 257 |
|
| 258 |
|
| 259 |
def test_execute_space_agent_tool_returns_persona_catalog() -> None:
|
|
@@ -396,8 +400,10 @@ def test_execute_space_agent_tool_stages_workspace_write_when_approval_required(
|
|
| 396 |
"workspace_root": str(workspace),
|
| 397 |
"require_workspace_approval": True,
|
| 398 |
"task_mode": "code",
|
| 399 |
-
"stage_workspace_write": lambda payload:
|
| 400 |
-
|
|
|
|
|
|
|
| 401 |
},
|
| 402 |
)
|
| 403 |
|
|
@@ -427,8 +433,10 @@ def test_execute_space_agent_tool_stages_huggingface_write_when_approval_require
|
|
| 427 |
context={
|
| 428 |
"require_publish_approval": True,
|
| 429 |
"task_mode": "design",
|
| 430 |
-
"stage_hf_write": lambda payload:
|
| 431 |
-
|
|
|
|
|
|
|
| 432 |
},
|
| 433 |
)
|
| 434 |
|
|
@@ -513,8 +521,9 @@ def test_generate_space_agent_reply_supports_multi_step_model_fix(
|
|
| 513 |
tool_context={
|
| 514 |
"require_publish_approval": True,
|
| 515 |
"task_mode": "improve",
|
| 516 |
-
"stage_hf_write": lambda payload:
|
| 517 |
-
|
|
|
|
| 518 |
},
|
| 519 |
)
|
| 520 |
|
|
|
|
| 236 |
assert "latest_agent_principles" in result
|
| 237 |
assert "recommended_loop" in result
|
| 238 |
assert "validate_dataset" in result["repo_commands"]
|
| 239 |
+
assert any(
|
| 240 |
+
"HF_TOKEN" in item or "MARIS_REPO_TOKEN" in item for item in result["required_setup"]
|
| 241 |
+
)
|
| 242 |
|
| 243 |
|
| 244 |
def test_execute_space_agent_tool_returns_browser_capabilities() -> None:
|
|
|
|
| 255 |
|
| 256 |
assert result["presets"]
|
| 257 |
assert any(group["category"] == "python" for group in result["presets"])
|
| 258 |
+
assert any(
|
| 259 |
+
item["id"] == "frontend-build" for group in result["presets"] for item in group["items"]
|
| 260 |
+
)
|
| 261 |
|
| 262 |
|
| 263 |
def test_execute_space_agent_tool_returns_persona_catalog() -> None:
|
|
|
|
| 400 |
"workspace_root": str(workspace),
|
| 401 |
"require_workspace_approval": True,
|
| 402 |
"task_mode": "code",
|
| 403 |
+
"stage_workspace_write": lambda payload: (
|
| 404 |
+
staged_payloads.append(payload)
|
| 405 |
+
or {"proposal_id": "workspace-1", "status": "pending", "diff": "draft diff"}
|
| 406 |
+
),
|
| 407 |
},
|
| 408 |
)
|
| 409 |
|
|
|
|
| 433 |
context={
|
| 434 |
"require_publish_approval": True,
|
| 435 |
"task_mode": "design",
|
| 436 |
+
"stage_hf_write": lambda payload: (
|
| 437 |
+
staged_payloads.append(payload)
|
| 438 |
+
or {"proposal_id": "proposal-1", "status": "pending"}
|
| 439 |
+
),
|
| 440 |
},
|
| 441 |
)
|
| 442 |
|
|
|
|
| 521 |
tool_context={
|
| 522 |
"require_publish_approval": True,
|
| 523 |
"task_mode": "improve",
|
| 524 |
+
"stage_hf_write": lambda payload: (
|
| 525 |
+
staged.append(payload) or {"proposal_id": "approval-1", "status": "pending"}
|
| 526 |
+
),
|
| 527 |
},
|
| 528 |
)
|
| 529 |
|
core-python/tests/test_space_ui.py
CHANGED
|
@@ -205,6 +205,20 @@ def test_parse_training_progress_detects_epoch_and_loss() -> None:
|
|
| 205 |
assert progress["loss"] == 0.125
|
| 206 |
|
| 207 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
def test_parse_training_progress_reports_completion() -> None:
|
| 209 |
progress = parse_training_progress(
|
| 210 |
"Training complete\n",
|
|
|
|
| 205 |
assert progress["loss"] == 0.125
|
| 206 |
|
| 207 |
|
| 208 |
+
def test_parse_training_progress_reports_structured_preparing_stage() -> None:
|
| 209 |
+
progress = parse_training_progress(
|
| 210 |
+
'{"maris_training_event": true, "event": "prepare_model", "stage": "preparing", "label": "Ielādē tokenizeri un modeli"}\n',
|
| 211 |
+
request={"num_epochs": 3},
|
| 212 |
+
running=True,
|
| 213 |
+
exit_code=None,
|
| 214 |
+
)
|
| 215 |
+
|
| 216 |
+
assert progress["stage"] == "preparing"
|
| 217 |
+
assert progress["label"] == "Ielādē tokenizeri un modeli"
|
| 218 |
+
assert progress["percent"] == 20
|
| 219 |
+
assert progress["events_detected"] == 1
|
| 220 |
+
|
| 221 |
+
|
| 222 |
def test_parse_training_progress_reports_completion() -> None:
|
| 223 |
progress = parse_training_progress(
|
| 224 |
"Training complete\n",
|
core-python/tests/test_text.py
CHANGED
|
@@ -203,7 +203,9 @@ def test_build_pipeline_wraps_runtime_model_in_compatibility_restore() -> None:
|
|
| 203 |
|
| 204 |
with (
|
| 205 |
_reset_pipeline_runtime(),
|
| 206 |
-
patch(
|
|
|
|
|
|
|
| 207 |
patch.dict(sys.modules, {"transformers": SimpleNamespace(pipeline=fake_pipeline)}),
|
| 208 |
patch("maris_core.text.generate.maris_hf_compatible_path", fake_compat_path),
|
| 209 |
):
|
|
@@ -312,10 +314,14 @@ async def test_generate_uses_requested_hf_fallback_model_when_runtime_is_unavail
|
|
| 312 |
def __init__(self) -> None:
|
| 313 |
self.called_model: str | None = None
|
| 314 |
|
| 315 |
-
def chat_completion(
|
|
|
|
|
|
|
| 316 |
del messages, max_tokens, temperature
|
| 317 |
self.called_model = model
|
| 318 |
-
return {
|
|
|
|
|
|
|
| 319 |
|
| 320 |
fake_client = FakeClient()
|
| 321 |
fake_hf_module = SimpleNamespace(InferenceClient=FakeClient)
|
|
@@ -324,7 +330,9 @@ async def test_generate_uses_requested_hf_fallback_model_when_runtime_is_unavail
|
|
| 324 |
with (
|
| 325 |
patch("maris_core.text.generate.get_pipeline", return_value=None),
|
| 326 |
patch("maris_core.text.generate.create_hf_inference_client", return_value=fake_client),
|
| 327 |
-
patch.dict(
|
|
|
|
|
|
|
| 328 |
patch(
|
| 329 |
"maris_core.utils.hf_integration.HFIntegration.save_conversation",
|
| 330 |
new_callable=AsyncMock,
|
|
|
|
| 203 |
|
| 204 |
with (
|
| 205 |
_reset_pipeline_runtime(),
|
| 206 |
+
patch(
|
| 207 |
+
"maris_core.text.generate.resolve_text_model", return_value="custom-user/maris-runtime"
|
| 208 |
+
),
|
| 209 |
patch.dict(sys.modules, {"transformers": SimpleNamespace(pipeline=fake_pipeline)}),
|
| 210 |
patch("maris_core.text.generate.maris_hf_compatible_path", fake_compat_path),
|
| 211 |
):
|
|
|
|
| 314 |
def __init__(self) -> None:
|
| 315 |
self.called_model: str | None = None
|
| 316 |
|
| 317 |
+
def chat_completion(
|
| 318 |
+
self, *, model: str, messages: list[dict[str, str]], max_tokens: int, temperature: float
|
| 319 |
+
) -> dict[str, Any]:
|
| 320 |
del messages, max_tokens, temperature
|
| 321 |
self.called_model = model
|
| 322 |
+
return {
|
| 323 |
+
"choices": [{"message": {"content": "Šī ir īsta fallback atbilde no HF modeļa."}}]
|
| 324 |
+
}
|
| 325 |
|
| 326 |
fake_client = FakeClient()
|
| 327 |
fake_hf_module = SimpleNamespace(InferenceClient=FakeClient)
|
|
|
|
| 330 |
with (
|
| 331 |
patch("maris_core.text.generate.get_pipeline", return_value=None),
|
| 332 |
patch("maris_core.text.generate.create_hf_inference_client", return_value=fake_client),
|
| 333 |
+
patch.dict(
|
| 334 |
+
sys.modules, {"huggingface_hub": fake_hf_module, "huggingface_hub.utils": fake_hf_utils}
|
| 335 |
+
),
|
| 336 |
patch(
|
| 337 |
"maris_core.utils.hf_integration.HFIntegration.save_conversation",
|
| 338 |
new_callable=AsyncMock,
|
core-python/tests/test_text_benchmark.py
CHANGED
|
@@ -580,7 +580,9 @@ def test_summarize_chat_benchmark_tracks_memory_metrics_and_trends() -> None:
|
|
| 580 |
assert history["trend_summary"]["average_latency_ms"]["latest"] == 80.0
|
| 581 |
|
| 582 |
|
| 583 |
-
def test_summarize_chat_benchmark_tracks_tool_multimodal_latency_and_hallucination_metrics() ->
|
|
|
|
|
|
|
| 584 |
results = [
|
| 585 |
ChatBenchmarkResult(
|
| 586 |
name="tooling-case",
|
|
|
|
| 580 |
assert history["trend_summary"]["average_latency_ms"]["latest"] == 80.0
|
| 581 |
|
| 582 |
|
| 583 |
+
def test_summarize_chat_benchmark_tracks_tool_multimodal_latency_and_hallucination_metrics() -> (
|
| 584 |
+
None
|
| 585 |
+
):
|
| 586 |
results = [
|
| 587 |
ChatBenchmarkResult(
|
| 588 |
name="tooling-case",
|
core-python/tests/test_training_hf_compat.py
CHANGED
|
@@ -47,9 +47,13 @@ def test_write_maris_compatibility_artifact_sanitizes_loader_fields(tmp_path: Pa
|
|
| 47 |
write_maris_compatibility_artifact(output_dir, maris_model_id="MarisUK/maris-ai-master")
|
| 48 |
apply_maris_compatibility_identity(output_dir)
|
| 49 |
|
| 50 |
-
manifest = json.loads(
|
|
|
|
|
|
|
| 51 |
config_payload = json.loads((output_dir / "config.json").read_text(encoding="utf-8"))
|
| 52 |
-
tokenizer_payload = json.loads(
|
|
|
|
|
|
|
| 53 |
adapter_payload = json.loads((output_dir / "adapter_config.json").read_text(encoding="utf-8"))
|
| 54 |
|
| 55 |
assert manifest["artifact_type"] == "maris-hf-compatibility"
|
|
@@ -97,7 +101,9 @@ def test_maris_hf_compatible_path_restores_remote_snapshot(monkeypatch, tmp_path
|
|
| 97 |
with maris_hf_compatible_path("MarisUK/maris-ai-master") as compatible_path:
|
| 98 |
restored_dir = Path(compatible_path)
|
| 99 |
assert restored_dir != snapshot_dir
|
| 100 |
-
restored_config = json.loads(
|
|
|
|
|
|
|
| 101 |
assert restored_config["model_type"] == "qwen2"
|
| 102 |
assert restored_config["architectures"] == ["Qwen2ForCausalLM"]
|
| 103 |
|
|
@@ -105,7 +111,9 @@ def test_maris_hf_compatible_path_restores_remote_snapshot(monkeypatch, tmp_path
|
|
| 105 |
assert original_config["model_type"] == "maris"
|
| 106 |
|
| 107 |
|
| 108 |
-
def test_maris_hf_compatible_path_restores_custom_runtime_snapshot(
|
|
|
|
|
|
|
| 109 |
snapshot_dir = tmp_path / "custom-runtime"
|
| 110 |
snapshot_dir.mkdir()
|
| 111 |
(snapshot_dir / "config.json").write_text(
|
|
@@ -127,7 +135,8 @@ def test_maris_hf_compatible_path_restores_custom_runtime_snapshot(monkeypatch,
|
|
| 127 |
types.SimpleNamespace(
|
| 128 |
snapshot_download=lambda **kwargs: (
|
| 129 |
str(snapshot_dir)
|
| 130 |
-
if kwargs["repo_id"] == "custom-user/maris-runtime"
|
|
|
|
| 131 |
else None
|
| 132 |
)
|
| 133 |
),
|
|
@@ -136,7 +145,9 @@ def test_maris_hf_compatible_path_restores_custom_runtime_snapshot(monkeypatch,
|
|
| 136 |
|
| 137 |
with maris_hf_compatible_path("custom-user/maris-runtime") as compatible_path:
|
| 138 |
restored_dir = Path(compatible_path)
|
| 139 |
-
restored_config = json.loads(
|
|
|
|
|
|
|
| 140 |
assert restored_config["model_type"] == "llama"
|
| 141 |
assert restored_config["architectures"] == ["LlamaForCausalLM"]
|
| 142 |
|
|
@@ -155,7 +166,8 @@ def test_maris_hf_compatible_path_returns_remote_snapshot_path_without_restore_a
|
|
| 155 |
types.SimpleNamespace(
|
| 156 |
snapshot_download=lambda **kwargs: (
|
| 157 |
str(snapshot_dir)
|
| 158 |
-
if kwargs["repo_id"] == "custom-user/plain-runtime"
|
|
|
|
| 159 |
else None
|
| 160 |
)
|
| 161 |
),
|
|
|
|
| 47 |
write_maris_compatibility_artifact(output_dir, maris_model_id="MarisUK/maris-ai-master")
|
| 48 |
apply_maris_compatibility_identity(output_dir)
|
| 49 |
|
| 50 |
+
manifest = json.loads(
|
| 51 |
+
(output_dir / MARIS_COMPATIBILITY_ARTIFACT_NAME).read_text(encoding="utf-8")
|
| 52 |
+
)
|
| 53 |
config_payload = json.loads((output_dir / "config.json").read_text(encoding="utf-8"))
|
| 54 |
+
tokenizer_payload = json.loads(
|
| 55 |
+
(output_dir / "tokenizer_config.json").read_text(encoding="utf-8")
|
| 56 |
+
)
|
| 57 |
adapter_payload = json.loads((output_dir / "adapter_config.json").read_text(encoding="utf-8"))
|
| 58 |
|
| 59 |
assert manifest["artifact_type"] == "maris-hf-compatibility"
|
|
|
|
| 101 |
with maris_hf_compatible_path("MarisUK/maris-ai-master") as compatible_path:
|
| 102 |
restored_dir = Path(compatible_path)
|
| 103 |
assert restored_dir != snapshot_dir
|
| 104 |
+
restored_config = json.loads(
|
| 105 |
+
restored_dir.joinpath("config.json").read_text(encoding="utf-8")
|
| 106 |
+
)
|
| 107 |
assert restored_config["model_type"] == "qwen2"
|
| 108 |
assert restored_config["architectures"] == ["Qwen2ForCausalLM"]
|
| 109 |
|
|
|
|
| 111 |
assert original_config["model_type"] == "maris"
|
| 112 |
|
| 113 |
|
| 114 |
+
def test_maris_hf_compatible_path_restores_custom_runtime_snapshot(
|
| 115 |
+
monkeypatch, tmp_path: Path
|
| 116 |
+
) -> None:
|
| 117 |
snapshot_dir = tmp_path / "custom-runtime"
|
| 118 |
snapshot_dir.mkdir()
|
| 119 |
(snapshot_dir / "config.json").write_text(
|
|
|
|
| 135 |
types.SimpleNamespace(
|
| 136 |
snapshot_download=lambda **kwargs: (
|
| 137 |
str(snapshot_dir)
|
| 138 |
+
if kwargs["repo_id"] == "custom-user/maris-runtime"
|
| 139 |
+
and kwargs["repo_type"] == "model"
|
| 140 |
else None
|
| 141 |
)
|
| 142 |
),
|
|
|
|
| 145 |
|
| 146 |
with maris_hf_compatible_path("custom-user/maris-runtime") as compatible_path:
|
| 147 |
restored_dir = Path(compatible_path)
|
| 148 |
+
restored_config = json.loads(
|
| 149 |
+
restored_dir.joinpath("config.json").read_text(encoding="utf-8")
|
| 150 |
+
)
|
| 151 |
assert restored_config["model_type"] == "llama"
|
| 152 |
assert restored_config["architectures"] == ["LlamaForCausalLM"]
|
| 153 |
|
|
|
|
| 166 |
types.SimpleNamespace(
|
| 167 |
snapshot_download=lambda **kwargs: (
|
| 168 |
str(snapshot_dir)
|
| 169 |
+
if kwargs["repo_id"] == "custom-user/plain-runtime"
|
| 170 |
+
and kwargs["repo_type"] == "model"
|
| 171 |
else None
|
| 172 |
)
|
| 173 |
),
|
core-python/tests/test_training_pipeline.py
CHANGED
|
@@ -292,6 +292,44 @@ def test_load_training_config_reads_optional_eval_dataset_repo(
|
|
| 292 |
assert config.eval_dataset_repo == "MarisUK/maris-ai-evals"
|
| 293 |
|
| 294 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
def test_load_training_config_reads_benchmark_and_preference_paths(
|
| 296 |
tmp_path: Path,
|
| 297 |
) -> None:
|
|
@@ -312,7 +350,7 @@ def test_load_training_config_reads_benchmark_and_preference_paths(
|
|
| 312 |
"branch_benchmark_targets": {"master": {"overall": 0.8, "reasoning": 0.78}},
|
| 313 |
"branch_benchmark_names": {
|
| 314 |
"master": "memory-quality",
|
| 315 |
-
"coder": "coder-release-quality"
|
| 316 |
},
|
| 317 |
"branch_benchmark_dataset_paths": {
|
| 318 |
"coder": "/tmp/benchmarks/coder-release.json",
|
|
@@ -388,7 +426,9 @@ def test_apply_branch_runtime_defaults_prefers_master_memory_suite() -> None:
|
|
| 388 |
resolved = train_module._apply_branch_runtime_defaults(config)
|
| 389 |
|
| 390 |
assert resolved.benchmark_name == "memory-quality"
|
| 391 |
-
assert resolved.benchmark_dataset_path.endswith(
|
|
|
|
|
|
|
| 392 |
|
| 393 |
|
| 394 |
def test_build_benchmark_gate_artifact_uses_world_class_defaults_and_blocks_regressions() -> None:
|
|
@@ -1528,6 +1568,18 @@ def test_train_prefers_existing_local_artifact_when_continue_mode_enabled(
|
|
| 1528 |
output_dir = tmp_path / "continued-model"
|
| 1529 |
output_dir.mkdir(parents=True, exist_ok=True)
|
| 1530 |
(output_dir / "config.json").write_text("{}", encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1531 |
monkeypatch.setattr(
|
| 1532 |
"maris_core.training.train.load_hf_dataset",
|
| 1533 |
lambda _: {
|
|
@@ -1612,6 +1664,36 @@ def test_train_prefers_existing_local_artifact_when_continue_mode_enabled(
|
|
| 1612 |
assert captured_paths["model"] == str(output_dir)
|
| 1613 |
|
| 1614 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1615 |
def test_train_restores_maris_artifacts_after_push_to_hub(tmp_path: Path, monkeypatch) -> None:
|
| 1616 |
class FakeDataset:
|
| 1617 |
def __init__(self, items):
|
|
@@ -1949,7 +2031,7 @@ def test_export_model_publishes_branch_suite_to_runtime_repos(tmp_path: Path, mo
|
|
| 1949 |
|
| 1950 |
suite_dir = tmp_path / "suite"
|
| 1951 |
suite_dir.mkdir()
|
| 1952 |
-
for branch_name in ("master", "image", "tts"):
|
| 1953 |
branch_dir = suite_dir / branch_name
|
| 1954 |
branch_dir.mkdir()
|
| 1955 |
branch_dir.joinpath("config.json").write_text("{}", encoding="utf-8")
|
|
@@ -1958,6 +2040,7 @@ def test_export_model_publishes_branch_suite_to_runtime_repos(tmp_path: Path, mo
|
|
| 1958 |
{
|
| 1959 |
"branches": {
|
| 1960 |
"master": {"output_dir": str(suite_dir / "master")},
|
|
|
|
| 1961 |
"image": {"output_dir": str(suite_dir / "image")},
|
| 1962 |
"tts": {"output_dir": str(suite_dir / "tts")},
|
| 1963 |
}
|
|
@@ -2002,7 +2085,7 @@ def test_export_model_publishes_branch_suite_to_runtime_repos(tmp_path: Path, mo
|
|
| 2002 |
},
|
| 2003 |
{
|
| 2004 |
"create_repo": {
|
| 2005 |
-
"repo_id": "MarisUK/maris-ai-
|
| 2006 |
"repo_type": "model",
|
| 2007 |
"exist_ok": True,
|
| 2008 |
}
|
|
@@ -2010,11 +2093,26 @@ def test_export_model_publishes_branch_suite_to_runtime_repos(tmp_path: Path, mo
|
|
| 2010 |
{
|
| 2011 |
"upload_folder": {
|
| 2012 |
"folder_path": str(suite_dir / "master"),
|
| 2013 |
-
"repo_id": "MarisUK/maris-ai-
|
| 2014 |
"repo_type": "model",
|
| 2015 |
"commit_message": "Maris AI model export (master)",
|
| 2016 |
}
|
| 2017 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2018 |
{
|
| 2019 |
"create_repo": {
|
| 2020 |
"repo_id": "MarisUK/maris-ai-image",
|
|
@@ -2628,7 +2726,9 @@ def test_train_uses_external_eval_dataset_when_configured(tmp_path: Path, monkey
|
|
| 2628 |
|
| 2629 |
metrics = train(
|
| 2630 |
output_dir=str(tmp_path / "trained-model"),
|
|
|
|
| 2631 |
eval_dataset_repo="MarisUK/maris-ai-evals",
|
|
|
|
| 2632 |
)
|
| 2633 |
|
| 2634 |
assert metrics["eval_loss"] == 0.2
|
|
@@ -2638,6 +2738,147 @@ def test_train_uses_external_eval_dataset_when_configured(tmp_path: Path, monkey
|
|
| 2638 |
assert len(FakeTrainer.last_instance.eval_dataset) == 1
|
| 2639 |
|
| 2640 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2641 |
def test_evaluate_with_config_prefers_external_eval_dataset(tmp_path: Path, monkeypatch) -> None:
|
| 2642 |
dataset_calls: list[str] = []
|
| 2643 |
trained_model_dir = tmp_path / "trained-model"
|
|
@@ -2751,6 +2992,7 @@ def test_evaluate_with_config_prefers_external_eval_dataset(tmp_path: Path, monk
|
|
| 2751 |
overrides={
|
| 2752 |
"output_dir": str(trained_model_dir),
|
| 2753 |
"eval_dataset_repo": "MarisUK/maris-ai-evals",
|
|
|
|
| 2754 |
"benchmark_dataset_path": str(tmp_path / "benchmark.json"),
|
| 2755 |
"benchmark_levels": ["ci"],
|
| 2756 |
}
|
|
|
|
| 292 |
assert config.eval_dataset_repo == "MarisUK/maris-ai-evals"
|
| 293 |
|
| 294 |
|
| 295 |
+
def test_load_training_config_reads_explicit_training_and_eval_dataset_repo_lists(
|
| 296 |
+
tmp_path: Path,
|
| 297 |
+
) -> None:
|
| 298 |
+
config_path = tmp_path / "training.json"
|
| 299 |
+
config_path.write_text(
|
| 300 |
+
json.dumps(
|
| 301 |
+
{
|
| 302 |
+
"dataset_repo": "MarisUK/maris-ai-lv-memory",
|
| 303 |
+
"dataset_repos": [
|
| 304 |
+
"MarisUK/maris-ai-memory",
|
| 305 |
+
"MarisUK/maris-ai-lv-memory",
|
| 306 |
+
"MarisUK/maris-ai-evals",
|
| 307 |
+
"MarisUK/maris-ai-benchmark",
|
| 308 |
+
],
|
| 309 |
+
"eval_dataset_repo": "MarisUK/maris-ai-evals",
|
| 310 |
+
"eval_dataset_repos": [
|
| 311 |
+
"MarisUK/maris-ai-evals",
|
| 312 |
+
"MarisUK/maris-ai-benchmark",
|
| 313 |
+
],
|
| 314 |
+
}
|
| 315 |
+
),
|
| 316 |
+
encoding="utf-8",
|
| 317 |
+
)
|
| 318 |
+
|
| 319 |
+
config = load_training_config(str(config_path))
|
| 320 |
+
|
| 321 |
+
assert config.dataset_repos == [
|
| 322 |
+
"MarisUK/maris-ai-memory",
|
| 323 |
+
"MarisUK/maris-ai-lv-memory",
|
| 324 |
+
"MarisUK/maris-ai-evals",
|
| 325 |
+
"MarisUK/maris-ai-benchmark",
|
| 326 |
+
]
|
| 327 |
+
assert config.eval_dataset_repos == [
|
| 328 |
+
"MarisUK/maris-ai-evals",
|
| 329 |
+
"MarisUK/maris-ai-benchmark",
|
| 330 |
+
]
|
| 331 |
+
|
| 332 |
+
|
| 333 |
def test_load_training_config_reads_benchmark_and_preference_paths(
|
| 334 |
tmp_path: Path,
|
| 335 |
) -> None:
|
|
|
|
| 350 |
"branch_benchmark_targets": {"master": {"overall": 0.8, "reasoning": 0.78}},
|
| 351 |
"branch_benchmark_names": {
|
| 352 |
"master": "memory-quality",
|
| 353 |
+
"coder": "coder-release-quality",
|
| 354 |
},
|
| 355 |
"branch_benchmark_dataset_paths": {
|
| 356 |
"coder": "/tmp/benchmarks/coder-release.json",
|
|
|
|
| 426 |
resolved = train_module._apply_branch_runtime_defaults(config)
|
| 427 |
|
| 428 |
assert resolved.benchmark_name == "memory-quality"
|
| 429 |
+
assert resolved.benchmark_dataset_path.endswith(
|
| 430 |
+
"core-python/evals/master_memory_benchmark.json"
|
| 431 |
+
)
|
| 432 |
|
| 433 |
|
| 434 |
def test_build_benchmark_gate_artifact_uses_world_class_defaults_and_blocks_regressions() -> None:
|
|
|
|
| 1568 |
output_dir = tmp_path / "continued-model"
|
| 1569 |
output_dir.mkdir(parents=True, exist_ok=True)
|
| 1570 |
(output_dir / "config.json").write_text("{}", encoding="utf-8")
|
| 1571 |
+
import maris_core.training.train as train_module
|
| 1572 |
+
|
| 1573 |
+
(output_dir / "training-config.json").write_text(
|
| 1574 |
+
json.dumps(
|
| 1575 |
+
{
|
| 1576 |
+
train_module.MODEL_SOURCE_FINGERPRINT_KEY: train_module._build_model_source_fingerprint(
|
| 1577 |
+
DEFAULT_TRAINING_BASE_MODEL
|
| 1578 |
+
)
|
| 1579 |
+
}
|
| 1580 |
+
),
|
| 1581 |
+
encoding="utf-8",
|
| 1582 |
+
)
|
| 1583 |
monkeypatch.setattr(
|
| 1584 |
"maris_core.training.train.load_hf_dataset",
|
| 1585 |
lambda _: {
|
|
|
|
| 1664 |
assert captured_paths["model"] == str(output_dir)
|
| 1665 |
|
| 1666 |
|
| 1667 |
+
def test_train_does_not_auto_resume_from_incompatible_output_artifact(
|
| 1668 |
+
tmp_path: Path, monkeypatch
|
| 1669 |
+
) -> None:
|
| 1670 |
+
output_dir = tmp_path / "incompatible-output"
|
| 1671 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 1672 |
+
(output_dir / "config.json").write_text("{}", encoding="utf-8")
|
| 1673 |
+
import maris_core.training.train as train_module
|
| 1674 |
+
|
| 1675 |
+
(output_dir / "training-config.json").write_text(
|
| 1676 |
+
json.dumps(
|
| 1677 |
+
{
|
| 1678 |
+
train_module.MODEL_SOURCE_FINGERPRINT_KEY: train_module._build_model_source_fingerprint(
|
| 1679 |
+
"meta-llama/Llama-3.2-3B-Instruct"
|
| 1680 |
+
)
|
| 1681 |
+
}
|
| 1682 |
+
),
|
| 1683 |
+
encoding="utf-8",
|
| 1684 |
+
)
|
| 1685 |
+
|
| 1686 |
+
config = load_training_config(
|
| 1687 |
+
overrides={
|
| 1688 |
+
"output_dir": str(output_dir),
|
| 1689 |
+
"model_name": "Qwen/Qwen2.5-1.5B-Instruct",
|
| 1690 |
+
"continue_from_latest_artifact": True,
|
| 1691 |
+
}
|
| 1692 |
+
)
|
| 1693 |
+
|
| 1694 |
+
assert train_module._resolve_training_model_source(config) == "Qwen/Qwen2.5-1.5B-Instruct"
|
| 1695 |
+
|
| 1696 |
+
|
| 1697 |
def test_train_restores_maris_artifacts_after_push_to_hub(tmp_path: Path, monkeypatch) -> None:
|
| 1698 |
class FakeDataset:
|
| 1699 |
def __init__(self, items):
|
|
|
|
| 2031 |
|
| 2032 |
suite_dir = tmp_path / "suite"
|
| 2033 |
suite_dir.mkdir()
|
| 2034 |
+
for branch_name in ("master", "coder", "image", "tts"):
|
| 2035 |
branch_dir = suite_dir / branch_name
|
| 2036 |
branch_dir.mkdir()
|
| 2037 |
branch_dir.joinpath("config.json").write_text("{}", encoding="utf-8")
|
|
|
|
| 2040 |
{
|
| 2041 |
"branches": {
|
| 2042 |
"master": {"output_dir": str(suite_dir / "master")},
|
| 2043 |
+
"coder": {"output_dir": str(suite_dir / "coder")},
|
| 2044 |
"image": {"output_dir": str(suite_dir / "image")},
|
| 2045 |
"tts": {"output_dir": str(suite_dir / "tts")},
|
| 2046 |
}
|
|
|
|
| 2085 |
},
|
| 2086 |
{
|
| 2087 |
"create_repo": {
|
| 2088 |
+
"repo_id": "MarisUK/maris-ai-lv",
|
| 2089 |
"repo_type": "model",
|
| 2090 |
"exist_ok": True,
|
| 2091 |
}
|
|
|
|
| 2093 |
{
|
| 2094 |
"upload_folder": {
|
| 2095 |
"folder_path": str(suite_dir / "master"),
|
| 2096 |
+
"repo_id": "MarisUK/maris-ai-lv",
|
| 2097 |
"repo_type": "model",
|
| 2098 |
"commit_message": "Maris AI model export (master)",
|
| 2099 |
}
|
| 2100 |
},
|
| 2101 |
+
{
|
| 2102 |
+
"create_repo": {
|
| 2103 |
+
"repo_id": "MarisUK/maris-ai-codex",
|
| 2104 |
+
"repo_type": "model",
|
| 2105 |
+
"exist_ok": True,
|
| 2106 |
+
}
|
| 2107 |
+
},
|
| 2108 |
+
{
|
| 2109 |
+
"upload_folder": {
|
| 2110 |
+
"folder_path": str(suite_dir / "coder"),
|
| 2111 |
+
"repo_id": "MarisUK/maris-ai-codex",
|
| 2112 |
+
"repo_type": "model",
|
| 2113 |
+
"commit_message": "Maris AI model export (coder)",
|
| 2114 |
+
}
|
| 2115 |
+
},
|
| 2116 |
{
|
| 2117 |
"create_repo": {
|
| 2118 |
"repo_id": "MarisUK/maris-ai-image",
|
|
|
|
| 2726 |
|
| 2727 |
metrics = train(
|
| 2728 |
output_dir=str(tmp_path / "trained-model"),
|
| 2729 |
+
dataset_repos=["MarisUK/maris-ai-lv-memory"],
|
| 2730 |
eval_dataset_repo="MarisUK/maris-ai-evals",
|
| 2731 |
+
eval_dataset_repos=["MarisUK/maris-ai-evals"],
|
| 2732 |
)
|
| 2733 |
|
| 2734 |
assert metrics["eval_loss"] == 0.2
|
|
|
|
| 2738 |
assert len(FakeTrainer.last_instance.eval_dataset) == 1
|
| 2739 |
|
| 2740 |
|
| 2741 |
+
def test_train_merges_multiple_dataset_repos_for_training_and_eval(
|
| 2742 |
+
tmp_path: Path,
|
| 2743 |
+
monkeypatch,
|
| 2744 |
+
) -> None:
|
| 2745 |
+
dataset_calls: list[str] = []
|
| 2746 |
+
|
| 2747 |
+
class FakeSplit(list):
|
| 2748 |
+
column_names = ["text"]
|
| 2749 |
+
|
| 2750 |
+
def map(self, function, **kwargs):
|
| 2751 |
+
del kwargs
|
| 2752 |
+
batch = {"text": [item["text"] for item in self]}
|
| 2753 |
+
mapped = function(batch)
|
| 2754 |
+
size = len(next(iter(mapped.values()))) if mapped else 0
|
| 2755 |
+
return FakeSplit(
|
| 2756 |
+
[{key: value[index] for key, value in mapped.items()} for index in range(size)]
|
| 2757 |
+
)
|
| 2758 |
+
|
| 2759 |
+
repo_rows = {
|
| 2760 |
+
"MarisUK/maris-ai-memory": {
|
| 2761 |
+
"train": [{"text": "memory-train"}],
|
| 2762 |
+
"validation": [{"text": "memory-val"}],
|
| 2763 |
+
},
|
| 2764 |
+
"MarisUK/maris-ai-lv-memory": {
|
| 2765 |
+
"train": [{"text": "lv-train"}],
|
| 2766 |
+
"validation": [{"text": "lv-val"}],
|
| 2767 |
+
},
|
| 2768 |
+
"MarisUK/maris-ai-evals": {
|
| 2769 |
+
"train": [{"text": "eval-train"}],
|
| 2770 |
+
"validation": [{"text": "eval-val"}],
|
| 2771 |
+
},
|
| 2772 |
+
"MarisUK/maris-ai-benchmark": {
|
| 2773 |
+
"train": [{"text": "bench-train"}],
|
| 2774 |
+
"validation": [{"text": "bench-val"}],
|
| 2775 |
+
},
|
| 2776 |
+
}
|
| 2777 |
+
|
| 2778 |
+
def fake_load_hf_dataset(repo_id: str):
|
| 2779 |
+
dataset_calls.append(repo_id)
|
| 2780 |
+
if repo_id not in repo_rows:
|
| 2781 |
+
raise AssertionError(f"Unexpected repo id: {repo_id}")
|
| 2782 |
+
payload = repo_rows[repo_id]
|
| 2783 |
+
return {split_name: FakeSplit(list(records)) for split_name, records in payload.items()}
|
| 2784 |
+
|
| 2785 |
+
class FakeTokenizer:
|
| 2786 |
+
pad_token = None
|
| 2787 |
+
eos_token = "<eos>"
|
| 2788 |
+
pad_token_id = None
|
| 2789 |
+
eos_token_id = 7
|
| 2790 |
+
|
| 2791 |
+
@classmethod
|
| 2792 |
+
def from_pretrained(cls, model_name):
|
| 2793 |
+
del model_name
|
| 2794 |
+
return cls()
|
| 2795 |
+
|
| 2796 |
+
def __call__(self, texts, **kwargs):
|
| 2797 |
+
del kwargs
|
| 2798 |
+
return {
|
| 2799 |
+
"input_ids": [[index + 1] for index, _ in enumerate(texts)],
|
| 2800 |
+
"attention_mask": [[1] for _ in texts],
|
| 2801 |
+
}
|
| 2802 |
+
|
| 2803 |
+
def save_pretrained(self, output_dir):
|
| 2804 |
+
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
| 2805 |
+
Path(output_dir, "tokenizer.json").write_text("{}", encoding="utf-8")
|
| 2806 |
+
|
| 2807 |
+
class FakeModel:
|
| 2808 |
+
config = types.SimpleNamespace(pad_token_id=None)
|
| 2809 |
+
|
| 2810 |
+
@classmethod
|
| 2811 |
+
def from_pretrained(cls, model_name):
|
| 2812 |
+
del model_name
|
| 2813 |
+
return cls()
|
| 2814 |
+
|
| 2815 |
+
class FakeTrainingArguments:
|
| 2816 |
+
def __init__(self, **kwargs):
|
| 2817 |
+
self.kwargs = kwargs
|
| 2818 |
+
|
| 2819 |
+
class FakeTrainer:
|
| 2820 |
+
last_instance = None
|
| 2821 |
+
|
| 2822 |
+
def __init__(self, *, model, args, train_dataset, eval_dataset=None, data_collator=None):
|
| 2823 |
+
del model, data_collator
|
| 2824 |
+
self.args = args
|
| 2825 |
+
self.train_dataset = train_dataset
|
| 2826 |
+
self.eval_dataset = eval_dataset
|
| 2827 |
+
FakeTrainer.last_instance = self
|
| 2828 |
+
|
| 2829 |
+
def train(self):
|
| 2830 |
+
return types.SimpleNamespace(metrics={"train_loss": 0.1})
|
| 2831 |
+
|
| 2832 |
+
def evaluate(self):
|
| 2833 |
+
return {"eval_loss": 0.2}
|
| 2834 |
+
|
| 2835 |
+
def save_model(self, output_dir):
|
| 2836 |
+
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
| 2837 |
+
Path(output_dir, "model.bin").write_text("ok", encoding="utf-8")
|
| 2838 |
+
|
| 2839 |
+
monkeypatch.setattr("maris_core.training.train.load_hf_dataset", fake_load_hf_dataset)
|
| 2840 |
+
monkeypatch.setitem(
|
| 2841 |
+
sys.modules,
|
| 2842 |
+
"transformers",
|
| 2843 |
+
types.SimpleNamespace(
|
| 2844 |
+
AutoModelForCausalLM=FakeModel,
|
| 2845 |
+
AutoTokenizer=FakeTokenizer,
|
| 2846 |
+
DataCollatorForLanguageModeling=lambda **kwargs: kwargs,
|
| 2847 |
+
Trainer=FakeTrainer,
|
| 2848 |
+
TrainingArguments=FakeTrainingArguments,
|
| 2849 |
+
),
|
| 2850 |
+
)
|
| 2851 |
+
|
| 2852 |
+
metrics = train(
|
| 2853 |
+
output_dir=str(tmp_path / "trained-model"),
|
| 2854 |
+
dataset_repo="MarisUK/maris-ai-lv-memory",
|
| 2855 |
+
dataset_repos=[
|
| 2856 |
+
"MarisUK/maris-ai-memory",
|
| 2857 |
+
"MarisUK/maris-ai-lv-memory",
|
| 2858 |
+
"MarisUK/maris-ai-evals",
|
| 2859 |
+
"MarisUK/maris-ai-benchmark",
|
| 2860 |
+
],
|
| 2861 |
+
eval_dataset_repo="MarisUK/maris-ai-evals",
|
| 2862 |
+
eval_dataset_repos=[
|
| 2863 |
+
"MarisUK/maris-ai-evals",
|
| 2864 |
+
"MarisUK/maris-ai-benchmark",
|
| 2865 |
+
],
|
| 2866 |
+
)
|
| 2867 |
+
|
| 2868 |
+
assert metrics["eval_loss"] == 0.2
|
| 2869 |
+
assert dataset_calls == [
|
| 2870 |
+
"MarisUK/maris-ai-memory",
|
| 2871 |
+
"MarisUK/maris-ai-lv-memory",
|
| 2872 |
+
"MarisUK/maris-ai-evals",
|
| 2873 |
+
"MarisUK/maris-ai-benchmark",
|
| 2874 |
+
"MarisUK/maris-ai-evals",
|
| 2875 |
+
"MarisUK/maris-ai-benchmark",
|
| 2876 |
+
]
|
| 2877 |
+
assert FakeTrainer.last_instance is not None
|
| 2878 |
+
assert len(FakeTrainer.last_instance.train_dataset) == 4
|
| 2879 |
+
assert len(FakeTrainer.last_instance.eval_dataset) == 2
|
| 2880 |
+
|
| 2881 |
+
|
| 2882 |
def test_evaluate_with_config_prefers_external_eval_dataset(tmp_path: Path, monkeypatch) -> None:
|
| 2883 |
dataset_calls: list[str] = []
|
| 2884 |
trained_model_dir = tmp_path / "trained-model"
|
|
|
|
| 2992 |
overrides={
|
| 2993 |
"output_dir": str(trained_model_dir),
|
| 2994 |
"eval_dataset_repo": "MarisUK/maris-ai-evals",
|
| 2995 |
+
"eval_dataset_repos": ["MarisUK/maris-ai-evals"],
|
| 2996 |
"benchmark_dataset_path": str(tmp_path / "benchmark.json"),
|
| 2997 |
"benchmark_levels": ["ci"],
|
| 2998 |
}
|
huggingface_human_training_space/app.py
CHANGED
|
@@ -679,7 +679,7 @@ def _auto_training_request() -> SpaceTrainingRequest:
|
|
| 679 |
"MARIS_SPACE_AUTO_TRAIN_CONTINUE_MODEL_PATH",
|
| 680 |
"MARIS_TRAIN_CONTINUE_MODEL_PATH",
|
| 681 |
"HF_TRAIN_CONTINUE_MODEL_PATH",
|
| 682 |
-
default=
|
| 683 |
).strip()
|
| 684 |
model_name = get_env_any_or_default(
|
| 685 |
"MARIS_HUMAN_TRAINING_AUTO_TRAIN_MODEL_NAME",
|
|
|
|
| 679 |
"MARIS_SPACE_AUTO_TRAIN_CONTINUE_MODEL_PATH",
|
| 680 |
"MARIS_TRAIN_CONTINUE_MODEL_PATH",
|
| 681 |
"HF_TRAIN_CONTINUE_MODEL_PATH",
|
| 682 |
+
default="",
|
| 683 |
).strip()
|
| 684 |
model_name = get_env_any_or_default(
|
| 685 |
"MARIS_HUMAN_TRAINING_AUTO_TRAIN_MODEL_NAME",
|