Lucas commited on
Commit ·
95cf15d
1
Parent(s): a1e6f13
Add llama.cpp parser backend for Space
Browse files- .env.example +7 -5
- DECISIONS.md +11 -7
- MVP_COMPROMISES.md +3 -2
- PLAN.md +6 -5
- README.md +17 -11
- requirements.txt +1 -4
- tests/test_env.py +4 -4
- tests/test_parser_llama_cpp.py +156 -0
- tests/test_parser_ollama.py +2 -2
- tests/test_parser_service.py +15 -2
- training_coach/parser_llama_cpp.py +156 -0
- training_coach/parser_ollama.py +1 -1
- training_coach/parser_service.py +14 -4
.env.example
CHANGED
|
@@ -3,7 +3,7 @@ HISTORY_STORE=local_json
|
|
| 3 |
LOCAL_HISTORY_PATH=data/completed_sessions.json
|
| 4 |
|
| 5 |
PARSER_BACKEND=ollama
|
| 6 |
-
OLLAMA_MODEL=qwen3:
|
| 7 |
# Private parser trace logs. These can include check-in text, so leave them off
|
| 8 |
# unless debugging parser failures.
|
| 9 |
PARSER_LOG_MESSAGES=0
|
|
@@ -11,10 +11,12 @@ PARSER_LOG_RESPONSES=0
|
|
| 11 |
PARSER_LOG_PARSED_JSON=0
|
| 12 |
|
| 13 |
# Hugging Face Space parser runtime.
|
| 14 |
-
# PARSER_BACKEND=
|
| 15 |
-
#
|
| 16 |
-
#
|
| 17 |
-
#
|
|
|
|
|
|
|
| 18 |
|
| 19 |
# Optional Hugging Face Dataset storage.
|
| 20 |
# Keep real tokens in your local .env or Hugging Face Space secrets.
|
|
|
|
| 3 |
LOCAL_HISTORY_PATH=data/completed_sessions.json
|
| 4 |
|
| 5 |
PARSER_BACKEND=ollama
|
| 6 |
+
OLLAMA_MODEL=qwen3:1.7B
|
| 7 |
# Private parser trace logs. These can include check-in text, so leave them off
|
| 8 |
# unless debugging parser failures.
|
| 9 |
PARSER_LOG_MESSAGES=0
|
|
|
|
| 11 |
PARSER_LOG_PARSED_JSON=0
|
| 12 |
|
| 13 |
# Hugging Face Space parser runtime.
|
| 14 |
+
# PARSER_BACKEND=llama_cpp
|
| 15 |
+
# LLAMA_CPP_MODEL_REPO=unsloth/Qwen3-1.7B-GGUF
|
| 16 |
+
# LLAMA_CPP_MODEL_FILE=Qwen3-1.7B-Q4_K_M.gguf
|
| 17 |
+
# LLAMA_CPP_MAX_TOKENS=384
|
| 18 |
+
# LLAMA_CPP_N_CTX=2048
|
| 19 |
+
# LLAMA_CPP_N_THREADS=2
|
| 20 |
|
| 21 |
# Optional Hugging Face Dataset storage.
|
| 22 |
# Keep real tokens in your local .env or Hugging Face Space secrets.
|
DECISIONS.md
CHANGED
|
@@ -74,8 +74,9 @@ with a one-line note on why.
|
|
| 74 |
the planned session and stores ordered `LoggedExercise` entries. Each `LoggedSet`
|
| 75 |
stores set number, actual reps, actual load, optional RPE, optional rest seconds before
|
| 76 |
the set, and notes.
|
| 77 |
-
- **Parser model:** local MVP parser uses Ollama `qwen3:
|
| 78 |
-
|
|
|
|
| 79 |
The parser may infer safe structured facts and possible context signals, but it must
|
| 80 |
not invent quantities or training actions. Pain information must use per-issue
|
| 81 |
`pain_issues`; exercise filtering is performed by the deterministic engine.
|
|
@@ -85,11 +86,14 @@ with a one-line note on why.
|
|
| 85 |
are acceptable before the engine builds a session.
|
| 86 |
- **Local parser runtime:** use Ollama/llama.cpp for local evaluation and development
|
| 87 |
because it is much faster on the Mac than local Transformers CPU/disk-offload.
|
| 88 |
-
`qwen3:
|
| 89 |
-
- **Space parser runtime:** Hugging Face Spaces use
|
| 90 |
-
configured with `PARSER_BACKEND=
|
| 91 |
-
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
| 93 |
- **MVP training template:** use the supplied fixed 4-day exercise template for the
|
| 94 |
first engine slice. Preserve exercise grouping, execution order, set counts, rep
|
| 95 |
targets, and notes. For MVP simplicity, all exercise rests are normalized to 1 minute.
|
|
|
|
| 74 |
the planned session and stores ordered `LoggedExercise` entries. Each `LoggedSet`
|
| 75 |
stores set number, actual reps, actual load, optional RPE, optional rest seconds before
|
| 76 |
the set, and notes.
|
| 77 |
+
- **Parser model:** local MVP parser uses Ollama `qwen3:1.7B` by default for speed.
|
| 78 |
+
Earlier `qwen3:4b` runs passed the current parser acceptance fixtures after prompt
|
| 79 |
+
optimization and deterministic parser cleanup.
|
| 80 |
The parser may infer safe structured facts and possible context signals, but it must
|
| 81 |
not invent quantities or training actions. Pain information must use per-issue
|
| 82 |
`pain_issues`; exercise filtering is performed by the deterministic engine.
|
|
|
|
| 86 |
are acceptable before the engine builds a session.
|
| 87 |
- **Local parser runtime:** use Ollama/llama.cpp for local evaluation and development
|
| 88 |
because it is much faster on the Mac than local Transformers CPU/disk-offload.
|
| 89 |
+
`qwen3:1.7B` is the current local Ollama default for MVP speed.
|
| 90 |
+
- **Space parser runtime:** Hugging Face Spaces use a GGUF llama.cpp backend,
|
| 91 |
+
configured with `PARSER_BACKEND=llama_cpp`,
|
| 92 |
+
`LLAMA_CPP_MODEL_REPO=unsloth/Qwen3-1.7B-GGUF`,
|
| 93 |
+
`LLAMA_CPP_MODEL_FILE=Qwen3-1.7B-Q4_K_M.gguf`, `LLAMA_CPP_MAX_TOKENS=384`,
|
| 94 |
+
`LLAMA_CPP_N_CTX=2048`, and a Space-specific `LLAMA_CPP_N_THREADS`. This replaces the
|
| 95 |
+
slow CPU Transformers runtime. The Transformers backend remains only as an explicit
|
| 96 |
+
local experiment/fallback path and is not installed by the default requirements.
|
| 97 |
- **MVP training template:** use the supplied fixed 4-day exercise template for the
|
| 98 |
first engine slice. Preserve exercise grouping, execution order, set counts, rep
|
| 99 |
targets, and notes. For MVP simplicity, all exercise rests are normalized to 1 minute.
|
MVP_COMPROMISES.md
CHANGED
|
@@ -47,8 +47,9 @@ forgetting what is missing afterward.
|
|
| 47 |
- **Multi-round clarification over one-shot perfection.** The small model proposes
|
| 48 |
structured follow-up questions, and deterministic cleanup removes obvious duplicates,
|
| 49 |
already-answered questions, and unsupported activity/body-area follow-ups.
|
| 50 |
-
- **Local development uses Ollama.** This is fast and useful locally
|
| 51 |
-
|
|
|
|
| 52 |
- **Acceptance fixtures are test coverage, not product proof.** They help catch obvious
|
| 53 |
regressions, but real value still comes from logged sessions.
|
| 54 |
|
|
|
|
| 47 |
- **Multi-round clarification over one-shot perfection.** The small model proposes
|
| 48 |
structured follow-up questions, and deterministic cleanup removes obvious duplicates,
|
| 49 |
already-answered questions, and unsupported activity/body-area follow-ups.
|
| 50 |
+
- **Local development uses Ollama.** This is fast and useful locally. Hugging Face
|
| 51 |
+
Spaces use a GGUF llama.cpp backend so CPU deployment does not rely on slow PyTorch
|
| 52 |
+
Transformers generation.
|
| 53 |
- **Acceptance fixtures are test coverage, not product proof.** They help catch obvious
|
| 54 |
regressions, but real value still comes from logged sessions.
|
| 55 |
|
PLAN.md
CHANGED
|
@@ -129,14 +129,14 @@ exists.
|
|
| 129 |
|
| 130 |
**Decision status:**
|
| 131 |
|
| 132 |
-
- [x] Choose local parser model: Ollama `qwen3:
|
| 133 |
-
- [x]
|
| 134 |
|
| 135 |
**Acceptance rule:**
|
| 136 |
|
| 137 |
- Local development/evaluation now prefers Ollama because quantized llama.cpp/Metal
|
| 138 |
inference is much faster on the Mac than local Transformers CPU/disk-offload.
|
| 139 |
-
- Current local parser
|
| 140 |
|
| 141 |
**Tasks:**
|
| 142 |
|
|
@@ -147,8 +147,9 @@ exists.
|
|
| 147 |
- [x] Fix parser keys and enum values through schema validation.
|
| 148 |
- [x] Validate model JSON into `ParsedCheckIn`.
|
| 149 |
- [x] Add optional local Transformers runtime wrapper.
|
| 150 |
-
- [x] Add parser backend selector for local Ollama vs Space
|
| 151 |
- [x] Add Ollama local runtime wrapper with JSON-schema structured outputs.
|
|
|
|
| 152 |
- [x] Run the model on one fixture and parse raw text into `ParsedCheckIn`.
|
| 153 |
- [x] Run the model against all acceptance fixtures.
|
| 154 |
- [x] Remove overfit fixture-shaped prompt examples.
|
|
@@ -328,7 +329,7 @@ progression have real history.
|
|
| 328 |
- [ ] Tiny Titan route.
|
| 329 |
- [ ] Research-reader.
|
| 330 |
- [ ] Fine-tuning.
|
| 331 |
-
- [
|
| 332 |
- [ ] Custom UI polish.
|
| 333 |
|
| 334 |
---
|
|
|
|
| 129 |
|
| 130 |
**Decision status:**
|
| 131 |
|
| 132 |
+
- [x] Choose local parser model: Ollama `qwen3:1.7B`.
|
| 133 |
+
- [x] Replace the Hugging Face Spaces Transformers parser with a GGUF llama.cpp path.
|
| 134 |
|
| 135 |
**Acceptance rule:**
|
| 136 |
|
| 137 |
- Local development/evaluation now prefers Ollama because quantized llama.cpp/Metal
|
| 138 |
inference is much faster on the Mac than local Transformers CPU/disk-offload.
|
| 139 |
+
- Current local parser default: `qwen3:1.7B`, chosen for MVP parser speed.
|
| 140 |
|
| 141 |
**Tasks:**
|
| 142 |
|
|
|
|
| 147 |
- [x] Fix parser keys and enum values through schema validation.
|
| 148 |
- [x] Validate model JSON into `ParsedCheckIn`.
|
| 149 |
- [x] Add optional local Transformers runtime wrapper.
|
| 150 |
+
- [x] Add parser backend selector for local Ollama vs Space llama.cpp runtime.
|
| 151 |
- [x] Add Ollama local runtime wrapper with JSON-schema structured outputs.
|
| 152 |
+
- [x] Add GGUF llama.cpp runtime wrapper for Hugging Face Spaces CPU deployment.
|
| 153 |
- [x] Run the model on one fixture and parse raw text into `ParsedCheckIn`.
|
| 154 |
- [x] Run the model against all acceptance fixtures.
|
| 155 |
- [x] Remove overfit fixture-shaped prompt examples.
|
|
|
|
| 329 |
- [ ] Tiny Titan route.
|
| 330 |
- [ ] Research-reader.
|
| 331 |
- [ ] Fine-tuning.
|
| 332 |
+
- [x] llama.cpp route.
|
| 333 |
- [ ] Custom UI polish.
|
| 334 |
|
| 335 |
---
|
README.md
CHANGED
|
@@ -11,7 +11,7 @@ pinned: false
|
|
| 11 |
license: apache-2.0
|
| 12 |
short_description: Small Models Big Adventures Hackaton - Weightlifting Trainer
|
| 13 |
preload_from_hub:
|
| 14 |
-
-
|
| 15 |
---
|
| 16 |
|
| 17 |
# Strength Training Coach
|
|
@@ -116,25 +116,31 @@ Local development defaults to Ollama:
|
|
| 116 |
|
| 117 |
```txt
|
| 118 |
PARSER_BACKEND=ollama
|
| 119 |
-
OLLAMA_MODEL=qwen3:
|
| 120 |
```
|
| 121 |
|
| 122 |
-
Hugging Face Spaces should use the
|
| 123 |
|
| 124 |
```txt
|
| 125 |
-
PARSER_BACKEND=
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
|
|
|
|
|
|
| 129 |
```
|
| 130 |
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
|
| 135 |
-
If `PARSER_BACKEND` is not set, the app chooses `
|
| 136 |
`SPACE_ID` environment variable exists, otherwise `ollama`.
|
| 137 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
## Rule Source
|
| 139 |
|
| 140 |
The canonical training-rule source is:
|
|
|
|
| 11 |
license: apache-2.0
|
| 12 |
short_description: Small Models Big Adventures Hackaton - Weightlifting Trainer
|
| 13 |
preload_from_hub:
|
| 14 |
+
- unsloth/Qwen3-1.7B-GGUF Qwen3-1.7B-Q4_K_M.gguf
|
| 15 |
---
|
| 16 |
|
| 17 |
# Strength Training Coach
|
|
|
|
| 116 |
|
| 117 |
```txt
|
| 118 |
PARSER_BACKEND=ollama
|
| 119 |
+
OLLAMA_MODEL=qwen3:1.7B
|
| 120 |
```
|
| 121 |
|
| 122 |
+
Hugging Face Spaces should use the GGUF llama.cpp runtime:
|
| 123 |
|
| 124 |
```txt
|
| 125 |
+
PARSER_BACKEND=llama_cpp
|
| 126 |
+
LLAMA_CPP_MODEL_REPO=unsloth/Qwen3-1.7B-GGUF
|
| 127 |
+
LLAMA_CPP_MODEL_FILE=Qwen3-1.7B-Q4_K_M.gguf
|
| 128 |
+
LLAMA_CPP_MAX_TOKENS=384
|
| 129 |
+
LLAMA_CPP_N_CTX=2048
|
| 130 |
+
LLAMA_CPP_N_THREADS=2
|
| 131 |
```
|
| 132 |
|
| 133 |
+
The Space README frontmatter preloads only the selected GGUF file at build time so the
|
| 134 |
+
app does not need to download model weights during the first parser request. Set
|
| 135 |
+
`LLAMA_CPP_N_THREADS` to the Space CPU thread count.
|
| 136 |
|
| 137 |
+
If `PARSER_BACKEND` is not set, the app chooses `llama_cpp` when the Hugging Face
|
| 138 |
`SPACE_ID` environment variable exists, otherwise `ollama`.
|
| 139 |
|
| 140 |
+
The old Transformers parser backend still exists for explicit local experiments, but it
|
| 141 |
+
is not installed by the default deployment requirements and is no longer the Space
|
| 142 |
+
runtime path.
|
| 143 |
+
|
| 144 |
## Rule Source
|
| 145 |
|
| 146 |
The canonical training-rule source is:
|
requirements.txt
CHANGED
|
@@ -1,7 +1,4 @@
|
|
| 1 |
gradio==6.17.3
|
| 2 |
huggingface_hub>=0.36.0
|
| 3 |
pydantic>=2.11.10,<2.13
|
| 4 |
-
|
| 5 |
-
spaces>=0.35.0
|
| 6 |
-
torch>=2.8.0
|
| 7 |
-
transformers>=4.48.0
|
|
|
|
| 1 |
gradio==6.17.3
|
| 2 |
huggingface_hub>=0.36.0
|
| 3 |
pydantic>=2.11.10,<2.13
|
| 4 |
+
llama-cpp-python>=0.3.16
|
|
|
|
|
|
|
|
|
tests/test_env.py
CHANGED
|
@@ -15,8 +15,8 @@ class EnvTests(unittest.TestCase):
|
|
| 15 |
"\n".join(
|
| 16 |
[
|
| 17 |
"# comment",
|
| 18 |
-
"PARSER_BACKEND=
|
| 19 |
-
"OLLAMA_MODEL='qwen3:
|
| 20 |
"EXISTING=value-from-file",
|
| 21 |
]
|
| 22 |
)
|
|
@@ -25,8 +25,8 @@ class EnvTests(unittest.TestCase):
|
|
| 25 |
with patch.dict(os.environ, {"EXISTING": "value-from-env"}, clear=True):
|
| 26 |
load_local_env(env_path)
|
| 27 |
|
| 28 |
-
self.assertEqual(os.environ["PARSER_BACKEND"], "
|
| 29 |
-
self.assertEqual(os.environ["OLLAMA_MODEL"], "qwen3:
|
| 30 |
self.assertEqual(os.environ["EXISTING"], "value-from-env")
|
| 31 |
|
| 32 |
|
|
|
|
| 15 |
"\n".join(
|
| 16 |
[
|
| 17 |
"# comment",
|
| 18 |
+
"PARSER_BACKEND=llama_cpp",
|
| 19 |
+
"OLLAMA_MODEL='qwen3:1.7B'",
|
| 20 |
"EXISTING=value-from-file",
|
| 21 |
]
|
| 22 |
)
|
|
|
|
| 25 |
with patch.dict(os.environ, {"EXISTING": "value-from-env"}, clear=True):
|
| 26 |
load_local_env(env_path)
|
| 27 |
|
| 28 |
+
self.assertEqual(os.environ["PARSER_BACKEND"], "llama_cpp")
|
| 29 |
+
self.assertEqual(os.environ["OLLAMA_MODEL"], "qwen3:1.7B")
|
| 30 |
self.assertEqual(os.environ["EXISTING"], "value-from-env")
|
| 31 |
|
| 32 |
|
tests/test_parser_llama_cpp.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import unittest
|
| 3 |
+
from unittest.mock import patch
|
| 4 |
+
|
| 5 |
+
from training_coach.parser_llama_cpp import (
|
| 6 |
+
DEFAULT_LLAMA_CPP_MAX_TOKENS,
|
| 7 |
+
DEFAULT_LLAMA_CPP_MODEL_FILE,
|
| 8 |
+
DEFAULT_LLAMA_CPP_MODEL_REPO,
|
| 9 |
+
DEFAULT_LLAMA_CPP_N_CTX,
|
| 10 |
+
LlamaCppRuntimeUnavailableError,
|
| 11 |
+
_load_llama_cpp,
|
| 12 |
+
generate_parser_response_llama_cpp,
|
| 13 |
+
llama_cpp_response_grammar,
|
| 14 |
+
load_llama_cpp_model,
|
| 15 |
+
parse_check_in_with_llama_cpp,
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class FakeLlama:
|
| 20 |
+
from_pretrained_calls = []
|
| 21 |
+
completion_calls = []
|
| 22 |
+
|
| 23 |
+
@classmethod
|
| 24 |
+
def from_pretrained(cls, **kwargs):
|
| 25 |
+
cls.from_pretrained_calls.append(kwargs)
|
| 26 |
+
return cls()
|
| 27 |
+
|
| 28 |
+
def create_chat_completion(self, **kwargs):
|
| 29 |
+
self.completion_calls.append(kwargs)
|
| 30 |
+
return {
|
| 31 |
+
"choices": [
|
| 32 |
+
{
|
| 33 |
+
"message": {
|
| 34 |
+
"content": json.dumps(
|
| 35 |
+
{
|
| 36 |
+
"check_in": {
|
| 37 |
+
"raw_text": "60 min",
|
| 38 |
+
"time_available_minutes": 60,
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
)
|
| 42 |
+
}
|
| 43 |
+
}
|
| 44 |
+
]
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class FakeGrammar:
|
| 49 |
+
schemas = []
|
| 50 |
+
|
| 51 |
+
@classmethod
|
| 52 |
+
def from_json_schema(cls, schema, verbose=True):
|
| 53 |
+
cls.schemas.append((schema, verbose))
|
| 54 |
+
return {"schema": schema, "verbose": verbose}
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
class LlamaCppParserTest(unittest.TestCase):
|
| 58 |
+
def setUp(self):
|
| 59 |
+
load_llama_cpp_model.cache_clear()
|
| 60 |
+
llama_cpp_response_grammar.cache_clear()
|
| 61 |
+
FakeLlama.from_pretrained_calls = []
|
| 62 |
+
FakeLlama.completion_calls = []
|
| 63 |
+
FakeGrammar.schemas = []
|
| 64 |
+
|
| 65 |
+
def tearDown(self):
|
| 66 |
+
load_llama_cpp_model.cache_clear()
|
| 67 |
+
llama_cpp_response_grammar.cache_clear()
|
| 68 |
+
|
| 69 |
+
def test_llama_cpp_defaults_are_locked(self):
|
| 70 |
+
self.assertEqual(DEFAULT_LLAMA_CPP_MODEL_REPO, "unsloth/Qwen3-1.7B-GGUF")
|
| 71 |
+
self.assertEqual(DEFAULT_LLAMA_CPP_MODEL_FILE, "Qwen3-1.7B-Q4_K_M.gguf")
|
| 72 |
+
self.assertEqual(DEFAULT_LLAMA_CPP_MAX_TOKENS, 384)
|
| 73 |
+
self.assertEqual(DEFAULT_LLAMA_CPP_N_CTX, 2048)
|
| 74 |
+
|
| 75 |
+
def test_runtime_can_import_llama_cpp_or_reports_clear_error(self):
|
| 76 |
+
try:
|
| 77 |
+
loaded = _load_llama_cpp()
|
| 78 |
+
except LlamaCppRuntimeUnavailableError as error:
|
| 79 |
+
self.assertIn("Install llama-cpp-python", str(error))
|
| 80 |
+
else:
|
| 81 |
+
self.assertEqual(len(loaded), 2)
|
| 82 |
+
|
| 83 |
+
def test_load_model_uses_repo_file_and_runtime_settings(self):
|
| 84 |
+
with patch(
|
| 85 |
+
"training_coach.parser_llama_cpp._load_llama_cpp",
|
| 86 |
+
return_value=(FakeLlama, FakeGrammar),
|
| 87 |
+
):
|
| 88 |
+
load_llama_cpp_model(
|
| 89 |
+
repo_id="repo/model",
|
| 90 |
+
filename="model.gguf",
|
| 91 |
+
n_ctx=4096,
|
| 92 |
+
n_threads=2,
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
self.assertEqual(
|
| 96 |
+
FakeLlama.from_pretrained_calls,
|
| 97 |
+
[
|
| 98 |
+
{
|
| 99 |
+
"repo_id": "repo/model",
|
| 100 |
+
"filename": "model.gguf",
|
| 101 |
+
"n_ctx": 4096,
|
| 102 |
+
"verbose": False,
|
| 103 |
+
"n_threads": 2,
|
| 104 |
+
}
|
| 105 |
+
],
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
def test_generate_parser_response_uses_schema_grammar(self):
|
| 109 |
+
with patch(
|
| 110 |
+
"training_coach.parser_llama_cpp._load_llama_cpp",
|
| 111 |
+
return_value=(FakeLlama, FakeGrammar),
|
| 112 |
+
):
|
| 113 |
+
response = generate_parser_response_llama_cpp(
|
| 114 |
+
"60 min",
|
| 115 |
+
repo_id="repo/model",
|
| 116 |
+
filename="model.gguf",
|
| 117 |
+
max_tokens=128,
|
| 118 |
+
n_ctx=2048,
|
| 119 |
+
n_threads=None,
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
completion_call = FakeLlama.completion_calls[0]
|
| 123 |
+
self.assertEqual(completion_call["max_tokens"], 128)
|
| 124 |
+
self.assertEqual(completion_call["temperature"], 0)
|
| 125 |
+
self.assertEqual(completion_call["grammar"]["verbose"], False)
|
| 126 |
+
self.assertEqual(FakeGrammar.schemas[0][1], False)
|
| 127 |
+
self.assertIn("ParsedCheckIn", FakeGrammar.schemas[0][0])
|
| 128 |
+
self.assertIn("60 min", completion_call["messages"][1]["content"])
|
| 129 |
+
self.assertIn("check_in", response)
|
| 130 |
+
|
| 131 |
+
def test_parse_check_in_with_llama_cpp_reads_env_and_validates_response(self):
|
| 132 |
+
with patch.dict(
|
| 133 |
+
"os.environ",
|
| 134 |
+
{
|
| 135 |
+
"LLAMA_CPP_MODEL_REPO": "repo/model",
|
| 136 |
+
"LLAMA_CPP_MODEL_FILE": "model.gguf",
|
| 137 |
+
"LLAMA_CPP_MAX_TOKENS": "128",
|
| 138 |
+
"LLAMA_CPP_N_CTX": "4096",
|
| 139 |
+
"LLAMA_CPP_N_THREADS": "2",
|
| 140 |
+
},
|
| 141 |
+
clear=True,
|
| 142 |
+
), patch(
|
| 143 |
+
"training_coach.parser_llama_cpp._load_llama_cpp",
|
| 144 |
+
return_value=(FakeLlama, FakeGrammar),
|
| 145 |
+
):
|
| 146 |
+
parsed = parse_check_in_with_llama_cpp("60 min")
|
| 147 |
+
|
| 148 |
+
self.assertEqual(parsed.check_in.time_available_minutes, 60)
|
| 149 |
+
self.assertEqual(FakeLlama.from_pretrained_calls[0]["repo_id"], "repo/model")
|
| 150 |
+
self.assertEqual(FakeLlama.from_pretrained_calls[0]["filename"], "model.gguf")
|
| 151 |
+
self.assertEqual(FakeLlama.from_pretrained_calls[0]["n_ctx"], 4096)
|
| 152 |
+
self.assertEqual(FakeLlama.from_pretrained_calls[0]["n_threads"], 2)
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
if __name__ == "__main__":
|
| 156 |
+
unittest.main()
|
tests/test_parser_ollama.py
CHANGED
|
@@ -27,7 +27,7 @@ class _FakeResponse:
|
|
| 27 |
|
| 28 |
class OllamaParserTest(unittest.TestCase):
|
| 29 |
def test_ollama_model_choices_are_locked(self):
|
| 30 |
-
self.assertEqual(OLLAMA_MODEL, "qwen3:
|
| 31 |
self.assertEqual(OLLAMA_TINY_CANDIDATE, "qwen3:4b")
|
| 32 |
|
| 33 |
def test_build_ollama_prompt_includes_schema_and_check_in(self):
|
|
@@ -66,7 +66,7 @@ class OllamaParserTest(unittest.TestCase):
|
|
| 66 |
|
| 67 |
sent_request = urlopen.call_args.args[0]
|
| 68 |
body = json.loads(sent_request.data.decode("utf-8"))
|
| 69 |
-
self.assertEqual(body["model"], "qwen3:
|
| 70 |
self.assertEqual(body["stream"], False)
|
| 71 |
self.assertEqual(body["think"], False)
|
| 72 |
self.assertEqual(body["format"]["title"], "ParsedCheckIn")
|
|
|
|
| 27 |
|
| 28 |
class OllamaParserTest(unittest.TestCase):
|
| 29 |
def test_ollama_model_choices_are_locked(self):
|
| 30 |
+
self.assertEqual(OLLAMA_MODEL, "qwen3:1.7B")
|
| 31 |
self.assertEqual(OLLAMA_TINY_CANDIDATE, "qwen3:4b")
|
| 32 |
|
| 33 |
def test_build_ollama_prompt_includes_schema_and_check_in(self):
|
|
|
|
| 66 |
|
| 67 |
sent_request = urlopen.call_args.args[0]
|
| 68 |
body = json.loads(sent_request.data.decode("utf-8"))
|
| 69 |
+
self.assertEqual(body["model"], "qwen3:1.7B")
|
| 70 |
self.assertEqual(body["stream"], False)
|
| 71 |
self.assertEqual(body["think"], False)
|
| 72 |
self.assertEqual(body["format"]["title"], "ParsedCheckIn")
|
tests/test_parser_service.py
CHANGED
|
@@ -12,7 +12,7 @@ class ParserServiceTest(unittest.TestCase):
|
|
| 12 |
|
| 13 |
def test_parser_backend_defaults_to_transformers_on_space(self):
|
| 14 |
with patch.dict("os.environ", {"SPACE_ID": "LucasMah/app"}, clear=True):
|
| 15 |
-
self.assertEqual(parser_backend(), "
|
| 16 |
|
| 17 |
def test_parser_backend_env_override_wins(self):
|
| 18 |
with patch.dict(
|
|
@@ -33,7 +33,20 @@ class ParserServiceTest(unittest.TestCase):
|
|
| 33 |
result = parse_check_in_with_configured_backend("60 min")
|
| 34 |
|
| 35 |
self.assertEqual(result, parsed)
|
| 36 |
-
parse_ollama.assert_called_once_with("60 min", model_name="qwen3:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
def test_configured_parser_uses_transformers_backend(self):
|
| 39 |
parsed = ParsedCheckIn(check_in=CheckIn(raw_text="60 min"))
|
|
|
|
| 12 |
|
| 13 |
def test_parser_backend_defaults_to_transformers_on_space(self):
|
| 14 |
with patch.dict("os.environ", {"SPACE_ID": "LucasMah/app"}, clear=True):
|
| 15 |
+
self.assertEqual(parser_backend(), "llama_cpp")
|
| 16 |
|
| 17 |
def test_parser_backend_env_override_wins(self):
|
| 18 |
with patch.dict(
|
|
|
|
| 33 |
result = parse_check_in_with_configured_backend("60 min")
|
| 34 |
|
| 35 |
self.assertEqual(result, parsed)
|
| 36 |
+
parse_ollama.assert_called_once_with("60 min", model_name="qwen3:1.7B")
|
| 37 |
+
|
| 38 |
+
def test_configured_parser_uses_llama_cpp_backend(self):
|
| 39 |
+
parsed = ParsedCheckIn(check_in=CheckIn(raw_text="60 min"))
|
| 40 |
+
|
| 41 |
+
with patch.dict("os.environ", {"PARSER_BACKEND": "llama_cpp"}, clear=True):
|
| 42 |
+
with patch(
|
| 43 |
+
"training_coach.parser_service.parse_check_in_with_llama_cpp",
|
| 44 |
+
return_value=parsed,
|
| 45 |
+
) as parse_llama_cpp:
|
| 46 |
+
result = parse_check_in_with_configured_backend("60 min")
|
| 47 |
+
|
| 48 |
+
self.assertEqual(result, parsed)
|
| 49 |
+
parse_llama_cpp.assert_called_once_with("60 min")
|
| 50 |
|
| 51 |
def test_configured_parser_uses_transformers_backend(self):
|
| 52 |
parsed = ParsedCheckIn(check_in=CheckIn(raw_text="60 min"))
|
training_coach/parser_llama_cpp.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from functools import lru_cache
|
| 2 |
+
import json
|
| 3 |
+
import logging
|
| 4 |
+
import os
|
| 5 |
+
from time import perf_counter
|
| 6 |
+
|
| 7 |
+
from training_coach.models import ParsedCheckIn
|
| 8 |
+
from training_coach.parser import (
|
| 9 |
+
build_parser_messages,
|
| 10 |
+
expected_response_format,
|
| 11 |
+
log_parser_messages,
|
| 12 |
+
log_parser_response_text,
|
| 13 |
+
parse_model_response,
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
DEFAULT_LLAMA_CPP_MODEL_REPO = "unsloth/Qwen3-1.7B-GGUF"
|
| 18 |
+
DEFAULT_LLAMA_CPP_MODEL_FILE = "Qwen3-1.7B-Q4_K_M.gguf"
|
| 19 |
+
DEFAULT_LLAMA_CPP_MAX_TOKENS = 384
|
| 20 |
+
DEFAULT_LLAMA_CPP_N_CTX = 2048
|
| 21 |
+
logger = logging.getLogger(__name__)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class LlamaCppRuntimeUnavailableError(RuntimeError):
|
| 25 |
+
pass
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def _load_llama_cpp():
|
| 29 |
+
try:
|
| 30 |
+
from llama_cpp import Llama, LlamaGrammar
|
| 31 |
+
except ImportError as error:
|
| 32 |
+
raise LlamaCppRuntimeUnavailableError(
|
| 33 |
+
"Install llama-cpp-python to run the GGUF parser backend."
|
| 34 |
+
) from error
|
| 35 |
+
|
| 36 |
+
return Llama, LlamaGrammar
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def _optional_int_env(name: str) -> int | None:
|
| 40 |
+
raw_value = os.getenv(name, "").strip()
|
| 41 |
+
if not raw_value:
|
| 42 |
+
return None
|
| 43 |
+
return int(raw_value)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
@lru_cache(maxsize=1)
|
| 47 |
+
def load_llama_cpp_model(
|
| 48 |
+
repo_id: str = DEFAULT_LLAMA_CPP_MODEL_REPO,
|
| 49 |
+
filename: str = DEFAULT_LLAMA_CPP_MODEL_FILE,
|
| 50 |
+
n_ctx: int = DEFAULT_LLAMA_CPP_N_CTX,
|
| 51 |
+
n_threads: int | None = None,
|
| 52 |
+
):
|
| 53 |
+
start_time = perf_counter()
|
| 54 |
+
logger.info(
|
| 55 |
+
"event=parser_llama_cpp_model_load_start repo=%s file=%s n_ctx=%s n_threads=%s",
|
| 56 |
+
repo_id,
|
| 57 |
+
filename,
|
| 58 |
+
n_ctx,
|
| 59 |
+
n_threads,
|
| 60 |
+
)
|
| 61 |
+
Llama, _LlamaGrammar = _load_llama_cpp()
|
| 62 |
+
kwargs = {
|
| 63 |
+
"repo_id": repo_id,
|
| 64 |
+
"filename": filename,
|
| 65 |
+
"n_ctx": n_ctx,
|
| 66 |
+
"verbose": False,
|
| 67 |
+
}
|
| 68 |
+
if n_threads is not None:
|
| 69 |
+
kwargs["n_threads"] = n_threads
|
| 70 |
+
|
| 71 |
+
model = Llama.from_pretrained(**kwargs)
|
| 72 |
+
logger.info(
|
| 73 |
+
"event=parser_llama_cpp_model_load_complete repo=%s file=%s elapsed_ms=%s",
|
| 74 |
+
repo_id,
|
| 75 |
+
filename,
|
| 76 |
+
round((perf_counter() - start_time) * 1000),
|
| 77 |
+
)
|
| 78 |
+
return model
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
@lru_cache(maxsize=1)
|
| 82 |
+
def llama_cpp_response_grammar():
|
| 83 |
+
_Llama, LlamaGrammar = _load_llama_cpp()
|
| 84 |
+
return LlamaGrammar.from_json_schema(
|
| 85 |
+
json.dumps(expected_response_format()),
|
| 86 |
+
verbose=False,
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def generate_parser_response_llama_cpp(
|
| 91 |
+
raw_text: str,
|
| 92 |
+
*,
|
| 93 |
+
repo_id: str = DEFAULT_LLAMA_CPP_MODEL_REPO,
|
| 94 |
+
filename: str = DEFAULT_LLAMA_CPP_MODEL_FILE,
|
| 95 |
+
max_tokens: int = DEFAULT_LLAMA_CPP_MAX_TOKENS,
|
| 96 |
+
n_ctx: int = DEFAULT_LLAMA_CPP_N_CTX,
|
| 97 |
+
n_threads: int | None = None,
|
| 98 |
+
) -> str:
|
| 99 |
+
start_time = perf_counter()
|
| 100 |
+
messages = build_parser_messages(raw_text)
|
| 101 |
+
logger.info(
|
| 102 |
+
"event=parser_llama_cpp_generate_start repo=%s file=%s text_chars=%s max_tokens=%s",
|
| 103 |
+
repo_id,
|
| 104 |
+
filename,
|
| 105 |
+
len(raw_text),
|
| 106 |
+
max_tokens,
|
| 107 |
+
)
|
| 108 |
+
log_parser_messages(
|
| 109 |
+
backend="llama_cpp",
|
| 110 |
+
model_name=f"{repo_id}/{filename}",
|
| 111 |
+
messages=messages,
|
| 112 |
+
)
|
| 113 |
+
model = load_llama_cpp_model(
|
| 114 |
+
repo_id=repo_id,
|
| 115 |
+
filename=filename,
|
| 116 |
+
n_ctx=n_ctx,
|
| 117 |
+
n_threads=n_threads,
|
| 118 |
+
)
|
| 119 |
+
response = model.create_chat_completion(
|
| 120 |
+
messages=messages,
|
| 121 |
+
max_tokens=max_tokens,
|
| 122 |
+
temperature=0,
|
| 123 |
+
grammar=llama_cpp_response_grammar(),
|
| 124 |
+
)
|
| 125 |
+
response_text = response["choices"][0]["message"]["content"].strip()
|
| 126 |
+
logger.info(
|
| 127 |
+
"event=parser_llama_cpp_generate_complete repo=%s file=%s "
|
| 128 |
+
"response_chars=%s elapsed_ms=%s",
|
| 129 |
+
repo_id,
|
| 130 |
+
filename,
|
| 131 |
+
len(response_text),
|
| 132 |
+
round((perf_counter() - start_time) * 1000),
|
| 133 |
+
)
|
| 134 |
+
log_parser_response_text(
|
| 135 |
+
backend="llama_cpp",
|
| 136 |
+
model_name=f"{repo_id}/{filename}",
|
| 137 |
+
response_text=response_text,
|
| 138 |
+
)
|
| 139 |
+
return response_text
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def parse_check_in_with_llama_cpp(raw_text: str) -> ParsedCheckIn:
|
| 143 |
+
repo_id = os.getenv("LLAMA_CPP_MODEL_REPO", DEFAULT_LLAMA_CPP_MODEL_REPO)
|
| 144 |
+
filename = os.getenv("LLAMA_CPP_MODEL_FILE", DEFAULT_LLAMA_CPP_MODEL_FILE)
|
| 145 |
+
max_tokens = int(os.getenv("LLAMA_CPP_MAX_TOKENS", str(DEFAULT_LLAMA_CPP_MAX_TOKENS)))
|
| 146 |
+
n_ctx = int(os.getenv("LLAMA_CPP_N_CTX", str(DEFAULT_LLAMA_CPP_N_CTX)))
|
| 147 |
+
n_threads = _optional_int_env("LLAMA_CPP_N_THREADS")
|
| 148 |
+
response_text = generate_parser_response_llama_cpp(
|
| 149 |
+
raw_text,
|
| 150 |
+
repo_id=repo_id,
|
| 151 |
+
filename=filename,
|
| 152 |
+
max_tokens=max_tokens,
|
| 153 |
+
n_ctx=n_ctx,
|
| 154 |
+
n_threads=n_threads,
|
| 155 |
+
)
|
| 156 |
+
return parse_model_response(response_text)
|
training_coach/parser_ollama.py
CHANGED
|
@@ -14,7 +14,7 @@ from training_coach.models import ParsedCheckIn
|
|
| 14 |
|
| 15 |
|
| 16 |
OLLAMA_URL = "http://127.0.0.1:11434/api/generate"
|
| 17 |
-
OLLAMA_MODEL = "qwen3:
|
| 18 |
OLLAMA_TINY_CANDIDATE = "qwen3:4b"
|
| 19 |
logger = logging.getLogger(__name__)
|
| 20 |
|
|
|
|
| 14 |
|
| 15 |
|
| 16 |
OLLAMA_URL = "http://127.0.0.1:11434/api/generate"
|
| 17 |
+
OLLAMA_MODEL = "qwen3:1.7B"
|
| 18 |
OLLAMA_TINY_CANDIDATE = "qwen3:4b"
|
| 19 |
logger = logging.getLogger(__name__)
|
| 20 |
|
training_coach/parser_service.py
CHANGED
|
@@ -2,12 +2,14 @@ import logging
|
|
| 2 |
import os
|
| 3 |
|
| 4 |
from training_coach.models import ParsedCheckIn
|
|
|
|
| 5 |
from training_coach.parser_ollama import parse_check_in_with_ollama
|
| 6 |
from training_coach.parser_runtime import parse_check_in_with_model
|
| 7 |
|
| 8 |
|
| 9 |
DEFAULT_LOCAL_BACKEND = "ollama"
|
| 10 |
-
DEFAULT_SPACE_BACKEND = "
|
|
|
|
| 11 |
logger = logging.getLogger(__name__)
|
| 12 |
|
| 13 |
|
|
@@ -30,7 +32,7 @@ def parse_check_in_with_configured_backend(raw_text: str) -> ParsedCheckIn:
|
|
| 30 |
if backend == "ollama":
|
| 31 |
parsed = parse_check_in_with_ollama(
|
| 32 |
raw_text,
|
| 33 |
-
model_name=os.getenv("OLLAMA_MODEL",
|
| 34 |
)
|
| 35 |
logger.info(
|
| 36 |
"event=parser_complete backend=%s missing_fields=%s follow_up_questions=%s",
|
|
@@ -39,6 +41,15 @@ def parse_check_in_with_configured_backend(raw_text: str) -> ParsedCheckIn:
|
|
| 39 |
len(parsed.follow_up_questions),
|
| 40 |
)
|
| 41 |
return parsed
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
if backend == "transformers":
|
| 43 |
parsed = parse_check_in_with_model(raw_text)
|
| 44 |
logger.info(
|
|
@@ -50,6 +61,5 @@ def parse_check_in_with_configured_backend(raw_text: str) -> ParsedCheckIn:
|
|
| 50 |
return parsed
|
| 51 |
logger.error("event=parser_unsupported_backend backend=%s", backend)
|
| 52 |
raise ValueError(
|
| 53 |
-
"Unsupported PARSER_BACKEND. Use 'ollama'
|
| 54 |
-
"'transformers' for Hugging Face Spaces."
|
| 55 |
)
|
|
|
|
| 2 |
import os
|
| 3 |
|
| 4 |
from training_coach.models import ParsedCheckIn
|
| 5 |
+
from training_coach.parser_llama_cpp import parse_check_in_with_llama_cpp
|
| 6 |
from training_coach.parser_ollama import parse_check_in_with_ollama
|
| 7 |
from training_coach.parser_runtime import parse_check_in_with_model
|
| 8 |
|
| 9 |
|
| 10 |
DEFAULT_LOCAL_BACKEND = "ollama"
|
| 11 |
+
DEFAULT_SPACE_BACKEND = "llama_cpp"
|
| 12 |
+
DEFAULT_OLLAMA_MODEL = "qwen3:1.7B"
|
| 13 |
logger = logging.getLogger(__name__)
|
| 14 |
|
| 15 |
|
|
|
|
| 32 |
if backend == "ollama":
|
| 33 |
parsed = parse_check_in_with_ollama(
|
| 34 |
raw_text,
|
| 35 |
+
model_name=os.getenv("OLLAMA_MODEL", DEFAULT_OLLAMA_MODEL),
|
| 36 |
)
|
| 37 |
logger.info(
|
| 38 |
"event=parser_complete backend=%s missing_fields=%s follow_up_questions=%s",
|
|
|
|
| 41 |
len(parsed.follow_up_questions),
|
| 42 |
)
|
| 43 |
return parsed
|
| 44 |
+
if backend == "llama_cpp":
|
| 45 |
+
parsed = parse_check_in_with_llama_cpp(raw_text)
|
| 46 |
+
logger.info(
|
| 47 |
+
"event=parser_complete backend=%s missing_fields=%s follow_up_questions=%s",
|
| 48 |
+
backend,
|
| 49 |
+
len(parsed.missing_fields),
|
| 50 |
+
len(parsed.follow_up_questions),
|
| 51 |
+
)
|
| 52 |
+
return parsed
|
| 53 |
if backend == "transformers":
|
| 54 |
parsed = parse_check_in_with_model(raw_text)
|
| 55 |
logger.info(
|
|
|
|
| 61 |
return parsed
|
| 62 |
logger.error("event=parser_unsupported_backend backend=%s", backend)
|
| 63 |
raise ValueError(
|
| 64 |
+
"Unsupported PARSER_BACKEND. Use 'ollama', 'llama_cpp', or 'transformers'."
|
|
|
|
| 65 |
)
|