acruxcore 0.4.1__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,8 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .pytest_cache/
5
+ *.egg-info/
6
+ build/
7
+ dist/
8
+ .env
@@ -0,0 +1,246 @@
1
+ Metadata-Version: 2.4
2
+ Name: acruxcore
3
+ Version: 0.4.1
4
+ Summary: Async Python SDK for Acrux Core — runtime prompt render, gateway chat, tool loops, traces, and feedback
5
+ Project-URL: Homepage, https://github.com/talhaanwarch/agenticprompthub/tree/main/packages/sdk-python#readme
6
+ Project-URL: Repository, https://github.com/talhaanwarch/agenticprompthub
7
+ Project-URL: Issues, https://github.com/talhaanwarch/agenticprompthub/issues
8
+ Author: Acrux Core
9
+ License: MIT
10
+ Keywords: acruxcore,ai-gateway,llm,observability,prompt-management,sdk,tracing
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.9
22
+ Requires-Dist: httpx<1,>=0.27
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
25
+ Requires-Dist: pytest>=8; extra == 'dev'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # acruxcore (Python)
29
+
30
+ Async Python SDK for Acrux Core. Fetch rendered prompts at runtime, call the AI
31
+ gateway, run client-side tool loops, and report/read traces — full feature
32
+ parity with the [TypeScript SDK](https://www.npmjs.com/package/@acruxcoreai/sdk),
33
+ with a Pythonic `async`/`await` API.
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ pip install acruxcore
39
+ ```
40
+
41
+ Requires Python 3.9+. Depends only on [`httpx`](https://www.python-httpx.org/).
42
+
43
+ > **Using Node or TypeScript instead?** Install the JavaScript SDK from npm:
44
+ > `npm install @acruxcoreai/sdk` — see
45
+ > [`@acruxcoreai/sdk` on npm](https://www.npmjs.com/package/@acruxcoreai/sdk).
46
+
47
+ ## Quickstart
48
+
49
+ ```python
50
+ import asyncio
51
+ from acruxcore import AcruxCore
52
+
53
+ async def main():
54
+ async with AcruxCore(
55
+ api_key="...", # or env ACRUXCORE_API_KEY
56
+ base_url="https://api.acruxcore.com/api/v1", # or env ACRUXCORE_BASE_URL
57
+ ) as hub:
58
+ result = await hub.render_prompt("summarise-article", "production", {"article": "..."})
59
+ print(result.messages)
60
+
61
+ asyncio.run(main())
62
+ ```
63
+
64
+ `AcruxCore` owns an `httpx.AsyncClient`, so use it as an async context manager
65
+ (`async with`) or call `await hub.aclose()` when done. Create one instance at
66
+ startup and reuse it — the render cache is a process-wide singleton.
67
+
68
+ ## Chat
69
+
70
+ `chat()` is a single, non-looping call to the gateway's OpenAI-compatible
71
+ `POST /gateway/chat/completions`. It routes to the right provider, prices the
72
+ call, and records a trace server-side.
73
+
74
+ ```python
75
+ r = await hub.chat("gpt-4o-mini", [{"role": "user", "content": "Say hi in one word."}])
76
+ print(r.content) # 'Hello!'
77
+ print(r.finish_reason) # 'stop'
78
+ print(r.usage) # ChatUsage(prompt_tokens=..., completion_tokens=..., total_tokens=...)
79
+ print(r.gateway) # GatewayCallMeta(request_id=..., provider=..., cost_usd=..., cache=...)
80
+ ```
81
+
82
+ Pass `tools=` / `tool_refs=` / `tool_choice=` just like the raw endpoint. If the
83
+ model calls a tool, `chat()` hands it back raw on `r.message["tool_calls"]` — it
84
+ never dispatches. Use `run_tool_loop()` for that.
85
+
86
+ ## Streaming
87
+
88
+ Pass `stream=True` to get an async iterator of chunks:
89
+
90
+ ```python
91
+ async for chunk in await hub.chat("gpt-4o-mini", messages, stream=True):
92
+ print(chunk.delta.get("content", ""), end="", flush=True)
93
+ if chunk.finish_reason:
94
+ print(f"\n(done: {chunk.finish_reason})")
95
+ ```
96
+
97
+ Each `chunk` mirrors one `chat.completion.chunk` SSE frame (`id`, `model`,
98
+ `delta`, `finish_reason`); iteration ends when the gateway sends `data: [DONE]`.
99
+
100
+ ## Tools
101
+
102
+ `render_prompt()` returns `RenderResult(messages, tools)` — `tools` are any Tool
103
+ Catalog tools attached to that prompt version, already in OpenAI shape. Feed
104
+ them into `run_tool_loop()` to drive the whole tool-calling round-trip:
105
+
106
+ ```python
107
+ async def dispatch(name: str, args: dict):
108
+ if name == "get_weather":
109
+ return {"tempC": 18, "condition": "cloudy"}
110
+ raise ValueError(f"Unknown tool: {name}")
111
+
112
+ result = await hub.run_tool_loop(
113
+ model="gpt-4o",
114
+ messages=render.messages,
115
+ dispatch=dispatch,
116
+ tools=render.tools,
117
+ )
118
+ print(result.content) # final assistant text
119
+ print(result.messages) # full transcript, incl. tool calls/results
120
+ print(result.iterations) # number of model round-trips
121
+ print(result.trace_id) # trace covering every round-trip + tool dispatch
122
+ ```
123
+
124
+ `run_tool_loop()` stops when the model responds without calling a tool, or after
125
+ `max_iterations` round-trips (default 10; `result.stopped_at_limit` is `True`
126
+ then). When the model requests several tools in one turn they are dispatched
127
+ **concurrently** (`asyncio.gather`); results are appended in call order, so
128
+ `dispatch` must be safe to run in parallel. A `dispatch` that raises is not
129
+ caught — wrap it yourself if you want a tool failure reported back to the model
130
+ as a tool-result message instead of aborting the loop.
131
+
132
+ The loop auto-reports one trace: the gateway records an `llm` span per
133
+ round-trip, and the SDK adds a `tool` span per `dispatch` call, threaded into the
134
+ same trace via the `x-trace-id` header. Turn it off with `trace=False`, or attach
135
+ to an existing trace with `trace={"trace_id": "..."}`.
136
+
137
+ ## Reporting traces
138
+
139
+ ```python
140
+ from datetime import datetime, timezone
141
+ now = datetime.now(timezone.utc).isoformat()
142
+
143
+ res = await hub.trace({
144
+ "name": "support-agent-run",
145
+ "spans": [
146
+ {"spanId": "s1", "name": "gpt-4o-mini", "kind": "llm", "startTime": now, "endTime": now,
147
+ "model": "gpt-4o-mini", "usage": {"promptTokens": 120, "completionTokens": 40, "totalTokens": 160}},
148
+ {"spanId": "s2", "parentSpanId": "s1", "name": "search_docs", "kind": "tool",
149
+ "startTime": now, "attributes": {"query": "refunds"}},
150
+ ],
151
+ })
152
+
153
+ # Append another span to the same trace later:
154
+ await hub.trace({"traceId": res.trace_id, "spans": [
155
+ {"spanId": "s3", "parentSpanId": "s1", "name": "finalize", "kind": "chain", "startTime": now}]})
156
+ ```
157
+
158
+ `kind` is one of `llm | tool | retrieval | embedding | agent | chain | other`;
159
+ `status` is `ok | error | unset`. `input`/`output` are stored only when your team
160
+ has payload capture on (or you pass `capturePayloads: True`). Up to 200 spans per
161
+ call. Span keys are camelCase (`spanId`, `parentSpanId`, `startTime`) because they
162
+ are sent to the API verbatim.
163
+
164
+ ## Feedback
165
+
166
+ ```python
167
+ fb = await hub.submit_feedback(
168
+ trace_id,
169
+ rating=-1, # -1..5
170
+ label="wrong_answer",
171
+ comment="The tool call missed relevant docs.",
172
+ source="end_user", # 'user' | 'developer' | 'end_user' | 'api'
173
+ )
174
+
175
+ await hub.submit_feedback(trace_id, span_id="s1", rating=5) # scope to one span
176
+
177
+ # Edit later (author only). Pass a value to change, None to clear, omit to keep:
178
+ await hub.update_feedback(trace_id, fb.id, rating=1)
179
+ ```
180
+
181
+ At least one of `rating` / `label` / `comment` is required per call.
182
+
183
+ ## Reading traces back
184
+
185
+ ```python
186
+ detail = await hub.get_trace(trace_id)
187
+ print(detail.trace.status, detail.trace.total_cost_usd, detail.trace.total_tokens)
188
+ print(detail.spans[0].model, detail.spans[0].latency_ms)
189
+
190
+ page = await hub.list_traces(session_id="tokyo-trip-plan-01", limit=10)
191
+ ```
192
+
193
+ ## Configuration
194
+
195
+ | Argument | Environment Variable | Default | Description |
196
+ |----------|---------------------|---------|-------------|
197
+ | `api_key` | `ACRUXCORE_API_KEY` | **required** | Your Acrux Core API key |
198
+ | `base_url` | `ACRUXCORE_BASE_URL` | **required** | API base URL (e.g. `https://api.acruxcore.com/api/v1`) |
199
+ | `cache_ttl` | — | `60000` (60s) | Milliseconds before a cached render is stale |
200
+ | `max_cache_size` | — | `500` | Max prompt entries in the in-process LRU cache |
201
+ | `max_retries` | — | `1` | Retries on transient failure (2 total attempts) |
202
+ | `retry_interval` | — | `500` | Milliseconds between retries |
203
+ | `timeout` | — | `30` | Per-request timeout, in seconds |
204
+
205
+ ## Error handling
206
+
207
+ ```python
208
+ from acruxcore import AcruxCoreError
209
+
210
+ try:
211
+ await hub.render_prompt("my-prompt", "production", vars)
212
+ except AcruxCoreError as err:
213
+ if err.code == "MISSING_VARIABLES":
214
+ print("Missing template variables:", err.body["error"]["missing"])
215
+ elif err.code == "NETWORK_ERROR":
216
+ print("Acrux Core API unreachable. Check base_url.")
217
+ elif err.code == "API_ERROR":
218
+ print(f"Acrux Core API error {err.status_code}")
219
+ raise
220
+ ```
221
+
222
+ Error codes: `MISSING_API_KEY`, `MISSING_BASE_URL`, `NETWORK_ERROR`, `API_ERROR`,
223
+ `MISSING_VARIABLES`.
224
+
225
+ ## Caching
226
+
227
+ - **Cache key:** `{api_key}:{prompt_name}:{alias}` — scoped per team, prompt, alias.
228
+ - **Variables are not part of the key** — different variable values share a slot.
229
+ - **Stale-while-revalidate:** a stale hit returns the cached value immediately and
230
+ fires a background refresh (`asyncio` task).
231
+ - **API unreachable + stale entry:** serves stale and logs a warning.
232
+ - **API unreachable + cold cache:** raises `AcruxCoreError(code="NETWORK_ERROR")`.
233
+
234
+ ## Method parity with the TypeScript SDK
235
+
236
+ | TypeScript | Python |
237
+ |------------|--------|
238
+ | `renderPrompt(name, alias, vars)` | `render_prompt(name, alias, variables)` |
239
+ | `chat({...})` | `chat(model, messages, *, ...)` |
240
+ | `chat({stream: true})` | `chat(..., stream=True)` → async iterator |
241
+ | `runToolLoop({...})` | `run_tool_loop(model, messages, dispatch, *, ...)` |
242
+ | `trace(input)` | `trace(input)` |
243
+ | `submitFeedback({...})` | `submit_feedback(trace_id, *, ...)` |
244
+ | `updateFeedback({...})` | `update_feedback(trace_id, feedback_id, *, ...)` |
245
+ | `getTrace(id)` | `get_trace(trace_id)` |
246
+ | `listTraces({...})` | `list_traces(*, ...)` |
@@ -0,0 +1,219 @@
1
+ # acruxcore (Python)
2
+
3
+ Async Python SDK for Acrux Core. Fetch rendered prompts at runtime, call the AI
4
+ gateway, run client-side tool loops, and report/read traces — full feature
5
+ parity with the [TypeScript SDK](https://www.npmjs.com/package/@acruxcoreai/sdk),
6
+ with a Pythonic `async`/`await` API.
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ pip install acruxcore
12
+ ```
13
+
14
+ Requires Python 3.9+. Depends only on [`httpx`](https://www.python-httpx.org/).
15
+
16
+ > **Using Node or TypeScript instead?** Install the JavaScript SDK from npm:
17
+ > `npm install @acruxcoreai/sdk` — see
18
+ > [`@acruxcoreai/sdk` on npm](https://www.npmjs.com/package/@acruxcoreai/sdk).
19
+
20
+ ## Quickstart
21
+
22
+ ```python
23
+ import asyncio
24
+ from acruxcore import AcruxCore
25
+
26
+ async def main():
27
+ async with AcruxCore(
28
+ api_key="...", # or env ACRUXCORE_API_KEY
29
+ base_url="https://api.acruxcore.com/api/v1", # or env ACRUXCORE_BASE_URL
30
+ ) as hub:
31
+ result = await hub.render_prompt("summarise-article", "production", {"article": "..."})
32
+ print(result.messages)
33
+
34
+ asyncio.run(main())
35
+ ```
36
+
37
+ `AcruxCore` owns an `httpx.AsyncClient`, so use it as an async context manager
38
+ (`async with`) or call `await hub.aclose()` when done. Create one instance at
39
+ startup and reuse it — the render cache is a process-wide singleton.
40
+
41
+ ## Chat
42
+
43
+ `chat()` is a single, non-looping call to the gateway's OpenAI-compatible
44
+ `POST /gateway/chat/completions`. It routes to the right provider, prices the
45
+ call, and records a trace server-side.
46
+
47
+ ```python
48
+ r = await hub.chat("gpt-4o-mini", [{"role": "user", "content": "Say hi in one word."}])
49
+ print(r.content) # 'Hello!'
50
+ print(r.finish_reason) # 'stop'
51
+ print(r.usage) # ChatUsage(prompt_tokens=..., completion_tokens=..., total_tokens=...)
52
+ print(r.gateway) # GatewayCallMeta(request_id=..., provider=..., cost_usd=..., cache=...)
53
+ ```
54
+
55
+ Pass `tools=` / `tool_refs=` / `tool_choice=` just like the raw endpoint. If the
56
+ model calls a tool, `chat()` hands it back raw on `r.message["tool_calls"]` — it
57
+ never dispatches. Use `run_tool_loop()` for that.
58
+
59
+ ## Streaming
60
+
61
+ Pass `stream=True` to get an async iterator of chunks:
62
+
63
+ ```python
64
+ async for chunk in await hub.chat("gpt-4o-mini", messages, stream=True):
65
+ print(chunk.delta.get("content", ""), end="", flush=True)
66
+ if chunk.finish_reason:
67
+ print(f"\n(done: {chunk.finish_reason})")
68
+ ```
69
+
70
+ Each `chunk` mirrors one `chat.completion.chunk` SSE frame (`id`, `model`,
71
+ `delta`, `finish_reason`); iteration ends when the gateway sends `data: [DONE]`.
72
+
73
+ ## Tools
74
+
75
+ `render_prompt()` returns `RenderResult(messages, tools)` — `tools` are any Tool
76
+ Catalog tools attached to that prompt version, already in OpenAI shape. Feed
77
+ them into `run_tool_loop()` to drive the whole tool-calling round-trip:
78
+
79
+ ```python
80
+ async def dispatch(name: str, args: dict):
81
+ if name == "get_weather":
82
+ return {"tempC": 18, "condition": "cloudy"}
83
+ raise ValueError(f"Unknown tool: {name}")
84
+
85
+ result = await hub.run_tool_loop(
86
+ model="gpt-4o",
87
+ messages=render.messages,
88
+ dispatch=dispatch,
89
+ tools=render.tools,
90
+ )
91
+ print(result.content) # final assistant text
92
+ print(result.messages) # full transcript, incl. tool calls/results
93
+ print(result.iterations) # number of model round-trips
94
+ print(result.trace_id) # trace covering every round-trip + tool dispatch
95
+ ```
96
+
97
+ `run_tool_loop()` stops when the model responds without calling a tool, or after
98
+ `max_iterations` round-trips (default 10; `result.stopped_at_limit` is `True`
99
+ then). When the model requests several tools in one turn they are dispatched
100
+ **concurrently** (`asyncio.gather`); results are appended in call order, so
101
+ `dispatch` must be safe to run in parallel. A `dispatch` that raises is not
102
+ caught — wrap it yourself if you want a tool failure reported back to the model
103
+ as a tool-result message instead of aborting the loop.
104
+
105
+ The loop auto-reports one trace: the gateway records an `llm` span per
106
+ round-trip, and the SDK adds a `tool` span per `dispatch` call, threaded into the
107
+ same trace via the `x-trace-id` header. Turn it off with `trace=False`, or attach
108
+ to an existing trace with `trace={"trace_id": "..."}`.
109
+
110
+ ## Reporting traces
111
+
112
+ ```python
113
+ from datetime import datetime, timezone
114
+ now = datetime.now(timezone.utc).isoformat()
115
+
116
+ res = await hub.trace({
117
+ "name": "support-agent-run",
118
+ "spans": [
119
+ {"spanId": "s1", "name": "gpt-4o-mini", "kind": "llm", "startTime": now, "endTime": now,
120
+ "model": "gpt-4o-mini", "usage": {"promptTokens": 120, "completionTokens": 40, "totalTokens": 160}},
121
+ {"spanId": "s2", "parentSpanId": "s1", "name": "search_docs", "kind": "tool",
122
+ "startTime": now, "attributes": {"query": "refunds"}},
123
+ ],
124
+ })
125
+
126
+ # Append another span to the same trace later:
127
+ await hub.trace({"traceId": res.trace_id, "spans": [
128
+ {"spanId": "s3", "parentSpanId": "s1", "name": "finalize", "kind": "chain", "startTime": now}]})
129
+ ```
130
+
131
+ `kind` is one of `llm | tool | retrieval | embedding | agent | chain | other`;
132
+ `status` is `ok | error | unset`. `input`/`output` are stored only when your team
133
+ has payload capture on (or you pass `capturePayloads: True`). Up to 200 spans per
134
+ call. Span keys are camelCase (`spanId`, `parentSpanId`, `startTime`) because they
135
+ are sent to the API verbatim.
136
+
137
+ ## Feedback
138
+
139
+ ```python
140
+ fb = await hub.submit_feedback(
141
+ trace_id,
142
+ rating=-1, # -1..5
143
+ label="wrong_answer",
144
+ comment="The tool call missed relevant docs.",
145
+ source="end_user", # 'user' | 'developer' | 'end_user' | 'api'
146
+ )
147
+
148
+ await hub.submit_feedback(trace_id, span_id="s1", rating=5) # scope to one span
149
+
150
+ # Edit later (author only). Pass a value to change, None to clear, omit to keep:
151
+ await hub.update_feedback(trace_id, fb.id, rating=1)
152
+ ```
153
+
154
+ At least one of `rating` / `label` / `comment` is required per call.
155
+
156
+ ## Reading traces back
157
+
158
+ ```python
159
+ detail = await hub.get_trace(trace_id)
160
+ print(detail.trace.status, detail.trace.total_cost_usd, detail.trace.total_tokens)
161
+ print(detail.spans[0].model, detail.spans[0].latency_ms)
162
+
163
+ page = await hub.list_traces(session_id="tokyo-trip-plan-01", limit=10)
164
+ ```
165
+
166
+ ## Configuration
167
+
168
+ | Argument | Environment Variable | Default | Description |
169
+ |----------|---------------------|---------|-------------|
170
+ | `api_key` | `ACRUXCORE_API_KEY` | **required** | Your Acrux Core API key |
171
+ | `base_url` | `ACRUXCORE_BASE_URL` | **required** | API base URL (e.g. `https://api.acruxcore.com/api/v1`) |
172
+ | `cache_ttl` | — | `60000` (60s) | Milliseconds before a cached render is stale |
173
+ | `max_cache_size` | — | `500` | Max prompt entries in the in-process LRU cache |
174
+ | `max_retries` | — | `1` | Retries on transient failure (2 total attempts) |
175
+ | `retry_interval` | — | `500` | Milliseconds between retries |
176
+ | `timeout` | — | `30` | Per-request timeout, in seconds |
177
+
178
+ ## Error handling
179
+
180
+ ```python
181
+ from acruxcore import AcruxCoreError
182
+
183
+ try:
184
+ await hub.render_prompt("my-prompt", "production", vars)
185
+ except AcruxCoreError as err:
186
+ if err.code == "MISSING_VARIABLES":
187
+ print("Missing template variables:", err.body["error"]["missing"])
188
+ elif err.code == "NETWORK_ERROR":
189
+ print("Acrux Core API unreachable. Check base_url.")
190
+ elif err.code == "API_ERROR":
191
+ print(f"Acrux Core API error {err.status_code}")
192
+ raise
193
+ ```
194
+
195
+ Error codes: `MISSING_API_KEY`, `MISSING_BASE_URL`, `NETWORK_ERROR`, `API_ERROR`,
196
+ `MISSING_VARIABLES`.
197
+
198
+ ## Caching
199
+
200
+ - **Cache key:** `{api_key}:{prompt_name}:{alias}` — scoped per team, prompt, alias.
201
+ - **Variables are not part of the key** — different variable values share a slot.
202
+ - **Stale-while-revalidate:** a stale hit returns the cached value immediately and
203
+ fires a background refresh (`asyncio` task).
204
+ - **API unreachable + stale entry:** serves stale and logs a warning.
205
+ - **API unreachable + cold cache:** raises `AcruxCoreError(code="NETWORK_ERROR")`.
206
+
207
+ ## Method parity with the TypeScript SDK
208
+
209
+ | TypeScript | Python |
210
+ |------------|--------|
211
+ | `renderPrompt(name, alias, vars)` | `render_prompt(name, alias, variables)` |
212
+ | `chat({...})` | `chat(model, messages, *, ...)` |
213
+ | `chat({stream: true})` | `chat(..., stream=True)` → async iterator |
214
+ | `runToolLoop({...})` | `run_tool_loop(model, messages, dispatch, *, ...)` |
215
+ | `trace(input)` | `trace(input)` |
216
+ | `submitFeedback({...})` | `submit_feedback(trace_id, *, ...)` |
217
+ | `updateFeedback({...})` | `update_feedback(trace_id, feedback_id, *, ...)` |
218
+ | `getTrace(id)` | `get_trace(trace_id)` |
219
+ | `listTraces({...})` | `list_traces(*, ...)` |
@@ -0,0 +1,52 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "acruxcore"
7
+ version = "0.4.1"
8
+ description = "Async Python SDK for Acrux Core — runtime prompt render, gateway chat, tool loops, traces, and feedback"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.9"
12
+ authors = [{ name = "Acrux Core" }]
13
+ keywords = [
14
+ "acruxcore",
15
+ "prompt-management",
16
+ "llm",
17
+ "ai-gateway",
18
+ "observability",
19
+ "tracing",
20
+ "sdk",
21
+ ]
22
+ classifiers = [
23
+ "Development Status :: 4 - Beta",
24
+ "Intended Audience :: Developers",
25
+ "License :: OSI Approved :: MIT License",
26
+ "Programming Language :: Python :: 3",
27
+ "Programming Language :: Python :: 3.9",
28
+ "Programming Language :: Python :: 3.10",
29
+ "Programming Language :: Python :: 3.11",
30
+ "Programming Language :: Python :: 3.12",
31
+ "Programming Language :: Python :: 3.13",
32
+ "Typing :: Typed",
33
+ ]
34
+ dependencies = ["httpx>=0.27,<1"]
35
+
36
+ [project.optional-dependencies]
37
+ dev = [
38
+ "pytest>=8",
39
+ "pytest-asyncio>=0.23",
40
+ ]
41
+
42
+ [project.urls]
43
+ Homepage = "https://github.com/talhaanwarch/agenticprompthub/tree/main/packages/sdk-python#readme"
44
+ Repository = "https://github.com/talhaanwarch/agenticprompthub"
45
+ Issues = "https://github.com/talhaanwarch/agenticprompthub/issues"
46
+
47
+ [tool.hatch.build.targets.wheel]
48
+ packages = ["src/acruxcore"]
49
+
50
+ [tool.pytest.ini_options]
51
+ asyncio_mode = "auto"
52
+ testpaths = ["tests"]
@@ -0,0 +1,81 @@
1
+ """Async Python SDK for Acrux Core.
2
+
3
+ Runtime prompt render, gateway chat (with streaming), client-side tool loops,
4
+ trace reporting, and feedback — full parity with the TypeScript SDK.
5
+
6
+ Example::
7
+
8
+ import asyncio
9
+ from acruxcore import AcruxCore
10
+
11
+ async def main():
12
+ async with AcruxCore(api_key=..., base_url="http://localhost:3000/api/v1") as hub:
13
+ result = await hub.render_prompt("greeting", "production", {"name": "Alice"})
14
+ print(result.messages)
15
+
16
+ asyncio.run(main())
17
+ """
18
+
19
+ from .client import AcruxCore, AsyncChatStream
20
+ from .errors import (
21
+ API_ERROR,
22
+ MISSING_API_KEY,
23
+ MISSING_BASE_URL,
24
+ MISSING_VARIABLES,
25
+ NETWORK_ERROR,
26
+ AcruxCoreError,
27
+ )
28
+ from .types import (
29
+ ChatChunk,
30
+ ChatResult,
31
+ ChatUsage,
32
+ FeedbackResult,
33
+ GatewayCallMeta,
34
+ GetTraceResult,
35
+ IngestSpan,
36
+ ListTracesResult,
37
+ Message,
38
+ RenderResult,
39
+ RunToolLoopResult,
40
+ ToolCall,
41
+ ToolChoice,
42
+ ToolDefinition,
43
+ ToolRef,
44
+ TraceInput,
45
+ TraceResult,
46
+ TraceSpan,
47
+ TraceSummary,
48
+ )
49
+
50
+ __version__ = "0.4.1"
51
+
52
+ __all__ = [
53
+ "AcruxCore",
54
+ "AsyncChatStream",
55
+ "AcruxCoreError",
56
+ "API_ERROR",
57
+ "MISSING_API_KEY",
58
+ "MISSING_BASE_URL",
59
+ "MISSING_VARIABLES",
60
+ "NETWORK_ERROR",
61
+ "ChatChunk",
62
+ "ChatResult",
63
+ "ChatUsage",
64
+ "FeedbackResult",
65
+ "GatewayCallMeta",
66
+ "GetTraceResult",
67
+ "IngestSpan",
68
+ "ListTracesResult",
69
+ "Message",
70
+ "RenderResult",
71
+ "RunToolLoopResult",
72
+ "ToolCall",
73
+ "ToolChoice",
74
+ "ToolDefinition",
75
+ "ToolRef",
76
+ "TraceInput",
77
+ "TraceResult",
78
+ "TraceSpan",
79
+ "TraceSummary",
80
+ "__version__",
81
+ ]
@@ -0,0 +1,66 @@
1
+ """Module-level LRU cache for rendered prompts (stale-while-revalidate).
2
+
3
+ Mirrors the TypeScript SDK's ``cache.ts``: a process-wide singleton so that a
4
+ stale entry can be served while a background refresh is in flight. ``max_size``
5
+ is honoured only on the first :func:`get_cache` call — reuse one client.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections import OrderedDict
11
+ from dataclasses import dataclass
12
+ from typing import Optional
13
+
14
+ from .types import RenderResult
15
+
16
+
17
+ @dataclass
18
+ class CacheEntry:
19
+ """One cache slot: the rendered value plus the epoch-ms it was fetched at."""
20
+
21
+ value: RenderResult
22
+ fetched_at: float # epoch milliseconds
23
+
24
+
25
+ class _LRUCache:
26
+ """A tiny LRU cache keyed by string, bounded by ``max_size`` entries."""
27
+
28
+ def __init__(self, max_size: int) -> None:
29
+ self._max = max_size
30
+ self._store: "OrderedDict[str, CacheEntry]" = OrderedDict()
31
+
32
+ def get(self, key: str) -> Optional[CacheEntry]:
33
+ entry = self._store.get(key)
34
+ if entry is not None:
35
+ self._store.move_to_end(key) # mark most-recently-used
36
+ return entry
37
+
38
+ def set(self, key: str, entry: CacheEntry) -> None:
39
+ self._store[key] = entry
40
+ self._store.move_to_end(key)
41
+ while len(self._store) > self._max:
42
+ self._store.popitem(last=False) # evict least-recently-used
43
+
44
+ def clear(self) -> None:
45
+ self._store.clear()
46
+
47
+
48
+ _cache: Optional[_LRUCache] = None
49
+
50
+
51
+ def get_cache(max_size: int) -> _LRUCache:
52
+ """Return the process-wide LRU cache, creating it on the first call.
53
+
54
+ :param max_size: Maximum entries. Effective only on the first call.
55
+ :returns: The shared cache instance.
56
+ """
57
+ global _cache
58
+ if _cache is None:
59
+ _cache = _LRUCache(max_size)
60
+ return _cache
61
+
62
+
63
+ def _reset_cache_for_testing() -> None:
64
+ """Reset the singleton. FOR TESTING ONLY."""
65
+ global _cache
66
+ _cache = None