helixor-reasoner 0.2.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,17 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ Copyright 2026 Helixor AI, Inc.
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
@@ -0,0 +1,57 @@
1
+ Metadata-Version: 2.4
2
+ Name: helixor-reasoner
3
+ Version: 0.2.0
4
+ Summary: Typed client for the Helixor reasoning API: decide, fetch proofs, report outcomes.
5
+ Project-URL: Homepage, https://helixor.dev
6
+ Author-email: Helixor <engineering@helixor.ai>
7
+ License-Expression: Apache-2.0
8
+ License-File: LICENSE
9
+ Keywords: decisions,helixor,reasoning,sdk
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.11
19
+ Requires-Dist: httpx>=0.27
20
+ Requires-Dist: pydantic>=2.7
21
+ Description-Content-Type: text/markdown
22
+
23
+ # helixor-reasoner
24
+
25
+ Typed client for the Helixor reasoning API: decide, fetch proofs, report outcomes.
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ pip install helixor-reasoner
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ ```python
36
+ from helixor_reasoner import ApiClient
37
+
38
+ with ApiClient(base_url="https://api.example.com") as client:
39
+ result = client.get_health()
40
+ ```
41
+
42
+ Responses are validated against the API contract; non-2xx responses raise typed exceptions.
43
+
44
+ ## Operations
45
+
46
+ | Method | HTTP | Summary |
47
+ | --- | --- | --- |
48
+ | `decide` | `POST /v1/decide` | Decide |
49
+ | `get_health` | `GET /v1/health` | Health |
50
+
51
+ ## License
52
+
53
+ `Apache-2.0` - see [LICENSE](LICENSE).
54
+
55
+ ## Links
56
+
57
+ - Homepage: https://helixor.dev
@@ -0,0 +1,35 @@
1
+ # helixor-reasoner
2
+
3
+ Typed client for the Helixor reasoning API: decide, fetch proofs, report outcomes.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install helixor-reasoner
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```python
14
+ from helixor_reasoner import ApiClient
15
+
16
+ with ApiClient(base_url="https://api.example.com") as client:
17
+ result = client.get_health()
18
+ ```
19
+
20
+ Responses are validated against the API contract; non-2xx responses raise typed exceptions.
21
+
22
+ ## Operations
23
+
24
+ | Method | HTTP | Summary |
25
+ | --- | --- | --- |
26
+ | `decide` | `POST /v1/decide` | Decide |
27
+ | `get_health` | `GET /v1/health` | Health |
28
+
29
+ ## License
30
+
31
+ `Apache-2.0` - see [LICENSE](LICENSE).
32
+
33
+ ## Links
34
+
35
+ - Homepage: https://helixor.dev
@@ -0,0 +1,7 @@
1
+ from .client import ApiClient
2
+ from . import models
3
+ from .contract import CONTRACT_HASH_ALGORITHM, OPERATION_HASH_ALGORITHM, OPERATION_SHA256, SPEC_SHA256
4
+ from .operation_contracts import OPERATION_CONTRACTS, OperationContractsManifest
5
+ from .errors import ApiException, ApiHttpException, ApiResponseValidationException
6
+
7
+ __all__ = ["ApiClient", "models", "CONTRACT_HASH_ALGORITHM", "OPERATION_HASH_ALGORITHM", "OPERATION_SHA256", "SPEC_SHA256", "OPERATION_CONTRACTS", "OperationContractsManifest", "ApiException", "ApiHttpException", "ApiResponseValidationException"]
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from ..client import ApiClient
7
+
8
+ from .public_operations import PublicOperations, PublicResourceMethods
9
+
10
+
11
+ class PublicResource(PublicResourceMethods):
12
+ def __init__(self, client: ApiClient) -> None:
13
+ self._client = client
14
+
15
+
16
+ __all__ = ['PublicOperations', 'PublicResourceMethods', 'PublicResource']
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Iterator, Literal, Mapping, Protocol, TYPE_CHECKING
4
+
5
+ from .. import models as Models
6
+
7
+ if TYPE_CHECKING:
8
+ from ..client import ApiClient
9
+
10
+
11
+ class _ClientHelpers(Protocol):
12
+ """What the operation methods below call on self; ApiClient satisfies it."""
13
+
14
+ def request(self, method: str, path: str, *, body: Any = None, content_type: str = 'application/json', response_model: type[Any] | None = None, request_headers: Mapping[str, Any] | None = None) -> Any: ...
15
+
16
+
17
+ class PublicOperations:
18
+ def decide(self: _ClientHelpers, body: Models.DecideRequest) -> Models.DecisionResponse:
19
+ path = '/v1/decide'
20
+ return self.request('POST', path, body=body, content_type='application/json', response_model=Models.DecisionResponse)
21
+
22
+ def get_health(self: _ClientHelpers) -> Models.HealthResponse:
23
+ path = '/v1/health'
24
+ return self.request('GET', path, response_model=Models.HealthResponse)
25
+
26
+
27
+ class PublicResourceMethods:
28
+ _client: ApiClient
29
+
30
+ def decide(self, body: Models.DecideRequest) -> Models.DecisionResponse:
31
+ return self._client.decide(body=body)
32
+
33
+ def get_health(self) -> Models.HealthResponse:
34
+ return self._client.get_health()
@@ -0,0 +1,193 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import time
5
+ from typing import Any, Callable, Iterator, Mapping, TypeVar
6
+ from urllib.parse import quote, urlencode
7
+
8
+ import httpx
9
+
10
+ from pydantic import BaseModel, TypeAdapter, ValidationError
11
+
12
+ from . import models as Models
13
+ from ._operations import PublicOperations, PublicResource
14
+ from .errors import ApiDeserializationException, ApiHttpException, ApiResponseValidationException, ApiSerializationException, ApiValidationException, RawResponse
15
+
16
+
17
+ T = TypeVar("T")
18
+
19
+
20
+ class ApiClient(PublicOperations):
21
+ def __init__(self, base_url: str | None = None, headers: Mapping[str, str] | None = None, api_key: str | Callable[[], str] | None = None, timeout: float | None = None) -> None:
22
+ self.base_url = (base_url or None or '').rstrip('/')
23
+ if not self.base_url:
24
+ raise ValueError('ApiClient requires base_url: this SDK has no default API host')
25
+ self.headers = dict(headers or {})
26
+ self.api_key = api_key
27
+ # The configured timeout is the DEFAULT, not a ceiling. A caller driving an
28
+ # LLM-backed operation legitimately needs longer than a CRUD caller, and one
29
+ # baked value forces it to reach around the generated client.
30
+ self.timeout = 60.0 if timeout is None else timeout
31
+ self._client = httpx.Client(base_url=self.base_url, headers=self.headers, timeout=self.timeout, follow_redirects=False)
32
+
33
+ self.public = PublicResource(self)
34
+
35
+ def close(self) -> None:
36
+ self._client.close()
37
+
38
+ def __enter__(self) -> ApiClient:
39
+ return self
40
+
41
+ def __exit__(self, exc_type: Any, exc: Any, traceback: Any) -> None:
42
+ self.close()
43
+
44
+ def request(self, method: str, path: str, *, body: Any = None, content_type: str = 'application/json', response_model: type[T] | None = None, request_headers: Mapping[str, Any] | None = None) -> T | Any | None:
45
+ try:
46
+ request_kwargs = self._encode_request_body(body, content_type)
47
+ except Exception as exc:
48
+ raise ApiSerializationException("Unable to serialize API request body") from exc
49
+ path = self._append_auth_query(path)
50
+ headers = self._build_headers(content_type)
51
+ if request_headers is not None:
52
+ headers.update({name: str(value) for name, value in request_headers.items() if value is not None})
53
+ max_attempts = 3
54
+ retry_statuses = [408, 409, 429, 500, 502, 503, 504]
55
+ retry_methods = ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE']
56
+ backoff = 0.25
57
+ max_backoff = 2.0
58
+ response = None
59
+ for attempt in range(1, max_attempts + 1):
60
+ try:
61
+ with self._client.stream(method, path, headers=headers, **request_kwargs) as streamed_response:
62
+ declared_length = streamed_response.headers.get('Content-Length')
63
+ if declared_length is not None and int(declared_length) > 10485760:
64
+ raise ApiDeserializationException('API response exceeds the configured size limit')
65
+ content = bytearray()
66
+ for chunk in streamed_response.iter_bytes():
67
+ if len(content) + len(chunk) > 10485760:
68
+ raise ApiDeserializationException('API response exceeds the configured size limit')
69
+ content.extend(chunk)
70
+ # iter_bytes() already decoded the body; drop the encoding headers so
71
+ # the rebuilt response does not decode (e.g. gunzip) it a second time.
72
+ decoded_headers = [
73
+ (name, value) for name, value in streamed_response.headers.multi_items()
74
+ if name.lower() not in ('content-encoding', 'content-length')
75
+ ]
76
+ response = httpx.Response(
77
+ status_code=streamed_response.status_code,
78
+ headers=decoded_headers,
79
+ content=bytes(content),
80
+ )
81
+ except httpx.RequestError:
82
+ if attempt >= max_attempts or method.upper() not in retry_methods or False:
83
+ raise
84
+ time.sleep(min(backoff, max_backoff))
85
+ backoff *= 2
86
+ continue
87
+ if response.status_code not in retry_statuses or method.upper() not in retry_methods or attempt >= max_attempts:
88
+ break
89
+ time.sleep(min(backoff, max_backoff))
90
+ backoff *= 2
91
+ assert response is not None
92
+ if response.status_code >= 400:
93
+ parsed_error: Any
94
+ try:
95
+ parsed_error = response.json()
96
+ except ValueError:
97
+ parsed_error = None
98
+ raise ApiHttpException(
99
+ f'API request failed with status {response.status_code}',
100
+ raw_response=RawResponse(response.status_code, response.headers, response.text),
101
+ parsed_error=parsed_error,
102
+ )
103
+ if not response.content:
104
+ return None
105
+ if response_model is bytes:
106
+ return response.content
107
+ try:
108
+ payload = response.json()
109
+ except ValueError as exc:
110
+ raise ApiDeserializationException(
111
+ "Unable to parse API response JSON",
112
+ raw_response=RawResponse(response.status_code, response.headers, response.text),
113
+ ) from exc
114
+ if response_model is None:
115
+ return payload
116
+ try:
117
+ return TypeAdapter(response_model).validate_python(payload)
118
+ except ValidationError as exc:
119
+ raise ApiResponseValidationException(
120
+ "API response failed schema validation",
121
+ raw_response=RawResponse(response.status_code, response.headers, response.text),
122
+ parsed_error=payload,
123
+ ) from exc
124
+
125
+ def _encode_body(self, body: Any) -> Any:
126
+ if isinstance(body, BaseModel):
127
+ return body.model_dump(mode="json", by_alias=True, exclude_none=True)
128
+ return body
129
+
130
+ def _encode_request_body(self, body: Any, content_type: str) -> dict[str, Any]:
131
+ if body is None:
132
+ return {}
133
+ if content_type == 'multipart/form-data':
134
+ multipart_body = (
135
+ body.model_dump(mode='python', by_alias=True, exclude_none=True)
136
+ if isinstance(body, BaseModel)
137
+ else body
138
+ )
139
+ if not isinstance(multipart_body, Mapping):
140
+ raise ApiSerializationException('Multipart request body must be a mapping')
141
+ parts: dict[str, Any] = {}
142
+ for key, value in multipart_body.items():
143
+ if value is None:
144
+ continue
145
+ if isinstance(value, (bytes, bytearray, memoryview)):
146
+ parts[key] = (key, bytes(value), 'application/octet-stream')
147
+ continue
148
+ if isinstance(value, bool):
149
+ form_value = 'true' if value else 'false'
150
+ elif isinstance(value, (dict, list)):
151
+ form_value = json.dumps(value, separators=(',', ':'))
152
+ else:
153
+ form_value = str(value)
154
+ parts[key] = (None, form_value)
155
+ return {'files': parts}
156
+ encoded = self._encode_body(body)
157
+ if content_type == 'application/json':
158
+ return {'json': encoded}
159
+ if content_type == 'application/x-www-form-urlencoded':
160
+ return {'data': encoded}
161
+ if content_type in {'application/octet-stream', 'application/pdf'} or content_type.startswith('image/'):
162
+ return {'content': encoded}
163
+ return {'content': encoded}
164
+
165
+ def _resolve_auth_value(self, value: str | Callable[[], str] | None) -> str | None:
166
+ if value is None:
167
+ return None
168
+ return value() if callable(value) else value
169
+
170
+ def _build_headers(self, content_type: str = 'application/json') -> dict[str, str]:
171
+ headers = dict(self.headers)
172
+ if content_type != 'multipart/form-data':
173
+ headers.setdefault('Content-Type', content_type)
174
+ api_key = self._resolve_auth_value(self.api_key)
175
+ if api_key:
176
+ headers['X-API-Key'] = api_key
177
+ return headers
178
+
179
+ def _append_auth_query(self, path: str) -> str:
180
+ return path
181
+
182
+ def _build_path(self, path: str, params: Mapping[str, Any]) -> str:
183
+ for key, value in params.items():
184
+ if value is None or value == "":
185
+ raise ApiValidationException(f"Missing required path parameter: {key}")
186
+ path = path.replace("{" + key + "}", quote(str(value), safe=""))
187
+ return path
188
+
189
+ def _append_query(self, path: str, params: Mapping[str, Any]) -> str:
190
+ query_params = {key: value for key, value in params.items() if value is not None}
191
+ if not query_params:
192
+ return path
193
+ return path + "?" + urlencode(query_params, doseq=True)
@@ -0,0 +1,15 @@
1
+ """Generated contract identity for drift detection.
2
+
3
+ SPEC_SHA256 is the canonical hash of the whole OpenAPI document.
4
+ OPERATION_SHA256 is the wire identity of each HTTP operation; login
5
+ compatibility uses the login operation hash, not the document hash.
6
+ """
7
+
8
+ SPEC_SHA256 = "a81ad4404e5cdbbc52afdc4a70c033345aa3fe39074cc42c9f30b4e082f30fa8"
9
+ CONTRACT_VERSION = "0.1.0"
10
+ CONTRACT_HASH_ALGORITHM = "sha256-canonical-json-v1"
11
+ OPERATION_HASH_ALGORITHM = "sha256-canonical-json-operation-v1"
12
+ OPERATION_SHA256 = {
13
+ "GET /v1/health": "5ee627b0358ea87788b51bfe6e9dceca9e8c790b52d52d3e53e626d72cd80f46",
14
+ "POST /v1/decide": "64422b005c949027ebc3599e842c9f9ae1b4a4f2e5dbfd727ec4a49f629d508a"
15
+ }
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Mapping
5
+
6
+
7
+ @dataclass(slots=True)
8
+ class RawResponse:
9
+ status_code: int | None
10
+ headers: Mapping[str, str]
11
+ body: str | bytes | None
12
+
13
+
14
+ class ApiErrorBody(dict):
15
+ pass
16
+
17
+
18
+ class ApiException(Exception):
19
+ def __init__(self, message: str, *, raw_response: RawResponse | None = None, parsed_error: Any = None) -> None:
20
+ super().__init__(message)
21
+ self.raw_response = raw_response
22
+ self.parsed_error = parsed_error
23
+
24
+
25
+ class ApiHttpException(ApiException):
26
+ pass
27
+
28
+
29
+ class ApiValidationException(ApiException):
30
+ pass
31
+
32
+
33
+ class ApiResponseValidationException(ApiException):
34
+ pass
35
+
36
+
37
+ class ApiNetworkException(ApiException):
38
+ pass
39
+
40
+
41
+ class ApiSerializationException(ApiException):
42
+ pass
43
+
44
+
45
+ class ApiDeserializationException(ApiException):
46
+ pass
@@ -0,0 +1,191 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import warnings
5
+ from datetime import date, datetime
6
+ from typing import Annotated, Any, Literal
7
+
8
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
9
+
10
+
11
+ _LOGGER = logging.getLogger(__name__)
12
+
13
+ _SEEN_UNKNOWN_FIELDS: set[tuple[str, tuple[str, ...]]] = set()
14
+
15
+
16
+ class _UnknownFieldWarningModel(BaseModel):
17
+ """Accepts unknown fields but reports them once per model and field set.
18
+
19
+ The unknown_fields policy for this SDK is "warn". Parsing stays permissive
20
+ on purpose: a producer adding a field must not break a consumer mid-rolling
21
+ deploy. But silence is how a renamed field becomes a None that surfaces as a
22
+ confusing error several layers away, so each new shape is logged once --
23
+ deduplicated because these models are parsed per response and an unbounded
24
+ log would be worse than no log at all.
25
+ """
26
+
27
+ @model_validator(mode="after")
28
+ def _report_unknown_fields(self):
29
+ extra = self.model_extra
30
+ if not extra:
31
+ return self
32
+ names = tuple(sorted(extra))
33
+ key = (type(self).__name__, names)
34
+ if key not in _SEEN_UNKNOWN_FIELDS:
35
+ _SEEN_UNKNOWN_FIELDS.add(key)
36
+ _LOGGER.warning(
37
+ "%s received field(s) not declared by the contract: %s. "
38
+ "The producer may be ahead of this generated SDK; regenerate it "
39
+ "if these fields are expected.",
40
+ type(self).__name__,
41
+ ", ".join(names),
42
+ )
43
+ return self
44
+
45
+
46
+ class ComposedVerdict(_UnknownFieldWarningModel):
47
+ model_config = ConfigDict(populate_by_name=True, extra='allow')
48
+ band: str | None = None
49
+ rule_ids: list[str] | None = None
50
+ verdict: str
51
+
52
+
53
+ class DecideFailureDetail(_UnknownFieldWarningModel):
54
+ model_config = ConfigDict(populate_by_name=True, extra='allow')
55
+ failure_code: str
56
+ message: str
57
+
58
+
59
+ class DecideFailureResponse(_UnknownFieldWarningModel):
60
+ model_config = ConfigDict(populate_by_name=True, extra='allow')
61
+ detail: DecideFailureDetail
62
+
63
+
64
+ with warnings.catch_warnings():
65
+ # Contract field(s) intentionally shadow pydantic BaseModel attributes: schema.
66
+ warnings.filterwarnings('ignore', message='^Field name "schema" in "DecideRequest" shadows an attribute in parent "_UnknownFieldWarningModel"$', category=UserWarning)
67
+ class DecideRequest(_UnknownFieldWarningModel):
68
+ model_config = ConfigDict(populate_by_name=True, extra='allow')
69
+ answer_under_test: Any | None = None
70
+ evidence: list[EvidenceItem] | None = None
71
+ goal: str
72
+ questions: list[DecisionQuestion]
73
+ require: DecideRequire | None = None
74
+ schema: Literal['helixor.decide_request.v1'] = Field(default='helixor.decide_request.v1')
75
+ scope: DecideScope | None = None
76
+
77
+
78
+ class DecideRequire(_UnknownFieldWarningModel):
79
+ model_config = ConfigDict(populate_by_name=True, extra='allow')
80
+ max_latency_ms: int | None = None
81
+ min_confidence: float | None = None
82
+ proof: bool | None = None
83
+
84
+
85
+ class DecideScope(_UnknownFieldWarningModel):
86
+ model_config = ConfigDict(populate_by_name=True, extra='allow')
87
+ domain: str | None = None
88
+ org: str | None = None
89
+ session: str | None = None
90
+ tenant: str | None = None
91
+ user: str | None = None
92
+
93
+
94
+ class DecisionCost(_UnknownFieldWarningModel):
95
+ model_config = ConfigDict(populate_by_name=True, extra='allow')
96
+ units: int
97
+
98
+
99
+ class DecisionOption(_UnknownFieldWarningModel):
100
+ model_config = ConfigDict(populate_by_name=True, extra='allow')
101
+ criteria: str | None = None
102
+ label: str | None = None
103
+ option_id: str
104
+
105
+
106
+ class DecisionProof(_UnknownFieldWarningModel):
107
+ model_config = ConfigDict(populate_by_name=True, extra='allow')
108
+ artifact: str | None = None
109
+ checks: list[str] | None = None
110
+ citations: list[str] | None = None
111
+ digest: str | None = None
112
+ kind: Literal['replay', 'solver_certificate', 'citation_set', 'tcn_consistent', 're_derivation_agreement', 'distribution_threshold']
113
+ threshold: float | None = None
114
+
115
+
116
+ with warnings.catch_warnings():
117
+ # Contract field(s) intentionally shadow pydantic BaseModel attributes: schema.
118
+ warnings.filterwarnings('ignore', message='^Field name "schema" in "DecisionQuestion" shadows an attribute in parent "_UnknownFieldWarningModel"$', category=UserWarning)
119
+ class DecisionQuestion(_UnknownFieldWarningModel):
120
+ model_config = ConfigDict(populate_by_name=True, extra='allow')
121
+ context_refs: list[str] | None = None
122
+ family_id: str
123
+ kind: Literal['choice', 'score', 'boolean']
124
+ metadata: dict[str, Any] | None = None
125
+ options: list[DecisionOption]
126
+ prompt: str
127
+ question_id: str
128
+ schema: Literal['helixor.decision_question.v1'] = Field(default='helixor.decision_question.v1')
129
+
130
+
131
+ with warnings.catch_warnings():
132
+ # Contract field(s) intentionally shadow pydantic BaseModel attributes: schema.
133
+ warnings.filterwarnings('ignore', message='^Field name "schema" in "DecisionResponse" shadows an attribute in parent "_UnknownFieldWarningModel"$', category=UserWarning)
134
+ class DecisionResponse(_UnknownFieldWarningModel):
135
+ model_config = ConfigDict(populate_by_name=True, extra='allow')
136
+ composed: ComposedVerdict | None = None
137
+ cost: DecisionCost | None = None
138
+ helix: HelixDecisionNote | None = None
139
+ latency_ms: int | None = None
140
+ proof_id: str | None = None
141
+ schema: Literal['helixor.decision.v1'] = Field(default='helixor.decision.v1')
142
+ verdicts: dict[str, QuestionVerdict]
143
+
144
+
145
+ class EvidenceItem(_UnknownFieldWarningModel):
146
+ model_config = ConfigDict(populate_by_name=True, extra='allow')
147
+ fields: dict[str, Any] | None = None
148
+ id: str
149
+ source: str | None = None
150
+ text: str | None = None
151
+
152
+
153
+ class HTTPValidationError(_UnknownFieldWarningModel):
154
+ model_config = ConfigDict(populate_by_name=True, extra='allow')
155
+ detail: list[ValidationError] | None = None
156
+
157
+
158
+ class HealthResponse(_UnknownFieldWarningModel):
159
+ model_config = ConfigDict(populate_by_name=True, extra='allow')
160
+ capabilities_loaded: int
161
+ capabilities_total: int
162
+ component: str
163
+ status: str
164
+ version: str
165
+
166
+
167
+ class HelixDecisionNote(_UnknownFieldWarningModel):
168
+ model_config = ConfigDict(populate_by_name=True, extra='allow')
169
+ degraded: list[dict[str, Any]] | None = None
170
+ fingerprint: str | None = None
171
+ version: str | None = None
172
+
173
+
174
+ class QuestionVerdict(_UnknownFieldWarningModel):
175
+ model_config = ConfigDict(populate_by_name=True, extra='allow')
176
+ masked: list[str] | None = None
177
+ p_correct: float | None = None
178
+ proof: DecisionProof | None = None
179
+ reason: Literal['not_certified', 'insufficient_evidence', 'over_budget'] | Literal['malformed', 'disallowed'] | None = None
180
+ value: Any | None = None
181
+ verdict: Literal['answer', 'abstain', 'refuse']
182
+ would_unmask: str | None = None
183
+
184
+
185
+ class ValidationError(_UnknownFieldWarningModel):
186
+ model_config = ConfigDict(populate_by_name=True, extra='allow')
187
+ ctx: dict[str, Any] | None = None
188
+ input: Any | None = None
189
+ loc: list[str | int]
190
+ msg: str
191
+ type: str
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+
5
+ from typing import Any, Literal
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+
10
+ class ContractRef(BaseModel):
11
+ content_types: list[str] = Field(default_factory=list)
12
+ schema_ref: str | None = None
13
+ schema_value: dict[str, Any] = Field(default_factory=dict, alias='schema')
14
+
15
+
16
+ class RequestContract(ContractRef):
17
+ required: bool = False
18
+
19
+
20
+ class ResponseContract(ContractRef):
21
+ status_code: str | None = None
22
+
23
+
24
+ class ErrorContract(ResponseContract):
25
+ envelope: dict[str, Any] = Field(default_factory=dict)
26
+
27
+
28
+ class OperationParameterContract(BaseModel):
29
+ name: str
30
+ location: str = Field(alias='in')
31
+ required: bool = False
32
+ schema_ref: str | None = None
33
+ schema_value: dict[str, Any] = Field(default_factory=dict, alias='schema')
34
+
35
+
36
+ class WorkflowContract(BaseModel):
37
+ query_key_strategy: str
38
+ audit_tags: list[str] = Field(default_factory=list)
39
+
40
+
41
+ class OperationContract(BaseModel):
42
+ operation_id: str = Field(alias='operationId')
43
+ kind: Literal['query', 'mutation']
44
+ method: str
45
+ path: str
46
+ sha256: str = ''
47
+ tags: list[str] = Field(default_factory=list)
48
+ summary: str = ''
49
+ idempotent: bool
50
+ retryable: bool
51
+ cache_key: list[str] = Field(alias='cacheKey')
52
+ parameters: list[OperationParameterContract] = Field(default_factory=list)
53
+ request_contract: RequestContract = Field(alias='requestContract')
54
+ response_contract: ResponseContract = Field(alias='responseContract')
55
+ error_contract: ErrorContract = Field(alias='errorContract')
56
+ auth: dict[str, Any]
57
+ pagination: dict[str, Any] | None = None
58
+ workflow: WorkflowContract
59
+
60
+
61
+ class OperationContractsManifest(BaseModel):
62
+ schema_version: str
63
+ package_name: str
64
+ base_url: str | None = None
65
+ auth: dict[str, Any]
66
+ runtime: dict[str, Any]
67
+ resources: dict[str, Any]
68
+ validation: dict[str, Any]
69
+ error_handling: dict[str, Any]
70
+ operations: list[OperationContract]
71
+ diagnostics: list[str] = Field(default_factory=list)
72
+
73
+ def by_operation_id(self) -> dict[str, OperationContract]:
74
+ return {operation.operation_id: operation for operation in self.operations}
75
+
76
+
77
+ RAW_OPERATION_CONTRACTS = json.loads("{\n \"auth\": {\n \"auto_detect\": true,\n \"default_provider\": null,\n \"enabled\": true,\n \"fail_on_missing_required\": true,\n \"headers\": {},\n \"operations\": {},\n \"providers\": {},\n \"schemes\": {\n \"apiKeyAuth\": {\n \"env_var\": \"HELIXOR_API_KEY\",\n \"location\": \"header\",\n \"name\": \"X-API-Key\",\n \"prefix\": \"Bearer\",\n \"required\": false,\n \"scopes\": [],\n \"security_scheme\": \"apiKeyAuth\",\n \"type\": \"api_key\"\n }\n }\n },\n \"base_url\": null,\n \"diagnostics\": [],\n \"error_handling\": {\n \"envelope\": {\n \"fields\": {\n \"code\": \"code\",\n \"details\": \"details\",\n \"message\": \"message\",\n \"request_id\": \"requestId\",\n \"timestamp\": \"timestamp\",\n \"trace_id\": \"traceId\"\n },\n \"import_from\": null,\n \"schema_ref\": null,\n \"source\": \"openapi\",\n \"type_name\": \"ApiErrorBody\"\n },\n \"exceptions\": {\n \"base_class\": \"ApiException\",\n \"deserialization_exception\": \"ApiDeserializationException\",\n \"http_exception\": \"ApiHttpException\",\n \"import_from\": null,\n \"network_exception\": \"ApiNetworkException\",\n \"response_validation_exception\": \"ApiResponseValidationException\",\n \"serialization_exception\": \"ApiSerializationException\",\n \"source\": \"generate\",\n \"validation_exception\": \"ApiValidationException\"\n },\n \"mode\": \"throw\",\n \"raw_payload\": {\n \"include_body\": true,\n \"include_headers\": true,\n \"max_body_bytes\": 65536\n },\n \"unknown_error_body\": \"wrap_raw\"\n },\n \"operations\": [\n {\n \"auth\": {\n \"headers\": [],\n \"required\": true,\n \"schemes\": [\n {\n \"configured\": true,\n \"env_var\": null,\n \"location\": \"header\",\n \"name\": \"ApiKeyAuth\",\n \"prefix\": null,\n \"required\": true,\n \"scopes\": [],\n \"security_scheme\": \"ApiKeyAuth\",\n \"type\": \"api_key\"\n }\n ],\n \"security\": [\n {\n \"ApiKeyAuth\": []\n }\n ]\n },\n \"cacheKey\": [\n \"decide\"\n ],\n \"errorContract\": {\n \"content_types\": [\n \"application/json\"\n ],\n \"envelope\": {\n \"fields\": {\n \"code\": \"code\",\n \"details\": \"details\",\n \"message\": \"message\",\n \"request_id\": \"requestId\",\n \"timestamp\": \"timestamp\",\n \"trace_id\": \"traceId\"\n },\n \"import_from\": null,\n \"schema_ref\": null,\n \"source\": \"openapi\",\n \"type_name\": \"ApiErrorBody\"\n },\n \"schema\": {},\n \"schema_ref\": \"#/components/schemas/DecideFailureResponse\",\n \"status_code\": \"404\"\n },\n \"idempotent\": false,\n \"kind\": \"mutation\",\n \"method\": \"POST\",\n \"operationId\": \"decide\",\n \"parameters\": [],\n \"path\": \"/v1/decide\",\n \"requestContract\": {\n \"content_types\": [\n \"application/json\"\n ],\n \"contents\": [\n {\n \"content_type\": \"application/json\",\n \"schema\": {},\n \"schema_ref\": \"#/components/schemas/DecideRequest\"\n }\n ],\n \"default_content_type\": \"application/json\",\n \"required\": true,\n \"schema\": {},\n \"schema_ref\": \"#/components/schemas/DecideRequest\"\n },\n \"resource\": {\n \"language_names\": {\n \"csharp\": \"DecideAsync\",\n \"java\": \"decide\",\n \"python\": \"decide\",\n \"rust\": \"decide\",\n \"typescript\": \"decide\"\n },\n \"method_name\": \"decide\",\n \"name\": \"public\"\n },\n \"responseContract\": {\n \"content_types\": [\n \"application/json\"\n ],\n \"schema\": {},\n \"schema_ref\": \"#/components/schemas/DecisionResponse\",\n \"status_code\": \"200\"\n },\n \"retryable\": false,\n \"runtime\": {\n \"idempotency\": {\n \"applies\": false,\n \"enabled\": false,\n \"header_name\": \"Idempotency-Key\",\n \"methods\": [\n \"POST\",\n \"PATCH\"\n ]\n },\n \"retry\": {\n \"enabled\": true,\n \"initial_backoff_seconds\": 0.25,\n \"max_attempts\": 3,\n \"max_backoff_seconds\": 2.0,\n \"retry_methods\": [\n \"GET\",\n \"HEAD\",\n \"OPTIONS\",\n \"PUT\",\n \"DELETE\"\n ],\n \"retry_on_network_error\": true,\n \"retry_statuses\": [\n 408,\n 409,\n 429,\n 500,\n 502,\n 503,\n 504\n ],\n \"retryable\": false\n },\n \"timeout\": {\n \"total_seconds\": 60.0\n }\n },\n \"sha256\": \"64422b005c949027ebc3599e842c9f9ae1b4a4f2e5dbfd727ec4a49f629d508a\",\n \"summary\": \"Decide\",\n \"tags\": [\n \"public\"\n ],\n \"workflow\": {\n \"audit_tags\": [\n \"sdk\",\n \"mutation\",\n \"post\"\n ],\n \"query_key_strategy\": \"operation_and_params\"\n }\n },\n {\n \"auth\": {\n \"headers\": [],\n \"required\": true,\n \"schemes\": [\n {\n \"configured\": true,\n \"env_var\": null,\n \"location\": \"header\",\n \"name\": \"ApiKeyAuth\",\n \"prefix\": null,\n \"required\": true,\n \"scopes\": [],\n \"security_scheme\": \"ApiKeyAuth\",\n \"type\": \"api_key\"\n }\n ],\n \"security\": [\n {\n \"ApiKeyAuth\": []\n }\n ]\n },\n \"cacheKey\": [\n \"getHealth\"\n ],\n \"errorContract\": {\n \"content_types\": [],\n \"envelope\": {\n \"fields\": {\n \"code\": \"code\",\n \"details\": \"details\",\n \"message\": \"message\",\n \"request_id\": \"requestId\",\n \"timestamp\": \"timestamp\",\n \"trace_id\": \"traceId\"\n },\n \"import_from\": null,\n \"schema_ref\": null,\n \"source\": \"openapi\",\n \"type_name\": \"ApiErrorBody\"\n },\n \"schema\": {},\n \"schema_ref\": null,\n \"status_code\": null\n },\n \"idempotent\": true,\n \"kind\": \"query\",\n \"method\": \"GET\",\n \"operationId\": \"getHealth\",\n \"parameters\": [],\n \"path\": \"/v1/health\",\n \"requestContract\": {\n \"content_types\": [],\n \"contents\": [],\n \"default_content_type\": null,\n \"required\": false,\n \"schema\": {},\n \"schema_ref\": null\n },\n \"resource\": {\n \"language_names\": {\n \"csharp\": \"GetHealthAsync\",\n \"java\": \"getHealth\",\n \"python\": \"get_health\",\n \"rust\": \"get_health\",\n \"typescript\": \"getHealth\"\n },\n \"method_name\": \"getHealth\",\n \"name\": \"public\"\n },\n \"responseContract\": {\n \"content_types\": [\n \"application/json\"\n ],\n \"schema\": {},\n \"schema_ref\": \"#/components/schemas/HealthResponse\",\n \"status_code\": \"200\"\n },\n \"retryable\": true,\n \"runtime\": {\n \"idempotency\": {\n \"applies\": false,\n \"enabled\": false,\n \"header_name\": \"Idempotency-Key\",\n \"methods\": [\n \"POST\",\n \"PATCH\"\n ]\n },\n \"retry\": {\n \"enabled\": true,\n \"initial_backoff_seconds\": 0.25,\n \"max_attempts\": 3,\n \"max_backoff_seconds\": 2.0,\n \"retry_methods\": [\n \"GET\",\n \"HEAD\",\n \"OPTIONS\",\n \"PUT\",\n \"DELETE\"\n ],\n \"retry_on_network_error\": true,\n \"retry_statuses\": [\n 408,\n 409,\n 429,\n 500,\n 502,\n 503,\n 504\n ],\n \"retryable\": true\n },\n \"timeout\": {\n \"total_seconds\": 60.0\n }\n },\n \"sha256\": \"5ee627b0358ea87788b51bfe6e9dceca9e8c790b52d52d3e53e626d72cd80f46\",\n \"summary\": \"Health\",\n \"tags\": [\n \"public\"\n ],\n \"workflow\": {\n \"audit_tags\": [\n \"sdk\",\n \"query\",\n \"get\"\n ],\n \"query_key_strategy\": \"operation_and_params\"\n }\n }\n ],\n \"package_name\": \"helixor-reasoner\",\n \"pagination\": {\n \"auto_detect\": false,\n \"enabled\": true,\n \"operations\": {},\n \"patterns\": {}\n },\n \"resources\": {\n \"enabled\": true,\n \"infer_from_tags\": true,\n \"max_python_methods_per_class\": 40,\n \"oversized_python_group_severity\": \"warning\",\n \"resources\": {}\n },\n \"runtime\": {\n \"idempotency\": {\n \"enabled\": false,\n \"header_name\": \"Idempotency-Key\",\n \"methods\": [\n \"POST\",\n \"PATCH\"\n ]\n },\n \"max_response_bytes\": 10485760,\n \"retry\": {\n \"enabled\": true,\n \"initial_backoff_seconds\": 0.25,\n \"max_attempts\": 3,\n \"max_backoff_seconds\": 2.0,\n \"retry_methods\": [\n \"GET\",\n \"HEAD\",\n \"OPTIONS\",\n \"PUT\",\n \"DELETE\"\n ],\n \"retry_on_network_error\": true,\n \"retry_statuses\": [\n 408,\n 409,\n 429,\n 500,\n 502,\n 503,\n 504\n ]\n },\n \"timeout\": {\n \"total_seconds\": 60.0\n }\n },\n \"schema_version\": \"sdk.operation-contracts.v1\",\n \"validation\": {\n \"error_response\": \"strict\",\n \"raw_payload_capture\": true,\n \"request\": \"strict\",\n \"response\": \"strict\",\n \"unknown_fields\": \"warn\"\n }\n}")
78
+
79
+ OPERATION_CONTRACTS = OperationContractsManifest.model_validate(RAW_OPERATION_CONTRACTS)
File without changes
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "helixor-reasoner"
7
+ version = "0.2.0"
8
+ description = "Typed client for the Helixor reasoning API: decide, fetch proofs, report outcomes."
9
+ readme = "README.md"
10
+ license = "Apache-2.0"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.11"
13
+ authors = [{name = "Helixor", email = "engineering@helixor.ai"}]
14
+ keywords = ["helixor", "reasoning", "decisions", "sdk"]
15
+ classifiers = [
16
+ "Intended Audience :: Developers",
17
+ "Operating System :: OS Independent",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3 :: Only",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Typing :: Typed",
24
+ ]
25
+ dependencies = ["httpx>=0.27", "pydantic>=2.7"]
26
+
27
+ [project.urls]
28
+ Homepage = "https://helixor.dev"
29
+
30
+ [tool.hatch.build.targets.wheel]
31
+ packages = ["helixor_reasoner"]
32
+ core-metadata-version = "2.4"
33
+
34
+ [tool.hatch.build.targets.sdist]
35
+ include = ["/helixor_reasoner", "/README.md", "/LICENSE", "/pyproject.toml"]
36
+ core-metadata-version = "2.4"