"""Gyeol Python SDK — typed decisions over Korean text, in one call. pip install requests from gyeol import Gyeol g = Gyeol("gk_...") # https://gyeol.pltt.dev/console r = g.decide("배송이 3일째 멈춰 있어요. 오늘 안 되면 취소할게요.", { "team": g.choice("이 문의를 처리할 팀은?", {"배송": "배송 지연·추적", "환불": "취소·환불", "기타": None}), "cancel": g.noul("고객이 취소 의사를 밝혔는가?"), "urgency": g.score("긴급도를 평가하라.", ["기한 없음", "며칠 내", "명확한 기한", "오늘 안에"]), }) r["team"].choice, r["cancel"].noul, r["urgency"].score, r["urgency"].confidence Every answer carries `probabilities`. Nothing is generated; the model only picks from what you list. """ from __future__ import annotations import time from dataclasses import dataclass, field from typing import Any, Dict, List, Optional, Union import requests __version__ = "0.1.0" DEFAULT_BASE = "https://gyeol.pltt.dev" @dataclass class Answer: type: str probabilities: Dict[str, float] confidence: float choice: Optional[str] = None noul: Optional[float] = None score: Optional[float] = None legend: Optional[Dict[str, str]] = None raw: Dict[str, Any] = field(default_factory=dict) @property def value(self): return self.choice if self.type == "choice" else self.noul if self.type == "noul" else self.score @property def top(self) -> str: return max(self.probabilities, key=self.probabilities.get) class GyeolError(RuntimeError): def __init__(self, status: int, detail: str): super().__init__(f"{status}: {detail}"); self.status, self.detail = status, detail class Gyeol: def __init__(self, api_key: Optional[str] = None, base_url: str = DEFAULT_BASE, timeout: float = 30.0, retries: int = 2): self.api_key, self.base_url, self.timeout, self.retries = api_key, base_url.rstrip("/"), timeout, retries self._s = requests.Session() # ---- question builders ---- @staticmethod def choice(instructions: str, criteria: Union[Dict[str, Optional[str]], List[str]]) -> dict: crit = {c: None for c in criteria} if isinstance(criteria, list) else dict(criteria) return {"type": "choice", "instructions": instructions, "criteria": crit} @staticmethod def noul(instructions: str, true: Optional[str] = None, false: Optional[str] = None) -> dict: q = {"type": "noul", "instructions": instructions} if true or false: q["criteria"] = {"true": true or "", "false": false or ""} return q @staticmethod def score(instructions: str, levels: List[str]) -> dict: return {"type": "score", "instructions": instructions, "criteria": list(levels)} # ---- the call ---- def decide(self, state: Union[str, dict, list], questions: Dict[str, dict], model: str = "gyeol-latest") -> Dict[str, Answer]: path = "/v1/systemone" if self.api_key else "/v1/demo" headers = {"Content-Type": "application/json"} if self.api_key: headers["Authorization"] = f"Bearer {self.api_key}" body = {"model": model, "state": state, "questions": questions} last = None for attempt in range(self.retries + 1): r = self._s.post(self.base_url + path, json=body, headers=headers, timeout=self.timeout) if r.status_code in (502, 503, 504) and attempt < self.retries: time.sleep(0.5 * (attempt + 1)); last = r; continue break if r.status_code >= 400: try: detail = r.json().get("detail", r.text) except Exception: detail = r.text raise GyeolError(r.status_code, detail) d = r.json() out = {} for name, a in d["answers"].items(): out[name] = Answer(type=a["type"], probabilities=a.get("probabilities", {}), confidence=a.get("confidence", 0.0), choice=a.get("choice"), noul=a.get("noul"), score=a.get("score"), legend=a.get("legend"), raw=a) out["_meta"] = d.get("timing", {}) | {"model": d.get("model"), "usage": d.get("usage", {})} # type: ignore[assignment] return out def me(self) -> dict: r = self._s.get(self.base_url + "/v1/me", headers={"Authorization": f"Bearer {self.api_key}"}, timeout=self.timeout) if r.status_code >= 400: raise GyeolError(r.status_code, r.json().get("detail", r.text)) return r.json()