feat(advice): LLM brewing advice on every observation (work in progress)
Stores a model's advice for each recorded reading, in its own field, so the brewer's own comment is never overwritten. Fire and forget: the advice is generated in a background thread, so recording a reading never blocks on the model. A failure is recorded as a visible note saying the advice could not be generated, because a silent failure is indistinguishable from "still thinking". The model is `glm-5.3:cloud` on the Ollama instance at 192.168.178.45:11434. Several models on that instance are retired and fail at call time while still appearing in the model list, so the default is one that was probed. The system prompt tells the model never to call a reading impossible: a gravity below 1.000 is always a mis-entry, but the rest of the run may be a genuine long fermentation and still worth understanding. 150 tests.
This commit is contained in:
@@ -188,6 +188,27 @@ class BeerRepository:
|
||||
existing = self.readings.find_one(query)
|
||||
return str(existing.get("_id")) if existing else ""
|
||||
|
||||
def set_reading_advice(self, reading_id: str, advice: str,
|
||||
ok: bool = True) -> int:
|
||||
"""Attach the LLM's advice to a reading.
|
||||
|
||||
The advice lives in its own field, so the brewer's own comment is
|
||||
never overwritten. ``ok`` says whether the advice was actually
|
||||
generated or is a note explaining that it could not be.
|
||||
"""
|
||||
doc_id = _as_doc_id(reading_id)
|
||||
if doc_id is None:
|
||||
return 0
|
||||
result = self.readings.update_one(
|
||||
{"_id": doc_id},
|
||||
{"$set": {
|
||||
"advice": str(advice),
|
||||
"advice_ok": bool(ok),
|
||||
"advice_at": _now_iso(),
|
||||
}},
|
||||
)
|
||||
return result.modified_count
|
||||
|
||||
def set_final_reading(self, beer_run_id: str, reading_date: str,
|
||||
final: bool) -> int:
|
||||
"""Flag (or unflag) a reading as the final reading of its run."""
|
||||
@@ -314,6 +335,28 @@ def _as_id(value: object) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
def _as_doc_id(value: object):
|
||||
"""Coerce a reading document id to something Mongo will match.
|
||||
|
||||
The ``_id`` of a reading is an ``ObjectId``, not a run id, so the
|
||||
``_as_id`` coercion does not apply - it would turn the ObjectId into
|
||||
an empty string and the update would silently match nothing.
|
||||
|
||||
Only an ObjectId or a plain string is accepted, so a dict can never
|
||||
be injected as a query operator.
|
||||
"""
|
||||
from bson import ObjectId
|
||||
|
||||
if isinstance(value, ObjectId):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
return ObjectId(value)
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _as_date(value: object) -> str:
|
||||
"""Coerce a reading date to a plain ``YYYY-MM-DD`` string."""
|
||||
if isinstance(value, str):
|
||||
|
||||
Reference in New Issue
Block a user