decision-ui 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,52 @@
1
+ Metadata-Version: 2.3
2
+ Name: decision-ui
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Author: panuthept
6
+ Author-email: panuthept <falcon_270394@hotmail.co.th>
7
+ Requires-Dist: fastapi>=0.143.0
8
+ Requires-Dist: openai>=3.29.0
9
+ Requires-Dist: pydantic>=2.14.1
10
+ Requires-Dist: transformers>=5.19.0
11
+ Requires-Dist: typer>=0.27.3
12
+ Requires-Dist: typesafe-sdk>=0.7.4
13
+ Requires-Dist: uvicorn>=0.54.0
14
+ Requires-Python: >=3.14
15
+ Description-Content-Type: text/markdown
16
+
17
+ # DecisionUI
18
+ A decision interface server, compatible with SystemOne API and OpenAI's Decisions API.
19
+
20
+ ## Usage
21
+ Install DecisionUI
22
+ ```bash
23
+ pip install decision-ui
24
+ ```
25
+
26
+ Deploy the model using any inference engine, e.g., vLLM.
27
+ ```bash
28
+ vllm serve google/gemma-4-31B-it --host localhost --port 8000
29
+ ```
30
+
31
+ Start a DecisionUI server
32
+ ```bash
33
+ decision-ui serve --base_url http://localhost:8000 --host localhost --port 9000
34
+ ```
35
+
36
+ Use it via TypeSafeClient
37
+ ```python
38
+ from typesafe_sdk import Choice, TypeSafeClient
39
+
40
+ with TypeSafeClient(base_url="http://localhost:9000", api_key="EMPTY") as client:
41
+ decision = client.system_one(
42
+ model="google/gemma-4-31B-it",
43
+ state={"document": "I was charged twice. Please fix this ASAP."},
44
+ questions={
45
+ "category": Choice(
46
+ instructions="What is this ticket about?",
47
+ criteria={"billing": None, "technical": None, "other": None},
48
+ ),
49
+ },
50
+ )
51
+ print(decision.choices["category"].probabilities)
52
+ ```
@@ -0,0 +1,36 @@
1
+ # DecisionUI
2
+ A decision interface server, compatible with SystemOne API and OpenAI's Decisions API.
3
+
4
+ ## Usage
5
+ Install DecisionUI
6
+ ```bash
7
+ pip install decision-ui
8
+ ```
9
+
10
+ Deploy the model using any inference engine, e.g., vLLM.
11
+ ```bash
12
+ vllm serve google/gemma-4-31B-it --host localhost --port 8000
13
+ ```
14
+
15
+ Start a DecisionUI server
16
+ ```bash
17
+ decision-ui serve --base_url http://localhost:8000 --host localhost --port 9000
18
+ ```
19
+
20
+ Use it via TypeSafeClient
21
+ ```python
22
+ from typesafe_sdk import Choice, TypeSafeClient
23
+
24
+ with TypeSafeClient(base_url="http://localhost:9000", api_key="EMPTY") as client:
25
+ decision = client.system_one(
26
+ model="google/gemma-4-31B-it",
27
+ state={"document": "I was charged twice. Please fix this ASAP."},
28
+ questions={
29
+ "category": Choice(
30
+ instructions="What is this ticket about?",
31
+ criteria={"billing": None, "technical": None, "other": None},
32
+ ),
33
+ },
34
+ )
35
+ print(decision.choices["category"].probabilities)
36
+ ```
@@ -0,0 +1,26 @@
1
+ [project]
2
+ name = "decision-ui"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.14"
7
+ dependencies = [
8
+ "fastapi>=0.143.0",
9
+ "openai>=3.29.0",
10
+ "pydantic>=2.14.1",
11
+ "transformers>=5.19.0",
12
+ "typer>=0.27.3",
13
+ "typesafe-sdk>=0.7.4",
14
+ "uvicorn>=0.54.0",
15
+ ]
16
+
17
+ [[project.authors]]
18
+ name = "panuthept"
19
+ email = "falcon_270394@hotmail.co.th"
20
+
21
+ [project.scripts]
22
+ decision-ui = "decision_ui.cli.main:entry_point"
23
+
24
+ [build-system]
25
+ requires = ["uv_build>=0.12.24,<0.13.0"]
26
+ build-backend = "uv_build"
@@ -0,0 +1,25 @@
1
+ [project]
2
+ name = "decision-ui"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "panuthept", email = "falcon_270394@hotmail.co.th" }
8
+ ]
9
+ requires-python = ">=3.14"
10
+ dependencies = [
11
+ "fastapi>=0.143.0",
12
+ "openai>=3.29.0",
13
+ "pydantic>=2.14.1",
14
+ "transformers>=5.19.0",
15
+ "typer>=0.27.3",
16
+ "typesafe-sdk>=0.7.4",
17
+ "uvicorn>=0.54.0",
18
+ ]
19
+
20
+ [project.scripts]
21
+ decision-ui = "decision_ui.cli.main:entry_point"
22
+
23
+ [build-system]
24
+ requires = ["uv_build>=0.12.24,<0.13.0"]
25
+ build-backend = "uv_build"
@@ -0,0 +1,7 @@
1
+ from .core import (
2
+ Choice,
3
+ Noul,
4
+ Score,
5
+ SystemOneRequest,
6
+ SystemOneResponse,
7
+ )
@@ -0,0 +1,20 @@
1
+ """Backend registry."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .base import Backend
6
+
7
+
8
+ def get_backend(name: str = "mock", **kwargs) -> Backend:
9
+ if name == "mock":
10
+ from decision_ui.backends.mock import MockBackend
11
+
12
+ return MockBackend()
13
+ if name == "vllm":
14
+ from decision_ui.backends.vllm import VLLMBackend
15
+
16
+ return VLLMBackend(**kwargs)
17
+ raise ValueError(f"Unknown backend '{name}'. Available: mock and vllm.")
18
+
19
+
20
+ __all__ = ["Backend", "get_backend"]
@@ -0,0 +1,82 @@
1
+ """Backend interface.
2
+
3
+ A backend turns ``(state, question)`` pairs into option logits. Every backend must
4
+ implement :meth:`score_options`, which is the single primitive the rest of OpenJev
5
+ builds on. Choice / Score / Noul are all expressed as "score these labels given this
6
+ prompt", which is exactly one forward pass on any causal LM.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import time
12
+ from typing import Callable
13
+ from abc import ABC, abstractmethod
14
+ from decision_ui.prompt_renders import get_prompt_render as default_prompt_render
15
+
16
+ from decision_ui.core.data_types import (
17
+ Answer,
18
+ Choice,
19
+ Noul,
20
+ Score,
21
+ SystemOneRequest,
22
+ SystemOneResponse,
23
+ Usage,
24
+ )
25
+ from decision_ui.core.utilities import (
26
+ build_choice_answer,
27
+ build_noul_answer,
28
+ build_score_answer,
29
+ )
30
+
31
+ NOUL_LABELS = ["yes", "no"]
32
+
33
+
34
+ class Backend(ABC):
35
+ def __init__(self, prompt_render: Callable = None, **kwargs):
36
+ self.prompt_render = prompt_render or default_prompt_render
37
+
38
+ @property
39
+ @abstractmethod
40
+ def available_models(self) -> list[str]:
41
+ """Return a list of available model IDs."""
42
+
43
+ @abstractmethod
44
+ def score_options(self, model: str, prompt: str, labels: list[str]) -> tuple[list[float], int]:
45
+ """Return one logit per label plus the number of input tokens consumed."""
46
+
47
+ def decide(self, request: SystemOneRequest, temperature: float = 1.0) -> SystemOneResponse:
48
+ t0 = time.perf_counter()
49
+ answers: dict[str, Answer] = {}
50
+ usage = Usage()
51
+ for qid, q in request.questions.items():
52
+ if isinstance(q, Choice):
53
+ labels = q.option_keys()
54
+ logits, n_in = self.score_options(
55
+ request.model,
56
+ self.prompt_render(request.state, q.instructions, labels),
57
+ labels
58
+ )
59
+ answers[qid] = build_choice_answer(q, logits, temperature)
60
+ elif isinstance(q, Score):
61
+ labels = [f"{lvl} ({q.legend[str(lvl)]})" for lvl in q.levels()]
62
+ logits, n_in = self.score_options(
63
+ request.model,
64
+ self.prompt_render(request.state, q.instructions, labels),
65
+ labels
66
+ )
67
+ answers[qid] = build_score_answer(q, logits, temperature)
68
+ elif isinstance(q, Noul):
69
+ logits, n_in = self.score_options(
70
+ request.model,
71
+ self.prompt_render(request.state, q.instructions, NOUL_LABELS),
72
+ NOUL_LABELS
73
+ )
74
+ answers[qid] = build_noul_answer(logits[0], logits[1], temperature)
75
+ else: # pragma: no cover
76
+ raise TypeError(f"Unknown question type: {type(q)!r}")
77
+ usage.input_tokens += n_in
78
+ usage.output_tokens += len(labels) if not isinstance(q, Noul) else 2
79
+ latency_ms = (time.perf_counter() - t0) * 1000
80
+ return SystemOneResponse(
81
+ model=request.model, answers=answers, usage=usage, latency_ms=round(latency_ms, 2)
82
+ )
@@ -0,0 +1,28 @@
1
+ """Deterministic mock backend.
2
+
3
+ Useful for tests, demos and CI: no model download, sub-millisecond latency, and
4
+ stable outputs derived from a hash of (prompt, label). It exists so the REPL, the
5
+ server and the SDK-compat tests can run anywhere.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+
12
+ from decision_ui.backends.base import Backend
13
+
14
+
15
+ class MockBackend(Backend):
16
+ @property
17
+ def available_models(self) -> list[str]:
18
+ return ["mock-model"]
19
+
20
+ def score_options(self, model: str, prompt: str, labels: list[str]) -> tuple[list[float], int]:
21
+ logits: list[float] = []
22
+ for label in labels:
23
+ h = hashlib.sha256((prompt + "\x00" + label).encode("utf-8")).digest()
24
+ # map first 4 bytes to a logit in roughly [-3, 3]
25
+ v = int.from_bytes(h[:4], "big") / 0xFFFFFFFF
26
+ logits.append((v - 0.5) * 6.0)
27
+ # crude token estimate: ~4 chars per token
28
+ return logits, max(1, len(prompt) // 4)
@@ -0,0 +1,72 @@
1
+ """vLLM backend.
2
+
3
+ One forward pass per (state, question). Logits are read at the last position and
4
+ only the first token of each label is compared, then softmaxed across labels so
5
+ nothing outside the option set can win.
6
+
7
+ Phase 1 will add: shared-state prefill with KV-cache reuse across questions, and
8
+ multi-token label scoring (sum of log-probs) for labels that are not single tokens.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from openai import OpenAI
14
+ from decision_ui.backends.base import Backend
15
+
16
+
17
+ class VLLMBackend(Backend):
18
+ def __init__(self, api_key: str | None = None, base_url: str | None = None, **kwargs):
19
+ super().__init__(**kwargs)
20
+ self.api_key = api_key
21
+ self.base_url = base_url
22
+ self._model_client = OpenAI(api_key=self.api_key, base_url=self.base_url)
23
+ self._tokenizer_client = OpenAI(api_key=self.api_key, base_url=self.base_url.replace("/v1", ""))
24
+
25
+ @property
26
+ def available_models(self) -> list[str]:
27
+ return [model.id for model in self._model_client.models.list().data]
28
+
29
+ def _tokenize(self, model: str, text: str) -> list[int]:
30
+ enc = self._tokenizer_client.post(
31
+ "/tokenize",
32
+ cast_to=dict,
33
+ body={
34
+ "model": model,
35
+ "prompt": text,
36
+ },
37
+ )
38
+ return enc["tokens"]
39
+
40
+ def _detokenize(self, model: str, token_ids: list[int]) -> str:
41
+ dec = self._tokenizer_client.post(
42
+ "/detokenize",
43
+ cast_to=dict,
44
+ body={
45
+ "model": model,
46
+ "tokens": token_ids,
47
+ },
48
+ )
49
+ return dec["prompt"]
50
+
51
+ def _first_token(self, model: str, label: str) -> str:
52
+ ids = self._tokenize(model, label)
53
+ return self._detokenize(model, [ids[0]])
54
+
55
+ def score_options(self, model: str, prompt: str, labels: list[str]) -> tuple[list[float], int]:
56
+ token_ids = self._tokenize(model, prompt)
57
+
58
+ response = self._model_client.chat.completions.create(
59
+ model=model,
60
+ messages=[{"role": "user", "content": prompt}],
61
+ logprobs=True,
62
+ top_logprobs=20,
63
+ extra_body={
64
+ "chat_template_kwargs": {
65
+ "enable_thinking": False
66
+ }
67
+ }
68
+ )
69
+ pred_tokens = {top_logprob.token: top_logprob.logprob for top_logprob in response.choices[0].logprobs.content[0].top_logprobs}
70
+ cand_tokens = [self._first_token(model, label) for label in labels]
71
+ logits = [pred_tokens.get(token, float("-inf")) for token in cand_tokens]
72
+ return logits, len(token_ids)
File without changes
@@ -0,0 +1,6 @@
1
+ import typer
2
+ from decision_ui.cli.serve_commands import serve_commands
3
+
4
+
5
+ entry_point = typer.Typer()
6
+ entry_point.add_typer(serve_commands)
@@ -0,0 +1,28 @@
1
+ import typer
2
+ import uvicorn
3
+ from typing import Annotated
4
+ from decision_ui.server import create_app
5
+
6
+
7
+ serve_commands = typer.Typer()
8
+
9
+ @serve_commands.command("serve")
10
+ def serve_command(
11
+ host: Annotated[str, typer.Option("--host", help="The host to serve on")] = "localhost",
12
+ port: Annotated[int, typer.Option("--port", help="The port to serve on")] = 8000,
13
+ timeout: Annotated[int, typer.Option("--timeout", help="The timeout for root.run() in seconds")] = 300,
14
+ backend: Annotated[str, typer.Option("--backend", help="The backend to use for the RØØT server")] = "vllm",
15
+ base_url: Annotated[str, typer.Option("--base_url", help="The base URL for the backend")] = "http://localhost:8000",
16
+ api_key: Annotated[str, typer.Option("--api_key", help="The API key for the backend")] = "EMPTY",
17
+ mock: Annotated[bool, typer.Option("--mock", help="Enable mock mode for the backend")] = False,
18
+ ):
19
+ # Create the FastAPI app and run it
20
+ app = create_app(
21
+ config={
22
+ "backend": backend if not mock else "mock",
23
+ "base_url": base_url if base_url.endswith("/v1") else base_url + "/v1",
24
+ "api_key": api_key,
25
+ "timeout": timeout,
26
+ }
27
+ )
28
+ uvicorn.run(app, host=host, port=port, workers=1)
@@ -0,0 +1,14 @@
1
+ from .data_types import (
2
+ Choice,
3
+ Noul,
4
+ Score,
5
+ SystemOneRequest,
6
+ SystemOneResponse
7
+ )
8
+ from .utilities import (
9
+ softmax,
10
+ confidence_from_probs,
11
+ build_choice_answer,
12
+ build_score_answer,
13
+ build_noul_answer
14
+ )
@@ -0,0 +1,126 @@
1
+ """System One primitives: Choice, Score, Noul.
2
+
3
+ These models mirror the request / response shape of TypeSafe's ``POST /v1/systemone``
4
+ endpoint so that the official SDKs work against OpenJev by changing only the base URL.
5
+
6
+ OpenJev reproduces the *interface pattern*, not TypeSafe's undisclosed model or training.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any, Literal, Union
12
+
13
+ from pydantic import BaseModel, Field, model_validator
14
+
15
+ # --------------------------------------------------------------------------------------
16
+ # Questions (request side)
17
+ # --------------------------------------------------------------------------------------
18
+
19
+
20
+ class Choice(BaseModel):
21
+ """Pick exactly one option from a runtime-defined set (2..255 options)."""
22
+
23
+ type: Literal["choice"] = "choice"
24
+ instructions: str
25
+ options: list[str] | None = None
26
+ criteria: dict[str, str | None] | None = None
27
+
28
+ @model_validator(mode="after")
29
+ def _check_options(self) -> Choice:
30
+ keys = self.option_keys()
31
+ if len(keys) < 2:
32
+ raise ValueError("Choice needs at least 2 options (use `options` or `criteria`).")
33
+ if len(keys) > 255:
34
+ raise ValueError("Choice supports at most 255 options.")
35
+ return self
36
+
37
+ def option_keys(self) -> list[str]:
38
+ if self.criteria:
39
+ return list(self.criteria.keys())
40
+ return list(self.options or [])
41
+
42
+
43
+ class Score(BaseModel):
44
+ """Grade the state on an ordered scale (2..10 levels). Returns a probability-weighted score.
45
+
46
+ Accepts either the official request form ``criteria=["level 0 desc", "level 1 desc", ...]``
47
+ or the response-style ``legend={"0": "...", "1": "..."}``. ``legend`` is always populated.
48
+ """
49
+
50
+ type: Literal["score"] = "score"
51
+ instructions: str
52
+ criteria: list[str] | None = None
53
+ legend: dict[str, str] | None = None
54
+
55
+ @model_validator(mode="after")
56
+ def _normalise(self) -> "Score":
57
+ if self.legend is None and self.criteria is not None:
58
+ self.legend = {str(i): desc for i, desc in enumerate(self.criteria)}
59
+ if self.legend is None:
60
+ raise ValueError("Score needs `criteria` (list) or `legend` (dict).")
61
+ if not (2 <= len(self.legend) <= 10):
62
+ raise ValueError("Score needs between 2 and 10 levels.")
63
+ for k in self.legend:
64
+ int(k) # raises if not numeric
65
+ return self
66
+
67
+ def levels(self) -> list[int]:
68
+ return sorted(int(k) for k in self.legend)
69
+
70
+
71
+ class Noul(BaseModel):
72
+ """A yes/no judgement. Returns P(true) in [0, 1]."""
73
+
74
+ type: Literal["noul"] = "noul"
75
+ instructions: str
76
+
77
+
78
+ Question = Union[Choice, Score, Noul]
79
+
80
+
81
+ class SystemOneRequest(BaseModel):
82
+ """Body of ``POST /v1/systemone``."""
83
+
84
+ state: Any
85
+ questions: dict[str, Question]
86
+ model: str = "openjev-latest"
87
+
88
+
89
+ # --------------------------------------------------------------------------------------
90
+ # Answers (response side)
91
+ # --------------------------------------------------------------------------------------
92
+
93
+
94
+ class ChoiceAnswer(BaseModel):
95
+ type: Literal["choice"] = "choice"
96
+ choice: str
97
+ probabilities: dict[str, float]
98
+ confidence: float
99
+
100
+
101
+ class ScoreAnswer(BaseModel):
102
+ type: Literal["score"] = "score"
103
+ score: float
104
+ legend: dict[str, str]
105
+ probabilities: dict[str, float]
106
+ confidence: float
107
+
108
+
109
+ class NoulAnswer(BaseModel):
110
+ type: Literal["noul"] = "noul"
111
+ noul: float
112
+
113
+
114
+ Answer = Union[ChoiceAnswer, ScoreAnswer, NoulAnswer]
115
+
116
+
117
+ class Usage(BaseModel):
118
+ input_tokens: int = 0
119
+ output_tokens: int = 0
120
+
121
+
122
+ class SystemOneResponse(BaseModel):
123
+ model: str
124
+ answers: dict[str, Answer]
125
+ usage: Usage = Field(default_factory=Usage)
126
+ latency_ms: float | None = None
@@ -0,0 +1,56 @@
1
+ import math
2
+ from decision_ui.core.data_types import Choice, ChoiceAnswer, Score, ScoreAnswer, NoulAnswer
3
+
4
+
5
+ def softmax(logits: list[float], temperature: float = 1.0) -> list[float]:
6
+ if temperature <= 0:
7
+ raise ValueError("temperature must be > 0")
8
+ scaled = [x / temperature for x in logits]
9
+ m = max(scaled)
10
+ if m == -math.inf:
11
+ return [0.0 for _ in logits]
12
+
13
+ exps = [math.exp(x - m) for x in scaled]
14
+ total = sum(exps)
15
+ return [e / total for e in exps]
16
+
17
+
18
+ def confidence_from_probs(probs: list[float]) -> float:
19
+ """Confidence in [0, 1] as 1 - normalized entropy.
20
+
21
+ TypeSafe does not publish its confidence formula. This is an interpretable
22
+ approximation: 1.0 when all mass is on one option, 0.0 when uniform.
23
+ """
24
+ n = len(probs)
25
+ if n <= 1:
26
+ return 1.0
27
+ ent = -sum(p * math.log(p) for p in probs if p > 0)
28
+ return max(0.0, min(1.0, 1.0 - ent / math.log(n)))
29
+
30
+
31
+ def build_choice_answer(
32
+ question: Choice, option_logits: list[float], temperature: float = 1.0
33
+ ) -> ChoiceAnswer:
34
+ keys = question.option_keys()
35
+ probs = softmax(option_logits, temperature)
36
+ dist = {k: round(p, 4) for k, p in zip(keys, probs)}
37
+ best = keys[max(range(len(keys)), key=lambda i: probs[i])]
38
+ return ChoiceAnswer(choice=best, probabilities=dist, confidence=round(confidence_from_probs(probs), 4))
39
+
40
+
41
+ def build_score_answer(question: Score, level_logits: list[float], temperature: float = 1.0) -> ScoreAnswer:
42
+ levels = question.levels()
43
+ probs = softmax(level_logits, temperature)
44
+ expectation = sum(l * p for l, p in zip(levels, probs))
45
+ dist = {str(l): round(p, 4) for l, p in zip(levels, probs)}
46
+ return ScoreAnswer(
47
+ score=round(expectation, 4),
48
+ legend=question.legend,
49
+ probabilities=dist,
50
+ confidence=round(confidence_from_probs(probs), 4),
51
+ )
52
+
53
+
54
+ def build_noul_answer(yes_logit: float, no_logit: float, temperature: float = 1.0) -> NoulAnswer:
55
+ p_yes, _ = softmax([yes_logit, no_logit], temperature)
56
+ return NoulAnswer(noul=round(p_yes, 4))
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+ from .default import render_prompt as default_render_prompt
5
+
6
+
7
+ def get_prompt_render(state: Any, instructions: str, labels: list[str], name: str = "default") -> str:
8
+ if name == "default":
9
+ return default_render_prompt(state, instructions, labels)
10
+ raise ValueError(f"Unknown prompt name: {name}")
11
+
12
+
13
+ __all__ = ["get_prompt_render"]
@@ -0,0 +1,19 @@
1
+ import json
2
+ from typing import Any
3
+
4
+
5
+ def render_state(state: Any) -> str:
6
+ if isinstance(state, str):
7
+ return state
8
+ return json.dumps(state, ensure_ascii=False, indent=2)
9
+
10
+ def render_prompt(state: Any, instructions: str, labels: list[str]) -> str:
11
+ """Chat-style prompt whose next token is expected to be one of ``labels``."""
12
+ options = "\n".join(f"- {label}" for label in labels)
13
+ return (
14
+ "You are a decision engine. Read the state, then answer the question by replying "
15
+ "with exactly one of the allowed labels and nothing else.\n\n"
16
+ f"### State\n{render_state(state)}\n\n"
17
+ f"### Question\n{instructions}\n\n"
18
+ f"### Allowed labels\n{options}\n"
19
+ )
@@ -0,0 +1 @@
1
+ from .server import create_app
@@ -0,0 +1,32 @@
1
+ from fastapi import APIRouter, Request
2
+ from fastapi.responses import JSONResponse
3
+ from decision_ui import Choice, Noul, Score, SystemOneRequest, SystemOneResponse
4
+
5
+ router = APIRouter()
6
+
7
+
8
+ @router.post("/decision")
9
+ async def decision(request: Request):
10
+ timeout = request.app.state.timeout
11
+ backend = request.app.state.backend
12
+
13
+ body: dict = await request.json()
14
+ print(body)
15
+
16
+ questions = {}
17
+ for key, value in body["questions"].items():
18
+ if value["type"] == "choice":
19
+ question = Choice(instructions=value["instructions"], options=value.get("options"), criteria=value.get("criteria"))
20
+ elif value["type"] == "noul":
21
+ question = Noul(instructions=value["instructions"])
22
+ elif value["type"] == "score":
23
+ question = Score(instructions=value["instructions"], criteria=value.get("criteria"), legend=value.get("legend"))
24
+ questions[key] = question
25
+
26
+ response: SystemOneResponse = backend.decide(SystemOneRequest(
27
+ model=body["model"],
28
+ state=body["state"],
29
+ questions=questions,
30
+ ))
31
+
32
+ return JSONResponse(response.model_dump())
@@ -0,0 +1,67 @@
1
+ from fastapi import APIRouter, Request
2
+ from fastapi.responses import JSONResponse
3
+ from decision_ui import Choice, Noul, Score, SystemOneRequest, SystemOneResponse
4
+
5
+ router = APIRouter()
6
+
7
+
8
+ @router.post("/decisions")
9
+ async def decisions(request: Request):
10
+ timeout = request.app.state.timeout
11
+ backend = request.app.state.backend
12
+
13
+ body: dict = await request.json()
14
+ print(body)
15
+
16
+ questions = {}
17
+ questions_types = {}
18
+ for value in body["questions"]:
19
+ key = value["name"]
20
+ if value["type"] == "choice":
21
+ criteria = {choice["value"]: choice["description"] for choice in value["choices"]}
22
+ question = Choice(instructions=value["instructions"], criteria=criteria)
23
+ questions_types[key] = "choice"
24
+ elif value["type"] == "predicate":
25
+ question = Noul(instructions=value["instructions"])
26
+ questions_types[key] = "predicate"
27
+ elif value["type"] == "score":
28
+ legend = {str(i): level["label"] for i, level in enumerate(value["levels"])}
29
+ question = Score(instructions=value["instructions"], legend=legend)
30
+ questions_types[key] = "score"
31
+ questions[key] = question
32
+
33
+ response: SystemOneResponse = backend.decide(SystemOneRequest(
34
+ model=body["model"],
35
+ state=body["input"],
36
+ questions=questions,
37
+ ))
38
+
39
+ def format_answer(answer, name: str):
40
+ if answer.type == "choice":
41
+ return {
42
+ "type": "choice",
43
+ "name": name,
44
+ "choice": answer.choice,
45
+ "probabilities": [{"value": value, "probability": prob} for value, prob in answer.probabilities.items()],
46
+ "confidence": answer.confidence
47
+ }
48
+ elif answer.type == "noul":
49
+ return {
50
+ "type": "predicate",
51
+ "name": name,
52
+ "probability": answer.noul
53
+ }
54
+ elif answer.type == "score":
55
+ return {
56
+ "type": "score",
57
+ "name": name,
58
+ "score": answer.score,
59
+ "probabilities": [{"value": value, "label": answer.legend[value], "probability": prob} for value, prob in answer.probabilities.items()],
60
+ "confidence": answer.confidence
61
+ }
62
+
63
+ return JSONResponse({
64
+ "model": body["model"],
65
+ "answers": [format_answer(response.answers[value["name"]], value["name"]) for value in body["questions"]],
66
+ "usage": response.usage.model_dump()
67
+ })
@@ -0,0 +1,32 @@
1
+ from fastapi import APIRouter, Request
2
+ from fastapi.responses import JSONResponse
3
+ from decision_ui import Choice, Noul, Score, SystemOneRequest, SystemOneResponse
4
+
5
+ router = APIRouter()
6
+
7
+
8
+ @router.post("/systemone")
9
+ async def systemone(request: Request):
10
+ timeout = request.app.state.timeout
11
+ backend = request.app.state.backend
12
+
13
+ body: dict = await request.json()
14
+ print(body)
15
+
16
+ questions = {}
17
+ for key, value in body["questions"].items():
18
+ if value["type"] == "choice":
19
+ question = Choice(instructions=value["instructions"], options=value.get("options"), criteria=value.get("criteria"))
20
+ elif value["type"] == "noul":
21
+ question = Noul(instructions=value["instructions"])
22
+ elif value["type"] == "score":
23
+ question = Score(instructions=value["instructions"], criteria=value.get("criteria"), legend=value.get("legend"))
24
+ questions[key] = question
25
+
26
+ response: SystemOneResponse = backend.decide(SystemOneRequest(
27
+ model=body["model"],
28
+ state=body["state"],
29
+ questions=questions,
30
+ ))
31
+
32
+ return JSONResponse(response.model_dump())
@@ -0,0 +1,34 @@
1
+ from fastapi import FastAPI
2
+ from decision_ui.backends import get_backend
3
+
4
+
5
+ def create_app(config: dict) -> FastAPI:
6
+ app = FastAPI()
7
+ print(f"Creating FastAPI app with configuration: {config}")
8
+ app.state.timeout = config.get("timeout")
9
+ app.state.backend = get_backend(
10
+ config["backend"],
11
+ base_url=config.get("base_url"),
12
+ api_key=config.get("api_key"),
13
+ )
14
+ print(f"Available models: {app.state.backend.available_models}")
15
+
16
+ # ------------------------------------------------------------------
17
+ # Inference routers (hooks fire on these)
18
+ # Registered under both "" and "/v1" so the proxy works regardless
19
+ # of whether the client sets base_url="http://host:port" or
20
+ # base_url="http://host:port/v1".
21
+ # ------------------------------------------------------------------
22
+ from .routes.systemone import router as systemone_router
23
+ from .routes.decisions import router as decisions_router
24
+ from .routes.decision import router as decision_router
25
+
26
+ app.include_router(systemone_router)
27
+ app.include_router(decisions_router)
28
+ app.include_router(decision_router)
29
+
30
+ app.include_router(systemone_router, prefix="/v1")
31
+ app.include_router(decisions_router, prefix="/v1")
32
+ app.include_router(decision_router, prefix="/alpha")
33
+
34
+ return app