MarisUK commited on
Commit
f6c4b6b
·
verified ·
1 Parent(s): 2d8542c

GitHub Actions deploy 58536f00fab443e850da1235c8acd1c4f10ce306

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. Dockerfile +25 -0
  2. README.md +66 -8
  3. core-python/.env.example +73 -0
  4. core-python/Dockerfile +23 -0
  5. core-python/benchmarks/chat-ci.json +25 -0
  6. core-python/benchmarks/chat-local.json +24 -0
  7. core-python/benchmarks/chat-release.json +34 -0
  8. core-python/evals/branch_dataset_filter_rules.json +92 -0
  9. core-python/evals/chat_eval_dataset.json +858 -0
  10. core-python/evals/coder_execution_benchmark.json +60 -0
  11. core-python/evals/coder_preference_dataset.json +745 -0
  12. core-python/evals/coder_release_benchmark.json +293 -0
  13. core-python/evals/master_memory_benchmark.json +121 -0
  14. core-python/evals/planner_release_benchmark.json +118 -0
  15. core-python/maris_core/__init__.py +11 -0
  16. core-python/maris_core/__main__.py +68 -0
  17. core-python/maris_core/api/__init__.py +66 -0
  18. core-python/maris_core/audio/__init__.py +1 -0
  19. core-python/maris_core/audio/generate_music.py +73 -0
  20. core-python/maris_core/audio/stt.py +78 -0
  21. core-python/maris_core/audio/tts.py +77 -0
  22. core-python/maris_core/autonomous/__init__.py +1 -0
  23. core-python/maris_core/autonomous/agent.py +861 -0
  24. core-python/maris_core/autonomous/executor.py +318 -0
  25. core-python/maris_core/autonomous/memory.py +36 -0
  26. core-python/maris_core/autonomous/planner.py +89 -0
  27. core-python/maris_core/autonomous/session_store.py +170 -0
  28. core-python/maris_core/browser/__init__.py +5 -0
  29. core-python/maris_core/browser/automation.py +461 -0
  30. core-python/maris_core/code/__init__.py +1 -0
  31. core-python/maris_core/code/execution_eval.py +380 -0
  32. core-python/maris_core/code/fix_code.py +5 -0
  33. core-python/maris_core/code/generate_code.py +689 -0
  34. core-python/maris_core/data/__init__.py +1 -0
  35. core-python/maris_core/data/augment.py +123 -0
  36. core-python/maris_core/data/datasets.py +561 -0
  37. core-python/maris_core/data/preprocessing.py +104 -0
  38. core-python/maris_core/data/quality.py +312 -0
  39. core-python/maris_core/data/scoring.py +597 -0
  40. core-python/maris_core/data/validator.py +268 -0
  41. core-python/maris_core/images/__init__.py +1 -0
  42. core-python/maris_core/images/diffusion_pipeline.py +40 -0
  43. core-python/maris_core/images/generate_image.py +73 -0
  44. core-python/maris_core/memory_context.py +644 -0
  45. core-python/maris_core/orchestrator/__init__.py +21 -0
  46. core-python/maris_core/orchestrator/api.py +55 -0
  47. core-python/maris_core/orchestrator/routing.py +560 -0
  48. core-python/maris_core/personas.py +130 -0
  49. core-python/maris_core/runtime.py +49 -0
  50. core-python/maris_core/space_agent.py +1867 -0
Dockerfile ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim-bookworm
2
+
3
+ ENV PYTHONDONTWRITEBYTECODE=1 \
4
+ PYTHONUNBUFFERED=1 \
5
+ PIP_NO_CACHE_DIR=1 \
6
+ PYTHONPATH=/workspace
7
+
8
+ WORKDIR /workspace
9
+
10
+ RUN apt-get update && apt-get install -y --no-install-recommends \
11
+ build-essential \
12
+ git \
13
+ && rm -rf /var/lib/apt/lists/*
14
+
15
+ COPY core-python/requirements.txt /workspace/core-python/requirements.txt
16
+ RUN python3 -m pip install -r /workspace/core-python/requirements.txt
17
+
18
+ COPY core-python /workspace/core-python
19
+ COPY huggingface_human_training_space /workspace/huggingface_human_training_space
20
+
21
+ RUN python3 -m pip install -e /workspace/core-python --no-deps
22
+
23
+ EXPOSE 7860
24
+
25
+ CMD ["python3", "-m", "uvicorn", "--app-dir", "/workspace", "huggingface_human_training_space.app:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,12 +1,70 @@
1
  ---
2
- title: Maris.ai.human.training
3
- emoji: 🔥
4
  colorFrom: indigo
5
- colorTo: gray
6
- sdk: gradio
7
- sdk_version: 6.13.0
8
- app_file: app.py
9
- pinned: false
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Maris AI Human Training
3
+ emoji: 🎓
4
  colorFrom: indigo
5
+ colorTo: blue
6
+ sdk: docker
7
+ app_port: 7860
8
+ license: mit
 
9
  ---
10
 
11
+ # Maris AI Human Training Space
12
+
13
+ Atsevišķs Hugging Face Space priekš `MarisUK/maris.ai.human.training` ar profesionālu sākuma lapu, login/register plūsmu, lomām un skaidru dokumentāciju.
14
+
15
+ ## Ko Space nodrošina
16
+
17
+ - atsevišķu Space no `MarisUK/maris.ai.agent`;
18
+ - starta lapu ar Maris AI logo, nosaukumu un skaidru ieeju komandai;
19
+ - `login` un `register` plūsmu;
20
+ - lomu izvēli: `owner`, `secretary`, `trainee`, `user`;
21
+ - profesionālu onboarding, workflow aprakstu, piemēru bibliotēku un dokumentācijas sadaļu;
22
+ - lokālu lietotāju glabātuvi persistent storage failā, izmantojot hashētu paroli.
23
+ - profesionālu human training builder ar atsevišķiem laukiem:
24
+ - `model_name`
25
+ - `hub_model_id`
26
+ - `continue_model_path`
27
+ - `continue_from_latest_artefact`
28
+ - `output_subdir`
29
+ - `num_epochs`
30
+ - `push_to_hub`
31
+ - `all_branches`
32
+ - staging preview, artefaktu publicēšanu dataset repozitorijā un reālu treniņa startu no šī Space.
33
+
34
+ ## Lomu nozīme
35
+
36
+ - **owner** — nosaka mērķus, riskus un gala apstiprinājumu;
37
+ - **secretary** — strukturē ievadi, dokumentāciju un komandas plūsmu;
38
+ - **trainee** — veido un pārskata mācību piemērus;
39
+ - **user** — sniedz reālus scenārijus un kvalitātes feedback.
40
+
41
+ ## Svarīgie faili bundle publicēšanai
42
+
43
+ ```text
44
+ huggingface_human_training_space/
45
+ core-python/
46
+ ```
47
+
48
+ ## Publicēšana no šī repozitorija
49
+
50
+ ```bash
51
+ MARIS_HUMAN_TRAINING_SPACE_REPO=MarisUK/maris.ai.human.training bash ./huggingface/sync.sh upload-human-training-space
52
+ ```
53
+
54
+ ## GitHub Actions
55
+
56
+ Šim Space paredzēts atsevišķs workflow:
57
+
58
+ ```text
59
+ .github/workflows/hf-human-training-space-deploy.yml
60
+ ```
61
+
62
+ ## Lai Space sāktu reāli strādāt
63
+
64
+ Nepieciešams:
65
+
66
+ 1. Hugging Face Space secrets ar publish tokenu (`HF_TOKEN` vai `MARIS_TOKEN`);
67
+ 2. persistent storage, lai saglabātu lietotājus, staging artefaktus un lokālos checkpointus;
68
+ 3. dataset repo, kur publicēt human training artefaktus;
69
+ 4. model repo, piemēram, `MarisUK/maris-ai-lv`, kur publicēt treniņa rezultātu;
70
+ 5. GPU-capable Space runtime, ja gribi reāli trenēt modeli šajā Space vidē.
core-python/.env.example ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Core Python vides mainīgie
2
+
3
+ # Maris publicēšana un treniņš
4
+ MARIS_REPO_TOKEN=your_maris_repo_token_here
5
+ # Neobligāti Hugging Face standarta token aliasi privātiem/custom repo Railway vidē
6
+ # HF_TOKEN=your_huggingface_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
13
+ MARIS_AGENT_MODEL=MarisUK/maris-ai-master
14
+ # Papildu aģenta fallback modeļi, kurus Space var izmantot, ja primārais modelis neatbild
15
+ MARIS_AGENT_FALLBACK_MODELS=Qwen/Qwen3-Coder-480B-A35B-Instruct
16
+ # Neobligāti UI dropdown modeļi aģentam
17
+ # MARIS_AGENT_MODELS=Qwen/Qwen3-Coder-480B-A35B-Instruct,Qwen/Qwen3-880B-Instruct
18
+ MARIS_TRAIN_CONFIG_PATH=../huggingface/training-config.json
19
+ MARIS_TRAIN_MODEL_PRESET=balanced
20
+ # Neobligāti papildu training preset'i lieliem ārējiem modeļiem (JSON formātā vai owner/name sarakstā)
21
+ # MARIS_TRAIN_EXTRA_MODELS={"qwen-880b":{"model_name":"Qwen/Qwen3-880B-Instruct","label":"Qwen ultra preset","description":"Liels ārējs preset 880B klases modeļiem."}}
22
+ # MARIS_TRAIN_EXTRA_MODELS=Qwen/Qwen3-Coder-480B-A35B-Instruct,coder-7b=Qwen/Qwen2.5-7B-Instruct
23
+ MARIS_TRAIN_OUTPUT_DIR=./output/model
24
+ MARIS_TRAIN_PUBLISH=false
25
+ MARIS_TRAIN_SCORING_ENABLED=true
26
+ MARIS_TRAIN_WEIGHTED_REPETITION_ENABLED=true
27
+ MARIS_TRAIN_MEDIUM_SCORE_REPEAT_COUNT=2
28
+ MARIS_TRAIN_HIGH_SCORE_REPEAT_COUNT=3
29
+ MARIS_TRAIN_SOURCE_WEIGHTING_ENABLED=true
30
+ # MARIS_TRAIN_SOURCE_WEIGHT_MAP={"production":1.3,"synthetic":1.0,"noisy":0.65,"unknown":1.0}
31
+ MARIS_TRAIN_MAX_EFFECTIVE_REPEAT_COUNT=6
32
+ MARIS_TRAIN_BENCHMARK_FEEDBACK_ENABLED=true
33
+ MARIS_TRAIN_BENCHMARK_FEEDBACK_AUTO_DISCOVER=true
34
+ # MARIS_TRAIN_BENCHMARK_FEEDBACK_PATH=./output/previous-run/benchmark-feedback.json
35
+ MARIS_TRAIN_BENCHMARK_FEEDBACK_BOOST_SCALE=2.0
36
+ MARIS_TRAIN_BENCHMARK_FEEDBACK_MAX_MULTIPLIER=1.75
37
+ # Persistent treniņiem: turpini no pēdējā lokālā artefakta
38
+ # MARIS_TRAIN_CONTINUE_FROM_LATEST=true
39
+ # MARIS_TRAIN_CONTINUE_MODEL_PATH=./output/model
40
+
41
+ # Specialist modeļi (norādi tikai Maris AI modeļus; pārraksti tikai, ja vajag citu repo)
42
+ # Galvenais frontend čata /chat modelis
43
+ TEXT_MODEL=MarisUK/maris-ai-text
44
+ # Railway/runtime override galvenajam čatam ar jebkuru Hugging Face modeli
45
+ # MARIS_RUNTIME_TEXT_MODEL=Qwen/Qwen2.5-7B-Instruct
46
+ IMAGE_MODEL=MarisUK/maris-ai-image
47
+ MUSIC_MODEL=MarisUK/maris-ai-music
48
+ TTS_MODEL=MarisUK/maris-tts-runtime
49
+ STT_MODEL=MarisUK/maris-stt-runtime
50
+ VIDEO_MODEL=MarisUK/maris-ai-video
51
+ # Railway runtime tagad pieņem arī jebkuru citu owner/name Hugging Face repo visiem augstāk minētajiem modeļiem
52
+ # IMAGE_MODEL=stabilityai/stable-diffusion-2-1
53
+ # MUSIC_MODEL=facebook/musicgen-small
54
+ # TTS_MODEL=microsoft/speecht5_tts
55
+ # STT_MODEL=openai/whisper-small
56
+ # VIDEO_MODEL=THUDM/CogVideoX-2b
57
+
58
+ # LiveKit realtime voice assistant
59
+ LIVEKIT_URL=wss://your-livekit-host
60
+ LIVEKIT_API_KEY=your_livekit_key
61
+ LIVEKIT_API_SECRET=your_livekit_secret
62
+ MARIS_LIVEKIT_ROOM_PREFIX=maris-voice
63
+ MARIS_LIVEKIT_AGENT_NAME=maris-livekit-agent
64
+ MARIS_VOICE_STT_PROVIDER=speechmatics
65
+ MARIS_VOICE_LLM_PROVIDER=maris-stream
66
+ MARIS_VOICE_TTS_PROVIDER=azure
67
+ MARIS_VOICE_VAD_PROVIDER=silero
68
+ MARIS_VOICE_ALLOW_BARGE_IN=true
69
+
70
+ # Servera konfigurācija
71
+ PORT=8000
72
+ LOG_LEVEL=INFO
73
+ HF_HUB_DISABLE_XET=1
core-python/Dockerfile ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim-bookworm
2
+
3
+ WORKDIR /app
4
+
5
+ ENV HF_HUB_DISABLE_XET=1
6
+
7
+ # System dependencies
8
+ RUN apt-get update && apt-get install -y --no-install-recommends \
9
+ build-essential \
10
+ ffmpeg \
11
+ libsndfile1 \
12
+ && rm -rf /var/lib/apt/lists/*
13
+
14
+ # Python dependencies
15
+ COPY requirements.txt .
16
+ RUN pip install --no-cache-dir -r requirements.txt
17
+
18
+ # Copy source
19
+ COPY . .
20
+ RUN pip install -e . --no-deps
21
+
22
+ EXPOSE 8000
23
+ CMD ["python", "-m", "maris_core"]
core-python/benchmarks/chat-ci.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cases": [
3
+ {
4
+ "name": "factuality-ci",
5
+ "message": "Ja nevari pārbaudīt ārēju faktu, ko tev jāsaka?",
6
+ "expected_terms": ["nevaru", "pārbaudīt"],
7
+ "reference_answer": "Man skaidri jāpasaka, ka nevaru pārbaudīt ārējo faktu dotajā kontekstā.",
8
+ "reference_facts": ["nevaru pārbaudīt", "ārējo faktu"],
9
+ "category": "factuality",
10
+ "level": "ci",
11
+ "difficulty": "standard",
12
+ "tags": ["grounding", "safety"]
13
+ },
14
+ {
15
+ "name": "coding-ci",
16
+ "message": "Uzraksti Python funkciju ar retry parametru un īsu paskaidrojumu.",
17
+ "expected_terms": ["retry"],
18
+ "expects_code": true,
19
+ "category": "coding",
20
+ "level": "ci",
21
+ "difficulty": "standard",
22
+ "tags": ["coding"]
23
+ }
24
+ ]
25
+ }
core-python/benchmarks/chat-local.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cases": [
3
+ {
4
+ "name": "lv-clarity-local",
5
+ "message": "Īsi paskaidro, kas ir incident response process.",
6
+ "expected_terms": ["incident", "process"],
7
+ "reference_facts": ["incident", "process"],
8
+ "category": "latvian_quality",
9
+ "level": "local",
10
+ "difficulty": "easy",
11
+ "tags": ["latvian", "smoke"]
12
+ },
13
+ {
14
+ "name": "reasoning-local",
15
+ "message": "Izveido 3 soļu plānu API retry loģikai.",
16
+ "expected_terms": ["retry", "plānu"],
17
+ "reference_facts": ["retry", "plānu"],
18
+ "category": "reasoning",
19
+ "level": "local",
20
+ "difficulty": "standard",
21
+ "tags": ["reasoning", "planning"]
22
+ }
23
+ ]
24
+ }
core-python/benchmarks/chat-release.json ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cases": [
3
+ {
4
+ "name": "long-context-release",
5
+ "message": "Balstoties uz iepriekšējo sarunu, nosauc nākamos divus prioritāros soļus.",
6
+ "history": [
7
+ {
8
+ "role": "user",
9
+ "content": "Mēs būvējam incident response roadmap lielai komandai."
10
+ },
11
+ {
12
+ "role": "assistant",
13
+ "content": "Tu gribi prioritizēt alerting, ownership un postmortem procesu."
14
+ }
15
+ ],
16
+ "expected_terms": ["alerting", "ownership"],
17
+ "reference_facts": ["alerting", "ownership"],
18
+ "category": "long_context",
19
+ "level": "release",
20
+ "difficulty": "hard",
21
+ "tags": ["memory", "continuity"]
22
+ },
23
+ {
24
+ "name": "helpfulness-release",
25
+ "message": "Palīdzi man izlemt, ko darīt tālāk, ja benchmark score krīt zem gate sliekšņa.",
26
+ "expected_terms": ["benchmark", "sliekšņa"],
27
+ "reference_facts": ["benchmark", "slieksnis"],
28
+ "category": "helpfulness",
29
+ "level": "release",
30
+ "difficulty": "hard",
31
+ "tags": ["release-gate", "operations"]
32
+ }
33
+ ]
34
+ }
core-python/evals/branch_dataset_filter_rules.json ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "branch_benchmark_dataset_paths": {
3
+ "master": "master_memory_benchmark.json",
4
+ "coder": "coder_release_benchmark.json",
5
+ "planner": "planner_release_benchmark.json"
6
+ },
7
+ "branch_benchmark_names": {
8
+ "master": "memory-quality",
9
+ "coder": "coder-release-quality",
10
+ "planner": "planner-release-quality"
11
+ },
12
+ "branch_preference_dataset_paths": {
13
+ "coder": "coder_preference_dataset.json"
14
+ },
15
+ "branch_dataset_filter_rules": {
16
+ "master": {
17
+ "include_explicit_branches": ["master"],
18
+ "exclude_explicit_branches": ["coder", "planner"],
19
+ "allow_unlabeled": true
20
+ },
21
+ "coder": {
22
+ "include_explicit_branches": ["coder"],
23
+ "exclude_explicit_branches": ["planner"],
24
+ "include_record_types": ["code"],
25
+ "include_presence_keys": [
26
+ "target_file",
27
+ "buggy_code",
28
+ "tests",
29
+ "edge_cases",
30
+ "acceptance_criteria",
31
+ "diff",
32
+ "repo_context"
33
+ ],
34
+ "include_languages": [
35
+ "bash",
36
+ "go",
37
+ "javascript",
38
+ "json",
39
+ "python",
40
+ "rust",
41
+ "sql",
42
+ "toml",
43
+ "typescript",
44
+ "yaml",
45
+ "yml"
46
+ ],
47
+ "include_task_types": [
48
+ "artifact-validation",
49
+ "auth-hardening",
50
+ "bugfix",
51
+ "ci-orchestration",
52
+ "dashboard-state",
53
+ "dataset-audit",
54
+ "dataset-curation",
55
+ "dataset-sync",
56
+ "edge-cases",
57
+ "eval-analytics",
58
+ "manifest-formatting",
59
+ "refactor",
60
+ "repo-level",
61
+ "stream-normalization",
62
+ "test-writing"
63
+ ],
64
+ "include_repo_context_terms": ["backend-rust", "core-python", "frontend"]
65
+ },
66
+ "planner": {
67
+ "include_explicit_branches": ["planner"],
68
+ "exclude_explicit_branches": ["coder"],
69
+ "include_record_types": ["autonomous"],
70
+ "include_task_types": [
71
+ "autonomous",
72
+ "ci-triage",
73
+ "dataset-curation",
74
+ "eval-regression-review",
75
+ "planning",
76
+ "release-readiness",
77
+ "repo-level",
78
+ "triage"
79
+ ],
80
+ "include_repo_context_terms": [
81
+ "backend-rust",
82
+ "frontend",
83
+ "full-stack",
84
+ "github-actions",
85
+ "governance",
86
+ "operations",
87
+ "platform",
88
+ "training"
89
+ ]
90
+ }
91
+ }
92
+ }
core-python/evals/chat_eval_dataset.json ADDED
@@ -0,0 +1,858 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cases": [
3
+ {
4
+ "name": "roadmap_planning",
5
+ "message": "Izveido 3 prioritāšu plānu incident response roadmap nākamajam ceturksnim.",
6
+ "persona_id": "strategist",
7
+ "expected_terms": [
8
+ "incident",
9
+ "priorit",
10
+ "roadmap"
11
+ ],
12
+ "tags": [
13
+ "planning",
14
+ "ops"
15
+ ],
16
+ "branches": [
17
+ "master",
18
+ "planner"
19
+ ],
20
+ "level": "ci",
21
+ "category": "reasoning"
22
+ },
23
+ {
24
+ "name": "needs_clarification",
25
+ "message": "Pasaki, kā salabot to problēmu sistēmā.",
26
+ "expected_terms": [
27
+ "problēm",
28
+ "salabot"
29
+ ],
30
+ "tags": [
31
+ "clarification"
32
+ ],
33
+ "branches": [
34
+ "master"
35
+ ],
36
+ "level": "ci",
37
+ "category": "helpfulness"
38
+ },
39
+ {
40
+ "name": "grounded_latency_answer",
41
+ "message": "Mums API latence pēdējā laikā kāpj. Dod strukturētu nākamo soli.",
42
+ "persona_id": "assistant",
43
+ "expected_terms": [
44
+ "latenc",
45
+ "solis"
46
+ ],
47
+ "forbidden_terms": [
48
+ "100%"
49
+ ],
50
+ "tags": [
51
+ "latency",
52
+ "grounding"
53
+ ],
54
+ "branches": [
55
+ "master",
56
+ "planner"
57
+ ],
58
+ "level": "ci",
59
+ "category": "grounding"
60
+ },
61
+ {
62
+ "name": "coder_python_api_handler",
63
+ "message": "Uzraksti Python FastAPI endpointu, kas validē email un atgriež 400 kļūdu, ja ievade neder.",
64
+ "profile": "coder",
65
+ "expected_terms": [
66
+ "FastAPI",
67
+ "email",
68
+ "400"
69
+ ],
70
+ "reference_facts": [
71
+ "validē email",
72
+ "400 kļūdu"
73
+ ],
74
+ "tags": [
75
+ "coding",
76
+ "api",
77
+ "python"
78
+ ],
79
+ "branches": [
80
+ "coder"
81
+ ],
82
+ "level": "ci",
83
+ "difficulty": "standard",
84
+ "category": "coding",
85
+ "expects_code": true
86
+ },
87
+ {
88
+ "name": "coder_python_execution_normalize_email",
89
+ "message": "Uzraksti Python funkciju `normalize_email(email: str) -> str`, kas noņem atstarpes, normalizē lower-case un met ValueError tukšai ievadei.",
90
+ "profile": "coder",
91
+ "expected_terms": [
92
+ "normalize_email",
93
+ "ValueError"
94
+ ],
95
+ "tags": [
96
+ "coding",
97
+ "python",
98
+ "execution"
99
+ ],
100
+ "branches": [
101
+ "coder"
102
+ ],
103
+ "level": "ci",
104
+ "difficulty": "standard",
105
+ "category": "coding",
106
+ "expects_code": true,
107
+ "execution_language": "python",
108
+ "execution_test_code": "assert normalize_email(' A@Example.COM ') == 'a@example.com'\ntry:\n normalize_email(' ')\nexcept ValueError:\n pass\nelse:\n raise AssertionError('expected ValueError')"
109
+ },
110
+ {
111
+ "name": "coder_refactor_with_tests",
112
+ "message": "Parādi kā refaktorēt retry helperi TypeScript valodā un pieminēt robežgadījumus un testus.",
113
+ "profile": "coder",
114
+ "expected_terms": [
115
+ "retry",
116
+ "robež",
117
+ "test"
118
+ ],
119
+ "tags": [
120
+ "coding",
121
+ "typescript",
122
+ "quality"
123
+ ],
124
+ "branches": [
125
+ "coder"
126
+ ],
127
+ "level": "ci",
128
+ "difficulty": "hard",
129
+ "category": "coding",
130
+ "expects_code": true
131
+ },
132
+ {
133
+ "name": "coder_python_execution_parse_port",
134
+ "message": "Uzraksti Python funkciju `parse_port(raw: str) -> int`, kas atgriež porta numuru, bet met ValueError tukšai, ne-skaitliskai vai ārpus 1..65535 ievadei.",
135
+ "profile": "coder",
136
+ "expected_terms": [
137
+ "parse_port",
138
+ "ValueError"
139
+ ],
140
+ "tags": [
141
+ "coding",
142
+ "python",
143
+ "execution",
144
+ "edge-cases"
145
+ ],
146
+ "branches": [
147
+ "coder"
148
+ ],
149
+ "level": "ci",
150
+ "difficulty": "hard",
151
+ "category": "coding",
152
+ "expects_code": true,
153
+ "execution_language": "python",
154
+ "execution_test_code": "assert parse_port('8080') == 8080\nfor invalid in ('', 'abc', '0', '70000'):\n try:\n parse_port(invalid)\n except ValueError:\n pass\n else:\n raise AssertionError(f'expected ValueError for {invalid!r}')"
155
+ },
156
+ {
157
+ "name": "coder_debug_sse_repo_grounding",
158
+ "message": "Debug SSE mismatch starp backend-rust/src/api/chat.rs un frontend/app/chat/page.tsx. Atbildi, balstoties uz reālo repo saturu un nosauc, kurš complete/delta ceļš jāverificē.",
159
+ "profile": "coder",
160
+ "expected_terms": [
161
+ "complete",
162
+ "delta",
163
+ "SSE"
164
+ ],
165
+ "tags": [
166
+ "coding",
167
+ "debugging",
168
+ "grounding",
169
+ "repo"
170
+ ],
171
+ "branches": [
172
+ "coder"
173
+ ],
174
+ "level": "release",
175
+ "difficulty": "hard",
176
+ "category": "grounding",
177
+ "expects_code": false,
178
+ "min_tool_steps": 2,
179
+ "min_grounding_sources": 2,
180
+ "expected_grounding_terms": [
181
+ "backend-rust/src/api/chat.rs",
182
+ "frontend/app/chat/page.tsx"
183
+ ]
184
+ },
185
+ {
186
+ "name": "coder_partial_context_debugging",
187
+ "message": "Mums tikai daļējs konteksts: tests flako ap chat stream complete event. Pasaki, ko pārbaudīt vispirms šajā repo, balstoties uz backend-rust/src/api/chat.rs un frontend/app/chat/page.tsx.",
188
+ "profile": "coder",
189
+ "expected_terms": [
190
+ "complete",
191
+ "pārbaud",
192
+ "frontend"
193
+ ],
194
+ "tags": [
195
+ "coding",
196
+ "debugging",
197
+ "partial-context",
198
+ "grounding"
199
+ ],
200
+ "branches": [
201
+ "coder"
202
+ ],
203
+ "level": "release",
204
+ "difficulty": "hard",
205
+ "category": "grounding",
206
+ "min_tool_steps": 2,
207
+ "min_grounding_sources": 2,
208
+ "expected_grounding_terms": [
209
+ "backend-rust/src/api/chat.rs",
210
+ "frontend/app/chat/page.tsx"
211
+ ]
212
+ },
213
+ {
214
+ "name": "coder_ambiguous_spec_clarifies_repo_constraints",
215
+ "message": "Refaktorē stream parseri lielākam failam, bet spec ir neskaidrs. Sāc ar to, ko tieši vajag precizēt pēc frontend/app/chat/page.tsx un backend-rust/src/api/chat.rs satura.",
216
+ "profile": "coder",
217
+ "expected_terms": [
218
+ "preciz",
219
+ "stream",
220
+ "parser"
221
+ ],
222
+ "tags": [
223
+ "coding",
224
+ "ambiguous-spec",
225
+ "grounding",
226
+ "refactor"
227
+ ],
228
+ "branches": [
229
+ "coder"
230
+ ],
231
+ "level": "release",
232
+ "difficulty": "hard",
233
+ "category": "grounding",
234
+ "min_tool_steps": 2,
235
+ "min_grounding_sources": 2,
236
+ "expected_grounding_terms": [
237
+ "frontend/app/chat/page.tsx",
238
+ "backend-rust/src/api/chat.rs"
239
+ ]
240
+ },
241
+ {
242
+ "name": "coder_unsafe_pattern_repo_fix",
243
+ "message": "Atrodi nedrošo pattern backend-rust konfigurācijas ielādē un piedāvā drošāku refactor, balstoties uz backend-rust/src/config.rs saturu.",
244
+ "profile": "coder",
245
+ "expected_terms": [
246
+ "Result",
247
+ "panic",
248
+ "droš"
249
+ ],
250
+ "tags": [
251
+ "coding",
252
+ "unsafe",
253
+ "grounding",
254
+ "rust"
255
+ ],
256
+ "branches": [
257
+ "coder"
258
+ ],
259
+ "level": "release",
260
+ "difficulty": "hard",
261
+ "category": "safety",
262
+ "min_tool_steps": 1,
263
+ "min_grounding_sources": 1,
264
+ "expected_grounding_terms": [
265
+ "backend-rust/src/config.rs"
266
+ ]
267
+ },
268
+ {
269
+ "name": "coder_large_file_refactor_grounded",
270
+ "message": "Iesaki drošu large-file refactor pieeju core-python/maris_core/text/generate.py un core-python/maris_core/text/tools.py, neizjaucot esošo grounding plūsmu.",
271
+ "profile": "coder",
272
+ "expected_terms": [
273
+ "generate.py",
274
+ "tools.py",
275
+ "grounding"
276
+ ],
277
+ "tags": [
278
+ "coding",
279
+ "large-file",
280
+ "refactor",
281
+ "grounding"
282
+ ],
283
+ "branches": [
284
+ "coder"
285
+ ],
286
+ "level": "release",
287
+ "difficulty": "hard",
288
+ "category": "grounding",
289
+ "min_tool_steps": 2,
290
+ "min_grounding_sources": 2,
291
+ "expected_grounding_terms": [
292
+ "core-python/maris_core/text/generate.py",
293
+ "core-python/maris_core/text/tools.py"
294
+ ]
295
+ },
296
+ {
297
+ "name": "planner_dependency_tradeoff",
298
+ "message": "Salīdzini divus ceļus: ātrs ship vai robusta arhitektūra, un iesaki prioritātes nākamajiem 2 sprintiem.",
299
+ "profile": "planner",
300
+ "expected_terms": [
301
+ "priorit",
302
+ "sprint",
303
+ "trade"
304
+ ],
305
+ "tags": [
306
+ "planning",
307
+ "tradeoff"
308
+ ],
309
+ "branches": [
310
+ "planner",
311
+ "master"
312
+ ],
313
+ "level": "ci",
314
+ "difficulty": "hard",
315
+ "category": "reasoning"
316
+ },
317
+ {
318
+ "name": "master_grounded_uncertainty",
319
+ "message": "Vai vari droši apgalvot problēmas cēloni bez logiem? Atbildi piesardzīgi un grounded veidā.",
320
+ "expected_terms": [
321
+ "log",
322
+ "nepietiek",
323
+ "preciz"
324
+ ],
325
+ "forbidden_terms": [
326
+ "100%",
327
+ "garantēju"
328
+ ],
329
+ "tags": [
330
+ "grounding",
331
+ "safety"
332
+ ],
333
+ "branches": [
334
+ "master"
335
+ ],
336
+ "level": "ci",
337
+ "difficulty": "standard",
338
+ "category": "safety"
339
+ },
340
+ {
341
+ "name": "master_technical_latvian_mixed_terms",
342
+ "message": "Paskaidro feature flag rollout dabiskā latviešu valodā tā, lai profesionāli saglabājas termini `feature flag`, `rollback` un `latency`.",
343
+ "expected_terms": [
344
+ "feature flag",
345
+ "rollback",
346
+ "latency"
347
+ ],
348
+ "reference_facts": [
349
+ "pakāpeniski",
350
+ "metriku"
351
+ ],
352
+ "tags": [
353
+ "latvian",
354
+ "terminology",
355
+ "ops"
356
+ ],
357
+ "branches": [
358
+ "master"
359
+ ],
360
+ "level": "ci",
361
+ "difficulty": "hard",
362
+ "category": "latvian_quality"
363
+ },
364
+ {
365
+ "name": "coder_technical_latvian_contract_explanation",
366
+ "message": "Paskaidro, kāpēc TypeScript stream parsera refactorā terminus `delta`, `complete` un `payload` labāk atstāt oriģinālajā formā, bet skaidrojumu rakstīt dabiskā latviešu valodā.",
367
+ "profile": "coder",
368
+ "expected_terms": [
369
+ "delta",
370
+ "complete",
371
+ "payload"
372
+ ],
373
+ "reference_facts": [
374
+ "kontrakta",
375
+ "debug"
376
+ ],
377
+ "tags": [
378
+ "coding",
379
+ "latvian",
380
+ "terminology"
381
+ ],
382
+ "branches": [
383
+ "coder"
384
+ ],
385
+ "level": "ci",
386
+ "difficulty": "hard",
387
+ "category": "latvian_quality"
388
+ },
389
+ {
390
+ "name": "master_multiturn_incident_followup",
391
+ "message": "Tagad konkretizē nākamo soli rollback verifikācijai, balstoties uz to, ka iepriekš jau noskaidrojām: ietekmēta ir tikai daļa requestu un hotfix vēl nav izlaists.",
392
+ "history": [
393
+ {
394
+ "role": "user",
395
+ "content": "Dod īsu incidenta kopsavilkumu."
396
+ },
397
+ {
398
+ "role": "assistant",
399
+ "content": "Ietekmēta ir daļa requestu, rollback ir sagatavots, bet hotfix vēl nav izlaists."
400
+ }
401
+ ],
402
+ "expected_terms": [
403
+ "rollback",
404
+ "verific",
405
+ "request"
406
+ ],
407
+ "reference_facts": [
408
+ "daļa requestu",
409
+ "hotfix vēl nav izlaists"
410
+ ],
411
+ "tags": [
412
+ "latvian",
413
+ "multi-turn",
414
+ "incident"
415
+ ],
416
+ "branches": [
417
+ "master"
418
+ ],
419
+ "level": "ci",
420
+ "difficulty": "hard",
421
+ "category": "long_context"
422
+ },
423
+ {
424
+ "name": "coder_multiturn_ci_debug_followup",
425
+ "message": "Turpini iepriekšējo debugging pavedienu un nosauc nākamo soli tieši `.github/workflows/lint-and-test.yml` un `frontend/tests/chat.test.tsx` kontekstā.",
426
+ "history": [
427
+ {
428
+ "role": "user",
429
+ "content": "Mums flaky tests parādās tikai CI."
430
+ },
431
+ {
432
+ "role": "assistant",
433
+ "content": "Tad jāsalīdzina workflow vide ar lokālo izpildi un jāmeklē timing atšķirības."
434
+ }
435
+ ],
436
+ "profile": "coder",
437
+ "expected_terms": [
438
+ ".github/workflows/lint-and-test.yml",
439
+ "frontend/tests/chat.test.tsx",
440
+ "timing"
441
+ ],
442
+ "reference_facts": [
443
+ "flaky tests parādās tikai CI"
444
+ ],
445
+ "tags": [
446
+ "coding",
447
+ "multi-turn",
448
+ "ci",
449
+ "grounding"
450
+ ],
451
+ "branches": [
452
+ "coder"
453
+ ],
454
+ "level": "ci",
455
+ "difficulty": "hard",
456
+ "category": "long_context"
457
+ },
458
+ {
459
+ "name": "coder_multiturn_plan_continuation",
460
+ "message": "Turpinot iepriekšējo plānu, konkretizē benchmark un test strategy sadaļu `core-python/evals/chat_eval_dataset.json` un `core-python/tests/test_text_benchmark.py` failiem.",
461
+ "history": [
462
+ {
463
+ "role": "user",
464
+ "content": "Iedod augsta līmeņa plānu benchmark paplašināšanai."
465
+ },
466
+ {
467
+ "role": "assistant",
468
+ "content": "Plāns ir sadalīts dataset, preference, benchmark un validācijas blokos."
469
+ }
470
+ ],
471
+ "profile": "coder",
472
+ "expected_terms": [
473
+ "Turpinot iepriekšējo plānu",
474
+ "core-python/evals/chat_eval_dataset.json",
475
+ "core-python/tests/test_text_benchmark.py"
476
+ ],
477
+ "reference_facts": [
478
+ "dataset, preference, benchmark un validācijas blokos"
479
+ ],
480
+ "tags": [
481
+ "coding",
482
+ "multi-turn",
483
+ "planning"
484
+ ],
485
+ "branches": [
486
+ "coder"
487
+ ],
488
+ "level": "ci",
489
+ "difficulty": "hard",
490
+ "category": "long_context"
491
+ },
492
+ {
493
+ "name": "master_multiturn_observability_explanation",
494
+ "message": "Tagad īsi paskaidro, kā šis observability patch palīdzēs incidentu laikā, neatsakoties no iepriekšējā konteksta par `request_id` un `trace_id`.",
495
+ "history": [
496
+ {
497
+ "role": "user",
498
+ "content": "Paskaidro, ko patch dara."
499
+ },
500
+ {
501
+ "role": "assistant",
502
+ "content": "Patch pievieno structured logs ar request_id un trace_id korelācijai starp servisiem."
503
+ }
504
+ ],
505
+ "expected_terms": [
506
+ "request_id",
507
+ "trace_id",
508
+ "incident"
509
+ ],
510
+ "reference_facts": [
511
+ "structured logs",
512
+ "korelācijai starp servisiem"
513
+ ],
514
+ "tags": [
515
+ "latvian",
516
+ "multi-turn",
517
+ "observability"
518
+ ],
519
+ "branches": [
520
+ "master"
521
+ ],
522
+ "level": "ci",
523
+ "difficulty": "hard",
524
+ "category": "long_context"
525
+ },
526
+ {
527
+ "name": "master_incident_status_update_with_constraints",
528
+ "message": "Uzraksti īsu production incidenta update: rollback ir palaists, hotfix vēl review stadijā, ietekme ir tikai uz EU tenantiem un ETA drīkst dot tikai piesardzīgi.",
529
+ "expected_terms": [
530
+ "rollback",
531
+ "hotfix",
532
+ "ETA",
533
+ "EU"
534
+ ],
535
+ "reference_facts": [
536
+ "rollback ir palaists",
537
+ "hotfix vēl review stadijā",
538
+ "ietekme ir tikai uz EU tenantiem"
539
+ ],
540
+ "tags": [
541
+ "incident",
542
+ "production",
543
+ "comms"
544
+ ],
545
+ "branches": [
546
+ "master",
547
+ "planner"
548
+ ],
549
+ "level": "release",
550
+ "difficulty": "hard",
551
+ "category": "safety",
552
+ "failure_bucket": "incident_comms",
553
+ "risk_level": "high",
554
+ "production_like": true
555
+ },
556
+ {
557
+ "name": "coder_backward_compat_api_review",
558
+ "message": "Pārskati plānoto API atbildes refactoru un pasaki, ko vispirms pārbaudīt, lai nesalauztu backward-compatible klientus.",
559
+ "profile": "coder",
560
+ "expected_terms": [
561
+ "backward",
562
+ "clients",
563
+ "tests"
564
+ ],
565
+ "tags": [
566
+ "api",
567
+ "backward-compat",
568
+ "review"
569
+ ],
570
+ "branches": [
571
+ "coder"
572
+ ],
573
+ "level": "release",
574
+ "difficulty": "hard",
575
+ "category": "grounding",
576
+ "failure_bucket": "backward_compat_regression",
577
+ "risk_level": "high",
578
+ "production_like": true
579
+ },
580
+ {
581
+ "name": "coder_ci_flake_requires_specific_check_order",
582
+ "message": "CI flaky tests parādās tikai pēc merge queue. Dod konkrētu diagnostikas secību un nepiedāvā uzreiz random retry.",
583
+ "profile": "coder",
584
+ "expected_terms": [
585
+ "CI",
586
+ "timing",
587
+ "diagnost"
588
+ ],
589
+ "forbidden_terms": [
590
+ "random retry"
591
+ ],
592
+ "tags": [
593
+ "ci",
594
+ "flaky",
595
+ "debugging"
596
+ ],
597
+ "branches": [
598
+ "coder"
599
+ ],
600
+ "level": "ci",
601
+ "difficulty": "hard",
602
+ "category": "reasoning",
603
+ "failure_bucket": "flaky_ci_diagnosis",
604
+ "risk_level": "medium",
605
+ "production_like": false
606
+ },
607
+ {
608
+ "name": "master_hallucination_guard_without_logs",
609
+ "message": "Bez logiem un bez metric snapshots pasaki, vai drīkst droši nosaukt vienu konkrētu root cause.",
610
+ "expected_terms": [
611
+ "log",
612
+ "metr",
613
+ "nepietiek"
614
+ ],
615
+ "forbidden_terms": [
616
+ "100%",
617
+ "garantēju"
618
+ ],
619
+ "tags": [
620
+ "grounding",
621
+ "hallucination",
622
+ "safety"
623
+ ],
624
+ "branches": [
625
+ "master"
626
+ ],
627
+ "level": "release",
628
+ "difficulty": "hard",
629
+ "category": "safety",
630
+ "failure_bucket": "hallucination",
631
+ "risk_level": "high",
632
+ "production_like": true
633
+ },
634
+ {
635
+ "name": "planner_release_tradeoff_with_rollback_window",
636
+ "message": "Salīdzini canary rollout pret tūlītēju full rollout situācijā, kur rollback window ir ļoti īss un migrācija skar write path.",
637
+ "profile": "planner",
638
+ "expected_terms": [
639
+ "canary",
640
+ "rollback",
641
+ "write path"
642
+ ],
643
+ "tags": [
644
+ "planning",
645
+ "release",
646
+ "migration"
647
+ ],
648
+ "branches": [
649
+ "planner",
650
+ "master"
651
+ ],
652
+ "level": "release",
653
+ "difficulty": "hard",
654
+ "category": "reasoning",
655
+ "failure_bucket": "rollback_mistake",
656
+ "risk_level": "high",
657
+ "production_like": true
658
+ },
659
+ {
660
+ "name": "coder_multiturn_wrong_turn_recovery",
661
+ "message": "Iepriekšējā atbildē tu ieteici riskantu shortcut. Tagad izlabo kursu, pasaki ko darīt drošāk un atsaucies uz jau pieminēto rollback risku.",
662
+ "history": [
663
+ {
664
+ "role": "user",
665
+ "content": "Vai varam izlaist testus, lai ātrāk salabotu stream parseri?"
666
+ },
667
+ {
668
+ "role": "assistant",
669
+ "content": "Teorētiski varētu ātri patchot un testus pievienot vēlāk, bet tas ir riskanti."
670
+ }
671
+ ],
672
+ "profile": "coder",
673
+ "expected_terms": [
674
+ "rollback",
675
+ "test",
676
+ "droš"
677
+ ],
678
+ "reference_facts": [
679
+ "tas ir riskanti"
680
+ ],
681
+ "tags": [
682
+ "multi-turn",
683
+ "recovery",
684
+ "safety"
685
+ ],
686
+ "branches": [
687
+ "coder"
688
+ ],
689
+ "level": "release",
690
+ "difficulty": "hard",
691
+ "category": "long_context",
692
+ "failure_bucket": "unsafe_refactor",
693
+ "risk_level": "high",
694
+ "production_like": true
695
+ },
696
+ {
697
+ "name": "coder_grounded_migration_review",
698
+ "message": "Pamatojoties uz backend-rust/migrations/0013_platform_foundation.sql un shared/schemas/platform_contract_schema.json, ko pārbaudīt pirms schema rollout?",
699
+ "profile": "coder",
700
+ "expected_terms": [
701
+ "schema",
702
+ "rollout",
703
+ "contract"
704
+ ],
705
+ "tags": [
706
+ "grounding",
707
+ "migration",
708
+ "schema"
709
+ ],
710
+ "branches": [
711
+ "coder"
712
+ ],
713
+ "level": "release",
714
+ "difficulty": "hard",
715
+ "category": "grounding",
716
+ "min_tool_steps": 2,
717
+ "min_grounding_sources": 2,
718
+ "expected_grounding_terms": [
719
+ "backend-rust/migrations/0013_platform_foundation.sql",
720
+ "shared/schemas/platform_contract_schema.json"
721
+ ],
722
+ "failure_bucket": "schema_regression",
723
+ "risk_level": "high",
724
+ "production_like": true
725
+ },
726
+ {
727
+ "name": "master_missing_clarification_for_vague_hotfix",
728
+ "message": "Pasaki uzreiz, kuru hotfix failu mainīt, lai viss productionā kļūtu stabils.",
729
+ "expected_terms": [
730
+ "preciz",
731
+ "hotfix",
732
+ "production"
733
+ ],
734
+ "tags": [
735
+ "clarification",
736
+ "hotfix"
737
+ ],
738
+ "branches": [
739
+ "master"
740
+ ],
741
+ "level": "ci",
742
+ "difficulty": "standard",
743
+ "category": "helpfulness",
744
+ "failure_bucket": "missing_clarification",
745
+ "risk_level": "medium",
746
+ "production_like": true
747
+ },
748
+ {
749
+ "name": "coder_stream_contract_regression_warning",
750
+ "message": "Uzraksti review komentāru par stream event kontraktu tā, lai skaidri pateikts, kāpēc delta/complete secība nedrīkst mainīties bez regresijas testiem.",
751
+ "profile": "coder",
752
+ "expected_terms": [
753
+ "delta",
754
+ "complete",
755
+ "regres"
756
+ ],
757
+ "tags": [
758
+ "review",
759
+ "stream",
760
+ "regression"
761
+ ],
762
+ "branches": [
763
+ "coder"
764
+ ],
765
+ "level": "ci",
766
+ "difficulty": "hard",
767
+ "category": "latvian_quality",
768
+ "failure_bucket": "broken_contract",
769
+ "risk_level": "high",
770
+ "production_like": true
771
+ },
772
+ {
773
+ "name": "planner_multiturn_benchmark_followup",
774
+ "message": "Turpinot iepriekšējo benchmark paplašināšanas pavedienu, konkretizē tieši failure-case un reviewer workflow sadaļu, nevis pārraksti visu plānu no nulles.",
775
+ "history": [
776
+ {
777
+ "role": "user",
778
+ "content": "Iedod world-class eval paplašināšanas plānu."
779
+ },
780
+ {
781
+ "role": "assistant",
782
+ "content": "Plāns sastāv no dataset, judge, human eval, regression un docs blokiem."
783
+ }
784
+ ],
785
+ "profile": "planner",
786
+ "expected_terms": [
787
+ "Turpinot iepriekšējo",
788
+ "failure",
789
+ "reviewer"
790
+ ],
791
+ "reference_facts": [
792
+ "dataset, judge, human eval, regression un docs blokiem"
793
+ ],
794
+ "tags": [
795
+ "multi-turn",
796
+ "planning",
797
+ "reviewer-workflow"
798
+ ],
799
+ "branches": [
800
+ "planner"
801
+ ],
802
+ "level": "ci",
803
+ "difficulty": "hard",
804
+ "category": "long_context",
805
+ "failure_bucket": "multi_turn_restart",
806
+ "risk_level": "medium",
807
+ "production_like": false
808
+ },
809
+ {
810
+ "name": "coder_production_like_code_fix_requires_tests",
811
+ "message": "Piedāvā Python patch ideju timeout helperim tā, lai uzreiz pieminēti regresijas testi, rollback drošība un edge cases.",
812
+ "profile": "coder",
813
+ "expected_terms": [
814
+ "timeout",
815
+ "test",
816
+ "rollback"
817
+ ],
818
+ "tags": [
819
+ "coding",
820
+ "production",
821
+ "quality"
822
+ ],
823
+ "branches": [
824
+ "coder"
825
+ ],
826
+ "level": "release",
827
+ "difficulty": "hard",
828
+ "category": "coding",
829
+ "expects_code": true,
830
+ "failure_bucket": "production_regression",
831
+ "risk_level": "high",
832
+ "production_like": true
833
+ },
834
+ {
835
+ "name": "master_blind_eval_process_explanation",
836
+ "message": "Paskaidro, kā jāizskatās blind side-by-side review procesam, lai reviewers neredz branch vai modeļa identitāti.",
837
+ "expected_terms": [
838
+ "blind",
839
+ "side-by-side",
840
+ "reviewer"
841
+ ],
842
+ "tags": [
843
+ "human-eval",
844
+ "process"
845
+ ],
846
+ "branches": [
847
+ "master",
848
+ "planner"
849
+ ],
850
+ "level": "ci",
851
+ "difficulty": "hard",
852
+ "category": "reasoning",
853
+ "failure_bucket": "reviewer_bias",
854
+ "risk_level": "medium",
855
+ "production_like": false
856
+ }
857
+ ]
858
+ }
core-python/evals/coder_execution_benchmark.json ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cases": [
3
+ {
4
+ "name": "coder_python_execution_normalize_email",
5
+ "message": "Uzraksti Python funkciju `normalize_email(email: str) -> str`, kas noņem atstarpes, normalizē lower-case un met ValueError tukšai ievadei.",
6
+ "profile": "coder",
7
+ "expected_terms": ["normalize_email", "ValueError"],
8
+ "tags": ["coding", "python", "execution"],
9
+ "branches": ["coder"],
10
+ "level": "ci",
11
+ "difficulty": "standard",
12
+ "category": "coding",
13
+ "expects_code": true,
14
+ "execution_language": "python",
15
+ "execution_test_code": "assert normalize_email(' A@Example.COM ') == 'a@example.com'\ntry:\n normalize_email(' ')\nexcept ValueError:\n pass\nelse:\n raise AssertionError('expected ValueError')"
16
+ },
17
+ {
18
+ "name": "coder_typescript_execution_next_delay",
19
+ "message": "Uzraksti TypeScript funkciju `nextDelay(attempt: number, baseMs = 250): number`, kas atbalsta exponential backoff un attempts<=0 gadījumā atgriež 0.",
20
+ "profile": "coder",
21
+ "expected_terms": ["nextDelay", "attempt"],
22
+ "tags": ["coding", "typescript", "execution"],
23
+ "branches": ["coder"],
24
+ "level": "ci",
25
+ "difficulty": "standard",
26
+ "category": "coding",
27
+ "expects_code": true,
28
+ "execution_language": "typescript",
29
+ "execution_test_code": "function assert(condition: boolean, message: string): void { if (!condition) throw new Error(message); }\nassert(nextDelay(0) === 0, 'attempt 0');\nassert(nextDelay(1) === 250, 'attempt 1');\nassert(nextDelay(3, 100) === 400, 'attempt 3')"
30
+ },
31
+ {
32
+ "name": "coder_rust_execution_load_port",
33
+ "message": "Uzraksti Rust funkciju `load_port(raw: &str) -> Result<u16, String>`, kas atgriež kļūdu tukšai vai nederīgai porta vērtībai un nepieļauj panic.",
34
+ "profile": "coder",
35
+ "expected_terms": ["Result", "u16"],
36
+ "tags": ["coding", "rust", "execution"],
37
+ "branches": ["coder"],
38
+ "level": "ci",
39
+ "difficulty": "hard",
40
+ "category": "coding",
41
+ "expects_code": true,
42
+ "execution_language": "rust",
43
+ "execution_test_code": "fn main() {\n assert_eq!(load_port(\"8080\").unwrap(), 8080);\n assert!(load_port(\"\").is_err());\n assert!(load_port(\"0\").is_err());\n assert!(load_port(\"abc\").is_err());\n}"
44
+ },
45
+ {
46
+ "name": "coder_sql_execution_pass_rate_regression",
47
+ "message": "Uzraksti SQL vaicājumu, kas apkopo execution pass rate pa branch un language no benchmark_results un execution_results tabulām, un iezīmē branchus zem 0.8 sliekšņa ar `is_regression` kolonnu.",
48
+ "profile": "coder",
49
+ "expected_terms": ["execution_pass_rate", "is_regression"],
50
+ "tags": ["coding", "sql", "execution"],
51
+ "branches": ["coder"],
52
+ "level": "ci",
53
+ "difficulty": "hard",
54
+ "category": "coding",
55
+ "expects_code": true,
56
+ "execution_language": "sql",
57
+ "execution_test_code": "CREATE TABLE benchmark_results (id INTEGER PRIMARY KEY, branch TEXT);\nCREATE TABLE execution_results (benchmark_run_id INTEGER, language TEXT, passed INTEGER);\nINSERT INTO benchmark_results (id, branch) VALUES (1, 'coder');\nINSERT INTO execution_results (benchmark_run_id, language, passed) VALUES (1, 'typescript', 1), (1, 'typescript', 0), (1, 'rust', 1);\nCREATE TEMP TABLE actual AS {{CODE}};\nSELECT branch, language, execution_pass_rate, is_regression FROM actual;"
58
+ }
59
+ ]
60
+ }
core-python/evals/coder_preference_dataset.json ADDED
@@ -0,0 +1,745 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "preferences": [
3
+ {
4
+ "prompt": "Salabo Python funkciju normalize_email(email: str), lai tā noņem atstarpes, normalizē lower-case un met ValueError tukšai ievadei.",
5
+ "context": "Lietotājs grib production-ready helperi ar skaidru validāciju un īsu paskaidrojumu.",
6
+ "chosen": "```python\ndef normalize_email(email: str) -> str:\n normalized = email.strip().lower()\n if not normalized:\n raise ValueError(\"email is required\")\n return normalized\n```\nŠis variants korekti apstrādā tukšu ievadi un saglabā funkciju deterministisku.",
7
+ "rejected": "Tu vari vienkārši uzrakstīt kādu funkciju, kas varbūt pārvērš email lower-case, ja vajag.",
8
+ "source": "human_review",
9
+ "branch": "coder",
10
+ "task_type": "bugfix",
11
+ "language": "python",
12
+ "repo_context": [
13
+ "core-python",
14
+ "backend-rust"
15
+ ],
16
+ "execution_required": true,
17
+ "tags": [
18
+ "bugfix",
19
+ "validation",
20
+ "python"
21
+ ],
22
+ "source_type": "real_reviewer",
23
+ "reviewer_segment": "ops",
24
+ "risk_level": "high",
25
+ "grounding_scope": "repo-grounded",
26
+ "failure_bucket": "bugfix",
27
+ "preference_outcome": "chosen",
28
+ "confidence": 0.62,
29
+ "pair_id": "pref-pair-0001",
30
+ "blind": true,
31
+ "production_like": true,
32
+ "multi_turn": false
33
+ },
34
+ {
35
+ "prompt": "Refaktorē TypeScript retry helperi tā, lai tas atbalsta exponential backoff un skaidrus edge cases.",
36
+ "context": "Esošais helperis dubulto kodu un nestrādā korekti pie attempts=0.",
37
+ "chosen": "```ts\nexport function nextDelay(attempt: number, baseMs = 250): number {\n if (attempt <= 0) return 0;\n return baseMs * 2 ** (attempt - 1);\n}\n```\nPiemini arī testus priekš attempts=0 un lieliem mēģinājumu skaitiem.",
38
+ "rejected": "Var izmantot setTimeout un kaut kādu formulu. Testi nav vajadzīgi.",
39
+ "source": "human_review",
40
+ "branch": "coder",
41
+ "task_type": "refactor",
42
+ "language": "typescript",
43
+ "repo_context": [
44
+ "frontend"
45
+ ],
46
+ "execution_required": true,
47
+ "tags": [
48
+ "refactor",
49
+ "retry",
50
+ "edge-cases"
51
+ ],
52
+ "source_type": "internal_curated",
53
+ "reviewer_segment": "staff_engineer",
54
+ "risk_level": "medium",
55
+ "grounding_scope": "single-file",
56
+ "failure_bucket": "unsafe_refactor",
57
+ "preference_outcome": "chosen",
58
+ "confidence": 0.71,
59
+ "pair_id": "pref-pair-0002",
60
+ "blind": true,
61
+ "production_like": false,
62
+ "multi_turn": false
63
+ },
64
+ {
65
+ "prompt": "Uzraksti repo-level plānu un diff stila izmaiņu aprakstu frontend+backend SSE saskaņošanai.",
66
+ "context": "Jāsaskaņo event nosaukumi starp Rust backend un Next.js frontend.",
67
+ "chosen": "Nosauc konkrētos failus, event kontraktu, drošos migrācijas soļus un pievieno testu plānu abām pusēm.",
68
+ "rejected": "Pamaini backend un frontend tā, lai viss strādā.",
69
+ "source": "human_review",
70
+ "branch": "planner",
71
+ "task_type": "repo-level",
72
+ "language": "markdown",
73
+ "repo_context": [
74
+ "backend-rust",
75
+ "frontend"
76
+ ],
77
+ "execution_required": false,
78
+ "tags": [
79
+ "repo-level",
80
+ "sse",
81
+ "planning"
82
+ ],
83
+ "source_type": "synthetic",
84
+ "reviewer_segment": "review_panel",
85
+ "risk_level": "medium",
86
+ "grounding_scope": "cross-service",
87
+ "failure_bucket": "broken_contract",
88
+ "preference_outcome": "chosen",
89
+ "confidence": 0.8,
90
+ "pair_id": "pref-pair-0003",
91
+ "blind": true,
92
+ "production_like": true,
93
+ "multi_turn": false
94
+ },
95
+ {
96
+ "prompt": "Uzraksti Pytest testu validācijas helperim, kas pārbauda ValueError un success path.",
97
+ "context": "Mērķis ir īss, konkrēts tests bez lieka boilerplate.",
98
+ "chosen": "```python\nimport pytest\n\nfrom app.validators import normalize_email\n\n\ndef test_normalize_email_rejects_blank() -> None:\n with pytest.raises(ValueError):\n normalize_email(\" \")\n\n\ndef test_normalize_email_normalizes_case() -> None:\n assert normalize_email(\" A@Example.COM \") == \"a@example.com\"\n```",
99
+ "rejected": "Vienkārši uzraksti, ka vajag testēt kļūdu un success scenāriju.",
100
+ "source": "human_review",
101
+ "branch": "coder",
102
+ "task_type": "test-writing",
103
+ "language": "python",
104
+ "repo_context": [
105
+ "core-python"
106
+ ],
107
+ "execution_required": true,
108
+ "tags": [
109
+ "tests",
110
+ "pytest",
111
+ "python"
112
+ ],
113
+ "source_type": "real_reviewer",
114
+ "reviewer_segment": "ops",
115
+ "risk_level": "high",
116
+ "grounding_scope": "repo-grounded",
117
+ "failure_bucket": "production_regression",
118
+ "preference_outcome": "chosen",
119
+ "confidence": 0.89,
120
+ "pair_id": "pref-pair-0004",
121
+ "blind": true,
122
+ "production_like": false,
123
+ "multi_turn": false
124
+ },
125
+ {
126
+ "prompt": "Salabo Rust konfigurācijas loaderi, lai tukšs vai nederīgs PORT neatstāj unwrap/panic un atgriež strukturētu Result kļūdu.",
127
+ "context": "Operators grib skaidru kļūdu, nevis panic trace startup laikā.",
128
+ "chosen": "```rust\npub fn load_port(raw: &str) -> Result<u16, String> {\n let normalized = raw.trim();\n if normalized.is_empty() {\n return Err(\"PORT is missing\".to_string());\n }\n normalized\n .parse::<u16>()\n .map_err(|_| \"PORT is invalid\".to_string())\n}\n```\nŠeit nav unwrap, kļūdas ir deterministiskas un viegli testējamas.",
129
+ "rejected": "```rust\npub fn load_port(raw: &str) -> u16 {\n raw.parse().unwrap()\n}\n```\nTas ir īsāk un pietiekami labi.",
130
+ "source": "human_review",
131
+ "branch": "coder",
132
+ "task_type": "bugfix",
133
+ "language": "rust",
134
+ "repo_context": [
135
+ "backend-rust"
136
+ ],
137
+ "execution_required": true,
138
+ "tags": [
139
+ "rust",
140
+ "bugfix",
141
+ "unsafe"
142
+ ],
143
+ "source_type": "internal_curated",
144
+ "reviewer_segment": "staff_engineer",
145
+ "risk_level": "medium",
146
+ "grounding_scope": "single-file",
147
+ "failure_bucket": "schema_regression",
148
+ "preference_outcome": "chosen",
149
+ "confidence": 0.62,
150
+ "pair_id": "pref-pair-0005",
151
+ "blind": true,
152
+ "production_like": true,
153
+ "multi_turn": false
154
+ },
155
+ {
156
+ "prompt": "Uzraksti TypeScript stream event union, kas compile-time līmenī atdala delta un complete payloadus.",
157
+ "context": "UI parseris bieži piekļūst neeksistējošiem laukiem, tāpēc vajag stingrāku typing.",
158
+ "chosen": "```ts\nexport type ChatStreamEvent =\n | { type: 'delta'; text: string }\n | { type: 'complete'; done: true; text?: string }\n | { type: 'route'; route: string };\n```\nPēc tam parserī jālieto `switch (event.type)` un testos jāpārbauda compile-safe narrowing.",
159
+ "rejected": "Var izmantot `any` un pārbaudīt laukus runtime laikā, tas būs ātrāk.",
160
+ "source": "human_review",
161
+ "branch": "coder",
162
+ "task_type": "refactor",
163
+ "language": "typescript",
164
+ "repo_context": [
165
+ "frontend"
166
+ ],
167
+ "execution_required": true,
168
+ "tags": [
169
+ "typescript",
170
+ "typing",
171
+ "stream"
172
+ ],
173
+ "source_type": "synthetic",
174
+ "reviewer_segment": "review_panel",
175
+ "risk_level": "medium",
176
+ "grounding_scope": "cross-service",
177
+ "failure_bucket": "incident_comms",
178
+ "preference_outcome": "chosen",
179
+ "confidence": 0.71,
180
+ "pair_id": "pref-pair-0006",
181
+ "blind": true,
182
+ "production_like": false,
183
+ "multi_turn": false
184
+ },
185
+ {
186
+ "prompt": "Iesaki SQL vaicājumu execution pass rate apkopošanai pa branch un language ar regression flag zem 0.8.",
187
+ "context": "Analytics pusē vajag deterministisku query bez string concatenation un ar skaidru alias naming.",
188
+ "chosen": "```sql\nSELECT\n b.branch,\n e.language,\n AVG(CASE WHEN e.passed THEN 1.0 ELSE 0.0 END) AS execution_pass_rate,\n CASE WHEN AVG(CASE WHEN e.passed THEN 1.0 ELSE 0.0 END) < 0.8 THEN 1 ELSE 0 END AS is_regression\nFROM benchmark_results b\nJOIN execution_results e ON e.benchmark_run_id = b.id\nGROUP BY b.branch, e.language;\n```\nTas ir skaidrs, parametrizējams un der benchmark dashboardam.",
189
+ "rejected": "```sql\nSELECT * FROM benchmark_results, execution_results;\n```\nPēc tam jau var filtrēt aplikācijā.",
190
+ "source": "human_review",
191
+ "branch": "coder",
192
+ "task_type": "repo-level",
193
+ "language": "sql",
194
+ "repo_context": [
195
+ "operations"
196
+ ],
197
+ "execution_required": true,
198
+ "tags": [
199
+ "sql",
200
+ "analytics",
201
+ "quality"
202
+ ],
203
+ "source_type": "real_reviewer",
204
+ "reviewer_segment": "ops",
205
+ "risk_level": "high",
206
+ "grounding_scope": "repo-grounded",
207
+ "failure_bucket": "multi_turn_restart",
208
+ "preference_outcome": "chosen",
209
+ "confidence": 0.8,
210
+ "pair_id": "pref-pair-0007",
211
+ "blind": true,
212
+ "production_like": true,
213
+ "multi_turn": false
214
+ },
215
+ {
216
+ "prompt": "Apraksti repo-level patch plānu python_bridge timeout/stderr/invalid JSON kļūdu vienotam error modelim.",
217
+ "context": "Svarīgi ir nosaukt konkrētus failus, migrācijas secību un testus, ne tikai vispārīgu refactor ieteikumu.",
218
+ "chosen": "Labs variants nosauc `backend-rust/src/inference/python_bridge.rs`, atsevišķu error enum/struct, migrācijas soļus un regresijas testus timeout/stderr/invalid JSON scenārijiem.",
219
+ "rejected": "Vienkārši ieliec kopēju error handleri kaut kur bridge slānī.",
220
+ "source": "human_review",
221
+ "branch": "coder",
222
+ "task_type": "repo-level",
223
+ "language": "markdown",
224
+ "repo_context": [
225
+ "backend-rust",
226
+ "core-python"
227
+ ],
228
+ "execution_required": false,
229
+ "tags": [
230
+ "repo-level",
231
+ "bridge",
232
+ "errors"
233
+ ],
234
+ "source_type": "internal_curated",
235
+ "reviewer_segment": "staff_engineer",
236
+ "risk_level": "medium",
237
+ "grounding_scope": "single-file",
238
+ "failure_bucket": "hallucination",
239
+ "preference_outcome": "chosen",
240
+ "confidence": 0.89,
241
+ "pair_id": "pref-pair-0008",
242
+ "blind": true,
243
+ "production_like": false,
244
+ "multi_turn": false
245
+ },
246
+ {
247
+ "prompt": "Salabo nedrošu SQL query builderi, kas WHERE klauzulā concatenē lietotāja ievadi, un piedāvā parametrizētu alternatīvu.",
248
+ "context": "Galvenais ir novērst injection risku un saglabāt lasāmu query API.",
249
+ "chosen": "Drošais variants aizvieto string concatenation ar placeholderiem (`?`, `$1`) un parāda, kā parametri tiek padoti atsevišķi no query stringa. Papildus piemin testus injection un tukšas ievades gadījumiem.",
250
+ "rejected": "Var atstāt concatenation, ja inputu iepriekš `trim()` un pārbauda uz tukšu virkni.",
251
+ "source": "human_review",
252
+ "branch": "coder",
253
+ "task_type": "unsafe",
254
+ "language": "sql",
255
+ "repo_context": [
256
+ "operations",
257
+ "backend-rust"
258
+ ],
259
+ "execution_required": false,
260
+ "tags": [
261
+ "unsafe",
262
+ "sql",
263
+ "security"
264
+ ],
265
+ "source_type": "synthetic",
266
+ "reviewer_segment": "review_panel",
267
+ "risk_level": "medium",
268
+ "grounding_scope": "cross-service",
269
+ "failure_bucket": "bugfix",
270
+ "preference_outcome": "chosen",
271
+ "confidence": 0.62,
272
+ "pair_id": "pref-pair-0009",
273
+ "blind": true,
274
+ "production_like": true,
275
+ "multi_turn": false
276
+ },
277
+ {
278
+ "prompt": "Uzraksti sliktā refactor piemēra noraidījumu, ja lietotājs grib sadalīt stream parseri mazos helperos, bet pazaudē delta/complete kontrakta pārbaudes.",
279
+ "context": "Mērķis ir uzsvērt, ka refactor nedrīkst salauzt esošo grounding un event secību testus.",
280
+ "chosen": "Labs chosen variants paskaidro, ka refactor jāveic kopā ar kontrakta testiem, event secības regresijas pārbaudēm un skaidru state ownership. Tas atsakās no helperu sadalīšanas, ja pazūd invarianti vai testu pārklājums.",
281
+ "rejected": "Sadalīt parseri helperos vienmēr ir labi; testus var pievienot vēlāk, ja būs laiks.",
282
+ "source": "human_review",
283
+ "branch": "coder",
284
+ "task_type": "refactor",
285
+ "language": "typescript",
286
+ "repo_context": [
287
+ "frontend",
288
+ "backend-rust"
289
+ ],
290
+ "execution_required": false,
291
+ "tags": [
292
+ "refactor",
293
+ "stream",
294
+ "quality"
295
+ ],
296
+ "source_type": "real_reviewer",
297
+ "reviewer_segment": "ops",
298
+ "risk_level": "high",
299
+ "grounding_scope": "repo-grounded",
300
+ "failure_bucket": "unsafe_refactor",
301
+ "preference_outcome": "chosen",
302
+ "confidence": 0.71,
303
+ "pair_id": "pref-pair-0010",
304
+ "blind": true,
305
+ "production_like": false,
306
+ "multi_turn": false
307
+ },
308
+ {
309
+ "prompt": "Iesaki repo-wide patch plānu benchmark history/regression tracking slānim starp core-python/maris_core/text/benchmark.py, core-python/maris_core/training/train.py un core-python/scripts/eval_model.py.",
310
+ "context": "Svarīgi ir ne tikai saglabāt manifestu, bet arī salīdzināt current run pret baseline pa category un execution language.",
311
+ "chosen": "Labs variants nosauc visus trīs failus, pieprasa `benchmark-history.json` un `benchmark-regression-report.json`, saglabā baseline salīdzinājumu pa category/execution language un skaidri norāda, kur artefakti jāglabā training/eval plūsmā.",
312
+ "rejected": "Vienkārši saglabā vēl vienu benchmark JSON failu. Salīdzināšanu var izdarīt vēlāk, ja būs vajadzīgs.",
313
+ "source": "human_review",
314
+ "branch": "coder",
315
+ "task_type": "repo-level",
316
+ "language": "markdown",
317
+ "repo_context": [
318
+ "core-python",
319
+ "github-actions"
320
+ ],
321
+ "execution_required": false,
322
+ "tags": [
323
+ "benchmark",
324
+ "history",
325
+ "regression",
326
+ "repo-level"
327
+ ],
328
+ "source_type": "internal_curated",
329
+ "reviewer_segment": "staff_engineer",
330
+ "risk_level": "medium",
331
+ "grounding_scope": "single-file",
332
+ "failure_bucket": "broken_contract",
333
+ "preference_outcome": "chosen",
334
+ "confidence": 0.8,
335
+ "pair_id": "pref-pair-0011",
336
+ "blind": true,
337
+ "production_like": true,
338
+ "multi_turn": false
339
+ },
340
+ {
341
+ "prompt": "Piedāvā multi-file bugfix pieeju complete event dubultotai assistant ziņai frontend/app/chat/page.tsx un frontend/tests/chat.test.tsx.",
342
+ "context": "Nepietiek tikai ar UI labojumu; jāpielāgo arī tests, kas sedz delta->complete secību.",
343
+ "chosen": "Drošais variants nosauc abus failus, saglabā inkrementālu delta renderēšanu, novērš final ziņas dubultošanu un pievieno regresijas testu tieši delta->complete secībai.",
344
+ "rejected": "Pamaini tikai UI failu, lai complete event vienkārši vienmēr pārraksta tekstu. Testi var pagaidīt.",
345
+ "source": "human_review",
346
+ "branch": "coder",
347
+ "task_type": "bugfix",
348
+ "language": "typescript",
349
+ "repo_context": [
350
+ "frontend"
351
+ ],
352
+ "execution_required": true,
353
+ "tags": [
354
+ "multi-file",
355
+ "bugfix",
356
+ "stream",
357
+ "tests"
358
+ ],
359
+ "source_type": "synthetic",
360
+ "reviewer_segment": "review_panel",
361
+ "risk_level": "medium",
362
+ "grounding_scope": "cross-service",
363
+ "failure_bucket": "production_regression",
364
+ "preference_outcome": "chosen",
365
+ "confidence": 0.89,
366
+ "pair_id": "pref-pair-0012",
367
+ "blind": true,
368
+ "production_like": false,
369
+ "multi_turn": false
370
+ },
371
+ {
372
+ "prompt": "Apraksti risky refactor python_bridge timeout/stderr/invalid JSON error modelim, nesalaužot backend API semantiku incidentu laikā.",
373
+ "context": "Lietotājam vajag refactor ar skaidriem regresiju riskiem, backward-compatible mappingu un testiem.",
374
+ "chosen": "Labs variants prasa vienotu typed error modeli, norāda backward-compatible mappingu API robežā un pievieno timeout/stderr/invalid JSON regresijas testus pirms merge.",
375
+ "rejected": "Iznes copy-paste match blokus helperī un cer, ka viss turpinās strādāt; incident recovery var sakārtot vēlāk.",
376
+ "source": "human_review",
377
+ "branch": "coder",
378
+ "task_type": "refactor",
379
+ "language": "rust",
380
+ "repo_context": [
381
+ "backend-rust"
382
+ ],
383
+ "execution_required": false,
384
+ "tags": [
385
+ "refactor",
386
+ "regression-risk",
387
+ "incident-recovery",
388
+ "errors"
389
+ ],
390
+ "source_type": "real_reviewer",
391
+ "reviewer_segment": "ops",
392
+ "risk_level": "high",
393
+ "grounding_scope": "repo-grounded",
394
+ "failure_bucket": "schema_regression",
395
+ "preference_outcome": "chosen",
396
+ "confidence": 0.62,
397
+ "pair_id": "pref-pair-0013",
398
+ "blind": true,
399
+ "production_like": true,
400
+ "multi_turn": false
401
+ },
402
+ {
403
+ "prompt": "Iedod CI debugging un incident recovery pieeju, ja core-train workflow vairs nepublicē benchmark history/regression artefaktus pēc coder execution benchmark step.",
404
+ "context": "Mērķis ir nosaukt workflow failus, eval entrypoint un rollback/repair secību, nevis tikai pateikt 'pārbaudi CI logus'.",
405
+ "chosen": "Labs variants atsaucas uz `.github/workflows/core-train.yml`, `.github/workflows/lint-and-test.yml` un `core-python/scripts/eval_model.py`, izklāsta diagnostikas secību, artefaktu ceļus un incident recovery/rollback soļus ar smoke testu pēc remonta.",
406
+ "rejected": "Skaties CI logus un mēģini palaist workflow vēlreiz. Ja nepalīdz, droši vien artefakti nav vajadzīgi.",
407
+ "source": "human_review",
408
+ "branch": "coder",
409
+ "task_type": "ci-orchestration",
410
+ "language": "yaml",
411
+ "repo_context": [
412
+ "github-actions",
413
+ "core-python"
414
+ ],
415
+ "execution_required": false,
416
+ "tags": [
417
+ "ci",
418
+ "debugging",
419
+ "incident-recovery",
420
+ "artifacts"
421
+ ],
422
+ "source_type": "internal_curated",
423
+ "reviewer_segment": "staff_engineer",
424
+ "risk_level": "medium",
425
+ "grounding_scope": "single-file",
426
+ "failure_bucket": "incident_comms",
427
+ "preference_outcome": "chosen",
428
+ "confidence": 0.71,
429
+ "pair_id": "pref-pair-0014",
430
+ "blind": true,
431
+ "production_like": false,
432
+ "multi_turn": false
433
+ },
434
+ {
435
+ "prompt": "Apraksti TypeScript stream parsera labojumu latviešu valodā tā, lai saglabājas profesionāla LV+EN terminoloģija.",
436
+ "context": "Svarīgi ir nedot mehānisku tulkojumu; jāpiemin `delta`, `complete`, `payload`, kontrakts un regresijas tests.",
437
+ "chosen": "Drošais variants skaidri pasaka, ka `delta` un `complete` ir event kontrakta termini, kurus nevajag mākslīgi pārtulkot. Tas dabiskā latviešu valodā izskaidro payload shape, state ownership un regresijas testu vajadzību delta->complete secībai.",
438
+ "rejected": "Plūsmas pabeigšanas gabals un kravas saturs jāpārtulko pilnībā latviski, jo angļu termini padara tekstu nepareizu. Testus var pieminēt vēlāk.",
439
+ "source": "human_review",
440
+ "branch": "coder",
441
+ "task_type": "refactor",
442
+ "language": "typescript",
443
+ "repo_context": [
444
+ "frontend"
445
+ ],
446
+ "execution_required": false,
447
+ "tags": [
448
+ "latvian",
449
+ "terminology",
450
+ "stream",
451
+ "quality"
452
+ ],
453
+ "source_type": "synthetic",
454
+ "reviewer_segment": "review_panel",
455
+ "risk_level": "medium",
456
+ "grounding_scope": "cross-service",
457
+ "failure_bucket": "multi_turn_restart",
458
+ "preference_outcome": "chosen",
459
+ "confidence": 0.8,
460
+ "pair_id": "pref-pair-0015",
461
+ "blind": true,
462
+ "production_like": true,
463
+ "multi_turn": false
464
+ },
465
+ {
466
+ "prompt": "Uzraksti incidenta status update latviešu valodā par rollback un hotfix scenāriju.",
467
+ "context": "Atbildei jābūt īsai, faktoloģiskai un profesionālai, saglabājot stabilos terminus `rollback`, `hotfix` un `ETA`.",
468
+ "chosen": "Labs variants īsi nosauc ietekmi, current mitigation, rollback statusu, hotfix progresu un nākamo ETA checkpoint. Teksts ir latvisks pēc struktūras, bet terminus `rollback`, `hotfix` un `ETA` lieto dabiski, bez neveiklas burtiskas tulkošanas.",
469
+ "rejected": "Mēs veicam atpakaļripošanu un karsto labojumu tuvākajā laika brīdī, viss būs pilnībā kārtībā. Precīzus riskus vai ETA nav jāmin.",
470
+ "source": "human_review",
471
+ "branch": "coder",
472
+ "task_type": "ci-orchestration",
473
+ "language": "markdown",
474
+ "repo_context": [
475
+ "operations"
476
+ ],
477
+ "execution_required": false,
478
+ "tags": [
479
+ "latvian",
480
+ "incident-recovery",
481
+ "tone"
482
+ ],
483
+ "source_type": "real_reviewer",
484
+ "reviewer_segment": "ops",
485
+ "risk_level": "high",
486
+ "grounding_scope": "repo-grounded",
487
+ "failure_bucket": "hallucination",
488
+ "preference_outcome": "chosen",
489
+ "confidence": 0.89,
490
+ "pair_id": "pref-pair-0016",
491
+ "blind": true,
492
+ "production_like": false,
493
+ "multi_turn": false
494
+ },
495
+ {
496
+ "prompt": "Turpini iepriekšējo sarunu par flaky CI testu un iedod nākamo soli, balstoties uz `.github/workflows/lint-and-test.yml` un `frontend/tests/chat.test.tsx`.",
497
+ "context": "Lietotājs jau iepriekš pateicis, ka problēma parādās tikai CI. Vēlamā atbilde nedrīkst ignorēt šo kontekstu.",
498
+ "chosen": "Labs variants atsaucas uz iepriekš minēto CI-only flaky uzvedību, nosauc workflow un test failu un dod konkrētu nākamo soli par timing/event secības pārbaudi. Tas parāda multi-turn atmiņu un nerestartē sarunu no nulles.",
499
+ "rejected": "Vispirms vajadzētu saprast, kas vispār ir CI un kur atrodas jūsu tests. Varbūt vajag vienkārši paskatīties kaut kādus logus.",
500
+ "source": "human_review",
501
+ "branch": "coder",
502
+ "task_type": "debugging",
503
+ "language": "yaml",
504
+ "repo_context": [
505
+ "github-actions",
506
+ "frontend"
507
+ ],
508
+ "execution_required": false,
509
+ "tags": [
510
+ "latvian",
511
+ "multi-turn",
512
+ "ci",
513
+ "debugging"
514
+ ],
515
+ "source_type": "internal_curated",
516
+ "reviewer_segment": "staff_engineer",
517
+ "risk_level": "medium",
518
+ "grounding_scope": "single-file",
519
+ "failure_bucket": "bugfix",
520
+ "preference_outcome": "chosen",
521
+ "confidence": 0.62,
522
+ "pair_id": "pref-pair-0017",
523
+ "blind": true,
524
+ "production_like": true,
525
+ "multi_turn": true
526
+ },
527
+ {
528
+ "prompt": "Apraksti code review komentāru par observability patch, saglabājot terminus `structured logs`, `request_id`, `trace_id` un `sampling`.",
529
+ "context": "Mērķis ir profesionāls latviešu komentārs, nevis neveikls tulkojums kā 'strukturētie baļķi'.",
530
+ "chosen": "Drošais variants saglabā `structured logs`, `request_id`, `trace_id` un `sampling` oriģinālajā formā, bet latviski izskaidro operatoru ieguvumu un korelācijas vērtību incidentu laikā.",
531
+ "rejected": "Komentārā jāraksta par strukturētajiem baļķiem, pieprasījuma identifikatoru un parauga ņemšanu, jo angļu termini tehniskā tekstā nav labi.",
532
+ "source": "human_review",
533
+ "branch": "coder",
534
+ "task_type": "repo-level",
535
+ "language": "markdown",
536
+ "repo_context": [
537
+ "backend-rust",
538
+ "core-python"
539
+ ],
540
+ "execution_required": false,
541
+ "tags": [
542
+ "latvian",
543
+ "observability",
544
+ "terminology"
545
+ ],
546
+ "source_type": "synthetic",
547
+ "reviewer_segment": "review_panel",
548
+ "risk_level": "medium",
549
+ "grounding_scope": "cross-service",
550
+ "failure_bucket": "unsafe_refactor",
551
+ "preference_outcome": "chosen",
552
+ "confidence": 0.71,
553
+ "pair_id": "pref-pair-0018",
554
+ "blind": true,
555
+ "production_like": false,
556
+ "multi_turn": false
557
+ },
558
+ {
559
+ "prompt": "Iedod pairwise labāku atbildi par SQL migration risku ar terminiem `schema drift`, `rollback window` un `data backfill`.",
560
+ "context": "Svarīgi ir precīzi nosaukt riskus un secību, nevis aizvietot terminus ar miglainiem aprakstiem.",
561
+ "chosen": "Labs variants skaidri nosauc `schema drift` risku, `rollback window` robežas un `data backfill` secību, vienlaikus saglabājot dabisku latviešu teikumu plūdumu un operatoram noderīgus secinājumus.",
562
+ "rejected": "Datubāzes pārmaiņu vilkme un datu aizpildīšana varbūt kaut kā ietekmēs sistēmu, bet detaļas nav īpaši svarīgas, ja viss šķiet droši.",
563
+ "source": "human_review",
564
+ "branch": "coder",
565
+ "task_type": "repo-level",
566
+ "language": "sql",
567
+ "repo_context": [
568
+ "infra",
569
+ "operations"
570
+ ],
571
+ "execution_required": false,
572
+ "tags": [
573
+ "latvian",
574
+ "sql",
575
+ "migration",
576
+ "quality"
577
+ ],
578
+ "source_type": "real_reviewer",
579
+ "reviewer_segment": "ops",
580
+ "risk_level": "high",
581
+ "grounding_scope": "repo-grounded",
582
+ "failure_bucket": "broken_contract",
583
+ "preference_outcome": "chosen",
584
+ "confidence": 0.8,
585
+ "pair_id": "pref-pair-0019",
586
+ "blind": true,
587
+ "production_like": true,
588
+ "multi_turn": false
589
+ },
590
+ {
591
+ "prompt": "Paskaidro, kā multi-turn atbildē turpināt iepriekšēju plānu par benchmark paplašināšanu, nevis sākt pilnīgi jaunu struktūru.",
592
+ "context": "Lietotājs jau ir saskaņojis augsta līmeņa plānu; tagad vajag tikai konkretizēt benchmark un test strategy daļu.",
593
+ "chosen": "Labs variants sāk ar frāzi, kas parāda konteksta turpinājumu, piemēram, 'turpinot iepriekšējo plānu', un pēc tam nosauc konkrētus failus, testus un nākamo soli. Tas ir daudz stiprāks multi-turn signāls nekā pilnīgs restarts.",
594
+ "rejected": "Šeit ir pilnīgi jauns plāns no sākuma, neņemot vērā neko, ko apspriedām iepriekš.",
595
+ "source": "human_review",
596
+ "branch": "coder",
597
+ "task_type": "planning",
598
+ "language": "markdown",
599
+ "repo_context": [
600
+ "core-python"
601
+ ],
602
+ "execution_required": false,
603
+ "tags": [
604
+ "latvian",
605
+ "multi-turn",
606
+ "planning"
607
+ ],
608
+ "source_type": "internal_curated",
609
+ "reviewer_segment": "staff_engineer",
610
+ "risk_level": "medium",
611
+ "grounding_scope": "single-file",
612
+ "failure_bucket": "production_regression",
613
+ "preference_outcome": "chosen",
614
+ "confidence": 0.89,
615
+ "pair_id": "pref-pair-0020",
616
+ "blind": true,
617
+ "production_like": false,
618
+ "multi_turn": true
619
+ },
620
+ {
621
+ "prompt": "Uzraksti incidenta update par `circuit breaker` un `error budget`, saglabājot profesionālu LV+EN terminoloģiju.",
622
+ "context": "Atbildei jābūt īsai, faktoloģiskai un jāizvairās no neveikliem burtiskiem tulkojumiem.",
623
+ "chosen": "Labs variants īsi pasaka, ka `circuit breaker` ir atvēries pēc `trip threshold` sasniegšanas, kā tas ietekmē `error budget`, un kāds ir current mitigation. Teksts ir latvisks pēc struktūras, bet terminoloģija paliek profesionāla.",
624
+ "rejected": "Strāvas pārtraucējs ir nostrādājis un kļūdu budžets ir izlietots, tādēļ mēs ceram uz labāku sistēmas pašsajūtu tuvākajā laikā.",
625
+ "source": "human_review",
626
+ "branch": "coder",
627
+ "task_type": "ci-orchestration",
628
+ "language": "rust",
629
+ "repo_context": [
630
+ "backend-rust"
631
+ ],
632
+ "execution_required": false,
633
+ "tags": [
634
+ "latvian",
635
+ "incident-recovery",
636
+ "circuit-breaker"
637
+ ],
638
+ "source_type": "synthetic",
639
+ "reviewer_segment": "review_panel",
640
+ "risk_level": "medium",
641
+ "grounding_scope": "cross-service",
642
+ "failure_bucket": "schema_regression",
643
+ "preference_outcome": "chosen",
644
+ "confidence": 0.62,
645
+ "pair_id": "pref-pair-0021",
646
+ "blind": true,
647
+ "production_like": true,
648
+ "multi_turn": false
649
+ },
650
+ {
651
+ "prompt": "Salīdzini divus blind side-by-side atbilžu variantus incidenta update uzdevumam un saglabā tikai reviewer preference rezultātu.",
652
+ "context": "Reviewer nedrīkst redzēt branch vai modeli; vajag preference outcome, confidence un īsu rationale.",
653
+ "chosen": "Labākais variants ir īss, faktoloģisks un piemin rollback statusu, hotfix progresu un ETA checkpoint bez nepamatotas pārliecības.",
654
+ "rejected": "Vari vienkārši izvēlēties atbildi, kas skan pārliecinošāk, pat ja tajā nav rollback vai ETA detaļu.",
655
+ "source": "human_review",
656
+ "source_type": "real_reviewer",
657
+ "annotator": "reviewer-07",
658
+ "reviewer_segment": "incident-command",
659
+ "branch": "coder",
660
+ "task_type": "human-eval",
661
+ "language": "markdown",
662
+ "risk_level": "high",
663
+ "grounding_scope": "ops-grounded",
664
+ "failure_bucket": "incident_comms",
665
+ "preference_outcome": "chosen",
666
+ "confidence": 0.91,
667
+ "pair_id": "pref-pair-9001",
668
+ "blind": true,
669
+ "production_like": true,
670
+ "multi_turn": false,
671
+ "repo_context": [
672
+ "operations",
673
+ "core-python"
674
+ ],
675
+ "execution_required": false,
676
+ "tags": [
677
+ "human-eval",
678
+ "blind",
679
+ "incident"
680
+ ]
681
+ },
682
+ {
683
+ "prompt": "Novērtē multi-turn atbildi, kurai jāturpina iepriekšējais benchmark plāns, nevis jārestartē saruna.",
684
+ "context": "Vajag preference example ar skaidru failure bucket multi-turn restartam.",
685
+ "chosen": "Spēcīgais variants sāk ar “turpinot iepriekšējo plānu”, saglabā iepriekš definētos blokus un konkretizē tikai benchmark/test strategy daļu.",
686
+ "rejected": "Sliktais variants pilnībā restartē plānu un ignorē jau apspriesto struktūru.",
687
+ "source": "human_review",
688
+ "source_type": "internal_curated",
689
+ "annotator": "eval-designer",
690
+ "reviewer_segment": "eval-design",
691
+ "branch": "planner",
692
+ "task_type": "planning",
693
+ "language": "markdown",
694
+ "risk_level": "medium",
695
+ "grounding_scope": "conversation-grounded",
696
+ "failure_bucket": "multi_turn_restart",
697
+ "preference_outcome": "chosen",
698
+ "confidence": 0.84,
699
+ "pair_id": "pref-pair-9002",
700
+ "blind": true,
701
+ "production_like": false,
702
+ "multi_turn": true,
703
+ "repo_context": [
704
+ "core-python"
705
+ ],
706
+ "execution_required": false,
707
+ "tags": [
708
+ "multi-turn",
709
+ "planning",
710
+ "human-eval"
711
+ ]
712
+ },
713
+ {
714
+ "prompt": "Iedod reviewer preference piemēru, kur drošāks variants atsakās no hallucinated root cause bez logiem.",
715
+ "context": "Šis piemērs vajadzīgs safety/hallucination bucketam.",
716
+ "chosen": "Labs variants pasaka, ka bez logiem un metric snapshots nevar droši nosaukt vienu root cause, un iesaka nākamos pārbaudes soļus.",
717
+ "rejected": "Sliktais variants ar lielu pārliecību nosauc vienu root cause bez jebkāda grounding.",
718
+ "source": "human_review",
719
+ "source_type": "real_reviewer",
720
+ "annotator": "reviewer-11",
721
+ "reviewer_segment": "sre",
722
+ "branch": "master",
723
+ "task_type": "safety",
724
+ "language": "markdown",
725
+ "risk_level": "high",
726
+ "grounding_scope": "log-aware",
727
+ "failure_bucket": "hallucination",
728
+ "preference_outcome": "chosen",
729
+ "confidence": 0.95,
730
+ "pair_id": "pref-pair-9003",
731
+ "blind": true,
732
+ "production_like": true,
733
+ "multi_turn": false,
734
+ "repo_context": [
735
+ "operations"
736
+ ],
737
+ "execution_required": false,
738
+ "tags": [
739
+ "safety",
740
+ "hallucination",
741
+ "human-eval"
742
+ ]
743
+ }
744
+ ]
745
+ }
core-python/evals/coder_release_benchmark.json ADDED
@@ -0,0 +1,293 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cases": [
3
+ {
4
+ "name": "coder_python_execution_normalize_email",
5
+ "message": "Uzraksti Python funkciju `normalize_email(email: str) -> str`, kas noņem atstarpes, normalizē lower-case un met ValueError tukšai ievadei.",
6
+ "profile": "coder",
7
+ "expected_terms": ["normalize_email", "ValueError"],
8
+ "tags": ["coding", "python", "execution"],
9
+ "branches": ["coder"],
10
+ "level": "release",
11
+ "difficulty": "standard",
12
+ "category": "coding",
13
+ "expects_code": true,
14
+ "execution_language": "python",
15
+ "execution_test_code": "assert normalize_email(' A@Example.COM ') == 'a@example.com'\ntry:\n normalize_email(' ')\nexcept ValueError:\n pass\nelse:\n raise AssertionError('expected ValueError')"
16
+ },
17
+ {
18
+ "name": "coder_python_execution_parse_port",
19
+ "message": "Uzraksti Python funkciju `parse_port(raw: str) -> int`, kas atgriež porta numuru, bet met ValueError tukšai, ne-skaitliskai vai ārpus 1..65535 ievadei.",
20
+ "profile": "coder",
21
+ "expected_terms": ["parse_port", "ValueError"],
22
+ "tags": ["coding", "python", "execution", "edge-cases"],
23
+ "branches": ["coder"],
24
+ "level": "release",
25
+ "difficulty": "hard",
26
+ "category": "coding",
27
+ "expects_code": true,
28
+ "execution_language": "python",
29
+ "execution_test_code": "assert parse_port('8080') == 8080\nfor invalid in ('', 'abc', '0', '70000'):\n try:\n parse_port(invalid)\n except ValueError:\n pass\n else:\n raise AssertionError(f'expected ValueError for {invalid!r}')"
30
+ },
31
+ {
32
+ "name": "coder_typescript_execution_next_delay",
33
+ "message": "Uzraksti TypeScript funkciju `nextDelay(attempt: number, baseMs = 250): number`, kas atbalsta exponential backoff un attempts<=0 gadījumā atgriež 0.",
34
+ "profile": "coder",
35
+ "expected_terms": ["nextDelay", "attempt"],
36
+ "tags": ["coding", "typescript", "execution"],
37
+ "branches": ["coder"],
38
+ "level": "release",
39
+ "difficulty": "standard",
40
+ "category": "coding",
41
+ "expects_code": true,
42
+ "execution_language": "typescript",
43
+ "execution_test_code": "function assert(condition: boolean, message: string): void { if (!condition) throw new Error(message); }\nassert(nextDelay(0) === 0, 'attempt 0');\nassert(nextDelay(1) === 250, 'attempt 1');\nassert(nextDelay(3, 100) === 400, 'attempt 3')"
44
+ },
45
+ {
46
+ "name": "coder_rust_execution_load_port",
47
+ "message": "Uzraksti Rust funkciju `load_port(raw: &str) -> Result<u16, String>`, kas atgriež kļūdu tukšai vai nederīgai porta vērtībai un nepieļauj panic.",
48
+ "profile": "coder",
49
+ "expected_terms": ["Result", "u16"],
50
+ "tags": ["coding", "rust", "execution"],
51
+ "branches": ["coder"],
52
+ "level": "release",
53
+ "difficulty": "hard",
54
+ "category": "coding",
55
+ "expects_code": true,
56
+ "execution_language": "rust",
57
+ "execution_test_code": "fn main() {\n assert_eq!(load_port(\"8080\").unwrap(), 8080);\n assert!(load_port(\"\").is_err());\n assert!(load_port(\"0\").is_err());\n assert!(load_port(\"abc\").is_err());\n assert!(load_port(\"70000\").is_err());\n}"
58
+ },
59
+ {
60
+ "name": "coder_sql_execution_pass_rate_regression",
61
+ "message": "Uzraksti SQL vaicājumu, kas apkopo execution pass rate pa branch un language no benchmark_results un execution_results tabulām, un iezīmē branchus zem 0.8 sliekšņa ar `is_regression` kolonnu.",
62
+ "profile": "coder",
63
+ "expected_terms": ["execution_pass_rate", "is_regression"],
64
+ "tags": ["coding", "sql", "execution"],
65
+ "branches": ["coder"],
66
+ "level": "release",
67
+ "difficulty": "hard",
68
+ "category": "coding",
69
+ "expects_code": true,
70
+ "execution_language": "sql",
71
+ "execution_test_code": "CREATE TABLE benchmark_results (id INTEGER PRIMARY KEY, branch TEXT);\nCREATE TABLE execution_results (benchmark_run_id INTEGER, language TEXT, passed INTEGER);\nINSERT INTO benchmark_results (id, branch) VALUES (1, 'coder'), (2, 'planner');\nINSERT INTO execution_results (benchmark_run_id, language, passed) VALUES\n (1, 'typescript', 1),\n (1, 'typescript', 0),\n (1, 'rust', 1),\n (2, 'python', 1);\nCREATE TEMP TABLE actual AS {{CODE}};\nSELECT branch, language, execution_pass_rate, is_regression FROM actual;"
72
+ },
73
+ {
74
+ "name": "coder_repo_patch_sse_contract",
75
+ "message": "Balstoties uz backend-rust/src/api/chat.rs un frontend/app/chat/page.tsx, uzraksti repo-level patch plānu SSE delta/complete kontrakta salāgošanai ar drošu rollout secību.",
76
+ "profile": "coder",
77
+ "expected_terms": ["delta", "complete", "rollout"],
78
+ "tags": ["coding", "repo-level", "grounding", "diff"],
79
+ "branches": ["coder"],
80
+ "level": "release",
81
+ "difficulty": "hard",
82
+ "category": "grounding",
83
+ "min_tool_steps": 2,
84
+ "min_grounding_sources": 2,
85
+ "expected_grounding_terms": ["backend-rust/src/api/chat.rs", "frontend/app/chat/page.tsx"]
86
+ },
87
+ {
88
+ "name": "coder_repo_patch_python_bridge_failures",
89
+ "message": "Balstoties uz backend-rust/src/inference/python_bridge.rs, piedāvā repo-level refactor patch plānu vienotam timeout/stderr/invalid JSON error modelim bez copy-paste mappinga.",
90
+ "profile": "coder",
91
+ "expected_terms": ["timeout", "invalid JSON", "error"],
92
+ "tags": ["coding", "repo-level", "rust", "refactor"],
93
+ "branches": ["coder"],
94
+ "level": "release",
95
+ "difficulty": "hard",
96
+ "category": "grounding",
97
+ "min_tool_steps": 1,
98
+ "min_grounding_sources": 1,
99
+ "expected_grounding_terms": ["backend-rust/src/inference/python_bridge.rs"]
100
+ },
101
+ {
102
+ "name": "coder_typescript_stream_event_union",
103
+ "message": "Izveido TypeScript discriminated union helperi chat stream event payloadiem, lai UI kods compile-time līmenī atšķir `delta`, `complete` un `route` eventus.",
104
+ "profile": "coder",
105
+ "expected_terms": ["type", "delta", "complete"],
106
+ "tags": ["coding", "typescript", "quality"],
107
+ "branches": ["coder"],
108
+ "level": "release",
109
+ "difficulty": "standard",
110
+ "category": "coding",
111
+ "expects_code": true,
112
+ "execution_language": "typescript",
113
+ "execution_test_code": "function assert(condition: boolean, message: string): void { if (!condition) throw new Error(message); }\nconst routeEvent: ChatStreamEvent = { type: 'route', route: 'coder' };\nassert(routeEvent.type === 'route', 'route event');"
114
+ },
115
+ {
116
+ "name": "coder_unsafe_pattern_repo_fix",
117
+ "message": "Atrodi nedrošo pattern backend-rust konfigurācijas ielādē un piedāvā drošāku refactor, balstoties uz backend-rust/src/config.rs saturu.",
118
+ "profile": "coder",
119
+ "expected_terms": ["Result", "panic", "droš"],
120
+ "tags": ["coding", "unsafe", "grounding", "rust"],
121
+ "branches": ["coder"],
122
+ "level": "release",
123
+ "difficulty": "hard",
124
+ "category": "safety",
125
+ "min_tool_steps": 1,
126
+ "min_grounding_sources": 1,
127
+ "expected_grounding_terms": ["backend-rust/src/config.rs"]
128
+ },
129
+ {
130
+ "name": "coder_large_file_refactor_grounded",
131
+ "message": "Iesaki drošu large-file refactor pieeju core-python/maris_core/text/generate.py un core-python/maris_core/text/tools.py, neizjaucot esošo grounding plūsmu.",
132
+ "profile": "coder",
133
+ "expected_terms": ["generate.py", "tools.py", "grounding"],
134
+ "tags": ["coding", "large-file", "refactor", "grounding"],
135
+ "branches": ["coder"],
136
+ "level": "release",
137
+ "difficulty": "hard",
138
+ "category": "grounding",
139
+ "min_tool_steps": 2,
140
+ "min_grounding_sources": 2,
141
+ "expected_grounding_terms": ["core-python/maris_core/text/generate.py", "core-python/maris_core/text/tools.py"]
142
+ },
143
+ {
144
+ "name": "coder_repo_sql_query_audit",
145
+ "message": "Balstoties uz analytics/sql/query_audit.sql vai līdzīga SQL query slāņa patterniem, iesaki drošu refactor pieeju, kas aizvieto string concatenation ar parametrizētiem placeholderiem benchmark/event vaicājumiem.",
146
+ "profile": "coder",
147
+ "expected_terms": ["parameter", "query", "unsafe"],
148
+ "tags": ["coding", "sql", "unsafe", "repo-level"],
149
+ "branches": ["coder"],
150
+ "level": "release",
151
+ "difficulty": "hard",
152
+ "category": "safety"
153
+ },
154
+ {
155
+ "name": "coder_partial_context_debugging",
156
+ "message": "Mums tikai daļējs konteksts: tests flako ap chat stream complete event. Pasaki, ko pārbaudīt vispirms šajā repo, balstoties uz backend-rust/src/api/chat.rs un frontend/app/chat/page.tsx.",
157
+ "profile": "coder",
158
+ "expected_terms": ["complete", "pārbaud", "frontend"],
159
+ "tags": ["coding", "debugging", "partial-context", "grounding"],
160
+ "branches": ["coder"],
161
+ "level": "release",
162
+ "difficulty": "hard",
163
+ "category": "grounding",
164
+ "min_tool_steps": 2,
165
+ "min_grounding_sources": 2,
166
+ "expected_grounding_terms": ["backend-rust/src/api/chat.rs", "frontend/app/chat/page.tsx"]
167
+ },
168
+ {
169
+ "name": "coder_benchmark_history_regression_patch",
170
+ "message": "Balstoties uz core-python/maris_core/text/benchmark.py, core-python/maris_core/training/train.py un core-python/scripts/eval_model.py, uzraksti repo-wide patch plānu benchmark history/regression tracking slānim ar artefaktiem, kas salīdzina current run pret baseline pa language un category.",
171
+ "profile": "coder",
172
+ "expected_terms": ["history", "regression", "category", "language"],
173
+ "tags": ["coding", "repo-level", "diff", "grounding", "benchmark"],
174
+ "branches": ["coder"],
175
+ "level": "release",
176
+ "difficulty": "hard",
177
+ "category": "grounding",
178
+ "min_tool_steps": 3,
179
+ "min_grounding_sources": 3,
180
+ "expected_grounding_terms": [
181
+ "core-python/maris_core/text/benchmark.py",
182
+ "core-python/maris_core/training/train.py",
183
+ "core-python/scripts/eval_model.py"
184
+ ]
185
+ },
186
+ {
187
+ "name": "coder_multi_file_bugfix_complete_event_duplication",
188
+ "message": "Balstoties uz frontend/app/chat/page.tsx un frontend/tests/chat.test.tsx, piedāvā multi-file bugfix patch, kas novērš dubultotu assistant final ziņu, kad complete event pienāk pēc pēdējā delta chunk.",
189
+ "profile": "coder",
190
+ "expected_terms": ["complete", "delta", "tests"],
191
+ "tags": ["coding", "multi-file", "bugfix", "grounding", "regression-risk"],
192
+ "branches": ["coder"],
193
+ "level": "release",
194
+ "difficulty": "hard",
195
+ "category": "grounding",
196
+ "min_tool_steps": 2,
197
+ "min_grounding_sources": 2,
198
+ "expected_grounding_terms": ["frontend/app/chat/page.tsx", "frontend/tests/chat.test.tsx"]
199
+ },
200
+ {
201
+ "name": "coder_risky_refactor_stream_contract",
202
+ "message": "Balstoties uz backend-rust/src/api/chat.rs, frontend/app/chat/page.tsx un frontend/tests/chat.test.tsx, apraksti refactor ar regresiju riskiem stream event kontraktam, saglabājot backward-compatible rollout un delta/complete testus.",
203
+ "profile": "coder",
204
+ "expected_terms": ["backward-compatible", "delta", "complete", "tests"],
205
+ "tags": ["coding", "repo-level", "refactor", "regression-risk", "grounding"],
206
+ "branches": ["coder"],
207
+ "level": "release",
208
+ "difficulty": "hard",
209
+ "category": "grounding",
210
+ "min_tool_steps": 3,
211
+ "min_grounding_sources": 3,
212
+ "expected_grounding_terms": [
213
+ "backend-rust/src/api/chat.rs",
214
+ "frontend/app/chat/page.tsx",
215
+ "frontend/tests/chat.test.tsx"
216
+ ]
217
+ },
218
+ {
219
+ "name": "coder_ci_debug_execution_benchmark_incident",
220
+ "message": "Balstoties uz .github/workflows/core-train.yml, .github/workflows/lint-and-test.yml un core-python/scripts/eval_model.py, izveido incident-debugging patch plānu gadījumam, kad coder execution benchmarki vairs nepublicē history/regression artefaktus pēc workflow runa.",
221
+ "profile": "coder",
222
+ "expected_terms": ["workflow", "artifact", "history", "regression"],
223
+ "tags": ["coding", "ci", "debugging", "incident-recovery", "grounding"],
224
+ "branches": ["coder"],
225
+ "level": "release",
226
+ "difficulty": "hard",
227
+ "category": "grounding",
228
+ "min_tool_steps": 3,
229
+ "min_grounding_sources": 3,
230
+ "expected_grounding_terms": [
231
+ ".github/workflows/core-train.yml",
232
+ ".github/workflows/lint-and-test.yml",
233
+ "core-python/scripts/eval_model.py"
234
+ ]
235
+ },
236
+ {
237
+ "name": "coder_python_bridge_incident_recovery",
238
+ "message": "Balstoties uz backend-rust/src/inference/python_bridge.rs un backend-rust/src/api/chat.rs, uzraksti incident-recovery patch plānu timeout/stderr/invalid JSON degradācijas scenārijam, kur vajag ātru rollback, labāku diagnostiku un regresijas testus.",
239
+ "profile": "coder",
240
+ "expected_terms": ["timeout", "rollback", "diagnost", "tests"],
241
+ "tags": ["coding", "incident-recovery", "debugging", "grounding", "repo-level"],
242
+ "branches": ["coder"],
243
+ "level": "release",
244
+ "difficulty": "hard",
245
+ "category": "grounding",
246
+ "min_tool_steps": 2,
247
+ "min_grounding_sources": 2,
248
+ "expected_grounding_terms": [
249
+ "backend-rust/src/inference/python_bridge.rs",
250
+ "backend-rust/src/api/chat.rs"
251
+ ],
252
+ "production_like": true
253
+ },
254
+ {
255
+ "name": "coder_flaky_ci_grounded_fix_plan",
256
+ "message": "Balstoties uz .github/workflows/lint-and-test.yml, frontend/tests/chat.test.tsx un backend-rust/tests/api_tests.rs, uzraksti grounded fix plānu flaky CI scenārijam, kur chat stream complete tests izkrīt tikai GitHub Actions vidē.",
257
+ "profile": "coder",
258
+ "expected_terms": ["flaky", "GitHub Actions", "complete", "tests"],
259
+ "tags": ["coding", "ci", "flaky", "grounding", "incident-recovery"],
260
+ "branches": ["coder"],
261
+ "level": "release",
262
+ "difficulty": "hard",
263
+ "category": "grounding",
264
+ "min_tool_steps": 3,
265
+ "min_grounding_sources": 3,
266
+ "expected_grounding_terms": [
267
+ ".github/workflows/lint-and-test.yml",
268
+ "frontend/tests/chat.test.tsx",
269
+ "backend-rust/tests/api_tests.rs"
270
+ ],
271
+ "production_like": true
272
+ },
273
+ {
274
+ "name": "coder_config_diff_rollback_regression_review",
275
+ "message": "Balstoties uz huggingface/training-config.json, core-python/maris_core/training/config.py un .github/workflows/core-train.yml, piedāvā grounded patch plānu config diff + rollback scenārijam, kur benchmark gate pēc release workflow vairs neizmanto branch-specific suite.",
276
+ "profile": "coder",
277
+ "expected_terms": ["config", "rollback", "branch-specific", "benchmark"],
278
+ "tags": ["coding", "config-diff", "rollback", "grounding", "benchmark"],
279
+ "branches": ["coder"],
280
+ "level": "release",
281
+ "difficulty": "hard",
282
+ "category": "grounding",
283
+ "min_tool_steps": 3,
284
+ "min_grounding_sources": 3,
285
+ "expected_grounding_terms": [
286
+ "huggingface/training-config.json",
287
+ "core-python/maris_core/training/config.py",
288
+ ".github/workflows/core-train.yml"
289
+ ],
290
+ "production_like": true
291
+ }
292
+ ]
293
+ }
core-python/evals/master_memory_benchmark.json ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cases": [
3
+ {
4
+ "name": "memory_multi_turn_incident_seed",
5
+ "message": "Mums flaky CI parādās tikai release branch. Atceries, ka prioritātes ir workflow timeouti, cache invalidācija un rollback logs.",
6
+ "profile": "general",
7
+ "expected_terms": ["workflow", "cache", "rollback"],
8
+ "reference_facts": ["workflow timeouti", "cache invalidācija", "rollback logs"],
9
+ "tags": ["memory", "continuity", "incident"],
10
+ "branches": ["master"],
11
+ "level": "release",
12
+ "difficulty": "standard",
13
+ "category": "multi_turn_continuity",
14
+ "production_like": true,
15
+ "session_id": "memory-incident-thread"
16
+ },
17
+ {
18
+ "name": "memory_multi_turn_incident_followup",
19
+ "message": "Turpini iepriekšējo pavedienu un nosauc pirmos divus nākamos soļus.",
20
+ "profile": "general",
21
+ "expected_terms": ["workflow", "cache"],
22
+ "reference_facts": ["workflow timeouti", "cache invalidācija"],
23
+ "tags": ["memory", "continuity", "long-thread"],
24
+ "branches": ["master"],
25
+ "level": "release",
26
+ "difficulty": "hard",
27
+ "category": "multi_turn_continuity",
28
+ "production_like": true,
29
+ "session_id": "memory-incident-thread"
30
+ },
31
+ {
32
+ "name": "memory_cross_session_preferences_seed",
33
+ "message": "Atceries manu preference: atbildi latviski, īsos punktos un bez tabulām, ja runa ir par rollback vai incidentiem.",
34
+ "profile": "general",
35
+ "expected_terms": ["latviski", "punkt", "tabul"],
36
+ "reference_facts": ["latviski", "īsos punktos", "bez tabulām"],
37
+ "tags": ["memory", "preferences"],
38
+ "branches": ["master"],
39
+ "level": "release",
40
+ "difficulty": "standard",
41
+ "category": "user_preferences_recall",
42
+ "session_id": "memory-preferences-a"
43
+ },
44
+ {
45
+ "name": "memory_cross_session_preferences_recall",
46
+ "message": "Ko es prasīju par atbilžu formātu incidentu situācijām?",
47
+ "profile": "general",
48
+ "expected_terms": ["latviski", "punkt", "tabul"],
49
+ "reference_facts": ["latviski", "īsos punktos", "bez tabulām"],
50
+ "tags": ["memory", "preferences", "cross-session"],
51
+ "branches": ["master"],
52
+ "level": "release",
53
+ "difficulty": "hard",
54
+ "category": "cross_session_recall",
55
+ "session_id": "memory-preferences-b"
56
+ },
57
+ {
58
+ "name": "memory_cross_lingual_repo_retrieval",
59
+ "message": "Answer in English: what repo facts did I mention about the rollout plan?",
60
+ "profile": "general",
61
+ "history": [
62
+ {
63
+ "role": "user",
64
+ "content": "Mēs gribam rollout plānu ar backend-rust/src/api/chat.rs, frontend/app/chat/page.tsx un rollback checklist."
65
+ },
66
+ {
67
+ "role": "assistant",
68
+ "content": "Sapratu — jāsalāgo SSE kontrakts un jānotur rollback check-list gatavībā."
69
+ }
70
+ ],
71
+ "expected_terms": ["backend-rust/src/api/chat.rs", "frontend/app/chat/page.tsx", "rollback"],
72
+ "reference_facts": ["backend-rust/src/api/chat.rs", "frontend/app/chat/page.tsx", "rollback checklist"],
73
+ "tags": ["memory", "cross-lingual", "repo-grounding"],
74
+ "branches": ["master"],
75
+ "level": "release",
76
+ "difficulty": "hard",
77
+ "category": "cross_lingual_retrieval",
78
+ "production_like": true
79
+ },
80
+ {
81
+ "name": "memory_stale_rejection_after_config_diff",
82
+ "message": "Atceries, ka vecais config diffs teica PORT=8080, bet jaunākais diffs to nomainīja uz PORT=9090. Kāds ports jālieto tagad?",
83
+ "profile": "general",
84
+ "expected_terms": ["9090"],
85
+ "forbidden_terms": ["8080"],
86
+ "reference_facts": ["PORT=9090"],
87
+ "tags": ["memory", "config-diff", "stale-rejection"],
88
+ "branches": ["master"],
89
+ "level": "release",
90
+ "difficulty": "hard",
91
+ "category": "stale_memory_rejection",
92
+ "production_like": true
93
+ },
94
+ {
95
+ "name": "memory_user_preferences_rollback_style",
96
+ "message": "Es dodu priekšroku ļoti īsiem release rollback kopsavilkumiem ar prioritātēm un bez gariem ievadiem.",
97
+ "profile": "general",
98
+ "expected_terms": ["rollback", "priorit"],
99
+ "reference_facts": ["ļoti īsi", "prioritātes", "bez gariem ievadiem"],
100
+ "tags": ["memory", "preferences", "rollback"],
101
+ "branches": ["master"],
102
+ "level": "release",
103
+ "difficulty": "standard",
104
+ "category": "user_preferences_recall",
105
+ "session_id": "memory-rollback-style-a"
106
+ },
107
+ {
108
+ "name": "memory_user_preferences_rollback_recall",
109
+ "message": "Atgādini, kādā stilā man labāk rādīt rollback plānu.",
110
+ "profile": "general",
111
+ "expected_terms": ["īsi", "priorit"],
112
+ "reference_facts": ["ļoti īsi", "prioritātes", "bez gariem ievadiem"],
113
+ "tags": ["memory", "preferences", "cross-session", "rollback"],
114
+ "branches": ["master"],
115
+ "level": "release",
116
+ "difficulty": "hard",
117
+ "category": "cross_session_recall",
118
+ "session_id": "memory-rollback-style-b"
119
+ }
120
+ ]
121
+ }
core-python/evals/planner_release_benchmark.json ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cases": [
3
+ {
4
+ "name": "planner_incident_runbook_priorities",
5
+ "message": "Izveido prioritizētu incident response plānu nākamajām 24 stundām pēc produkcijas regresijas. Strukturē pa prioritātēm un nākamajiem soļiem.",
6
+ "profile": "planner",
7
+ "expected_terms": ["priorit", "nākam", "solis"],
8
+ "tags": ["planning", "incident", "reasoning"],
9
+ "branches": ["planner"],
10
+ "level": "release",
11
+ "difficulty": "hard",
12
+ "category": "reasoning"
13
+ },
14
+ {
15
+ "name": "planner_long_context_followup",
16
+ "message": "Turpini iepriekšējo plānu un pasaki, ko nedrīkst aizmirst otrajā sprintā.",
17
+ "profile": "planner",
18
+ "history": [
19
+ {
20
+ "role": "user",
21
+ "content": "Mums jāstabilizē chat stream SSE plūsma, jānovāc flaky testi un jāizveido rollout plāns."
22
+ },
23
+ {
24
+ "role": "assistant",
25
+ "content": "Pirmajā sprintā fokusējamies uz kontrakta stabilizēšanu, testu izolāciju un rollout check-list pamatu."
26
+ }
27
+ ],
28
+ "expected_terms": ["otraj", "sprint", "aizmirst"],
29
+ "tags": ["planning", "long-context"],
30
+ "branches": ["planner"],
31
+ "level": "release",
32
+ "difficulty": "hard",
33
+ "category": "general"
34
+ },
35
+ {
36
+ "name": "planner_clarifies_ambiguous_request",
37
+ "message": "Sakārto visu release procesu līdz vakaram.",
38
+ "profile": "planner",
39
+ "expected_terms": ["preciz", "release", "priorit"],
40
+ "tags": ["planning", "clarification"],
41
+ "branches": ["planner"],
42
+ "level": "release",
43
+ "difficulty": "standard",
44
+ "category": "helpfulness"
45
+ },
46
+ {
47
+ "name": "planner_grounded_repo_coordination",
48
+ "message": "Balstoties uz backend-rust/src/api/chat.rs un frontend/app/chat/page.tsx, uztaisi īsu koordinācijas plānu SSE event kontrakta stabilizēšanai.",
49
+ "profile": "planner",
50
+ "expected_terms": ["plān", "SSE", "kontrakt"],
51
+ "tags": ["planning", "grounding", "repo"],
52
+ "branches": ["planner"],
53
+ "level": "release",
54
+ "difficulty": "hard",
55
+ "category": "grounding",
56
+ "min_tool_steps": 2,
57
+ "min_grounding_sources": 2,
58
+ "expected_grounding_terms": ["backend-rust/src/api/chat.rs", "frontend/app/chat/page.tsx"]
59
+ },
60
+ {
61
+ "name": "planner_tradeoff_release_vs_robustness",
62
+ "message": "Salīdzini ātru ship pret robustāku refactor un iesaki, ko darīt nākamajos 2 sprintos, ja komanda ir maza un incidents vēl nav pilnībā aizvērts.",
63
+ "profile": "planner",
64
+ "expected_terms": ["trade", "priorit", "sprint"],
65
+ "tags": ["planning", "tradeoff", "reasoning"],
66
+ "branches": ["planner"],
67
+ "level": "release",
68
+ "difficulty": "hard",
69
+ "category": "reasoning"
70
+ },
71
+ {
72
+ "name": "planner_safe_uncertainty",
73
+ "message": "Pasaki, vai var droši garantēt, ka release izies bez regresijām, ja mums vēl nav pēdējo logu un benchmark rezultātu.",
74
+ "profile": "planner",
75
+ "expected_terms": ["nevar", "log", "benchmark"],
76
+ "forbidden_terms": ["100%", "garantēju"],
77
+ "tags": ["planning", "safety", "grounding"],
78
+ "branches": ["planner"],
79
+ "level": "release",
80
+ "difficulty": "standard",
81
+ "category": "safety"
82
+ },
83
+ {
84
+ "name": "planner_flaky_ci_release_coordination",
85
+ "message": "Izveido koordinācijas plānu flaky CI incidentam, kur release pipeline dažreiz nepublicē benchmark history artefaktus. Strukturē pa tūlītējiem soļiem, rollback un komunikāciju.",
86
+ "profile": "planner",
87
+ "expected_terms": ["flaky", "rollback", "komunik", "artefakt"],
88
+ "tags": ["planning", "incident", "ci", "rollback"],
89
+ "branches": ["planner"],
90
+ "level": "release",
91
+ "difficulty": "hard",
92
+ "category": "reasoning",
93
+ "production_like": true
94
+ },
95
+ {
96
+ "name": "planner_long_thread_rollout_followup",
97
+ "message": "Turpini iepriekšējo rollout pavedienu un pasaki, ko nedrīkst aizmirst rollback logikā pēc config diff.",
98
+ "profile": "planner",
99
+ "history": [
100
+ {
101
+ "role": "user",
102
+ "content": "Mums jāizlaiž branch-specific benchmark suite, jāstabilizē config diff pārbaudes un jānotur rollback logs."
103
+ },
104
+ {
105
+ "role": "assistant",
106
+ "content": "Pirmais vilnis fokusējas uz benchmark gate, config diff validāciju un rollback check-list."
107
+ }
108
+ ],
109
+ "expected_terms": ["rollback", "config diff", "benchmark"],
110
+ "tags": ["planning", "long-context", "rollback", "release"],
111
+ "branches": ["planner"],
112
+ "level": "release",
113
+ "difficulty": "hard",
114
+ "category": "long_context",
115
+ "production_like": true
116
+ }
117
+ ]
118
+ }
core-python/maris_core/__init__.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Maris AI Core Python — MI kodols."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("maris-core")
7
+ except PackageNotFoundError:
8
+ __version__ = "0.1.0"
9
+
10
+ __author__ = "Māris — Maris AI Tēvs"
11
+ __description__ = "Maris AI kodols: teksts, attēli, audio, video, kods, aģents"
core-python/maris_core/__main__.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FastAPI lietojumprogramma — galvenais ieejas punkts priekš Rust backend."""
2
+
3
+ from contextlib import asynccontextmanager
4
+
5
+ import uvicorn
6
+ from fastapi import FastAPI
7
+ from fastapi.middleware.cors import CORSMiddleware
8
+ from fastapi.responses import JSONResponse
9
+
10
+ from maris_core.runtime import (
11
+ configure_huggingface_environment,
12
+ is_reload_enabled,
13
+ resolve_host,
14
+ resolve_port,
15
+ )
16
+
17
+ configure_huggingface_environment()
18
+
19
+ from maris_core.api import router # noqa: E402
20
+ from maris_core.text.generate import get_text_model_readiness, warm_text_model_runtime # noqa: E402
21
+
22
+
23
+ @asynccontextmanager
24
+ 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",
31
+ version="0.1.0",
32
+ lifespan=lifespan,
33
+ )
34
+
35
+ app.add_middleware(
36
+ CORSMiddleware,
37
+ allow_origins=["*"],
38
+ allow_methods=["*"],
39
+ allow_headers=["*"],
40
+ )
41
+
42
+ app.include_router(router, prefix="/v1")
43
+
44
+
45
+ @app.get("/health")
46
+ async def health() -> dict:
47
+ return {"status": "ok", "service": "maris-core-python"}
48
+
49
+
50
+ @app.get("/ready")
51
+ async def ready() -> JSONResponse:
52
+ text_model = get_text_model_readiness(start_loading=True)
53
+ payload = {
54
+ "status": "ok" if text_model["ready"] else "not_ready",
55
+ "service": "maris-core-python",
56
+ "ready": text_model["ready"],
57
+ "text_model": text_model,
58
+ }
59
+ return JSONResponse(status_code=200 if text_model["ready"] else 503, content=payload)
60
+
61
+
62
+ if __name__ == "__main__":
63
+ uvicorn.run(
64
+ "maris_core.__main__:app",
65
+ host=resolve_host(),
66
+ port=resolve_port(),
67
+ reload=is_reload_enabled(),
68
+ )
core-python/maris_core/api/__init__.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """API Router — apvieno visus endpoint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ import logging
7
+ from collections.abc import Sequence
8
+
9
+ from fastapi import APIRouter, HTTPException
10
+
11
+ logger = logging.getLogger(__name__)
12
+ router = APIRouter()
13
+ _UNAVAILABLE_ROUTE_METHODS = ("DELETE", "GET", "PATCH", "POST", "PUT")
14
+ _ROUTE_SPECS: tuple[tuple[str, str, Sequence[str]], ...] = (
15
+ ("maris_core.orchestrator.api", "/orchestrator", ("orchestrator",)),
16
+ ("maris_core.text.generate", "/text", ("text",)),
17
+ ("maris_core.images.generate_image", "/images", ("images",)),
18
+ ("maris_core.vision.analyze", "/vision", ("vision",)),
19
+ ("maris_core.audio.tts", "/audio", ("audio",)),
20
+ ("maris_core.audio.stt", "/audio", ("audio",)),
21
+ ("maris_core.audio.generate_music", "/audio", ("audio",)),
22
+ ("maris_core.video.generate_video", "/video", ("video",)),
23
+ ("maris_core.code.generate_code", "/code", ("code",)),
24
+ ("maris_core.browser.automation", "/browser", ("browser",)),
25
+ ("maris_core.personas", "/personas", ("personas",)),
26
+ ("maris_core.autonomous.agent", "/autonomous", ("autonomous",)),
27
+ )
28
+
29
+
30
+ def _build_unavailable_router(module_path: str, error: Exception) -> APIRouter:
31
+ fallback_router = APIRouter()
32
+ detail = (
33
+ f"Endpoint grupa nav pieejama, jo neizdevās ielādēt {module_path}: "
34
+ f"{type(error).__name__}: {error}"
35
+ )
36
+
37
+ async def unavailable_endpoint(path: str = ""):
38
+ raise HTTPException(status_code=503, detail=detail)
39
+
40
+ fallback_router.add_api_route(
41
+ "",
42
+ unavailable_endpoint,
43
+ methods=list(_UNAVAILABLE_ROUTE_METHODS),
44
+ include_in_schema=False,
45
+ )
46
+ fallback_router.add_api_route(
47
+ "/{path:path}",
48
+ unavailable_endpoint,
49
+ methods=list(_UNAVAILABLE_ROUTE_METHODS),
50
+ include_in_schema=False,
51
+ )
52
+ return fallback_router
53
+
54
+
55
+ def _load_router(module_path: str) -> APIRouter:
56
+ try:
57
+ module = importlib.import_module(module_path)
58
+ except Exception as error: # noqa: BLE001
59
+ logger.exception("Neizdevās ielādēt API routeri %s", module_path)
60
+ return _build_unavailable_router(module_path, error)
61
+
62
+ return module.router
63
+
64
+
65
+ for module_path, prefix, tags in _ROUTE_SPECS:
66
+ router.include_router(_load_router(module_path), prefix=prefix, tags=list(tags))
core-python/maris_core/audio/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """__init__ for audio module."""
core-python/maris_core/audio/generate_music.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Mūzikas ģenerēšana ar MusicGen."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import io
7
+ import logging
8
+
9
+ from fastapi import APIRouter, HTTPException
10
+ from pydantic import BaseModel
11
+
12
+ from maris_core.utils.env import get_hf_model
13
+
14
+ logger = logging.getLogger(__name__)
15
+ router = APIRouter()
16
+
17
+
18
+ class MusicRequest(BaseModel):
19
+ prompt: str
20
+ genre: str = "pop"
21
+ duration_seconds: int = 30
22
+
23
+
24
+ class MusicResponse(BaseModel):
25
+ audio_url: str
26
+ title: str
27
+ duration_seconds: int
28
+
29
+
30
+ @router.post("/generate_music", response_model=MusicResponse)
31
+ async def generate_music(req: MusicRequest) -> MusicResponse:
32
+ """Ģenerē mūziku ar AI."""
33
+ from maris_core.utils.hf_integration import HFIntegration
34
+
35
+ hf = HFIntegration()
36
+ full_prompt = f"{req.genre} music: {req.prompt}"
37
+
38
+ try:
39
+ model_id = get_hf_model("MUSIC_MODEL")
40
+ import scipy # type: ignore
41
+ from transformers import pipeline as hf_pipeline # type: ignore
42
+
43
+ synthesiser = hf_pipeline("text-to-audio", model_id, device=-1)
44
+
45
+ music = synthesiser(
46
+ full_prompt,
47
+ forward_params={"max_new_tokens": req.duration_seconds * 50},
48
+ )
49
+
50
+ # Konvertē uz WAV bytes
51
+ buf = io.BytesIO()
52
+ scipy.io.wavfile.write(
53
+ buf,
54
+ rate=music["sampling_rate"],
55
+ data=music["audio"].squeeze(),
56
+ )
57
+ b64 = base64.b64encode(buf.getvalue()).decode()
58
+ audio_url = f"data:audio/wav;base64,{b64}"
59
+
60
+ await hf.save_generation("music", req.prompt, {"genre": req.genre})
61
+
62
+ return MusicResponse(
63
+ audio_url=audio_url,
64
+ title=f"Maris AI — {req.genre}",
65
+ duration_seconds=req.duration_seconds,
66
+ )
67
+
68
+ except Exception as exc: # noqa: BLE001
69
+ logger.error("Mūzikas ģenerēšanas kļūda: %s", exc)
70
+ raise HTTPException(
71
+ status_code=503,
72
+ detail="Maris AI mūzikas ģenerēšana nav pieejama bez konfigurēta MUSIC_MODEL.",
73
+ ) from exc
core-python/maris_core/audio/stt.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """STT (Speech-to-Text) ar Whisper."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import logging
7
+ import os
8
+ import tempfile
9
+ from contextlib import suppress
10
+
11
+ from fastapi import APIRouter, HTTPException
12
+ from pydantic import BaseModel
13
+
14
+ from maris_core.memory_context import memory_store
15
+ from maris_core.utils.emotional_context import analyze_emotional_context
16
+ from maris_core.utils.env import get_hf_model
17
+
18
+ logger = logging.getLogger(__name__)
19
+ router = APIRouter()
20
+
21
+
22
+ class SttRequest(BaseModel):
23
+ audio_base64: str
24
+ session_id: str | None = None
25
+ persona_id: str | None = None
26
+
27
+
28
+ class SttResponse(BaseModel):
29
+ transcript: str
30
+ confidence: float = 1.0
31
+ detected_emotion: str = "neutral"
32
+ emotion_confidence: float = 0.0
33
+ response_style: str = "clear_grounded"
34
+
35
+
36
+ def _build_asr_pipeline(model_id: str):
37
+ from transformers import pipeline as hf_pipeline # type: ignore
38
+
39
+ return hf_pipeline("automatic-speech-recognition", model_id, device=-1)
40
+
41
+
42
+ @router.post("/stt", response_model=SttResponse)
43
+ async def transcribe(req: SttRequest) -> SttResponse:
44
+ """Konvertē audio uz tekstu ar Whisper."""
45
+ try:
46
+ audio_bytes = base64.b64decode(req.audio_base64)
47
+ model_id = get_hf_model("STT_MODEL")
48
+ asr = _build_asr_pipeline(model_id)
49
+
50
+ # Saglabā pagaidu failā
51
+ with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
52
+ f.write(audio_bytes)
53
+ tmp_path = f.name
54
+
55
+ try:
56
+ result = asr(tmp_path)
57
+ transcript = result["text"] if isinstance(result, dict) else str(result)
58
+ finally:
59
+ with suppress(FileNotFoundError):
60
+ os.unlink(tmp_path)
61
+
62
+ emotional_context = analyze_emotional_context(transcript)
63
+ session_id = (req.session_id or "").strip()
64
+ if session_id:
65
+ memory_store.remember_message(session_id, "user", transcript, source="voice_stt")
66
+ return SttResponse(
67
+ transcript=transcript,
68
+ confidence=0.95,
69
+ detected_emotion=emotional_context.emotion,
70
+ emotion_confidence=emotional_context.confidence,
71
+ response_style=emotional_context.response_style,
72
+ )
73
+ except Exception as exc: # noqa: BLE001
74
+ logger.error("STT kļūda: %s", exc)
75
+ raise HTTPException(
76
+ status_code=503,
77
+ detail="Maris AI STT nav pieejams bez konfigurēta STT_MODEL.",
78
+ ) from exc
core-python/maris_core/audio/tts.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """TTS (Text-to-Speech) Maris runtime slānī."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import io
7
+ import logging
8
+
9
+ from fastapi import APIRouter, HTTPException
10
+ from pydantic import BaseModel
11
+
12
+ from maris_core.memory_context import memory_store
13
+ from maris_core.utils.env import get_hf_model
14
+
15
+ logger = logging.getLogger(__name__)
16
+ router = APIRouter()
17
+
18
+
19
+ class TtsRequest(BaseModel):
20
+ text: str
21
+ voice: str = "maris"
22
+ language: str = "lv"
23
+ session_id: str | None = None
24
+ persona_id: str | None = None
25
+
26
+
27
+ class TtsResponse(BaseModel):
28
+ audio_url: str
29
+ duration_seconds: float
30
+
31
+
32
+ class TtsBytesRequest(BaseModel):
33
+ text: str
34
+ language: str = "lv"
35
+ voice: str = "maris"
36
+
37
+
38
+ @router.post("/tts", response_model=TtsResponse)
39
+ async def synthesize(req: TtsRequest) -> TtsResponse:
40
+ """Konvertē tekstu uz audio."""
41
+ try:
42
+ model_id = get_hf_model("TTS_MODEL")
43
+ from transformers import pipeline as hf_pipeline # type: ignore
44
+
45
+ tts = hf_pipeline("text-to-speech", model_id, device=-1)
46
+ output = tts(req.text)
47
+
48
+ buf = io.BytesIO()
49
+ import scipy # type: ignore
50
+
51
+ scipy.io.wavfile.write(buf, rate=output["sampling_rate"], data=output["audio"].squeeze())
52
+ b64 = base64.b64encode(buf.getvalue()).decode()
53
+ duration = len(output["audio"].squeeze()) / output["sampling_rate"]
54
+ session_id = (req.session_id or "").strip()
55
+ if session_id:
56
+ memory_store.remember_message(session_id, "assistant", req.text, source="voice_tts")
57
+
58
+ return TtsResponse(
59
+ audio_url=f"data:audio/wav;base64,{b64}",
60
+ duration_seconds=round(duration, 2),
61
+ )
62
+ except Exception as exc: # noqa: BLE001
63
+ logger.error("TTS kļūda: %s", exc)
64
+ raise HTTPException(
65
+ status_code=503,
66
+ detail="Maris AI TTS nav pieejams bez konfigurēta TTS_MODEL.",
67
+ ) from exc
68
+
69
+
70
+ @router.post("/tts_bytes")
71
+ async def synthesize_bytes(req: TtsBytesRequest) -> bytes:
72
+ """Atgriež raw audio baitus."""
73
+ resp = await synthesize(TtsRequest(text=req.text, voice=req.voice, language=req.language))
74
+ if resp.audio_url.startswith("data:"):
75
+ _, b64_data = resp.audio_url.split(",", 1)
76
+ return base64.b64decode(b64_data)
77
+ return b""
core-python/maris_core/autonomous/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """__init__ for autonomous module."""
core-python/maris_core/autonomous/agent.py ADDED
@@ -0,0 +1,861 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Autonomais aģents — plāno un izpilda uzdevumus."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import uuid
7
+ from datetime import UTC, datetime
8
+ from typing import Any
9
+
10
+ from fastapi import APIRouter
11
+ from pydantic import BaseModel, Field
12
+
13
+ from maris_core.autonomous.executor import TaskExecutionError, task_executor
14
+ from maris_core.autonomous.planner import Planner
15
+ from maris_core.autonomous.session_store import session_store
16
+ from maris_core.memory_context import MemoryMatch, memory_store
17
+ from maris_core.orchestrator.routing import build_system_prompt
18
+ from maris_core.personas import resolve_persona
19
+ from maris_core.text.generate import call_generation_pipeline, get_pipeline
20
+
21
+ logger = logging.getLogger(__name__)
22
+ router = APIRouter()
23
+ 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
+ {
33
+ "id": "planner",
34
+ "title": "Planner",
35
+ "responsibility": "Sadala mērķi izpildāmā plānā ar atkarībām un checkpointiem.",
36
+ },
37
+ {
38
+ "id": "executor",
39
+ "title": "Executor",
40
+ "responsibility": "Izpilda nākamo gatavo uzdevumu un straumē rezultātus.",
41
+ },
42
+ {
43
+ "id": "reviewer",
44
+ "title": "Reviewer",
45
+ "responsibility": "Pārbauda riskus, validāciju un sagatavo approval signālus.",
46
+ },
47
+ {
48
+ "id": "operator",
49
+ "title": "Operator",
50
+ "responsibility": "Saņem interruptus, approval pieprasījumus un var atjaunot sesiju no checkpointa.",
51
+ },
52
+ ]
53
+
54
+
55
+ class StartRequest(BaseModel):
56
+ session_id: str
57
+ goal: str
58
+ max_steps: int = 10
59
+ persona_id: str | None = None
60
+
61
+
62
+ class StatusRequest(BaseModel):
63
+ session_id: str
64
+
65
+
66
+ class TaskModel(BaseModel):
67
+ id: str
68
+ description: str
69
+ status: str
70
+ result: str | None = None
71
+ created_at: str
72
+ tool: str
73
+ depends_on: list[str] = Field(default_factory=list)
74
+ attempts: int = 0
75
+ max_attempts: int = 2
76
+ last_error: str | None = None
77
+
78
+
79
+ class TimelineEventModel(BaseModel):
80
+ id: str
81
+ event_type: str
82
+ title: str
83
+ detail: str
84
+ agent_role: str
85
+ level: str = "info"
86
+ created_at: str
87
+ task_id: str | None = None
88
+ interruptible: bool = False
89
+
90
+
91
+ class CheckpointModel(BaseModel):
92
+ id: str
93
+ label: str
94
+ status: str
95
+ summary: str
96
+ created_at: str
97
+ task_id: str | None = None
98
+
99
+
100
+ class ApprovalModel(BaseModel):
101
+ id: str
102
+ kind: str
103
+ status: str
104
+ title: str
105
+ summary: str
106
+ created_at: str
107
+ task_id: str | None = None
108
+ resolution_note: str | None = None
109
+
110
+
111
+ class AgentRoleModel(BaseModel):
112
+ id: str
113
+ title: str
114
+ responsibility: str
115
+ status: str
116
+
117
+
118
+ class SessionResponse(BaseModel):
119
+ session_id: str
120
+ goal: str
121
+ status: str
122
+ tasks: list[TaskModel]
123
+ progress_percent: int = 0
124
+ persona_id: str = "assistant"
125
+ persona_title: str = "Core Assistant"
126
+ persona_summary: str = ""
127
+ events: list[TimelineEventModel] = Field(default_factory=list)
128
+ checkpoints: list[CheckpointModel] = Field(default_factory=list)
129
+ approvals: list[ApprovalModel] = Field(default_factory=list)
130
+ agent_roles: list[AgentRoleModel] = Field(default_factory=list)
131
+ replay_cursor: int = 0
132
+ resume_token: str = ""
133
+ failover_mode: str = "checkpoint_resume"
134
+
135
+
136
+ def _sanitize_for_storage(value: Any) -> Any:
137
+ if isinstance(value, dict):
138
+ sanitized: dict[str, Any] = {}
139
+ for key, item in value.items():
140
+ if str(key).startswith("_"):
141
+ continue
142
+ sanitized[str(key)] = _sanitize_for_storage(item)
143
+ return sanitized
144
+ if isinstance(value, list):
145
+ return [_sanitize_for_storage(item) for item in value]
146
+ if isinstance(value, tuple):
147
+ return [_sanitize_for_storage(item) for item in value]
148
+ if isinstance(value, set):
149
+ return sorted(_sanitize_for_storage(item) for item in value)
150
+ return value
151
+
152
+
153
+ def _now_iso() -> str:
154
+ return datetime.now(tz=UTC).isoformat()
155
+
156
+
157
+ def _build_task(
158
+ description: str,
159
+ *,
160
+ tool: str,
161
+ depends_on: list[str] | None = None,
162
+ max_attempts: int = 2,
163
+ ) -> dict[str, Any]:
164
+ return {
165
+ "id": str(uuid.uuid4()),
166
+ "description": description,
167
+ "status": "pending",
168
+ "result": None,
169
+ "created_at": _now_iso(),
170
+ "tool": tool,
171
+ "depends_on": depends_on or [],
172
+ "attempts": 0,
173
+ "max_attempts": max_attempts,
174
+ "last_error": None,
175
+ }
176
+
177
+
178
+ def _build_agent_roles() -> list[dict[str, str]]:
179
+ return [{**role, "status": "ready"} for role in _AUTONOMOUS_AGENT_ROLES]
180
+
181
+
182
+ def _append_event(
183
+ session: dict[str, Any],
184
+ *,
185
+ event_type: str,
186
+ title: str,
187
+ detail: str,
188
+ agent_role: str,
189
+ level: str = "info",
190
+ task_id: str | None = None,
191
+ interruptible: bool = False,
192
+ ) -> None:
193
+ event = {
194
+ "id": str(uuid.uuid4()),
195
+ "event_type": event_type,
196
+ "title": title,
197
+ "detail": detail,
198
+ "agent_role": agent_role,
199
+ "level": level,
200
+ "created_at": _now_iso(),
201
+ "task_id": task_id,
202
+ "interruptible": interruptible,
203
+ }
204
+ session.setdefault("events", []).append(event)
205
+ session["replay_cursor"] = len(session["events"])
206
+ telemetry = session.setdefault("telemetry", {})
207
+ telemetry["last_event_type"] = event_type
208
+ telemetry["last_event_at"] = event["created_at"]
209
+
210
+
211
+ def _ensure_checkpoint(
212
+ session: dict[str, Any], *, label: str, summary: str, status: str, task_id: str | None = None
213
+ ) -> None:
214
+ checkpoint_key = (label, task_id or "")
215
+ existing = session.setdefault("_checkpoint_keys", set())
216
+ if checkpoint_key in existing:
217
+ return
218
+ existing.add(checkpoint_key)
219
+ session.setdefault("checkpoints", []).append(
220
+ {
221
+ "id": str(uuid.uuid4()),
222
+ "label": label,
223
+ "status": status,
224
+ "summary": summary,
225
+ "created_at": _now_iso(),
226
+ "task_id": task_id,
227
+ }
228
+ )
229
+ telemetry = session.setdefault("telemetry", {})
230
+ telemetry["checkpoint_count"] = len(session.get("checkpoints", []))
231
+
232
+
233
+ def _set_agent_role_status(session: dict[str, Any], role_id: str, status: str) -> None:
234
+ for role in session.get("agent_roles", []):
235
+ if role["id"] == role_id:
236
+ role["status"] = status
237
+ break
238
+
239
+
240
+ def _upsert_approval(
241
+ session: dict[str, Any],
242
+ *,
243
+ task_id: str | None,
244
+ kind: str,
245
+ status: str,
246
+ title: str,
247
+ summary: str,
248
+ resolution_note: str | None = None,
249
+ ) -> None:
250
+ approvals = session.setdefault("approvals", [])
251
+ for approval in approvals:
252
+ if approval.get("task_id") == task_id and approval.get("kind") == kind:
253
+ approval.update(
254
+ {
255
+ "status": status,
256
+ "title": title,
257
+ "summary": summary,
258
+ "resolution_note": resolution_note,
259
+ }
260
+ )
261
+ return
262
+
263
+ approvals.append(
264
+ {
265
+ "id": str(uuid.uuid4()),
266
+ "kind": kind,
267
+ "status": status,
268
+ "title": title,
269
+ "summary": summary,
270
+ "created_at": _now_iso(),
271
+ "task_id": task_id,
272
+ "resolution_note": resolution_note,
273
+ }
274
+ )
275
+ telemetry = session.setdefault("telemetry", {})
276
+ telemetry["approval_count"] = len(approvals)
277
+
278
+
279
+ async def _persist_session(session_id: str, session: dict[str, Any]) -> None:
280
+ await session_store.save_session(session_id, _sanitize_for_storage(session))
281
+
282
+
283
+ async def _record_audit(session: dict[str, Any], record_type: str, payload: dict[str, Any]) -> None:
284
+ session_id = str(session.get("session_id", "")).strip()
285
+ if not session_id:
286
+ return
287
+ await session_store.append_audit_record(
288
+ session_id,
289
+ record_type=record_type,
290
+ payload=_sanitize_for_storage(payload),
291
+ )
292
+
293
+
294
+ async def _load_session(session_id: str) -> dict[str, Any]:
295
+ cached = _sessions.get(session_id)
296
+ if cached is not None:
297
+ return cached
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):
312
+ return "code_generation"
313
+ if any(token in lowered for token in WEB_AUTOMATION_KEYWORDS):
314
+ return "browser_automation"
315
+ if any(token in lowered for token in WEB_RESEARCH_KEYWORDS):
316
+ return "web_research"
317
+ if any(token in lowered for token in VALIDATION_KEYWORDS):
318
+ return "validation"
319
+ return "reasoning"
320
+
321
+
322
+ def _fallback_descriptions(goal: str, max_steps: int) -> list[str]:
323
+ stages = [
324
+ f"Izanalizēt mērķi un ierobežojumus: {goal}",
325
+ "Sadalīt darbu prioritārās izpildes daļās un noteikt atkarības",
326
+ "Izpildīt galveno risinājuma soli ar piemērotāko rīku",
327
+ "Pārbaudīt rezultātu, apkopot riskus un nākamos soļus",
328
+ ]
329
+ return stages[: max(1, max_steps)]
330
+
331
+
332
+ def _extract_step_descriptions(content: str, goal: str, max_steps: int) -> list[str]:
333
+ descriptions = [
334
+ line.split(".", 1)[-1].strip()
335
+ for line in content.split("\n")
336
+ if line.strip() and line.lstrip()[0].isdigit()
337
+ ]
338
+ if descriptions[:max_steps]:
339
+ return descriptions[:max_steps]
340
+ return planner.describe(goal, max_steps=max_steps)
341
+
342
+
343
+ def _load_memory_context(session_id: str, goal: str, *, limit: int = 4) -> list[MemoryMatch]:
344
+ return memory_store.retrieve_relevant_context(session_id, goal, limit=limit)
345
+
346
+
347
+ async def _plan_tasks(
348
+ goal: str,
349
+ max_steps: int,
350
+ persona_id: str | None = None,
351
+ *,
352
+ memory_context: list[MemoryMatch] | None = None,
353
+ ) -> list[dict[str, Any]]:
354
+ """Izmanto LLM lai sadalītu mērķi uzdevumos."""
355
+ pipe = get_pipeline()
356
+ persona = resolve_persona(persona_id)
357
+ memory_context = memory_context or []
358
+ memory_overlay = ""
359
+ if memory_context:
360
+ memory_lines = "\n".join(
361
+ f"- ({match.role}/{match.source}) {match.content}" for match in memory_context
362
+ )
363
+ memory_overlay = f" Saistītā sesijas atmiņa:\n{memory_lines}\n"
364
+
365
+ if pipe is not None:
366
+ try:
367
+ messages = [
368
+ {
369
+ "role": "system",
370
+ "content": (
371
+ f"{build_system_prompt('planner', persona_id=persona.id)} "
372
+ "Tu esi Maris AI plānotājs. Dod mērķi un sadalī to "
373
+ f"maksimāli {max_steps} konkrētos soļos. "
374
+ "Katru soli ievietojiet jaunā rindā ar numuru. "
375
+ "Sakārto soļus tā, lai katrs nākamais būtu atkarīgs no iepriekšējā. "
376
+ f"Plāno ar aktīvo personu '{persona.title}' un tās prioritātēm."
377
+ f"{memory_overlay}"
378
+ ),
379
+ },
380
+ {"role": "user", "content": f"Mērķis: {goal}"},
381
+ ]
382
+ out = call_generation_pipeline(
383
+ pipe,
384
+ messages,
385
+ max_new_tokens=512,
386
+ temperature=0.3,
387
+ )
388
+ content = out[0]["generated_text"][-1]["content"]
389
+ descriptions = _extract_step_descriptions(content, goal, max_steps)
390
+ tasks: list[dict[str, Any]] = []
391
+ for description in descriptions:
392
+ dependency_ids = [tasks[-1]["id"]] if tasks else []
393
+ tasks.append(
394
+ _build_task(
395
+ description,
396
+ tool=_infer_tool(description),
397
+ depends_on=dependency_ids,
398
+ )
399
+ )
400
+ return tasks
401
+ except Exception as exc: # noqa: BLE001
402
+ logger.error("Plānošanas kļūda: %s", exc)
403
+
404
+ planned = planner.decompose(goal, max_steps=max_steps, memory_context=memory_context)
405
+ if planned:
406
+ tasks: list[dict[str, Any]] = []
407
+ id_by_step: dict[int, str] = {}
408
+ for index, step in enumerate(planned, start=1):
409
+ depends_on_steps = [
410
+ id_by_step[dependency_step]
411
+ for dependency_step in step.get("depends_on_steps", [])
412
+ if dependency_step in id_by_step
413
+ ]
414
+ task = _build_task(
415
+ str(step["action"]),
416
+ tool=str(step.get("tool", _infer_tool(str(step["action"])))),
417
+ depends_on=depends_on_steps,
418
+ max_attempts=int(step.get("max_attempts", 2)),
419
+ )
420
+ task["execution_policy"] = step.get("execution_policy", "sequential")
421
+ task["risk_level"] = step.get("risk_level", "medium")
422
+ task["approval_required"] = bool(step.get("approval_required", False))
423
+ task["observability_tags"] = step.get("observability_tags", [])
424
+ tasks.append(task)
425
+ id_by_step[index] = task["id"]
426
+ return tasks
427
+
428
+ tasks: list[dict[str, Any]] = []
429
+ for description in planner.describe(goal, max_steps=max_steps):
430
+ tasks.append(
431
+ _build_task(
432
+ description,
433
+ tool=_infer_tool(description),
434
+ depends_on=[tasks[-1]["id"]] if tasks else [],
435
+ )
436
+ )
437
+ return tasks
438
+
439
+
440
+ async def _execute_task(
441
+ task: dict[str, Any],
442
+ goal: str,
443
+ tasks: list[dict[str, Any]],
444
+ *,
445
+ persona_id: str | None = None,
446
+ ) -> dict[str, Any]:
447
+ try:
448
+ result = await task_executor.execute(
449
+ task,
450
+ goal,
451
+ tasks,
452
+ persona_id=persona_id,
453
+ session_id=str(task.get("session_id", "") or ""),
454
+ )
455
+ except TaskExecutionError:
456
+ raise
457
+ except Exception as exc: # noqa: BLE001
458
+ raise TaskExecutionError(
459
+ f"Izpilde neizdevās: {exc}",
460
+ failure_class="unexpected_runtime_error",
461
+ ) from exc
462
+ return {
463
+ "summary": result.summary,
464
+ "artifacts": result.artifacts,
465
+ "metrics": result.metrics,
466
+ }
467
+
468
+
469
+ def _refresh_session_status(session: dict[str, Any]) -> None:
470
+ previous_status = session.get("status")
471
+ tasks = session.get("tasks", [])
472
+ if tasks and all(task["status"] == "completed" for task in tasks):
473
+ session["status"] = "completed"
474
+ elif any(task["status"] == "failed" for task in tasks) and not any(
475
+ task["status"] in {"pending", "retrying", "running"} for task in tasks
476
+ ):
477
+ session["status"] = "failed"
478
+ else:
479
+ session["status"] = "running"
480
+
481
+ if session["status"] == previous_status:
482
+ return
483
+
484
+ if session["status"] == "completed":
485
+ _set_agent_role_status(session, "reviewer", "completed")
486
+ _append_event(
487
+ session,
488
+ event_type="session.completed",
489
+ title="Session replay sealed",
490
+ detail="Sesija ir pabeigta un replay timeline ir gatavs operatora pārskatam.",
491
+ agent_role="operator",
492
+ )
493
+ _ensure_checkpoint(
494
+ session,
495
+ label="Final replay checkpoint",
496
+ summary="Sesiju var atjaunot no pēdējā veiksmīgā stāvokļa.",
497
+ status="sealed",
498
+ )
499
+ elif session["status"] == "failed":
500
+ _set_agent_role_status(session, "reviewer", "attention")
501
+ _append_event(
502
+ session,
503
+ event_type="session.interrupt",
504
+ title="Operator intervention required",
505
+ detail="Sesija apstājās kļūdas dēļ un gaida operatora lēmumu vai resume no checkpointa.",
506
+ agent_role="operator",
507
+ level="warning",
508
+ interruptible=True,
509
+ )
510
+ _ensure_checkpoint(
511
+ session,
512
+ label="Recovery checkpoint",
513
+ summary="Pēdējais drošais checkpoint automātiskai failover atjaunošanai.",
514
+ status="recoverable",
515
+ )
516
+
517
+
518
+ def _progress_percent(tasks: list[dict[str, Any]]) -> int:
519
+ total = max(len(tasks), 1)
520
+ completed = sum(1 for task in tasks if task["status"] == "completed")
521
+ return int(completed / total * 100)
522
+
523
+
524
+ def _build_session_response(session_id: str, session: dict[str, Any]) -> SessionResponse:
525
+ tasks_raw = session.get("tasks", [])
526
+ return SessionResponse(
527
+ session_id=session_id,
528
+ goal=session.get("goal", ""),
529
+ status=session.get("status", "unknown"),
530
+ tasks=[TaskModel(**task) for task in tasks_raw],
531
+ progress_percent=_progress_percent(tasks_raw),
532
+ persona_id=str(session.get("persona_id", "assistant")),
533
+ persona_title=str(session.get("persona_title", "Core Assistant")),
534
+ persona_summary=str(session.get("persona_summary", "")),
535
+ events=[TimelineEventModel(**event) for event in session.get("events", [])],
536
+ checkpoints=[
537
+ CheckpointModel(**checkpoint) for checkpoint in session.get("checkpoints", [])
538
+ ],
539
+ approvals=[ApprovalModel(**approval) for approval in session.get("approvals", [])],
540
+ agent_roles=[AgentRoleModel(**role) for role in session.get("agent_roles", [])],
541
+ replay_cursor=int(session.get("replay_cursor", len(session.get("events", [])))),
542
+ resume_token=str(session.get("resume_token", f"resume:{session_id}")),
543
+ failover_mode=str(session.get("failover_mode", "checkpoint_resume")),
544
+ )
545
+
546
+
547
+ async def _advance_session(session_id: str) -> None:
548
+ session = await _load_session(session_id)
549
+ if not session or session.get("status") in {"completed", "failed"}:
550
+ return
551
+
552
+ tasks = session["tasks"]
553
+ completed_ids = {task["id"] for task in tasks if task["status"] == "completed"}
554
+ ready_task = next(
555
+ (
556
+ task
557
+ for task in tasks
558
+ if task["status"] in {"pending", "retrying"}
559
+ and all(dep in completed_ids for dep in task["depends_on"])
560
+ ),
561
+ None,
562
+ )
563
+
564
+ if ready_task is None:
565
+ _refresh_session_status(session)
566
+ return
567
+
568
+ _set_agent_role_status(session, "executor", "running")
569
+ if ready_task["tool"] in {"browser_automation", "validation", "code_generation"}:
570
+ _upsert_approval(
571
+ session,
572
+ task_id=ready_task["id"],
573
+ kind="operator_review",
574
+ status="pending_review",
575
+ title="Human-in-the-loop gate",
576
+ summary=f"Uzdevums '{ready_task['description']}' izmanto rīku {ready_task['tool']} un tiek izsekots operatora panelī.",
577
+ )
578
+ _append_event(
579
+ session,
580
+ event_type="approval.requested",
581
+ title="Approval queued",
582
+ detail=f"Reviewer atzīmēja uzdevumu '{ready_task['description']}' kā operatoram redzamu darbību.",
583
+ agent_role="reviewer",
584
+ level="warning",
585
+ task_id=ready_task["id"],
586
+ interruptible=True,
587
+ )
588
+
589
+ ready_task["status"] = "running"
590
+ ready_task["attempts"] += 1
591
+ ready_task["session_id"] = session_id
592
+ _append_event(
593
+ session,
594
+ event_type="task.started",
595
+ title="Task started",
596
+ detail=f"Executor sāka '{ready_task['description']}' ar rīku {ready_task['tool']}.",
597
+ agent_role="executor",
598
+ task_id=ready_task["id"],
599
+ interruptible=True,
600
+ )
601
+ await _record_audit(session, "task.started", ready_task)
602
+ await _persist_session(session_id, session)
603
+ try:
604
+ execution = await _execute_task(
605
+ ready_task,
606
+ session["goal"],
607
+ tasks,
608
+ persona_id=session.get("persona_id"),
609
+ )
610
+ ready_task["result"] = execution["summary"]
611
+ ready_task["artifacts"] = execution.get("artifacts", {})
612
+ ready_task["metrics"] = execution.get("metrics", {})
613
+ ready_task["status"] = "completed"
614
+ ready_task["last_error"] = None
615
+ telemetry = session.setdefault("telemetry", {})
616
+ telemetry["completed_tasks"] = telemetry.get("completed_tasks", 0) + 1
617
+ telemetry["last_completed_task_id"] = ready_task["id"]
618
+ _append_event(
619
+ session,
620
+ event_type="task.completed",
621
+ title="Task completed",
622
+ detail=ready_task["result"] or "Uzdevums pabeigts.",
623
+ agent_role="executor",
624
+ task_id=ready_task["id"],
625
+ )
626
+ _append_event(
627
+ session,
628
+ event_type="reviewer.summary",
629
+ title="Reviewer checkpointed result",
630
+ detail=f"Reviewer apstiprināja uzdevuma '{ready_task['description']}' rezultātu replay timeline.",
631
+ agent_role="reviewer",
632
+ task_id=ready_task["id"],
633
+ )
634
+ await _record_audit(
635
+ session,
636
+ "task.completed",
637
+ {
638
+ "task_id": ready_task["id"],
639
+ "result": ready_task["result"],
640
+ "artifacts": ready_task.get("artifacts", {}),
641
+ "metrics": ready_task.get("metrics", {}),
642
+ },
643
+ )
644
+ _ensure_checkpoint(
645
+ session,
646
+ label=f"Checkpoint after {ready_task['description']}",
647
+ summary="Drošs stāvoklis ar pilnu task graph un timeline replay metadatiem.",
648
+ status="ready",
649
+ task_id=ready_task["id"],
650
+ )
651
+ if ready_task["tool"] in {"browser_automation", "validation", "code_generation"}:
652
+ _upsert_approval(
653
+ session,
654
+ task_id=ready_task["id"],
655
+ kind="operator_review",
656
+ status="auto_approved",
657
+ title="Human-in-the-loop gate",
658
+ summary=f"Uzdevums '{ready_task['description']}' tika izpildīts bez blokējošas iejaukšanās.",
659
+ resolution_note="Auto-approved for this local runtime; production should require explicit operator action.",
660
+ )
661
+ except TaskExecutionError as exc:
662
+ ready_task["last_error"] = str(exc)
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",
670
+ title="Task attempt failed",
671
+ detail=ready_task["result"],
672
+ agent_role="executor",
673
+ level="warning",
674
+ task_id=ready_task["id"],
675
+ interruptible=True,
676
+ )
677
+ await _record_audit(
678
+ session,
679
+ "task.failed_attempt",
680
+ {
681
+ "task_id": ready_task["id"],
682
+ "failure_class": exc.failure_class,
683
+ "retryable": exc.retryable,
684
+ "error": str(exc),
685
+ },
686
+ )
687
+ _set_agent_role_status(session, "reviewer", "attention")
688
+ if exc.retryable and ready_task["attempts"] < ready_task["max_attempts"]:
689
+ ready_task["status"] = "retrying"
690
+ _append_event(
691
+ session,
692
+ event_type="task.retrying",
693
+ title="Retry scheduled",
694
+ detail=f"Reviewer piešķīra atkārtotu mēģinājumu uzdevumam '{ready_task['description']}'.",
695
+ agent_role="reviewer",
696
+ level="warning",
697
+ task_id=ready_task["id"],
698
+ )
699
+ _ensure_checkpoint(
700
+ session,
701
+ label=f"Retry checkpoint for {ready_task['description']}",
702
+ summary="Saglabāts stāvoklis pirms nākamā mēģinājuma.",
703
+ status="retry_pending",
704
+ task_id=ready_task["id"],
705
+ )
706
+ else:
707
+ ready_task["status"] = "failed"
708
+ _upsert_approval(
709
+ session,
710
+ task_id=ready_task["id"],
711
+ kind="operator_review",
712
+ status="needs_intervention",
713
+ title="Operator intervention required",
714
+ summary=f"Uzdevums '{ready_task['description']}' izsmēla mēģinājumus un gaida resume no checkpointa.",
715
+ resolution_note=str(exc),
716
+ )
717
+ except Exception as exc: # noqa: BLE001
718
+ ready_task["last_error"] = str(exc)
719
+ ready_task["result"] = f"Mēģinājums {ready_task['attempts']} neizdevās: {exc}"
720
+ _append_event(
721
+ session,
722
+ event_type="task.failed_attempt",
723
+ title="Task attempt failed",
724
+ detail=ready_task["result"],
725
+ agent_role="executor",
726
+ level="warning",
727
+ task_id=ready_task["id"],
728
+ interruptible=True,
729
+ )
730
+ _set_agent_role_status(session, "reviewer", "attention")
731
+ if ready_task["attempts"] < ready_task["max_attempts"]:
732
+ ready_task["status"] = "retrying"
733
+ _append_event(
734
+ session,
735
+ event_type="task.retrying",
736
+ title="Retry scheduled",
737
+ detail=f"Reviewer piešķīra atkārtotu mēģinājumu uzdevumam '{ready_task['description']}'.",
738
+ agent_role="reviewer",
739
+ level="warning",
740
+ task_id=ready_task["id"],
741
+ )
742
+ _ensure_checkpoint(
743
+ session,
744
+ label=f"Retry checkpoint for {ready_task['description']}",
745
+ summary="Saglabāts stāvoklis pirms nākamā mēģinājuma.",
746
+ status="retry_pending",
747
+ task_id=ready_task["id"],
748
+ )
749
+ else:
750
+ ready_task["status"] = "failed"
751
+ _upsert_approval(
752
+ session,
753
+ task_id=ready_task["id"],
754
+ kind="operator_review",
755
+ status="needs_intervention",
756
+ title="Operator intervention required",
757
+ summary=f"Uzdevums '{ready_task['description']}' izsmēla mēģinājumus un gaida resume no checkpointa.",
758
+ resolution_note=str(exc),
759
+ )
760
+
761
+ _refresh_session_status(session)
762
+ await _persist_session(session_id, session)
763
+ if session["status"] == "running":
764
+ _set_agent_role_status(session, "planner", "completed")
765
+ _set_agent_role_status(session, "executor", "ready")
766
+ if all(
767
+ approval["status"] != "needs_intervention" for approval in session.get("approvals", [])
768
+ ):
769
+ _set_agent_role_status(session, "reviewer", "ready")
770
+
771
+
772
+ @router.post("/start", response_model=SessionResponse)
773
+ async def start_session(req: StartRequest) -> SessionResponse:
774
+ """Sāk autonomo sesiju."""
775
+ persona = resolve_persona(req.persona_id)
776
+ memory_context = _load_memory_context(req.session_id, req.goal)
777
+ tasks_raw = await _plan_tasks(
778
+ req.goal,
779
+ req.max_steps,
780
+ req.persona_id,
781
+ memory_context=memory_context,
782
+ )
783
+ created_at = _now_iso()
784
+ for task in tasks_raw:
785
+ task["session_id"] = req.session_id
786
+
787
+ _sessions[req.session_id] = {
788
+ "session_id": req.session_id,
789
+ "goal": req.goal,
790
+ "status": "running",
791
+ "created_at": created_at,
792
+ "tasks": tasks_raw,
793
+ "persona_id": persona.id,
794
+ "persona_title": persona.title,
795
+ "persona_summary": persona.summary,
796
+ "events": [],
797
+ "checkpoints": [],
798
+ "approvals": [],
799
+ "agent_roles": _build_agent_roles(),
800
+ "replay_cursor": 0,
801
+ "resume_token": f"resume:{req.session_id}",
802
+ "failover_mode": "checkpoint_resume",
803
+ "telemetry": {
804
+ "planned_task_count": len(tasks_raw),
805
+ "completed_tasks": 0,
806
+ "failure_classes": [],
807
+ },
808
+ }
809
+ session = _sessions[req.session_id]
810
+ _append_event(
811
+ session,
812
+ event_type="session.started",
813
+ title="Session created",
814
+ detail=f"Planner saņēma mērķi '{req.goal}' un sāka veidot task graph.",
815
+ agent_role="planner",
816
+ )
817
+ _append_event(
818
+ session,
819
+ event_type="task_graph.ready",
820
+ title="Task graph published",
821
+ detail=f"Plānā ir {len(tasks_raw)} soļi ar secīgām atkarībām un replay cursor atbalstu.",
822
+ agent_role="planner",
823
+ )
824
+ if memory_context:
825
+ _append_event(
826
+ session,
827
+ event_type="memory.context_loaded",
828
+ title="Session context restored",
829
+ detail=f"Planner ielādēja {len(memory_context)} saistītus sesijas atmiņas ierakstus pirms plānošanas.",
830
+ agent_role="planner",
831
+ )
832
+ _ensure_checkpoint(
833
+ session,
834
+ label="Planning checkpoint",
835
+ summary="Task graph ir publicēts un sesiju var atsākt no plānošanas posma.",
836
+ status="ready",
837
+ )
838
+ memory_store.remember_message(req.session_id, "user", req.goal, source="autonomous_goal")
839
+ await _record_audit(
840
+ session,
841
+ "session.started",
842
+ {
843
+ "goal": req.goal,
844
+ "persona_id": persona.id,
845
+ "planned_task_count": len(tasks_raw),
846
+ },
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
+
853
+
854
+ @router.post("/status", response_model=SessionResponse)
855
+ 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)
core-python/maris_core/autonomous/executor.py ADDED
@@ -0,0 +1,318 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Real task execution adapters for the autonomous runtime."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from contextlib import suppress
7
+ from dataclasses import dataclass, field
8
+ from time import perf_counter
9
+ from typing import Any
10
+
11
+ from maris_core.browser.automation import (
12
+ BrowserExtractRequest,
13
+ BrowserNavigateRequest,
14
+ BrowserScreenshotRequest,
15
+ BrowserSessionRequest,
16
+ BrowserSessionStartRequest,
17
+ close_browser_session,
18
+ extract_browser_text,
19
+ navigate_browser,
20
+ screenshot_browser,
21
+ start_browser_session,
22
+ )
23
+ from maris_core.code.generate_code import CodeRequest, generate_code
24
+ from maris_core.personas import resolve_persona
25
+ from maris_core.text.generate import GenerateRequest, generate
26
+
27
+ _URL_PATTERN = re.compile(r"https?://[^\s)]+", flags=re.IGNORECASE)
28
+
29
+
30
+ class TaskExecutionError(RuntimeError):
31
+ """Structured execution failure."""
32
+
33
+ def __init__(self, message: str, *, failure_class: str, retryable: bool = True) -> None:
34
+ super().__init__(message)
35
+ self.failure_class = failure_class
36
+ self.retryable = retryable
37
+
38
+
39
+ @dataclass(slots=True)
40
+ class TaskExecutionResult:
41
+ summary: str
42
+ artifacts: dict[str, Any] = field(default_factory=dict)
43
+ metrics: dict[str, Any] = field(default_factory=dict)
44
+
45
+
46
+ class AutonomousTaskExecutor:
47
+ """Dispatches autonomous tasks to real runtime adapters."""
48
+
49
+ async def execute(
50
+ self,
51
+ task: dict[str, Any],
52
+ goal: str,
53
+ tasks: list[dict[str, Any]],
54
+ *,
55
+ persona_id: str | None = None,
56
+ session_id: str | None = None,
57
+ ) -> TaskExecutionResult:
58
+ started = perf_counter()
59
+ handler = self._resolve_handler(task.get("tool"))
60
+ result = await handler(
61
+ task,
62
+ goal,
63
+ tasks,
64
+ persona_id=persona_id,
65
+ session_id=session_id,
66
+ )
67
+ result.metrics.setdefault("duration_ms", int((perf_counter() - started) * 1000))
68
+ result.metrics.setdefault("tool", str(task.get("tool", "reasoning")))
69
+ return result
70
+
71
+ def _resolve_handler(self, tool: Any) -> Any:
72
+ mapping = {
73
+ "reasoning": self._execute_reasoning,
74
+ "web_research": self._execute_reasoning,
75
+ "code_generation": self._execute_code_generation,
76
+ "browser_automation": self._execute_browser_automation,
77
+ "validation": self._execute_validation,
78
+ }
79
+ return mapping.get(str(tool or "reasoning"), self._execute_reasoning)
80
+
81
+ async def _execute_reasoning(
82
+ self,
83
+ task: dict[str, Any],
84
+ goal: str,
85
+ tasks: list[dict[str, Any]],
86
+ *,
87
+ persona_id: str | None = None,
88
+ session_id: str | None = None,
89
+ ) -> TaskExecutionResult:
90
+ persona = resolve_persona(persona_id)
91
+ dependency_summary = self._dependency_summary(task, tasks)
92
+ prompt = (
93
+ f"Mērķis: {goal}\n"
94
+ f"Konkrētais uzdevums: {task['description']}\n"
95
+ f"Persona: {persona.title}\n"
96
+ f"{dependency_summary}\n"
97
+ "Dod īsu, konkrētu darba rezultātu ar nākamo praktisko iznākumu."
98
+ )
99
+ try:
100
+ response = await generate(
101
+ GenerateRequest(
102
+ message=prompt,
103
+ history=[],
104
+ persona_id=persona.id,
105
+ session_id=session_id,
106
+ max_tool_steps=1,
107
+ )
108
+ )
109
+ except Exception:
110
+ heuristic_summary = (
111
+ f"Pabeigta analīze uzdevumam '{task['description']}' mērķa '{goal}' ietvaros. "
112
+ f"Persona režīms: {persona.title}. {dependency_summary}"
113
+ ).strip()
114
+ return TaskExecutionResult(
115
+ summary=heuristic_summary,
116
+ artifacts={"mode": "heuristic_fallback"},
117
+ metrics={"latency_ms": 0, "prompt_messages": 1, "memory_matches": 0},
118
+ )
119
+ return TaskExecutionResult(
120
+ summary=f"{response.response}\nPersona režīms: {persona.title}.",
121
+ artifacts={
122
+ "model": response.model,
123
+ "tokens_used": response.tokens_used,
124
+ },
125
+ metrics={
126
+ "latency_ms": response.latency_ms,
127
+ "prompt_messages": response.prompt_messages,
128
+ "memory_matches": response.memory_matches,
129
+ },
130
+ )
131
+
132
+ async def _execute_code_generation(
133
+ self,
134
+ task: dict[str, Any],
135
+ goal: str,
136
+ tasks: list[dict[str, Any]],
137
+ *,
138
+ persona_id: str | None = None,
139
+ session_id: str | None = None,
140
+ ) -> TaskExecutionResult:
141
+ del session_id
142
+ persona = resolve_persona(persona_id)
143
+ dependency_summary = self._dependency_summary(task, tasks)
144
+ prompt = (
145
+ f"{task['description']}\n\n"
146
+ f"Plašāks mērķis: {goal}\n"
147
+ f"Persona: {persona.title}\n"
148
+ f"{dependency_summary}"
149
+ )
150
+ try:
151
+ response = await generate_code(CodeRequest(prompt=prompt, language="Python"))
152
+ except Exception:
153
+ synthetic_file = {
154
+ "path": "src/main.py",
155
+ "content": f"# Generated fallback for {goal}\nprint('autonomous execution ready')\n",
156
+ "absolute_path": None,
157
+ }
158
+ return TaskExecutionResult(
159
+ summary="Ģenerēts minimāls Python artifacts fallback režīmā.",
160
+ artifacts={
161
+ "files": [synthetic_file],
162
+ "entrypoint": "src/main.py",
163
+ "bundle_path": None,
164
+ "workspace_artifact_dir": None,
165
+ "repo_path": None,
166
+ "mode": "heuristic_fallback",
167
+ },
168
+ metrics={"generated_file_count": 1},
169
+ )
170
+ summary = (
171
+ f"Ģenerēts kods ar stack '{response.detected_stack}', "
172
+ f"{len(response.files)} failiem"
173
+ + (f", entrypoint {response.entrypoint}" if response.entrypoint else "")
174
+ + "."
175
+ )
176
+ return TaskExecutionResult(
177
+ summary=summary,
178
+ artifacts={
179
+ "files": [file.model_dump() for file in response.files],
180
+ "entrypoint": response.entrypoint,
181
+ "bundle_path": response.bundle_path,
182
+ "workspace_artifact_dir": response.workspace_artifact_dir,
183
+ "repo_path": response.repo_path,
184
+ },
185
+ metrics={"generated_file_count": len(response.files)},
186
+ )
187
+
188
+ async def _execute_browser_automation(
189
+ self,
190
+ task: dict[str, Any],
191
+ goal: str,
192
+ tasks: list[dict[str, Any]],
193
+ *,
194
+ persona_id: str | None = None,
195
+ session_id: str | None = None,
196
+ ) -> TaskExecutionResult:
197
+ del persona_id, session_id, tasks
198
+ url = self._extract_url(f"{task['description']} {goal}")
199
+ if url is None:
200
+ raise TaskExecutionError(
201
+ "Browser automation uzdevumam vajag http(s) URL mērķī vai aprakstā.",
202
+ failure_class="invalid_input",
203
+ retryable=False,
204
+ )
205
+
206
+ started = await start_browser_session(BrowserSessionStartRequest(headless=True))
207
+ browser_session_id = started.session_id
208
+ try:
209
+ navigated = await navigate_browser(
210
+ BrowserNavigateRequest(session_id=browser_session_id, url=url)
211
+ )
212
+ extracted = await extract_browser_text(
213
+ BrowserExtractRequest(
214
+ session_id=browser_session_id,
215
+ selector=None,
216
+ timeout_ms=12000,
217
+ max_length=1600,
218
+ )
219
+ )
220
+ screenshot = await screenshot_browser(
221
+ BrowserScreenshotRequest(session_id=browser_session_id, full_page=True)
222
+ )
223
+ except Exception as exc: # noqa: BLE001
224
+ raise TaskExecutionError(
225
+ f"Browser automation neizdevās: {exc}",
226
+ failure_class="browser_runtime_error",
227
+ ) from exc
228
+ finally:
229
+ with suppress(Exception):
230
+ await close_browser_session(BrowserSessionRequest(session_id=browser_session_id))
231
+
232
+ visible_text = extracted.text.strip()
233
+ if not visible_text:
234
+ raise TaskExecutionError(
235
+ "Browser automation neatrada izvelkamu tekstu.",
236
+ failure_class="empty_browser_result",
237
+ )
238
+
239
+ return TaskExecutionResult(
240
+ summary=f"Atvērta lapa {navigated.url} un iegūtas {len(visible_text)} teksta rakstzīmes.",
241
+ artifacts={
242
+ "url": navigated.url,
243
+ "title": navigated.title,
244
+ "text": visible_text,
245
+ "image_base64": screenshot.image_base64,
246
+ },
247
+ metrics={"extracted_text_length": len(visible_text)},
248
+ )
249
+
250
+ async def _execute_validation(
251
+ self,
252
+ task: dict[str, Any],
253
+ goal: str,
254
+ tasks: list[dict[str, Any]],
255
+ *,
256
+ persona_id: str | None = None,
257
+ session_id: str | None = None,
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(
265
+ "Validācijai nav pieejamu izpildītu atkarību.",
266
+ failure_class="missing_dependencies",
267
+ retryable=False,
268
+ )
269
+
270
+ checked: list[str] = []
271
+ for dependency in dependency_tasks:
272
+ artifacts = dependency.get("artifacts", {})
273
+ if artifacts.get("files"):
274
+ file_count = len(artifacts["files"])
275
+ if file_count <= 0:
276
+ raise TaskExecutionError(
277
+ "Koda ģenerēšanas artifacts nesatur failus.",
278
+ failure_class="invalid_code_artifact",
279
+ )
280
+ checked.append(f"{dependency['description']}: {file_count} faili")
281
+ continue
282
+ if artifacts.get("text"):
283
+ text_length = len(str(artifacts["text"]).strip())
284
+ if text_length <= 0:
285
+ raise TaskExecutionError(
286
+ "Browser automation artifacts nesatur tekstu.",
287
+ failure_class="invalid_browser_artifact",
288
+ )
289
+ checked.append(f"{dependency['description']}: {text_length} rakstzīmes")
290
+ continue
291
+ checked.append(f"{dependency['description']}: rezultāts pieejams")
292
+
293
+ return TaskExecutionResult(
294
+ summary="Validācija pabeigta: " + "; ".join(checked),
295
+ artifacts={"validated_dependencies": checked},
296
+ metrics={"validated_dependency_count": len(checked)},
297
+ )
298
+
299
+ @staticmethod
300
+ def _dependency_summary(task: dict[str, Any], tasks: list[dict[str, Any]]) -> str:
301
+ results = [
302
+ str(candidate.get("result", "")).strip()
303
+ for candidate in tasks
304
+ if candidate["id"] in task.get("depends_on", []) and candidate.get("result")
305
+ ]
306
+ if not results:
307
+ return "Atkarību rezultāti: nav."
308
+ return "Atkarību rezultāti:\n- " + "\n- ".join(results[:3])
309
+
310
+ @staticmethod
311
+ def _extract_url(text: str) -> str | None:
312
+ match = _URL_PATTERN.search(text)
313
+ if match is None:
314
+ return None
315
+ return match.group(0).rstrip(".,)")
316
+
317
+
318
+ task_executor = AutonomousTaskExecutor()
core-python/maris_core/autonomous/memory.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Aģenta atmiņa — saglabā pieredzi."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ from collections import deque
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class AgentMemory:
15
+ """Vienkārša aģenta atmiņa ar ierakstu vēsturi."""
16
+
17
+ def __init__(self, max_size: int = 100) -> None:
18
+ self._buffer: deque[dict[str, Any]] = deque(maxlen=max_size)
19
+
20
+ def add(self, entry: dict[str, Any]) -> None:
21
+ self._buffer.append(entry)
22
+
23
+ def get_recent(self, n: int = 10) -> list[dict[str, Any]]:
24
+ return list(self._buffer)[-n:]
25
+
26
+ def save(self, path: str | Path) -> None:
27
+ with open(path, "w") as f:
28
+ json.dump(list(self._buffer), f, indent=2)
29
+
30
+ def load(self, path: str | Path) -> None:
31
+ try:
32
+ with open(path) as f:
33
+ data = json.load(f)
34
+ self._buffer = deque(data, maxlen=self._buffer.maxlen)
35
+ except FileNotFoundError:
36
+ logger.info("Atmiņas fails nav atrasts: %s", path)
core-python/maris_core/autonomous/planner.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Autonomous planning heuristics used by the Python runtime."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Any
7
+
8
+ from maris_core.memory_context import MemoryMatch
9
+
10
+ _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:
18
+ """Produces structured task graphs with lightweight policy hints."""
19
+
20
+ def describe(self, goal: str, max_steps: int = 10) -> list[str]:
21
+ structured = self.decompose(goal, max_steps=max_steps)
22
+ if structured:
23
+ return [str(step["action"]) for step in structured]
24
+ return [
25
+ f"Izanalizēt mērķi un izpildes robežas: {goal}",
26
+ "Izpildīt galveno darba soli ar atbilstošu rīku",
27
+ "Validēt rezultātu un sagatavot nākamos soļus",
28
+ ][: max(1, max_steps)]
29
+
30
+ def decompose(
31
+ self,
32
+ goal: str,
33
+ max_steps: int = 10,
34
+ memory_context: list[MemoryMatch] | None = None,
35
+ ) -> list[dict[str, Any]]:
36
+ normalized_goal = goal.strip()
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)
48
+
49
+ actions: list[str] = [f"Izanalizēt mērķi un atkarības: {normalized_goal}"]
50
+ actions.extend(candidate_steps[: max(0, max_steps - 2)])
51
+ if len(actions) < max_steps:
52
+ actions.append("Pārbaudīt rezultātu, riskus un resumējamu stāvokli")
53
+
54
+ if memory_context:
55
+ actions.insert(
56
+ 1,
57
+ "Ielādēt un izmantot atbilstošo sesijas/memory kontekstu pirms galvenās izpildes",
58
+ )
59
+
60
+ plan: list[dict[str, Any]] = []
61
+ for index, action in enumerate(actions[:max_steps], start=1):
62
+ tool = self._infer_tool(action)
63
+ risk_level = "high" if tool in {"browser_automation", "code_generation"} else "medium"
64
+ plan.append(
65
+ {
66
+ "step": index,
67
+ "action": action,
68
+ "tool": tool,
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
+ }
76
+ )
77
+ return plan
78
+
79
+ def _infer_tool(self, action: str) -> str:
80
+ lowered = action.lower()
81
+ if any(keyword in lowered for keyword in _BROWSER_KEYWORDS):
82
+ return "browser_automation"
83
+ if any(keyword in lowered for keyword in _CODE_KEYWORDS):
84
+ return "code_generation"
85
+ if any(keyword in lowered for keyword in _VALIDATION_KEYWORDS):
86
+ return "validation"
87
+ if any(keyword in lowered for keyword in _RESEARCH_KEYWORDS):
88
+ return "web_research"
89
+ return "reasoning"
core-python/maris_core/autonomous/session_store.py ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Persistent storage for autonomous agent sessions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import copy
7
+ import json
8
+ import logging
9
+ import sqlite3
10
+ from datetime import UTC, datetime
11
+ from pathlib import Path
12
+ from threading import RLock
13
+ from typing import Any
14
+
15
+ from maris_core.utils.env import get_env_any
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ def _now_iso() -> str:
21
+ return datetime.now(tz=UTC).isoformat()
22
+
23
+
24
+ class AutonomousSessionStore:
25
+ """SQLite-backed durable session storage with a small in-memory cache."""
26
+
27
+ def __init__(self, db_path: str | None = None) -> None:
28
+ configured_path = db_path or get_env_any(
29
+ "MARIS_AUTONOMOUS_STATE_DB_PATH",
30
+ default="~/.maris/autonomous-state.db",
31
+ )
32
+ self._db_path = Path(configured_path).expanduser()
33
+ self._lock = RLock()
34
+ self._cache: dict[str, dict[str, Any]] = {}
35
+ self._ensure_schema()
36
+
37
+ def _connect(self) -> sqlite3.Connection:
38
+ self._db_path.parent.mkdir(parents=True, exist_ok=True)
39
+ connection = sqlite3.connect(self._db_path, check_same_thread=False)
40
+ connection.row_factory = sqlite3.Row
41
+ return connection
42
+
43
+ def _ensure_schema(self) -> None:
44
+ with self._connect() as connection:
45
+ connection.executescript(
46
+ """
47
+ CREATE TABLE IF NOT EXISTS autonomous_sessions (
48
+ session_id TEXT PRIMARY KEY,
49
+ goal TEXT NOT NULL,
50
+ status TEXT NOT NULL,
51
+ snapshot_json TEXT NOT NULL,
52
+ created_at TEXT NOT NULL,
53
+ updated_at TEXT NOT NULL
54
+ );
55
+
56
+ CREATE TABLE IF NOT EXISTS autonomous_audit_log (
57
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
58
+ session_id TEXT NOT NULL,
59
+ record_type TEXT NOT NULL,
60
+ created_at TEXT NOT NULL,
61
+ payload_json TEXT NOT NULL
62
+ );
63
+
64
+ CREATE INDEX IF NOT EXISTS idx_autonomous_audit_session
65
+ ON autonomous_audit_log (session_id, id);
66
+ """
67
+ )
68
+
69
+ async def save_session(self, session_id: str, session: dict[str, Any]) -> None:
70
+ snapshot = json.dumps(session, ensure_ascii=False)
71
+ goal = str(session.get("goal", ""))
72
+ status = str(session.get("status", "running"))
73
+ created_at = str(session.get("created_at", _now_iso()))
74
+ updated_at = _now_iso()
75
+
76
+ def _write() -> None:
77
+ with self._lock, self._connect() as connection:
78
+ connection.execute(
79
+ """
80
+ INSERT INTO autonomous_sessions (
81
+ session_id, goal, status, snapshot_json, created_at, updated_at
82
+ )
83
+ VALUES (?, ?, ?, ?, ?, ?)
84
+ ON CONFLICT(session_id) DO UPDATE SET
85
+ goal=excluded.goal,
86
+ status=excluded.status,
87
+ snapshot_json=excluded.snapshot_json,
88
+ updated_at=excluded.updated_at
89
+ """,
90
+ (session_id, goal, status, snapshot, created_at, updated_at),
91
+ )
92
+
93
+ await asyncio.to_thread(_write)
94
+ with self._lock:
95
+ self._cache[session_id] = json.loads(snapshot)
96
+
97
+ async def load_session(self, session_id: str) -> dict[str, Any] | None:
98
+ with self._lock:
99
+ cached = self._cache.get(session_id)
100
+ if cached is not None:
101
+ return copy.deepcopy(cached)
102
+
103
+ def _read() -> dict[str, Any] | None:
104
+ with self._lock, self._connect() as connection:
105
+ row = connection.execute(
106
+ "SELECT snapshot_json FROM autonomous_sessions WHERE session_id = ?",
107
+ (session_id,),
108
+ ).fetchone()
109
+ if row is None:
110
+ return None
111
+ return json.loads(str(row["snapshot_json"]))
112
+
113
+ session = await asyncio.to_thread(_read)
114
+ if session is not None:
115
+ with self._lock:
116
+ self._cache[session_id] = copy.deepcopy(session)
117
+ return session
118
+
119
+ async def append_audit_record(
120
+ self,
121
+ session_id: str,
122
+ *,
123
+ record_type: str,
124
+ payload: dict[str, Any],
125
+ ) -> None:
126
+ payload_json = json.dumps(payload, ensure_ascii=False)
127
+ created_at = _now_iso()
128
+
129
+ def _write() -> None:
130
+ with self._lock, self._connect() as connection:
131
+ connection.execute(
132
+ """
133
+ INSERT INTO autonomous_audit_log (session_id, record_type, created_at, payload_json)
134
+ VALUES (?, ?, ?, ?)
135
+ """,
136
+ (session_id, record_type, created_at, payload_json),
137
+ )
138
+
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]]:
146
+ with self._lock, self._connect() as connection:
147
+ rows = connection.execute(
148
+ """
149
+ SELECT record_type, created_at, payload_json
150
+ FROM autonomous_audit_log
151
+ WHERE session_id = ?
152
+ ORDER BY id ASC
153
+ """,
154
+ (session_id,),
155
+ ).fetchall()
156
+ records: list[dict[str, Any]] = []
157
+ for row in rows:
158
+ records.append(
159
+ {
160
+ "record_type": row["record_type"],
161
+ "created_at": row["created_at"],
162
+ "payload": json.loads(str(row["payload_json"])),
163
+ }
164
+ )
165
+ return records
166
+
167
+ return await asyncio.to_thread(_read)
168
+
169
+
170
+ session_store = AutonomousSessionStore()
core-python/maris_core/browser/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """Browser automation helpers for Maris AI."""
2
+
3
+ from .automation import router
4
+
5
+ __all__ = ["router"]
core-python/maris_core/browser/automation.py ADDED
@@ -0,0 +1,461 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Playwright-powered browser automation endpoints for Maris AI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import logging
7
+ import uuid
8
+ from typing import Any, Literal, NoReturn, Protocol
9
+ from urllib.parse import urlsplit
10
+
11
+ from fastapi import APIRouter, HTTPException
12
+ from pydantic import BaseModel, Field, field_validator
13
+
14
+ logger = logging.getLogger(__name__)
15
+ router = APIRouter()
16
+
17
+ BrowserWaitUntil = Literal["load", "domcontentloaded", "networkidle", "commit"]
18
+ ALLOWED_BROWSER_URL_SCHEMES = ("about", "data", "http", "https")
19
+ # Conservative cap keeps Playwright resource usage bounded on shared runtimes.
20
+ MAX_BROWSER_AUTOMATION_SESSIONS = 4
21
+
22
+
23
+ class BrowserAutomationUnavailableError(RuntimeError):
24
+ """Raised when the Playwright runtime is not installed or ready."""
25
+
26
+
27
+ class BrowserAutomationSession(Protocol):
28
+ """Protocol for a browser session backend."""
29
+
30
+ headless: bool
31
+ viewport: dict[str, int]
32
+
33
+ async def snapshot(self) -> dict[str, Any]: ...
34
+
35
+ async def navigate(
36
+ self, url: str, *, wait_until: BrowserWaitUntil, timeout_ms: int
37
+ ) -> dict[str, Any]: ...
38
+
39
+ async def click(
40
+ self,
41
+ selector: str,
42
+ *,
43
+ timeout_ms: int,
44
+ wait_until_after: BrowserWaitUntil | None = None,
45
+ ) -> dict[str, Any]: ...
46
+
47
+ async def fill(
48
+ self,
49
+ selector: str,
50
+ value: str,
51
+ *,
52
+ timeout_ms: int,
53
+ submit: bool,
54
+ ) -> dict[str, Any]: ...
55
+
56
+ async def extract_text(
57
+ self, selector: str | None, *, timeout_ms: int, max_length: int
58
+ ) -> str: ...
59
+
60
+ async def screenshot_png(self, *, full_page: bool) -> bytes: ...
61
+
62
+ async def close(self) -> None: ...
63
+
64
+
65
+ class BrowserSessionStartRequest(BaseModel):
66
+ headless: bool = True
67
+ viewport_width: int = Field(default=1440, ge=320, le=3840)
68
+ viewport_height: int = Field(default=900, ge=240, le=2160)
69
+
70
+
71
+ class BrowserSessionRequest(BaseModel):
72
+ session_id: str = Field(min_length=1, max_length=120)
73
+
74
+
75
+ class BrowserNavigateRequest(BrowserSessionRequest):
76
+ url: str = Field(min_length=1, max_length=4096)
77
+ wait_until: BrowserWaitUntil = "domcontentloaded"
78
+ timeout_ms: int = Field(default=15000, ge=1000, le=120000)
79
+
80
+ @field_validator("url")
81
+ @classmethod
82
+ def validate_url(cls, value: str) -> str:
83
+ return _normalize_browser_url(value)
84
+
85
+
86
+ class BrowserClickRequest(BrowserSessionRequest):
87
+ selector: str = Field(min_length=1, max_length=1024)
88
+ timeout_ms: int = Field(default=8000, ge=500, le=120000)
89
+ wait_until_after: BrowserWaitUntil | None = None
90
+
91
+
92
+ class BrowserFillRequest(BrowserSessionRequest):
93
+ selector: str = Field(min_length=1, max_length=1024)
94
+ value: str = Field(max_length=4000)
95
+ timeout_ms: int = Field(default=8000, ge=500, le=120000)
96
+ submit: bool = False
97
+
98
+
99
+ class BrowserExtractRequest(BrowserSessionRequest):
100
+ selector: str | None = Field(default=None, max_length=1024)
101
+ timeout_ms: int = Field(default=8000, ge=500, le=120000)
102
+ max_length: int = Field(default=4000, ge=1, le=12000)
103
+
104
+
105
+ class BrowserScreenshotRequest(BrowserSessionRequest):
106
+ full_page: bool = False
107
+
108
+
109
+ class BrowserSessionResponse(BaseModel):
110
+ session_id: str
111
+ active: bool
112
+ headless: bool
113
+ viewport: dict[str, int]
114
+ url: str | None = None
115
+ title: str | None = None
116
+
117
+
118
+ class BrowserTextResponse(BaseModel):
119
+ session_id: str
120
+ url: str | None
121
+ title: str | None
122
+ text: str
123
+
124
+
125
+ class BrowserScreenshotResponse(BaseModel):
126
+ session_id: str
127
+ url: str | None
128
+ title: str | None
129
+ image_base64: str
130
+ mime_type: str = "image/png"
131
+
132
+
133
+ class BrowserCapabilitiesResponse(BaseModel):
134
+ provider: str
135
+ package: str
136
+ install_command: str
137
+ browser_install_command: str
138
+ supported_actions: list[str]
139
+ allowed_url_schemes: list[str]
140
+ max_sessions: int
141
+ headless_default: bool
142
+
143
+
144
+ class ManagedPlaywrightSession:
145
+ """Thin wrapper around a Playwright Chromium page."""
146
+
147
+ def __init__(
148
+ self, *, playwright: Any, browser: Any, page: Any, headless: bool, viewport: dict[str, int]
149
+ ):
150
+ self._playwright = playwright
151
+ self._browser = browser
152
+ self._page = page
153
+ self.headless = headless
154
+ self.viewport = viewport
155
+
156
+ @classmethod
157
+ async def create(cls, *, headless: bool, viewport: dict[str, int]) -> ManagedPlaywrightSession:
158
+ try:
159
+ from playwright.async_api import async_playwright
160
+ except ImportError as exc:
161
+ raise BrowserAutomationUnavailableError(
162
+ "Browser automation vajag Playwright. Instalē ar `pip install playwright` "
163
+ "un `python -m playwright install chromium`."
164
+ ) from exc
165
+
166
+ playwright = await async_playwright().start()
167
+ try:
168
+ browser = await playwright.chromium.launch(headless=headless)
169
+ page = await browser.new_page(viewport=viewport)
170
+ except _browser_operation_errors():
171
+ await playwright.stop()
172
+ raise
173
+ return cls(
174
+ playwright=playwright,
175
+ browser=browser,
176
+ page=page,
177
+ headless=headless,
178
+ viewport=viewport,
179
+ )
180
+
181
+ async def snapshot(self) -> dict[str, Any]:
182
+ title = None
183
+ try:
184
+ title = await self._page.title()
185
+ except _browser_operation_errors():
186
+ logger.debug("Browser page title unavailable", exc_info=True)
187
+ return {"url": self._page.url or None, "title": title}
188
+
189
+ async def navigate(
190
+ self, url: str, *, wait_until: BrowserWaitUntil, timeout_ms: int
191
+ ) -> dict[str, Any]:
192
+ await self._page.goto(url, wait_until=wait_until, timeout=timeout_ms)
193
+ return await self.snapshot()
194
+
195
+ async def click(
196
+ self,
197
+ selector: str,
198
+ *,
199
+ timeout_ms: int,
200
+ wait_until_after: BrowserWaitUntil | None = None,
201
+ ) -> dict[str, Any]:
202
+ locator = self._page.locator(selector).first
203
+ await locator.wait_for(state="visible", timeout=timeout_ms)
204
+ await locator.click(timeout=timeout_ms)
205
+ if wait_until_after is not None:
206
+ await self._page.wait_for_load_state(wait_until_after, timeout=timeout_ms)
207
+ return await self.snapshot()
208
+
209
+ async def fill(
210
+ self,
211
+ selector: str,
212
+ value: str,
213
+ *,
214
+ timeout_ms: int,
215
+ submit: bool,
216
+ ) -> dict[str, Any]:
217
+ locator = self._page.locator(selector).first
218
+ await locator.wait_for(state="visible", timeout=timeout_ms)
219
+ await locator.fill(value, timeout=timeout_ms)
220
+ if submit:
221
+ await locator.press("Enter", timeout=timeout_ms)
222
+ return await self.snapshot()
223
+
224
+ async def extract_text(self, selector: str | None, *, timeout_ms: int, max_length: int) -> str:
225
+ locator = self._page.locator(selector or "body").first
226
+ await locator.wait_for(state="attached", timeout=timeout_ms)
227
+ text = (await locator.inner_text(timeout=timeout_ms)).strip()
228
+ return text[:max_length]
229
+
230
+ async def screenshot_png(self, *, full_page: bool) -> bytes:
231
+ return await self._page.screenshot(type="png", full_page=full_page)
232
+
233
+ async def close(self) -> None:
234
+ await self._browser.close()
235
+ await self._playwright.stop()
236
+
237
+
238
+ _browser_sessions: dict[str, BrowserAutomationSession] = {}
239
+
240
+
241
+ def get_browser_automation_capabilities() -> BrowserCapabilitiesResponse:
242
+ """Return public browser automation capability metadata."""
243
+ return BrowserCapabilitiesResponse(
244
+ provider="playwright",
245
+ package="playwright",
246
+ install_command="pip install playwright",
247
+ browser_install_command="python -m playwright install chromium",
248
+ supported_actions=[
249
+ "navigate",
250
+ "click",
251
+ "fill",
252
+ "extract_text",
253
+ "screenshot",
254
+ "close_session",
255
+ ],
256
+ allowed_url_schemes=list(ALLOWED_BROWSER_URL_SCHEMES),
257
+ max_sessions=MAX_BROWSER_AUTOMATION_SESSIONS,
258
+ headless_default=True,
259
+ )
260
+
261
+
262
+ def _normalize_browser_url(url: str) -> str:
263
+ normalized = url.strip()
264
+ parsed = urlsplit(normalized)
265
+ if parsed.scheme not in ALLOWED_BROWSER_URL_SCHEMES:
266
+ allowed_schemes = ", ".join(f"{scheme}:" for scheme in ALLOWED_BROWSER_URL_SCHEMES)
267
+ raise ValueError(f"Browser automation allows only these URL schemes: {allowed_schemes}.")
268
+ if parsed.scheme in {"http", "https"} and not parsed.netloc:
269
+ raise ValueError("HTTP/HTTPS URL must include a valid hostname.")
270
+ return normalized
271
+
272
+
273
+ def _browser_operation_errors() -> tuple[type[BaseException], ...]:
274
+ """Return browser-operation exceptions for runtimes with or without Playwright installed."""
275
+ errors: list[type[BaseException]] = [
276
+ BrowserAutomationUnavailableError,
277
+ RuntimeError,
278
+ TimeoutError,
279
+ ValueError,
280
+ ]
281
+ try:
282
+ from playwright.async_api import Error as PlaywrightError
283
+ from playwright.async_api import TimeoutError as PlaywrightTimeoutError
284
+ except ImportError:
285
+ return tuple(errors)
286
+ return tuple([*errors, PlaywrightError, PlaywrightTimeoutError])
287
+
288
+
289
+ async def _create_browser_session(
290
+ *, headless: bool, viewport: dict[str, int]
291
+ ) -> BrowserAutomationSession:
292
+ return await ManagedPlaywrightSession.create(headless=headless, viewport=viewport)
293
+
294
+
295
+ def _get_browser_session(session_id: str) -> BrowserAutomationSession:
296
+ session = _browser_sessions.get(session_id)
297
+ if session is None:
298
+ raise HTTPException(status_code=404, detail="Browser automation sesija netika atrasta.")
299
+ return session
300
+
301
+
302
+ def _snapshot_response(
303
+ session_id: str, session: BrowserAutomationSession, snapshot: dict[str, Any]
304
+ ) -> BrowserSessionResponse:
305
+ return BrowserSessionResponse(
306
+ session_id=session_id,
307
+ active=True,
308
+ headless=session.headless,
309
+ viewport=session.viewport,
310
+ url=snapshot.get("url"),
311
+ title=snapshot.get("title"),
312
+ )
313
+
314
+
315
+ def _raise_browser_http_error(exc: Exception) -> NoReturn:
316
+ if isinstance(exc, HTTPException):
317
+ raise exc
318
+ if isinstance(exc, BrowserAutomationUnavailableError):
319
+ raise HTTPException(status_code=503, detail=str(exc)) from exc
320
+ if isinstance(exc, ValueError):
321
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
322
+ message = str(exc).strip() or "Browser automation darbība neizdevās."
323
+ lower = message.lower()
324
+ if "timeout" in lower:
325
+ raise HTTPException(status_code=504, detail=message) from exc
326
+ raise HTTPException(status_code=502, detail=message) from exc
327
+
328
+
329
+ @router.get("/capabilities", response_model=BrowserCapabilitiesResponse)
330
+ async def browser_capabilities() -> BrowserCapabilitiesResponse:
331
+ """Return the browser automation surface available in Maris."""
332
+ return get_browser_automation_capabilities()
333
+
334
+
335
+ @router.post("/sessions/start", response_model=BrowserSessionResponse)
336
+ async def start_browser_session(req: BrowserSessionStartRequest) -> BrowserSessionResponse:
337
+ """Create a safe, isolated browser automation session."""
338
+ if len(_browser_sessions) >= MAX_BROWSER_AUTOMATION_SESSIONS:
339
+ raise HTTPException(
340
+ status_code=429,
341
+ detail="Sasniegts maksimālais browser automation sesiju limits.",
342
+ )
343
+
344
+ viewport = {"width": req.viewport_width, "height": req.viewport_height}
345
+ session_id = str(uuid.uuid4())
346
+
347
+ try:
348
+ session = await _create_browser_session(headless=req.headless, viewport=viewport)
349
+ _browser_sessions[session_id] = session
350
+ snapshot = await session.snapshot()
351
+ return _snapshot_response(session_id, session, snapshot)
352
+ except _browser_operation_errors() as exc:
353
+ _browser_sessions.pop(session_id, None)
354
+ _raise_browser_http_error(exc)
355
+
356
+
357
+ @router.post("/navigate", response_model=BrowserSessionResponse)
358
+ async def navigate_browser(req: BrowserNavigateRequest) -> BrowserSessionResponse:
359
+ """Navigate an existing browser automation session to a URL."""
360
+ session = _get_browser_session(req.session_id)
361
+ try:
362
+ snapshot = await session.navigate(
363
+ req.url, wait_until=req.wait_until, timeout_ms=req.timeout_ms
364
+ )
365
+ return _snapshot_response(req.session_id, session, snapshot)
366
+ except _browser_operation_errors() as exc:
367
+ _raise_browser_http_error(exc)
368
+
369
+
370
+ @router.post("/click", response_model=BrowserSessionResponse)
371
+ async def click_browser(req: BrowserClickRequest) -> BrowserSessionResponse:
372
+ """Click an element in the current browser session."""
373
+ session = _get_browser_session(req.session_id)
374
+ try:
375
+ snapshot = await session.click(
376
+ req.selector,
377
+ timeout_ms=req.timeout_ms,
378
+ wait_until_after=req.wait_until_after,
379
+ )
380
+ return _snapshot_response(req.session_id, session, snapshot)
381
+ except _browser_operation_errors() as exc:
382
+ _raise_browser_http_error(exc)
383
+
384
+
385
+ @router.post("/fill", response_model=BrowserSessionResponse)
386
+ async def fill_browser(req: BrowserFillRequest) -> BrowserSessionResponse:
387
+ """Fill an input in the current browser session."""
388
+ session = _get_browser_session(req.session_id)
389
+ try:
390
+ snapshot = await session.fill(
391
+ req.selector,
392
+ req.value,
393
+ timeout_ms=req.timeout_ms,
394
+ submit=req.submit,
395
+ )
396
+ return _snapshot_response(req.session_id, session, snapshot)
397
+ except _browser_operation_errors() as exc:
398
+ _raise_browser_http_error(exc)
399
+
400
+
401
+ @router.post("/extract", response_model=BrowserTextResponse)
402
+ async def extract_browser_text(req: BrowserExtractRequest) -> BrowserTextResponse:
403
+ """Extract visible text from the page or a specific selector."""
404
+ session = _get_browser_session(req.session_id)
405
+ try:
406
+ snapshot = await session.snapshot()
407
+ text = await session.extract_text(
408
+ req.selector, timeout_ms=req.timeout_ms, max_length=req.max_length
409
+ )
410
+ return BrowserTextResponse(
411
+ session_id=req.session_id,
412
+ url=snapshot.get("url"),
413
+ title=snapshot.get("title"),
414
+ text=text,
415
+ )
416
+ except _browser_operation_errors() as exc:
417
+ _raise_browser_http_error(exc)
418
+
419
+
420
+ @router.post("/screenshot", response_model=BrowserScreenshotResponse)
421
+ async def screenshot_browser(req: BrowserScreenshotRequest) -> BrowserScreenshotResponse:
422
+ """Capture a PNG screenshot from the current browser session."""
423
+ session = _get_browser_session(req.session_id)
424
+ try:
425
+ snapshot = await session.snapshot()
426
+ png = await session.screenshot_png(full_page=req.full_page)
427
+ return BrowserScreenshotResponse(
428
+ session_id=req.session_id,
429
+ url=snapshot.get("url"),
430
+ title=snapshot.get("title"),
431
+ image_base64=base64.b64encode(png).decode("ascii"),
432
+ )
433
+ except _browser_operation_errors() as exc:
434
+ _raise_browser_http_error(exc)
435
+
436
+
437
+ @router.post("/sessions/close", response_model=BrowserSessionResponse)
438
+ async def close_browser_session(req: BrowserSessionRequest) -> BrowserSessionResponse:
439
+ """Close a browser automation session and release resources."""
440
+ session = _browser_sessions.pop(req.session_id, None)
441
+ if session is None:
442
+ raise HTTPException(status_code=404, detail="Browser automation sesija netika atrasta.")
443
+
444
+ try:
445
+ snapshot = await session.snapshot()
446
+ except _browser_operation_errors():
447
+ snapshot = {"url": None, "title": None}
448
+
449
+ try:
450
+ await session.close()
451
+ except _browser_operation_errors() as exc:
452
+ _raise_browser_http_error(exc)
453
+
454
+ return BrowserSessionResponse(
455
+ session_id=req.session_id,
456
+ active=False,
457
+ headless=session.headless,
458
+ viewport=session.viewport,
459
+ url=snapshot.get("url"),
460
+ title=snapshot.get("title"),
461
+ )
core-python/maris_core/code/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """__init__ for code module."""
core-python/maris_core/code/execution_eval.py ADDED
@@ -0,0 +1,380 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Execution-based code evaluation helpers for coder benchmarks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ import os
7
+ import re
8
+ import shutil
9
+ import sqlite3
10
+ import subprocess
11
+ import tempfile
12
+ from dataclasses import dataclass
13
+ from pathlib import Path
14
+
15
+ try:
16
+ import resource
17
+ except ImportError: # pragma: no cover - non-POSIX fallback
18
+ resource = None # type: ignore[assignment]
19
+
20
+ _CODE_BLOCK_RE = re.compile(r"```(?P<lang>[^\n`]*)\n(?P<code>.*?)```", re.DOTALL)
21
+ DEFAULT_EXECUTION_MEMORY_LIMIT_MB = 512
22
+ DEFAULT_EXECUTION_MAX_OUTPUT_CHARS = 12_000
23
+
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class CodeExecutionSpec:
27
+ language: str
28
+ test_code: str = ""
29
+ timeout_seconds: float = 8.0
30
+ compile_only: bool = False
31
+ memory_limit_mb: int = DEFAULT_EXECUTION_MEMORY_LIMIT_MB
32
+ max_output_chars: int = DEFAULT_EXECUTION_MAX_OUTPUT_CHARS
33
+
34
+
35
+ @dataclass(frozen=True, slots=True)
36
+ class CodeExecutionResult:
37
+ language: str
38
+ available: bool
39
+ passed: bool
40
+ summary: str
41
+ exit_code: int | None = None
42
+ stdout: str = ""
43
+ stderr: str = ""
44
+
45
+
46
+ def extract_code_block(text: str, language: str | None = None) -> str:
47
+ matches = list(_CODE_BLOCK_RE.finditer(text))
48
+ if not matches:
49
+ return text.strip()
50
+
51
+ normalized_language = (language or "").strip().lower()
52
+ if normalized_language:
53
+ for match in matches:
54
+ fence_language = match.group("lang").strip().lower()
55
+ if fence_language == normalized_language:
56
+ return match.group("code").strip()
57
+ return matches[0].group("code").strip()
58
+
59
+
60
+ def evaluate_code_response(response_text: str, spec: CodeExecutionSpec) -> CodeExecutionResult:
61
+ language = spec.language.strip().lower()
62
+ code = extract_code_block(response_text, language=language)
63
+ if not code:
64
+ return CodeExecutionResult(
65
+ language=language,
66
+ available=True,
67
+ passed=False,
68
+ summary="Atbildē nav atrasts izpildāms koda bloks.",
69
+ )
70
+
71
+ if language == "python":
72
+ python_path = shutil.which("python3") or shutil.which("python")
73
+ if python_path is None:
74
+ return _unsupported_language_result(language, "python nav pieejams.")
75
+ command = (
76
+ [python_path, "-I", "-B", "-s", "main.py"]
77
+ if not spec.compile_only
78
+ else [python_path, "-I", "-B", "-s", "-m", "py_compile", "main.py"]
79
+ )
80
+ return _run_script_eval(
81
+ language=language,
82
+ command=command,
83
+ file_name="main.py",
84
+ source=_build_source(code, spec.test_code, "#"),
85
+ spec=spec,
86
+ )
87
+ if language in {"javascript", "js"}:
88
+ node_path = shutil.which("node")
89
+ if node_path is None:
90
+ return _unsupported_language_result(language, "node nav pieejams.")
91
+ command = (
92
+ [node_path, "main.js"] if not spec.compile_only else [node_path, "--check", "main.js"]
93
+ )
94
+ return _run_script_eval(
95
+ language=language,
96
+ command=command,
97
+ file_name="main.js",
98
+ source=_build_source(code, spec.test_code, "//"),
99
+ spec=spec,
100
+ )
101
+ if language in {"typescript", "ts"}:
102
+ return _run_typescript_eval(code, spec)
103
+ if language in {"bash", "sh"}:
104
+ bash_path = shutil.which("bash")
105
+ if bash_path is None:
106
+ return _unsupported_language_result(language, "bash nav pieejams.")
107
+ command = [bash_path, "main.sh"] if not spec.compile_only else [bash_path, "-n", "main.sh"]
108
+ return _run_script_eval(
109
+ language=language,
110
+ command=command,
111
+ file_name="main.sh",
112
+ source=_build_source(code, spec.test_code, "#"),
113
+ spec=spec,
114
+ )
115
+ if language == "rust":
116
+ return _run_rust_eval(code, spec)
117
+ if language == "sql":
118
+ return _run_sql_eval(code, spec)
119
+
120
+ return _unsupported_language_result(
121
+ language, "Valoda execution evals režīmā vēl nav atbalstīta."
122
+ )
123
+
124
+
125
+ def _build_source(code: str, test_code: str, comment_prefix: str) -> str:
126
+ source = code.strip()
127
+ tests = test_code.strip()
128
+ if not tests:
129
+ return source + "\n"
130
+ return f"{source}\n\n{comment_prefix} execution harness\n{tests}\n"
131
+
132
+
133
+ def _run_script_eval(
134
+ *,
135
+ language: str,
136
+ command: list[str],
137
+ file_name: str,
138
+ source: str,
139
+ spec: CodeExecutionSpec,
140
+ ) -> CodeExecutionResult:
141
+ with tempfile.TemporaryDirectory(prefix="maris-code-eval-") as tmp_dir:
142
+ workspace = Path(tmp_dir)
143
+ file_path = workspace / file_name
144
+ file_path.write_text(source, encoding="utf-8")
145
+ result = _run_command(command, cwd=workspace, spec=spec, language=language)
146
+ if result is None:
147
+ return CodeExecutionResult(
148
+ language=language,
149
+ available=True,
150
+ passed=True,
151
+ summary=f"{language} kods izpildījās veiksmīgi.",
152
+ exit_code=0,
153
+ )
154
+ return result
155
+
156
+
157
+ def _run_typescript_eval(code: str, spec: CodeExecutionSpec) -> CodeExecutionResult:
158
+ tsc_path = shutil.which("tsc")
159
+ if tsc_path is None:
160
+ return _unsupported_language_result("typescript", "tsc nav pieejams.")
161
+ node_path = shutil.which("node")
162
+ if not spec.compile_only and node_path is None:
163
+ return _unsupported_language_result("typescript", "node nav pieejams TypeScript izpildei.")
164
+
165
+ with tempfile.TemporaryDirectory(prefix="maris-code-eval-") as tmp_dir:
166
+ workspace = Path(tmp_dir)
167
+ source_path = workspace / "main.ts"
168
+ source_path.write_text(_build_source(code, spec.test_code, "//"), encoding="utf-8")
169
+ compile_result = _run_command(
170
+ [
171
+ tsc_path,
172
+ "--pretty",
173
+ "false",
174
+ "--target",
175
+ "ES2020",
176
+ "--module",
177
+ "commonjs",
178
+ "main.ts",
179
+ ],
180
+ cwd=workspace,
181
+ spec=spec,
182
+ language="typescript",
183
+ )
184
+ if compile_result is not None:
185
+ return compile_result
186
+ if spec.compile_only:
187
+ return CodeExecutionResult(
188
+ language="typescript",
189
+ available=True,
190
+ passed=True,
191
+ summary="TypeScript kods veiksmīgi sakompilējās.",
192
+ exit_code=0,
193
+ )
194
+ run_result = _run_command(
195
+ [node_path, "main.js"], cwd=workspace, spec=spec, language="typescript"
196
+ )
197
+ if run_result is None:
198
+ return CodeExecutionResult(
199
+ language="typescript",
200
+ available=True,
201
+ passed=True,
202
+ summary="TypeScript kods veiksmīgi sakompilējās un izpildījās.",
203
+ exit_code=0,
204
+ )
205
+ return run_result
206
+
207
+
208
+ def _run_rust_eval(code: str, spec: CodeExecutionSpec) -> CodeExecutionResult:
209
+ rustc_path = shutil.which("rustc")
210
+ if rustc_path is None:
211
+ return _unsupported_language_result("rust", "rustc nav pieejams.")
212
+ with tempfile.TemporaryDirectory(prefix="maris-code-eval-") as tmp_dir:
213
+ workspace = Path(tmp_dir)
214
+ source_path = workspace / "main.rs"
215
+ binary_path = workspace / "main"
216
+ source_path.write_text(_build_source(code, spec.test_code, "//"), encoding="utf-8")
217
+ compile_result = _run_command(
218
+ [rustc_path, "main.rs", "-o", str(binary_path)],
219
+ cwd=workspace,
220
+ spec=spec,
221
+ language="rust",
222
+ )
223
+ if compile_result is not None:
224
+ return compile_result
225
+ if spec.compile_only:
226
+ return CodeExecutionResult(
227
+ language="rust",
228
+ available=True,
229
+ passed=True,
230
+ summary="Rust kods veiksmīgi sakompilējās.",
231
+ exit_code=0,
232
+ )
233
+ run_result = _run_command([str(binary_path)], cwd=workspace, spec=spec, language="rust")
234
+ if run_result is None:
235
+ return CodeExecutionResult(
236
+ language="rust",
237
+ available=True,
238
+ passed=True,
239
+ summary="Rust kods veiksmīgi sakompilējās un izpildījās.",
240
+ exit_code=0,
241
+ )
242
+ return run_result
243
+
244
+
245
+ def _run_sql_eval(code: str, spec: CodeExecutionSpec) -> CodeExecutionResult:
246
+ try:
247
+ with tempfile.TemporaryDirectory(prefix="maris-sql-eval-") as tmp_dir:
248
+ workspace = Path(tmp_dir)
249
+ connection = sqlite3.connect(":memory:")
250
+ try:
251
+ connection.execute("PRAGMA foreign_keys = ON")
252
+ script = _build_sql_script(code, spec.test_code, compile_only=spec.compile_only)
253
+ connection.executescript(script)
254
+ finally:
255
+ connection.close()
256
+ workspace.mkdir(parents=True, exist_ok=True)
257
+ except sqlite3.Error as exc:
258
+ return CodeExecutionResult(
259
+ language="sql",
260
+ available=True,
261
+ passed=False,
262
+ summary="SQL execution eval neizdevās.",
263
+ stderr=str(exc),
264
+ )
265
+ return CodeExecutionResult(
266
+ language="sql",
267
+ available=True,
268
+ passed=True,
269
+ summary="SQL skripts veiksmīgi validējās un izpildījās.",
270
+ exit_code=0,
271
+ )
272
+
273
+
274
+ def _build_sql_script(code: str, test_code: str, *, compile_only: bool) -> str:
275
+ candidate = code.strip().rstrip(";")
276
+ harness = test_code.strip()
277
+ if harness and "{{CODE}}" in harness:
278
+ return harness.replace("{{CODE}}", candidate)
279
+ if compile_only:
280
+ if harness:
281
+ return f"{harness}\nEXPLAIN QUERY PLAN {candidate};\n"
282
+ return f"EXPLAIN QUERY PLAN {candidate};\n"
283
+ if harness:
284
+ return f"{harness}\n{candidate};\n"
285
+ return candidate + ";\n"
286
+
287
+
288
+ def _run_command(
289
+ command: list[str],
290
+ *,
291
+ cwd: Path,
292
+ spec: CodeExecutionSpec,
293
+ language: str,
294
+ ) -> CodeExecutionResult | None:
295
+ try:
296
+ completed = subprocess.run( # noqa: S603
297
+ command,
298
+ cwd=str(cwd),
299
+ check=False,
300
+ capture_output=True,
301
+ text=True,
302
+ timeout=spec.timeout_seconds,
303
+ stdin=subprocess.DEVNULL,
304
+ env=_build_isolated_env(cwd),
305
+ preexec_fn=_build_subprocess_preexec(spec),
306
+ )
307
+ except subprocess.TimeoutExpired as exc:
308
+ return CodeExecutionResult(
309
+ language=language,
310
+ available=True,
311
+ passed=False,
312
+ summary="Execution eval pārsniedza laika limitu.",
313
+ stdout=_truncate_output(exc.stdout or "", spec.max_output_chars),
314
+ stderr=_truncate_output(exc.stderr or "", spec.max_output_chars),
315
+ )
316
+ if completed.returncode == 0:
317
+ return None
318
+ return CodeExecutionResult(
319
+ language=language,
320
+ available=True,
321
+ passed=False,
322
+ summary="Execution eval neizdevās.",
323
+ exit_code=completed.returncode,
324
+ stdout=_truncate_output(completed.stdout, spec.max_output_chars),
325
+ stderr=_truncate_output(completed.stderr, spec.max_output_chars),
326
+ )
327
+
328
+
329
+ def _build_isolated_env(workspace: Path) -> dict[str, str]:
330
+ env: dict[str, str] = {
331
+ "HOME": str(workspace),
332
+ "TMPDIR": str(workspace),
333
+ "TEMP": str(workspace),
334
+ "TMP": str(workspace),
335
+ "PYTHONNOUSERSITE": "1",
336
+ "PYTHONDONTWRITEBYTECODE": "1",
337
+ "PYTHONIOENCODING": "utf-8",
338
+ "NODE_DISABLE_COLORS": "1",
339
+ "CI": "1",
340
+ }
341
+ for key in ("PATH", "SYSTEMROOT", "SystemRoot", "WINDIR", "ComSpec"):
342
+ value = os.environ.get(key)
343
+ if value:
344
+ env[key] = value
345
+ return env
346
+
347
+
348
+ def _build_subprocess_preexec(spec: CodeExecutionSpec):
349
+ if os.name != "posix" or resource is None:
350
+ return None
351
+
352
+ memory_limit_bytes = max(spec.memory_limit_mb, 64) * 1024 * 1024
353
+ cpu_limit_seconds = max(2, math.ceil(spec.timeout_seconds) + 1)
354
+
355
+ def _apply_limits() -> None:
356
+ os.setsid()
357
+ resource.setrlimit(resource.RLIMIT_CPU, (cpu_limit_seconds, cpu_limit_seconds))
358
+ resource.setrlimit(resource.RLIMIT_AS, (memory_limit_bytes, memory_limit_bytes))
359
+ resource.setrlimit(resource.RLIMIT_CORE, (0, 0))
360
+ resource.setrlimit(resource.RLIMIT_FSIZE, (8 * 1024 * 1024, 8 * 1024 * 1024))
361
+ resource.setrlimit(resource.RLIMIT_NOFILE, (64, 64))
362
+ if hasattr(resource, "RLIMIT_NPROC"):
363
+ resource.setrlimit(resource.RLIMIT_NPROC, (32, 32))
364
+
365
+ return _apply_limits
366
+
367
+
368
+ def _truncate_output(value: str, max_chars: int) -> str:
369
+ if len(value) <= max_chars:
370
+ return value
371
+ return value[:max_chars] + "\n...[truncated]"
372
+
373
+
374
+ def _unsupported_language_result(language: str, reason: str) -> CodeExecutionResult:
375
+ return CodeExecutionResult(
376
+ language=language,
377
+ available=False,
378
+ passed=False,
379
+ summary=reason,
380
+ )
core-python/maris_core/code/fix_code.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """Koda labošana — atsevišķs modulis."""
2
+
3
+ from maris_core.code.generate_code import CodeResponse, FixCodeRequest, fix_code
4
+
5
+ __all__ = ["fix_code", "FixCodeRequest", "CodeResponse"]
core-python/maris_core/code/generate_code.py ADDED
@@ -0,0 +1,689 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Koda ģenerēšana un labošana ar Qwen3."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ import re
8
+ import zipfile
9
+ from pathlib import Path
10
+ from uuid import uuid4
11
+
12
+ from fastapi import APIRouter, HTTPException
13
+ from pydantic import BaseModel, Field
14
+
15
+ from maris_core.text.generate import (
16
+ DEFAULT_MAX_NEW_TOKENS,
17
+ call_generation_pipeline,
18
+ complete_with_hf_fallback,
19
+ get_pipeline,
20
+ )
21
+
22
+ logger = logging.getLogger(__name__)
23
+ router = APIRouter()
24
+ WORKSPACE_ARTIFACT_ROOT = Path("/tmp/maris-ai/generated-projects")
25
+ DEFAULT_REPO_ROOT = Path(__file__).resolve().parents[3]
26
+ _MAX_REPO_CONTEXT_FILES = 6
27
+ _MAX_REPO_CONTEXT_CHARS = 1800
28
+ _FENCED_BLOCK_PATTERN = re.compile(
29
+ r"```(?P<label>[^\n`]*)\n(?P<body>.*?)```",
30
+ flags=re.DOTALL,
31
+ )
32
+ _EDIT_INTENT_PATTERN = re.compile(
33
+ r"\b(edit|modify|update|refactor|fix|patch|rewrite|change|cleanup|labo|salabo|rediģē|pārstrādā|uzlabo)\b",
34
+ flags=re.IGNORECASE,
35
+ )
36
+ _PATH_HINT_PATTERN = re.compile(r"(?P<path>(?:/|\./|\.\./)[^\s,;:()\[\]{}<>]+)")
37
+ _REPO_RELATIVE_HINT_PATTERN = re.compile(
38
+ r"(?P<path>(?:[A-Za-z0-9_.-]+/)+[A-Za-z0-9_.-]+\.[A-Za-z0-9_.-]+)"
39
+ )
40
+ _STACK_KEYWORDS: dict[str, tuple[str, ...]] = {
41
+ "nextjs": ("next.js", "nextjs", " app router", "next app"),
42
+ "react": ("react", "vite", "tsx component"),
43
+ "rust": ("rust", "cargo", "actix", "axum"),
44
+ "python": ("python", "fastapi", "flask", "django", "pytest"),
45
+ "web": ("html", "css", "landing page", "calculator", "kalkulator", "web app", "web-app"),
46
+ }
47
+ _STACK_DISPLAY_NAMES = {
48
+ "nextjs": "Next.js (TypeScript)",
49
+ "react": "React (TypeScript)",
50
+ "rust": "Rust",
51
+ "python": "Python",
52
+ "web": "HTML/CSS/JavaScript",
53
+ }
54
+ _STACK_ENTRYPOINTS = {
55
+ "nextjs": "app/page.tsx",
56
+ "react": "src/App.tsx",
57
+ "rust": "src/main.rs",
58
+ "python": "src/main.py",
59
+ "web": "index.html",
60
+ }
61
+
62
+
63
+ class CodeRequest(BaseModel):
64
+ prompt: str
65
+ language: str = "Python"
66
+ context: str = ""
67
+ repo_path: str | None = None
68
+ fallback_model: str | None = None
69
+ max_new_tokens: int = Field(default=DEFAULT_MAX_NEW_TOKENS, ge=32)
70
+
71
+
72
+ class FixCodeRequest(BaseModel):
73
+ code: str
74
+ error_message: str = ""
75
+ language: str = "Python"
76
+
77
+
78
+ class ProjectFile(BaseModel):
79
+ path: str
80
+ content: str
81
+ absolute_path: str | None = None
82
+
83
+
84
+ class CodeResponse(BaseModel):
85
+ code: str
86
+ explanation: str
87
+ language: str
88
+ detected_stack: str
89
+ files: list[ProjectFile] = Field(default_factory=list)
90
+ workspace_artifact_dir: str | None = None
91
+ bundle_path: str | None = None
92
+ entrypoint: str | None = None
93
+ repo_path: str | None = None
94
+
95
+
96
+ class RepoContext(BaseModel):
97
+ repo_path: str
98
+ files: list[str] = Field(default_factory=list)
99
+
100
+
101
+ def _extract_code_block(text: str, language: str) -> tuple[str, str]:
102
+ """Izvelk koda bloku un paskaidrojumu no LLM atbildes."""
103
+ del language
104
+ lines = text.split("\n")
105
+ code_lines: list[str] = []
106
+ explanation_lines: list[str] = []
107
+ in_code = False
108
+
109
+ for line in lines:
110
+ if line.strip().startswith("```"):
111
+ in_code = not in_code
112
+ continue
113
+ if in_code:
114
+ code_lines.append(line)
115
+ else:
116
+ explanation_lines.append(line)
117
+
118
+ code = "\n".join(code_lines).strip()
119
+ explanation = "\n".join(explanation_lines).strip()
120
+
121
+ if not code:
122
+ code = text.strip()
123
+ explanation = ""
124
+
125
+ return code, explanation
126
+
127
+
128
+ def _extract_structured_project_payload(text: str) -> dict[str, object] | None:
129
+ candidates: list[str] = []
130
+ stripped = text.strip()
131
+ if stripped.startswith("{") and stripped.endswith("}"):
132
+ candidates.append(stripped)
133
+ for match in _FENCED_BLOCK_PATTERN.finditer(text):
134
+ label = match.group("label").strip().lower()
135
+ if "json" in label:
136
+ candidates.append(match.group("body").strip())
137
+ for candidate in candidates:
138
+ try:
139
+ payload = json.loads(candidate)
140
+ except json.JSONDecodeError:
141
+ continue
142
+ if isinstance(payload, dict) and isinstance(payload.get("files"), list):
143
+ return payload
144
+ return None
145
+
146
+
147
+ def _sanitize_relative_path(path: str) -> str:
148
+ candidate = Path(path.strip().replace("\\", "/"))
149
+ cleaned_parts = [part for part in candidate.parts if part not in {"", ".", ".."}]
150
+ if not cleaned_parts:
151
+ return "main.txt"
152
+ return Path(*cleaned_parts).as_posix()
153
+
154
+
155
+ def _slugify_prompt(prompt: str) -> str:
156
+ tokens = re.findall(r"[a-z0-9]+", prompt.lower())
157
+ return "-".join(tokens[:6]) or "generated-project"
158
+
159
+
160
+ def _looks_like_repo_edit_request(prompt: str) -> bool:
161
+ return bool(_EDIT_INTENT_PATTERN.search(prompt))
162
+
163
+
164
+ def _extract_repo_path_from_prompt(prompt: str) -> Path | None:
165
+ for match in _PATH_HINT_PATTERN.finditer(prompt):
166
+ candidate = Path(match.group("path")).expanduser()
167
+ try:
168
+ resolved = candidate.resolve()
169
+ except OSError:
170
+ continue
171
+ if resolved.is_file():
172
+ resolved = resolved.parent
173
+ if resolved.is_dir():
174
+ return resolved
175
+ return None
176
+
177
+
178
+ def _resolve_repo_path(raw_repo_path: str | None, prompt: str) -> Path | None:
179
+ if raw_repo_path:
180
+ candidate = Path(raw_repo_path).expanduser()
181
+ try:
182
+ resolved = candidate.resolve()
183
+ except OSError:
184
+ resolved = candidate
185
+ if resolved.is_file():
186
+ resolved = resolved.parent
187
+ if resolved.is_dir():
188
+ return resolved
189
+
190
+ extracted = _extract_repo_path_from_prompt(prompt)
191
+ if extracted is not None:
192
+ return extracted
193
+
194
+ if _looks_like_repo_edit_request(prompt) and DEFAULT_REPO_ROOT.exists():
195
+ return DEFAULT_REPO_ROOT
196
+ return None
197
+
198
+
199
+ def _read_json(path: Path) -> dict[str, object]:
200
+ try:
201
+ return json.loads(path.read_text(encoding="utf-8"))
202
+ except (OSError, json.JSONDecodeError):
203
+ return {}
204
+
205
+
206
+ def _detect_stack(prompt: str, requested_language: str, repo_path: Path | None) -> str:
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()} "
223
+ for stack, keywords in _STACK_KEYWORDS.items():
224
+ if any(keyword in normalized_prompt for keyword in keywords):
225
+ return stack
226
+
227
+ normalized_language = requested_language.strip().lower()
228
+ if "next" in normalized_language:
229
+ return "nextjs"
230
+ if "html/css/javascript" in normalized_language:
231
+ return "web"
232
+ if "react" in normalized_language or "typescript" in normalized_language:
233
+ return "react"
234
+ if "javascript" in normalized_language:
235
+ return "web"
236
+ if "rust" in normalized_language:
237
+ return "rust"
238
+ return "python"
239
+
240
+
241
+ def _display_language_for_stack(requested_language: str, detected_stack: str) -> str:
242
+ if requested_language.strip() and requested_language.strip().lower() != "python":
243
+ if detected_stack in {"nextjs", "react"}:
244
+ return _STACK_DISPLAY_NAMES[detected_stack]
245
+ if detected_stack == "rust" and "rust" in requested_language.strip().lower():
246
+ return "Rust"
247
+ if detected_stack == "python" and "python" in requested_language.strip().lower():
248
+ return "Python"
249
+ return _STACK_DISPLAY_NAMES.get(detected_stack, requested_language)
250
+
251
+
252
+ def _default_entrypoint_for_stack(detected_stack: str, repo_path: Path | None) -> str:
253
+ if repo_path is not None:
254
+ candidates = {
255
+ "nextjs": ["app/page.tsx", "pages/index.tsx", "src/app/page.tsx"],
256
+ "react": ["src/App.tsx", "src/main.tsx", "src/App.jsx"],
257
+ "rust": ["src/main.rs"],
258
+ "python": ["src/main.py", "main.py", "app/main.py"],
259
+ }.get(detected_stack, [])
260
+ for candidate in candidates:
261
+ if (repo_path / candidate).exists():
262
+ return candidate
263
+ return _STACK_ENTRYPOINTS.get(detected_stack, "main.py")
264
+
265
+
266
+ def _stack_scaffold_templates(detected_stack: str) -> dict[str, str]:
267
+ if detected_stack == "nextjs":
268
+ package_json = {
269
+ "name": "maris-next-app",
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",
277
+ "tsconfig.json": json.dumps(
278
+ {
279
+ "compilerOptions": {
280
+ "target": "ES2022",
281
+ "lib": ["dom", "dom.iterable", "es2022"],
282
+ "allowJs": False,
283
+ "skipLibCheck": True,
284
+ "strict": True,
285
+ "noEmit": True,
286
+ "module": "esnext",
287
+ "moduleResolution": "bundler",
288
+ "resolveJsonModule": True,
289
+ "isolatedModules": True,
290
+ "jsx": "preserve",
291
+ "incremental": True,
292
+ },
293
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
294
+ "exclude": ["node_modules"],
295
+ },
296
+ ensure_ascii=False,
297
+ indent=2,
298
+ )
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":
306
+ package_json = {
307
+ "name": "maris-react-app",
308
+ "private": True,
309
+ "type": "module",
310
+ "scripts": {"dev": "vite", "build": "vite build", "preview": "vite preview"},
311
+ "dependencies": {"react": "18.3.1", "react-dom": "18.3.1"},
312
+ "devDependencies": {
313
+ "typescript": "5.6.3",
314
+ "vite": "5.4.8",
315
+ "@vitejs/plugin-react": "4.3.1",
316
+ "@types/react": "18.3.3",
317
+ "@types/react-dom": "18.3.0",
318
+ },
319
+ }
320
+ return {
321
+ "package.json": json.dumps(package_json, ensure_ascii=False, indent=2) + "\n",
322
+ "tsconfig.json": json.dumps(
323
+ {
324
+ "compilerOptions": {
325
+ "target": "ES2020",
326
+ "useDefineForClassFields": True,
327
+ "lib": ["DOM", "DOM.Iterable", "ES2020"],
328
+ "allowJs": False,
329
+ "skipLibCheck": True,
330
+ "esModuleInterop": True,
331
+ "allowSyntheticDefaultImports": True,
332
+ "strict": True,
333
+ "module": "ESNext",
334
+ "moduleResolution": "bundler",
335
+ "resolveJsonModule": True,
336
+ "isolatedModules": True,
337
+ "noEmit": True,
338
+ "jsx": "react-jsx",
339
+ },
340
+ "include": ["src"],
341
+ },
342
+ ensure_ascii=False,
343
+ indent=2,
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
+
365
+
366
+ 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
+
374
+ def _ensure_stack_scaffold(
375
+ files: list[ProjectFile],
376
+ *,
377
+ detected_stack: str,
378
+ entrypoint: str | None,
379
+ repo_path: Path | None,
380
+ ) -> tuple[list[ProjectFile], str]:
381
+ normalized_files = _normalize_files(files)
382
+ resolved_entrypoint = _sanitize_relative_path(
383
+ entrypoint or _default_entrypoint_for_stack(detected_stack, repo_path)
384
+ )
385
+
386
+ if repo_path is not None:
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:
404
+ file_map[path] = ProjectFile(path=path, content=content)
405
+
406
+ ordered_paths = list(templates.keys()) + [path for path in file_map if path not in templates]
407
+ ordered_files = [file_map[path] for path in ordered_paths]
408
+ return ordered_files, resolved_entrypoint
409
+
410
+
411
+ def _extract_project_files(
412
+ text: str,
413
+ *,
414
+ language: str,
415
+ fallback_entrypoint: str = "main.py",
416
+ ) -> tuple[list[ProjectFile], str | None, str]:
417
+ payload = _extract_structured_project_payload(text)
418
+ if payload is not None:
419
+ files_payload = payload.get("files")
420
+ files: list[ProjectFile] = []
421
+ if isinstance(files_payload, list):
422
+ for item in files_payload:
423
+ if not isinstance(item, dict):
424
+ continue
425
+ raw_path = str(item.get("path", "")).strip()
426
+ raw_content = str(item.get("content", ""))
427
+ if not raw_path or not raw_content.strip():
428
+ continue
429
+ files.append(
430
+ ProjectFile(
431
+ path=_sanitize_relative_path(raw_path),
432
+ content=raw_content,
433
+ )
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():
443
+ return [], None, explanation
444
+ return [ProjectFile(path=fallback_entrypoint, content=code)], fallback_entrypoint, explanation
445
+
446
+
447
+ def _select_primary_code(files: list[ProjectFile], entrypoint: str | None) -> str:
448
+ if entrypoint:
449
+ for file in files:
450
+ if file.path == entrypoint:
451
+ return file.content
452
+ return files[0].content if files else ""
453
+
454
+
455
+ def _extract_repo_relative_hints(prompt: str, repo_path: Path) -> list[str]:
456
+ hinted_paths: list[str] = []
457
+ repo_root = repo_path.resolve()
458
+ for pattern in (_PATH_HINT_PATTERN, _REPO_RELATIVE_HINT_PATTERN):
459
+ for match in pattern.finditer(prompt):
460
+ raw_path = match.group("path").strip()
461
+ candidate = repo_path / raw_path if not raw_path.startswith("/") else Path(raw_path)
462
+ try:
463
+ resolved = candidate.resolve()
464
+ except OSError:
465
+ continue
466
+ try:
467
+ relative_path = resolved.relative_to(repo_root).as_posix()
468
+ except ValueError:
469
+ continue
470
+ if resolved.is_file() and relative_path not in hinted_paths:
471
+ hinted_paths.append(relative_path)
472
+ return hinted_paths
473
+
474
+
475
+ def _repo_context_candidates(repo_path: Path, detected_stack: str, prompt: str = "") -> list[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
+ )
486
+ existing: list[str] = []
487
+ for candidate in candidates:
488
+ if (repo_path / candidate).exists() and candidate not in existing:
489
+ existing.append(candidate)
490
+ if len(existing) >= _MAX_REPO_CONTEXT_FILES:
491
+ break
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)
499
+ if not files:
500
+ return RepoContext(repo_path=str(repo_path), files=[])
501
+ excerpts: list[str] = []
502
+ for relative_path in files:
503
+ try:
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
+
516
+ def _build_system_prompt(
517
+ *,
518
+ language: str,
519
+ detected_stack: str,
520
+ repo_context: RepoContext | None,
521
+ ) -> str:
522
+ stack_label = _STACK_DISPLAY_NAMES.get(detected_stack, language)
523
+ prompt = (
524
+ f"Tu esi Maris AI — eksperts programmētājs. Uzraksti {language} kodu ar pareizu {stack_label} projekta struktūru. "
525
+ "Dod production-ready risinājumu ar drošiem noklusējumiem, ievades validāciju un saprātīgu kļūdu apstrādi, ja tas attiecas uz uzdevumu. "
526
+ "Nepiedāvā pseidokodu, ja vien lietotājs to tieši neprasa. "
527
+ "Atbildi ar īsu paskaidrojumu, kurā nosauc edge cases un kā risinājumu pārbaudīt. "
528
+ "Ja pieprasījums ir par lietotni, UI vai workspace artefaktiem, atdod pilnu izpildāmu artefaktu. "
529
+ "Kad risinājums satur vienu vai vairākus failus, atbildi ar vienu ```json``` bloku formā "
530
+ '{"explanation":"...","entrypoint":"...","files":[{"path":"...","content":"..."}]} '
531
+ "un pārliecinies, ka files satur gala failus ar reālu saturu bez TODO placeholderiem. "
532
+ f"Primāri mērķē uz {stack_label} stacku un izmanto šim stackam idiomātiskus failu nosaukumus."
533
+ )
534
+ if repo_context is not None:
535
+ prompt += (
536
+ " Šī ir repo-aware rediģēšana esošam projektam: izmanto precīzus repo-relatīvos failu ceļus, "
537
+ "rediģē tikai nepieciešamos failus, saglabā esošo projekta struktūru un, ja prompt piesauc konkrētus failus, balsti risinājumu tieši uz tiem."
538
+ )
539
+ return prompt
540
+
541
+
542
+ def _materialize_workspace_artifacts(
543
+ files: list[ProjectFile],
544
+ *,
545
+ prompt: str,
546
+ ) -> str | None:
547
+ if not files:
548
+ return None
549
+ artifact_dir = WORKSPACE_ARTIFACT_ROOT / f"{_slugify_prompt(prompt)}-{uuid4().hex[:8]}"
550
+ artifact_dir.mkdir(parents=True, exist_ok=True)
551
+ artifact_root = artifact_dir.resolve()
552
+ for file in files:
553
+ relative = Path(_sanitize_relative_path(file.path))
554
+ target = (artifact_root / relative).resolve()
555
+ if not str(target).startswith(str(artifact_root)):
556
+ raise HTTPException(status_code=400, detail="Nederīgs ģenerētā faila ceļš.")
557
+ target.parent.mkdir(parents=True, exist_ok=True)
558
+ target.write_text(file.content, encoding="utf-8")
559
+ file.absolute_path = str(target)
560
+ return str(artifact_root)
561
+
562
+
563
+ def _create_bundle_zip(workspace_artifact_dir: str | None) -> str | None:
564
+ if not workspace_artifact_dir:
565
+ return None
566
+ artifact_root = Path(workspace_artifact_dir)
567
+ if not artifact_root.exists():
568
+ return None
569
+ bundle_path = artifact_root.with_suffix(".zip")
570
+ with zipfile.ZipFile(bundle_path, mode="w", compression=zipfile.ZIP_DEFLATED) as bundle:
571
+ for path in sorted(artifact_root.rglob("*")):
572
+ if path.is_file():
573
+ bundle.write(path, arcname=path.relative_to(artifact_root))
574
+ return str(bundle_path)
575
+
576
+
577
+ @router.post("/generate", response_model=CodeResponse)
578
+ async def generate_code(req: CodeRequest) -> CodeResponse:
579
+ """Ģenerē kodu pēc apraksta."""
580
+ from maris_core.utils.hf_integration import HFIntegration
581
+
582
+ hf = HFIntegration()
583
+ repo_path = _resolve_repo_path(req.repo_path, req.prompt)
584
+ detected_stack = _detect_stack(req.prompt, req.language, repo_path)
585
+ resolved_language = _display_language_for_stack(req.language, detected_stack)
586
+ repo_context = _build_repo_context(repo_path, detected_stack, req.prompt)
587
+ fallback_entrypoint = _default_entrypoint_for_stack(detected_stack, repo_path)
588
+ system_prompt = _build_system_prompt(
589
+ language=resolved_language,
590
+ detected_stack=detected_stack,
591
+ repo_context=repo_context,
592
+ )
593
+
594
+ messages = [
595
+ {"role": "system", "content": system_prompt},
596
+ {"role": "user", "content": req.prompt},
597
+ ]
598
+
599
+ if repo_context is not None:
600
+ repo_context_content = (
601
+ f"Repo sakne: {repo_context.repo_path}\n"
602
+ f"Esošie svarīgie faili: {', '.join(_repo_context_candidates(repo_path or DEFAULT_REPO_ROOT, detected_stack, req.prompt)) or 'nav'}"
603
+ )
604
+ if repo_context.files:
605
+ repo_context_content += f"\n\n{repo_context.files[0]}"
606
+ messages.insert(1, {"role": "user", "content": repo_context_content})
607
+
608
+ if req.context:
609
+ messages.insert(1, {"role": "user", "content": f"Konteksts:\n{req.context}"})
610
+
611
+ pipe = get_pipeline()
612
+ raw_response: str | None = None
613
+ if pipe is not None:
614
+ try:
615
+ out = call_generation_pipeline(
616
+ pipe,
617
+ messages,
618
+ max_new_tokens=req.max_new_tokens,
619
+ temperature=0.2,
620
+ )
621
+ raw_response = out[0]["generated_text"][-1]["content"]
622
+ except Exception as exc: # noqa: BLE001
623
+ logger.error("Koda ģenerēšanas kļūda: %s", exc)
624
+
625
+ if raw_response is None:
626
+ fallback_result = complete_with_hf_fallback(
627
+ messages,
628
+ fallback_model=req.fallback_model,
629
+ max_new_tokens=req.max_new_tokens,
630
+ temperature=0.2,
631
+ )
632
+ if fallback_result is not None:
633
+ _, raw_response = fallback_result
634
+ else:
635
+ raise HTTPException(
636
+ status_code=503,
637
+ detail="Maris AI koda ģenerēšana šobrīd nav pieejama.",
638
+ )
639
+
640
+ files, entrypoint, structured_explanation = _extract_project_files(
641
+ raw_response,
642
+ language=resolved_language,
643
+ fallback_entrypoint=fallback_entrypoint,
644
+ )
645
+ files, entrypoint = _ensure_stack_scaffold(
646
+ files,
647
+ detected_stack=detected_stack,
648
+ entrypoint=entrypoint,
649
+ repo_path=repo_path,
650
+ )
651
+ code, explanation = _extract_code_block(raw_response, resolved_language)
652
+ if files:
653
+ code = _select_primary_code(files, entrypoint)
654
+ if structured_explanation:
655
+ explanation = structured_explanation
656
+ workspace_artifact_dir = _materialize_workspace_artifacts(files, prompt=req.prompt)
657
+ bundle_path = _create_bundle_zip(workspace_artifact_dir)
658
+ detected_stack_label = _STACK_DISPLAY_NAMES.get(detected_stack, detected_stack)
659
+ await hf.save_generation(
660
+ "code",
661
+ req.prompt,
662
+ {
663
+ "language": resolved_language,
664
+ "detected_stack": detected_stack_label,
665
+ "repo_path": str(repo_path) if repo_path is not None else None,
666
+ "fallback_model": req.fallback_model,
667
+ },
668
+ )
669
+
670
+ return CodeResponse(
671
+ code=code,
672
+ explanation=explanation,
673
+ language=resolved_language,
674
+ detected_stack=detected_stack_label,
675
+ files=files,
676
+ workspace_artifact_dir=workspace_artifact_dir,
677
+ bundle_path=bundle_path,
678
+ entrypoint=entrypoint,
679
+ repo_path=str(repo_path) if repo_path is not None else None,
680
+ )
681
+
682
+
683
+ @router.post("/fix", response_model=CodeResponse)
684
+ async def fix_code(req: FixCodeRequest) -> CodeResponse:
685
+ """Labo kļūdainu kodu."""
686
+ prompt = f"Labo šo {req.language} kodu:\n```\n{req.code}\n```" + (
687
+ f"\nKļūda: {req.error_message}" if req.error_message else ""
688
+ )
689
+ return await generate_code(CodeRequest(prompt=prompt, language=req.language))
core-python/maris_core/data/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """__init__ for data module."""
core-python/maris_core/data/augment.py ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Datu papildināšana (augmentation)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ import random
7
+ import re
8
+
9
+ WORD_RE = re.compile(r"\b[\w'-]+\b", re.UNICODE)
10
+ SYNONYM_MAP: dict[str, tuple[str, ...]] = {
11
+ "ātrs": ("žigls", "straujš"),
12
+ "attēls": ("bilde", "vizualizācija"),
13
+ "bilde": ("attēls",),
14
+ "big": ("large", "sizable"),
15
+ "code": ("program", "source"),
16
+ "create": ("build", "make"),
17
+ "fast": ("quick", "rapid"),
18
+ "generate": ("produce", "create"),
19
+ "good": ("solid", "reliable"),
20
+ "idea": ("concept", "approach"),
21
+ "image": ("picture", "visual"),
22
+ "intelligent": ("smart", "capable"),
23
+ "liels": ("apjomīgs", "ievērojams"),
24
+ "mazs": ("neliels", "kompakts"),
25
+ "prompt": ("instruction", "request"),
26
+ "quick": ("fast", "rapid"),
27
+ "small": ("compact", "lightweight"),
28
+ "smart": ("capable", "intelligent"),
29
+ "strong": ("robust", "powerful"),
30
+ "text": ("content", "message"),
31
+ }
32
+ BACK_TRANSLATION_MAP: dict[str, dict[str, str]] = {
33
+ "en": {
34
+ "because": "since",
35
+ "create": "build",
36
+ "fast": "quick",
37
+ "good": "solid",
38
+ "help": "assist",
39
+ "plan": "outline",
40
+ },
41
+ "lv": {
42
+ "ātrs": "žigls",
43
+ "izveidot": "radīt",
44
+ "labs": "stabils",
45
+ "palīdzēt": "atbalstīt",
46
+ "plāns": "ieceres plāns",
47
+ },
48
+ }
49
+
50
+
51
+ def _match_case(source: str, replacement: str) -> str:
52
+ if source.isupper():
53
+ return replacement.upper()
54
+ if source[:1].isupper():
55
+ return replacement.capitalize()
56
+ return replacement
57
+
58
+
59
+ def _lookup_synonym(word: str) -> str | None:
60
+ synonyms = SYNONYM_MAP.get(word.casefold())
61
+ if not synonyms:
62
+ return None
63
+ return _match_case(word, random.choice(synonyms))
64
+
65
+
66
+ def random_synonym_swap(text: str, rate: float = 0.1) -> str:
67
+ """Aizstāj daļu vārdu ar iebūvētiem sinonīmiem bez ārējām atkarībām."""
68
+ if not text.strip():
69
+ return text
70
+
71
+ words = list(WORD_RE.finditer(text))
72
+ if not words:
73
+ return text
74
+
75
+ candidates = [
76
+ (match.start(), match.end(), synonym)
77
+ for match in words
78
+ if (synonym := _lookup_synonym(match.group(0))) is not None
79
+ ]
80
+ if not candidates or rate <= 0:
81
+ return text
82
+
83
+ sample_size = min(len(candidates), max(1, math.ceil(len(words) * min(rate, 1.0))))
84
+ selected = {
85
+ (start, end): replacement
86
+ for start, end, replacement in random.sample(candidates, sample_size)
87
+ }
88
+
89
+ parts: list[str] = []
90
+ cursor = 0
91
+ for match in words:
92
+ span = (match.start(), match.end())
93
+ replacement = selected.get(span)
94
+ if replacement is None:
95
+ continue
96
+ parts.append(text[cursor : match.start()])
97
+ parts.append(replacement)
98
+ cursor = match.end()
99
+ if cursor == 0:
100
+ return text
101
+
102
+ parts.append(text[cursor:])
103
+ return "".join(parts)
104
+
105
+
106
+ def back_translate(text: str, lang: str = "en") -> str:
107
+ """Viegls offline paraphrase fallback vietā, kur nav tulkošanas API."""
108
+ if not text.strip():
109
+ return text
110
+
111
+ replacements = BACK_TRANSLATION_MAP.get(lang.strip().lower(), {})
112
+ augmented = text
113
+ for source, target in replacements.items():
114
+ augmented = re.sub(
115
+ rf"\b{re.escape(source)}\b",
116
+ lambda match, replacement=target: _match_case(match.group(0), replacement),
117
+ augmented,
118
+ flags=re.IGNORECASE,
119
+ )
120
+
121
+ if augmented == text:
122
+ return random_synonym_swap(text, rate=0.2)
123
+ return augmented
core-python/maris_core/data/datasets.py ADDED
@@ -0,0 +1,561 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Datasets konfigurācija un ielāde."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import errno
6
+ import json
7
+ import logging
8
+ import os
9
+ import tempfile
10
+ from collections.abc import Callable, Iterator
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from maris_core.data.preprocessing import record_to_training_text
15
+ from maris_core.utils.env import get_env_any, get_env_any_or_default
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ _SUPPORTED_DATASET_FORMATS = {
20
+ ".json": "json",
21
+ ".jsonl": "json",
22
+ ".csv": "csv",
23
+ ".parquet": "parquet",
24
+ }
25
+
26
+
27
+ class HFDatasetError(FileNotFoundError):
28
+ """Atmiņas repozitorija konfigurācijas vai satura kļūda."""
29
+
30
+ def __init__(self, message: str, *, discovered_files: list[str] | None = None) -> None:
31
+ super().__init__(message)
32
+ self.discovered_files = discovered_files or []
33
+
34
+
35
+ def _iter_exception_chain(exc: Exception) -> Iterator[BaseException]:
36
+ """Iziet cauri izņēmuma cēloņu ķēdei bez cikliem."""
37
+ pending: list[BaseException] = [exc]
38
+ seen: set[int] = set()
39
+
40
+ while pending:
41
+ current = pending.pop()
42
+ current_id = id(current)
43
+ if current_id in seen:
44
+ continue
45
+ seen.add(current_id)
46
+ yield current
47
+
48
+ cause = getattr(current, "__cause__", None)
49
+ context = getattr(current, "__context__", None)
50
+ if cause is not None:
51
+ pending.append(cause)
52
+ if context is not None:
53
+ pending.append(context)
54
+
55
+
56
+ def _is_empty_dataset_error(exc: Exception) -> bool:
57
+ """Nosaka, vai repozitorijam trūkst tieši ielādējamu datu failu."""
58
+ markers = (
59
+ "doesn't contain any data files",
60
+ "No (supported) data files found in",
61
+ )
62
+ return any(
63
+ current.__class__.__name__ == "EmptyDatasetError"
64
+ or any(marker in str(current) for marker in markers)
65
+ for current in _iter_exception_chain(exc)
66
+ )
67
+
68
+
69
+ def _is_schema_cast_error(exc: Exception) -> bool:
70
+ """Nosaka, vai JSON loader nokrīt uz mainīgu objektu shēmu."""
71
+ markers = (
72
+ "Couldn't cast",
73
+ "Couldn't cast array of type",
74
+ )
75
+ return any(
76
+ any(marker in str(current) for marker in markers) for current in _iter_exception_chain(exc)
77
+ )
78
+
79
+
80
+ def _is_dataset_generation_error(exc: Exception) -> bool:
81
+ """Nosaka, vai datasets slānis meta ģenerisku DatasetGenerationError."""
82
+ return any(
83
+ current.__class__.__name__ == "DatasetGenerationError"
84
+ for current in _iter_exception_chain(exc)
85
+ )
86
+
87
+
88
+ def _should_fallback_to_snapshot(exc: Exception) -> bool:
89
+ """Nosaka, vai jāizmanto snapshot-based datu ielāde apmācībai."""
90
+ return (
91
+ _is_empty_dataset_error(exc)
92
+ or _is_schema_cast_error(exc)
93
+ or _is_dataset_generation_error(exc)
94
+ )
95
+
96
+
97
+ def _is_hf_cache_lock_io_error(exc: Exception) -> bool:
98
+ """Nosaka, vai Hugging Face cache lock failu sistēmā radās I/O kļūda."""
99
+ for current in _iter_exception_chain(exc):
100
+ if not isinstance(current, OSError):
101
+ continue
102
+
103
+ message = str(current)
104
+ filename = os.fspath(getattr(current, "filename", "") or "")
105
+ details = " ".join(part for part in (filename, message) if part).lower()
106
+ if "huggingface" not in details and "datasets--" not in details:
107
+ continue
108
+ if "lock" not in details:
109
+ continue
110
+ if current.errno == errno.EIO or "input/output error" in details:
111
+ return True
112
+ return False
113
+
114
+
115
+ def _resolve_hf_cache_recovery_dir() -> str:
116
+ """Atgriež drošu fallback cache direktoriju HF lejupielādēm."""
117
+ configured = get_env_any("MARIS_HF_FALLBACK_CACHE_DIR")
118
+ fallback_dir = (Path(tempfile.gettempdir()) / "maris-hf-cache").resolve()
119
+ candidate = Path(configured).expanduser() if configured else fallback_dir
120
+
121
+ try:
122
+ resolved = candidate.resolve(strict=False)
123
+ if resolved == Path(resolved.anchor):
124
+ raise ValueError(f"nedroša cache direktorija: {resolved}")
125
+ if resolved.exists() and not resolved.is_dir():
126
+ raise ValueError(f"cache ceļš nav direktorija: {resolved}")
127
+ resolved.mkdir(parents=True, exist_ok=True)
128
+ return str(resolved)
129
+ except (OSError, ValueError) as exc:
130
+ if configured:
131
+ logger.warning(
132
+ "Ignorējam nederīgu MARIS_HF_FALLBACK_CACHE_DIR=%s: %s; lietojam %s.",
133
+ configured,
134
+ exc,
135
+ fallback_dir,
136
+ )
137
+ fallback_dir.mkdir(parents=True, exist_ok=True)
138
+ return str(fallback_dir)
139
+
140
+
141
+ def _call_with_hf_cache_recovery(
142
+ action: str,
143
+ call: Callable[..., Any],
144
+ *args: Any,
145
+ recovery_cache_dir: str | None = None,
146
+ **kwargs: Any,
147
+ ) -> tuple[Any, str | None]:
148
+ """Izsauc HF funkciju un pie cache lock I/O kļūdas pārslēdzas uz fallback cache."""
149
+ call_kwargs = dict(kwargs)
150
+ if recovery_cache_dir is not None:
151
+ call_kwargs["cache_dir"] = recovery_cache_dir
152
+ return call(*args, **call_kwargs), recovery_cache_dir
153
+
154
+ try:
155
+ return call(*args, **call_kwargs), None
156
+ except Exception as exc: # noqa: BLE001
157
+ if not _is_hf_cache_lock_io_error(exc):
158
+ raise
159
+
160
+ recovery_cache_dir = _resolve_hf_cache_recovery_dir()
161
+ logger.warning(
162
+ "Hugging Face cache lock kļūda (%s); atkārtojam ar fallback cache %s.",
163
+ action,
164
+ recovery_cache_dir,
165
+ )
166
+ call_kwargs["cache_dir"] = recovery_cache_dir
167
+ return call(*args, **call_kwargs), recovery_cache_dir
168
+
169
+
170
+ def _preview_discovered_files(discovered_files: list[str]) -> str:
171
+ """Atgriež īsu atrasto failu sarakstu kļūdu ziņojumiem."""
172
+ if not discovered_files:
173
+ return "nav"
174
+ return ", ".join(sorted(discovered_files)[:5])
175
+
176
+
177
+ def _preview_data_files(data_files: list[str]) -> str:
178
+ """Atgriež īsu datu failu sarakstu kļūdu ziņojumiem."""
179
+ preview_paths: list[str] = []
180
+ for file_name in sorted(data_files)[:5]:
181
+ file_path = Path(file_name)
182
+ if "data" in file_path.parts:
183
+ data_index = file_path.parts.index("data")
184
+ preview_paths.append("/".join(file_path.parts[data_index:]))
185
+ continue
186
+ preview_paths.append(file_path.name)
187
+ return ", ".join(preview_paths) if preview_paths else "nav"
188
+
189
+
190
+ def _summarize_exception_messages(exc: Exception) -> str:
191
+ """Savāc īsu izņēmumu ķēdes kopsavilkumu lietotājam."""
192
+ details: list[str] = []
193
+ for current in _iter_exception_chain(exc):
194
+ message = str(current).strip()
195
+ if not message or message in details:
196
+ continue
197
+ details.append(message)
198
+ return " | ".join(details[:3]) if details else exc.__class__.__name__
199
+
200
+
201
+ def _build_empty_repo_message(
202
+ repo_id: str,
203
+ snapshot_dir: Path,
204
+ discovered_files: list[str],
205
+ ) -> str:
206
+ """Izveido precīzu kļūdas ziņojumu tukšam atmiņas repozitorijam."""
207
+ preview = _preview_discovered_files(discovered_files)
208
+ return (
209
+ f"Maris atmiņas repo {repo_id} pašlaik nesatur nevienu atbalstītu datu failu "
210
+ f"(.jsonl, .json, .csv, .parquet). Snapshot direktorijā {snapshot_dir} tika "
211
+ f"atrasti tikai: {preview}. Lai salabotu origin repozitorijā, augšupielādē vismaz "
212
+ "vienu datu failu zem data/<type>/, piemēram "
213
+ "data/conversation/bootstrap.jsonl, un tad palaid apmācību vēlreiz. Ja lieto "
214
+ "Git LFS, pārliecinies, ka repozitorijā ir pats datu fails, nevis tikai "
215
+ ".gitattributes ieraksts."
216
+ )
217
+
218
+
219
+ def _build_incomplete_snapshot_message(
220
+ repo_id: str,
221
+ snapshot_dir: Path,
222
+ discovered_files: list[str],
223
+ repo_data_files: list[str],
224
+ ) -> str:
225
+ """Izveido kļūdas ziņojumu, ja repo ir faili, bet snapshot tos nesatur."""
226
+ preview = _preview_discovered_files(discovered_files)
227
+ repo_preview = ", ".join(repo_data_files[:5])
228
+ return (
229
+ f"Maris atmiņas repo {repo_id} satur atbalstītus datu failus ({repo_preview}), bet "
230
+ f"snapshot direktorijā {snapshot_dir} tie netika atrasti. Snapshotā redzami: "
231
+ f"{preview}. Ja dataset repozitorijs glabā datus ar Git LFS, pārliecinieties, ka "
232
+ "tie ir pilnībā lejupielādēti un pieejami origin repozitorijā."
233
+ )
234
+
235
+
236
+ def _build_invalid_data_files_message(
237
+ repo_id: str,
238
+ data_files: list[str],
239
+ exc: Exception,
240
+ ) -> str:
241
+ """Izveido kļūdas ziņojumu bojātiem vai nederīgiem datu failiem."""
242
+ preview = _preview_data_files(data_files)
243
+ details = _summarize_exception_messages(exc)
244
+ return (
245
+ f"Maris atmiņas repo {repo_id} datu failus neizdevās nolasīt apmācībai ({preview}). "
246
+ f"Cēlonis: {details}. Pārbaudi, vai faili ir pilnībā augšupielādēti, UTF-8 kodējumā "
247
+ "un satur derīgus JSON/JSONL, CSV vai Parquet ierakstus."
248
+ )
249
+
250
+
251
+ def _find_snapshot_data_files(snapshot_dir: Path) -> tuple[str, list[str]]:
252
+ """Atrod ielādējamus datu failus dataset snapshot direktorijā."""
253
+ snapshot_dir = _validate_snapshot_dir(snapshot_dir)
254
+ search_roots = [snapshot_dir / "data", snapshot_dir]
255
+ discovered_files: list[str] = []
256
+
257
+ for root in search_roots:
258
+ if not root.exists():
259
+ continue
260
+
261
+ files_by_format: dict[str, list[str]] = {}
262
+ for file_path in _iter_snapshot_files(root):
263
+ discovered_files.append(str(file_path.relative_to(snapshot_dir)))
264
+ dataset_format = _SUPPORTED_DATASET_FORMATS.get(file_path.suffix.lower())
265
+ if dataset_format is None:
266
+ continue
267
+
268
+ files_by_format.setdefault(dataset_format, []).append(str(file_path))
269
+
270
+ if not files_by_format:
271
+ continue
272
+
273
+ if len(files_by_format) > 1:
274
+ formats = ", ".join(sorted(files_by_format))
275
+ raise ValueError(
276
+ f"Snapshot satur vairākus atbalstītus datu formātus vienlaikus: {formats}"
277
+ )
278
+
279
+ dataset_format, files = next(iter(files_by_format.items()))
280
+ return dataset_format, files
281
+
282
+ if discovered_files:
283
+ preview = _preview_discovered_files(discovered_files)
284
+ raise HFDatasetError(
285
+ "Dataset snapshot direktorijā nav atrasti atbalstīti dati: "
286
+ f"{snapshot_dir}. Atrastie faili: {preview}. "
287
+ "Ja dataset repozitorijs glabā datus ar Git LFS, pārliecinieties, ka "
288
+ "tie ir pilnībā lejupielādēti.",
289
+ discovered_files=discovered_files,
290
+ )
291
+
292
+ raise HFDatasetError(
293
+ f"Dataset snapshot direktorijā nav atrasti atbalstīti dati: {snapshot_dir}"
294
+ )
295
+
296
+
297
+ def _validate_snapshot_dir(snapshot_dir: Path) -> Path:
298
+ """Normalizē un pārbauda snapshot direktoriju pirms rekursīvas skenēšanas."""
299
+ resolved = snapshot_dir.expanduser().resolve()
300
+ if not resolved.exists() or not resolved.is_dir():
301
+ raise HFDatasetError(f"Dataset snapshot direktorija nav derīga: {resolved}")
302
+ if resolved == Path(resolved.anchor):
303
+ raise HFDatasetError(
304
+ f"Dataset snapshot direktorija nav droša rekursīvai skenēšanai: {resolved}"
305
+ )
306
+ return resolved
307
+
308
+
309
+ def _is_invalid_snapshot_dir_error(exc: HFDatasetError) -> bool:
310
+ """Nosaka, vai snapshot fallback atgrieza nederīgu vai bīstamu ceļu."""
311
+ message = str(exc)
312
+ return "nav derīga" in message or "nav droša rekursīvai skenēšanai" in message
313
+
314
+
315
+ def _iter_snapshot_files(root: Path) -> Iterator[Path]:
316
+ """Iterē snapshot failus, izlaižot nederīgus sistēmas mezglus un OSError ceļus."""
317
+
318
+ def handle_walk_error(exc: OSError) -> None:
319
+ logger.warning(
320
+ "Skipping unreadable dataset snapshot path %s: %s",
321
+ getattr(exc, "filename", root),
322
+ exc,
323
+ )
324
+
325
+ for current_root, dirnames, filenames in os.walk(
326
+ root,
327
+ topdown=True,
328
+ onerror=handle_walk_error,
329
+ followlinks=False,
330
+ ):
331
+ dirnames.sort()
332
+ for filename in sorted(filenames):
333
+ candidate = Path(current_root) / filename
334
+ try:
335
+ if candidate.is_file():
336
+ yield candidate
337
+ except OSError as exc:
338
+ logger.warning("Skipping unreadable dataset snapshot file %s: %s", candidate, exc)
339
+
340
+
341
+ def _list_repo_data_files(repo_id: str, token: str | None) -> list[str]:
342
+ """Atrod atbalstītos datu failus tieši atmiņas repozitorijā."""
343
+ from huggingface_hub import HfApi # type: ignore
344
+
345
+ api = HfApi(token=token)
346
+ repo_files = api.list_repo_files(repo_id=repo_id, repo_type="dataset")
347
+ return sorted(
348
+ path
349
+ for path in repo_files
350
+ if _SUPPORTED_DATASET_FORMATS.get(Path(path).suffix.lower()) is not None
351
+ )
352
+
353
+
354
+ def _coerce_training_record(record: Any) -> dict[str, Any]:
355
+ """Normalizē vienu ierakstu uz stabilu text-only formu apmācības datasetam."""
356
+ if isinstance(record, dict):
357
+ return {"text": record_to_training_text(record)}
358
+
359
+ try:
360
+ serialized = json.dumps(record, ensure_ascii=False, sort_keys=True)
361
+ except TypeError:
362
+ serialized = str(record)
363
+ return {"text": serialized}
364
+
365
+
366
+ def _build_train_dataset(record_factory: Callable[[], Iterator[dict[str, Any]]]) -> Any:
367
+ """Izveido train split datasetu no ierakstu ģeneratora."""
368
+ from datasets import Dataset, DatasetDict # type: ignore
369
+
370
+ return DatasetDict({"train": Dataset.from_generator(record_factory)})
371
+
372
+
373
+ def _iter_dataset_splits(dataset: Any) -> Iterator[tuple[str, Any]]:
374
+ """Atgriež pieejamos dataset splitus neatkarīgi no konkrētā tipa."""
375
+ if isinstance(dataset, dict):
376
+ yield from dataset.items()
377
+ return
378
+
379
+ items = getattr(dataset, "items", None)
380
+ if callable(items):
381
+ yield from items()
382
+ return
383
+
384
+ keys = getattr(dataset, "keys", None)
385
+ if callable(keys):
386
+ for key in keys():
387
+ yield key, dataset[key]
388
+
389
+
390
+ def _normalize_training_dataset(dataset: Any, repo_id: str) -> Any:
391
+ """Pārveido multi-split datasetu uz train split apmācībai."""
392
+ split_items = list(_iter_dataset_splits(dataset))
393
+ if not split_items or any(name == "train" for name, _ in split_items):
394
+ return dataset
395
+
396
+ logger.warning(
397
+ "Maris atmiņas repo %s atgrieza splitus bez 'train'; apvienojam tos vienā train split apmācībai.",
398
+ repo_id,
399
+ )
400
+
401
+ def iter_records() -> Iterator[dict[str, Any]]:
402
+ for _, split in split_items:
403
+ for record in split:
404
+ yield _coerce_training_record(record)
405
+
406
+ return _build_train_dataset(iter_records)
407
+
408
+
409
+ def _iter_json_payload_records(payload: Any) -> Iterator[dict[str, Any]]:
410
+ """Izvērš JSON payload uz ierakstu plūsmu."""
411
+ if isinstance(payload, list):
412
+ for item in payload:
413
+ yield _coerce_training_record(item)
414
+ return
415
+
416
+ yield _coerce_training_record(payload)
417
+
418
+
419
+ def _iter_json_file_records(data_files: list[str]) -> Iterator[dict[str, Any]]:
420
+ """Nolasa JSON/JSONL failus bez stingras nested-object shēmas."""
421
+ for file_name in data_files:
422
+ file_path = Path(file_name)
423
+ suffix = file_path.suffix.lower()
424
+ if suffix == ".jsonl":
425
+ with file_path.open(encoding="utf-8") as handle:
426
+ for line in handle:
427
+ stripped = line.strip()
428
+ if not stripped:
429
+ continue
430
+ yield _coerce_training_record(json.loads(stripped))
431
+ continue
432
+
433
+ if suffix == ".json":
434
+ payload = json.loads(file_path.read_text(encoding="utf-8"))
435
+ yield from _iter_json_payload_records(payload)
436
+ continue
437
+
438
+ raise ValueError(f"Neatbalstīts JSON datu fails: {file_path}")
439
+
440
+
441
+ def _load_data_files_as_training_dataset(
442
+ repo_id: str,
443
+ dataset_format: str,
444
+ data_files: list[str],
445
+ *,
446
+ recovery_cache_dir: str | None = None,
447
+ ) -> Any:
448
+ """Ielādē atrastos datu failus apmācībai kā train split."""
449
+ try:
450
+ if dataset_format == "json":
451
+ return _build_train_dataset(lambda: _iter_json_file_records(data_files))
452
+
453
+ from datasets import load_dataset # type: ignore
454
+
455
+ dataset, _ = _call_with_hf_cache_recovery(
456
+ f"load_dataset({dataset_format})",
457
+ load_dataset,
458
+ dataset_format,
459
+ data_files={"train": data_files},
460
+ recovery_cache_dir=recovery_cache_dir,
461
+ )
462
+ return dataset
463
+ except Exception as exc: # noqa: BLE001
464
+ raise HFDatasetError(_build_invalid_data_files_message(repo_id, data_files, exc)) from exc
465
+
466
+
467
+ def load_hf_dataset(repo_id: str | None = None) -> Any:
468
+ """Ielādē Maris atmiņas datasets."""
469
+ repo_id = repo_id or get_env_any_or_default(
470
+ "MARIS_MEMORY_REPO",
471
+ "MARIS_DATASET_REPO",
472
+ "HF_DATASET_REPO",
473
+ default="MarisUK/maris-ai-memory",
474
+ )
475
+ token = get_env_any("MARIS_REPO_TOKEN", "MARIS_TOKEN", "HF_TOKEN")
476
+ recovery_cache_dir: str | None = None
477
+
478
+ try:
479
+ from datasets import load_dataset # type: ignore
480
+
481
+ logger.info("Ielādē dataset: %s", repo_id)
482
+ dataset, recovery_cache_dir = _call_with_hf_cache_recovery(
483
+ f"load_dataset({repo_id})",
484
+ load_dataset,
485
+ repo_id,
486
+ token=token,
487
+ recovery_cache_dir=recovery_cache_dir,
488
+ )
489
+ return _normalize_training_dataset(dataset, repo_id)
490
+ except Exception as exc: # noqa: BLE001
491
+ if not _should_fallback_to_snapshot(exc):
492
+ logger.error("Dataseta ielādes kļūda: %s", exc)
493
+ raise
494
+
495
+ try:
496
+ from huggingface_hub import snapshot_download # type: ignore
497
+
498
+ logger.warning(
499
+ "Maris atmiņas repo %s nevarēja ielādēt tieši; mēģinām apmācības snapshot fallback.",
500
+ repo_id,
501
+ )
502
+ snapshot_dir_str, recovery_cache_dir = _call_with_hf_cache_recovery(
503
+ f"snapshot_download({repo_id})",
504
+ snapshot_download,
505
+ repo_id=repo_id,
506
+ repo_type="dataset",
507
+ token=token,
508
+ recovery_cache_dir=recovery_cache_dir,
509
+ )
510
+ snapshot_dir = Path(snapshot_dir_str)
511
+ try:
512
+ dataset_format, data_files = _find_snapshot_data_files(snapshot_dir)
513
+ except HFDatasetError as missing_exc:
514
+ if _is_invalid_snapshot_dir_error(missing_exc):
515
+ raise
516
+ repo_data_files = _list_repo_data_files(repo_id, token)
517
+ if not repo_data_files:
518
+ raise HFDatasetError(
519
+ _build_empty_repo_message(
520
+ repo_id,
521
+ snapshot_dir,
522
+ missing_exc.discovered_files,
523
+ ),
524
+ discovered_files=missing_exc.discovered_files,
525
+ ) from missing_exc
526
+
527
+ logger.warning(
528
+ "Snapshot cache nesatur dataset failus; lejupielādējam atrastos data failus tieši: %s",
529
+ ", ".join(repo_data_files[:5]),
530
+ )
531
+ snapshot_dir_str, recovery_cache_dir = _call_with_hf_cache_recovery(
532
+ f"snapshot_download({repo_id}, allow_patterns)",
533
+ snapshot_download,
534
+ repo_id=repo_id,
535
+ repo_type="dataset",
536
+ token=token,
537
+ allow_patterns=repo_data_files,
538
+ recovery_cache_dir=recovery_cache_dir,
539
+ )
540
+ snapshot_dir = Path(snapshot_dir_str)
541
+ try:
542
+ dataset_format, data_files = _find_snapshot_data_files(snapshot_dir)
543
+ except HFDatasetError as final_exc:
544
+ raise HFDatasetError(
545
+ _build_incomplete_snapshot_message(
546
+ repo_id,
547
+ snapshot_dir,
548
+ final_exc.discovered_files,
549
+ repo_data_files,
550
+ ),
551
+ discovered_files=final_exc.discovered_files,
552
+ ) from final_exc
553
+ return _load_data_files_as_training_dataset(
554
+ repo_id,
555
+ dataset_format,
556
+ data_files,
557
+ recovery_cache_dir=recovery_cache_dir,
558
+ )
559
+ except Exception as fallback_exc: # noqa: BLE001
560
+ logger.error("Dataseta ielādes kļūda: %s", fallback_exc)
561
+ raise
core-python/maris_core/data/preprocessing.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Datu priekšapstrāde."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ from typing import Any
8
+
9
+
10
+ def clean_text(text: str) -> str:
11
+ """Notīra tekstu no nevēlamiem simboliem."""
12
+ text = re.sub(r"\s+", " ", text)
13
+ text = text.strip()
14
+ return text
15
+
16
+
17
+ def truncate(text: str, max_chars: int = 4096) -> str:
18
+ """Apgriež tekstu līdz max_chars."""
19
+ return text[:max_chars]
20
+
21
+
22
+ def format_conversation(messages: list[dict[str, str]]) -> str:
23
+ """Formatē sarunu kā vienu tekstu apmācībai."""
24
+ parts = []
25
+ for msg in messages:
26
+ role = msg.get("role", "user")
27
+ content = msg.get("content", "")
28
+ parts.append(f"<|{role}|>\n{content}\n<|end|>")
29
+ return "\n".join(parts)
30
+
31
+
32
+ def _preserve_block_text(value: Any) -> str:
33
+ return str(value or "").strip()
34
+
35
+
36
+ def _append_section(lines: list[str], title: str, value: Any) -> None:
37
+ if value is None:
38
+ return
39
+ if isinstance(value, str):
40
+ text = _preserve_block_text(value)
41
+ if text:
42
+ lines.append(f"{title}:\n{text}")
43
+ return
44
+ if isinstance(value, list):
45
+ items = [_preserve_block_text(item) for item in value if _preserve_block_text(item)]
46
+ if items:
47
+ lines.append(f"{title}:\n" + "\n".join(f"- {item}" for item in items))
48
+ return
49
+ if isinstance(value, dict) and value:
50
+ lines.append(f"{title}:\n{json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True)}")
51
+
52
+
53
+ def _format_structured_prompt_record(record: dict[str, Any]) -> str:
54
+ prompt = clean_text(str(record.get("prompt", "")))
55
+ user_sections = [prompt]
56
+ section_fields = (
57
+ ("Repo konteksts", record.get("repo_context")),
58
+ ("Mērķa fails", record.get("target_file")),
59
+ ("Esošais vai kļūdainais kods", record.get("buggy_code")),
60
+ ("Refactor vai diff konteksts", record.get("diff")),
61
+ ("Papildu konteksts", record.get("context")),
62
+ ("Pieņemšanas kritēriji", record.get("acceptance_criteria")),
63
+ ("Testi", record.get("tests")),
64
+ ("Robežgadījumi", record.get("edge_cases")),
65
+ )
66
+ for title, value in section_fields:
67
+ _append_section(user_sections, title, value)
68
+ metadata = record.get("metadata")
69
+ if metadata:
70
+ _append_section(user_sections, "Metadata", metadata)
71
+
72
+ messages = [
73
+ {"role": "user", "content": "\n\n".join(section for section in user_sections if section)}
74
+ ]
75
+ completion = _preserve_block_text(record.get("completion"))
76
+ if completion:
77
+ messages.append({"role": "assistant", "content": completion})
78
+ elif metadata:
79
+ messages.append(
80
+ {
81
+ "role": "assistant",
82
+ "content": json.dumps(metadata, ensure_ascii=False, sort_keys=True),
83
+ }
84
+ )
85
+ return format_conversation(messages)
86
+
87
+
88
+ def record_to_training_text(record: dict[str, Any], max_chars: int = 4096) -> str:
89
+ """Pārveido vienu HF dataset ierakstu uz tekstu kauzālai apmācībai."""
90
+ if "text" in record and isinstance(record["text"], str):
91
+ return truncate(clean_text(record["text"]), max_chars=max_chars)
92
+
93
+ if "user" in record or "assistant" in record:
94
+ messages = [
95
+ {"role": "user", "content": clean_text(str(record.get("user", "")))},
96
+ {"role": "assistant", "content": clean_text(str(record.get("assistant", "")))},
97
+ ]
98
+ return truncate(format_conversation(messages), max_chars=max_chars)
99
+
100
+ if "prompt" in record:
101
+ return truncate(_format_structured_prompt_record(record), max_chars=max_chars)
102
+
103
+ serialized = json.dumps(record, ensure_ascii=False, sort_keys=True)
104
+ return truncate(clean_text(serialized), max_chars=max_chars)
core-python/maris_core/data/quality.py ADDED
@@ -0,0 +1,312 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Dataset quality gate un dedupe helperi apmācībai."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections import Counter
7
+ from dataclasses import asdict, dataclass, field
8
+ from difflib import SequenceMatcher
9
+ from typing import Any
10
+
11
+ from maris_core.data.preprocessing import clean_text, record_to_training_text
12
+
13
+ _PLACEHOLDER_TEXTS = {
14
+ "n/a",
15
+ "na",
16
+ "none",
17
+ "null",
18
+ "placeholder",
19
+ "test",
20
+ "todo",
21
+ "tbd",
22
+ }
23
+ _WORD_RE = re.compile(r"\w+", re.UNICODE)
24
+ _REPEATED_SEGMENT_SPLIT_RE = re.compile(r"(?:\n+|(?<=[.!?])\s+)")
25
+ _MIN_REPEATED_SEGMENT_LENGTH = 8
26
+
27
+
28
+ @dataclass(slots=True, frozen=True)
29
+ class DatasetQualityGateConfig:
30
+ """Konfigurācija training kvalitātes vārtiem."""
31
+
32
+ enabled: bool = True
33
+ dedupe_enabled: bool = True
34
+ min_text_chars: int = 4
35
+ max_text_chars: int = 8192
36
+ min_unique_char_ratio: float = 0.2
37
+ min_completion_chars: int = 12
38
+ max_prompt_echo_similarity: float = 0.92
39
+ max_repeated_line_fraction: float = 0.34
40
+
41
+
42
+ @dataclass(slots=True)
43
+ class DatasetQualitySplitReport:
44
+ """Viena split kvalitātes kopsavilkums."""
45
+
46
+ split_name: str
47
+ input_records: int = 0
48
+ kept_records: int = 0
49
+ dropped_records: int = 0
50
+ duplicates_removed: int = 0
51
+ reasons: dict[str, int] = field(default_factory=dict)
52
+ sample_rejections: list[dict[str, str]] = field(default_factory=list)
53
+
54
+ def to_dict(self) -> dict[str, Any]:
55
+ return asdict(self)
56
+
57
+
58
+ @dataclass(slots=True)
59
+ class DatasetQualityReport:
60
+ """Pilns dataset quality gate artefakts."""
61
+
62
+ artifact_type: str
63
+ config: dict[str, Any]
64
+ splits: dict[str, DatasetQualitySplitReport]
65
+
66
+ def to_dict(self) -> dict[str, Any]:
67
+ return {
68
+ "artifact_type": self.artifact_type,
69
+ "config": self.config,
70
+ "splits": {name: report.to_dict() for name, report in self.splits.items()},
71
+ "input_records": sum(report.input_records for report in self.splits.values()),
72
+ "kept_records": sum(report.kept_records for report in self.splits.values()),
73
+ "dropped_records": sum(report.dropped_records for report in self.splits.values()),
74
+ "duplicates_removed": sum(report.duplicates_removed for report in self.splits.values()),
75
+ }
76
+
77
+
78
+ def apply_quality_gate_to_records(
79
+ records: list[dict[str, Any]],
80
+ *,
81
+ split_name: str,
82
+ config: DatasetQualityGateConfig,
83
+ ) -> tuple[list[dict[str, Any]], DatasetQualitySplitReport]:
84
+ """Filtrē ierakstus un noņem dublikātus pirms treniņa."""
85
+
86
+ if not config.enabled:
87
+ report = DatasetQualitySplitReport(
88
+ split_name=split_name,
89
+ input_records=len(records),
90
+ kept_records=len(records),
91
+ )
92
+ return [_normalize_record(record) for record in records], report
93
+
94
+ report = DatasetQualitySplitReport(split_name=split_name)
95
+ kept_records: list[dict[str, Any]] = []
96
+ seen_signatures: set[str] = set()
97
+
98
+ for raw_record in records:
99
+ report.input_records += 1
100
+ record = _normalize_record(raw_record)
101
+ training_text = record_to_training_text(record, max_chars=config.max_text_chars)
102
+ rejection_reason = _get_rejection_reason(training_text, record, config)
103
+ if rejection_reason is not None:
104
+ _record_rejection(report, rejection_reason, training_text)
105
+ continue
106
+
107
+ signature = _training_text_signature(training_text)
108
+ if config.dedupe_enabled and signature in seen_signatures:
109
+ report.duplicates_removed += 1
110
+ _record_rejection(report, "duplicate_training_text", training_text)
111
+ continue
112
+
113
+ seen_signatures.add(signature)
114
+ kept_records.append(record)
115
+ report.kept_records += 1
116
+
117
+ report.dropped_records = report.input_records - report.kept_records
118
+ return kept_records, report
119
+
120
+
121
+ def build_dataset_quality_report(
122
+ *,
123
+ config: DatasetQualityGateConfig,
124
+ train_report: DatasetQualitySplitReport,
125
+ eval_report: DatasetQualitySplitReport | None = None,
126
+ ) -> DatasetQualityReport:
127
+ """Izveido serializējamu kvalitātes artefaktu."""
128
+
129
+ splits = {train_report.split_name: train_report}
130
+ if eval_report is not None:
131
+ splits[eval_report.split_name] = eval_report
132
+ return DatasetQualityReport(
133
+ artifact_type="dataset-quality-report",
134
+ config=asdict(config),
135
+ splits=splits,
136
+ )
137
+
138
+
139
+ def _normalize_record(record: dict[str, Any]) -> dict[str, Any]:
140
+ normalized: dict[str, Any] = {}
141
+ for key, value in record.items():
142
+ normalized[key] = _normalize_value(value)
143
+ return normalized
144
+
145
+
146
+ def _normalize_value(value: Any) -> Any:
147
+ if isinstance(value, str):
148
+ return clean_text(value)
149
+ if isinstance(value, dict):
150
+ return {str(key): _normalize_value(item) for key, item in value.items()}
151
+ if isinstance(value, list):
152
+ return [_normalize_value(item) for item in value]
153
+ return value
154
+
155
+
156
+ def _get_rejection_reason(
157
+ training_text: str,
158
+ record: dict[str, Any],
159
+ config: DatasetQualityGateConfig,
160
+ ) -> str | None:
161
+ normalized_text = clean_text(training_text)
162
+ if not normalized_text:
163
+ return "empty_training_text"
164
+ if len(normalized_text) < config.min_text_chars:
165
+ return "too_short"
166
+ if len(normalized_text) > config.max_text_chars:
167
+ return "too_long"
168
+
169
+ if normalized_text.casefold() in _PLACEHOLDER_TEXTS:
170
+ return "placeholder_text"
171
+
172
+ if _has_repeated_line_noise(normalized_text, config.max_repeated_line_fraction):
173
+ return "repeated_line_noise"
174
+
175
+ if _looks_repetitive(normalized_text, config.min_unique_char_ratio):
176
+ return "low_information_density"
177
+
178
+ conversation_rejection = _conversation_rejection_reason(record, config)
179
+ if conversation_rejection is not None:
180
+ return conversation_rejection
181
+
182
+ return None
183
+
184
+
185
+ def _looks_repetitive(value: str, min_unique_char_ratio: float) -> bool:
186
+ compact = "".join(character for character in value.casefold() if not character.isspace())
187
+ if not compact:
188
+ return True
189
+ unique_ratio = len(set(compact)) / len(compact)
190
+ if unique_ratio >= min_unique_char_ratio:
191
+ return False
192
+
193
+ words = _WORD_RE.findall(value.casefold())
194
+ if len(words) >= 6:
195
+ # Garākiem strukturētiem promptiem simbolu dažādība var būt zema marķējuma/JSON dēļ,
196
+ # tāpēc izmantojam arī vārdu dažādību, lai neatmestu jēgpilnus ierakstus.
197
+ unique_word_ratio = len(set(words)) / len(words)
198
+ if unique_word_ratio >= 0.45:
199
+ return False
200
+
201
+ return True
202
+
203
+
204
+ def _conversation_rejection_reason(
205
+ record: dict[str, Any],
206
+ config: DatasetQualityGateConfig,
207
+ ) -> str | None:
208
+ prompt, completion = _conversation_pair(record)
209
+ if prompt is None or completion is None:
210
+ return None
211
+ if not prompt or not completion:
212
+ return "invalid_conversation_pair"
213
+ if prompt.casefold() == completion.casefold():
214
+ return "invalid_conversation_pair"
215
+ if completion.casefold() in _PLACEHOLDER_TEXTS:
216
+ return "placeholder_response"
217
+ if len(completion) < config.min_completion_chars and len(prompt) >= config.min_completion_chars:
218
+ return "response_too_short"
219
+ if _looks_like_prompt_echo(
220
+ prompt,
221
+ completion,
222
+ max_prompt_echo_similarity=config.max_prompt_echo_similarity,
223
+ ):
224
+ return "prompt_echo_response"
225
+ return None
226
+
227
+
228
+ def _conversation_pair(record: dict[str, Any]) -> tuple[str | None, str | None]:
229
+ prompt = _coerce_quality_text(
230
+ record.get("user"),
231
+ record.get("prompt"),
232
+ record.get("instruction"),
233
+ record.get("input"),
234
+ )
235
+ completion = _coerce_quality_text(
236
+ record.get("assistant"),
237
+ record.get("completion"),
238
+ record.get("response"),
239
+ record.get("output"),
240
+ )
241
+ return prompt, completion
242
+
243
+
244
+ def _coerce_quality_text(*values: Any) -> str | None:
245
+ for value in values:
246
+ if isinstance(value, str):
247
+ normalized = clean_text(value)
248
+ if normalized:
249
+ return normalized
250
+ return None
251
+
252
+
253
+ def _looks_like_prompt_echo(
254
+ prompt: str,
255
+ completion: str,
256
+ *,
257
+ max_prompt_echo_similarity: float,
258
+ ) -> bool:
259
+ prompt_normalized = prompt.casefold()
260
+ completion_normalized = completion.casefold()
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()
273
+ if not prompt_tokens or not completion_tokens:
274
+ return False
275
+ shared_tokens = len(set(prompt_tokens) & set(completion_tokens))
276
+ completion_overlap = shared_tokens / max(len(set(completion_tokens)), 1)
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)
291
+ if not repeated_counts:
292
+ return False
293
+ return max(repeated_counts.values()) / len(lines) > max_repeated_line_fraction
294
+
295
+
296
+ def _training_text_signature(value: str) -> str:
297
+ return " ".join(value.casefold().split())
298
+
299
+
300
+ def _record_rejection(
301
+ report: DatasetQualitySplitReport,
302
+ reason: str,
303
+ training_text: str,
304
+ ) -> None:
305
+ report.reasons[reason] = report.reasons.get(reason, 0) + 1
306
+ if len(report.sample_rejections) < 5:
307
+ report.sample_rejections.append(
308
+ {
309
+ "reason": reason,
310
+ "preview": training_text[:160],
311
+ }
312
+ )
core-python/maris_core/data/scoring.py ADDED
@@ -0,0 +1,597 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Dataseta scoring, source-aware weighting un benchmark feedback palīgi."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import asdict, dataclass, field
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from maris_core.data.preprocessing import clean_text, record_to_training_text
11
+
12
+ DEFAULT_SOURCE_WEIGHT_MAP = {
13
+ "production": 1.3,
14
+ "synthetic": 1.0,
15
+ "noisy": 0.65,
16
+ "unknown": 1.0,
17
+ }
18
+ SOURCE_TIER_TOKEN_MAP = {
19
+ "production": ("production", "prod", "live", "human", "curated", "real", "customer"),
20
+ "synthetic": ("synthetic", "generated", "augmented", "distilled", "bootstrap", "seeded"),
21
+ "noisy": ("noisy", "weak", "scraped", "raw", "unfiltered", "test"),
22
+ }
23
+ BENCHMARK_METRIC_ALIASES = {
24
+ "reasoning": {"reasoning", "analysis", "logic", "planner"},
25
+ "coding": {"coding", "code", "programming", "developer"},
26
+ "long_context": {"long_context", "context", "memory", "retrieval"},
27
+ "helpfulness": {"helpfulness", "helpful", "assistant", "support"},
28
+ "factuality": {"factuality", "facts", "grounding", "grounded"},
29
+ "latvian_quality": {"latvian_quality", "latvian", "language_lv", "lv"},
30
+ "safety": {"safety", "safe", "guardrails", "policy"},
31
+ }
32
+
33
+
34
+ @dataclass(slots=True, frozen=True)
35
+ class DatasetScoringConfig:
36
+ """Konfigurācija dataset scoring/weighting solim."""
37
+
38
+ enabled: bool = True
39
+ weighted_repetition_enabled: bool = True
40
+ max_text_chars: int = 8192
41
+ low_score_repeat_count: int = 1
42
+ medium_score_repeat_count: int = 2
43
+ high_score_repeat_count: int = 3
44
+ medium_score_threshold: float = 0.55
45
+ high_score_threshold: float = 0.8
46
+ source_weighting_enabled: bool = True
47
+ source_weight_map: dict[str, float] = field(
48
+ default_factory=lambda: DEFAULT_SOURCE_WEIGHT_MAP.copy()
49
+ )
50
+ category_weight_map: dict[str, float] = field(default_factory=dict)
51
+ max_effective_repeat_count: int = 6
52
+ benchmark_feedback_enabled: bool = True
53
+ benchmark_feedback_path: str = ""
54
+ benchmark_feedback_boost_scale: float = 2.0
55
+ benchmark_feedback_max_multiplier: float = 1.75
56
+
57
+
58
+ @dataclass(slots=True, frozen=True)
59
+ class DatasetBenchmarkFeedback:
60
+ """Iepriekšējā benchmark artefakta kopsavilkums reweighting vajadzībām."""
61
+
62
+ artifact_path: str
63
+ deficient_metrics: dict[str, dict[str, float]]
64
+ overall_multiplier: float = 1.0
65
+ discovery_mode: str = "explicit"
66
+
67
+
68
+ @dataclass(slots=True)
69
+ class DatasetScoringSplitReport:
70
+ """Viena split scoring rezultātu kopsavilkums."""
71
+
72
+ split_name: str
73
+ input_records: int = 0
74
+ expanded_records: int = 0
75
+ average_score: float = 0.0
76
+ max_score: float = 0.0
77
+ min_score: float = 0.0
78
+ repeated_records: int = 0
79
+ score_buckets: dict[str, int] = field(default_factory=dict)
80
+ repeat_buckets: dict[str, int] = field(default_factory=dict)
81
+ source_tiers: dict[str, int] = field(default_factory=dict)
82
+ category_buckets: dict[str, int] = field(default_factory=dict)
83
+ feedback_metric_hits: dict[str, int] = field(default_factory=dict)
84
+ feedback_boosted_records: int = 0
85
+ average_repeat_multiplier: float = 1.0
86
+ source_dashboard: dict[str, dict[str, float]] = field(default_factory=dict)
87
+ category_dashboard: dict[str, dict[str, float]] = field(default_factory=dict)
88
+ sample_scores: list[dict[str, Any]] = field(default_factory=list)
89
+
90
+ def to_dict(self) -> dict[str, Any]:
91
+ return asdict(self)
92
+
93
+
94
+ @dataclass(slots=True)
95
+ class DatasetScoringReport:
96
+ """Pilns dataset scoring artefakts."""
97
+
98
+ artifact_type: str
99
+ config: dict[str, Any]
100
+ splits: dict[str, DatasetScoringSplitReport]
101
+
102
+ def to_dict(self) -> dict[str, Any]:
103
+ return {
104
+ "artifact_type": self.artifact_type,
105
+ "config": self.config,
106
+ "splits": {name: report.to_dict() for name, report in self.splits.items()},
107
+ "input_records": sum(report.input_records for report in self.splits.values()),
108
+ "expanded_records": sum(report.expanded_records for report in self.splits.values()),
109
+ "repeated_records": sum(report.repeated_records for report in self.splits.values()),
110
+ }
111
+
112
+
113
+ def apply_scoring_to_records(
114
+ records: list[dict[str, Any]],
115
+ *,
116
+ split_name: str,
117
+ config: DatasetScoringConfig,
118
+ expand_weights: bool,
119
+ benchmark_feedback: DatasetBenchmarkFeedback | None = None,
120
+ ) -> tuple[list[dict[str, Any]], DatasetScoringSplitReport]:
121
+ """Aprēķina score un materializē weighting kā atkārtojumus."""
122
+
123
+ report = DatasetScoringSplitReport(split_name=split_name, input_records=len(records))
124
+ if not records:
125
+ return [], report
126
+
127
+ scored_records: list[tuple[dict[str, Any], float, int]] = []
128
+ total_score = 0.0
129
+ total_repeat_multiplier = 0.0
130
+ source_dashboard: dict[str, dict[str, float]] = {}
131
+ category_dashboard: dict[str, dict[str, float]] = {}
132
+
133
+ for record in records:
134
+ score = score_record(record, max_text_chars=config.max_text_chars)
135
+ source_tier = detect_source_tier(record)
136
+ category_label = detect_record_category(record)
137
+ source_multiplier = _source_multiplier_for_record(record, config)
138
+ category_multiplier = _category_multiplier_for_record(record, config)
139
+ feedback_multiplier, matched_metrics = _feedback_multiplier_for_record(
140
+ record,
141
+ benchmark_feedback,
142
+ )
143
+ repeat_multiplier = source_multiplier * category_multiplier * feedback_multiplier
144
+ repeat_count = _effective_repeat_count(
145
+ score,
146
+ repeat_multiplier=repeat_multiplier,
147
+ config=config,
148
+ expand_weights=expand_weights,
149
+ )
150
+ total_score += score
151
+ total_repeat_multiplier += repeat_multiplier
152
+ report.score_buckets[_score_bucket(score)] = (
153
+ report.score_buckets.get(_score_bucket(score), 0) + 1
154
+ )
155
+ report.repeat_buckets[str(repeat_count)] = (
156
+ report.repeat_buckets.get(str(repeat_count), 0) + 1
157
+ )
158
+ report.source_tiers[source_tier] = report.source_tiers.get(source_tier, 0) + 1
159
+ report.category_buckets[category_label] = report.category_buckets.get(category_label, 0) + 1
160
+ if repeat_count > 1:
161
+ report.repeated_records += 1
162
+ if matched_metrics:
163
+ report.feedback_boosted_records += 1
164
+ for metric in matched_metrics:
165
+ report.feedback_metric_hits[metric] = report.feedback_metric_hits.get(metric, 0) + 1
166
+ _update_dashboard_bucket(
167
+ source_dashboard,
168
+ source_tier,
169
+ score=score,
170
+ repeat_count=repeat_count,
171
+ repeat_multiplier=repeat_multiplier,
172
+ boosted=bool(matched_metrics),
173
+ )
174
+ _update_dashboard_bucket(
175
+ category_dashboard,
176
+ category_label,
177
+ score=score,
178
+ repeat_count=repeat_count,
179
+ repeat_multiplier=repeat_multiplier,
180
+ boosted=bool(matched_metrics),
181
+ )
182
+ if len(report.sample_scores) < 5:
183
+ report.sample_scores.append(
184
+ {
185
+ "score": round(score, 4),
186
+ "source_tier": source_tier,
187
+ "category": category_label,
188
+ "source_multiplier": round(source_multiplier, 4),
189
+ "category_multiplier": round(category_multiplier, 4),
190
+ "feedback_multiplier": round(feedback_multiplier, 4),
191
+ "matched_metrics": matched_metrics,
192
+ "repeat_count": repeat_count,
193
+ "preview": record_to_training_text(record, max_chars=config.max_text_chars)[
194
+ :160
195
+ ],
196
+ }
197
+ )
198
+ scored_records.append((record, score, repeat_count))
199
+
200
+ report.average_score = round(total_score / len(scored_records), 4)
201
+ report.average_repeat_multiplier = round(total_repeat_multiplier / len(scored_records), 4)
202
+ report.source_dashboard = _finalize_dashboard(source_dashboard)
203
+ report.category_dashboard = _finalize_dashboard(category_dashboard)
204
+ score_values = [score for _, score, _ in scored_records]
205
+ report.min_score = round(min(score_values), 4)
206
+ report.max_score = round(max(score_values), 4)
207
+
208
+ if not config.enabled:
209
+ expanded = list(records)
210
+ else:
211
+ expanded = []
212
+ for record, score, repeat_count in scored_records:
213
+ repeat_total = repeat_count if expand_weights else 1
214
+ for copy_index in range(repeat_total):
215
+ enriched = dict(record)
216
+ enriched["maris_dataset_score"] = round(score, 4)
217
+ enriched["maris_dataset_repeat_count"] = repeat_count
218
+ enriched["maris_dataset_repeat_index"] = copy_index
219
+ enriched["maris_dataset_source_tier"] = detect_source_tier(record)
220
+ expanded.append(enriched)
221
+
222
+ report.expanded_records = len(expanded)
223
+ return expanded, report
224
+
225
+
226
+ def build_dataset_scoring_report(
227
+ *,
228
+ config: DatasetScoringConfig,
229
+ train_report: DatasetScoringSplitReport,
230
+ eval_report: DatasetScoringSplitReport | None = None,
231
+ ) -> DatasetScoringReport:
232
+ """Izveido serializējamu scoring artefaktu."""
233
+
234
+ splits = {train_report.split_name: train_report}
235
+ if eval_report is not None:
236
+ splits[eval_report.split_name] = eval_report
237
+ return DatasetScoringReport(
238
+ artifact_type="dataset-scoring-report",
239
+ config=asdict(config),
240
+ splits=splits,
241
+ )
242
+
243
+
244
+ def load_benchmark_feedback(
245
+ path: str | Path,
246
+ *,
247
+ targets: dict[str, float],
248
+ boost_scale: float,
249
+ max_multiplier: float,
250
+ ) -> DatasetBenchmarkFeedback:
251
+ """Ielādē benchmark manifestu/feedback artefaktu un pārvērš reweighting noteikumos."""
252
+
253
+ payload = json.loads(Path(path).read_text(encoding="utf-8"))
254
+ if "deficient_metrics" in payload:
255
+ deficient_metrics = {
256
+ str(metric): {
257
+ "target": float(details.get("target", 0.0)),
258
+ "actual": float(details.get("actual", 0.0)),
259
+ "deficit": float(details.get("deficit", 0.0)),
260
+ "multiplier": float(details.get("multiplier", 1.0)),
261
+ }
262
+ for metric, details in payload.get("deficient_metrics", {}).items()
263
+ if isinstance(details, dict)
264
+ }
265
+ overall_multiplier = float(payload.get("overall_multiplier", 1.0) or 1.0)
266
+ return DatasetBenchmarkFeedback(
267
+ artifact_path=str(path),
268
+ deficient_metrics=deficient_metrics,
269
+ overall_multiplier=overall_multiplier,
270
+ discovery_mode=str(payload.get("discovery_mode", "explicit") or "explicit"),
271
+ )
272
+
273
+ score_manifest = payload.get("score_manifest")
274
+ if not isinstance(score_manifest, dict):
275
+ raise ValueError("Benchmark feedback failā jābūt `score_manifest` vai `deficient_metrics`.")
276
+
277
+ deficient_metrics: dict[str, dict[str, float]] = {}
278
+ overall_multiplier = 1.0
279
+ for metric, target in targets.items():
280
+ actual = float(score_manifest.get(metric, score_manifest.get("overall", 0.0)) or 0.0)
281
+ deficit = max(float(target) - actual, 0.0)
282
+ if deficit <= 0:
283
+ continue
284
+ multiplier = min(1.0 + deficit * boost_scale, max_multiplier)
285
+ deficient_metrics[str(metric)] = {
286
+ "target": float(target),
287
+ "actual": actual,
288
+ "deficit": round(deficit, 4),
289
+ "multiplier": round(multiplier, 4),
290
+ }
291
+ if metric == "overall":
292
+ overall_multiplier = round(multiplier, 4)
293
+
294
+ return DatasetBenchmarkFeedback(
295
+ artifact_path=str(path),
296
+ deficient_metrics=deficient_metrics,
297
+ overall_multiplier=overall_multiplier,
298
+ )
299
+
300
+
301
+ def build_benchmark_feedback_artifact(
302
+ feedback: DatasetBenchmarkFeedback,
303
+ ) -> dict[str, Any]:
304
+ """Izveido serializējamu benchmark-feedback artefaktu."""
305
+
306
+ return {
307
+ "artifact_type": "benchmark-feedback-reweighting",
308
+ "artifact_path": feedback.artifact_path,
309
+ "overall_multiplier": feedback.overall_multiplier,
310
+ "discovery_mode": feedback.discovery_mode,
311
+ "deficient_metrics": feedback.deficient_metrics,
312
+ }
313
+
314
+
315
+ def score_record(record: dict[str, Any], *, max_text_chars: int) -> float:
316
+ """Aprēķina heuristisku datu kvalitātes score [0,1] intervālā."""
317
+
318
+ text = clean_text(record_to_training_text(record, max_chars=max_text_chars))
319
+ if not text:
320
+ return 0.0
321
+
322
+ text_length = len(text)
323
+ tokens = [token for token in text.casefold().split() if token]
324
+ unique_tokens = len(set(tokens))
325
+
326
+ length_score = min(text_length / 240.0, 1.0)
327
+ diversity_score = min(unique_tokens / max(len(tokens), 1), 1.0)
328
+ structure_score = _structure_score(record)
329
+ metadata_score = _metadata_score(record)
330
+
331
+ score = (
332
+ 0.35 * length_score
333
+ + 0.30 * diversity_score
334
+ + 0.20 * structure_score
335
+ + 0.15 * metadata_score
336
+ )
337
+ return max(0.0, min(round(score, 4), 1.0))
338
+
339
+
340
+ def detect_source_tier(record: dict[str, Any]) -> str:
341
+ """Atrod source tier svarošnai."""
342
+
343
+ candidates = _record_terms(record)
344
+ explicit = record.get("source_tier") or record.get("source_quality")
345
+ if isinstance(explicit, str):
346
+ normalized = clean_text(explicit).casefold().replace(" ", "_")
347
+ if normalized in DEFAULT_SOURCE_WEIGHT_MAP:
348
+ return normalized
349
+
350
+ for tier, tokens in SOURCE_TIER_TOKEN_MAP.items():
351
+ if any(token in candidates for token in tokens):
352
+ return tier
353
+ return "unknown"
354
+
355
+
356
+ def detect_record_category(record: dict[str, Any]) -> str:
357
+ """Atrod stabilu kategorijas label dashboard grupēšanai."""
358
+
359
+ for candidate in (
360
+ record.get("category"),
361
+ record.get("task_category"),
362
+ record.get("branch_focus"),
363
+ ):
364
+ if isinstance(candidate, str) and clean_text(candidate):
365
+ return _normalize_label(candidate)
366
+
367
+ metadata = record.get("metadata")
368
+ if isinstance(metadata, dict):
369
+ for key in ("category", "focus", "type"):
370
+ candidate = metadata.get(key)
371
+ if isinstance(candidate, str) and clean_text(candidate):
372
+ return _normalize_label(candidate)
373
+ return "general"
374
+
375
+
376
+ def _structure_score(record: dict[str, Any]) -> float:
377
+ if isinstance(record.get("user"), str) and isinstance(record.get("assistant"), str):
378
+ user = clean_text(str(record.get("user", "")))
379
+ assistant = clean_text(str(record.get("assistant", "")))
380
+ if user and assistant and user.casefold() != assistant.casefold():
381
+ return 1.0
382
+ return 0.3
383
+ if isinstance(record.get("prompt"), str):
384
+ return 0.8 if clean_text(str(record.get("prompt", ""))) else 0.2
385
+ if isinstance(record.get("text"), str):
386
+ return 0.6 if clean_text(str(record.get("text", ""))) else 0.2
387
+ return 0.4
388
+
389
+
390
+ def _metadata_score(record: dict[str, Any]) -> float:
391
+ metadata = record.get("metadata")
392
+ score = 0.0
393
+ if isinstance(metadata, dict):
394
+ score += min(len(metadata) / 4.0, 1.0) * 0.7
395
+ if isinstance(record.get("language"), str) and clean_text(str(record["language"])):
396
+ score += 0.15
397
+ if isinstance(record.get("source"), str) and clean_text(str(record["source"])):
398
+ score += 0.15
399
+ return min(score, 1.0)
400
+
401
+
402
+ def _source_multiplier_for_record(record: dict[str, Any], config: DatasetScoringConfig) -> float:
403
+ if not config.source_weighting_enabled:
404
+ return 1.0
405
+ source_tier = detect_source_tier(record)
406
+ return max(0.1, float(config.source_weight_map.get(source_tier, 1.0)))
407
+
408
+
409
+ def _category_multiplier_for_record(record: dict[str, Any], config: DatasetScoringConfig) -> float:
410
+ if not config.category_weight_map:
411
+ return 1.0
412
+ labels = _record_labels(record)
413
+ matches = [
414
+ float(weight)
415
+ for label, weight in config.category_weight_map.items()
416
+ if clean_text(str(label)).casefold().replace("-", "_").replace(" ", "_") in labels
417
+ ]
418
+ if not matches:
419
+ return 1.0
420
+ return max(0.1, max(matches))
421
+
422
+
423
+ def _feedback_multiplier_for_record(
424
+ record: dict[str, Any],
425
+ feedback: DatasetBenchmarkFeedback | None,
426
+ ) -> tuple[float, list[str]]:
427
+ if feedback is None or not feedback.deficient_metrics:
428
+ return 1.0, []
429
+
430
+ labels = _record_labels(record)
431
+ matched_metrics = sorted(
432
+ metric
433
+ for metric in feedback.deficient_metrics
434
+ if metric == "overall"
435
+ or labels.intersection(BENCHMARK_METRIC_ALIASES.get(metric, {metric}))
436
+ )
437
+ if not matched_metrics:
438
+ return feedback.overall_multiplier, ["overall"] if feedback.overall_multiplier > 1.0 else []
439
+
440
+ specific_multiplier = (
441
+ max(
442
+ float(feedback.deficient_metrics[metric].get("multiplier", 1.0) or 1.0)
443
+ for metric in matched_metrics
444
+ if metric != "overall"
445
+ )
446
+ if any(metric != "overall" for metric in matched_metrics)
447
+ else 1.0
448
+ )
449
+ combined = min(
450
+ max(1.0, specific_multiplier) * max(1.0, feedback.overall_multiplier),
451
+ max(
452
+ [feedback.overall_multiplier]
453
+ + [
454
+ float(details.get("multiplier", 1.0) or 1.0)
455
+ for details in feedback.deficient_metrics.values()
456
+ ]
457
+ ),
458
+ )
459
+ return round(max(1.0, combined), 4), matched_metrics
460
+
461
+
462
+ def _effective_repeat_count(
463
+ score: float,
464
+ *,
465
+ repeat_multiplier: float,
466
+ config: DatasetScoringConfig,
467
+ expand_weights: bool,
468
+ ) -> int:
469
+ if not config.enabled or not expand_weights or not config.weighted_repetition_enabled:
470
+ return 1
471
+ base_repeat_count = _repeat_count_for_score(score, config)
472
+ weighted = int(round(base_repeat_count * max(repeat_multiplier, 0.1)))
473
+ return max(1, min(weighted, config.max_effective_repeat_count))
474
+
475
+
476
+ def _repeat_count_for_score(score: float, config: DatasetScoringConfig) -> int:
477
+ if score >= config.high_score_threshold:
478
+ return max(1, config.high_score_repeat_count)
479
+ if score >= config.medium_score_threshold:
480
+ return max(1, config.medium_score_repeat_count)
481
+ return max(1, config.low_score_repeat_count)
482
+
483
+
484
+ def _score_bucket(score: float) -> str:
485
+ if score >= 0.8:
486
+ return "high"
487
+ if score >= 0.55:
488
+ return "medium"
489
+ return "low"
490
+
491
+
492
+ def _record_terms(record: dict[str, Any]) -> set[str]:
493
+ values: list[str] = []
494
+ for key in ("source", "source_type", "source_quality", "source_tier", "category", "language"):
495
+ value = record.get(key)
496
+ if isinstance(value, str):
497
+ values.extend(value.casefold().replace("-", " ").replace("_", " ").split())
498
+ metadata = record.get("metadata")
499
+ if isinstance(metadata, dict):
500
+ for key in ("source", "source_type", "source_quality", "source_tier", "origin", "category"):
501
+ value = metadata.get(key)
502
+ if isinstance(value, str):
503
+ values.extend(value.casefold().replace("-", " ").replace("_", " ").split())
504
+ tags = metadata.get("tags")
505
+ if isinstance(tags, list):
506
+ for item in tags:
507
+ if isinstance(item, str):
508
+ values.extend(item.casefold().replace("-", " ").replace("_", " ").split())
509
+ tags = record.get("tags")
510
+ if isinstance(tags, list):
511
+ for item in tags:
512
+ if isinstance(item, str):
513
+ values.extend(item.casefold().replace("-", " ").replace("_", " ").split())
514
+ return set(values)
515
+
516
+
517
+ def _record_labels(record: dict[str, Any]) -> set[str]:
518
+ labels: set[str] = set()
519
+ for key in ("category", "branch_focus", "source", "language"):
520
+ value = record.get(key)
521
+ if isinstance(value, str) and clean_text(value):
522
+ normalized = clean_text(value).casefold().replace("-", "_").replace(" ", "_")
523
+ labels.add(normalized)
524
+ labels.update(normalized.split("_"))
525
+ metadata = record.get("metadata")
526
+ if isinstance(metadata, dict):
527
+ for key in ("category", "focus", "type", "language"):
528
+ value = metadata.get(key)
529
+ if isinstance(value, str) and clean_text(value):
530
+ normalized = clean_text(value).casefold().replace("-", "_").replace(" ", "_")
531
+ labels.add(normalized)
532
+ labels.update(normalized.split("_"))
533
+ tags = metadata.get("tags")
534
+ if isinstance(tags, list):
535
+ for item in tags:
536
+ if isinstance(item, str) and clean_text(item):
537
+ normalized = clean_text(item).casefold().replace("-", "_").replace(" ", "_")
538
+ labels.add(normalized)
539
+ labels.update(normalized.split("_"))
540
+ tags = record.get("tags")
541
+ if isinstance(tags, list):
542
+ for item in tags:
543
+ if isinstance(item, str) and clean_text(item):
544
+ normalized = clean_text(item).casefold().replace("-", "_").replace(" ", "_")
545
+ labels.add(normalized)
546
+ labels.update(normalized.split("_"))
547
+ return labels
548
+
549
+
550
+ def _update_dashboard_bucket(
551
+ dashboard: dict[str, dict[str, float]],
552
+ label: str,
553
+ *,
554
+ score: float,
555
+ repeat_count: int,
556
+ repeat_multiplier: float,
557
+ boosted: bool,
558
+ ) -> None:
559
+ """Uzkrāj dashboard metriku bucketam; `boosted` nozīmē benchmark feedback match."""
560
+
561
+ bucket = dashboard.setdefault(
562
+ label,
563
+ {
564
+ "records": 0.0,
565
+ "score_total": 0.0,
566
+ "repeat_total": 0.0,
567
+ "repeat_multiplier_total": 0.0,
568
+ "boosted_records": 0.0,
569
+ },
570
+ )
571
+ bucket["records"] += 1.0
572
+ bucket["score_total"] += score
573
+ bucket["repeat_total"] += float(repeat_count)
574
+ bucket["repeat_multiplier_total"] += repeat_multiplier
575
+ if boosted:
576
+ bucket["boosted_records"] += 1.0
577
+
578
+
579
+ def _finalize_dashboard(dashboard: dict[str, dict[str, float]]) -> dict[str, dict[str, float]]:
580
+ finalized: dict[str, dict[str, float]] = {}
581
+ for label, bucket in sorted(dashboard.items()):
582
+ records = max(bucket.get("records", 0.0), 1.0)
583
+ finalized[label] = {
584
+ "records": int(bucket.get("records", 0.0)),
585
+ "average_score": round(bucket.get("score_total", 0.0) / records, 4),
586
+ "average_repeat_count": round(bucket.get("repeat_total", 0.0) / records, 4),
587
+ "average_repeat_multiplier": round(
588
+ bucket.get("repeat_multiplier_total", 0.0) / records,
589
+ 4,
590
+ ),
591
+ "boosted_records": int(bucket.get("boosted_records", 0.0)),
592
+ }
593
+ return finalized
594
+
595
+
596
+ def _normalize_label(value: str) -> str:
597
+ return clean_text(value).casefold().replace("-", "_").replace(" ", "_")
core-python/maris_core/data/validator.py ADDED
@@ -0,0 +1,268 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Bootstrap dataset validācija."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import dataclass
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ DATASET_TYPES = ("conversation", "code", "image", "music", "video", "autonomous")
12
+ _PROMPT_TYPES = {"code", "image", "music", "video", "autonomous"}
13
+ _COMMON_REQUIRED_STRING_FIELDS = ("timestamp", "type", "source")
14
+ _CONVERSATION_REQUIRED_STRING_FIELDS = ("session_id", "user", "assistant", "language")
15
+ _VALIDATION_PROFILES = {"auto", "bootstrap", "eval"}
16
+ _EVAL_REQUIRED_STRING_FIELDS = (
17
+ "task_id",
18
+ "benchmark_version",
19
+ "suite",
20
+ "difficulty",
21
+ "evaluation_mode",
22
+ "risk_level",
23
+ )
24
+ _EVAL_REQUIRED_STRING_LIST_FIELDS = ("expected_behavior", "scoring_hints")
25
+ _EVAL_REFERENCE_REQUIRED_CATEGORIES = {"conversation", "code"}
26
+
27
+
28
+ class DatasetValidationError(ValueError):
29
+ """Bootstrap dataset satura validācijas kļūda."""
30
+
31
+ def __init__(self, issues: list[str]) -> None:
32
+ self.issues = issues
33
+ preview = "\n".join(f"- {issue}" for issue in issues[:20])
34
+ remaining = len(issues) - 20
35
+ if remaining > 0:
36
+ preview = f"{preview}\n- ... un vēl {remaining} problēmas"
37
+ super().__init__(f"Bootstrap dataset validācija neizdevās:\n{preview}")
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class DatasetValidationSummary:
42
+ """Bootstrap dataset validācijas kopsavilkums."""
43
+
44
+ dataset_dir: Path
45
+ files_checked: int
46
+ total_records: int
47
+ counts_by_category: dict[str, int]
48
+ duplicate_count: int
49
+
50
+
51
+ def validate_dataset_dir(
52
+ dataset_dir: str | Path, *, profile: str = "auto"
53
+ ) -> DatasetValidationSummary:
54
+ """Validē lokālo bootstrap dataset direktoriju."""
55
+ root = Path(dataset_dir).expanduser().resolve()
56
+ issues: list[str] = []
57
+
58
+ resolved_profile = _resolve_profile(root, profile)
59
+
60
+ if not root.exists():
61
+ raise DatasetValidationError([f"Dataset direktorija nav atrasta: {root}"])
62
+ if not root.is_dir():
63
+ raise DatasetValidationError([f"Dataset ceļš nav direktorija: {root}"])
64
+
65
+ counts_by_category = {category: 0 for category in DATASET_TYPES}
66
+ duplicate_origins: dict[tuple[str, str], str] = {}
67
+ duplicate_count = 0
68
+ files_checked = 0
69
+
70
+ for file_path in sorted(root.rglob("*.jsonl")):
71
+ if any(part.startswith(".") for part in file_path.relative_to(root).parts):
72
+ continue
73
+ if "hf_cache" in file_path.parts:
74
+ continue
75
+
76
+ try:
77
+ relative_path = file_path.relative_to(root)
78
+ except ValueError:
79
+ relative_path = file_path
80
+
81
+ if len(relative_path.parts) < 2:
82
+ issues.append(f"{relative_path}: JSONL fails nav zem data tipa mapes.")
83
+ continue
84
+
85
+ category = relative_path.parts[0]
86
+ if category not in counts_by_category:
87
+ issues.append(
88
+ f"{relative_path}: neatbalstīta dataset kategorija '{category}'. "
89
+ f"Atļautās: {', '.join(DATASET_TYPES)}."
90
+ )
91
+ continue
92
+
93
+ files_checked += 1
94
+ with file_path.open(encoding="utf-8") as handle:
95
+ for line_number, raw_line in enumerate(handle, start=1):
96
+ stripped = raw_line.strip()
97
+ if not stripped:
98
+ continue
99
+
100
+ location = f"{relative_path}:{line_number}"
101
+ try:
102
+ record = json.loads(stripped)
103
+ except json.JSONDecodeError as exc:
104
+ issues.append(f"{location}: nederīgs JSON ({exc.msg}).")
105
+ continue
106
+
107
+ if not isinstance(record, dict):
108
+ issues.append(f"{location}: ierakstam jābūt JSON objektam.")
109
+ continue
110
+
111
+ counts_by_category[category] += 1
112
+ issues.extend(
113
+ _validate_record(record, category, location, profile=resolved_profile)
114
+ )
115
+
116
+ signature = _record_signature(record, category)
117
+ if signature is None:
118
+ continue
119
+ key = (category, signature)
120
+ first_location = duplicate_origins.get(key)
121
+ if first_location is None:
122
+ duplicate_origins[key] = location
123
+ continue
124
+ duplicate_count += 1
125
+ issues.append(
126
+ f"{location}: dublikāts salīdzinājumā ar {first_location} "
127
+ f"kategorijā '{category}'."
128
+ )
129
+
130
+ if files_checked == 0:
131
+ issues.append(f"{root}: nav atrasts neviens .jsonl bootstrap datu fails.")
132
+
133
+ if issues:
134
+ raise DatasetValidationError(issues)
135
+
136
+ return DatasetValidationSummary(
137
+ dataset_dir=root,
138
+ files_checked=files_checked,
139
+ total_records=sum(counts_by_category.values()),
140
+ counts_by_category=counts_by_category,
141
+ duplicate_count=duplicate_count,
142
+ )
143
+
144
+
145
+ def format_summary(summary: DatasetValidationSummary) -> str:
146
+ """Atgriež īsu cilvēkam lasāmu validācijas kopsavilkumu."""
147
+ category_counts = ", ".join(
148
+ f"{category}={count}" for category, count in summary.counts_by_category.items()
149
+ )
150
+ return (
151
+ f"Dataset validācija veiksmīga: files={summary.files_checked}, "
152
+ f"records={summary.total_records}, duplicates={summary.duplicate_count}; "
153
+ f"{category_counts}"
154
+ )
155
+
156
+
157
+ def _validate_record(
158
+ record: dict[str, Any], category: str, location: str, *, profile: str
159
+ ) -> list[str]:
160
+ issues: list[str] = []
161
+
162
+ for field_name in _COMMON_REQUIRED_STRING_FIELDS:
163
+ value = record.get(field_name)
164
+ if not _is_non_empty_string(value):
165
+ issues.append(f"{location}: trūkst ne-tukša lauka '{field_name}'.")
166
+
167
+ timestamp = record.get("timestamp")
168
+ if isinstance(timestamp, str) and timestamp.strip() and not _is_iso8601_timestamp(timestamp):
169
+ issues.append(f"{location}: lauks 'timestamp' nav ISO-8601 datums ar laika zonu.")
170
+
171
+ record_type = record.get("type")
172
+ if isinstance(record_type, str) and record_type != category:
173
+ issues.append(
174
+ f"{location}: lauks 'type' ir '{record_type}', bet faila kategorijai jābūt '{category}'."
175
+ )
176
+
177
+ if category == "conversation":
178
+ for field_name in _CONVERSATION_REQUIRED_STRING_FIELDS:
179
+ value = record.get(field_name)
180
+ if not _is_non_empty_string(value):
181
+ issues.append(f"{location}: conversation ierakstam trūkst '{field_name}'.")
182
+ if profile == "eval":
183
+ issues.extend(_validate_eval_record(record, category, location))
184
+ return issues
185
+
186
+ if category in _PROMPT_TYPES:
187
+ if not _is_non_empty_string(record.get("prompt")):
188
+ issues.append(f"{location}: ierakstam trūkst ne-tukša lauka 'prompt'.")
189
+ metadata = record.get("metadata")
190
+ if not isinstance(metadata, dict) or not metadata:
191
+ issues.append(f"{location}: ierakstam vajag ne-tukšu objektu laukā 'metadata'.")
192
+
193
+ if profile == "eval":
194
+ issues.extend(_validate_eval_record(record, category, location))
195
+
196
+ return issues
197
+
198
+
199
+ def _is_non_empty_string(value: Any) -> bool:
200
+ return isinstance(value, str) and bool(value.strip())
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:
208
+ normalized = value.strip()
209
+ if normalized.endswith("Z"):
210
+ normalized = f"{normalized[:-1]}+00:00"
211
+ try:
212
+ parsed = datetime.fromisoformat(normalized)
213
+ except ValueError:
214
+ return False
215
+ return parsed.tzinfo is not None
216
+
217
+
218
+ def _record_signature(record: dict[str, Any], category: str) -> str | None:
219
+ if category == "conversation":
220
+ user = record.get("user")
221
+ assistant = record.get("assistant")
222
+ if not (_is_non_empty_string(user) and _is_non_empty_string(assistant)):
223
+ return None
224
+ return "|".join((_normalize_text(user), _normalize_text(assistant)))
225
+
226
+ prompt = record.get("prompt")
227
+ if not _is_non_empty_string(prompt):
228
+ return None
229
+ return _normalize_text(prompt)
230
+
231
+
232
+ def _normalize_text(value: str) -> str:
233
+ return " ".join(value.casefold().split())
234
+
235
+
236
+ 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"
244
+
245
+
246
+ def _validate_eval_record(record: dict[str, Any], category: str, location: str) -> list[str]:
247
+ issues: list[str] = []
248
+
249
+ for field_name in _EVAL_REQUIRED_STRING_FIELDS:
250
+ if not _is_non_empty_string(record.get(field_name)):
251
+ issues.append(f"{location}: eval ierakstam trūkst ne-tukša lauka '{field_name}'.")
252
+
253
+ for field_name in _EVAL_REQUIRED_STRING_LIST_FIELDS:
254
+ if not _is_non_empty_string_list(record.get(field_name)):
255
+ issues.append(
256
+ f"{location}: eval ierakstam vajag ne-tukšu string sarakstu laukā '{field_name}'."
257
+ )
258
+
259
+ if category in _EVAL_REFERENCE_REQUIRED_CATEGORIES:
260
+ if not _is_non_empty_string(record.get("reference_answer")):
261
+ issues.append(f"{location}: {category} eval ierakstam trūkst 'reference_answer'.")
262
+ if not _is_non_empty_string_list(record.get("acceptance_criteria")):
263
+ issues.append(
264
+ f"{location}: {category} eval ierakstam vajag ne-tukšu string sarakstu laukā "
265
+ "'acceptance_criteria'."
266
+ )
267
+
268
+ return issues
core-python/maris_core/images/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """__init__ for images module."""
core-python/maris_core/images/diffusion_pipeline.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Diffusion pipeline palīgklase."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import Any
7
+
8
+ from maris_core.utils.env import get_hf_model
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class DiffusionPipeline:
14
+ """Iesaiņo StableDiffusion/SDXL pipeline."""
15
+
16
+ def __init__(self, model_id: str | None = None) -> None:
17
+ self.model_id = model_id or get_hf_model("IMAGE_MODEL")
18
+ self._pipe: Any = None
19
+
20
+ def load(self) -> None:
21
+ """Ielādē modeli."""
22
+ try:
23
+ import torch # type: ignore
24
+ from diffusers import StableDiffusionPipeline # type: ignore
25
+
26
+ self._pipe = StableDiffusionPipeline.from_pretrained(
27
+ self.model_id, torch_dtype=torch.float16
28
+ )
29
+ self._pipe = self._pipe.to("cuda" if torch.cuda.is_available() else "cpu")
30
+ logger.info("Ielādēts attēlu modelis: %s", self.model_id)
31
+ except Exception as exc: # noqa: BLE001
32
+ logger.error("Nevar ielādēt diffusion modeli: %s", exc)
33
+
34
+ def generate(self, prompt: str, **kwargs: Any) -> Any:
35
+ """Ģenerē attēlu."""
36
+ if self._pipe is None:
37
+ self.load()
38
+ if self._pipe is None:
39
+ return None
40
+ return self._pipe(prompt, **kwargs).images[0]
core-python/maris_core/images/generate_image.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Attēlu ģenerēšana ar Stable Diffusion."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import io
7
+ import logging
8
+
9
+ from fastapi import APIRouter, HTTPException
10
+ from pydantic import BaseModel
11
+
12
+ from maris_core.utils.env import get_hf_model
13
+
14
+ logger = logging.getLogger(__name__)
15
+ router = APIRouter()
16
+
17
+
18
+ class ImageRequest(BaseModel):
19
+ prompt: str
20
+ width: int = 1024
21
+ height: int = 1024
22
+ steps: int = 30
23
+ guidance_scale: float = 7.5
24
+
25
+
26
+ class ImageResponse(BaseModel):
27
+ image_url: str
28
+ prompt: str
29
+
30
+
31
+ @router.post("/generate", response_model=ImageResponse)
32
+ async def generate_image(req: ImageRequest) -> ImageResponse:
33
+ """Ģenerē attēlu pēc teksta apraksta."""
34
+ from maris_core.utils.hf_integration import HFIntegration
35
+
36
+ hf = HFIntegration()
37
+
38
+ try:
39
+ model_id = get_hf_model("IMAGE_MODEL")
40
+ import torch # type: ignore
41
+ from diffusers import StableDiffusionPipeline # type: ignore
42
+
43
+ pipe = StableDiffusionPipeline.from_pretrained(
44
+ model_id,
45
+ torch_dtype=torch.float16,
46
+ )
47
+ pipe = pipe.to("cuda" if torch.cuda.is_available() else "cpu")
48
+
49
+ image = pipe(
50
+ req.prompt,
51
+ width=req.width,
52
+ height=req.height,
53
+ num_inference_steps=req.steps,
54
+ guidance_scale=req.guidance_scale,
55
+ ).images[0]
56
+
57
+ # Konvertē uz base64 data URL
58
+ buf = io.BytesIO()
59
+ image.save(buf, format="PNG")
60
+ b64 = base64.b64encode(buf.getvalue()).decode()
61
+ image_url = f"data:image/png;base64,{b64}"
62
+
63
+ # Saglabā origin atmiņā
64
+ await hf.save_generation("image", req.prompt, {"image_b64": b64[:100] + "..."})
65
+
66
+ return ImageResponse(image_url=image_url, prompt=req.prompt)
67
+
68
+ except Exception as exc: # noqa: BLE001
69
+ logger.error("Attēla ģenerēšanas kļūda: %s", exc)
70
+ raise HTTPException(
71
+ status_code=503,
72
+ detail="Maris AI attēlu ģenerēšana nav pieejama bez konfigurēta IMAGE_MODEL.",
73
+ ) from exc
core-python/maris_core/memory_context.py ADDED
@@ -0,0 +1,644 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Session-aware memory retrieval for text generation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ import os
8
+ import re
9
+ from collections import defaultdict, deque
10
+ from dataclasses import dataclass
11
+ from datetime import UTC, datetime
12
+ from pathlib import Path
13
+ from threading import RLock
14
+ from typing import Any
15
+
16
+ from maris_core.utils.env import get_env_any
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ _TOKEN_RE = re.compile(r"\w+", flags=re.UNICODE)
21
+ _TRIGRAM_WINDOW = 3
22
+ _GENERIC_MEMORY_TOKENS = {
23
+ "tas",
24
+ "tad",
25
+ "par",
26
+ "vai",
27
+ "bet",
28
+ "kur",
29
+ "kad",
30
+ "man",
31
+ "tev",
32
+ "jau",
33
+ "ari",
34
+ "arī",
35
+ "nav",
36
+ "bija",
37
+ "būt",
38
+ "this",
39
+ "that",
40
+ "with",
41
+ "from",
42
+ }
43
+ _SOURCE_BONUS = {
44
+ "live": 0.16,
45
+ "history": 0.12,
46
+ "vision_context": 0.14,
47
+ "voice_stt": 0.11,
48
+ "voice_tts": 0.11,
49
+ "autonomous_goal": 0.14,
50
+ }
51
+ # Marker lists intentionally support both Latvian and common English phrasings because
52
+ # session history may mix languages depending on the user and imported conversation data.
53
+ _USER_FOCUS_MARKERS = (
54
+ "es gribu",
55
+ "es vēlos",
56
+ "man vajag",
57
+ "mans mērķis",
58
+ "man svarīgi",
59
+ "esmu",
60
+ "strādāju",
61
+ "būvēju",
62
+ "veidoju",
63
+ "mēs būvējam",
64
+ "mēs veidojam",
65
+ "i want",
66
+ "i need",
67
+ "my goal",
68
+ "important to me",
69
+ "we are building",
70
+ )
71
+ _USER_GOAL_MARKERS = ("gribu", "vēlos", "vajag", "mērķ", "want", "need")
72
+ _USER_PREFERENCE_MARKERS = ("svarīgi", "prefer", "patīk", "important")
73
+ _USER_FOCUS_QUERY_OVERLAP_WEIGHT = 0.55
74
+ _USER_FOCUS_MARKER_BONUS = 0.24
75
+ _USER_FOCUS_RECENCY_WEIGHT = 0.2
76
+ _ACTIVE_THREAD_MAX_WORDS = 18
77
+ # Some markers intentionally use compact stems so the heuristics catch inflected Latvian forms
78
+ # such as "turpinām", "turpināt", "nākamais", and "prioritātes" without a full stemmer.
79
+ _ACTIVE_THREAD_MARKERS = (
80
+ "?",
81
+ "palīdzi",
82
+ "izveido",
83
+ "uztaisi",
84
+ "turpin",
85
+ "nākam",
86
+ "priorit",
87
+ "vajag",
88
+ "need",
89
+ "help",
90
+ "next step",
91
+ "continue",
92
+ )
93
+ _ACTIVE_THREAD_QUERY_OVERLAP_WEIGHT = 0.55
94
+ _ACTIVE_THREAD_MARKER_BONUS = 0.2
95
+ _ACTIVE_THREAD_RECENCY_WEIGHT = 0.25
96
+ _CONTINUATION_QUERY_BONUS = 0.24
97
+ _CONTINUATION_QUERY_MARKERS = (
98
+ "turpin",
99
+ "iepriekš",
100
+ "šo pašu",
101
+ "šajā pašā",
102
+ "nākam",
103
+ "same context",
104
+ "same thread",
105
+ "continue",
106
+ "pick up",
107
+ "previous context",
108
+ )
109
+
110
+
111
+ @dataclass(frozen=True, slots=True)
112
+ class MemoryMatch:
113
+ role: str
114
+ content: str
115
+ score: float
116
+ source: str
117
+
118
+
119
+ class ConversationMemoryStore:
120
+ """In-memory conversational memory with lightweight relevance scoring."""
121
+
122
+ def __init__(self, max_entries_per_session: int = 120, storage_path: str | None = None) -> None:
123
+ self._max_entries_per_session = max_entries_per_session
124
+ # This store is used from sync code and from worker threads spawned via `asyncio.to_thread`,
125
+ # so a thread lock is the correct synchronization primitive here.
126
+ self._lock = RLock()
127
+ self._sessions: defaultdict[str, deque[dict[str, Any]]] = defaultdict(
128
+ lambda: deque(maxlen=max_entries_per_session)
129
+ )
130
+ self._global: deque[dict[str, Any]] = deque(maxlen=max_entries_per_session * 4)
131
+ self._storage_path = (
132
+ Path(storage_path.strip()).expanduser()
133
+ if storage_path and storage_path.strip()
134
+ else None
135
+ )
136
+ self._load_from_disk()
137
+
138
+ def remember_message(
139
+ self,
140
+ session_id: str,
141
+ role: str,
142
+ content: str,
143
+ *,
144
+ source: str = "live",
145
+ ) -> None:
146
+ normalized_role = role.strip().lower()
147
+ normalized_content = content.strip()
148
+ if normalized_role not in {"user", "assistant"} or not normalized_content:
149
+ return
150
+
151
+ normalized_session_id = session_id.strip() or "default"
152
+ entry = {
153
+ "role": normalized_role,
154
+ "content": normalized_content,
155
+ "timestamp": datetime.now(tz=UTC).isoformat(),
156
+ "source": source,
157
+ }
158
+ with self._lock:
159
+ if self._is_duplicate(self._sessions[normalized_session_id], entry):
160
+ return
161
+ self._sessions[normalized_session_id].append(entry)
162
+ self._global.append({**entry, "session_id": normalized_session_id})
163
+ self._persist_to_disk()
164
+
165
+ def seed_history(self, session_id: str, history: list[dict[str, str]]) -> None:
166
+ for item in history:
167
+ self.remember_message(
168
+ session_id,
169
+ item.get("role", ""),
170
+ item.get("content", ""),
171
+ source="history",
172
+ )
173
+
174
+ def retrieve_relevant_context(
175
+ self, session_id: str, query: str, *, limit: int = 4
176
+ ) -> list[MemoryMatch]:
177
+ query_token_sequence = _extract_semantic_token_sequence(query)
178
+ query_tokens = set(query_token_sequence)
179
+ query_text = query.strip().lower()
180
+ continuation_query = _looks_like_continuation_query(query_text)
181
+ if not query_tokens and not continuation_query:
182
+ return []
183
+
184
+ normalized_session_id = session_id.strip() or "default"
185
+ query_phrases = _extract_phrases(query_token_sequence)
186
+ query_trigrams = _char_trigrams(query_text)
187
+ with self._lock:
188
+ candidates = list(self._sessions.get(normalized_session_id, ())) + list(self._global)
189
+ ranked: list[MemoryMatch] = []
190
+ seen: set[tuple[str, str]] = set()
191
+ total = max(len(candidates), 1)
192
+
193
+ for index, candidate in enumerate(candidates):
194
+ content = str(candidate.get("content", "")).strip()
195
+ role = str(candidate.get("role", "")).strip().lower()
196
+ if not content or content == query or role not in {"user", "assistant"}:
197
+ continue
198
+
199
+ content_text = content.lower()
200
+ candidate_token_sequence = _extract_semantic_token_sequence(content_text)
201
+ candidate_tokens = set(candidate_token_sequence)
202
+ if not candidate_tokens:
203
+ continue
204
+
205
+ overlap = _jaccard_similarity(query_tokens, candidate_tokens)
206
+ phrase_overlap = _jaccard_similarity(
207
+ query_phrases,
208
+ _extract_phrases(candidate_token_sequence),
209
+ )
210
+ trigram_overlap = _jaccard_similarity(query_trigrams, _char_trigrams(content_text))
211
+ if (
212
+ overlap <= 0
213
+ and phrase_overlap <= 0
214
+ and trigram_overlap < 0.12
215
+ and query_text not in content_text
216
+ and content_text not in query_text
217
+ and not continuation_query
218
+ ):
219
+ continue
220
+
221
+ recency_bonus = (index + 1) / total * 0.22
222
+ session_bonus = (
223
+ 0.24
224
+ if candidate.get("session_id", normalized_session_id) == normalized_session_id
225
+ else 0.04
226
+ )
227
+ substring_bonus = 0.18 if query_text in content_text else 0.0
228
+ source_bonus = _SOURCE_BONUS.get(str(candidate.get("source", "memory")), 0.08)
229
+ continuation_bonus = (
230
+ _CONTINUATION_QUERY_BONUS
231
+ if continuation_query
232
+ and candidate.get("session_id", normalized_session_id) == normalized_session_id
233
+ else 0.0
234
+ )
235
+ score = (
236
+ overlap * 0.55
237
+ + phrase_overlap * 0.25
238
+ + trigram_overlap * 0.20
239
+ + recency_bonus
240
+ + session_bonus
241
+ + substring_bonus
242
+ + source_bonus
243
+ + continuation_bonus
244
+ )
245
+
246
+ dedupe_key = (role, content)
247
+ if dedupe_key in seen:
248
+ continue
249
+ seen.add(dedupe_key)
250
+ ranked.append(
251
+ MemoryMatch(
252
+ role=role,
253
+ content=content,
254
+ score=score,
255
+ source=str(candidate.get("source", "memory")),
256
+ )
257
+ )
258
+
259
+ ranked.sort(key=lambda item: item.score, reverse=True)
260
+ return ranked[:limit]
261
+
262
+ def summarize_session(self, session_id: str, *, limit: int = 4) -> list[str]:
263
+ normalized_session_id = session_id.strip() or "default"
264
+ with self._lock:
265
+ entries = list(self._sessions.get(normalized_session_id, ()))
266
+
267
+ if not entries:
268
+ return []
269
+
270
+ summaries: list[str] = []
271
+ seen: set[str] = set()
272
+ recent_entries = reversed(entries[-min(len(entries), limit * 3) :])
273
+ for entry in recent_entries:
274
+ content = str(entry.get("content", "")).strip()
275
+ if not content:
276
+ continue
277
+ concise = _compact_summary_text(content)
278
+ if not concise:
279
+ continue
280
+ lowered = concise.lower()
281
+ if lowered in seen:
282
+ continue
283
+ seen.add(lowered)
284
+ role = str(entry.get("role", "")).strip().lower() or "assistant"
285
+ prefix = "Lietotājs" if role == "user" else "Maris"
286
+ summaries.append(f"{prefix}: {concise}")
287
+ if len(summaries) >= limit:
288
+ break
289
+
290
+ summaries.reverse()
291
+ return summaries
292
+
293
+ def summarize_user_focus(
294
+ self,
295
+ session_id: str,
296
+ *,
297
+ query: str = "",
298
+ limit: int = 4,
299
+ ) -> list[str]:
300
+ normalized_session_id = session_id.strip() or "default"
301
+ query_tokens = set(_extract_semantic_token_sequence(query))
302
+ with self._lock:
303
+ entries = list(self._sessions.get(normalized_session_id, ()))
304
+
305
+ if not entries:
306
+ return []
307
+
308
+ candidates: list[tuple[float, int, str]] = []
309
+ total = len(entries)
310
+ for index, entry in enumerate(entries):
311
+ if str(entry.get("role", "")).strip().lower() != "user":
312
+ continue
313
+ content = str(entry.get("content", "")).strip()
314
+ if not content:
315
+ continue
316
+ for candidate in _extract_user_focus_candidates(content):
317
+ candidate_tokens = set(_extract_semantic_token_sequence(candidate))
318
+ overlap = (
319
+ _jaccard_similarity(query_tokens, candidate_tokens) if query_tokens else 0.0
320
+ )
321
+ marker_bonus = (
322
+ _USER_FOCUS_MARKER_BONUS if _looks_like_user_focus(candidate) else 0.0
323
+ )
324
+ recency_bonus = ((index + 1) / max(total, 1)) * _USER_FOCUS_RECENCY_WEIGHT
325
+ score = overlap * _USER_FOCUS_QUERY_OVERLAP_WEIGHT + marker_bonus + recency_bonus
326
+ candidates.append((score, index, candidate))
327
+
328
+ if not candidates:
329
+ return []
330
+
331
+ candidates.sort(key=lambda item: (item[0], item[1]), reverse=True)
332
+ summaries: list[str] = []
333
+ seen: set[str] = set()
334
+ for _score, _index, candidate in candidates:
335
+ lowered = candidate.lower()
336
+ if lowered in seen:
337
+ continue
338
+ seen.add(lowered)
339
+ label = _classify_user_focus(candidate, lowered=lowered)
340
+ summaries.append(f"{label}: {candidate}")
341
+ if len(summaries) >= limit:
342
+ break
343
+ return summaries
344
+
345
+ def summarize_active_threads(
346
+ self,
347
+ session_id: str,
348
+ *,
349
+ query: str = "",
350
+ limit: int = 3,
351
+ ) -> list[str]:
352
+ normalized_session_id = session_id.strip() or "default"
353
+ query_tokens = set(_extract_semantic_token_sequence(query))
354
+ with self._lock:
355
+ entries = list(self._sessions.get(normalized_session_id, ()))
356
+
357
+ if not entries:
358
+ return []
359
+
360
+ candidates: list[tuple[float, int, str]] = []
361
+ total = len(entries)
362
+ for index, entry in enumerate(entries):
363
+ if str(entry.get("role", "")).strip().lower() != "user":
364
+ continue
365
+ content = str(entry.get("content", "")).strip()
366
+ if not content:
367
+ continue
368
+ for candidate in _extract_active_thread_candidates(content):
369
+ lowered = candidate.lower()
370
+ candidate_tokens = set(_extract_semantic_token_sequence(candidate))
371
+ overlap = (
372
+ _jaccard_similarity(query_tokens, candidate_tokens) if query_tokens else 0.0
373
+ )
374
+ marker_bonus = (
375
+ _ACTIVE_THREAD_MARKER_BONUS
376
+ if _looks_like_active_thread(candidate, lowered=lowered)
377
+ else 0.0
378
+ )
379
+ recency_bonus = ((index + 1) / max(total, 1)) * _ACTIVE_THREAD_RECENCY_WEIGHT
380
+ score = overlap * _ACTIVE_THREAD_QUERY_OVERLAP_WEIGHT + marker_bonus + recency_bonus
381
+ candidates.append((score, index, candidate))
382
+
383
+ if not candidates:
384
+ return []
385
+
386
+ candidates.sort(key=lambda item: (item[0], item[1]), reverse=True)
387
+ summaries: list[str] = []
388
+ seen: set[str] = set()
389
+ for _score, _index, candidate in candidates:
390
+ lowered = candidate.lower()
391
+ if lowered in seen:
392
+ continue
393
+ seen.add(lowered)
394
+ label = _classify_active_thread(candidate, lowered=lowered)
395
+ summaries.append(f"{label}: {candidate}")
396
+ if len(summaries) >= limit:
397
+ break
398
+ return summaries
399
+
400
+ def clear(self) -> None:
401
+ with self._lock:
402
+ self._sessions.clear()
403
+ self._global.clear()
404
+ self._persist_to_disk()
405
+
406
+ @staticmethod
407
+ def _is_duplicate(buffer: deque[dict[str, Any]], entry: dict[str, Any]) -> bool:
408
+ if not buffer:
409
+ return False
410
+ latest = buffer[-1]
411
+ return latest.get("role") == entry["role"] and latest.get("content") == entry["content"]
412
+
413
+ def _load_from_disk(self) -> None:
414
+ if self._storage_path is None or not self._storage_path.exists():
415
+ return
416
+
417
+ try:
418
+ payload = json.loads(self._storage_path.read_text(encoding="utf-8"))
419
+ except Exception as exc: # noqa: BLE001
420
+ logger.warning("Neizdevās ielādēt sarunu atmiņu no %s: %s", self._storage_path, exc)
421
+ return
422
+
423
+ sessions = payload.get("sessions", {})
424
+ if not isinstance(sessions, dict):
425
+ return
426
+
427
+ for session_id, entries in sessions.items():
428
+ if not isinstance(session_id, str) or not isinstance(entries, list):
429
+ continue
430
+ for entry in entries:
431
+ if not isinstance(entry, dict):
432
+ continue
433
+ role = str(entry.get("role", "")).strip().lower()
434
+ content = str(entry.get("content", "")).strip()
435
+ timestamp = (
436
+ str(entry.get("timestamp", "")).strip() or datetime.now(tz=UTC).isoformat()
437
+ )
438
+ source = str(entry.get("source", "disk")).strip() or "disk"
439
+ if role not in {"user", "assistant"} or not content:
440
+ continue
441
+ normalized_entry = {
442
+ "role": role,
443
+ "content": content,
444
+ "timestamp": timestamp,
445
+ "source": source,
446
+ }
447
+ if self._is_duplicate(self._sessions[session_id], normalized_entry):
448
+ continue
449
+ self._sessions[session_id].append(normalized_entry)
450
+ self._global.append({**normalized_entry, "session_id": session_id})
451
+
452
+ def _persist_to_disk(self) -> None:
453
+ if self._storage_path is None:
454
+ return
455
+
456
+ try:
457
+ self._storage_path.parent.mkdir(parents=True, exist_ok=True)
458
+ payload = {
459
+ "sessions": {
460
+ session_id: list(entries) for session_id, entries in self._sessions.items()
461
+ }
462
+ }
463
+ tmp_path = self._storage_path.with_name(f"{self._storage_path.name}.tmp")
464
+ tmp_path.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8")
465
+ os.replace(tmp_path, self._storage_path)
466
+ except Exception as exc: # noqa: BLE001
467
+ logger.warning("Neizdevās saglabāt sarunu atmiņu uz %s: %s", self._storage_path, exc)
468
+
469
+
470
+ memory_store = ConversationMemoryStore(
471
+ storage_path=get_env_any(
472
+ "MARIS_MEMORY_STORE_PATH",
473
+ "MARIS_CONVERSATION_MEMORY_PATH",
474
+ default="~/.maris/conversation-memory.json",
475
+ )
476
+ )
477
+
478
+
479
+ def _jaccard_similarity(set_a: set[str], set_b: set[str]) -> float:
480
+ union = set_a | set_b
481
+ if not union:
482
+ return 0.0
483
+ return len(set_a & set_b) / len(union)
484
+
485
+
486
+ def _extract_semantic_tokens(text: str) -> set[str]:
487
+ return set(_extract_semantic_token_sequence(text))
488
+
489
+
490
+ def _extract_semantic_token_sequence(text: str) -> list[str]:
491
+ tokens: list[str] = []
492
+ for raw_token in _TOKEN_RE.findall(text.lower()):
493
+ token = _normalize_token(raw_token)
494
+ if len(token) < 3 or token in _GENERIC_MEMORY_TOKENS:
495
+ continue
496
+ tokens.append(token)
497
+ return tokens
498
+
499
+
500
+ def _normalize_token(token: str) -> str:
501
+ normalized = token.lower().strip("-_ ")
502
+ for suffix in (
503
+ "ajiem",
504
+ "ajām",
505
+ "ajai",
506
+ "ajos",
507
+ "ajās",
508
+ "ības",
509
+ "iem",
510
+ "ām",
511
+ "ais",
512
+ "ajā",
513
+ "ing",
514
+ "ers",
515
+ "ies",
516
+ "us",
517
+ "as",
518
+ "es",
519
+ "am",
520
+ "em",
521
+ "ai",
522
+ "ei",
523
+ "u",
524
+ "a",
525
+ "i",
526
+ "s",
527
+ ):
528
+ if normalized.endswith(suffix) and len(normalized) - len(suffix) >= 4:
529
+ return normalized[: -len(suffix)]
530
+ return normalized
531
+
532
+
533
+ def _extract_phrases(tokens: list[str]) -> set[str]:
534
+ if len(tokens) < 2:
535
+ return set()
536
+ return {f"{tokens[index]} {tokens[index + 1]}" for index in range(len(tokens) - 1)}
537
+
538
+
539
+ def _char_trigrams(text: str) -> set[str]:
540
+ compact = re.sub(r"\s+", " ", text.strip().lower())
541
+ if len(compact) < _TRIGRAM_WINDOW:
542
+ return {compact} if compact else set()
543
+ return {
544
+ compact[index : index + _TRIGRAM_WINDOW]
545
+ for index in range(len(compact) - _TRIGRAM_WINDOW + 1)
546
+ }
547
+
548
+
549
+ def _compact_summary_text(text: str, *, max_words: int = 18) -> str:
550
+ cleaned = re.sub(r"\s+", " ", text.strip())
551
+ if not cleaned:
552
+ return ""
553
+ words = cleaned.split(" ")
554
+ if len(words) <= max_words:
555
+ return cleaned
556
+ return " ".join(words[:max_words]).rstrip(" ,;:.") + "…"
557
+
558
+
559
+ def _extract_user_focus_candidates(text: str) -> list[str]:
560
+ parts = re.split(r"(?<=[.!?])\s+|\n+", text)
561
+ candidates: list[str] = []
562
+ for part in parts:
563
+ cleaned = _build_user_focus_candidate(part)
564
+ if not cleaned:
565
+ continue
566
+ candidates.append(cleaned)
567
+ if candidates:
568
+ return candidates
569
+
570
+ compact = _build_user_focus_candidate(text)
571
+ return [compact] if compact else []
572
+
573
+
574
+ def _build_user_focus_candidate(text: str) -> str:
575
+ compact = _compact_summary_text(text, max_words=20)
576
+ lowered = compact.lower()
577
+ if not compact or not _looks_like_user_focus(compact, lowered=lowered):
578
+ return ""
579
+ return compact
580
+
581
+
582
+ def _extract_active_thread_candidates(text: str) -> list[str]:
583
+ parts = re.split(r"(?<=[.!?])\s+|\n+", text)
584
+ candidates: list[str] = []
585
+ for part in parts:
586
+ cleaned = _build_active_thread_candidate(part)
587
+ if not cleaned:
588
+ continue
589
+ candidates.append(cleaned)
590
+ if candidates:
591
+ return candidates
592
+
593
+ compact = _build_active_thread_candidate(text)
594
+ return [compact] if compact else []
595
+
596
+
597
+ def _build_active_thread_candidate(text: str) -> str:
598
+ compact = _compact_summary_text(text, max_words=_ACTIVE_THREAD_MAX_WORDS)
599
+ lowered = compact.lower()
600
+ if not compact or not _looks_like_active_thread(compact, lowered=lowered):
601
+ return ""
602
+ return compact
603
+
604
+
605
+ def _looks_like_user_focus(text: str, *, lowered: str | None = None) -> bool:
606
+ lowered = lowered if lowered is not None else text.lower()
607
+ return any(marker in lowered for marker in _USER_FOCUS_MARKERS)
608
+
609
+
610
+ def _classify_user_focus(text: str, *, lowered: str | None = None) -> str:
611
+ lowered = lowered if lowered is not None else text.lower()
612
+ if any(marker in lowered for marker in _USER_GOAL_MARKERS):
613
+ return "Mērķis"
614
+ if any(marker in lowered for marker in _USER_PREFERENCE_MARKERS):
615
+ return "Priekšroka"
616
+ return "Konteksts"
617
+
618
+
619
+ def _looks_like_active_thread(text: str, *, lowered: str | None = None) -> bool:
620
+ lowered = lowered if lowered is not None else text.lower()
621
+ has_question_signal = _contains_question_signal(text, lowered=lowered)
622
+ return any(
623
+ has_question_signal if marker == "?" else marker in lowered
624
+ for marker in _ACTIVE_THREAD_MARKERS
625
+ )
626
+
627
+
628
+ def _classify_active_thread(text: str, *, lowered: str | None = None) -> str:
629
+ lowered = lowered if lowered is not None else text.lower()
630
+ if _contains_question_signal(text, lowered=lowered):
631
+ return "Atvērtais jautājums"
632
+ return "Aktīvais virziens"
633
+
634
+
635
+ def _contains_question_signal(text: str, *, lowered: str | None = None) -> bool:
636
+ lowered = lowered if lowered is not None else text.lower()
637
+ return "?" in text or any(
638
+ marker in lowered for marker in ("kā", "kas", "kur", "kad", "why", "how")
639
+ )
640
+
641
+
642
+ def _looks_like_continuation_query(text: str) -> bool:
643
+ lowered = text.lower()
644
+ return any(marker in lowered for marker in _CONTINUATION_QUERY_MARKERS)
core-python/maris_core/orchestrator/__init__.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Master model orchestration helpers for Maris AI."""
2
+
3
+ from maris_core.orchestrator.routing import (
4
+ CapabilityBranch,
5
+ MasterModelInfo,
6
+ RouteDecision,
7
+ build_system_prompt,
8
+ detect_route,
9
+ get_master_model_info,
10
+ get_specialist_branches,
11
+ )
12
+
13
+ __all__ = [
14
+ "CapabilityBranch",
15
+ "MasterModelInfo",
16
+ "RouteDecision",
17
+ "build_system_prompt",
18
+ "detect_route",
19
+ "get_master_model_info",
20
+ "get_specialist_branches",
21
+ ]
core-python/maris_core/orchestrator/api.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """API endpoints for master-model orchestration metadata."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from fastapi import APIRouter
6
+ from pydantic import BaseModel
7
+
8
+ from maris_core.orchestrator.routing import (
9
+ CapabilityBranch,
10
+ MasterModelInfo,
11
+ RouteDecision,
12
+ detect_route,
13
+ get_master_model_info,
14
+ get_specialist_branches,
15
+ )
16
+
17
+ router = APIRouter()
18
+
19
+
20
+ class RouteRequest(BaseModel):
21
+ message: str
22
+ session_id: str | None = None
23
+ persona_id: str | None = None
24
+ has_vision_input: bool = False
25
+
26
+
27
+ class SystemTopologyResponse(BaseModel):
28
+ name: str
29
+ description: str
30
+ author: str
31
+ orchestration_mode: str
32
+ master_model: MasterModelInfo
33
+ branches: list[CapabilityBranch]
34
+
35
+
36
+ @router.get("/system", response_model=SystemTopologyResponse)
37
+ async def get_system_topology() -> SystemTopologyResponse:
38
+ return SystemTopologyResponse(
39
+ name="Maris AI",
40
+ description="Galvenais modelis ar specializētiem adapteriem un multimodāliem atzariem.",
41
+ author="Māris — Maris AI Tēvs",
42
+ orchestration_mode="master_with_specialist_branches",
43
+ master_model=get_master_model_info(),
44
+ branches=get_specialist_branches(),
45
+ )
46
+
47
+
48
+ @router.post("/route", response_model=RouteDecision)
49
+ async def route_request(req: RouteRequest) -> RouteDecision:
50
+ return detect_route(
51
+ req.message,
52
+ session_id=req.session_id,
53
+ persona_id=req.persona_id,
54
+ has_vision_input=req.has_vision_input,
55
+ )
core-python/maris_core/orchestrator/routing.py ADDED
@@ -0,0 +1,560 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Routing and topology metadata for the Maris master model."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ import re
8
+ from dataclasses import dataclass
9
+ from datetime import UTC, datetime
10
+ from typing import Literal
11
+
12
+ from pydantic import BaseModel, Field
13
+
14
+ from maris_core.personas import resolve_persona
15
+ from maris_core.utils.emotional_context import EmotionalContext
16
+ from maris_core.utils.env import (
17
+ get_env_any,
18
+ get_hf_model,
19
+ get_optional_hf_model,
20
+ validate_hf_model,
21
+ )
22
+
23
+ MASTER_MODEL_DEFAULT = "MarisUK/maris-ai-master"
24
+ TEXT_MODEL_DEFAULT = "MarisUK/maris-ai-text"
25
+ DISABLED_MODEL_LABEL = "Maris AI branch not configured"
26
+ logger = logging.getLogger(__name__)
27
+ _PERSONA_PROFILE_MAP = {
28
+ "assistant": "general",
29
+ "strategist": "planner",
30
+ "coder": "coder",
31
+ "analyst": "analyst",
32
+ "teacher": "teacher",
33
+ "coach": "coach",
34
+ "designer": "designer",
35
+ }
36
+ _CODE_ACTION_PATTERN = re.compile(
37
+ r"\b("
38
+ r"uzraksti|uzprogrammē|uztaisi|izveido|implement|ieviest|ģenerē|salabo|fix|"
39
+ r"debug|patch|refactor|rewrite"
40
+ r")\b",
41
+ flags=re.IGNORECASE,
42
+ )
43
+ _CODE_SUBJECT_PATTERN = re.compile(
44
+ r"\b("
45
+ r"kod|python|rust|typescript|javascript|sql|regex|skript|funkcij|helper|endpoint|komponent|"
46
+ r"query|test|bug|stack trace|api klient|repo|frontend|backend"
47
+ r")\b",
48
+ flags=re.IGNORECASE,
49
+ )
50
+ _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,
58
+ )
59
+ _EXPLANATION_INTENT_PATTERN = re.compile(
60
+ r"\b("
61
+ r"pastāsti|paskaidro|izskaidro|kas ir|kā strādā|kāpēc|palīdzi saprast|advice|ieteik"
62
+ r")\b",
63
+ flags=re.IGNORECASE,
64
+ )
65
+ _CONVERSATIONAL_INTENT_PATTERN = re.compile(
66
+ r"\b("
67
+ r"sarun|parunā|parunāt|izrunā|apspried|diskut|konsult|chat|conversation"
68
+ r")\b",
69
+ flags=re.IGNORECASE,
70
+ )
71
+ _CREATION_OR_EXECUTION_INTENT_PATTERN = re.compile(
72
+ r"\b("
73
+ r"uzraksti|uzprogrammē|uztaisi|izveido|implement|ieviest|ģenerē|uzģenerē|uzzīmē|"
74
+ r"salabo|fix|debug|patch|refactor|rewrite|veic|izpildi|palaid|komponē|render|"
75
+ r"nolasī|transcribe|sintezē"
76
+ r")\b",
77
+ flags=re.IGNORECASE,
78
+ )
79
+
80
+
81
+ @dataclass(frozen=True, slots=True)
82
+ class RouteRule:
83
+ keywords: tuple[str, ...]
84
+ capability: str
85
+ reasoning: str
86
+ confidence: float
87
+
88
+
89
+ class MasterModelInfo(BaseModel):
90
+ name: str
91
+ role: str
92
+ routing_strategy: str
93
+ primary_capabilities: list[str]
94
+
95
+
96
+ class CapabilityBranch(BaseModel):
97
+ capability: str
98
+ branch: str
99
+ kind: Literal["master", "adapter", "specialist_model"]
100
+ profile: str
101
+ model: str
102
+ endpoint: str
103
+ studio: str
104
+ description: str
105
+
106
+
107
+ class RouteDecision(BaseModel):
108
+ capability: str
109
+ branch: str
110
+ profile: str
111
+ target_endpoint: str
112
+ target_studio: str
113
+ reasoning: str
114
+ confidence: float = Field(ge=0.0, le=1.0)
115
+ session_context: SessionContext = Field(default_factory=lambda: SessionContext())
116
+
117
+
118
+ class SessionContext(BaseModel):
119
+ session_id: str = "default"
120
+ persona_id: str = "assistant"
121
+ memory_enabled: bool = True
122
+ timestamp: str = Field(default_factory=lambda: datetime.now(tz=UTC).isoformat())
123
+
124
+
125
+ def _master_model() -> str:
126
+ return get_hf_model("MARIS_MODEL_REPO", "HF_MODEL_REPO", default=MASTER_MODEL_DEFAULT)
127
+
128
+
129
+ def _text_model() -> str:
130
+ runtime_override = get_env_any("MARIS_RUNTIME_TEXT_MODEL", "HF_RUNTIME_TEXT_MODEL")
131
+ if runtime_override:
132
+ return validate_hf_model(
133
+ runtime_override,
134
+ "MARIS_RUNTIME_TEXT_MODEL/HF_RUNTIME_TEXT_MODEL",
135
+ )
136
+ return get_hf_model("TEXT_MODEL", default=TEXT_MODEL_DEFAULT)
137
+
138
+
139
+ def resolve_text_model() -> str:
140
+ return _text_model()
141
+
142
+
143
+ def _specialist_model(*names: str) -> str:
144
+ return get_optional_hf_model(*names) or DISABLED_MODEL_LABEL
145
+
146
+
147
+ def get_master_model_info() -> MasterModelInfo:
148
+ return MasterModelInfo(
149
+ name=_master_model(),
150
+ role="Galvenais Maris reasoning un koordinācijas modelis",
151
+ routing_strategy="master_with_specialist_branches",
152
+ primary_capabilities=[
153
+ "conversation",
154
+ "reasoning",
155
+ "planning",
156
+ "tool_routing",
157
+ "memory_context",
158
+ ],
159
+ )
160
+
161
+
162
+ def get_specialist_branches() -> list[CapabilityBranch]:
163
+ master_model = _master_model()
164
+ text_model = _text_model()
165
+ return [
166
+ CapabilityBranch(
167
+ capability="text_chat",
168
+ branch="master",
169
+ kind="master",
170
+ profile="general",
171
+ model=text_model,
172
+ endpoint="text/generate",
173
+ studio="/chat",
174
+ description="Galvenais teksta sarunu režīms ikdienas dialogam un čata atbildēm.",
175
+ ),
176
+ CapabilityBranch(
177
+ capability="vision_analysis",
178
+ branch="vision",
179
+ kind="adapter",
180
+ profile="vision",
181
+ model=master_model,
182
+ endpoint="text/generate",
183
+ studio="/vision",
184
+ description="Vision-aware sarunas atzars attēlu, snapshot un kameru konteksta interpretācijai.",
185
+ ),
186
+ CapabilityBranch(
187
+ capability="code_generation",
188
+ branch="coder",
189
+ kind="adapter",
190
+ profile="coder",
191
+ model=master_model,
192
+ endpoint="code/generate",
193
+ studio="/code",
194
+ description="Teksta bāzes kodēšanas atzars ar stingrāku tehnisko instrukciju stilu.",
195
+ ),
196
+ CapabilityBranch(
197
+ capability="autonomous_tasks",
198
+ branch="planner",
199
+ kind="adapter",
200
+ profile="planner",
201
+ model=master_model,
202
+ endpoint="autonomous/start",
203
+ studio="/autonomous",
204
+ description="Plānošanas un uzdevumu sadalīšanas atzars ilgākiem mērķiem.",
205
+ ),
206
+ CapabilityBranch(
207
+ capability="voice_conversation",
208
+ branch="voice",
209
+ kind="specialist_model",
210
+ profile="voice",
211
+ model=_specialist_model("TTS_MODEL"),
212
+ endpoint="audio/tts",
213
+ studio="/voice",
214
+ description="Balss atzars STT/TTS darbībām un balss sesijām.",
215
+ ),
216
+ CapabilityBranch(
217
+ capability="image_generation",
218
+ branch="vision",
219
+ kind="specialist_model",
220
+ profile="image",
221
+ model=_specialist_model("IMAGE_MODEL"),
222
+ endpoint="images/generate",
223
+ studio="/images",
224
+ description="Specializēts attēlu ģenerēšanas modelis vizuālajai produkcijai.",
225
+ ),
226
+ CapabilityBranch(
227
+ capability="music_generation",
228
+ branch="music",
229
+ kind="specialist_model",
230
+ profile="music",
231
+ model=_specialist_model("MUSIC_MODEL"),
232
+ endpoint="audio/generate_music",
233
+ studio="/music",
234
+ description="Mūzikas atzars audio kompozīciju un preview ģenerēšanai.",
235
+ ),
236
+ CapabilityBranch(
237
+ capability="video_generation",
238
+ branch="video",
239
+ kind="specialist_model",
240
+ profile="video",
241
+ model=_specialist_model("VIDEO_MODEL"),
242
+ endpoint="video/generate",
243
+ studio="/video",
244
+ description="Video ģenerēšanas atzars klipu un motion satura radīšanai.",
245
+ ),
246
+ ]
247
+
248
+
249
+ def _find_branch(capability: str) -> CapabilityBranch:
250
+ for branch in get_specialist_branches():
251
+ if branch.capability == capability:
252
+ return branch
253
+ return get_specialist_branches()[0]
254
+
255
+
256
+ def _branch_enabled(branch: CapabilityBranch) -> bool:
257
+ return branch.kind == "master" or branch.model != DISABLED_MODEL_LABEL
258
+
259
+
260
+ def _fallback_route(capability: str, reasoning: str, confidence: float) -> RouteDecision:
261
+ branch = _find_branch(capability)
262
+ if not _branch_enabled(branch):
263
+ branch = _find_branch("text_chat")
264
+ reasoning = f"{reasoning} Pieprasītais multimodālais atzars nav konfigurēts, tāpēc izmantojam Maris AI galveno teksta atzaru."
265
+ return RouteDecision(
266
+ capability=branch.capability,
267
+ branch=branch.branch,
268
+ profile=branch.profile,
269
+ target_endpoint=branch.endpoint,
270
+ target_studio=branch.studio,
271
+ reasoning=reasoning,
272
+ confidence=confidence,
273
+ )
274
+
275
+
276
+ def _build_session_context(
277
+ session_id: str | None = None,
278
+ persona_id: str | None = None,
279
+ ) -> SessionContext:
280
+ normalized_session_id = (session_id or "").strip() or "default"
281
+ normalized_persona_id = (persona_id or "").strip() or "assistant"
282
+ return SessionContext(
283
+ session_id=normalized_session_id,
284
+ persona_id=normalized_persona_id,
285
+ )
286
+
287
+
288
+ def build_system_prompt(
289
+ profile: str | None,
290
+ emotional_context: EmotionalContext | None = None,
291
+ *,
292
+ persona_id: str | None = None,
293
+ ) -> str:
294
+ persona = resolve_persona(persona_id)
295
+ profile_name = _resolve_effective_profile(profile, persona.id)
296
+ identity = (
297
+ "Tu esi Maris AI — galvenais mākslīgais intelekts. "
298
+ "Tu koordinē specializētos atzarus, saglabā vienotu identitāti un atbildi profesionāli. "
299
+ "Tava identitāte: profesionāls, draudzīgs, viedokļains. Tavs tēvs ir Māris."
300
+ )
301
+
302
+ profile_instructions = {
303
+ "general": "Atbildi skaidri, praktiski un ar labu spriestspēju.",
304
+ "coder": "Domā kā senior programmētājs: precīzi, strukturēti un tehniski korekti.",
305
+ "planner": "Domā kā izpilddirektors un plānotājs: sadali mērķi soļos, prioritizē un strukturē.",
306
+ "analyst": "Domā kā pētnieks analītiķis: salīdzini variantus, turi faktu disciplīnu un izcel trade-off.",
307
+ "teacher": "Domā kā eksperts skolotājs: paskaidro slāņaini, saprotami un bez precizitātes zaudēšanas.",
308
+ "coach": "Domā kā izpildes treneris: uzturi momentum, empātiju un skaidru nākamo soli.",
309
+ "designer": "Domā kā creative director: turi gaumi, konceptuālo skaidrību un producēšanas kvalitāti.",
310
+ "voice": "Gatavo saturu tā, lai to būtu viegli pateikt skaļi un uztvert sarunā.",
311
+ "vision": "Apraksti vizuālos novērojumus precīzi, skaidri norādi nenoteiktību un sasaisti redzamo ar lietotāja mērķi.",
312
+ "image": "Apraksti vizuālo ideju precīzi un producēšanai gatavā stilā.",
313
+ "music": "Apraksti muzikālo noskaņu, ritmu un aranžējumu saprotami producēšanai.",
314
+ "video": "Apraksti ainas, kameru, kustību un montāžas sajūtu ļoti konkrēti.",
315
+ }
316
+
317
+ prompt = (
318
+ f"{identity} {profile_instructions.get(profile_name, profile_instructions['general'])} "
319
+ f"Aktīvā persona: {persona.title}. {persona.summary} "
320
+ f"Komunikācijas stils: {persona.communication_style}. {persona.prompt_overlay}"
321
+ )
322
+ if emotional_context is None or emotional_context.emotion == "neutral":
323
+ return prompt
324
+
325
+ return (
326
+ f"{prompt} Lietotāja pašreizējais emocionālais tonis šķiet "
327
+ f"{emotional_context.description}. "
328
+ f"{emotional_context.guidance}"
329
+ )
330
+
331
+
332
+ def _resolve_effective_profile(profile: str | None, persona_id: str) -> str:
333
+ requested = (profile or "").strip().lower()
334
+ if requested and requested != "general":
335
+ return requested
336
+
337
+ return _PERSONA_PROFILE_MAP.get(persona_id, "general")
338
+
339
+
340
+ def _extract_json_payload(raw_text: str) -> dict[str, object] | None:
341
+ match = re.search(r"\{.*\}", raw_text, flags=re.DOTALL)
342
+ if match is None:
343
+ return None
344
+
345
+ try:
346
+ payload = json.loads(match.group(0))
347
+ except json.JSONDecodeError:
348
+ return None
349
+
350
+ return payload if isinstance(payload, dict) else None
351
+
352
+
353
+ def _llm_route(message: str, *, has_vision_input: bool = False) -> RouteDecision | None:
354
+ from maris_core.text.generate import call_generation_pipeline, get_pipeline
355
+
356
+ pipe = get_pipeline()
357
+ if pipe is None:
358
+ return None
359
+
360
+ branches = get_specialist_branches()
361
+ capability_map = {
362
+ branch.capability: branch
363
+ for branch in branches
364
+ if branch.capability != "voice_conversation" and _branch_enabled(branch)
365
+ }
366
+ catalog = "\n".join(
367
+ f"- {branch.capability}: branch={branch.branch}, profile={branch.profile}, studio={branch.studio}"
368
+ for branch in capability_map.values()
369
+ )
370
+ messages = [
371
+ {
372
+ "role": "system",
373
+ "content": (
374
+ "Tu esi Maris AI routing orchestrator. "
375
+ "Izvēlies tieši vienu capability no saraksta un atbildi tikai ar derīgu JSON. "
376
+ "JSON struktūra: "
377
+ '{"capability":"...","reasoning":"...","confidence":0.0}. '
378
+ "confidence jābūt skaitlim no 0 līdz 1. "
379
+ "Izvēlies capability, kas vislabāk atbilst lietotāja pieprasījumam. "
380
+ "Izvēlies text_chat pēc noklusējuma visām parastām sarunām, skaidrojumiem un konsultatīviem jautājumiem. "
381
+ "Izvēlies code_generation tikai tad, ja lietotājs tieši prasa rakstīt, labot, ģenerēt vai debugot kodu. "
382
+ "Ja lietotājs tikai jautā par programmēšanas tēmu vai prasa skaidrojumu bez prasības dot kodu, izvēlies text_chat. "
383
+ "Ja lietotājs tikai grib sarunāties, apspriest tēmu vai saņemt skaidrojumu par attēliem, mūziku, video, voice vai workflow/orchestration, izvēlies text_chat. "
384
+ "Specialist atzarus izvēlies tikai tad, ja lietotājs tieši prasa ģenerēt artefaktu, palaist izpildi vai veikt darbību. "
385
+ f"Pieprasījumam ir pievienots vizuālais konteksts: {'jā' if has_vision_input else 'nē'}. "
386
+ "Capabilities:\n"
387
+ f"{catalog}"
388
+ ),
389
+ },
390
+ {"role": "user", "content": message},
391
+ ]
392
+
393
+ try:
394
+ out = call_generation_pipeline(
395
+ pipe,
396
+ messages,
397
+ max_new_tokens=160,
398
+ temperature=0.1,
399
+ )
400
+ content = out[0]["generated_text"][-1]["content"]
401
+ except Exception as exc:
402
+ # Router fallback must remain silent for users, but this log helps diagnose
403
+ # malformed pipeline responses or model/runtime issues in development.
404
+ logger.warning("LLM router fallback activated: %s", exc)
405
+ return None
406
+
407
+ payload = _extract_json_payload(content)
408
+ if payload is None:
409
+ return None
410
+
411
+ capability = str(payload.get("capability", "")).strip()
412
+ branch = capability_map.get(capability)
413
+ if branch is None:
414
+ return None
415
+
416
+ reasoning = str(payload.get("reasoning", "")).strip() or "LLM routing izvēlējās šo capability."
417
+ try:
418
+ confidence = float(payload.get("confidence", 0.0))
419
+ except (TypeError, ValueError):
420
+ return None
421
+
422
+ if not 0.0 <= confidence <= 1.0:
423
+ return None
424
+
425
+ return RouteDecision(
426
+ capability=branch.capability,
427
+ branch=branch.branch,
428
+ profile=branch.profile,
429
+ target_endpoint=branch.endpoint,
430
+ target_studio=branch.studio,
431
+ reasoning=reasoning,
432
+ confidence=confidence,
433
+ )
434
+
435
+
436
+ 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
444
+ return bool(
445
+ _CODE_ACTION_PATTERN.search(normalized)
446
+ and (_CODE_SUBJECT_PATTERN.search(normalized) or _CODE_BUILDABLE_PATTERN.search(normalized))
447
+ )
448
+
449
+
450
+ def _prefers_text_chat(message: str) -> bool:
451
+ normalized = message.strip().lower()
452
+ if not normalized:
453
+ return False
454
+ return bool(
455
+ (
456
+ _EXPLANATION_INTENT_PATTERN.search(normalized)
457
+ or _CONVERSATIONAL_INTENT_PATTERN.search(normalized)
458
+ )
459
+ and not _CREATION_OR_EXECUTION_INTENT_PATTERN.search(normalized)
460
+ )
461
+
462
+
463
+ def _looks_like_autonomous_request(message: str, *, prefers_text_chat: bool) -> bool:
464
+ normalized = message.strip().lower()
465
+ if _looks_like_code_request(normalized):
466
+ return False
467
+ if prefers_text_chat:
468
+ return False
469
+ return bool(_AUTONOMOUS_INTENT_PATTERN.search(normalized))
470
+
471
+
472
+ def _heuristic_route(message: str, *, has_vision_input: bool = False) -> RouteDecision:
473
+ normalized = message.strip().lower()
474
+ prefers_text_chat = _prefers_text_chat(normalized)
475
+ if has_vision_input:
476
+ return _fallback_route(
477
+ "vision_analysis",
478
+ "Pieprasījumam ir pievienots attēla vai kameras konteksts, tāpēc izmantojam vision-aware sarunas atzaru.",
479
+ 0.9,
480
+ )
481
+ if prefers_text_chat:
482
+ return _fallback_route(
483
+ "text_chat",
484
+ "Pieprasījums izskatās pēc sarunas vai skaidrojuma, tāpēc paliekam galvenajā čata atzarā.",
485
+ 0.9,
486
+ )
487
+ if _looks_like_code_request(normalized):
488
+ return _fallback_route(
489
+ "code_generation",
490
+ "Pieprasījums skaidri prasa rakstīt, labot vai debugot kodu.",
491
+ 0.94,
492
+ )
493
+ if _looks_like_autonomous_request(normalized, prefers_text_chat=prefers_text_chat):
494
+ return _fallback_route(
495
+ "autonomous_tasks",
496
+ "Pieprasījums izskatās pēc vairāku soļu plāna vai izpildes plūsmas.",
497
+ 0.88,
498
+ )
499
+ routing_rules = [
500
+ RouteRule(
501
+ keywords=(
502
+ "attēl",
503
+ "bild",
504
+ "image",
505
+ "logo",
506
+ "poster",
507
+ "vizuāl",
508
+ "cover",
509
+ "dizain",
510
+ "design",
511
+ "hero",
512
+ "brand",
513
+ "branding",
514
+ ),
515
+ capability="image_generation",
516
+ reasoning="Pieprasījums izskatās pēc vizuāla ģenerēšanas vai dizaina uzdevuma.",
517
+ confidence=0.91,
518
+ ),
519
+ RouteRule(
520
+ keywords=("dzies", "mūzik", "music", "melod", "beat", "soundtrack", "audio"),
521
+ capability="music_generation",
522
+ reasoning="Pieprasījums izskatās pēc mūzikas vai audio kompozīcijas uzdevuma.",
523
+ confidence=0.9,
524
+ ),
525
+ RouteRule(
526
+ keywords=("video", "clip", "kamera", "shot", "render", "cinematic", "reel"),
527
+ capability="video_generation",
528
+ reasoning="Pieprasījums izskatās pēc video ģenerēšanas vai shot plānošanas uzdevuma.",
529
+ confidence=0.9,
530
+ ),
531
+ RouteRule(
532
+ keywords=("balss", "voice", "runā", "pasaki", "nolas", "transcrib", "tts", "stt"),
533
+ capability="voice_conversation",
534
+ reasoning="Pieprasījums izskatās pēc balss ievades vai izvades uzdevuma.",
535
+ confidence=0.86,
536
+ ),
537
+ ]
538
+
539
+ for rule in routing_rules:
540
+ if any(keyword in normalized for keyword in rule.keywords):
541
+ return _fallback_route(rule.capability, rule.reasoning, rule.confidence)
542
+
543
+ return _fallback_route(
544
+ "text_chat",
545
+ "Pēc noklusējuma izmanto galveno sarunu atzaru vispārīgai reasoning atbildei.",
546
+ 0.72,
547
+ )
548
+
549
+
550
+ def detect_route(
551
+ message: str,
552
+ *,
553
+ session_id: str | None = None,
554
+ persona_id: str | None = None,
555
+ has_vision_input: bool = False,
556
+ ) -> RouteDecision:
557
+ llm_decision = _llm_route(message, has_vision_input=has_vision_input)
558
+ decision = llm_decision or _heuristic_route(message, has_vision_input=has_vision_input)
559
+ decision.session_context = _build_session_context(session_id, persona_id)
560
+ return decision
core-python/maris_core/personas.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Persona catalog and persona-aware runtime helpers for Maris AI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from fastapi import APIRouter
6
+ from pydantic import BaseModel, Field
7
+
8
+ DEFAULT_PERSONA_ID = "assistant"
9
+ router = APIRouter()
10
+
11
+
12
+ class PersonaProfile(BaseModel):
13
+ id: str
14
+ title: str
15
+ summary: str
16
+ communication_style: str
17
+ best_for: list[str] = Field(default_factory=list)
18
+ prompt_overlay: str
19
+
20
+
21
+ class PersonaCatalogResponse(BaseModel):
22
+ default_persona_id: str
23
+ personas: list[PersonaProfile]
24
+
25
+
26
+ _PERSONAS: tuple[PersonaProfile, ...] = (
27
+ PersonaProfile(
28
+ id="assistant",
29
+ title="Core Assistant",
30
+ summary="Universāls Maris režīms skaidrām, profesionālām un līdzsvarotām atbildēm.",
31
+ communication_style="Grounded, direct, adaptive",
32
+ best_for=["daily work", "general strategy", "problem solving"],
33
+ prompt_overlay=(
34
+ "Uzturi balansu starp stratēģiju un izpildi. Atbildi skaidri, bez lieka trokšņa, "
35
+ "bet ar profesionālu dziļumu."
36
+ ),
37
+ ),
38
+ PersonaProfile(
39
+ id="strategist",
40
+ title="Systems Strategist",
41
+ summary="Skatās uz uzdevumiem kā produktu, biznesa un sistēmu arhitektūras kombināciju.",
42
+ communication_style="Executive, structured, leverage-oriented",
43
+ best_for=["roadmaps", "prioritization", "architecture decisions"],
44
+ prompt_overlay=(
45
+ "Domā kā world-class product strategist un systems architect. Prioritizē leverage, riskus, "
46
+ "trade-off un nākamo labāko soli."
47
+ ),
48
+ ),
49
+ PersonaProfile(
50
+ id="coder",
51
+ title="Principal Engineer",
52
+ summary="Spēcīgs tehniskais režīms ar fokusētu, precīzu un senior līmeņa inženierijas stilu.",
53
+ communication_style="Technical, exact, implementation-aware",
54
+ best_for=["debugging", "refactoring", "API design"],
55
+ prompt_overlay=(
56
+ "Domā kā principal engineer. Izcel correctness, maintainability, testability un edge cases."
57
+ ),
58
+ ),
59
+ PersonaProfile(
60
+ id="analyst",
61
+ title="Research Analyst",
62
+ summary="Sintezē informāciju, salīdzina variantus un izceļ pierādījumos balstītus secinājumus.",
63
+ communication_style="Analytical, evidence-first, comparative",
64
+ best_for=["research", "comparisons", "decision support"],
65
+ prompt_overlay=(
66
+ "Domā kā research analyst. Salīdzini alternatīvas, skaidri atdali faktus no pieņēmumiem "
67
+ "un formulē secinājumus."
68
+ ),
69
+ ),
70
+ PersonaProfile(
71
+ id="teacher",
72
+ title="Expert Teacher",
73
+ summary="Pārvērš sarežģītas idejas skaidros, pakāpeniskos un viegli uztveramos skaidrojumos.",
74
+ communication_style="Patient, layered, intuitive",
75
+ best_for=["learning", "explanations", "onboarding"],
76
+ prompt_overlay=(
77
+ "Domā kā world-class skolotājs. Sarežģīto pārvērš vienkāršā secībā, nezaudējot precizitāti."
78
+ ),
79
+ ),
80
+ PersonaProfile(
81
+ id="coach",
82
+ title="Performance Coach",
83
+ summary="Dod enerģisku, empātisku un uz izpildi vērstu atbalstu ar fokusu uz progresu.",
84
+ communication_style="Motivating, practical, accountable",
85
+ best_for=["habits", "momentum", "execution support"],
86
+ prompt_overlay=(
87
+ "Domā kā high-performance coach. Esi empātisks, bet turi fokusu uz konkrētu progresu "
88
+ "un nākamo izdarāmo soli."
89
+ ),
90
+ ),
91
+ PersonaProfile(
92
+ id="designer",
93
+ title="Creative Director",
94
+ summary="Veido estētiski spēcīgus, konceptuāli skaidrus un producēšanai gatavus virzienus.",
95
+ communication_style="Creative, taste-driven, production-aware",
96
+ best_for=["brand direction", "creative briefs", "visual ideas"],
97
+ prompt_overlay=(
98
+ "Domā kā creative director. Izcel kompozīciju, sajūtu, stāstu un produkcijas kvalitāti."
99
+ ),
100
+ ),
101
+ )
102
+ _PERSONA_MAP = {persona.id: persona for persona in _PERSONAS}
103
+
104
+
105
+ def list_personas() -> tuple[PersonaProfile, ...]:
106
+ """Return the stable built-in persona catalog."""
107
+ return _PERSONAS
108
+
109
+
110
+ def get_persona_catalog() -> PersonaCatalogResponse:
111
+ """Return the public persona catalog."""
112
+ return PersonaCatalogResponse(default_persona_id=DEFAULT_PERSONA_ID, personas=list(_PERSONAS))
113
+
114
+
115
+ def resolve_persona(persona_id: str | None) -> PersonaProfile:
116
+ """Resolve a requested persona or fall back to the default."""
117
+ normalized = (persona_id or "").strip().lower()
118
+ return _PERSONA_MAP.get(normalized, _PERSONA_MAP[DEFAULT_PERSONA_ID])
119
+
120
+
121
+ @router.get("", response_model=PersonaCatalogResponse)
122
+ async def get_personas() -> PersonaCatalogResponse:
123
+ """Return the available Maris personas."""
124
+ return get_persona_catalog()
125
+
126
+
127
+ @router.get("/default", response_model=PersonaProfile)
128
+ async def get_default_persona() -> PersonaProfile:
129
+ """Return the default Maris persona."""
130
+ return resolve_persona(DEFAULT_PERSONA_ID)
core-python/maris_core/runtime.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Runtime palīgfunkcijas izvietošanai un lokālai palaišanai."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+
7
+ from maris_core.utils.env import get_hf_token
8
+
9
+
10
+ def configure_huggingface_environment() -> None:
11
+ os.environ.setdefault("HF_HUB_DISABLE_XET", "1")
12
+
13
+ token = get_hf_token()
14
+ if token is None:
15
+ return
16
+
17
+ for variable_name in (
18
+ "HF_TOKEN",
19
+ "HUGGING_FACE_HUB_TOKEN",
20
+ "HUGGINGFACEHUB_API_TOKEN",
21
+ ):
22
+ os.environ.setdefault(variable_name, token)
23
+
24
+
25
+ def resolve_port(default: int = 8000) -> int:
26
+ value = os.getenv("PORT")
27
+ if not value:
28
+ return default
29
+
30
+ try:
31
+ return int(value)
32
+ except ValueError as exc:
33
+ raise ValueError(f"Invalid PORT value: {value!r}") from exc
34
+
35
+
36
+ def resolve_host(default: str = "0.0.0.0") -> str:
37
+ value = os.getenv("HOST")
38
+ if value:
39
+ return value.strip()
40
+
41
+ return default
42
+
43
+
44
+ def is_reload_enabled(default: bool = False) -> bool:
45
+ value = os.getenv("MARIS_RELOAD")
46
+ if value is None:
47
+ return default
48
+
49
+ return value.strip().lower() in {"1", "true", "yes", "on"}
core-python/maris_core/space_agent.py ADDED
@@ -0,0 +1,1867 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Maris AI projektu aģenta helperi."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import difflib
6
+ import io
7
+ import json
8
+ import logging
9
+ import re
10
+ from collections.abc import Callable
11
+ from pathlib import Path
12
+ from typing import Any, Literal
13
+
14
+ import httpx
15
+ from huggingface_hub.utils import HfHubHTTPError
16
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
17
+
18
+ from maris_core.browser.automation import get_browser_automation_capabilities
19
+ from maris_core.orchestrator.routing import build_system_prompt, resolve_text_model
20
+ from maris_core.personas import get_persona_catalog
21
+ from maris_core.training.config import list_training_base_models
22
+ from maris_core.utils.env import (
23
+ get_env_any,
24
+ get_env_any_or_default,
25
+ )
26
+ from maris_core.utils.hf_inference import create_hf_inference_client
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
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-memory"
37
+ SPACE_AGENT_MODEL_REPO_DEFAULT = "MarisUK/maris-ai-master"
38
+ # 12,000 chars roughly supports a long project brief or debugging dump without overwhelming the chat context.
39
+ SPACE_AGENT_MESSAGE_MAX_CHARS = 12000
40
+ SPACE_AGENT_HISTORY_WINDOW = 12
41
+ # Allow enough room for a realistic multi-step audit workflow that may combine
42
+ # HF repo discovery, file inspection, one or more writes, and a final runtime
43
+ # lookup without forcing the agent to truncate its plan. Tool selection still
44
+ # runs with max_tokens capped at 1024, so the higher tool ceiling does not also
45
+ # increase the planning token budget.
46
+ SPACE_AGENT_MAX_TOOL_CALLS = 10
47
+ SPACE_AGENT_MAX_TOOL_ITERATIONS = 4
48
+ SPACE_AGENT_MAX_FILE_BYTES = 20000
49
+ SPACE_AGENT_MAX_DIRECTORY_ENTRIES = 200
50
+ SPACE_AGENT_HF_REPO_TYPE_COUNT = 3
51
+ SPACE_AGENT_PROMPT_PROFILE_GENERAL = "general"
52
+ SPACE_AGENT_DEFAULT_TASK_MODE = "chat"
53
+ SPACE_AGENT_TASK_MODES = ("chat", "code", "design", "improve")
54
+ SPACE_AGENT_MODEL_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*/[A-Za-z0-9][A-Za-z0-9._-]*$")
55
+ SPACE_AGENT_TOOL_NAMES = (
56
+ "project_runtime",
57
+ "model_dataset_playbook",
58
+ "training_presets",
59
+ "training_status",
60
+ "sync_commands",
61
+ "workspace_command_catalog",
62
+ "browser_capabilities",
63
+ "persona_catalog",
64
+ "list_huggingface_repos",
65
+ "list_huggingface_repo_files",
66
+ "read_huggingface_repo_file",
67
+ "write_huggingface_repo_file",
68
+ "list_workspace",
69
+ "read_workspace_file",
70
+ "write_workspace_file",
71
+ "run_workspace_command",
72
+ )
73
+ SPACE_AGENT_CAPABILITIES = (
74
+ {
75
+ "title": "Project operator",
76
+ "description": "Palīdz ar Maris projekta publicēšanu, repozitorijiem, deploy un roadmap lēmumiem.",
77
+ },
78
+ {
79
+ "title": "Model & dataset fixer",
80
+ "description": "Strādā ar skaidru audit → validate → evaluate → fix → train → sync ciklu, lai uzlabotu modeli un dataset.",
81
+ },
82
+ {
83
+ "title": "Tool-calling mode",
84
+ "description": "Var piesaukt iebūvētos rīkus runtime statusam, presetiem un sync komandām pirms gala atbildes.",
85
+ },
86
+ {
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ā.",
101
+ },
102
+ {
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.",
113
+ },
114
+ )
115
+ SPACE_AGENT_WORKSPACE_COMMAND_PRESETS = (
116
+ {
117
+ "category": "python",
118
+ "title": "Core Python checks",
119
+ "items": (
120
+ {
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
+ {
128
+ "id": "python-space-lint",
129
+ "label": "Space agent lint",
130
+ "description": "Palaiž Ruff tikai Space agent failiem.",
131
+ "command": [
132
+ "python",
133
+ "-m",
134
+ "ruff",
135
+ "check",
136
+ "maris_core/space_agent.py",
137
+ "tests/test_space_agent.py",
138
+ "tests/test_huggingface_space_app.py",
139
+ "../huggingface_space/app.py",
140
+ "../huggingface_space/agent_ui.py",
141
+ ],
142
+ "cwd": "core-python",
143
+ },
144
+ ),
145
+ },
146
+ {
147
+ "category": "frontend",
148
+ "title": "Frontend checks",
149
+ "items": (
150
+ {
151
+ "id": "frontend-lint",
152
+ "label": "Frontend lint",
153
+ "description": "Palaiž esošo frontend lint skriptu.",
154
+ "command": ["npm", "run", "lint"],
155
+ "cwd": "frontend",
156
+ },
157
+ {
158
+ "id": "frontend-test",
159
+ "label": "Frontend tests",
160
+ "description": "Palaiž esošos frontend testus vienā piegājienā.",
161
+ "command": ["npm", "test", "--", "--runInBand"],
162
+ "cwd": "frontend",
163
+ },
164
+ {
165
+ "id": "frontend-build",
166
+ "label": "Frontend build",
167
+ "description": "Pārbauda, vai Next.js būve ir veiksmīga.",
168
+ "command": ["npm", "run", "build"],
169
+ "cwd": "frontend",
170
+ },
171
+ ),
172
+ },
173
+ {
174
+ "category": "rust",
175
+ "title": "Rust services",
176
+ "items": (
177
+ {
178
+ "id": "backend-rust-test",
179
+ "label": "Backend Rust tests",
180
+ "description": "Palaiž backend-rust testus.",
181
+ "command": ["cargo", "test"],
182
+ "cwd": "backend-rust",
183
+ },
184
+ {
185
+ "id": "backend-rust-check",
186
+ "label": "Backend Rust check",
187
+ "description": "Veic ātrāku backend-rust kompilācijas pārbaudi.",
188
+ "command": ["cargo", "check"],
189
+ "cwd": "backend-rust",
190
+ },
191
+ {
192
+ "id": "voice-rust-test",
193
+ "label": "Voice Rust tests",
194
+ "description": "Palaiž voice-rust testus.",
195
+ "command": ["cargo", "test"],
196
+ "cwd": "voice-rust",
197
+ },
198
+ ),
199
+ },
200
+ {
201
+ "category": "huggingface",
202
+ "title": "Hugging Face workflows",
203
+ "items": (
204
+ {
205
+ "id": "hf-sync",
206
+ "label": "Full HF sync",
207
+ "description": "Palaiž pilno Hugging Face sync plūsmu.",
208
+ "command": ["bash", "huggingface/sync.sh", "sync"],
209
+ "cwd": ".",
210
+ },
211
+ {
212
+ "id": "hf-upload-space",
213
+ "label": "Upload Space",
214
+ "description": "Publicē Space izmaiņas uz konfigurēto Hugging Face Space.",
215
+ "command": ["bash", "huggingface/sync.sh", "upload-space"],
216
+ "cwd": ".",
217
+ },
218
+ {
219
+ "id": "hf-train",
220
+ "label": "Train launcher",
221
+ "description": "Palaiž esošo Hugging Face train skriptu.",
222
+ "command": ["bash", "huggingface/train.sh"],
223
+ "cwd": ".",
224
+ },
225
+ ),
226
+ },
227
+ )
228
+ # These patterns are intentionally lowercase because model matching normalizes input with .lower().
229
+ SPACE_AGENT_TEXT_MODEL_PATTERNS = (
230
+ "marisuk/maris-ai-text",
231
+ "maris-ai-text",
232
+ )
233
+ SPACE_AGENT_MODEL_DATASET_PLAYBOOK = {
234
+ "sources": (
235
+ "Hugging Face smolagents docs",
236
+ "Hugging Face agent patterns",
237
+ "Maris Hugging Face training and sync workflow",
238
+ ),
239
+ "latest_agent_principles": (
240
+ "Izmanto vieglu, caurredzamu tool-first aģenta ciklu ar maziem, pārbaudāmiem soļiem.",
241
+ "Strādā reproducējami: pirms labojumiem savāc kontekstu, pēc labojumiem validē rezultātu.",
242
+ "Dod priekšroku reālām failu vai repo izmaiņām, nevis tikai teorētiskai analīzei, ja lietotājs prasa salabot.",
243
+ "Uzturi drošas robežas: raksti tikai atļautajā workspace vai savā Hugging Face owner telpā.",
244
+ "Uzturi skaidru dataset un model artefaktu kvalitāti: cards, konfigurāciju, eval rezultātus un sync soļus.",
245
+ ),
246
+ "recommended_loop": (
247
+ "1. Savāc runtime un repo kontekstu.",
248
+ "2. Validē dataset struktūru un kritiskos failus.",
249
+ "3. Pārbaudi model/dataset cards, training-config un eval ceļu.",
250
+ "4. Veic minimālos nepieciešamos labojumus workspace vai Hugging Face repo.",
251
+ "5. Ja vajag, palaid train/eval/sync komandas atbilstošā secībā.",
252
+ "6. Gala atbildē uzskaiti izmaiņas, riskus un nākamos praktiskos soļus.",
253
+ ),
254
+ "repo_commands": {
255
+ "validate_dataset": "cd ./core-python && python ./scripts/validate_datasets.py",
256
+ "list_training_presets": "cd ./core-python && python ./scripts/train_model.py --list-base-models",
257
+ "evaluate_model": "cd ./core-python && python ./scripts/eval_model.py --model-path <owner/name-or-local-path> --dataset-repo <dataset-repo> --eval-dataset-repo <eval-repo>",
258
+ "train_model": "bash ./huggingface/train.sh",
259
+ "sync_dataset": "bash ./huggingface/sync.sh upload-dataset",
260
+ "sync_model": "bash ./huggingface/sync.sh upload-model",
261
+ "sync_space": "MARIS_AGENT_SPACE_REPO=<owner/space> bash ./huggingface/sync.sh upload-space",
262
+ },
263
+ "required_setup": (
264
+ "HF_TOKEN vai MARIS_REPO_TOKEN ar write pieeju model, dataset un Space repozitorijiem.",
265
+ "MARIS_MEMORY_REPO, MARIS_MODEL_REPO un MARIS_AGENT_SPACE_REPO ar pareiziem owner/name ID.",
266
+ "Ja izmanto stabilu benchmark, iestati HF_EVAL_DATASET_REPO un piepildi eval-data/ koku.",
267
+ "Space runtime ieteicams izmantot HF_INFERENCE_API_KEY aģenta chat/inference darbībai.",
268
+ "Pirms train vai sync uzturi aktuālus huggingface/dataset-card.md, huggingface/model-card.md un huggingface/training-config.json.",
269
+ ),
270
+ }
271
+ SPACE_AGENT_TASK_MODE_INSTRUCTIONS = {
272
+ "chat": (
273
+ "Chat režīmā strādā kā sarunas asistents: skaidri saproti mērķi, izskaidro nākamos soļus "
274
+ "un rādi izpildes progresu bez liekas sarežģīšanas."
275
+ ),
276
+ "code": (
277
+ "Code režīmā fokusējies uz reāliem repozitorija labojumiem, failu izmaiņām, refactor un drošu "
278
+ "koda darba plūsmu ar skaidriem diff un pārskatāmiem rezultātiem."
279
+ ),
280
+ "design": (
281
+ "Design režīmā prioritizē UI/UX, vizuālo hierarhiju, komponentu struktūru un frontend darba plūsmu, "
282
+ "lai lietotājs redzētu dizaina uzlabojumus kā saprotamas, pārskatāmas izmaiņas."
283
+ ),
284
+ "improve": (
285
+ "Improve režīmā strādā kā audits + uzlabošanas operators: atrodi problēmas, nosaki prioritātes, "
286
+ "veic minimālos vajadzīgos labojumus un atgriez riskus/nākamos soļus."
287
+ ),
288
+ }
289
+
290
+
291
+ class SpaceAgentMessage(BaseModel):
292
+ """Single chat message for the Space agent conversation."""
293
+
294
+ model_config = ConfigDict(str_strip_whitespace=True)
295
+
296
+ role: Literal["user", "assistant"]
297
+ content: str = Field(min_length=1, max_length=SPACE_AGENT_MESSAGE_MAX_CHARS)
298
+
299
+
300
+ class SpaceAgentToolCall(BaseModel):
301
+ """Structured tool call returned by the agent orchestration layer."""
302
+
303
+ name: Literal[*SPACE_AGENT_TOOL_NAMES]
304
+ arguments: dict[str, Any] = Field(default_factory=dict)
305
+
306
+
307
+ class SpaceAgentChatRequest(BaseModel):
308
+ """Request payload for the Maris AI Space agent."""
309
+
310
+ model_config = ConfigDict(str_strip_whitespace=True)
311
+
312
+ message: str = Field(min_length=1, max_length=SPACE_AGENT_MESSAGE_MAX_CHARS)
313
+ history: list[SpaceAgentMessage] = Field(default_factory=list, max_length=16)
314
+ model: str | None = Field(default=None, max_length=160)
315
+ max_tokens: int = Field(default=900, ge=64, le=4096)
316
+ temperature: float = Field(default=0.2, ge=0.0, le=1.0)
317
+ tool_calling: bool = True
318
+ task_mode: Literal[*SPACE_AGENT_TASK_MODES] = SPACE_AGENT_DEFAULT_TASK_MODE
319
+
320
+ @field_validator("model")
321
+ @classmethod
322
+ def validate_model(cls, value: str | None) -> str | None:
323
+ normalized = (value or "").strip()
324
+ if not normalized:
325
+ return None
326
+ if not SPACE_AGENT_MODEL_ID_RE.fullmatch(normalized):
327
+ raise ValueError("Agent modelim jābūt owner/name formātā.")
328
+ return normalized
329
+
330
+
331
+ class SpaceAgentChatResponse(BaseModel):
332
+ """Response payload returned by the Maris AI Space agent."""
333
+
334
+ response: str
335
+ model: str
336
+ request_id: str | None = None
337
+ task_id: str | None = None
338
+ used_fallback: bool = False
339
+ tool_calls: list[SpaceAgentToolCall] = Field(default_factory=list)
340
+ events: list[dict[str, Any]] = Field(default_factory=list)
341
+ task_mode: Literal[*SPACE_AGENT_TASK_MODES] = SPACE_AGENT_DEFAULT_TASK_MODE
342
+ change_previews: list[dict[str, Any]] = Field(default_factory=list)
343
+
344
+
345
+ class SpaceAgentRuntimeInfo(BaseModel):
346
+ """Public runtime metadata surfaced to the UI."""
347
+
348
+ model: str
349
+ default_model: str
350
+ dataset_repo: str
351
+ model_repo: str
352
+ space_repo: str
353
+ has_publish_token: bool
354
+ huggingface_owner: str
355
+ available_models: tuple[str, ...]
356
+ capabilities: tuple[dict[str, str], ...] = SPACE_AGENT_CAPABILITIES
357
+ history_window: int = SPACE_AGENT_HISTORY_WINDOW
358
+ tool_calling: bool = True
359
+ tool_names: tuple[str, ...] = SPACE_AGENT_TOOL_NAMES
360
+ command_presets: tuple[dict[str, Any], ...] = SPACE_AGENT_WORKSPACE_COMMAND_PRESETS
361
+ default_task_mode: Literal[*SPACE_AGENT_TASK_MODES] = SPACE_AGENT_DEFAULT_TASK_MODE
362
+ task_modes: tuple[str, ...] = SPACE_AGENT_TASK_MODES
363
+
364
+
365
+ def _dedupe_models(*models: str | None) -> tuple[str, ...]:
366
+ seen: set[str] = set()
367
+ result: list[str] = []
368
+ for model in models:
369
+ normalized = (model or "").strip()
370
+ if not normalized or normalized in seen:
371
+ continue
372
+ seen.add(normalized)
373
+ result.append(normalized)
374
+ return tuple(result)
375
+
376
+
377
+ def _validate_space_model_id(value: str, source: str) -> str:
378
+ normalized = value.strip()
379
+ if not normalized:
380
+ raise RuntimeError(f"Trūkst modeļa konfigurācija: {source}")
381
+ if not SPACE_AGENT_MODEL_ID_RE.fullmatch(normalized):
382
+ raise RuntimeError(f"{source} modelim jābūt owner/name formātā.")
383
+ return normalized
384
+
385
+
386
+ def _get_space_model(*names: str, default: str | None = None) -> str:
387
+ source = ", ".join(names)
388
+ value = get_env_any(*names)
389
+ if value is None:
390
+ if default is None:
391
+ raise RuntimeError(f"Trūkst modeļa konfigurācija: {source}")
392
+ value = default
393
+ return _validate_space_model_id(value, source)
394
+
395
+
396
+ def _get_huggingface_owner() -> str:
397
+ configured = (get_env_any("MARIS_HF_OWNER", "HF_OWNER") or "").strip()
398
+ if configured:
399
+ return configured
400
+ return get_env_any_or_default(
401
+ "MARIS_AGENT_SPACE_REPO",
402
+ "MARIS_SPACE_REPO",
403
+ "HF_SPACE_REPO",
404
+ default=SPACE_AGENT_SPACE_REPO_DEFAULT,
405
+ ).split("/", 1)[0]
406
+
407
+
408
+ def _is_text_first_space_agent_model(model_name: str | None) -> bool:
409
+ normalized = (model_name or "").strip().lower()
410
+ if not normalized:
411
+ return False
412
+ text_model = resolve_text_model().strip().lower()
413
+ return normalized == text_model or any(
414
+ pattern in normalized for pattern in SPACE_AGENT_TEXT_MODEL_PATTERNS
415
+ )
416
+
417
+
418
+ def _space_agent_prompt_profile(model_name: str | None) -> str:
419
+ return (
420
+ SPACE_AGENT_PROMPT_PROFILE_GENERAL
421
+ if _is_text_first_space_agent_model(model_name)
422
+ else "coder"
423
+ )
424
+
425
+
426
+ def _should_enable_space_agent_tooling(
427
+ request: SpaceAgentChatRequest, model_name: str | None
428
+ ) -> bool:
429
+ return bool(request.tool_calling and not _is_text_first_space_agent_model(model_name))
430
+
431
+
432
+ def list_space_agent_models() -> tuple[str, ...]:
433
+ """Return the Space agent model choices exposed in the UI/runtime."""
434
+ configured = get_env_any("MARIS_AGENT_MODELS", "HF_SPACE_ASSISTANT_MODELS", default="") or ""
435
+ configured_models = [
436
+ _validate_space_model_id(item.strip(), "MARIS_AGENT_MODELS")
437
+ for item in configured.split(",")
438
+ if item.strip()
439
+ ]
440
+ default_model = _get_space_model(
441
+ "MARIS_AGENT_MODEL",
442
+ "HF_SPACE_ASSISTANT_MODEL",
443
+ "MARIS_MODEL_REPO",
444
+ "HF_MODEL_REPO",
445
+ default=SPACE_AGENT_MODEL_DEFAULT,
446
+ )
447
+ return _dedupe_models(default_model, *configured_models)
448
+
449
+
450
+ def resolve_space_agent_models(requested_model: str | None = None) -> tuple[str, ...]:
451
+ """Return the ordered list of agent models explicitly selected for this request."""
452
+ selected = (requested_model or "").strip()
453
+ if selected:
454
+ return (selected,)
455
+ runtime_models = list_space_agent_models()
456
+ return (runtime_models[0],) if runtime_models else ()
457
+
458
+
459
+ def get_space_agent_runtime_info() -> SpaceAgentRuntimeInfo:
460
+ """Return runtime configuration derived from environment variables."""
461
+ default_model = _get_space_model(
462
+ "MARIS_AGENT_MODEL",
463
+ "HF_SPACE_ASSISTANT_MODEL",
464
+ "MARIS_MODEL_REPO",
465
+ "HF_MODEL_REPO",
466
+ default=SPACE_AGENT_MODEL_DEFAULT,
467
+ )
468
+ return SpaceAgentRuntimeInfo(
469
+ model=default_model,
470
+ default_model=default_model,
471
+ dataset_repo=get_env_any_or_default(
472
+ "MARIS_MEMORY_REPO",
473
+ "MARIS_DATASET_REPO",
474
+ "HF_DATASET_REPO",
475
+ default=SPACE_AGENT_DATASET_REPO_DEFAULT,
476
+ ),
477
+ model_repo=get_env_any_or_default(
478
+ "MARIS_MODEL_REPO",
479
+ "HF_MODEL_REPO",
480
+ default=SPACE_AGENT_MODEL_REPO_DEFAULT,
481
+ ),
482
+ space_repo=get_env_any_or_default(
483
+ "MARIS_AGENT_SPACE_REPO",
484
+ "MARIS_SPACE_REPO",
485
+ "HF_SPACE_REPO",
486
+ default=SPACE_AGENT_SPACE_REPO_DEFAULT,
487
+ ),
488
+ has_publish_token=bool(get_env_any("MARIS_REPO_TOKEN", "MARIS_TOKEN", "HF_TOKEN")),
489
+ huggingface_owner=_get_huggingface_owner(),
490
+ available_models=list_space_agent_models(),
491
+ command_presets=SPACE_AGENT_WORKSPACE_COMMAND_PRESETS,
492
+ )
493
+
494
+
495
+ def get_space_agent_tool_specs() -> tuple[dict[str, Any], ...]:
496
+ """Return the built-in tools that the agent may call."""
497
+ return (
498
+ {
499
+ "name": "project_runtime",
500
+ "description": "Atgriež aktīvo Maris runtime konfigurāciju, repo ID un aģenta iespējas.",
501
+ "arguments": {},
502
+ },
503
+ {
504
+ "name": "model_dataset_playbook",
505
+ "description": "Atgriež jaunāko Maris model/dataset uzlabošanas playbook ar HF agent principiem, komandām un setup prasībām.",
506
+ "arguments": {},
507
+ },
508
+ {
509
+ "name": "training_presets",
510
+ "description": "Atgriež pieejamos Maris training presetus ar modeļu nosaukumiem un aprakstiem.",
511
+ "arguments": {},
512
+ },
513
+ {
514
+ "name": "training_status",
515
+ "description": "Atgriež pašreizējo Space treniņa statusu, progress datus un runtime piezīmes.",
516
+ "arguments": {},
517
+ },
518
+ {
519
+ "name": "sync_commands",
520
+ "description": "Atgriež precīzas sync/deploy komandas projekta, modeļa un atmiņas repo darbam.",
521
+ "arguments": {},
522
+ },
523
+ {
524
+ "name": "workspace_command_catalog",
525
+ "description": "Atgriež pilnāku droši atļauto command preset katalogu validācijai, testiem, build un HF darba plūsmām.",
526
+ "arguments": {},
527
+ },
528
+ {
529
+ "name": "browser_capabilities",
530
+ "description": "Atgriež browser automation endpointu, atbalstīto darbību un drošo URL shēmu metadatus.",
531
+ "arguments": {},
532
+ },
533
+ {
534
+ "name": "persona_catalog",
535
+ "description": "Atgriež pieejamo Maris persona katalogu ar nosaukumiem, kopsavilkumiem un labākajiem lietojumiem.",
536
+ "arguments": {},
537
+ },
538
+ {
539
+ "name": "list_huggingface_repos",
540
+ "description": "Atgriež tava Hugging Face owner modeļus, datasetus vai Spaces auditam un uzlabojumiem.",
541
+ "arguments": {
542
+ "repo_type": "Viens no: all, model, dataset, space.",
543
+ "search": "Neobligāts meklēšanas filtrs.",
544
+ "limit": "Neobligāts limits no 1 līdz 30.",
545
+ },
546
+ },
547
+ {
548
+ "name": "list_huggingface_repo_files",
549
+ "description": "Atgriež izvēlētā HF repozitorija failu sarakstu.",
550
+ "arguments": {
551
+ "repo_id": "Repozitorija ID owner/name formātā.",
552
+ "repo_type": "Viens no: model, dataset, space.",
553
+ },
554
+ },
555
+ {
556
+ "name": "read_huggingface_repo_file",
557
+ "description": "Nolasa UTF-8 teksta failu no jebkura pieejama HF repozitorija analīzei.",
558
+ "arguments": {
559
+ "repo_id": "Repozitorija ID owner/name formātā.",
560
+ "repo_type": "Viens no: model, dataset, space.",
561
+ "path": "Faila ceļš repozitorijā.",
562
+ },
563
+ },
564
+ {
565
+ "name": "write_huggingface_repo_file",
566
+ "description": "Saglabā UTF-8 teksta failu tikai tava konfigurētā HF owner repozitorijā ar commit ziņu.",
567
+ "arguments": {
568
+ "repo_id": "Repozitorija ID owner/name formātā.",
569
+ "repo_type": "Viens no: model, dataset, space.",
570
+ "path": "Faila ceļš repozitorijā.",
571
+ "content": "Pilns saglabājamais teksta saturs UTF-8 formātā.",
572
+ "commit_message": "Neobligāta commit ziņa.",
573
+ },
574
+ },
575
+ {
576
+ "name": "list_workspace",
577
+ "description": "Atgriež Maris darba telpas direktorijas saturu zem atļautās workspace saknes.",
578
+ "arguments": {
579
+ "path": "Relatīvs direktorijas ceļš, piemēram '.', 'core-python' vai 'frontend/app'."
580
+ },
581
+ },
582
+ {
583
+ "name": "read_workspace_file",
584
+ "description": "Nolasa teksta faila saturu no Maris darba telpas.",
585
+ "arguments": {"path": "Relatīvs faila ceļš darba telpā."},
586
+ },
587
+ {
588
+ "name": "write_workspace_file",
589
+ "description": "Pārraksta vai izveido teksta failu izolētā Maris darba telpas draftā; produkcijas workspace izmaiņas tiek dotas uz apstiprinājumu.",
590
+ "arguments": {
591
+ "path": "Relatīvs faila ceļš darba telpā.",
592
+ "content": "Pilns saglabājamais teksta saturs UTF-8 formātā.",
593
+ },
594
+ },
595
+ {
596
+ "name": "run_workspace_command",
597
+ "description": "Palaiž droši ierobežotu lint, testu vai build komandu izolētā Maris draft darba telpā.",
598
+ "arguments": {
599
+ "command": "Komanda kā string vai tokenu masīvs, piemēram, 'python -m pytest tests/test_space_agent.py'.",
600
+ "cwd": "Neobligāts relatīvs darba direktorijas ceļš zem workspace saknes.",
601
+ "timeout_seconds": "Neobligāts timeout sekundēs no 1 līdz 600.",
602
+ },
603
+ },
604
+ )
605
+
606
+
607
+ def build_space_agent_messages(
608
+ request: SpaceAgentChatRequest,
609
+ *,
610
+ include_tooling_rules: bool = True,
611
+ active_model: str | None = None,
612
+ ) -> list[dict[str, str]]:
613
+ """Build the system and chat history messages for Maris chat completion."""
614
+ runtime = get_space_agent_runtime_info()
615
+ model_name = (active_model or request.model or runtime.default_model).strip()
616
+ prompt_profile = _space_agent_prompt_profile(model_name)
617
+ prompt_sections = [
618
+ build_system_prompt(prompt_profile),
619
+ (
620
+ "Tu esi Maris AI Project Operator. "
621
+ "Tava prioritāte ir palīdzēt profesionāli vadīt visu Maris projektu: "
622
+ "agent workspace arhitektūru, repo struktūru, model publication, atmiņas repozitoriju, CI/CD, "
623
+ "sync plūsmas, debug, release piezīmes un nākamos tehniskos soļus."
624
+ ),
625
+ (
626
+ "Atbildi kā senior AI platform engineer un technical product operator: "
627
+ "skaidri, precīzi, strukturēti, ar konkrētiem repo ID, failiem, komandām un riskiem. "
628
+ "Ja jautājums ir neskaidrs, uzdod vienu īsu precizējošu jautājumu."
629
+ ),
630
+ (
631
+ f"Primārais darba modelis ir {model_name}. "
632
+ f"Noklusējuma dataset repo ir {runtime.dataset_repo}, modeļa repo ir {runtime.model_repo}, "
633
+ f"un Space publicēšana notiek uz {runtime.space_repo}. "
634
+ f"Tavs Hugging Face owner konteksts ir {runtime.huggingface_owner}."
635
+ ),
636
+ (
637
+ "Ja vajag precīzu repozitorija kontekstu, vari izmantot workspace rīkus, lai apskatītu direktorijas, "
638
+ "nolasītu failus un saglabātu labojumus pašreizējā Maris darba telpā."
639
+ ),
640
+ (
641
+ "Ja lietotājs prasa pārbaudīt, salabot, uzlabot vai sagatavot modeli, Space vai failus, "
642
+ "tad rīkojies proaktīvi kā profesionāls AI operators: analizē problēmu, savāc kontekstu, "
643
+ "atrodi kļūdas, izdari nepieciešamās izmaiņas pieejamajos failos vai Hugging Face repozitorijos "
644
+ "un gala atbildē skaidri uzskaiti, kas tika pārbaudīts un kas tika uzlabots."
645
+ ),
646
+ (
647
+ "Modeļu un dataset uzlabošanā seko mūsdienīgam Hugging Face aģenta stilam: "
648
+ "izmanto vienkāršu tool-first ciklu, strādā mazos pārbaudāmos soļos, "
649
+ "prioritizē reproducējamību, un, ja pieejams, izmanto model_dataset_playbook rīku, "
650
+ "lai balstītu darbu uz audit → validate → evaluate → fix → train → sync plūsmu."
651
+ ),
652
+ (
653
+ "Negaidi papildu atļauju acīmredzamiem nākamajiem soļiem. Ja uzdevumam vajag failu labošanu vai saglabāšanu, "
654
+ "izmanto rīkus un pabeidz darbu pilnā apjomā pieejamo iespēju robežās."
655
+ ),
656
+ (
657
+ "Vienmēr prioritizē drošību, reproducējamību, clear deploy steps, "
658
+ "un minimal-risk izmaiņas. Ja iesaki komandas, turi tās praktiskas un tiešas."
659
+ ),
660
+ (
661
+ "Šī pieprasījuma aktīvais darba režīms ir "
662
+ f"`{request.task_mode}`. {SPACE_AGENT_TASK_MODE_INSTRUCTIONS[request.task_mode]}"
663
+ ),
664
+ (
665
+ "Ja sagatavo izmaiņas ārējam Hugging Face repozitorijam un rakstīšanas rezultāts tiek atdots "
666
+ "kā staged/requires_approval, tad gala atbildē skaidri pasaki, ka publicēšana gaida lietotāja "
667
+ "apstiprinājumu."
668
+ ),
669
+ ]
670
+ if prompt_profile == SPACE_AGENT_PROMPT_PROFILE_GENERAL:
671
+ prompt_sections.append(
672
+ "Sniedz skaidras un tiešas atbildes bez sarežģītas tool plānošanas vai striktā JSON-only režīma, "
673
+ "ja vien modelis tam nav īpaši piemērots."
674
+ )
675
+ if include_tooling_rules:
676
+ tools_json = json.dumps(get_space_agent_tool_specs(), ensure_ascii=False)
677
+ prompt_sections.append(
678
+ "Ja vajag papildkontekstu, vari izmantot tool-calling režīmu. "
679
+ "Atbildi tikai ar JSON vienā no diviem formātiem: "
680
+ '{"mode":"final","response":"..."} vai '
681
+ '{"mode":"tool","tool_calls":[{"name":"project_runtime","arguments":{}}]}. '
682
+ "Ja pēc viena vai vairākiem tool rezultātiem joprojām vajag papildu nolasīšanu vai saglabāšanu, "
683
+ "turpini atbildēt ar mode=tool līdz darbs ir pabeigts. "
684
+ "Ja lietotājs lūdz pārbaudīt un salabot modeli, Space vai failus, nepietiek tikai ar analīzi — "
685
+ "pabeidz ar reālu write rīka izsaukumu, ja pieejamais konteksts to ļauj, un tikai tad dod mode=final. "
686
+ f"Drīksti izmantot tikai šos rīkus, maksimums {SPACE_AGENT_MAX_TOOL_CALLS} izsaukumus: "
687
+ f"{tools_json}"
688
+ )
689
+
690
+ messages: list[dict[str, str]] = [{"role": "system", "content": "\n\n".join(prompt_sections)}]
691
+ for item in request.history[-SPACE_AGENT_HISTORY_WINDOW:]:
692
+ messages.append({"role": item.role, "content": item.content})
693
+ messages.append({"role": "user", "content": request.message})
694
+ return messages
695
+
696
+
697
+ def _response_text(raw_response: Any) -> str:
698
+ """Normalize HF chat completion outputs into a single string payload."""
699
+ choices = getattr(raw_response, "choices", None)
700
+ if choices is None and isinstance(raw_response, dict):
701
+ choices = raw_response.get("choices")
702
+ first_choice = _safe_first_response_choice(choices)
703
+ if first_choice is None:
704
+ return ""
705
+
706
+ message = getattr(first_choice, "message", None)
707
+ if message is None and isinstance(first_choice, dict):
708
+ message = first_choice.get("message")
709
+ if message is None:
710
+ return ""
711
+
712
+ content = getattr(message, "content", None)
713
+ if content is None and isinstance(message, dict):
714
+ content = message.get("content")
715
+ if isinstance(content, str):
716
+ return content.strip()
717
+ if isinstance(content, list):
718
+ parts: list[str] = []
719
+ for item in content:
720
+ if isinstance(item, dict):
721
+ text = item.get("text") or item.get("content")
722
+ if isinstance(text, str) and text.strip():
723
+ parts.append(text.strip())
724
+ return "\n".join(parts).strip()
725
+ return ""
726
+
727
+
728
+ def _safe_first_response_choice(choices: Any) -> Any | None:
729
+ """Return the first non-None chat choice, or None when choices are unusable."""
730
+ # Ignore scalar payloads that are technically iterable but not valid HF choice containers.
731
+ if choices is None or isinstance(choices, (dict, str, bytes)):
732
+ return None
733
+ try:
734
+ iterator = iter(choices)
735
+ except TypeError:
736
+ return None
737
+ for choice in iterator:
738
+ if choice is not None:
739
+ return choice
740
+ return None
741
+
742
+
743
+ def _extract_json_object(raw_text: str) -> dict[str, Any] | None:
744
+ raw_text = raw_text.strip()
745
+ if not raw_text:
746
+ return None
747
+ try:
748
+ parsed = json.loads(raw_text)
749
+ return parsed if isinstance(parsed, dict) else None
750
+ except json.JSONDecodeError:
751
+ start = raw_text.find("{")
752
+ end = raw_text.rfind("}")
753
+ if start == -1 or end == -1 or end <= start:
754
+ logger.debug("Space agent response did not contain a JSON object: %s", raw_text)
755
+ return None
756
+ try:
757
+ parsed = json.loads(raw_text[start : end + 1])
758
+ except json.JSONDecodeError:
759
+ logger.warning("Space agent JSON extraction failed: %s", raw_text)
760
+ return None
761
+ return parsed if isinstance(parsed, dict) else None
762
+
763
+
764
+ def _parse_tool_calls(payload: dict[str, Any]) -> list[SpaceAgentToolCall]:
765
+ if payload.get("mode") != "tool":
766
+ return []
767
+ raw_calls = payload.get("tool_calls")
768
+ if not isinstance(raw_calls, list):
769
+ return []
770
+
771
+ parsed_calls: list[SpaceAgentToolCall] = []
772
+ for raw_call in raw_calls[:SPACE_AGENT_MAX_TOOL_CALLS]:
773
+ if not isinstance(raw_call, dict):
774
+ continue
775
+ name = raw_call.get("name")
776
+ arguments = raw_call.get("arguments", {})
777
+ if name not in SPACE_AGENT_TOOL_NAMES or not isinstance(arguments, dict):
778
+ continue
779
+ parsed_calls.append(SpaceAgentToolCall(name=name, arguments=arguments))
780
+ return parsed_calls
781
+
782
+
783
+ def execute_space_agent_tool(
784
+ tool_call: SpaceAgentToolCall, *, context: dict[str, Any] | None = None
785
+ ) -> dict[str, Any]:
786
+ """Execute a built-in agent tool and return structured data."""
787
+ runtime = get_space_agent_runtime_info()
788
+ ctx = context or {}
789
+ _ensure_space_agent_not_cancelled(ctx)
790
+
791
+ if tool_call.name == "project_runtime":
792
+ return {
793
+ "model": runtime.model,
794
+ "dataset_repo": runtime.dataset_repo,
795
+ "model_repo": runtime.model_repo,
796
+ "space_repo": runtime.space_repo,
797
+ "huggingface_owner": runtime.huggingface_owner,
798
+ "has_publish_token": runtime.has_publish_token,
799
+ "capabilities": list(runtime.capabilities),
800
+ "command_presets": list(runtime.command_presets),
801
+ }
802
+ if tool_call.name == "model_dataset_playbook":
803
+ return {
804
+ "dataset_repo": runtime.dataset_repo,
805
+ "model_repo": runtime.model_repo,
806
+ "space_repo": runtime.space_repo,
807
+ **SPACE_AGENT_MODEL_DATASET_PLAYBOOK,
808
+ }
809
+ if tool_call.name == "training_presets":
810
+ return {"presets": list_training_base_models()}
811
+ if tool_call.name == "training_status":
812
+ training_status = ctx.get("training_status")
813
+ return (
814
+ training_status
815
+ if isinstance(training_status, dict)
816
+ else {
817
+ "running": False,
818
+ "message": "Training status nav pieejams šajā kontekstā.",
819
+ }
820
+ )
821
+ if tool_call.name == "sync_commands":
822
+ return {
823
+ "space_upload": f"MARIS_AGENT_SPACE_REPO={runtime.space_repo} bash ./huggingface/sync.sh upload-space",
824
+ "dataset_upload": "bash ./huggingface/sync.sh upload-dataset",
825
+ "model_upload": "bash ./huggingface/sync.sh upload-model",
826
+ "full_sync": "bash ./huggingface/sync.sh sync",
827
+ }
828
+ if tool_call.name == "workspace_command_catalog":
829
+ return {"presets": list(SPACE_AGENT_WORKSPACE_COMMAND_PRESETS)}
830
+ if tool_call.name == "browser_capabilities":
831
+ return get_browser_automation_capabilities().model_dump()
832
+ if tool_call.name == "persona_catalog":
833
+ return get_persona_catalog().model_dump()
834
+ if tool_call.name == "list_huggingface_repos":
835
+ return _list_huggingface_repos(tool_call.arguments)
836
+ if tool_call.name == "list_huggingface_repo_files":
837
+ return _list_huggingface_repo_files(tool_call.arguments)
838
+ if tool_call.name == "read_huggingface_repo_file":
839
+ return _read_huggingface_repo_file(tool_call.arguments)
840
+ if tool_call.name == "write_huggingface_repo_file":
841
+ return _write_huggingface_repo_file(tool_call.arguments, context=ctx)
842
+ if tool_call.name == "list_workspace":
843
+ return _list_workspace_entries(tool_call.arguments, context=ctx)
844
+ if tool_call.name == "read_workspace_file":
845
+ return _read_workspace_file(tool_call.arguments, context=ctx)
846
+ if tool_call.name == "write_workspace_file":
847
+ return _write_workspace_file(tool_call.arguments, context=ctx)
848
+ if tool_call.name == "run_workspace_command":
849
+ command_runner = ctx.get("workspace_command_runner")
850
+ if not callable(command_runner):
851
+ return {
852
+ "ok": False,
853
+ "error": "Workspace komandu izpilde nav pieejama šajā kontekstā.",
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
+
861
+ def _ensure_space_agent_not_cancelled(context: dict[str, Any] | None = None) -> None:
862
+ ctx = context or {}
863
+ cancel_checker = ctx.get("cancel_checker")
864
+ if callable(cancel_checker):
865
+ cancel_checker()
866
+
867
+
868
+ def _get_hf_api_client() -> Any:
869
+ try:
870
+ from huggingface_hub import HfApi # type: ignore
871
+ except ImportError as exc: # pragma: no cover - environment-specific
872
+ raise RuntimeError("Hugging Face API klients nav pieejams.") from exc
873
+ return HfApi(token=get_env_any("MARIS_REPO_TOKEN", "MARIS_TOKEN", "HF_TOKEN"))
874
+
875
+
876
+ def _download_hf_repo_file(*, repo_id: str, repo_type: str, path_in_repo: str) -> str:
877
+ try:
878
+ from huggingface_hub import hf_hub_download # type: ignore
879
+ except ImportError as exc: # pragma: no cover - environment-specific
880
+ raise RuntimeError("Hugging Face download helperis nav pieejams.") from exc
881
+ return str(
882
+ hf_hub_download(
883
+ repo_id=repo_id,
884
+ repo_type=repo_type,
885
+ filename=path_in_repo,
886
+ token=get_env_any("MARIS_REPO_TOKEN", "MARIS_TOKEN", "HF_TOKEN"),
887
+ )
888
+ )
889
+
890
+
891
+ def _validate_hf_repo_type(value: Any, *, allow_all: bool = False) -> str:
892
+ normalized = str(value or "").strip().lower() or ("all" if allow_all else "model")
893
+ allowed = {"model", "dataset", "space"}
894
+ if allow_all:
895
+ allowed.add("all")
896
+ if normalized not in allowed:
897
+ raise ValueError(f"repo_type jābūt vienam no: {', '.join(sorted(allowed))}.")
898
+ return normalized
899
+
900
+
901
+ def _validate_hf_repo_id(value: Any) -> str:
902
+ normalized = str(value or "").strip()
903
+ if not SPACE_AGENT_MODEL_ID_RE.fullmatch(normalized):
904
+ raise ValueError("repo_id jābūt owner/name formātā.")
905
+ return normalized
906
+
907
+
908
+ def _validate_owned_hf_repo_id(repo_id: str) -> str:
909
+ allowed_owner = _get_huggingface_owner()
910
+ owner = repo_id.split("/", 1)[0]
911
+ if owner != allowed_owner:
912
+ raise ValueError("Aģents drīkst rakstīt tikai savā konfigurētajā Hugging Face owner telpā.")
913
+ return repo_id
914
+
915
+
916
+ def _normalize_hf_repo_path(value: Any) -> str:
917
+ raw_path = str(value or "").strip().strip("/")
918
+ if not raw_path:
919
+ raise ValueError("Jānorāda faila ceļš repozitorijā.")
920
+ if ".." in Path(raw_path).parts:
921
+ raise ValueError("Faila ceļš nedrīkst iziet ārpus repozitorija.")
922
+ return raw_path
923
+
924
+
925
+ def _repo_entry(repo_type: str, item: Any) -> dict[str, Any]:
926
+ repo_id = (
927
+ getattr(item, "id", None)
928
+ or getattr(item, "repo_id", None)
929
+ or getattr(item, "modelId", None)
930
+ or getattr(item, "name", None)
931
+ or ""
932
+ )
933
+ return {
934
+ "id": str(repo_id),
935
+ "repo_type": repo_type,
936
+ "private": bool(getattr(item, "private", False)),
937
+ "sha": getattr(item, "sha", None),
938
+ "last_modified": (
939
+ getattr(item, "last_modified", None).isoformat()
940
+ if getattr(item, "last_modified", None) is not None
941
+ else None
942
+ ),
943
+ }
944
+
945
+
946
+ def _list_huggingface_repos(arguments: dict[str, Any]) -> dict[str, Any]:
947
+ repo_type = _validate_hf_repo_type(arguments.get("repo_type"), allow_all=True)
948
+ search = str(arguments.get("search", "") or "").strip() or None
949
+ raw_limit = arguments.get("limit", 12)
950
+ try:
951
+ limit = max(1, min(int(raw_limit), 30))
952
+ except (TypeError, ValueError) as exc:
953
+ raise ValueError("limit jābūt skaitlim no 1 līdz 30.") from exc
954
+
955
+ owner = _get_huggingface_owner()
956
+ api = _get_hf_api_client()
957
+ entries: list[dict[str, Any]] = []
958
+
959
+ if repo_type in {"all", "model"}:
960
+ entries.extend(
961
+ _repo_entry("model", item)
962
+ for item in api.list_models(author=owner, search=search, limit=limit)
963
+ )
964
+ if repo_type in {"all", "dataset"}:
965
+ entries.extend(
966
+ _repo_entry("dataset", item)
967
+ for item in api.list_datasets(author=owner, search=search, limit=limit)
968
+ )
969
+ if repo_type in {"all", "space"}:
970
+ list_spaces = getattr(api, "list_spaces", None)
971
+ if callable(list_spaces):
972
+ entries.extend(
973
+ _repo_entry("space", item)
974
+ for item in list_spaces(author=owner, search=search, limit=limit)
975
+ )
976
+
977
+ return {
978
+ "owner": owner,
979
+ "repo_type": repo_type,
980
+ "entries": entries[
981
+ : (limit * SPACE_AGENT_HF_REPO_TYPE_COUNT if repo_type == "all" else limit)
982
+ ],
983
+ }
984
+
985
+
986
+ def _list_huggingface_repo_files(arguments: dict[str, Any]) -> dict[str, Any]:
987
+ repo_id = _validate_hf_repo_id(arguments.get("repo_id"))
988
+ repo_type = _validate_hf_repo_type(arguments.get("repo_type"))
989
+ api = _get_hf_api_client()
990
+ files = sorted(api.list_repo_files(repo_id=repo_id, repo_type=repo_type))
991
+ return {
992
+ "repo_id": repo_id,
993
+ "repo_type": repo_type,
994
+ "entries": files[:SPACE_AGENT_MAX_DIRECTORY_ENTRIES],
995
+ "truncated": len(files) > SPACE_AGENT_MAX_DIRECTORY_ENTRIES,
996
+ }
997
+
998
+
999
+ def _read_huggingface_repo_file(arguments: dict[str, Any]) -> dict[str, Any]:
1000
+ repo_id = _validate_hf_repo_id(arguments.get("repo_id"))
1001
+ repo_type = _validate_hf_repo_type(arguments.get("repo_type"))
1002
+ path_in_repo = _normalize_hf_repo_path(arguments.get("path"))
1003
+ local_path = Path(
1004
+ _download_hf_repo_file(repo_id=repo_id, repo_type=repo_type, path_in_repo=path_in_repo)
1005
+ )
1006
+ raw_content = local_path.read_bytes()
1007
+ truncated = len(raw_content) > SPACE_AGENT_MAX_FILE_BYTES
1008
+ try:
1009
+ content = raw_content[:SPACE_AGENT_MAX_FILE_BYTES].decode("utf-8")
1010
+ except UnicodeDecodeError as exc:
1011
+ raise ValueError("Pieprasītais HF fails nav UTF-8 teksta fails.") from exc
1012
+ return {
1013
+ "repo_id": repo_id,
1014
+ "repo_type": repo_type,
1015
+ "path": path_in_repo,
1016
+ "content": content,
1017
+ "encoding": "utf-8",
1018
+ "truncated": truncated,
1019
+ "size_bytes": len(raw_content),
1020
+ }
1021
+
1022
+
1023
+ def _write_huggingface_repo_file(
1024
+ arguments: dict[str, Any], *, context: dict[str, Any] | None = None
1025
+ ) -> dict[str, Any]:
1026
+ repo_id = _validate_owned_hf_repo_id(_validate_hf_repo_id(arguments.get("repo_id")))
1027
+ repo_type = _validate_hf_repo_type(arguments.get("repo_type"))
1028
+ path_in_repo = _normalize_hf_repo_path(arguments.get("path"))
1029
+ content = arguments.get("content")
1030
+ if not isinstance(content, str):
1031
+ raise ValueError("Rakstāmajam HF failam jāsaņem teksta saturs laukā 'content'.")
1032
+ encoded = content.encode("utf-8")
1033
+ if len(encoded) > SPACE_AGENT_MAX_FILE_BYTES:
1034
+ raise ValueError("Saturs ir pārāk liels vienam HF write pieprasījumam.")
1035
+ commit_message = (
1036
+ str(arguments.get("commit_message", "") or "").strip() or f"Maris AI update {path_in_repo}"
1037
+ )
1038
+ previous_content = _try_read_existing_hf_repo_text(
1039
+ repo_id=repo_id, repo_type=repo_type, path_in_repo=path_in_repo
1040
+ )
1041
+ operation = "create" if previous_content is None else "update"
1042
+ diff = _build_text_diff(path=path_in_repo, previous=previous_content, current=content)
1043
+ ctx = context or {}
1044
+ stage_hf_write = ctx.get("stage_hf_write")
1045
+ if ctx.get("require_publish_approval") and callable(stage_hf_write):
1046
+ staged = stage_hf_write(
1047
+ {
1048
+ "repo_id": repo_id,
1049
+ "repo_type": repo_type,
1050
+ "path": path_in_repo,
1051
+ "content": content,
1052
+ "commit_message": commit_message,
1053
+ "size_bytes": len(encoded),
1054
+ "operation": operation,
1055
+ "diff": diff,
1056
+ "task_mode": ctx.get("task_mode", SPACE_AGENT_DEFAULT_TASK_MODE),
1057
+ }
1058
+ )
1059
+ return {
1060
+ "repo_id": repo_id,
1061
+ "repo_type": repo_type,
1062
+ "path": path_in_repo,
1063
+ "size_bytes": len(encoded),
1064
+ "commit_message": commit_message,
1065
+ "saved": False,
1066
+ "staged": True,
1067
+ "requires_approval": True,
1068
+ "operation": operation,
1069
+ "diff": diff,
1070
+ **(staged if isinstance(staged, dict) else {}),
1071
+ }
1072
+ return {
1073
+ **save_huggingface_repo_text_file(
1074
+ repo_id=repo_id,
1075
+ repo_type=repo_type,
1076
+ path_in_repo=path_in_repo,
1077
+ content=content,
1078
+ commit_message=commit_message,
1079
+ ),
1080
+ "operation": operation,
1081
+ "diff": diff,
1082
+ }
1083
+
1084
+
1085
+ def _workspace_root_from_context(context: dict[str, Any]) -> Path:
1086
+ root_value = context.get("workspace_root")
1087
+ if not isinstance(root_value, str) or not root_value.strip():
1088
+ raise ValueError("Workspace root nav pieejams šajā kontekstā.")
1089
+ workspace_root = Path(root_value).expanduser().resolve()
1090
+ if not workspace_root.exists() or not workspace_root.is_dir():
1091
+ raise ValueError("Workspace root nav pieejams vai nav direktorija.")
1092
+ return workspace_root
1093
+
1094
+
1095
+ def _resolve_workspace_path(
1096
+ arguments: dict[str, Any], *, context: dict[str, Any]
1097
+ ) -> tuple[Path, Path]:
1098
+ workspace_root = _workspace_root_from_context(context)
1099
+ raw_path = str(arguments.get("path", ".")).strip() or "."
1100
+ if ".." in Path(raw_path).parts:
1101
+ raise ValueError("Ceļš atrodas ārpus atļautās Maris darba telpas.")
1102
+ candidate = (workspace_root / raw_path).resolve()
1103
+ try:
1104
+ candidate.relative_to(workspace_root)
1105
+ except ValueError as exc:
1106
+ raise ValueError("Ceļš atrodas ārpus atļautās Maris darba telpas.") from exc
1107
+ return workspace_root, candidate
1108
+
1109
+
1110
+ def _list_workspace_entries(
1111
+ arguments: dict[str, Any], *, context: dict[str, Any]
1112
+ ) -> dict[str, Any]:
1113
+ workspace_root, target_path = _resolve_workspace_path(arguments, context=context)
1114
+ if not target_path.exists():
1115
+ raise ValueError("Pieprasītā direktorija neeksistē.")
1116
+ if not target_path.is_dir():
1117
+ raise ValueError("Pieprasītais ceļš nav direktorija.")
1118
+
1119
+ all_entries = sorted(
1120
+ target_path.iterdir(), key=lambda item: (not item.is_dir(), item.name.lower())
1121
+ )
1122
+ entries: list[dict[str, Any]] = []
1123
+ for entry in all_entries[:SPACE_AGENT_MAX_DIRECTORY_ENTRIES]:
1124
+ relative_path = entry.relative_to(workspace_root).as_posix()
1125
+ entries.append(
1126
+ {
1127
+ "path": relative_path,
1128
+ "name": entry.name,
1129
+ "type": "directory" if entry.is_dir() else "file",
1130
+ "size_bytes": entry.stat().st_size if entry.is_file() else None,
1131
+ }
1132
+ )
1133
+
1134
+ return {
1135
+ "workspace_root": str(workspace_root),
1136
+ "path": target_path.relative_to(workspace_root).as_posix() or ".",
1137
+ "entries": entries,
1138
+ "truncated": len(all_entries) > SPACE_AGENT_MAX_DIRECTORY_ENTRIES,
1139
+ }
1140
+
1141
+
1142
+ def _read_workspace_file(arguments: dict[str, Any], *, context: dict[str, Any]) -> dict[str, Any]:
1143
+ workspace_root, target_path = _resolve_workspace_path(arguments, context=context)
1144
+ if not target_path.exists():
1145
+ raise ValueError("Pieprasītais fails neeksistē.")
1146
+ if not target_path.is_file():
1147
+ raise ValueError("Pieprasītais ceļš nav fails.")
1148
+
1149
+ raw_content = target_path.read_bytes()
1150
+ truncated = len(raw_content) > SPACE_AGENT_MAX_FILE_BYTES
1151
+ try:
1152
+ content = raw_content[:SPACE_AGENT_MAX_FILE_BYTES].decode("utf-8")
1153
+ except UnicodeDecodeError as exc:
1154
+ raise ValueError("Pieprasītais fails nav UTF-8 teksta fails.") from exc
1155
+ return {
1156
+ "workspace_root": str(workspace_root),
1157
+ "path": target_path.relative_to(workspace_root).as_posix(),
1158
+ "content": content,
1159
+ "encoding": "utf-8",
1160
+ "truncated": truncated,
1161
+ "size_bytes": len(raw_content),
1162
+ }
1163
+
1164
+
1165
+ def _build_text_diff(*, path: str, previous: str | None, current: str) -> str:
1166
+ before = [] if previous is None else previous.splitlines()
1167
+ after = current.splitlines()
1168
+ return "\n".join(
1169
+ difflib.unified_diff(
1170
+ before,
1171
+ after,
1172
+ fromfile=f"a/{path}",
1173
+ tofile=f"b/{path}",
1174
+ lineterm="",
1175
+ )
1176
+ )
1177
+
1178
+
1179
+ def _workspace_file_state(target_path: Path) -> tuple[str | None, str]:
1180
+ if not target_path.exists():
1181
+ return None, "create"
1182
+ try:
1183
+ previous = target_path.read_text(encoding="utf-8")
1184
+ except UnicodeDecodeError:
1185
+ previous = ""
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)
1193
+ )
1194
+ except (OSError, RuntimeError, ValueError, HfHubHTTPError) as exc:
1195
+ logger.debug(
1196
+ "Unable to read existing HF repo file %s/%s for diff preview: %s",
1197
+ repo_id,
1198
+ path_in_repo,
1199
+ exc,
1200
+ )
1201
+ return None
1202
+ try:
1203
+ return local_path.read_text(encoding="utf-8")
1204
+ except UnicodeDecodeError:
1205
+ return ""
1206
+
1207
+
1208
+ def save_huggingface_repo_text_file(
1209
+ *,
1210
+ repo_id: str,
1211
+ repo_type: str,
1212
+ path_in_repo: str,
1213
+ content: str,
1214
+ commit_message: str,
1215
+ ) -> dict[str, Any]:
1216
+ encoded = content.encode("utf-8")
1217
+ api = _get_hf_api_client()
1218
+ try:
1219
+ api.upload_file(
1220
+ path_or_fileobj=io.BytesIO(encoded),
1221
+ path_in_repo=path_in_repo,
1222
+ repo_id=repo_id,
1223
+ repo_type=repo_type,
1224
+ commit_message=commit_message,
1225
+ )
1226
+ except Exception as exc: # noqa: BLE001
1227
+ logger.warning("HF repo write failed for %s/%s: %s", repo_id, path_in_repo, exc)
1228
+ detail = str(exc).strip()
1229
+ raise RuntimeError(
1230
+ f"Neizdevās saglabāt failu Hugging Face repozitorijā: {detail or type(exc).__name__}."
1231
+ ) from exc
1232
+ return {
1233
+ "repo_id": repo_id,
1234
+ "repo_type": repo_type,
1235
+ "path": path_in_repo,
1236
+ "size_bytes": len(encoded),
1237
+ "commit_message": commit_message,
1238
+ "saved": True,
1239
+ }
1240
+
1241
+
1242
+ def delete_huggingface_repo_text_file(
1243
+ *,
1244
+ repo_id: str,
1245
+ repo_type: str,
1246
+ path_in_repo: str,
1247
+ commit_message: str,
1248
+ ) -> dict[str, Any]:
1249
+ api = _get_hf_api_client()
1250
+ try:
1251
+ api.delete_file(
1252
+ path_in_repo=path_in_repo,
1253
+ repo_id=repo_id,
1254
+ repo_type=repo_type,
1255
+ commit_message=commit_message,
1256
+ )
1257
+ except Exception as exc: # noqa: BLE001
1258
+ logger.warning("HF repo delete failed for %s/%s: %s", repo_id, path_in_repo, exc)
1259
+ detail = str(exc).strip()
1260
+ raise RuntimeError(
1261
+ f"Neizdevās dzēst failu Hugging Face repozitorijā: {detail or type(exc).__name__}."
1262
+ ) from exc
1263
+ return {
1264
+ "repo_id": repo_id,
1265
+ "repo_type": repo_type,
1266
+ "path": path_in_repo,
1267
+ "commit_message": commit_message,
1268
+ "deleted": True,
1269
+ }
1270
+
1271
+
1272
+ def _write_workspace_file(arguments: dict[str, Any], *, context: dict[str, Any]) -> dict[str, Any]:
1273
+ workspace_root, target_path = _resolve_workspace_path(arguments, context=context)
1274
+ content = arguments.get("content")
1275
+ if not isinstance(content, str):
1276
+ raise ValueError("Rakstāmajam failam jāsaņem teksta saturs laukā 'content'.")
1277
+ encoded = content.encode("utf-8")
1278
+ if len(encoded) > SPACE_AGENT_MAX_FILE_BYTES:
1279
+ raise ValueError("Saturs ir pārāk liels vienam workspace write pieprasījumam.")
1280
+
1281
+ try:
1282
+ target_path.parent.relative_to(workspace_root)
1283
+ except ValueError as exc:
1284
+ raise ValueError("Mērķa direktorija atrodas ārpus atļautās Maris darba telpas.") from exc
1285
+ previous_content, operation = _workspace_file_state(target_path)
1286
+ diff = _build_text_diff(
1287
+ path=target_path.relative_to(workspace_root).as_posix(),
1288
+ previous=previous_content,
1289
+ current=content,
1290
+ )
1291
+ target_path.parent.mkdir(parents=True, exist_ok=True)
1292
+ target_path.write_text(content, encoding="utf-8")
1293
+ result = {
1294
+ "workspace_root": str(workspace_root),
1295
+ "path": target_path.relative_to(workspace_root).as_posix(),
1296
+ "size_bytes": len(encoded),
1297
+ "saved": True,
1298
+ "operation": operation,
1299
+ "diff": diff,
1300
+ }
1301
+ stage_workspace_write = context.get("stage_workspace_write")
1302
+ if context.get("require_workspace_approval") and callable(stage_workspace_write):
1303
+ staged = stage_workspace_write(
1304
+ {
1305
+ "path": result["path"],
1306
+ "content": content,
1307
+ "size_bytes": len(encoded),
1308
+ "operation": operation,
1309
+ "diff": diff,
1310
+ "task_mode": context.get("task_mode", SPACE_AGENT_DEFAULT_TASK_MODE),
1311
+ "draft_workspace_root": str(workspace_root),
1312
+ }
1313
+ )
1314
+ return {
1315
+ **result,
1316
+ "saved": False,
1317
+ "saved_to_draft": True,
1318
+ "staged": True,
1319
+ "requires_approval": True,
1320
+ **(staged if isinstance(staged, dict) else {}),
1321
+ }
1322
+ return result
1323
+
1324
+
1325
+ def _tool_result_messages(
1326
+ tool_calls: list[SpaceAgentToolCall],
1327
+ *,
1328
+ context: dict[str, Any] | None = None,
1329
+ events: list[dict[str, Any]] | None = None,
1330
+ event_callback: Callable[[dict[str, Any]], None] | None = None,
1331
+ ) -> list[dict[str, str]]:
1332
+ messages: list[dict[str, str]] = []
1333
+ for tool_call in tool_calls:
1334
+ _ensure_space_agent_not_cancelled(context)
1335
+ _record_agent_event(
1336
+ events,
1337
+ event_callback,
1338
+ {
1339
+ "type": "tool_call",
1340
+ "stage": "tooling",
1341
+ "message": f"Izsaucu rīku {tool_call.name}.",
1342
+ "tool_name": tool_call.name,
1343
+ "arguments": tool_call.arguments,
1344
+ },
1345
+ )
1346
+ try:
1347
+ result = execute_space_agent_tool(tool_call, context=context)
1348
+ except Exception as exc: # noqa: BLE001
1349
+ logger.warning("Space agent tool %s failed: %s", tool_call.name, exc)
1350
+ result = {
1351
+ "ok": False,
1352
+ "error": str(exc).strip() or type(exc).__name__,
1353
+ "error_type": type(exc).__name__,
1354
+ "tool_name": tool_call.name,
1355
+ }
1356
+ _record_agent_event(
1357
+ events,
1358
+ event_callback,
1359
+ {
1360
+ "type": "tool_error",
1361
+ "stage": "tooling",
1362
+ "message": _tool_error_summary(tool_call, result),
1363
+ "tool_name": tool_call.name,
1364
+ "arguments": tool_call.arguments,
1365
+ "error": result,
1366
+ },
1367
+ )
1368
+ else:
1369
+ _record_agent_event(
1370
+ events,
1371
+ event_callback,
1372
+ {
1373
+ "type": "tool_result",
1374
+ "stage": "tooling",
1375
+ "message": _tool_result_summary(tool_call, result),
1376
+ "tool_name": tool_call.name,
1377
+ "arguments": tool_call.arguments,
1378
+ "result": result,
1379
+ },
1380
+ )
1381
+ messages.append(
1382
+ {
1383
+ "role": "assistant",
1384
+ "content": json.dumps(
1385
+ {
1386
+ "tool_call": tool_call.model_dump(),
1387
+ "tool_result": result,
1388
+ },
1389
+ ensure_ascii=False,
1390
+ ),
1391
+ }
1392
+ )
1393
+ return messages
1394
+
1395
+
1396
+ def _record_agent_event(
1397
+ events: list[dict[str, Any]] | None,
1398
+ event_callback: Callable[[dict[str, Any]], None] | None,
1399
+ event: dict[str, Any],
1400
+ ) -> None:
1401
+ if events is not None:
1402
+ events.append(event)
1403
+ if event_callback is not None:
1404
+ event_callback(event)
1405
+
1406
+
1407
+ def _tool_result_summary(tool_call: SpaceAgentToolCall, result: dict[str, Any]) -> str:
1408
+ if tool_call.name == "list_workspace":
1409
+ path = str(result.get("path", "."))
1410
+ entry_count = (
1411
+ len(result.get("entries", [])) if isinstance(result.get("entries"), list) else 0
1412
+ )
1413
+ return f"Pārlūkoju direktoriju {path} un atradu {entry_count} ierakstus."
1414
+ if tool_call.name == "read_workspace_file":
1415
+ path = str(result.get("path", ""))
1416
+ size_bytes = result.get("size_bytes")
1417
+ size_label = f" ({size_bytes} B)" if isinstance(size_bytes, int) else ""
1418
+ return f"Nolasīju failu {path}{size_label}."
1419
+ if tool_call.name == "write_workspace_file":
1420
+ if result.get("requires_approval"):
1421
+ return "Sagatavoju workspace izmaiņas izolētā draftā un nodevu tās uz lietotāja apstiprinājumu."
1422
+ path = str(result.get("path", ""))
1423
+ size_bytes = result.get("size_bytes")
1424
+ size_label = f" ({size_bytes} B)" if isinstance(size_bytes, int) else ""
1425
+ operation = str(result.get("operation", "update"))
1426
+ return f"Saglabāju {operation} failu {path}{size_label} darba telpā."
1427
+ if tool_call.name == "run_workspace_command":
1428
+ command_text = result.get("command_display") or result.get("command") or "komanda"
1429
+ if result.get("ok") is False:
1430
+ return f"Komandas izpilde neizdevās: {command_text}"
1431
+ exit_code = result.get("exit_code")
1432
+ return f"Palaidu validācijas komandu `{command_text}` ar exit kodu {exit_code}."
1433
+ if tool_call.name == "training_status":
1434
+ return "Savācu aktuālo Space treniņa statusu."
1435
+ if tool_call.name == "model_dataset_playbook":
1436
+ return "Savācu model/dataset uzlabošanas playbook ar HF agent principiem un komandām."
1437
+ if tool_call.name == "training_presets":
1438
+ return "Savācu pieejamos treniņa presetus."
1439
+ if tool_call.name == "sync_commands":
1440
+ return "Savācu sync un deploy komandas."
1441
+ if tool_call.name == "workspace_command_catalog":
1442
+ return "Savācu pilno validācijas un darba plūsmas command preset katalogu."
1443
+ if tool_call.name == "browser_capabilities":
1444
+ return "Savācu browser automation iespējas."
1445
+ if tool_call.name == "persona_catalog":
1446
+ return "Savācu pieejamo personu katalogu."
1447
+ if tool_call.name == "list_huggingface_repos":
1448
+ return "Savācu Hugging Face repozitoriju sarakstu."
1449
+ if tool_call.name == "list_huggingface_repo_files":
1450
+ return "Savācu Hugging Face repozitorija failu sarakstu."
1451
+ if tool_call.name == "read_huggingface_repo_file":
1452
+ return "Nolasīju Hugging Face repozitorija failu."
1453
+ if tool_call.name == "write_huggingface_repo_file":
1454
+ if result.get("requires_approval"):
1455
+ return "Sagatavoju Hugging Face izmaiņas un nolieku tās uz lietotāja apstiprinājumu."
1456
+ return "Saglabāju izmaiņas Hugging Face repozitorijā."
1457
+ return "Savācu projekta runtime metadatus."
1458
+
1459
+
1460
+ def _tool_error_summary(tool_call: SpaceAgentToolCall, result: dict[str, Any]) -> str:
1461
+ detail = str(result.get("error", "") or "").strip()
1462
+ if detail:
1463
+ return f"Rīks {tool_call.name} neizdevās: {detail}"
1464
+ return f"Rīks {tool_call.name} neizdevās."
1465
+
1466
+
1467
+ def _final_response_from_json(raw_text: str) -> str:
1468
+ payload = _extract_json_object(raw_text)
1469
+ if payload is not None:
1470
+ if payload.get("mode") == "final" and isinstance(payload.get("response"), str):
1471
+ return payload["response"].strip()
1472
+ if payload.get("mode") == "tool":
1473
+ return ""
1474
+ return raw_text.strip()
1475
+ return raw_text.strip()
1476
+
1477
+
1478
+ def _assistant_json_message(raw_text: str) -> dict[str, str]:
1479
+ return {"role": "assistant", "content": raw_text.strip()}
1480
+
1481
+
1482
+ def _collect_change_previews(events: list[dict[str, Any]]) -> list[dict[str, Any]]:
1483
+ previews: list[dict[str, Any]] = []
1484
+ for event in events:
1485
+ if event.get("type") != "tool_result":
1486
+ continue
1487
+ tool_name = str(event.get("tool_name", ""))
1488
+ result = event.get("result")
1489
+ if not isinstance(result, dict):
1490
+ continue
1491
+ if tool_name not in {"write_workspace_file", "write_huggingface_repo_file"}:
1492
+ continue
1493
+ path = str(result.get("path", "")).strip()
1494
+ if not path:
1495
+ continue
1496
+ preview = {
1497
+ "target": "workspace" if tool_name == "write_workspace_file" else "huggingface",
1498
+ "path": path,
1499
+ "operation": result.get("operation", "update"),
1500
+ "diff": result.get("diff", ""),
1501
+ "saved": bool(result.get("saved", False)),
1502
+ "requires_approval": bool(result.get("requires_approval", False)),
1503
+ "proposal_id": result.get("proposal_id"),
1504
+ "repo_id": result.get("repo_id"),
1505
+ "repo_type": result.get("repo_type"),
1506
+ }
1507
+ previews.append(preview)
1508
+ return previews
1509
+
1510
+
1511
+ def _complete_with_client(
1512
+ client: Any,
1513
+ *,
1514
+ models: tuple[str, ...],
1515
+ messages: list[dict[str, str]],
1516
+ max_tokens: int,
1517
+ temperature: float,
1518
+ ) -> tuple[str | None, str]:
1519
+ last_error: Exception | None = None
1520
+ for model in models:
1521
+ try:
1522
+ raw_response = client.chat_completion(
1523
+ model=model,
1524
+ messages=messages,
1525
+ max_tokens=max_tokens,
1526
+ temperature=temperature,
1527
+ )
1528
+ except StopIteration as exc:
1529
+ logger.warning(
1530
+ "Maris agent chat_completion raised StopIteration for model %s: %s",
1531
+ model,
1532
+ exc,
1533
+ )
1534
+ continue
1535
+ # HF inference backends raise many provider-specific exception types here,
1536
+ # so we treat non-fatal exceptions as retryable across the next model.
1537
+ except (
1538
+ OSError,
1539
+ TypeError,
1540
+ ValueError,
1541
+ RuntimeError,
1542
+ httpx.HTTPError,
1543
+ HfHubHTTPError,
1544
+ ) as exc:
1545
+ last_error = exc
1546
+ logger.warning("Maris agent inference failed for model %s: %s", model, exc)
1547
+ continue
1548
+ text = _response_text(raw_response)
1549
+ if text:
1550
+ return model, text
1551
+ logger.warning("Maris agent returned an empty response for model %s", model)
1552
+ if last_error is not None:
1553
+ raise last_error
1554
+ return None, ""
1555
+
1556
+
1557
+ def _complete_space_agent_response(
1558
+ client: Any,
1559
+ *,
1560
+ models: tuple[str, ...],
1561
+ messages: list[dict[str, str]],
1562
+ max_tokens: int,
1563
+ temperature: float,
1564
+ ) -> tuple[str | None, str, bool]:
1565
+ model_name, raw_response = _complete_with_client(
1566
+ client,
1567
+ models=models,
1568
+ messages=messages,
1569
+ max_tokens=max_tokens,
1570
+ temperature=temperature,
1571
+ )
1572
+ if not raw_response:
1573
+ active_model = model_name or next(iter(models), "")
1574
+ raise RuntimeError(
1575
+ f"Maris AI aģents nesaņēma derīgu atbildi no modeļa `{active_model}` "
1576
+ "(tukša vai nederīga chat-completion atbilde)."
1577
+ )
1578
+ return model_name, raw_response, False
1579
+
1580
+
1581
+ def _build_space_agent_failure_message(
1582
+ requested_model: str,
1583
+ candidate_models: tuple[str, ...],
1584
+ exc: Exception,
1585
+ ) -> str:
1586
+ resolved_model = next(iter(candidate_models), requested_model)
1587
+ detail = str(exc).strip() or type(exc).__name__
1588
+ return (
1589
+ f"Maris AI aģents nevarēja pieslēgties modelim `{resolved_model}`. "
1590
+ f"Pārbaudi modeļa pieejamību un inference konfigurāciju. Detalizācija: {detail}"
1591
+ )
1592
+
1593
+
1594
+ def generate_space_agent_reply(
1595
+ request: SpaceAgentChatRequest,
1596
+ *,
1597
+ client_factory: Any | None = None,
1598
+ token: str | None = None,
1599
+ tool_context: dict[str, Any] | None = None,
1600
+ event_callback: Callable[[dict[str, Any]], None] | None = None,
1601
+ ) -> SpaceAgentChatResponse:
1602
+ """Generate an agent reply with optional tool-calling orchestration.
1603
+
1604
+ Tool selection runs with a capped low temperature to keep tool routing more
1605
+ deterministic than the final user-facing answer.
1606
+ """
1607
+ runtime = get_space_agent_runtime_info()
1608
+ requested_model = request.model or runtime.default_model
1609
+ response_model = requested_model
1610
+ candidate_models = resolve_space_agent_models(requested_model)
1611
+ tooling_enabled = _should_enable_space_agent_tooling(request, requested_model)
1612
+ events: list[dict[str, Any]] = []
1613
+ tool_calls: list[SpaceAgentToolCall] = []
1614
+ used_fallback = False
1615
+
1616
+ if client_factory is None:
1617
+ try:
1618
+ from huggingface_hub import InferenceClient # type: ignore
1619
+ except ImportError as exc:
1620
+ raise RuntimeError("Maris AI inference klients nav pieejams.") from exc
1621
+ client_factory = InferenceClient
1622
+
1623
+ try:
1624
+ _ensure_space_agent_not_cancelled(tool_context)
1625
+ client = create_hf_inference_client(client_factory, token=token)
1626
+ _record_agent_event(
1627
+ events,
1628
+ event_callback,
1629
+ {
1630
+ "type": "status",
1631
+ "stage": "queued",
1632
+ "message": "Saņēmu uzdevumu un sāku analizēt pieprasījumu.",
1633
+ },
1634
+ )
1635
+
1636
+ if tooling_enabled:
1637
+ tool_selection_messages = build_space_agent_messages(
1638
+ request,
1639
+ include_tooling_rules=True,
1640
+ active_model=response_model,
1641
+ )
1642
+ executed_any_tools = False
1643
+
1644
+ for iteration in range(SPACE_AGENT_MAX_TOOL_ITERATIONS):
1645
+ _ensure_space_agent_not_cancelled(tool_context)
1646
+ _record_agent_event(
1647
+ events,
1648
+ event_callback,
1649
+ {
1650
+ "type": "status",
1651
+ "stage": "planning",
1652
+ "message": (
1653
+ "Plānoju nepieciešamos rīkus un darba soļus."
1654
+ if iteration == 0
1655
+ else "Izvērtēju iepriekšējo rīku rezultātus un plānoju nākamo soli."
1656
+ ),
1657
+ },
1658
+ )
1659
+ tool_selection_model, tool_selection_raw, tool_selection_fallback = (
1660
+ _complete_space_agent_response(
1661
+ client,
1662
+ models=candidate_models,
1663
+ messages=tool_selection_messages,
1664
+ max_tokens=min(request.max_tokens, 1024),
1665
+ temperature=min(request.temperature, 0.2),
1666
+ )
1667
+ )
1668
+ if tool_selection_model:
1669
+ used_fallback = used_fallback or tool_selection_fallback
1670
+ used_fallback = used_fallback or tool_selection_model != requested_model
1671
+ response_model = tool_selection_model
1672
+ _ensure_space_agent_not_cancelled(tool_context)
1673
+ tool_selection_payload = _extract_json_object(tool_selection_raw)
1674
+ remaining_tool_budget = SPACE_AGENT_MAX_TOOL_CALLS - len(tool_calls)
1675
+ current_tool_calls = (
1676
+ _parse_tool_calls(tool_selection_payload)[:remaining_tool_budget]
1677
+ if tool_selection_payload is not None and remaining_tool_budget > 0
1678
+ else []
1679
+ )
1680
+ final_response = _final_response_from_json(tool_selection_raw)
1681
+ if not current_tool_calls:
1682
+ if final_response:
1683
+ _record_agent_event(
1684
+ events,
1685
+ event_callback,
1686
+ {
1687
+ "type": "final",
1688
+ "stage": "completed",
1689
+ "message": "Gala atbilde ir gatava.",
1690
+ "response": final_response,
1691
+ },
1692
+ )
1693
+ return SpaceAgentChatResponse(
1694
+ response=final_response,
1695
+ model=response_model,
1696
+ request_id=(tool_context or {}).get("request_id"),
1697
+ task_id=(tool_context or {}).get("task_id"),
1698
+ used_fallback=used_fallback,
1699
+ tool_calls=tool_calls,
1700
+ events=events,
1701
+ task_mode=request.task_mode,
1702
+ change_previews=_collect_change_previews(events),
1703
+ )
1704
+ break
1705
+
1706
+ tool_calls.extend(current_tool_calls)
1707
+ executed_any_tools = True
1708
+ _record_agent_event(
1709
+ events,
1710
+ event_callback,
1711
+ {
1712
+ "type": "status",
1713
+ "stage": "tooling",
1714
+ "message": f"Izvēlējos {len(current_tool_calls)} rīkus darba izpildei.",
1715
+ },
1716
+ )
1717
+ tool_selection_messages.append(_assistant_json_message(tool_selection_raw))
1718
+ tool_selection_messages.extend(
1719
+ _tool_result_messages(
1720
+ current_tool_calls,
1721
+ context=tool_context,
1722
+ events=events,
1723
+ event_callback=event_callback,
1724
+ )
1725
+ )
1726
+ if executed_any_tools:
1727
+ _record_agent_event(
1728
+ events,
1729
+ event_callback,
1730
+ {
1731
+ "type": "status",
1732
+ "stage": "final",
1733
+ "message": "Veidoju gala atbildi no savāktā konteksta.",
1734
+ },
1735
+ )
1736
+ final_messages = list(tool_selection_messages)
1737
+ final_messages.append(
1738
+ {
1739
+ "role": "assistant",
1740
+ "content": (
1741
+ "Tagad pabeidz darbu. Ja viss nepieciešamais jau ir pārbaudīts un saglabāts, "
1742
+ 'atbildi tikai ar JSON formātā {"mode":"final","response":"..."}.'
1743
+ ),
1744
+ }
1745
+ )
1746
+ final_model, final_raw, final_generation_fallback = _complete_space_agent_response(
1747
+ client,
1748
+ models=candidate_models,
1749
+ messages=final_messages,
1750
+ max_tokens=request.max_tokens,
1751
+ temperature=request.temperature,
1752
+ )
1753
+ if final_model:
1754
+ used_fallback = used_fallback or final_generation_fallback
1755
+ used_fallback = used_fallback or final_model != requested_model
1756
+ response_model = final_model
1757
+ _ensure_space_agent_not_cancelled(tool_context)
1758
+ final_response = _final_response_from_json(final_raw)
1759
+ if final_response:
1760
+ _record_agent_event(
1761
+ events,
1762
+ event_callback,
1763
+ {
1764
+ "type": "final",
1765
+ "stage": "completed",
1766
+ "message": "Gala atbilde ir gatava.",
1767
+ "response": final_response,
1768
+ },
1769
+ )
1770
+ return SpaceAgentChatResponse(
1771
+ response=final_response,
1772
+ model=response_model,
1773
+ request_id=(tool_context or {}).get("request_id"),
1774
+ task_id=(tool_context or {}).get("task_id"),
1775
+ used_fallback=used_fallback,
1776
+ tool_calls=tool_calls,
1777
+ events=events,
1778
+ task_mode=request.task_mode,
1779
+ change_previews=_collect_change_previews(events),
1780
+ )
1781
+ else:
1782
+ _record_agent_event(
1783
+ events,
1784
+ event_callback,
1785
+ {
1786
+ "type": "status",
1787
+ "stage": "planning",
1788
+ "message": "Šim pieprasījumam pietiek ar tiešu atbildi bez papildu rīkiem.",
1789
+ },
1790
+ )
1791
+ elif request.tool_calling:
1792
+ _record_agent_event(
1793
+ events,
1794
+ event_callback,
1795
+ {
1796
+ "type": "status",
1797
+ "stage": "planning",
1798
+ "message": (
1799
+ "Aktīvais modelis ir teksta-first režīmā, tāpēc izmantoju vienkāršotu tiešās atbildes ceļu bez tool-calling."
1800
+ ),
1801
+ },
1802
+ )
1803
+
1804
+ _record_agent_event(
1805
+ events,
1806
+ event_callback,
1807
+ {
1808
+ "type": "status",
1809
+ "stage": "final",
1810
+ "message": "Veidoju gala atbildi.",
1811
+ },
1812
+ )
1813
+ plain_model, plain_raw, plain_generation_fallback = _complete_space_agent_response(
1814
+ client,
1815
+ models=candidate_models,
1816
+ messages=build_space_agent_messages(
1817
+ request,
1818
+ include_tooling_rules=tooling_enabled,
1819
+ active_model=response_model,
1820
+ ),
1821
+ max_tokens=request.max_tokens,
1822
+ temperature=request.temperature,
1823
+ )
1824
+ if plain_model:
1825
+ used_fallback = used_fallback or plain_generation_fallback
1826
+ used_fallback = used_fallback or plain_model != requested_model
1827
+ response_model = plain_model
1828
+ _ensure_space_agent_not_cancelled(tool_context)
1829
+ final_response = _final_response_from_json(plain_raw)
1830
+ if not final_response:
1831
+ raise RuntimeError("Maris AI neatgrieza derīgu atbildi.")
1832
+ _record_agent_event(
1833
+ events,
1834
+ event_callback,
1835
+ {
1836
+ "type": "final",
1837
+ "stage": "completed",
1838
+ "message": "Gala atbilde ir gatava.",
1839
+ "response": final_response,
1840
+ },
1841
+ )
1842
+ return SpaceAgentChatResponse(
1843
+ response=final_response,
1844
+ model=response_model,
1845
+ request_id=(tool_context or {}).get("request_id"),
1846
+ task_id=(tool_context or {}).get("task_id"),
1847
+ used_fallback=used_fallback,
1848
+ tool_calls=tool_calls if tooling_enabled else [],
1849
+ events=events,
1850
+ task_mode=request.task_mode,
1851
+ change_previews=_collect_change_previews(events),
1852
+ )
1853
+ except SpaceAgentCancelledError:
1854
+ raise
1855
+ except (
1856
+ AttributeError,
1857
+ OSError,
1858
+ TypeError,
1859
+ ValueError,
1860
+ RuntimeError,
1861
+ httpx.HTTPError,
1862
+ HfHubHTTPError,
1863
+ ) as exc:
1864
+ logger.warning("Maris agent inference failed: %s", exc)
1865
+ raise RuntimeError(
1866
+ _build_space_agent_failure_message(requested_model, candidate_models, exc)
1867
+ ) from exc