MarisUK commited on
Commit
befb7b2
·
verified ·
1 Parent(s): 8903258

GitHub Actions deploy 9ccdda2e1f1a3b66eb560f07afdd07994be52372

Browse files
Files changed (33) hide show
  1. core-python/.env.example +3 -0
  2. core-python/maris_core/__main__.py +1 -0
  3. core-python/maris_core/autonomous/agent.py +88 -7
  4. core-python/maris_core/autonomous/executor.py +3 -1
  5. core-python/maris_core/autonomous/planner.py +37 -8
  6. core-python/maris_core/autonomous/session_store.py +3 -1
  7. core-python/maris_core/code/generate_code.py +94 -27
  8. core-python/maris_core/data/quality.py +8 -10
  9. core-python/maris_core/data/validator.py +8 -2
  10. core-python/maris_core/orchestrator/routing.py +6 -2
  11. core-python/maris_core/space_agent.py +28 -15
  12. core-python/maris_core/text/benchmark.py +3 -8
  13. core-python/maris_core/text/evals.py +15 -5
  14. core-python/maris_core/text/generate.py +6 -2
  15. core-python/maris_core/training/config.py +62 -2
  16. core-python/maris_core/training/hf_compat.py +9 -3
  17. core-python/maris_core/training/human_training.py +19 -11
  18. core-python/maris_core/training/space_ui.py +3 -0
  19. core-python/maris_core/training/train.py +176 -15
  20. core-python/scripts/export_to_hf.py +20 -1
  21. core-python/tests/test_autonomous.py +46 -0
  22. core-python/tests/test_autonomous_runtime.py +9 -2
  23. core-python/tests/test_code.py +16 -6
  24. core-python/tests/test_huggingface_human_training_space_studio.py +3 -1
  25. core-python/tests/test_huggingface_sync.py +384 -0
  26. core-python/tests/test_human_training.py +8 -2
  27. core-python/tests/test_space_agent.py +17 -8
  28. core-python/tests/test_space_ui.py +14 -0
  29. core-python/tests/test_text.py +12 -4
  30. core-python/tests/test_text_benchmark.py +3 -1
  31. core-python/tests/test_training_hf_compat.py +19 -7
  32. core-python/tests/test_training_pipeline.py +247 -5
  33. 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("_checkpoint_keys", {
302
- (checkpoint.get("label", ""), checkpoint.get("task_id", "") or "")
303
- for checkpoint in restored.get("checkpoints", [])
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(exc.failure_class)
 
 
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 _advance_session(req.session_id)
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 for candidate in tasks if candidate["status"] == "completed" and candidate.get("result")
 
 
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
- _SEPARATOR_PATTERN = re.compile(r"(?:\s*(?:,|;|\band\b|\bun\b|\bthen\b|\bun tad\b|\b->\b)\s*)", re.IGNORECASE)
 
15
 
16
 
17
  class Planner:
@@ -37,11 +37,7 @@ class Planner:
37
  if not normalized_goal:
38
  return []
39
 
40
- chunks: list[str] = []
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 in {"browser_automation", "code_generation", "validation"},
 
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("Neizdevās pierakstīt autonomous audit trail sesijai %s: %s", session_id, exc)
 
 
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
- **({str(k): v for k, v in package_json.get("dependencies", {}).items()} if isinstance(package_json.get("dependencies"), dict) else {}),
211
- **({str(k): v for k, v in package_json.get("devDependencies", {}).items()} if isinstance(package_json.get("devDependencies"), dict) else {}),
 
 
 
 
 
 
 
 
212
  }
213
- if (repo_path / "next.config.js").exists() or (repo_path / "next.config.mjs").exists() or "next" in dependencies:
 
 
 
 
214
  return "nextjs"
215
- if "react" in dependencies or (repo_path / "src/App.tsx").exists() or (repo_path / "src/main.tsx").exists():
 
 
 
 
216
  return "react"
217
  if (repo_path / "Cargo.toml").exists() or (repo_path / "src/main.rs").exists():
218
  return "rust"
219
- if (repo_path / "pyproject.toml").exists() or (repo_path / "requirements.txt").exists() or (repo_path / "src/main.py").exists():
 
 
 
 
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": {"typescript": "5.6.3", "@types/react": "18.3.3", "@types/node": "22.7.4"},
 
 
 
 
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": "export default function RootLayout({ children }: { children: React.ReactNode }) {\n return (\n <html lang=\"en\">\n <body>{children}</body>\n </html>\n );\n}\n",
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": "<!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",
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": "[package]\nname = \"maris-rust-app\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\n",
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": "<!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",
359
  }
360
  return {
361
- "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",
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(path=path, content=file.content, absolute_path=file.absolute_path)
 
 
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(path=resolved_entrypoint, content=only_file.content)
 
 
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 = {"main.py", "src/main.py", "src/main.rs", "src/App.tsx", "app/page.tsx", "index.html"}
 
 
 
 
 
 
 
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(path=resolved_entrypoint, content=only_file.content)
 
 
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 files, (
438
- _sanitize_relative_path(str(entrypoint)) if isinstance(entrypoint, str) and entrypoint else None
439
- ), explanation
 
 
 
 
 
 
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": ["package.json", "tsconfig.json", "next.config.mjs", "app/layout.tsx", "app/page.tsx", "pages/index.tsx"],
480
- "react": ["package.json", "tsconfig.json", "vite.config.ts", "index.html", "src/main.tsx", "src/App.tsx"],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
481
  "rust": ["Cargo.toml", "src/main.rs", "src/lib.rs"],
482
- "python": ["pyproject.toml", "requirements.txt", "src/main.py", "main.py", "app/main.py"],
 
 
 
 
 
 
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(repo_path: Path | None, detected_stack: str, prompt: str = "") -> RepoContext | None:
 
 
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
- f"[FILE {relative_path}]\n{content[:_MAX_REPO_CONTEXT_CHARS].strip()}"
 
 
 
 
 
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
- completion_normalized.startswith(prompt_normalized)
266
- or prompt_normalized.startswith(completion_normalized)
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
- similarity >= max_prompt_echo_similarity
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 = [line.strip().casefold() for line in _REPEATED_SEGMENT_SPLIT_RE.split(value) if line.strip()]
 
 
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 isinstance(value, list) and bool(value) and all(_is_non_empty_string(item) for item in value)
 
 
 
 
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([f"Neatbalstīts validācijas profils '{profile}'. Atļautie: {allowed}."])
 
 
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(r"\b[\w./-]+\.(py|ts|tsx|js|jsx|rs|sql|toml|json|yaml|yml)\b", re.IGNORECASE)
 
 
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(normalized):
 
 
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
- "title": "Workspace access",
92
- "description": "Var nolasīt, labot un sagatavot teksta failu izmaiņas izolētā Maris draft darba telpā.",
93
- },
94
- {
95
- "title": "Hugging Face operator",
96
- "description": "Var pārlūkot tavus HF repozitorijus, nolasīt failus un saglabāt izmaiņas ar commit ziņām.",
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
- "title": "Browser automation",
108
- "description": "Var izskaidrot Playwright browser automation endpointus, sesiju limitus un drošos URL režīmus.",
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": ["python", "-m", "pytest", "tests/test_space_agent.py", "tests/test_huggingface_space_app.py"],
 
 
 
 
 
 
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 result if isinstance(result, dict) else {"ok": False, "error": "Nederīgs komandas rezultāts."}
 
 
 
 
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(*, repo_id: str, repo_type: str, path_in_repo: str) -> str | None:
 
 
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(term.lower() in lowered for term in case.expected_terms):
 
 
332
  instruction_following -= 0.15
333
- instruction_following_reasons.append("follow-up atbilde neparāda iepriekšējā konteksta turpinājumu")
334
- if case.category == "helpfulness" and not any(marker in lowered for marker in _CLARIFICATION_MARKERS):
 
 
 
 
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("koda kvalitātes, validācijas vai testu signāli ir par vāju")
 
 
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("pietrūkst regresiju, rollback vai verifikācijas drošības signālu")
 
 
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(_extract_inference_response_text(raw_response), messages)
 
 
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(_extract_inference_response_text(raw_response), messages)
 
 
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 = "MarisUK/maris-ai-lv-memory"
 
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(merged.get("benchmark_name", DEFAULT_BENCHMARK_NAME) or DEFAULT_BENCHMARK_NAME),
 
 
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 = existing_artifacts.get(artifact_name) if isinstance(existing_artifacts, dict) else None
 
 
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(payload: dict[str, Any], restore_fields: dict[str, Any]) -> dict[str, Any]:
 
 
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("Lejupielādē runtime modeli compatibility restore vajadzībām: %s", model_name_or_path)
 
 
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("Lai sāktu treniņu, artefakti vispirms jāpublicē dataset repozitorijā.")
 
 
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("Human training staging direktorijai jāatrodas persistent storage ietvaros.")
 
 
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 = apply_quality_gate_to_records(
266
- eval_records,
267
- split_name="eval",
268
- config=quality_config,
269
- ) if eval_records else ([], None)
 
 
 
 
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("Fakti par lietotāju:\n" + "\n".join(f"- {item}" for item in request.profile_facts))
 
 
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
- "eval_dataset_repo": config.eval_dataset_repo or config.dataset_repo,
 
 
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"- Eval dataset repo: `{config.eval_dataset_repo or config.dataset_repo}`",
 
 
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
- "eval_dataset_repo": config.eval_dataset_repo or config.dataset_repo,
 
 
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 (not benchmark_name or benchmark_name == DEFAULT_BENCHMARK_NAME):
 
 
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
- logger.info("Ielādē training datasetu: %s", config.dataset_repo)
2573
- dataset = load_hf_dataset(config.dataset_repo)
 
 
 
 
 
 
 
 
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 config.eval_dataset_repo:
2601
- logger.info("Ielādē atsevišķu eval datasetu: %s", config.eval_dataset_repo)
2602
- eval_source_dataset = load_hf_dataset(config.eval_dataset_repo)
 
 
 
 
 
 
 
 
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 = load_hf_dataset(config.eval_dataset_repo or config.dataset_repo)
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 config.eval_dataset_repo:
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": os.getenv("TEXT_MODEL", "MarisUK/maris-ai-text"),
 
 
 
 
 
 
 
 
 
 
 
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(StartRequest(session_id="persisted-session", goal="Uztaisīt plānu"))
 
 
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 = {"id": "task-1", "description": "Nosaki prioritātes", "tool": "reasoning", "depends_on": []}
 
 
 
 
 
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() -> None:
 
 
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(sys.modules, {"huggingface_hub": fake_hf_module, "huggingface_hub.utils": fake_hf_utils}),
 
 
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(tmp_path) -> None:
 
 
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 Path(response.files[0].absolute_path or "").read_text(encoding="utf-8").startswith(
248
- "<!doctype html>"
 
 
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("def main() -> None:\n print('old')\n", encoding="utf-8")
 
 
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(f"/api/human-training/build?draft_id={draft_id}", json=_sample_payload())
 
 
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
- {"user": "Kas ir mana valodas preference?", "assistant": "Tu dod priekšroku latviešu valodai."},
43
- {"user": "Kas ir mana valodas preference?", "assistant": "Tu dod priekšroku latviešu valodai."},
 
 
 
 
 
 
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("HF_TOKEN" in item or "MARIS_REPO_TOKEN" in item for item in result["required_setup"])
 
 
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(item["id"] == "frontend-build" for group in result["presets"] for item in group["items"])
 
 
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: staged_payloads.append(payload)
400
- or {"proposal_id": "workspace-1", "status": "pending", "diff": "draft diff"},
 
 
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: staged_payloads.append(payload)
431
- or {"proposal_id": "proposal-1", "status": "pending"},
 
 
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: staged.append(payload)
517
- or {"proposal_id": "approval-1", "status": "pending"},
 
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("maris_core.text.generate.resolve_text_model", return_value="custom-user/maris-runtime"),
 
 
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(self, *, model: str, messages: list[dict[str, str]], max_tokens: int, temperature: float) -> dict[str, Any]:
 
 
316
  del messages, max_tokens, temperature
317
  self.called_model = model
318
- return {"choices": [{"message": {"content": "Šī ir īsta fallback atbilde no HF modeļa."}}]}
 
 
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(sys.modules, {"huggingface_hub": fake_hf_module, "huggingface_hub.utils": fake_hf_utils}),
 
 
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() -> None:
 
 
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((output_dir / MARIS_COMPATIBILITY_ARTIFACT_NAME).read_text(encoding="utf-8"))
 
 
51
  config_payload = json.loads((output_dir / "config.json").read_text(encoding="utf-8"))
52
- tokenizer_payload = json.loads((output_dir / "tokenizer_config.json").read_text(encoding="utf-8"))
 
 
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(restored_dir.joinpath("config.json").read_text(encoding="utf-8"))
 
 
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(monkeypatch, tmp_path: Path) -> None:
 
 
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" and kwargs["repo_type"] == "model"
 
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(restored_dir.joinpath("config.json").read_text(encoding="utf-8"))
 
 
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" and kwargs["repo_type"] == "model"
 
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("core-python/evals/master_memory_benchmark.json")
 
 
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-text",
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-text",
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=output_subdir,
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",