| from __future__ import annotations |
|
|
| import json |
| from typing import Protocol |
| from urllib.parse import urlparse |
|
|
| import httpx |
|
|
| from compliment_forest.prompts import author_messages, critic_messages |
|
|
|
|
| class TextBackend(Protocol): |
| def author( |
| self, |
| name: str, |
| situation: str, |
| *, |
| feedback: dict[str, str] | None = None, |
| original: dict[str, object] | None = None, |
| ) -> str: ... |
|
|
| def critic(self, name: str, situation: str, forest: dict[str, object]) -> str: ... |
|
|
|
|
| class DemoTextBackend: |
| """Deterministic development backend with the same model contract.""" |
|
|
| _CLEARINGS = ( |
| ( |
| "The Patient Fox", |
| "patience under pressure", |
| "a gentle russet fox sitting in a mossy clearing, soft kind eyes", |
| "I am allowed to learn.", |
| ), |
| ( |
| "The Listening Owl", |
| "careful perspective", |
| "a round tawny owl resting on a low branch, attentive kind eyes", |
| "I can listen before I leap.", |
| ), |
| ( |
| "The Brave Snail", |
| "quiet courage", |
| "a tiny snail crossing a fern frond, softly glowing spiral shell", |
| "I make progress at my pace.", |
| ), |
| ( |
| "The Steady Deer", |
| "steadiness through change", |
| "a small deer standing in morning mist, calm gentle expression", |
| "I can meet one moment.", |
| ), |
| ( |
| "The Clear-Voiced Wren", |
| "honest self-expression", |
| "a tiny wren singing beside dusty rose wildflowers", |
| "I can speak gently and clearly.", |
| ), |
| ) |
|
|
| def author( |
| self, |
| name: str, |
| situation: str, |
| *, |
| feedback: dict[str, str] | None = None, |
| original: dict[str, object] | None = None, |
| ) -> str: |
| clearings = [] |
| for creature, strength, image_prompt, spell in self._CLEARINGS: |
| clearings.append( |
| { |
| "creature": creature, |
| "strength": strength, |
| "line": ( |
| f"{situation.rstrip('.')} may feel uncertain. Your {strength} " |
| "lets you respond without pretending the hard part is easy." |
| ), |
| "reflection": ( |
| "What becomes possible when you ask for one honest next step " |
| "instead of a perfect answer?" |
| ), |
| "spell": spell, |
| "image_prompt": image_prompt, |
| } |
| ) |
| payload = { |
| "forest_title": f"{name}'s Path Through the New", |
| "proposed_strengths": [item[1] for item in self._CLEARINGS], |
| "clearings": clearings, |
| } |
| return json.dumps(payload) |
|
|
| def critic(self, name: str, situation: str, forest: dict[str, object]) -> 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) |
|
|
| def _complete(self, messages: list[dict[str, str]], max_tokens: int) -> str: |
| response = self.client.post( |
| f"{self.base_url}/v1/chat/completions", |
| json={ |
| "model": self.model, |
| "messages": messages, |
| "temperature": 0.35, |
| "top_p": 0.9, |
| "max_tokens": max_tokens, |
| "response_format": {"type": "json_object"}, |
| "chat_template_kwargs": {"enable_thinking": False}, |
| }, |
| ) |
| response.raise_for_status() |
| payload = response.json() |
| return payload["choices"][0]["message"]["content"] |
|
|
| def author( |
| self, |
| name: str, |
| situation: str, |
| *, |
| feedback: dict[str, str] | None = None, |
| original: dict[str, object] | None = None, |
| ) -> str: |
| return self._complete( |
| author_messages(name, situation, feedback=feedback, original=original), |
| max_tokens=1700, |
| ) |
|
|
| def critic(self, name: str, situation: str, forest: dict[str, object]) -> str: |
| return self._complete(critic_messages(name, situation, forest), max_tokens=800) |
|
|