nxuskit-py 1.0.3__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 (52) hide show
  1. nxuskit/__init__.py +287 -0
  2. nxuskit/__init__.pyi +248 -0
  3. nxuskit/_bn_ffi.py +64 -0
  4. nxuskit/_clips_ffi.py +61 -0
  5. nxuskit/_ffi.py +431 -0
  6. nxuskit/_ffi_errors.py +79 -0
  7. nxuskit/_ffi_provider.py +263 -0
  8. nxuskit/_ffi_types.py +124 -0
  9. nxuskit/_solver_ffi.py +61 -0
  10. nxuskit/_version.py +5 -0
  11. nxuskit/_zen_ffi.py +61 -0
  12. nxuskit/auth.py +323 -0
  13. nxuskit/auth_oauth.py +125 -0
  14. nxuskit/bn.py +485 -0
  15. nxuskit/clips.py +547 -0
  16. nxuskit/errors.py +85 -0
  17. nxuskit/libs/README.md +37 -0
  18. nxuskit/license.py +308 -0
  19. nxuskit/message.py +45 -0
  20. nxuskit/mock.py +84 -0
  21. nxuskit/plugin_trust.py +127 -0
  22. nxuskit/provider.py +97 -0
  23. nxuskit/providers/__init__.py +5 -0
  24. nxuskit/providers/base.py +209 -0
  25. nxuskit/providers/claude.py +378 -0
  26. nxuskit/providers/factory.py +329 -0
  27. nxuskit/providers/fireworks.py +41 -0
  28. nxuskit/providers/groq.py +41 -0
  29. nxuskit/providers/lmstudio.py +48 -0
  30. nxuskit/providers/mistral.py +41 -0
  31. nxuskit/providers/ollama.py +288 -0
  32. nxuskit/providers/openai.py +41 -0
  33. nxuskit/providers/openai_compatible.py +310 -0
  34. nxuskit/providers/openrouter.py +44 -0
  35. nxuskit/providers/perplexity.py +41 -0
  36. nxuskit/providers/together.py +41 -0
  37. nxuskit/providers/xai.py +41 -0
  38. nxuskit/py.typed +0 -0
  39. nxuskit/retry.py +308 -0
  40. nxuskit/security.py +144 -0
  41. nxuskit/solver.py +407 -0
  42. nxuskit/solver_types.py +396 -0
  43. nxuskit/streaming.py +218 -0
  44. nxuskit/tools.py +124 -0
  45. nxuskit/types.py +548 -0
  46. nxuskit/vision.py +259 -0
  47. nxuskit/zen.py +91 -0
  48. nxuskit_py-1.0.3.dist-info/METADATA +302 -0
  49. nxuskit_py-1.0.3.dist-info/RECORD +52 -0
  50. nxuskit_py-1.0.3.dist-info/WHEEL +4 -0
  51. nxuskit_py-1.0.3.dist-info/licenses/LICENSE +14 -0
  52. nxuskit_py-1.0.3.dist-info/licenses/NOTICE +10 -0
nxuskit/__init__.py ADDED
@@ -0,0 +1,287 @@
1
+ """nxuskit - Pure Python library mirroring nxusKit Rust API."""
2
+
3
+ from nxuskit._ffi_errors import (
4
+ EditionInsufficientError,
5
+ FeatureUnavailableError,
6
+ LicenseExpiredError,
7
+ LicenseRequiredError,
8
+ NxuskitError,
9
+ )
10
+ from nxuskit._version import __author__, __license__, __version__
11
+ from nxuskit.errors import (
12
+ AuthenticationError,
13
+ LLMError,
14
+ NetworkError,
15
+ ProviderError,
16
+ RateLimitError,
17
+ TimeoutError,
18
+ )
19
+ from nxuskit.message import Message
20
+ from nxuskit.provider import LLMProvider
21
+ from nxuskit.providers import Provider
22
+ from nxuskit.retry import (
23
+ AdaptiveRateLimiter,
24
+ RetryConfig,
25
+ RetryIterator,
26
+ retry_on_rate_limit,
27
+ retry_with_backoff,
28
+ should_retry,
29
+ )
30
+ from nxuskit.security import (
31
+ SecurityIssue,
32
+ SecuritySeverity,
33
+ SecurityValidationResult,
34
+ SecurityValidator,
35
+ )
36
+ from nxuskit.solver_types import (
37
+ ConstraintDef,
38
+ ConstraintType,
39
+ DomainDef,
40
+ MultiObjectiveMode,
41
+ ObjectiveDef,
42
+ ObjectiveDirection,
43
+ SessionStatus,
44
+ SolverCapabilities,
45
+ SolverConfig,
46
+ SolveResult,
47
+ SolverExplanation,
48
+ SolverStats,
49
+ SolverValue,
50
+ SolveStatus,
51
+ VariableDef,
52
+ VariableType,
53
+ )
54
+ from nxuskit.streaming import (
55
+ StreamBuffer,
56
+ collect_stream,
57
+ stream_to_file,
58
+ stream_with_callback,
59
+ )
60
+ from nxuskit.tools import (
61
+ FunctionCall,
62
+ FunctionDefinition,
63
+ ToolCall,
64
+ ToolDefinition,
65
+ ToolResultMessage,
66
+ tool_choice_auto,
67
+ tool_choice_named,
68
+ tool_choice_none,
69
+ tool_choice_required,
70
+ )
71
+ from nxuskit.types import (
72
+ PUBLIC_CAPABILITY_FIELDS,
73
+ CapabilityStatus,
74
+ ChatRequest,
75
+ ChatResponse,
76
+ ImageSource,
77
+ ImageSourceType,
78
+ LogprobsData,
79
+ ManifestPublicationPosture,
80
+ ModelInfo,
81
+ PublicCapabilityManifest,
82
+ PublicProviderCapability,
83
+ ResponseFormat,
84
+ Role,
85
+ StreamChunk,
86
+ TokenLogprob,
87
+ TokenUsage,
88
+ TopLogprob,
89
+ )
90
+ from nxuskit.vision import (
91
+ ImageLoader,
92
+ add_images_to_message,
93
+ detect_image_type,
94
+ image_to_data_url,
95
+ is_base64,
96
+ is_valid_url,
97
+ load_image_base64,
98
+ )
99
+
100
+ # FFI-dependent modules are imported lazily to allow pure-Python usage
101
+ # (unit tests, type inspection) without the native library present.
102
+ # These modules load libnxuskit at import time via _ffi.py.
103
+ _FFI_NAMES = {
104
+ # auth_oauth
105
+ "OAuthResult",
106
+ "OAuthStatus",
107
+ "oauth_start",
108
+ "oauth_status",
109
+ "oauth_revoke",
110
+ # clips
111
+ "ClipsSession",
112
+ "ClipsError",
113
+ # license
114
+ "ActivationResult",
115
+ "LicenseResolution",
116
+ "TokenInfo",
117
+ "TrialResult",
118
+ "license_activate",
119
+ "license_deactivate",
120
+ "license_machine_id",
121
+ "license_resolve",
122
+ "license_trial_activate",
123
+ "license_trial_issue",
124
+ "license_validate",
125
+ # solver (stream chunk requires _solver_ffi)
126
+ "SolverStreamChunk",
127
+ # zen
128
+ "zen_evaluate",
129
+ "zen_evaluate_async",
130
+ }
131
+
132
+ _FFI_MODULE_MAP = {
133
+ "OAuthResult": "nxuskit.auth_oauth",
134
+ "OAuthStatus": "nxuskit.auth_oauth",
135
+ "oauth_start": "nxuskit.auth_oauth",
136
+ "oauth_status": "nxuskit.auth_oauth",
137
+ "oauth_revoke": "nxuskit.auth_oauth",
138
+ "ClipsSession": "nxuskit.clips",
139
+ "ClipsError": "nxuskit.clips",
140
+ "ActivationResult": "nxuskit.license",
141
+ "LicenseResolution": "nxuskit.license",
142
+ "TokenInfo": "nxuskit.license",
143
+ "TrialResult": "nxuskit.license",
144
+ "license_activate": "nxuskit.license",
145
+ "license_deactivate": "nxuskit.license",
146
+ "license_machine_id": "nxuskit.license",
147
+ "license_resolve": "nxuskit.license",
148
+ "license_trial_activate": "nxuskit.license",
149
+ "license_trial_issue": "nxuskit.license",
150
+ "license_validate": "nxuskit.license",
151
+ "SolverStreamChunk": "nxuskit.solver",
152
+ "zen_evaluate": "nxuskit.zen",
153
+ "zen_evaluate_async": "nxuskit.zen",
154
+ }
155
+
156
+
157
+ def __getattr__(name: str):
158
+ if name in _FFI_NAMES:
159
+ import importlib
160
+
161
+ module = importlib.import_module(_FFI_MODULE_MAP[name])
162
+ value = getattr(module, name)
163
+ # Cache in module namespace for subsequent access
164
+ globals()[name] = value
165
+ return value
166
+ raise AttributeError(f"module 'nxuskit' has no attribute {name!r}")
167
+
168
+
169
+ __all__ = [
170
+ "__version__",
171
+ "__author__",
172
+ "__license__",
173
+ # Types
174
+ "Role",
175
+ "ImageSourceType",
176
+ "ImageSource",
177
+ "TokenUsage",
178
+ "ChatRequest",
179
+ "ChatResponse",
180
+ "StreamChunk",
181
+ "ModelInfo",
182
+ "ResponseFormat",
183
+ "CapabilityStatus",
184
+ "ManifestPublicationPosture",
185
+ "PUBLIC_CAPABILITY_FIELDS",
186
+ "PublicProviderCapability",
187
+ "PublicCapabilityManifest",
188
+ "LogprobsData",
189
+ "TokenLogprob",
190
+ "TopLogprob",
191
+ # Message
192
+ "Message",
193
+ # Errors
194
+ "LLMError",
195
+ "AuthenticationError",
196
+ "RateLimitError",
197
+ "NetworkError",
198
+ "ProviderError",
199
+ "TimeoutError",
200
+ # FFI / entitlement errors
201
+ "NxuskitError",
202
+ "FeatureUnavailableError",
203
+ "LicenseRequiredError",
204
+ "LicenseExpiredError",
205
+ "EditionInsufficientError",
206
+ # Provider protocol
207
+ "LLMProvider",
208
+ # Provider factory
209
+ "Provider",
210
+ # Streaming utilities
211
+ "collect_stream",
212
+ "stream_with_callback",
213
+ "stream_to_file",
214
+ "StreamBuffer",
215
+ # Vision utilities
216
+ "load_image_base64",
217
+ "detect_image_type",
218
+ "is_valid_url",
219
+ "is_base64",
220
+ "add_images_to_message",
221
+ "image_to_data_url",
222
+ "ImageLoader",
223
+ # Retry utilities
224
+ "RetryConfig",
225
+ "should_retry",
226
+ "retry_with_backoff",
227
+ "retry_on_rate_limit",
228
+ "RetryIterator",
229
+ "AdaptiveRateLimiter",
230
+ # Solver types
231
+ "SolverStreamChunk",
232
+ "VariableType",
233
+ "VariableDef",
234
+ "DomainDef",
235
+ "ConstraintType",
236
+ "ConstraintDef",
237
+ "ObjectiveDirection",
238
+ "ObjectiveDef",
239
+ "MultiObjectiveMode",
240
+ "SolverConfig",
241
+ "SolveStatus",
242
+ "SolverValue",
243
+ "SolverStats",
244
+ "SolverExplanation",
245
+ "SolveResult",
246
+ "SolverCapabilities",
247
+ "SessionStatus",
248
+ # Tool calling
249
+ "ToolDefinition",
250
+ "FunctionDefinition",
251
+ "ToolCall",
252
+ "FunctionCall",
253
+ "ToolResultMessage",
254
+ "tool_choice_auto",
255
+ "tool_choice_none",
256
+ "tool_choice_required",
257
+ "tool_choice_named",
258
+ # CLIPS Session (FFI-dependent, lazy-loaded)
259
+ "ClipsSession",
260
+ "ClipsError",
261
+ # License management (FFI-dependent, lazy-loaded)
262
+ "ActivationResult",
263
+ "LicenseResolution",
264
+ "TokenInfo",
265
+ "TrialResult",
266
+ "license_activate",
267
+ "license_deactivate",
268
+ "license_machine_id",
269
+ "license_resolve",
270
+ "license_trial_activate",
271
+ "license_trial_issue",
272
+ "license_validate",
273
+ # OAuth authentication (FFI-dependent, lazy-loaded)
274
+ "OAuthResult",
275
+ "OAuthStatus",
276
+ "oauth_start",
277
+ "oauth_status",
278
+ "oauth_revoke",
279
+ # ZEN evaluation (FFI-dependent, lazy-loaded)
280
+ "zen_evaluate",
281
+ "zen_evaluate_async",
282
+ # Security validation
283
+ "SecurityValidator",
284
+ "SecurityValidationResult",
285
+ "SecurityIssue",
286
+ "SecuritySeverity",
287
+ ]
nxuskit/__init__.pyi ADDED
@@ -0,0 +1,248 @@
1
+ """PEP 561 type stubs for the nxuskit package."""
2
+
3
+ from dataclasses import dataclass
4
+ from enum import Enum
5
+ from typing import Any, Iterator, Optional, Protocol, Union
6
+
7
+ # ── Core Types ──────────────────────────────────────────────────
8
+
9
+ class CapabilityStatus(str, Enum):
10
+ SUPPORTED: str
11
+ UNSUPPORTED: str
12
+ RECOGNIZED: str
13
+ PROVIDER_SPECIFIC: str
14
+ FUTURE: str
15
+ UNKNOWN: str
16
+
17
+ class ManifestPublicationPosture(str, Enum):
18
+ SPLIT: str
19
+
20
+ PUBLIC_CAPABILITY_FIELDS: tuple[str, ...]
21
+
22
+ @dataclass
23
+ class PublicProviderCapability:
24
+ name: str
25
+ display_name: str
26
+ last_reviewed_on: str
27
+ provider_status: str
28
+ capabilities: dict[str, CapabilityStatus]
29
+ def to_dict(self) -> dict[str, Any]: ...
30
+
31
+ @dataclass
32
+ class PublicCapabilityManifest:
33
+ schema_version: str
34
+ posture: ManifestPublicationPosture
35
+ providers: list[PublicProviderCapability]
36
+ def to_dict(self) -> dict[str, Any]: ...
37
+
38
+ @dataclass
39
+ class TokenUsage:
40
+ input_tokens: int
41
+ output_tokens: int
42
+ total_tokens: int
43
+ @property
44
+ def prompt_tokens(self) -> int: ...
45
+ @property
46
+ def completion_tokens(self) -> int: ...
47
+
48
+ @dataclass
49
+ class ChatResponse:
50
+ content: Optional[str]
51
+ usage: TokenUsage
52
+ model: str
53
+ finish_reason: Optional[str]
54
+ tool_calls: Optional[list]
55
+ provider: Optional[str]
56
+ warnings: list[str]
57
+ @property
58
+ def stop_reason(self) -> Optional[str]: ...
59
+
60
+ @dataclass
61
+ class StreamChunk:
62
+ delta: str
63
+ model: Optional[str]
64
+ finish_reason: Optional[str]
65
+ thinking: Optional[str]
66
+ usage: Optional[TokenUsage]
67
+ tool_calls: Optional[list]
68
+ def is_final(self) -> bool: ...
69
+ def has_thinking(self) -> bool: ...
70
+ def has_tool_calls(self) -> bool: ...
71
+
72
+ @dataclass
73
+ class ModelInfo:
74
+ id: str
75
+ name: str
76
+ description: Optional[str]
77
+ size_bytes: Optional[int]
78
+ context_window: Optional[int]
79
+ provider: str
80
+ metadata: dict[str, str]
81
+ def supports_vision(self) -> bool: ...
82
+ def modalities(self) -> list[str]: ...
83
+ def max_images(self) -> Optional[int]: ...
84
+ @classmethod
85
+ def from_dict(cls, data: dict) -> "ModelInfo": ...
86
+
87
+ @dataclass
88
+ class ImageSource:
89
+ source_type: Any # ImageSourceType enum
90
+ data: str
91
+ media_type: Optional[str]
92
+
93
+ # ── Message ─────────────────────────────────────────────────────
94
+
95
+ @dataclass
96
+ class Message:
97
+ role: Any # Role enum
98
+ content: str
99
+ images: list[ImageSource]
100
+ @staticmethod
101
+ def user(content: str) -> "Message": ...
102
+ @staticmethod
103
+ def assistant(content: str) -> "Message": ...
104
+ @staticmethod
105
+ def system(content: str) -> "Message": ...
106
+ def with_image_url(self, url: str) -> "Message": ...
107
+ def with_image_base64(self, data: str) -> "Message": ...
108
+ def with_image_file(self, path: str) -> "Message": ...
109
+
110
+ # ── Tool Calling ────────────────────────────────────────────────
111
+
112
+ @dataclass
113
+ class FunctionDefinition:
114
+ name: str
115
+ description: str
116
+ parameters: dict
117
+
118
+ @dataclass
119
+ class ToolDefinition:
120
+ type: str
121
+ function: FunctionDefinition
122
+ @staticmethod
123
+ def create(name: str, description: str, parameters: dict) -> "ToolDefinition": ...
124
+ def to_dict(self) -> dict: ...
125
+
126
+ @dataclass
127
+ class FunctionCall:
128
+ name: str
129
+ arguments: str
130
+
131
+ @dataclass
132
+ class ToolCall:
133
+ id: str
134
+ type: str
135
+ function: FunctionCall
136
+ @classmethod
137
+ def from_dict(cls, data: dict) -> "ToolCall": ...
138
+
139
+ def tool_choice_auto() -> str: ...
140
+ def tool_choice_none() -> str: ...
141
+ def tool_choice_required() -> str: ...
142
+ def tool_choice_named(name: str) -> dict: ...
143
+
144
+ # ── Provider Protocol ───────────────────────────────────────────
145
+
146
+ class LLMProvider(Protocol):
147
+ @property
148
+ def model(self) -> str: ...
149
+ @property
150
+ def provider_name(self) -> str: ...
151
+ def chat(
152
+ self,
153
+ messages: list[Message],
154
+ *,
155
+ model: Optional[str] = ...,
156
+ temperature: Optional[float] = ...,
157
+ max_tokens: Optional[int] = ...,
158
+ top_p: Optional[float] = ...,
159
+ stop: Optional[Union[str, list[str]]] = ...,
160
+ response_format: Optional[Any] = ...,
161
+ tools: Optional[list[ToolDefinition]] = ...,
162
+ tool_choice: Optional[Any] = ...,
163
+ ) -> ChatResponse: ...
164
+ def chat_stream(
165
+ self,
166
+ messages: list[Message],
167
+ *,
168
+ model: Optional[str] = ...,
169
+ temperature: Optional[float] = ...,
170
+ max_tokens: Optional[int] = ...,
171
+ top_p: Optional[float] = ...,
172
+ stop: Optional[Union[str, list[str]]] = ...,
173
+ response_format: Optional[Any] = ...,
174
+ tools: Optional[list[ToolDefinition]] = ...,
175
+ tool_choice: Optional[Any] = ...,
176
+ ) -> Iterator[StreamChunk]: ...
177
+ def list_models(self) -> list[ModelInfo]: ...
178
+
179
+ # ── Provider Factory ────────────────────────────────────────────
180
+
181
+ class Provider:
182
+ @staticmethod
183
+ def claude(*, model: str = ..., api_key: Optional[str] = ..., **kwargs: Any) -> Any: ...
184
+ @staticmethod
185
+ def openai(*, model: str = ..., api_key: Optional[str] = ..., **kwargs: Any) -> Any: ...
186
+ @staticmethod
187
+ def ollama(*, model: str = ..., **kwargs: Any) -> Any: ...
188
+ @staticmethod
189
+ def groq(*, model: str = ..., api_key: Optional[str] = ..., **kwargs: Any) -> Any: ...
190
+ @staticmethod
191
+ def xai(*, model: str = ..., api_key: Optional[str] = ..., **kwargs: Any) -> Any: ...
192
+ @staticmethod
193
+ def mistral(*, model: str = ..., api_key: Optional[str] = ..., **kwargs: Any) -> Any: ...
194
+ @staticmethod
195
+ def fireworks(*, model: str = ..., api_key: Optional[str] = ..., **kwargs: Any) -> Any: ...
196
+ @staticmethod
197
+ def together(*, model: str = ..., api_key: Optional[str] = ..., **kwargs: Any) -> Any: ...
198
+ @staticmethod
199
+ def openrouter(*, model: str = ..., api_key: Optional[str] = ..., **kwargs: Any) -> Any: ...
200
+ @staticmethod
201
+ def perplexity(*, model: str = ..., api_key: Optional[str] = ..., **kwargs: Any) -> Any: ...
202
+ @staticmethod
203
+ def lmstudio(*, model: str = ..., **kwargs: Any) -> Any: ...
204
+
205
+ # ── Errors ──────────────────────────────────────────────────────
206
+
207
+ class LLMError(Exception):
208
+ status_code: Optional[int]
209
+ provider: Optional[str]
210
+ model: Optional[str]
211
+ @property
212
+ def is_retryable(self) -> bool: ...
213
+
214
+ class AuthenticationError(LLMError): ...
215
+
216
+ class RateLimitError(LLMError):
217
+ retry_after: Optional[float]
218
+
219
+ class NetworkError(LLMError): ...
220
+ class TimeoutError(LLMError): ...
221
+ class ProviderError(LLMError): ...
222
+
223
+ # FFI errors
224
+ class NxuskitError(Exception):
225
+ error_type: str
226
+ message: str
227
+ provider: Optional[str]
228
+ feature: Optional[str]
229
+
230
+ class ConfigError(NxuskitError): ...
231
+ class FeatureUnavailableError(NxuskitError): ...
232
+ class LicenseRequiredError(NxuskitError): ...
233
+ class LicenseExpiredError(NxuskitError): ...
234
+
235
+ class EditionInsufficientError(NxuskitError):
236
+ required_edition: Optional[str]
237
+
238
+ # ── FFI Provider ────────────────────────────────────────────────
239
+
240
+ class FFIProvider:
241
+ def chat(self, request: dict[str, Any]) -> ChatResponse: ...
242
+ def stream(self, request: dict[str, Any]) -> Iterator[StreamChunk]: ...
243
+ def list_models(self) -> list[ModelInfo]: ...
244
+ def close(self) -> None: ...
245
+ def __enter__(self) -> "FFIProvider": ...
246
+ def __exit__(self, *args: Any) -> None: ...
247
+
248
+ def create_ffi_provider(config: dict[str, Any]) -> FFIProvider: ...
nxuskit/_bn_ffi.py ADDED
@@ -0,0 +1,64 @@
1
+ """Low-level cffi bindings for the Bayesian Network C ABI.
2
+
3
+ Uses the shared FFI instance and library handle from _ffi.py.
4
+ Higher-level Python wrappers are in bn.py.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+
10
+ class BnLibraryNotFoundError(RuntimeError):
11
+ """Raised when the nxuskit native library cannot be found."""
12
+
13
+
14
+ # Lazy reference — populated on first _get_lib() call.
15
+ _bn_lib = None
16
+
17
+
18
+ def _get_lib():
19
+ """Get the shared library handle for BN functions."""
20
+ global _bn_lib
21
+ if _bn_lib is not None:
22
+ return _bn_lib
23
+ try:
24
+ from nxuskit._ffi import lib
25
+
26
+ _bn_lib = lib
27
+ return _bn_lib
28
+ except Exception as e:
29
+ raise BnLibraryNotFoundError(
30
+ f"Failed to load nxuskit native library: {e}. "
31
+ "Set NXUSKIT_LIB_DIR, NXUSKIT_SDK_DIR, or install at ~/.nxuskit/sdk/current/."
32
+ ) from e
33
+
34
+
35
+ def last_error() -> str:
36
+ """Read the thread-local error message from the C ABI."""
37
+ from nxuskit._ffi import ffi
38
+
39
+ lib = _get_lib()
40
+ ptr = lib.nxuskit_last_error()
41
+ if ptr == ffi.NULL:
42
+ return ""
43
+ return ffi.string(ptr).decode("utf-8", errors="replace")
44
+
45
+
46
+ def read_and_free_string(ptr) -> str:
47
+ """Convert a C string to Python, free the C memory."""
48
+ from nxuskit._ffi import ffi
49
+
50
+ lib = _get_lib()
51
+ if ptr == ffi.NULL:
52
+ err = last_error()
53
+ raise RuntimeError(f"nxuskit bn: NULL string returned: {err}")
54
+ s = ffi.string(ptr).decode("utf-8")
55
+ lib.nxuskit_free_string(ptr)
56
+ return s
57
+
58
+
59
+ # Backward compat: code that imported bn_ffi directly can still use it
60
+ # for callback definitions. Point to the shared ffi instance.
61
+ try:
62
+ from nxuskit._ffi import ffi as bn_ffi
63
+ except Exception:
64
+ bn_ffi = None # type: ignore[assignment]
nxuskit/_clips_ffi.py ADDED
@@ -0,0 +1,61 @@
1
+ """Low-level cffi bindings for the CLIPS Session C ABI.
2
+
3
+ Uses the shared FFI instance and library handle from _ffi.py.
4
+ Higher-level Python wrappers are in clips.py.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+
10
+ class ClipsLibraryNotFoundError(RuntimeError):
11
+ """Raised when the nxuskit native library cannot be found."""
12
+
13
+
14
+ _clips_lib = None
15
+
16
+
17
+ def _get_lib():
18
+ """Get the shared library handle for CLIPS functions."""
19
+ global _clips_lib
20
+ if _clips_lib is not None:
21
+ return _clips_lib
22
+ try:
23
+ from nxuskit._ffi import lib
24
+
25
+ _clips_lib = lib
26
+ return _clips_lib
27
+ except Exception as e:
28
+ raise ClipsLibraryNotFoundError(
29
+ f"Failed to load nxuskit native library: {e}. "
30
+ "Set NXUSKIT_LIB_DIR, NXUSKIT_SDK_DIR, or install at ~/.nxuskit/sdk/current/."
31
+ ) from e
32
+
33
+
34
+ def last_error() -> str:
35
+ """Read the thread-local error message from the C ABI."""
36
+ from nxuskit._ffi import ffi
37
+
38
+ lib = _get_lib()
39
+ ptr = lib.nxuskit_last_error()
40
+ if ptr == ffi.NULL:
41
+ return ""
42
+ return ffi.string(ptr).decode("utf-8", errors="replace")
43
+
44
+
45
+ def read_and_free_string(ptr) -> str:
46
+ """Convert a C string to Python, free the C memory."""
47
+ from nxuskit._ffi import ffi
48
+
49
+ lib = _get_lib()
50
+ if ptr == ffi.NULL:
51
+ err = last_error()
52
+ raise RuntimeError(f"nxuskit clips: NULL string returned: {err}")
53
+ s = ffi.string(ptr).decode("utf-8")
54
+ lib.nxuskit_free_string(ptr)
55
+ return s
56
+
57
+
58
+ try:
59
+ from nxuskit._ffi import ffi as clips_ffi
60
+ except Exception:
61
+ clips_ffi = None # type: ignore[assignment]