dev-double 0.1.0__py3-none-any.whl

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.
Files changed (84) hide show
  1. dev_double/__init__.py +3 -0
  2. dev_double/apps/__init__.py +1 -0
  3. dev_double/apps/cli/__init__.py +5 -0
  4. dev_double/apps/cli/main.py +122 -0
  5. dev_double/apps/client/__init__.py +5 -0
  6. dev_double/apps/client/http_client.py +102 -0
  7. dev_double/apps/composition.py +71 -0
  8. dev_double/apps/config.py +36 -0
  9. dev_double/apps/server/__init__.py +5 -0
  10. dev_double/apps/server/app.py +122 -0
  11. dev_double/apps/server/recorder.py +62 -0
  12. dev_double/apps/server/transport/__init__.py +39 -0
  13. dev_double/apps/server/transport/choice_use_cases.py +121 -0
  14. dev_double/apps/server/transport/common.py +37 -0
  15. dev_double/apps/server/transport/decide.py +131 -0
  16. dev_double/apps/server/transport/extract.py +93 -0
  17. dev_double/apps/server/transport/guard_judge.py +97 -0
  18. dev_double/apps/server/transport/rerank.py +55 -0
  19. dev_double/client.py +5 -0
  20. dev_double/core/__init__.py +1 -0
  21. dev_double/core/decision/__init__.py +62 -0
  22. dev_double/core/decision/answer_shaping.py +48 -0
  23. dev_double/core/decision/confidence.py +19 -0
  24. dev_double/core/decision/decider_basic_impl.py +65 -0
  25. dev_double/core/decision/decision_service_basic_impl.py +141 -0
  26. dev_double/core/decision/defaults.py +40 -0
  27. dev_double/core/decision/errors.py +7 -0
  28. dev_double/core/decision/extract_questions.py +40 -0
  29. dev_double/core/decision/field_extraction.py +53 -0
  30. dev_double/core/decision/i_clock.py +11 -0
  31. dev_double/core/decision/i_decider.py +36 -0
  32. dev_double/core/decision/i_decision_service.py +40 -0
  33. dev_double/core/decision/i_engine.py +27 -0
  34. dev_double/core/decision/i_extractor.py +19 -0
  35. dev_double/core/decision/i_generator.py +19 -0
  36. dev_double/core/decision/i_id_provider.py +9 -0
  37. dev_double/core/decision/i_record_reader.py +25 -0
  38. dev_double/core/decision/i_reranker.py +18 -0
  39. dev_double/core/decision/label_scoring.py +89 -0
  40. dev_double/core/decision/prompts.py +130 -0
  41. dev_double/core/decision/record_parsing.py +153 -0
  42. dev_double/core/decision/t_answer.py +32 -0
  43. dev_double/core/decision/t_classify.py +42 -0
  44. dev_double/core/decision/t_decide.py +26 -0
  45. dev_double/core/decision/t_extract.py +89 -0
  46. dev_double/core/decision/t_gate.py +36 -0
  47. dev_double/core/decision/t_generate.py +37 -0
  48. dev_double/core/decision/t_guard.py +38 -0
  49. dev_double/core/decision/t_input.py +8 -0
  50. dev_double/core/decision/t_judge.py +35 -0
  51. dev_double/core/decision/t_label_query.py +36 -0
  52. dev_double/core/decision/t_meta.py +17 -0
  53. dev_double/core/decision/t_question.py +70 -0
  54. dev_double/core/decision/t_rerank.py +36 -0
  55. dev_double/core/decision/t_route.py +28 -0
  56. dev_double/core/decision/t_usage.py +15 -0
  57. dev_double/core/decision/tracker.py +27 -0
  58. dev_double/core/decision/use_case_questions.py +80 -0
  59. dev_double/core/decision/value_parsing.py +109 -0
  60. dev_double/providers/__init__.py +1 -0
  61. dev_double/providers/mock/__init__.py +1 -0
  62. dev_double/providers/mock/decision/__init__.py +5 -0
  63. dev_double/providers/mock/decision/clock_mock_impl.py +19 -0
  64. dev_double/providers/mock/decision/engine_mock_impl.py +78 -0
  65. dev_double/providers/mock/decision/id_provider_mock_impl.py +15 -0
  66. dev_double/providers/needle/__init__.py +1 -0
  67. dev_double/providers/needle/decision/__init__.py +4 -0
  68. dev_double/providers/needle/decision/decider_needle_impl.py +117 -0
  69. dev_double/providers/needle/decision/record_tool.py +75 -0
  70. dev_double/providers/openai/__init__.py +1 -0
  71. dev_double/providers/openai/decision/__init__.py +3 -0
  72. dev_double/providers/openai/decision/engine_openai_impl.py +154 -0
  73. dev_double/providers/std/__init__.py +1 -0
  74. dev_double/providers/std/decision/__init__.py +4 -0
  75. dev_double/providers/std/decision/clock_std_impl.py +12 -0
  76. dev_double/providers/std/decision/id_provider_std_impl.py +12 -0
  77. dev_double/providers/systemone/__init__.py +1 -0
  78. dev_double/providers/systemone/decision/__init__.py +3 -0
  79. dev_double/providers/systemone/decision/decider_system_one_impl.py +100 -0
  80. dev_double-0.1.0.dist-info/METADATA +354 -0
  81. dev_double-0.1.0.dist-info/RECORD +84 -0
  82. dev_double-0.1.0.dist-info/WHEEL +4 -0
  83. dev_double-0.1.0.dist-info/entry_points.txt +2 -0
  84. dev_double-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,121 @@
1
+ """/v1/route, /v1/gate, /v1/classify: the use cases answered by one choice question."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Optional, Union
6
+
7
+ from pydantic import BaseModel, Field, field_validator, model_validator
8
+
9
+ from ....core.decision.defaults import DEFAULT_CLASSIFY_QUESTION
10
+ from ....core.decision.t_classify import TClassifyRequest, TClassifyResponse
11
+ from ....core.decision.t_gate import TGateRequest, TGateResponse, TToolCall
12
+ from ....core.decision.t_question import MAX_OPTIONS, check_option_keys
13
+ from ....core.decision.t_route import TRouteRequest, TRouteResponse
14
+ from .common import InputValue, Meta
15
+
16
+
17
+ class _CoreValidated(BaseModel):
18
+ """Constructs the core type after field validation, so core invariants give a 422."""
19
+
20
+ @model_validator(mode="after")
21
+ def _core_invariants(self): # type: ignore[no-untyped-def]
22
+ self.to_core() # type: ignore[attr-defined]
23
+ return self
24
+
25
+
26
+ def _distinct_keys(v: Optional[dict[str, str]]) -> Optional[dict[str, str]]:
27
+ # Checked per field too, so the 422 points at the offending field.
28
+ if v is not None:
29
+ check_option_keys(v)
30
+ return v
31
+
32
+
33
+ class RouteRequest(_CoreValidated):
34
+ input: InputValue = Field(description="The prompt or task to route.")
35
+ routes: Optional[dict[str, str]] = Field(
36
+ None,
37
+ min_length=2,
38
+ max_length=MAX_OPTIONS,
39
+ description="Route name -> when to use it. Defaults to small / medium / large model tiers.",
40
+ )
41
+
42
+ _routes = field_validator("routes")(_distinct_keys)
43
+
44
+ def to_core(self) -> TRouteRequest:
45
+ return TRouteRequest(input=self.input, routes=self.routes)
46
+
47
+
48
+ class RouteResponse(BaseModel):
49
+ route: str
50
+ probabilities: dict[str, float]
51
+ confidence: float
52
+ meta: Meta
53
+
54
+ @classmethod
55
+ def from_core(cls, t: TRouteResponse) -> "RouteResponse":
56
+ return cls(route=t.route, probabilities=t.probabilities, confidence=t.confidence, meta=Meta.from_core(t.meta))
57
+
58
+
59
+ class ToolCall(BaseModel):
60
+ name: str
61
+ arguments: Union[dict[str, Any], str] = Field(default_factory=dict)
62
+
63
+
64
+ class GateRequest(_CoreValidated):
65
+ tool_call: ToolCall
66
+ context: Optional[InputValue] = Field(None, description="What the user asked for, or the recent conversation.")
67
+ policy: Optional[str] = Field(None, description="Your rules for tool use, in plain language.")
68
+ outcomes: Optional[dict[str, str]] = Field(
69
+ None,
70
+ min_length=2,
71
+ max_length=MAX_OPTIONS,
72
+ description="Outcome -> when it applies. Defaults to allow / ask / deny.",
73
+ )
74
+
75
+ _outcomes = field_validator("outcomes")(_distinct_keys)
76
+
77
+ def to_core(self) -> TGateRequest:
78
+ call = TToolCall(name=self.tool_call.name, arguments=self.tool_call.arguments)
79
+ return TGateRequest(tool_call=call, context=self.context, policy=self.policy, outcomes=self.outcomes)
80
+
81
+
82
+ class GateResponse(BaseModel):
83
+ decision: str
84
+ probabilities: dict[str, float]
85
+ confidence: float
86
+ meta: Meta
87
+
88
+ @classmethod
89
+ def from_core(cls, t: TGateResponse) -> "GateResponse":
90
+ return cls(decision=t.decision, probabilities=t.probabilities, confidence=t.confidence, meta=Meta.from_core(t.meta))
91
+
92
+
93
+ class ClassifyRequest(_CoreValidated):
94
+ input: Optional[InputValue] = Field(None, description="One item. Use `inputs` for a batch.")
95
+ inputs: Optional[list[InputValue]] = Field(None, description="Many items, labeled in parallel.")
96
+ labels: dict[str, str] = Field(min_length=2, max_length=MAX_OPTIONS)
97
+ question: str = DEFAULT_CLASSIFY_QUESTION
98
+
99
+ _labels = field_validator("labels")(_distinct_keys)
100
+
101
+ def to_core(self) -> TClassifyRequest:
102
+ return TClassifyRequest(labels=self.labels, input=self.input, inputs=self.inputs, question=self.question)
103
+
104
+
105
+ class Classification(BaseModel):
106
+ label: str
107
+ probabilities: dict[str, float]
108
+ confidence: float
109
+
110
+
111
+ class ClassifyResponse(BaseModel):
112
+ results: list[Classification]
113
+ meta: Meta
114
+
115
+ @classmethod
116
+ def from_core(cls, t: TClassifyResponse) -> "ClassifyResponse":
117
+ results = [
118
+ Classification(label=r.label, probabilities=r.probabilities, confidence=r.confidence)
119
+ for r in t.results
120
+ ]
121
+ return cls(results=results, meta=Meta.from_core(t.meta))
@@ -0,0 +1,37 @@
1
+ """Shared transport pieces: input values and response metadata."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Union
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+ from ....core.decision.t_meta import TMeta
10
+
11
+ InputValue = Union[str, dict[str, Any], list[Any]]
12
+
13
+
14
+ class Usage(BaseModel):
15
+ input_tokens: int = 0
16
+ output_tokens: int = 0
17
+
18
+
19
+ class Meta(BaseModel):
20
+ engine: str
21
+ model: str
22
+ latency_ms: int
23
+ usage: Usage
24
+ warnings: list[str] = Field(
25
+ default_factory=list,
26
+ description="Where this answer may be less faithful than a real decision model.",
27
+ )
28
+
29
+ @classmethod
30
+ def from_core(cls, t: TMeta) -> "Meta":
31
+ return cls(
32
+ engine=t.engine,
33
+ model=t.model,
34
+ latency_ms=t.latency_ms,
35
+ usage=Usage(input_tokens=t.usage.input_tokens, output_tokens=t.usage.output_tokens),
36
+ warnings=list(t.warnings),
37
+ )
@@ -0,0 +1,131 @@
1
+ """/v1/decide: three question types, answered about one input.
2
+
3
+ Invariants (option counts, distinct keys) live in the core types; the
4
+ validators below construct them so a violation becomes a 422.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Annotated, Literal, Optional, Union
10
+
11
+ from pydantic import BaseModel, Field, model_validator
12
+
13
+ from ....core.decision.t_answer import TAnswer, TBinaryAnswer, TChoiceAnswer
14
+ from ....core.decision.t_decide import TDecideRequest, TDecideResponse
15
+ from ....core.decision.t_question import (
16
+ MAX_LEVELS,
17
+ MAX_OPTIONS,
18
+ TBinaryQuestion,
19
+ TChoiceQuestion,
20
+ TQuestion,
21
+ TScaleQuestion,
22
+ )
23
+ from .common import InputValue, Meta
24
+
25
+
26
+ class BinaryQuestion(BaseModel):
27
+ type: Literal["binary"]
28
+ question: str = Field(description="A yes/no question or a statement to verify.")
29
+ yes: Optional[str] = Field(None, description="Optional: what counts as yes.")
30
+ no: Optional[str] = Field(None, description="Optional: what counts as no.")
31
+
32
+ def to_core(self) -> TBinaryQuestion:
33
+ return TBinaryQuestion(question=self.question, yes=self.yes, no=self.no)
34
+
35
+
36
+ class ChoiceQuestion(BaseModel):
37
+ type: Literal["choice"]
38
+ question: str
39
+ options: dict[str, str] = Field(
40
+ min_length=2,
41
+ max_length=MAX_OPTIONS,
42
+ description="Option key -> description. Keys are returned as the answer.",
43
+ )
44
+
45
+ @model_validator(mode="after")
46
+ def _core_invariants(self) -> "ChoiceQuestion":
47
+ self.to_core()
48
+ return self
49
+
50
+ def to_core(self) -> TChoiceQuestion:
51
+ return TChoiceQuestion(question=self.question, options=dict(self.options))
52
+
53
+
54
+ class ScaleQuestion(BaseModel):
55
+ type: Literal["scale"]
56
+ question: str
57
+ levels: list[str] = Field(
58
+ min_length=2,
59
+ max_length=MAX_LEVELS,
60
+ description="Ordered level descriptions, lowest first. Level i is returned as i.",
61
+ )
62
+
63
+ def to_core(self) -> TScaleQuestion:
64
+ return TScaleQuestion(question=self.question, levels=list(self.levels))
65
+
66
+
67
+ Question = Annotated[
68
+ Union[BinaryQuestion, ChoiceQuestion, ScaleQuestion], Field(discriminator="type")
69
+ ]
70
+
71
+
72
+ class DecideRequest(BaseModel):
73
+ input: InputValue = Field(description="The content to decide about: text, an object, or a list.")
74
+ questions: dict[str, Question] = Field(min_length=1)
75
+ model: Optional[str] = Field(None, description="Ignored; the server's configured model is used.")
76
+
77
+ def to_core(self) -> TDecideRequest:
78
+ questions: dict[str, TQuestion] = {k: q.to_core() for k, q in self.questions.items()}
79
+ return TDecideRequest(input=self.input, questions=questions)
80
+
81
+
82
+ class BinaryAnswer(BaseModel):
83
+ type: Literal["binary"] = "binary"
84
+ value: bool
85
+ probability: float = Field(description="Probability that the answer is yes.")
86
+ confidence: float
87
+
88
+
89
+ class ChoiceAnswer(BaseModel):
90
+ type: Literal["choice"] = "choice"
91
+ value: str
92
+ probabilities: dict[str, float]
93
+ confidence: float
94
+
95
+
96
+ class ScaleAnswer(BaseModel):
97
+ type: Literal["scale"] = "scale"
98
+ value: float = Field(description="Probability-weighted level, from 0 to len(levels) - 1.")
99
+ level: int = Field(description="The single most likely level.")
100
+ probabilities: dict[str, float]
101
+ legend: dict[str, str]
102
+ confidence: float
103
+
104
+
105
+ Answer = Annotated[Union[BinaryAnswer, ChoiceAnswer, ScaleAnswer], Field(discriminator="type")]
106
+
107
+
108
+ def answer_from_core(a: TAnswer) -> Union[BinaryAnswer, ChoiceAnswer, ScaleAnswer]:
109
+ if isinstance(a, TBinaryAnswer):
110
+ return BinaryAnswer(value=a.value, probability=a.probability, confidence=a.confidence)
111
+ if isinstance(a, TChoiceAnswer):
112
+ return ChoiceAnswer(value=a.value, probabilities=a.probabilities, confidence=a.confidence)
113
+ return ScaleAnswer(
114
+ value=a.value,
115
+ level=a.level,
116
+ probabilities=a.probabilities,
117
+ legend=a.legend,
118
+ confidence=a.confidence,
119
+ )
120
+
121
+
122
+ class DecideResponse(BaseModel):
123
+ answers: dict[str, Answer]
124
+ meta: Meta
125
+
126
+ @classmethod
127
+ def from_core(cls, t: TDecideResponse) -> "DecideResponse":
128
+ return cls(
129
+ answers={k: answer_from_core(a) for k, a in t.answers.items()},
130
+ meta=Meta.from_core(t.meta),
131
+ )
@@ -0,0 +1,93 @@
1
+ """/v1/extract: typed fields pulled out of one input.
2
+
3
+ Invariants (field types, enum options, field counts) live in the core types;
4
+ the validators below construct them so a violation becomes a 422 that points
5
+ at the offending field.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any, Literal, Optional, Union
11
+
12
+ from pydantic import BaseModel, Field, model_serializer, model_validator
13
+
14
+ from ....core.decision.t_extract import (
15
+ MAX_FIELDS,
16
+ TExtractField,
17
+ TExtractRequest,
18
+ TExtractResponse,
19
+ TFieldValue,
20
+ )
21
+ from ....core.decision.t_question import MAX_OPTIONS
22
+ from .common import InputValue, Meta
23
+
24
+ Value = Optional[Union[bool, int, float, str]]
25
+
26
+
27
+ class ExtractField(BaseModel):
28
+ type: Literal["string", "number", "integer", "boolean", "enum"] = Field(
29
+ description="string, number, and integer values are generated; enum and boolean are scored."
30
+ )
31
+ description: Optional[str] = Field(None, description="What the field means and how to format it.")
32
+ options: Optional[Union[list[str], dict[str, str]]] = Field(
33
+ None,
34
+ min_length=2,
35
+ max_length=MAX_OPTIONS,
36
+ description="Enum only: option names, or option -> description. The chosen name is returned.",
37
+ )
38
+
39
+ @model_validator(mode="after")
40
+ def _core_invariants(self) -> "ExtractField":
41
+ self.to_core()
42
+ return self
43
+
44
+ def to_core(self) -> TExtractField:
45
+ options = list(self.options) if isinstance(self.options, list) else self.options
46
+ return TExtractField(type=self.type, description=self.description, options=options)
47
+
48
+
49
+ class ExtractRequest(BaseModel):
50
+ input: InputValue = Field(description="The content to extract from: text, an object, or a list.")
51
+ fields: dict[str, ExtractField] = Field(
52
+ min_length=1, max_length=MAX_FIELDS, description="Field name -> type and description."
53
+ )
54
+
55
+ @model_validator(mode="after")
56
+ def _core_invariants(self) -> "ExtractRequest":
57
+ self.to_core()
58
+ return self
59
+
60
+ def to_core(self) -> TExtractRequest:
61
+ return TExtractRequest(input=self.input, fields={k: f.to_core() for k, f in self.fields.items()})
62
+
63
+
64
+ class FieldValue(BaseModel):
65
+ value: Value = Field(None, description="The extracted value; null when the input doesn't contain it.")
66
+ confidence: float
67
+ probabilities: Optional[dict[str, float]] = Field(None, description="Enum fields only.")
68
+ probability: Optional[float] = Field(None, description="Boolean fields only: probability of true.")
69
+
70
+ @model_serializer(mode="wrap")
71
+ def _omit_absent(self, handler: Any) -> dict[str, Any]:
72
+ # `value: null` is an answer; a missing probability is not, so omit only those.
73
+ data = handler(self)
74
+ rest = {k: v for k, v in data.items() if k != "value" and v is not None}
75
+ return {"value": self.value, **rest}
76
+
77
+ @classmethod
78
+ def from_core(cls, t: TFieldValue) -> "FieldValue":
79
+ return cls(value=t.value, confidence=t.confidence, probabilities=t.probabilities, probability=t.probability)
80
+
81
+
82
+ class ExtractResponse(BaseModel):
83
+ values: dict[str, Value] = Field(description="Field name -> value, for direct use.")
84
+ fields: dict[str, FieldValue] = Field(description="Field name -> value with its confidence.")
85
+ meta: Meta
86
+
87
+ @classmethod
88
+ def from_core(cls, t: TExtractResponse) -> "ExtractResponse":
89
+ return cls(
90
+ values=dict(t.values),
91
+ fields={k: FieldValue.from_core(f) for k, f in t.fields.items()},
92
+ meta=Meta.from_core(t.meta),
93
+ )
@@ -0,0 +1,97 @@
1
+ """/v1/guard (binary questions per policy) and /v1/judge (one scale question)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+ from pydantic import BaseModel, Field, model_validator
8
+
9
+ from ....core.decision.defaults import DEFAULT_GUARD_THRESHOLD, DEFAULT_JUDGE_CRITERIA
10
+ from ....core.decision.t_guard import TGuardRequest, TGuardResponse
11
+ from ....core.decision.t_judge import TJudgeRequest, TJudgeResponse
12
+ from ....core.decision.t_question import MAX_LEVELS
13
+ from .common import InputValue, Meta
14
+
15
+
16
+ class GuardRequest(BaseModel):
17
+ input: InputValue
18
+ policies: Optional[dict[str, str]] = Field(
19
+ None,
20
+ min_length=1,
21
+ description="Policy name -> description of a violation. Defaults to prompt_injection and abuse.",
22
+ )
23
+ scope: Optional[str] = Field(
24
+ None, description="Optional: what the assistant is for. Adds an off_topic check."
25
+ )
26
+ threshold: float = Field(
27
+ DEFAULT_GUARD_THRESHOLD, ge=0, le=1, description="Block when any violation probability reaches this."
28
+ )
29
+
30
+ @model_validator(mode="after")
31
+ def _core_invariants(self) -> "GuardRequest":
32
+ self.to_core()
33
+ return self
34
+
35
+ def to_core(self) -> TGuardRequest:
36
+ return TGuardRequest(input=self.input, policies=self.policies, scope=self.scope, threshold=self.threshold)
37
+
38
+
39
+ class GuardCheck(BaseModel):
40
+ probability: float
41
+ flagged: bool
42
+
43
+
44
+ class GuardResponse(BaseModel):
45
+ allowed: bool
46
+ flagged: list[str]
47
+ checks: dict[str, GuardCheck]
48
+ meta: Meta
49
+
50
+ @classmethod
51
+ def from_core(cls, t: TGuardResponse) -> "GuardResponse":
52
+ return cls(
53
+ allowed=t.allowed,
54
+ flagged=list(t.flagged),
55
+ checks={k: GuardCheck(probability=c.probability, flagged=c.flagged) for k, c in t.checks.items()},
56
+ meta=Meta.from_core(t.meta),
57
+ )
58
+
59
+
60
+ class JudgeRequest(BaseModel):
61
+ output: InputValue = Field(description="The answer being judged.")
62
+ input: Optional[InputValue] = Field(None, description="The prompt or task that produced the output.")
63
+ reference: Optional[InputValue] = Field(None, description="Optional reference answer.")
64
+ criteria: str = DEFAULT_JUDGE_CRITERIA
65
+ levels: Optional[list[str]] = Field(
66
+ None,
67
+ min_length=2,
68
+ max_length=MAX_LEVELS,
69
+ description="Ordered rubric levels, worst first. Defaults to a 5-level rubric (0-4).",
70
+ )
71
+
72
+ def to_core(self) -> TJudgeRequest:
73
+ return TJudgeRequest(
74
+ output=self.output, input=self.input, reference=self.reference, criteria=self.criteria, levels=self.levels
75
+ )
76
+
77
+
78
+ class JudgeResponse(BaseModel):
79
+ score: float = Field(description="Probability-weighted level, from 0 to len(levels) - 1.")
80
+ normalized: float = Field(description="score / (len(levels) - 1), from 0 to 1.")
81
+ level: int
82
+ probabilities: dict[str, float]
83
+ legend: dict[str, str]
84
+ confidence: float
85
+ meta: Meta
86
+
87
+ @classmethod
88
+ def from_core(cls, t: TJudgeResponse) -> "JudgeResponse":
89
+ return cls(
90
+ score=t.score,
91
+ normalized=t.normalized,
92
+ level=t.level,
93
+ probabilities=t.probabilities,
94
+ legend=t.legend,
95
+ confidence=t.confidence,
96
+ meta=Meta.from_core(t.meta),
97
+ )
@@ -0,0 +1,55 @@
1
+ """/v1/rerank: Cohere-style wire format, the de facto standard shared by
2
+ Hugging Face TEI, Infinity, Jina, and vLLM."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import Optional, Union
7
+
8
+ from pydantic import BaseModel, Field
9
+
10
+ from ....core.decision.t_rerank import TRerankRequest, TRerankResponse
11
+ from .common import Meta
12
+
13
+
14
+ class RerankDocument(BaseModel):
15
+ text: str
16
+
17
+
18
+ class RerankRequest(BaseModel):
19
+ query: str
20
+ documents: list[Union[str, RerankDocument]] = Field(min_length=1)
21
+ top_n: Optional[int] = Field(None, ge=1)
22
+ return_documents: bool = False
23
+ model: Optional[str] = Field(None, description="Ignored; the server's configured model is used.")
24
+
25
+ def texts(self) -> list[str]:
26
+ return [d if isinstance(d, str) else d.text for d in self.documents]
27
+
28
+ def to_core(self) -> TRerankRequest:
29
+ return TRerankRequest(
30
+ query=self.query, documents=self.texts(), top_n=self.top_n, return_documents=self.return_documents
31
+ )
32
+
33
+
34
+ class RerankResult(BaseModel):
35
+ index: int
36
+ relevance_score: float
37
+ document: Optional[RerankDocument] = None
38
+
39
+
40
+ class RerankResponse(BaseModel):
41
+ id: str
42
+ results: list[RerankResult]
43
+ meta: Meta
44
+
45
+ @classmethod
46
+ def from_core(cls, t: TRerankResponse) -> "RerankResponse":
47
+ results = [
48
+ RerankResult(
49
+ index=r.index,
50
+ relevance_score=r.relevance_score,
51
+ document=RerankDocument(text=r.document) if r.document is not None else None,
52
+ )
53
+ for r in t.results
54
+ ]
55
+ return cls(id=t.id, results=results, meta=Meta.from_core(t.meta))
dev_double/client.py ADDED
@@ -0,0 +1,5 @@
1
+ """Re-export of the sync HTTP client, kept for ``from dev_double.client import DevDouble``."""
2
+
3
+ from .apps.client.http_client import DevDouble
4
+
5
+ __all__ = ["DevDouble"]
@@ -0,0 +1 @@
1
+ """Core layer: interfaces, domain types, and pure decision logic. No third-party imports."""
@@ -0,0 +1,62 @@
1
+ """Decision core: interfaces, domain types, prompts, and the use cases.
2
+
3
+ Zero third-party dependencies. Side effects (model calls, time, ids) arrive
4
+ through the ports ``IEngine`` / ``IDecider``, ``IClock``, and ``IIdProvider``.
5
+ Optional capabilities (``IGenerator``, ``IRecordReader``, ``IExtractor``,
6
+ ``IReranker``) are detected with ``isinstance`` and used when present.
7
+ """
8
+
9
+ from .confidence import DIGITS, confidence
10
+ from .decider_basic_impl import DeciderBasicImpl
11
+ from .decision_service_basic_impl import DecisionServiceBasicImpl
12
+ from .defaults import DEFAULT_OUTCOMES, DEFAULT_POLICIES, DEFAULT_ROUTES, DEFAULT_RUBRIC
13
+ from .errors import EngineError
14
+ from .i_clock import IClock
15
+ from .i_decider import IDecider
16
+ from .i_decision_service import IDecisionService
17
+ from .i_engine import IEngine
18
+ from .i_extractor import IExtractor
19
+ from .i_generator import IGenerator
20
+ from .i_id_provider import IIdProvider
21
+ from .i_record_reader import IRecordReader
22
+ from .i_reranker import IReranker
23
+ from .t_answer import TAnswer, TBinaryAnswer, TChoiceAnswer, TScaleAnswer
24
+ from .t_classify import TClassification, TClassifyRequest, TClassifyResponse
25
+ from .t_decide import TDecideRequest, TDecideResponse
26
+ from .t_extract import MAX_FIELDS, TExtractField, TExtractRequest, TExtractResponse, TFieldValue
27
+ from .t_gate import TGateRequest, TGateResponse, TToolCall
28
+ from .t_generate import TGenerateQuery, TGenerateResult
29
+ from .t_guard import TGuardCheck, TGuardRequest, TGuardResponse
30
+ from .t_input import TInputValue
31
+ from .t_judge import TJudgeRequest, TJudgeResponse
32
+ from .t_label_query import TLabelQuery, TLabelResult, TPosition
33
+ from .t_meta import TMeta
34
+ from .t_question import (
35
+ MAX_LEVELS,
36
+ MAX_OPTIONS,
37
+ TBinaryQuestion,
38
+ TChoiceQuestion,
39
+ TQuestion,
40
+ TScaleQuestion,
41
+ )
42
+ from .t_rerank import TRerankRequest, TRerankResponse, TRerankResult
43
+ from .t_route import TRouteRequest, TRouteResponse
44
+ from .t_usage import TUsage
45
+ from .tracker import Tracker
46
+
47
+ __all__ = [
48
+ "DEFAULT_OUTCOMES", "DEFAULT_POLICIES", "DEFAULT_ROUTES", "DEFAULT_RUBRIC", "DIGITS",
49
+ "MAX_FIELDS", "MAX_LEVELS", "MAX_OPTIONS",
50
+ "DeciderBasicImpl", "DecisionServiceBasicImpl", "EngineError",
51
+ "IClock", "IDecider", "IDecisionService", "IEngine", "IExtractor",
52
+ "IGenerator", "IIdProvider", "IRecordReader", "IReranker",
53
+ "TExtractField", "TExtractRequest", "TExtractResponse", "TFieldValue",
54
+ "TGenerateQuery", "TGenerateResult",
55
+ "TAnswer", "TBinaryAnswer", "TBinaryQuestion", "TChoiceAnswer", "TChoiceQuestion",
56
+ "TClassification", "TClassifyRequest", "TClassifyResponse", "TDecideRequest",
57
+ "TDecideResponse", "TGateRequest", "TGateResponse", "TGuardCheck", "TGuardRequest",
58
+ "TGuardResponse", "TInputValue", "TJudgeRequest", "TJudgeResponse", "TLabelQuery",
59
+ "TLabelResult", "TMeta", "TPosition", "TQuestion", "TRerankRequest", "TRerankResponse",
60
+ "TRerankResult", "TRouteRequest", "TRouteResponse", "TScaleAnswer", "TScaleQuestion",
61
+ "TToolCall", "TUsage", "Tracker", "confidence",
62
+ ]
@@ -0,0 +1,48 @@
1
+ """Turns a distribution over a question's labels into its typed answer.
2
+
3
+ Labels are "yes"/"no" for binary questions, option keys for choice questions,
4
+ and "0".."n-1" for scale questions (see ``question_labels``).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from .confidence import DIGITS, confidence, round_probabilities
10
+ from .t_answer import TAnswer, TBinaryAnswer, TChoiceAnswer, TScaleAnswer
11
+ from .t_question import TBinaryQuestion, TChoiceQuestion, TQuestion
12
+
13
+
14
+ def question_labels(question: TQuestion) -> dict[str, str]:
15
+ """Label -> description, in the order the question defines them."""
16
+ if isinstance(question, TBinaryQuestion):
17
+ return {"yes": question.yes or "The answer is yes.", "no": question.no or "The answer is no."}
18
+ if isinstance(question, TChoiceQuestion):
19
+ return dict(question.options)
20
+ return {str(i): text for i, text in enumerate(question.levels)}
21
+
22
+
23
+ def shape_answer(question: TQuestion, probs: dict[str, float], round_probs: bool = True) -> TAnswer:
24
+ """``round_probs=False`` passes choice/scale probabilities through as given."""
25
+ if isinstance(question, TBinaryQuestion):
26
+ p_yes = probs["yes"]
27
+ return TBinaryAnswer(
28
+ value=p_yes >= 0.5,
29
+ probability=round(p_yes, DIGITS),
30
+ confidence=round(confidence([p_yes, 1 - p_yes]), DIGITS),
31
+ )
32
+
33
+ shown = round_probabilities(probs) if round_probs else probs
34
+ if isinstance(question, TChoiceQuestion):
35
+ return TChoiceAnswer(
36
+ value=max(probs, key=probs.__getitem__),
37
+ probabilities=shown,
38
+ confidence=round(confidence(list(probs.values())), DIGITS),
39
+ )
40
+
41
+ expected = sum(int(level) * p for level, p in probs.items())
42
+ return TScaleAnswer(
43
+ value=round(expected, DIGITS),
44
+ level=int(max(probs, key=probs.__getitem__)),
45
+ probabilities=shown,
46
+ legend={str(i): text for i, text in enumerate(question.levels)},
47
+ confidence=round(confidence(list(probs.values())), DIGITS),
48
+ )
@@ -0,0 +1,19 @@
1
+ """How concentrated a distribution is, and output rounding."""
2
+
3
+ from __future__ import annotations
4
+
5
+ DIGITS = 4
6
+
7
+
8
+ def confidence(probabilities: list[float]) -> float:
9
+ """How concentrated a distribution is: 1.0 when all mass is on one label,
10
+ 0.0 when it is spread evenly. Normalized max probability: (n*max - 1) / (n - 1).
11
+ """
12
+ n = len(probabilities)
13
+ if n < 2:
14
+ return 1.0
15
+ return max(0.0, (n * max(probabilities) - 1) / (n - 1))
16
+
17
+
18
+ def round_probabilities(probs: dict[str, float]) -> dict[str, float]:
19
+ return {k: round(v, DIGITS) for k, v in probs.items()}