| from __future__ import annotations |
|
|
| import hashlib |
| import hmac |
| import json |
| from collections.abc import Callable |
| from typing import Any, Protocol |
| from urllib.parse import urlparse |
|
|
| import httpx |
| from huggingface_hub import InferenceClient |
|
|
| from compliment_forest.prompts import ( |
| author_messages, |
| critic_messages, |
| intake_messages, |
| planner_messages, |
| ) |
|
|
|
|
| def sign_modal_text_request( |
| key: str, |
| messages: list[dict[str, str]], |
| *, |
| max_new_tokens: int, |
| temperature: float, |
| top_p: float, |
| seed: int, |
| enable_thinking: bool, |
| ) -> str: |
| """HMAC the load-bearing request fields. Mirrors serve_minicpm.sign_text_request.""" |
| body = "\n".join(f"{m['role']}\x1f{m['content']}" for m in messages) |
| payload = f"{body}\n{max_new_tokens}\n{temperature}\n{top_p}\n{seed}\n{int(enable_thinking)}" |
| return hmac.new(key.encode(), payload.encode(), hashlib.sha256).hexdigest() |
|
|
|
|
| _DEMO_INTAKE_QUESTIONS: tuple[dict[str, object], ...] = ( |
| { |
| "question": "Which part of this feels loudest right now?", |
| "options": [ |
| "What might go wrong", |
| "What other people will think", |
| "The unknown right after this", |
| ], |
| }, |
| { |
| "question": "When does this feel hardest?", |
| "options": [ |
| "When I have time alone with it", |
| "When I'm around the people involved", |
| "Right before I have to act on it", |
| ], |
| }, |
| { |
| "question": "Who else feels part of this for you?", |
| "options": [ |
| "Just me", |
| "One specific person", |
| "A group I belong to", |
| ], |
| }, |
| { |
| "question": "What feels most at stake if this does not go well?", |
| "options": [ |
| "How I see myself", |
| "How others see me", |
| "Time or chances I cannot get back", |
| ], |
| }, |
| { |
| "question": "What would a small win here look like?", |
| "options": [ |
| "Just getting through it", |
| "Doing one part well", |
| "Knowing I was honest about how I felt", |
| ], |
| }, |
| ) |
|
|
|
|
| class TextBackend(Protocol): |
| def next_intake_question( |
| self, |
| name: str, |
| situation: str, |
| history: list[dict[str, str]], |
| *, |
| rejected_questions: list[str] | None = None, |
| seed: int = 3407, |
| ) -> str: ... |
|
|
| def plan(self, name: str, situation: str, *, seed: int = 3407) -> str: ... |
|
|
| def author( |
| self, |
| name: str, |
| situation: str, |
| *, |
| plan: dict[str, object], |
| feedback: dict[str, str] | None = None, |
| original: dict[str, object] | None = None, |
| seed: int = 3407, |
| ) -> str: ... |
|
|
| def critic( |
| self, |
| name: str, |
| situation: str, |
| forest: dict[str, object], |
| *, |
| plan: dict[str, object], |
| seed: int = 3407, |
| ) -> str: ... |
|
|
|
|
| class DemoTextBackend: |
| """Deterministic development backend with the storytelling model contract.""" |
|
|
| |
| |
| |
| _CHAPTERS = ( |
| ( |
| "arrive", |
| "The Threshold in Mist", |
| "The path opens quietly, asking nothing of you yet.", |
| "honesty about uncertainty", |
| ( |
| "{situation} sounds painful because it touches something that matters" |
| " to you.\n\nYou do not need to argue with the feeling before looking" |
| " at the problem." |
| ), |
| "Which part of this hurts most right now?", |
| "I can name the part that hurts.", |
| "a quiet path reaching a misty threshold with a soft opening ahead", |
| ), |
| ( |
| "steady", |
| "The Lantern Beside the Map", |
| "A small light is already keeping pace beside the path.", |
| "careful attention", |
| ( |
| "A worried thought can feel like a fact when it repeats often." |
| "\n\nIt can help to ask what happened, then separate it from what" |
| " fear predicts will happen next." |
| ), |
| "What is a fact here, and what is still a prediction?", |
| "I can separate facts from predictions.", |
| "a small lantern beside an unfolded map on a quiet wooden table", |
| ), |
| ( |
| "widen", |
| "The Window and the Horizon", |
| "The path lifts, and the room around the worry begins to feel taller.", |
| "room for more than one outcome", |
| ( |
| "One hard moment can have several explanations. It may show a skill" |
| " to practice, a question to ask, or a standard that was too harsh." |
| "\n\nYou can compare those options before choosing the worst one." |
| ), |
| "Which explanation fits the facts best?", |
| "I can consider more than one explanation.", |
| "an open window looking toward a broad horizon after light rain", |
| ), |
| ( |
| "step", |
| "The First Stepping Stone", |
| "The horizon settles back into the path beneath the feet.", |
| "permission to move without certainty", |
| ( |
| "You could write down the smallest question this situation raises." |
| "\n\nThen choose one safe way to get more information before making" |
| " a larger decision." |
| ), |
| "What question would give you the most useful information?", |
| "I can take one useful next step.", |
| ( |
| "an adult seen from behind pausing before the first stepping stone " |
| "across a shallow stream" |
| ), |
| ), |
| ( |
| "carry", |
| "The Quiet Companion", |
| ( |
| "The walk begins to turn back toward the ordinary day, " |
| "but it does not turn back alone." |
| ), |
| "a simple plan", |
| ( |
| "A simple rule can make the next hard moment easier to handle:" |
| " name the fact, name the fear, then choose one useful action." |
| "\n\nYou do not need to solve the whole problem at once." |
| ), |
| "Which part of that rule would help you first?", |
| "I can name one fact and one next step.", |
| "a small fox-shaped silhouette walking quietly beside an adult at sunset", |
| ), |
| ) |
|
|
| def next_intake_question( |
| self, |
| name: str, |
| situation: str, |
| history: list[dict[str, str]], |
| *, |
| rejected_questions: list[str] | None = None, |
| seed: int = 3407, |
| ) -> str: |
| index = min(len(history), len(_DEMO_INTAKE_QUESTIONS) - 1) |
| payload = dict(_DEMO_INTAKE_QUESTIONS[index]) |
| payload["rationale"] = "deterministic demo intake question" |
| return json.dumps(payload) |
|
|
| def plan(self, name: str, situation: str, *, seed: int = 3407) -> str: |
| return json.dumps( |
| { |
| "faithful_summary": f"{name} is carrying this situation: {situation}", |
| "fact_anchors": [{"source_phrase": situation, "meaning": situation}], |
| "central_uncertainty": "What will happen next", |
| "desired_direction": "Meet the next part with care and agency", |
| } |
| ) |
|
|
| def author( |
| self, |
| name: str, |
| situation: str, |
| *, |
| plan: dict[str, object], |
| feedback: dict[str, str] | None = None, |
| original: dict[str, object] | None = None, |
| seed: int = 3407, |
| ) -> str: |
| anchors = plan.get("fact_anchors") or [] |
| source_phrase = situation |
| if isinstance(anchors, list) and anchors: |
| first = anchors[0] |
| if isinstance(first, dict): |
| source_phrase = str(first.get("source_phrase") or situation) |
| situation_phrase = f"“{source_phrase}”" if len(source_phrase) <= 80 else source_phrase |
| clearings = [ |
| { |
| "arc_role": arc_role, |
| "source_phrase": source_phrase, |
| "scene_title": scene_title, |
| "scene_intro": scene_intro, |
| "strength": strength, |
| "narration": narration.format(situation=situation_phrase), |
| "reflection": reflection, |
| "spell": spell, |
| "image_prompt": image_prompt, |
| } |
| for ( |
| arc_role, |
| scene_title, |
| scene_intro, |
| strength, |
| narration, |
| reflection, |
| spell, |
| image_prompt, |
| ) in self._CHAPTERS |
| ] |
| return json.dumps( |
| { |
| "forest_title": f"{name}'s Path Through the New", |
| "proposed_strengths": [chapter[3] for chapter in self._CHAPTERS[:5]], |
| "clearings": clearings, |
| } |
| ) |
|
|
| def critic( |
| self, |
| name: str, |
| situation: str, |
| forest: dict[str, object], |
| *, |
| plan: dict[str, object], |
| seed: int = 3407, |
| ) -> str: |
| count = min(len(forest.get("clearings", [])), 5) |
| return json.dumps( |
| { |
| "keep_indices": list(range(count)), |
| "revise_indices": [], |
| "reasons": {}, |
| } |
| ) |
|
|
|
|
| class LlamaCppTextBackend: |
| """OpenAI-compatible client restricted to a local llama.cpp server.""" |
|
|
| def __init__( |
| self, |
| base_url: str = "http://127.0.0.1:8080", |
| model: str = "compliment-forest-minicpm5-1b", |
| timeout: float = 90, |
| ) -> None: |
| parsed = urlparse(base_url) |
| if parsed.hostname not in {"127.0.0.1", "localhost", "::1"}: |
| raise ValueError("llama.cpp base URL must resolve to the local machine") |
| self.base_url = base_url.rstrip("/") |
| self.model = model |
| self.client = httpx.Client(timeout=timeout, follow_redirects=True) |
|
|
| def _complete( |
| self, |
| messages: list[dict[str, str]], |
| max_tokens: int, |
| *, |
| seed: int, |
| temperature: float, |
| ) -> str: |
| response = self.client.post( |
| f"{self.base_url}/v1/chat/completions", |
| json={ |
| "model": self.model, |
| "messages": messages, |
| "temperature": temperature, |
| "top_p": 0.9, |
| "max_tokens": max_tokens, |
| "seed": seed, |
| "response_format": {"type": "json_object"}, |
| "chat_template_kwargs": {"enable_thinking": True}, |
| }, |
| ) |
| response.raise_for_status() |
| payload = response.json() |
| return payload["choices"][0]["message"]["content"] |
|
|
| def next_intake_question( |
| self, |
| name: str, |
| situation: str, |
| history: list[dict[str, str]], |
| *, |
| rejected_questions: list[str] | None = None, |
| seed: int = 3407, |
| ) -> str: |
| return self._complete( |
| intake_messages( |
| name, |
| situation, |
| history=history, |
| rejected_questions=rejected_questions, |
| seed=seed, |
| ), |
| max_tokens=8192, |
| seed=seed, |
| temperature=0.4, |
| ) |
|
|
| def plan( |
| self, |
| name: str, |
| situation: str, |
| *, |
| seed: int = 3407, |
| ) -> str: |
| return self._complete( |
| planner_messages(name, situation, seed=seed), |
| max_tokens=8192, |
| seed=seed, |
| temperature=0.2, |
| ) |
|
|
| def author( |
| self, |
| name: str, |
| situation: str, |
| *, |
| plan: dict[str, object], |
| feedback: dict[str, str] | None = None, |
| original: dict[str, object] | None = None, |
| seed: int = 3407, |
| ) -> str: |
| return self._complete( |
| author_messages( |
| name, |
| situation, |
| plan=plan, |
| feedback=feedback, |
| original=original, |
| seed=seed, |
| ), |
| max_tokens=8192, |
| seed=seed, |
| temperature=0.65, |
| ) |
|
|
| def critic( |
| self, |
| name: str, |
| situation: str, |
| forest: dict[str, object], |
| *, |
| plan: dict[str, object], |
| seed: int = 3407, |
| ) -> str: |
| return self._complete( |
| critic_messages(name, situation, forest, plan=plan, seed=seed), |
| max_tokens=8192, |
| seed=seed, |
| temperature=0.15, |
| ) |
|
|
|
|
| class HfInferenceTextBackend: |
| """Hosted chat completion backend for the development Space.""" |
|
|
| def __init__( |
| self, |
| model: str = "openbmb/MiniCPM4.1-8B", |
| *, |
| client: Any | None = None, |
| provider: str = "auto", |
| timeout: float = 120, |
| ) -> None: |
| self.model = model |
| self.client = client or InferenceClient(provider=provider, timeout=timeout) |
|
|
| @staticmethod |
| def _content(response: Any) -> str: |
| content = response.choices[0].message.content |
| if not isinstance(content, str): |
| raise ValueError("hosted model response did not contain text") |
| return content |
|
|
| def next_intake_question( |
| self, |
| name: str, |
| situation: str, |
| history: list[dict[str, str]], |
| *, |
| rejected_questions: list[str] | None = None, |
| seed: int = 3407, |
| ) -> str: |
| response = self.client.chat_completion( |
| model=self.model, |
| messages=intake_messages( |
| name, |
| situation, |
| history=history, |
| rejected_questions=rejected_questions, |
| seed=seed, |
| ), |
| max_tokens=8192, |
| temperature=0.4, |
| top_p=0.9, |
| seed=seed, |
| response_format={"type": "json_object"}, |
| chat_template_kwargs={"enable_thinking": True}, |
| ) |
| return self._content(response) |
|
|
| def plan( |
| self, |
| name: str, |
| situation: str, |
| *, |
| seed: int = 3407, |
| ) -> str: |
| response = self.client.chat_completion( |
| model=self.model, |
| messages=planner_messages(name, situation, seed=seed), |
| max_tokens=8192, |
| temperature=0.2, |
| top_p=0.9, |
| seed=seed, |
| response_format={"type": "json_object"}, |
| chat_template_kwargs={"enable_thinking": True}, |
| ) |
| return self._content(response) |
|
|
| def author( |
| self, |
| name: str, |
| situation: str, |
| *, |
| plan: dict[str, object], |
| feedback: dict[str, str] | None = None, |
| original: dict[str, object] | None = None, |
| seed: int = 3407, |
| ) -> str: |
| response = self.client.chat_completion( |
| model=self.model, |
| messages=author_messages( |
| name, |
| situation, |
| plan=plan, |
| feedback=feedback, |
| original=original, |
| seed=seed, |
| ), |
| max_tokens=8192, |
| temperature=0.82, |
| top_p=0.92, |
| seed=seed, |
| response_format={"type": "json_object"}, |
| chat_template_kwargs={"enable_thinking": True}, |
| ) |
| return self._content(response) |
|
|
| def critic( |
| self, |
| name: str, |
| situation: str, |
| forest: dict[str, object], |
| *, |
| plan: dict[str, object], |
| seed: int = 3407, |
| ) -> str: |
| response = self.client.chat_completion( |
| model=self.model, |
| messages=critic_messages(name, situation, forest, plan=plan, seed=seed), |
| max_tokens=8192, |
| temperature=0.15, |
| top_p=0.9, |
| seed=seed, |
| response_format={"type": "json_object"}, |
| chat_template_kwargs={"enable_thinking": True}, |
| ) |
| return self._content(response) |
|
|
|
|
| class TransformersTextBackend: |
| """Routes prompts through an externally provided GPU generator closure.""" |
|
|
| def __init__( |
| self, |
| model: str = "openbmb/MiniCPM4.1-8B", |
| *, |
| generator: Callable[[list[dict[str, str]], dict[str, object]], str], |
| ) -> None: |
| self.model = model |
| self._generate = generator |
|
|
| def _call( |
| self, |
| messages: list[dict[str, str]], |
| *, |
| max_new_tokens: int, |
| temperature: float, |
| top_p: float, |
| seed: int, |
| ) -> str: |
| params: dict[str, object] = { |
| "max_new_tokens": max_new_tokens, |
| "temperature": temperature, |
| "top_p": top_p, |
| "seed": seed, |
| "chat_template_kwargs": {"enable_thinking": True}, |
| } |
| return self._generate(messages, params) |
|
|
| def next_intake_question( |
| self, |
| name: str, |
| situation: str, |
| history: list[dict[str, str]], |
| *, |
| rejected_questions: list[str] | None = None, |
| seed: int = 3407, |
| ) -> str: |
| return self._call( |
| intake_messages( |
| name, |
| situation, |
| history=history, |
| rejected_questions=rejected_questions, |
| seed=seed, |
| ), |
| max_new_tokens=8192, |
| temperature=0.4, |
| top_p=0.9, |
| seed=seed, |
| ) |
|
|
| def plan(self, name: str, situation: str, *, seed: int = 3407) -> str: |
| return self._call( |
| planner_messages(name, situation, seed=seed), |
| max_new_tokens=8192, |
| temperature=0.2, |
| top_p=0.9, |
| seed=seed, |
| ) |
|
|
| def author( |
| self, |
| name: str, |
| situation: str, |
| *, |
| plan: dict[str, object], |
| feedback: dict[str, str] | None = None, |
| original: dict[str, object] | None = None, |
| seed: int = 3407, |
| ) -> str: |
| return self._call( |
| author_messages( |
| name, |
| situation, |
| plan=plan, |
| feedback=feedback, |
| original=original, |
| seed=seed, |
| ), |
| max_new_tokens=8192, |
| temperature=0.82, |
| top_p=0.92, |
| seed=seed, |
| ) |
|
|
| def critic( |
| self, |
| name: str, |
| situation: str, |
| forest: dict[str, object], |
| *, |
| plan: dict[str, object], |
| seed: int = 3407, |
| ) -> str: |
| return self._call( |
| critic_messages(name, situation, forest, plan=plan, seed=seed), |
| max_new_tokens=8192, |
| temperature=0.15, |
| top_p=0.9, |
| seed=seed, |
| ) |
|
|
|
|
| class ModalTextBackend: |
| """Call the private-token Modal service that hosts MiniCPM4.1-8B.""" |
|
|
| def __init__( |
| self, |
| endpoint: str, |
| signing_key: str, |
| *, |
| client: Any | None = None, |
| timeout: float = 600, |
| ) -> None: |
| if urlparse(endpoint).scheme != "https": |
| raise ValueError("modal text endpoint must use HTTPS") |
| self.endpoint = endpoint |
| self.signing_key = signing_key |
| self.client = client or httpx.Client(timeout=timeout, follow_redirects=True) |
|
|
| def _call( |
| self, |
| messages: list[dict[str, str]], |
| *, |
| max_new_tokens: int, |
| temperature: float, |
| top_p: float, |
| seed: int, |
| enable_thinking: bool = True, |
| ) -> str: |
| signature = sign_modal_text_request( |
| self.signing_key, |
| messages, |
| max_new_tokens=max_new_tokens, |
| temperature=temperature, |
| top_p=top_p, |
| seed=seed, |
| enable_thinking=enable_thinking, |
| ) |
| try: |
| response = self.client.post( |
| self.endpoint, |
| json={ |
| "messages": messages, |
| "max_new_tokens": max_new_tokens, |
| "temperature": temperature, |
| "top_p": top_p, |
| "seed": seed, |
| "enable_thinking": enable_thinking, |
| "signature": signature, |
| }, |
| ) |
| response.raise_for_status() |
| except httpx.HTTPStatusError as error: |
| raise ValueError( |
| f"Modal text request failed with HTTP {error.response.status_code}" |
| ) from error |
| except httpx.RequestError as error: |
| raise ValueError(f"Modal text request failed: {error}") from error |
| payload = response.json() |
| content = payload.get("content") |
| if not isinstance(content, str): |
| raise ValueError("Modal text response did not contain a string content") |
| return content |
|
|
| def next_intake_question( |
| self, |
| name: str, |
| situation: str, |
| history: list[dict[str, str]], |
| *, |
| rejected_questions: list[str] | None = None, |
| seed: int = 3407, |
| ) -> str: |
| return self._call( |
| intake_messages( |
| name, |
| situation, |
| history=history, |
| rejected_questions=rejected_questions, |
| seed=seed, |
| ), |
| max_new_tokens=2048, |
| temperature=0.4, |
| top_p=0.9, |
| seed=seed, |
| enable_thinking=False, |
| ) |
|
|
| def plan(self, name: str, situation: str, *, seed: int = 3407) -> str: |
| return self._call( |
| planner_messages(name, situation, seed=seed), |
| max_new_tokens=8192, |
| temperature=0.2, |
| top_p=0.9, |
| seed=seed, |
| enable_thinking=False, |
| ) |
|
|
| def author( |
| self, |
| name: str, |
| situation: str, |
| *, |
| plan: dict[str, object], |
| feedback: dict[str, str] | None = None, |
| original: dict[str, object] | None = None, |
| seed: int = 3407, |
| ) -> str: |
| return self._call( |
| author_messages( |
| name, |
| situation, |
| plan=plan, |
| feedback=feedback, |
| original=original, |
| seed=seed, |
| ), |
| max_new_tokens=8192, |
| temperature=0.82, |
| top_p=0.92, |
| seed=seed, |
| enable_thinking=False, |
| ) |
|
|
| def critic( |
| self, |
| name: str, |
| situation: str, |
| forest: dict[str, object], |
| *, |
| plan: dict[str, object], |
| seed: int = 3407, |
| ) -> str: |
| return self._call( |
| critic_messages(name, situation, forest, plan=plan, seed=seed), |
| max_new_tokens=8192, |
| temperature=0.15, |
| top_p=0.9, |
| seed=seed, |
| enable_thinking=False, |
| ) |
|
|