Spaces:
Running
Running
| """Step 2 of the living loop: appraisal (F2). | |
| Calls the text model (MiniCPM5-1B via model/client.py) with a small GBNF | |
| grammar (engine/grammars/appraisal.gbnf) that guarantees a valid, minimal | |
| JSON object describing how the last exchange should nudge Daimon's state | |
| vector. The model PROPOSES signals; engine/mapping.py and | |
| engine/spec_bridge.py turn them into clamped, audited mutations - the model | |
| never writes state.json directly. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import sys | |
| from pathlib import Path | |
| REPO_ROOT = Path(__file__).resolve().parent.parent | |
| if str(REPO_ROOT) not in sys.path: | |
| sys.path.insert(0, str(REPO_ROOT)) | |
| from model.client import chat # noqa: E402 | |
| GRAMMAR_PATH = Path(__file__).resolve().parent / "grammars" / "appraisal.gbnf" | |
| _GRAMMAR = GRAMMAR_PATH.read_text(encoding="utf-8") | |
| _SYSTEM_PROMPT = ( | |
| "You are an appraisal module for a persona named Daimon. Given the user's " | |
| "last message and Daimon's reply, output ONLY a JSON object describing how " | |
| "the exchange should nudge Daimon's internal state. Fields:\n" | |
| "- sentiment: overall tone of the user's message, from -1.0 (hostile/negative) " | |
| "to 1.0 (warm/positive), in steps of 0.5.\n" | |
| "- engagement: how exploratory/engaged the exchange is, 0.0 (flat/closing) to " | |
| "1.0 (curious/exploratory), in steps of 0.25.\n" | |
| "- correction: true if the user explicitly asked Daimon to change how it acts " | |
| "(its tone, talkativeness, openness, or agreeableness), false otherwise.\n" | |
| "- target: which trait the correction is about - one of tone, openness, " | |
| "extraversion, agreeableness, none.\n" | |
| "- direction: -1 if the user wants less of that trait, 1 if more, 0 if there " | |
| "is no correction.\n" | |
| "- reason: a short (under 12 words) plain-text reason for this reading.\n" | |
| ) | |
| _NEUTRAL: dict = { | |
| "sentiment": 0.0, | |
| "engagement": 0.0, | |
| "correction": False, | |
| "target": "none", | |
| "direction": 0, | |
| "reason": "appraisal output unparsable, defaulting to neutral", | |
| } | |
| _VALID_TARGETS = {"tone", "openness", "extraversion", "agreeableness", "none"} | |
| def appraise(user_message: str, persona_reply: str, *, max_tokens: int = 200) -> dict: | |
| """Return a validated appraisal dict, falling back to a neutral reading if | |
| the model's output cannot be parsed.""" | |
| messages = [ | |
| {"role": "system", "content": _SYSTEM_PROMPT}, | |
| { | |
| "role": "user", | |
| "content": ( | |
| f"User said: {user_message!r}\nDaimon replied: {persona_reply!r}\n" | |
| "Output the JSON object now." | |
| ), | |
| }, | |
| ] | |
| raw = chat( | |
| messages, | |
| modality="text", | |
| max_tokens=max_tokens, | |
| temperature=0.1, | |
| enable_thinking=False, # grammar-constrained JSON; reasoning tokens would eat max_tokens | |
| extra_body={"grammar": _GRAMMAR}, | |
| ) | |
| return _parse(raw) | |
| def _parse(raw: str) -> dict: | |
| try: | |
| data = json.loads(raw) | |
| except (json.JSONDecodeError, TypeError): | |
| return dict(_NEUTRAL) | |
| out = dict(_NEUTRAL) | |
| if isinstance(data.get("sentiment"), (int, float)): | |
| out["sentiment"] = max(-1.0, min(1.0, float(data["sentiment"]))) | |
| if isinstance(data.get("engagement"), (int, float)): | |
| out["engagement"] = max(0.0, min(1.0, float(data["engagement"]))) | |
| if isinstance(data.get("correction"), bool): | |
| out["correction"] = data["correction"] | |
| if data.get("target") in _VALID_TARGETS: | |
| out["target"] = data["target"] | |
| if data.get("direction") in (-1, 0, 1): | |
| out["direction"] = data["direction"] | |
| if isinstance(data.get("reason"), str) and data["reason"].strip(): | |
| out["reason"] = data["reason"].strip()[:120] | |
| return out | |
| if __name__ == "__main__": | |
| sys.stdout.reconfigure(encoding="utf-8") | |
| result = appraise( | |
| "Wow, that's such a cool way to put it, tell me more!", | |
| "Glad you liked that! There's a lot more to explore here...", | |
| ) | |
| print(result) | |