singleaxis-fabric 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- singleaxis_fabric-0.1.0/.gitignore +108 -0
- singleaxis_fabric-0.1.0/LICENSE +15 -0
- singleaxis_fabric-0.1.0/PKG-INFO +257 -0
- singleaxis_fabric-0.1.0/README.md +217 -0
- singleaxis_fabric-0.1.0/pyproject.toml +117 -0
- singleaxis_fabric-0.1.0/src/fabric/__init__.py +59 -0
- singleaxis_fabric-0.1.0/src/fabric/_chain.py +91 -0
- singleaxis_fabric-0.1.0/src/fabric/_uds.py +48 -0
- singleaxis_fabric-0.1.0/src/fabric/_version.py +3 -0
- singleaxis_fabric-0.1.0/src/fabric/adapters/__init__.py +15 -0
- singleaxis_fabric-0.1.0/src/fabric/adapters/agent_framework.py +174 -0
- singleaxis_fabric-0.1.0/src/fabric/adapters/crewai.py +157 -0
- singleaxis_fabric-0.1.0/src/fabric/adapters/langgraph.py +90 -0
- singleaxis_fabric-0.1.0/src/fabric/client.py +215 -0
- singleaxis_fabric-0.1.0/src/fabric/decision.py +390 -0
- singleaxis_fabric-0.1.0/src/fabric/escalation.py +97 -0
- singleaxis_fabric-0.1.0/src/fabric/guardrails.py +80 -0
- singleaxis_fabric-0.1.0/src/fabric/memory.py +78 -0
- singleaxis_fabric-0.1.0/src/fabric/nemo.py +151 -0
- singleaxis_fabric-0.1.0/src/fabric/presidio.py +115 -0
- singleaxis_fabric-0.1.0/src/fabric/py.typed +0 -0
- singleaxis_fabric-0.1.0/src/fabric/retrieval.py +92 -0
- singleaxis_fabric-0.1.0/src/fabric/tracing.py +59 -0
- singleaxis_fabric-0.1.0/tests/__init__.py +2 -0
- singleaxis_fabric-0.1.0/tests/_fake_sidecar.py +102 -0
- singleaxis_fabric-0.1.0/tests/conftest.py +45 -0
- singleaxis_fabric-0.1.0/tests/test_adapters_agent_framework.py +162 -0
- singleaxis_fabric-0.1.0/tests/test_adapters_crewai.py +142 -0
- singleaxis_fabric-0.1.0/tests/test_adapters_langgraph.py +100 -0
- singleaxis_fabric-0.1.0/tests/test_client.py +53 -0
- singleaxis_fabric-0.1.0/tests/test_decision.py +160 -0
- singleaxis_fabric-0.1.0/tests/test_escalation.py +166 -0
- singleaxis_fabric-0.1.0/tests/test_guardrail_chain.py +271 -0
- singleaxis_fabric-0.1.0/tests/test_memory.py +144 -0
- singleaxis_fabric-0.1.0/tests/test_nemo.py +131 -0
- singleaxis_fabric-0.1.0/tests/test_presidio.py +94 -0
- singleaxis_fabric-0.1.0/tests/test_retrieval.py +176 -0
- singleaxis_fabric-0.1.0/tests/test_tracing.py +42 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# --- Secrets (never commit) ---
|
|
2
|
+
.env
|
|
3
|
+
.env.*
|
|
4
|
+
!.env.example
|
|
5
|
+
|
|
6
|
+
# --- Layer 2 / Layer 3 staging (SingleAxis-internal, never public) ---
|
|
7
|
+
# See _internal/README.md. Moves to a separate private repo per the
|
|
8
|
+
# documented Phase-1 split; excluded here so the public OSS tree
|
|
9
|
+
# stays Layer-1 only.
|
|
10
|
+
/_internal/
|
|
11
|
+
*.pem
|
|
12
|
+
*.key
|
|
13
|
+
*.crt
|
|
14
|
+
*.p12
|
|
15
|
+
*.pfx
|
|
16
|
+
secrets/
|
|
17
|
+
.secrets/
|
|
18
|
+
|
|
19
|
+
# --- Python ---
|
|
20
|
+
__pycache__/
|
|
21
|
+
*.py[cod]
|
|
22
|
+
*$py.class
|
|
23
|
+
*.egg-info/
|
|
24
|
+
.eggs/
|
|
25
|
+
.pytest_cache/
|
|
26
|
+
.mypy_cache/
|
|
27
|
+
.ruff_cache/
|
|
28
|
+
.coverage
|
|
29
|
+
.coverage.*
|
|
30
|
+
coverage.xml
|
|
31
|
+
htmlcov/
|
|
32
|
+
dist/
|
|
33
|
+
build/
|
|
34
|
+
.tox/
|
|
35
|
+
.nox/
|
|
36
|
+
.venv/
|
|
37
|
+
venv/
|
|
38
|
+
ENV/
|
|
39
|
+
env/
|
|
40
|
+
*.whl
|
|
41
|
+
|
|
42
|
+
# --- Go ---
|
|
43
|
+
bin/
|
|
44
|
+
vendor/
|
|
45
|
+
*.test
|
|
46
|
+
*.out
|
|
47
|
+
|
|
48
|
+
# --- Node / JS (docs site, SDK) ---
|
|
49
|
+
node_modules/
|
|
50
|
+
npm-debug.log*
|
|
51
|
+
yarn-debug.log*
|
|
52
|
+
yarn-error.log*
|
|
53
|
+
.pnpm-debug.log*
|
|
54
|
+
|
|
55
|
+
# --- Helm ---
|
|
56
|
+
# Note: first-party subcharts under charts/fabric/charts/<name>/ ARE
|
|
57
|
+
# committed. Only ignore .tgz packages produced by `helm dependency
|
|
58
|
+
# update` and the deprecated lock file.
|
|
59
|
+
charts/*/*.tgz
|
|
60
|
+
charts/*/charts/*.tgz
|
|
61
|
+
charts/*/requirements.lock
|
|
62
|
+
|
|
63
|
+
# --- Terraform ---
|
|
64
|
+
*.tfstate
|
|
65
|
+
*.tfstate.*
|
|
66
|
+
.terraform/
|
|
67
|
+
.terraform.lock.hcl
|
|
68
|
+
crash.log
|
|
69
|
+
*.tfplan
|
|
70
|
+
|
|
71
|
+
# --- Docker ---
|
|
72
|
+
.docker/
|
|
73
|
+
|
|
74
|
+
# --- IDE / Editor ---
|
|
75
|
+
.claude/
|
|
76
|
+
.vscode/
|
|
77
|
+
!.vscode/settings.json.example
|
|
78
|
+
!.vscode/extensions.json
|
|
79
|
+
.idea/
|
|
80
|
+
*.swp
|
|
81
|
+
*.swo
|
|
82
|
+
*~
|
|
83
|
+
.DS_Store
|
|
84
|
+
Thumbs.db
|
|
85
|
+
|
|
86
|
+
# --- OS ---
|
|
87
|
+
*.log
|
|
88
|
+
*.pid
|
|
89
|
+
*.seed
|
|
90
|
+
|
|
91
|
+
# --- Build artefacts ---
|
|
92
|
+
*.tar.gz
|
|
93
|
+
*.zip
|
|
94
|
+
sbom*.json
|
|
95
|
+
sbom*.xml
|
|
96
|
+
*.sig
|
|
97
|
+
*.sigstore
|
|
98
|
+
*.attestation
|
|
99
|
+
|
|
100
|
+
# --- Local development ---
|
|
101
|
+
.local/
|
|
102
|
+
tmp/
|
|
103
|
+
tmp_*/
|
|
104
|
+
|
|
105
|
+
# --- Docs build output ---
|
|
106
|
+
docs/_build/
|
|
107
|
+
docs/.cache/
|
|
108
|
+
site/
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
you may not use this file except in compliance with the License.
|
|
7
|
+
You may obtain a copy of the License at
|
|
8
|
+
|
|
9
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
|
|
11
|
+
Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
See the License for the specific language governing permissions and
|
|
15
|
+
limitations under the License.
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: singleaxis-fabric
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SingleAxis Fabric SDK for Python — inline guardrails, memory, tracing, escalation helpers.
|
|
5
|
+
Project-URL: Homepage, https://github.com/ai5labs/singleaxis-fabric
|
|
6
|
+
Project-URL: Repository, https://github.com/ai5labs/singleaxis-fabric
|
|
7
|
+
Project-URL: Documentation, https://github.com/ai5labs/singleaxis-fabric/blob/main/specs/002-architecture.md
|
|
8
|
+
Project-URL: Issues, https://github.com/ai5labs/singleaxis-fabric/issues
|
|
9
|
+
Author-email: "AI5 Labs, Inc." <fabric@ai5labs.com>
|
|
10
|
+
License-Expression: Apache-2.0
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agent,compliance,fabric,guardrails,llm,otel,singleaxis
|
|
13
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Requires-Dist: opentelemetry-api>=1.27
|
|
22
|
+
Requires-Dist: opentelemetry-sdk>=1.27
|
|
23
|
+
Requires-Dist: pydantic<3.0,>=2.8
|
|
24
|
+
Provides-Extra: agent-framework
|
|
25
|
+
Requires-Dist: agent-framework-core<2.0,>=0.1; extra == 'agent-framework'
|
|
26
|
+
Provides-Extra: crewai
|
|
27
|
+
Requires-Dist: crewai<1.0,>=0.80; extra == 'crewai'
|
|
28
|
+
Requires-Dist: litellm>=1.83.0; extra == 'crewai'
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: mypy>=1.11.2; extra == 'dev'
|
|
31
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.27; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest>=8.3; extra == 'dev'
|
|
34
|
+
Requires-Dist: ruff>=0.6.9; extra == 'dev'
|
|
35
|
+
Provides-Extra: langgraph
|
|
36
|
+
Requires-Dist: langgraph<1.0,>=0.2; extra == 'langgraph'
|
|
37
|
+
Provides-Extra: otlp
|
|
38
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.27; extra == 'otlp'
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
|
+
|
|
41
|
+
# singleaxis-fabric (Python SDK)
|
|
42
|
+
|
|
43
|
+
Native Python SDK that tenant agents import in-process. Provides the
|
|
44
|
+
decision-span contract, guardrail/escalation types, OTel plumbing,
|
|
45
|
+
and optional adapters for LangGraph, Microsoft Agent Framework, and
|
|
46
|
+
CrewAI (installed via extras — the core SDK stays framework-neutral).
|
|
47
|
+
|
|
48
|
+
## Authoritative specs
|
|
49
|
+
|
|
50
|
+
- [`../../specs/002-architecture.md`](../../specs/002-architecture.md)
|
|
51
|
+
- [`../../specs/005-guardrails-inline.md`](../../specs/005-guardrails-inline.md)
|
|
52
|
+
- [`../../specs/003-context-graph.md`](../../specs/003-context-graph.md) (retrieval audit)
|
|
53
|
+
- [`../../specs/007-escalation-workflow.md`](../../specs/007-escalation-workflow.md) (pause primitive)
|
|
54
|
+
|
|
55
|
+
## Status
|
|
56
|
+
|
|
57
|
+
Pre-alpha — **Phase 1a shipping.**
|
|
58
|
+
|
|
59
|
+
### Shipping now
|
|
60
|
+
|
|
61
|
+
- `Fabric` client (`Fabric.from_env`, `FabricConfig`, `close()`)
|
|
62
|
+
- `Decision` context manager — opens an OTel span per agent call and
|
|
63
|
+
tags it with the Fabric-standard attributes:
|
|
64
|
+
- `fabric.tenant_id`, `fabric.agent_id`, `fabric.profile`
|
|
65
|
+
- `fabric.session_id`, `fabric.request_id`, `fabric.user_id`
|
|
66
|
+
- `fabric.blocked`, `fabric.blocked.policies` (on block)
|
|
67
|
+
- Guardrail types: `GuardrailResult`, `EntitySummary`,
|
|
68
|
+
`GuardrailBlocked`, `GuardrailNotConfiguredError`
|
|
69
|
+
- Presidio rail via UDS sidecar: `UDSPresidioClient`,
|
|
70
|
+
`RedactionResult`, `RedactionError`. `Decision.guard_input`,
|
|
71
|
+
`guard_output_chunk`, and `guard_output_final` route through the
|
|
72
|
+
chain and emit `fabric.guardrail` span events (phase, latency_ms,
|
|
73
|
+
blocked, entities, policies).
|
|
74
|
+
- NeMo Colang rail via UDS sidecar: `UDSNemoClient`, `NemoResult`,
|
|
75
|
+
`NemoError`. Wired into the same chain; runs **after** Presidio so
|
|
76
|
+
the Colang / LLM checks never see raw PII. May block
|
|
77
|
+
(`action == "block"`), with the canned response surfaced on the
|
|
78
|
+
`GuardrailResult`.
|
|
79
|
+
- OTel helpers: `get_tracer`, `install_default_provider`
|
|
80
|
+
- Decision-level block recording (`record_block`, `raise_for_block`)
|
|
81
|
+
- Retrieval recording: `RetrievalSource`, `RetrievalRecord`,
|
|
82
|
+
`Decision.record_retrieval(source, query=..., result_count=..., ...)`.
|
|
83
|
+
Hashes the query with SHA-256 locally (raw text never hits the
|
|
84
|
+
span), emits a `fabric.retrieval` span event with allowlisted
|
|
85
|
+
attributes, and maintains rolling `fabric.retrieval_count` and
|
|
86
|
+
`fabric.retrieval_sources` on the decision span so the Telemetry
|
|
87
|
+
Bridge can fold them into the `DecisionSummary` wire event. Maps
|
|
88
|
+
onto the Context Graph's `Retrieval` node (spec 003).
|
|
89
|
+
- Escalation pause primitive: `EscalationSummary`, `EscalationRequested`,
|
|
90
|
+
`Decision.request_escalation`, `Decision.raise_for_escalation`.
|
|
91
|
+
Records `fabric.escalated`, `fabric.escalation.reason/rubric_id/
|
|
92
|
+
mode/triggering_score` on the span and emits a `fabric.escalation`
|
|
93
|
+
span event. `EscalationSummary.to_payload()` returns the
|
|
94
|
+
framework-agnostic dict tenants hand to whatever interrupt
|
|
95
|
+
primitive their orchestrator exposes (LangGraph `interrupt()`,
|
|
96
|
+
Agent Framework checkpoints, a bespoke queue). The SDK owns the
|
|
97
|
+
*local* signal only; the downstream SASF review + signed-verdict
|
|
98
|
+
resume lives in the escalation service (spec 007).
|
|
99
|
+
- Memory write recording: `MemoryKind`, `MemoryRecord`,
|
|
100
|
+
`Decision.remember(kind=..., content=..., key=..., tags=...,
|
|
101
|
+
ttl_seconds=...)`. Tenants perform the actual write against their
|
|
102
|
+
own memory store; the SDK SHA-256s the content locally (raw text
|
|
103
|
+
never hits the span) and emits a `fabric.memory` span event with
|
|
104
|
+
the allowlisted metadata, plus rolling `fabric.memory_write_count`
|
|
105
|
+
and `fabric.memory_kinds` attributes the Telemetry Bridge folds
|
|
106
|
+
into the `DecisionSummary` wire event. Symmetric to
|
|
107
|
+
`record_retrieval` — the Context Graph materializes the write as
|
|
108
|
+
a `Retrieval` node with `source=memory` tied to the owning
|
|
109
|
+
`Decision`.
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from fabric import MemoryKind
|
|
113
|
+
|
|
114
|
+
with fabric.decision(session_id=sess, request_id=req) as decision:
|
|
115
|
+
answer = my_agent.run(user_input)
|
|
116
|
+
my_memory_store.write(key="last_answer", value=answer)
|
|
117
|
+
decision.remember(
|
|
118
|
+
kind=MemoryKind.EPISODIC,
|
|
119
|
+
key="last_answer",
|
|
120
|
+
content=answer,
|
|
121
|
+
tags=("turn", "assistant"),
|
|
122
|
+
)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
When no rails are configured, `guard_input` / `guard_output_*` raise
|
|
126
|
+
`GuardrailNotConfiguredError`. This is a deliberate fail-loud
|
|
127
|
+
posture — a silently passing guardrail is a compliance footgun.
|
|
128
|
+
|
|
129
|
+
### Framework adapters (optional)
|
|
130
|
+
|
|
131
|
+
The core SDK is framework-neutral. Adapters live under
|
|
132
|
+
`fabric.adapters.*` and are each gated behind an install extra so the
|
|
133
|
+
core install does not pull in any orchestration package.
|
|
134
|
+
|
|
135
|
+
- `fabric.adapters.langgraph.escalate(decision, summary)` — records
|
|
136
|
+
the Fabric escalation on the decision span and calls
|
|
137
|
+
`langgraph.types.interrupt(payload)`. Returns whatever the host
|
|
138
|
+
resumes the graph with (typically the signed verdict).
|
|
139
|
+
- `fabric.adapters.agent_framework.request_escalation(ctx, decision,
|
|
140
|
+
summary, *, response_type=...)` — records on span, then
|
|
141
|
+
`await ctx.request_info(request_data=..., response_type=...)`. The
|
|
142
|
+
resumed response is routed to a MAF `@response_handler` method
|
|
143
|
+
(dispatch-based, per MAF design).
|
|
144
|
+
- `fabric.adapters.crewai.attach_callbacks(decision)` returns
|
|
145
|
+
`CrewCallbacks` (step + task callbacks that record CrewAI
|
|
146
|
+
lifecycle events on the decision span).
|
|
147
|
+
`fabric.adapters.crewai.request_escalation(decision, summary)`
|
|
148
|
+
records on span and returns the canonical payload — the tenant
|
|
149
|
+
pairs it with their chosen CrewAI HITL channel (`@human_feedback`
|
|
150
|
+
Flow, `Task(human_input=True)`, or enterprise `/resume`).
|
|
151
|
+
|
|
152
|
+
## Install
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
pip install singleaxis-fabric # core
|
|
156
|
+
pip install "singleaxis-fabric[otlp]" # + OTLP/HTTP exporter
|
|
157
|
+
pip install "singleaxis-fabric[langgraph]" # + LangGraph adapter
|
|
158
|
+
pip install "singleaxis-fabric[agent-framework]" # + MAF adapter
|
|
159
|
+
pip install "singleaxis-fabric[crewai]" # + CrewAI adapter
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Quick start
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
import os
|
|
166
|
+
from fabric import Fabric, install_default_provider
|
|
167
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
168
|
+
|
|
169
|
+
# Host chooses how to export — typically an OTLP endpoint pointing at
|
|
170
|
+
# the Fabric OTel Collector. install_default_provider is a convenience
|
|
171
|
+
# for small agents; production hosts wire the provider themselves.
|
|
172
|
+
install_default_provider(
|
|
173
|
+
service_name="support-bot",
|
|
174
|
+
exporter=OTLPSpanExporter(endpoint=os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"]),
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
fabric = Fabric.from_env()
|
|
178
|
+
|
|
179
|
+
with fabric.decision(
|
|
180
|
+
session_id=session.id,
|
|
181
|
+
request_id=req.id,
|
|
182
|
+
user_id=user.id,
|
|
183
|
+
) as decision:
|
|
184
|
+
# real work happens here; the decision span wraps it all
|
|
185
|
+
safe_input = decision.guard_input(req.body)
|
|
186
|
+
output = llm.complete(prompt=safe_input)
|
|
187
|
+
final = decision.guard_output_final(output)
|
|
188
|
+
decision.set_attribute("llm.model", "claude-opus-4-7")
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
`guard_input` / `guard_output_*` are no-ops that raise
|
|
192
|
+
`GuardrailNotConfiguredError` unless `FABRIC_PRESIDIO_UNIX_SOCKET` is
|
|
193
|
+
set (or a `PresidioClient` is passed to `Fabric(...)` directly).
|
|
194
|
+
|
|
195
|
+
## Environment variables
|
|
196
|
+
|
|
197
|
+
| Variable | Required | Purpose |
|
|
198
|
+
| --- | --- | --- |
|
|
199
|
+
| `FABRIC_TENANT_ID` | yes | Tenant scope for all emitted events. |
|
|
200
|
+
| `FABRIC_AGENT_ID` | yes | Which agent in the tenant is running. |
|
|
201
|
+
| `FABRIC_PROFILE` | no | Regulatory profile (default `permissive-dev`). |
|
|
202
|
+
| `FABRIC_PRESIDIO_UNIX_SOCKET` | no | Unix socket path to the Presidio sidecar (`/v1/redact`). If unset, the Presidio rail is not installed. |
|
|
203
|
+
| `FABRIC_PRESIDIO_TIMEOUT_SECONDS` | no | Per-call timeout for the sidecar (float, default `0.5`). |
|
|
204
|
+
| `FABRIC_NEMO_UNIX_SOCKET` | no | Unix socket path to the NeMo Colang sidecar (`/v1/check`). If unset, the NeMo rail is not installed. |
|
|
205
|
+
| `FABRIC_NEMO_TIMEOUT_SECONDS` | no | Per-call timeout for the NeMo sidecar (float, default `1.0`). |
|
|
206
|
+
|
|
207
|
+
## Module layout
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
sdk/python/
|
|
211
|
+
├── pyproject.toml
|
|
212
|
+
├── src/fabric/
|
|
213
|
+
│ ├── __init__.py
|
|
214
|
+
│ ├── client.py # Fabric, FabricConfig, from_env
|
|
215
|
+
│ ├── decision.py # Decision context manager
|
|
216
|
+
│ ├── guardrails.py # result + error types
|
|
217
|
+
│ ├── escalation.py # EscalationSummary + EscalationRequested
|
|
218
|
+
│ ├── presidio.py # PresidioClient protocol + UDS impl
|
|
219
|
+
│ ├── nemo.py # NemoClient protocol + UDS impl
|
|
220
|
+
│ ├── retrieval.py # RetrievalSource + RetrievalRecord
|
|
221
|
+
│ ├── _chain.py # GuardrailChain (internal)
|
|
222
|
+
│ ├── _uds.py # HTTP-over-unix-socket transport
|
|
223
|
+
│ ├── tracing.py # OTel helpers
|
|
224
|
+
│ └── py.typed
|
|
225
|
+
└── tests/
|
|
226
|
+
├── conftest.py
|
|
227
|
+
├── _fake_sidecar.py
|
|
228
|
+
├── test_client.py
|
|
229
|
+
├── test_decision.py
|
|
230
|
+
├── test_guardrail_chain.py
|
|
231
|
+
├── test_escalation.py
|
|
232
|
+
├── test_nemo.py
|
|
233
|
+
├── test_presidio.py
|
|
234
|
+
├── test_retrieval.py
|
|
235
|
+
└── test_tracing.py
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Tests
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
python -m venv .venv && . .venv/bin/activate
|
|
242
|
+
pip install -e '.[dev]'
|
|
243
|
+
pytest
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Coverage threshold is 85% at the pyproject level. Current baseline is
|
|
247
|
+
~98% because the Phase 1a surface is narrow; as guardrails and memory
|
|
248
|
+
land, keep the 85% floor honest rather than moving it.
|
|
249
|
+
|
|
250
|
+
## Versioning
|
|
251
|
+
|
|
252
|
+
Independent of the Fabric umbrella version pre-1.0.0. Tenant agents
|
|
253
|
+
pin the SDK; the Control Plane advertises compatibility ranges.
|
|
254
|
+
|
|
255
|
+
## License
|
|
256
|
+
|
|
257
|
+
Apache-2.0. See `LICENSE`.
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# singleaxis-fabric (Python SDK)
|
|
2
|
+
|
|
3
|
+
Native Python SDK that tenant agents import in-process. Provides the
|
|
4
|
+
decision-span contract, guardrail/escalation types, OTel plumbing,
|
|
5
|
+
and optional adapters for LangGraph, Microsoft Agent Framework, and
|
|
6
|
+
CrewAI (installed via extras — the core SDK stays framework-neutral).
|
|
7
|
+
|
|
8
|
+
## Authoritative specs
|
|
9
|
+
|
|
10
|
+
- [`../../specs/002-architecture.md`](../../specs/002-architecture.md)
|
|
11
|
+
- [`../../specs/005-guardrails-inline.md`](../../specs/005-guardrails-inline.md)
|
|
12
|
+
- [`../../specs/003-context-graph.md`](../../specs/003-context-graph.md) (retrieval audit)
|
|
13
|
+
- [`../../specs/007-escalation-workflow.md`](../../specs/007-escalation-workflow.md) (pause primitive)
|
|
14
|
+
|
|
15
|
+
## Status
|
|
16
|
+
|
|
17
|
+
Pre-alpha — **Phase 1a shipping.**
|
|
18
|
+
|
|
19
|
+
### Shipping now
|
|
20
|
+
|
|
21
|
+
- `Fabric` client (`Fabric.from_env`, `FabricConfig`, `close()`)
|
|
22
|
+
- `Decision` context manager — opens an OTel span per agent call and
|
|
23
|
+
tags it with the Fabric-standard attributes:
|
|
24
|
+
- `fabric.tenant_id`, `fabric.agent_id`, `fabric.profile`
|
|
25
|
+
- `fabric.session_id`, `fabric.request_id`, `fabric.user_id`
|
|
26
|
+
- `fabric.blocked`, `fabric.blocked.policies` (on block)
|
|
27
|
+
- Guardrail types: `GuardrailResult`, `EntitySummary`,
|
|
28
|
+
`GuardrailBlocked`, `GuardrailNotConfiguredError`
|
|
29
|
+
- Presidio rail via UDS sidecar: `UDSPresidioClient`,
|
|
30
|
+
`RedactionResult`, `RedactionError`. `Decision.guard_input`,
|
|
31
|
+
`guard_output_chunk`, and `guard_output_final` route through the
|
|
32
|
+
chain and emit `fabric.guardrail` span events (phase, latency_ms,
|
|
33
|
+
blocked, entities, policies).
|
|
34
|
+
- NeMo Colang rail via UDS sidecar: `UDSNemoClient`, `NemoResult`,
|
|
35
|
+
`NemoError`. Wired into the same chain; runs **after** Presidio so
|
|
36
|
+
the Colang / LLM checks never see raw PII. May block
|
|
37
|
+
(`action == "block"`), with the canned response surfaced on the
|
|
38
|
+
`GuardrailResult`.
|
|
39
|
+
- OTel helpers: `get_tracer`, `install_default_provider`
|
|
40
|
+
- Decision-level block recording (`record_block`, `raise_for_block`)
|
|
41
|
+
- Retrieval recording: `RetrievalSource`, `RetrievalRecord`,
|
|
42
|
+
`Decision.record_retrieval(source, query=..., result_count=..., ...)`.
|
|
43
|
+
Hashes the query with SHA-256 locally (raw text never hits the
|
|
44
|
+
span), emits a `fabric.retrieval` span event with allowlisted
|
|
45
|
+
attributes, and maintains rolling `fabric.retrieval_count` and
|
|
46
|
+
`fabric.retrieval_sources` on the decision span so the Telemetry
|
|
47
|
+
Bridge can fold them into the `DecisionSummary` wire event. Maps
|
|
48
|
+
onto the Context Graph's `Retrieval` node (spec 003).
|
|
49
|
+
- Escalation pause primitive: `EscalationSummary`, `EscalationRequested`,
|
|
50
|
+
`Decision.request_escalation`, `Decision.raise_for_escalation`.
|
|
51
|
+
Records `fabric.escalated`, `fabric.escalation.reason/rubric_id/
|
|
52
|
+
mode/triggering_score` on the span and emits a `fabric.escalation`
|
|
53
|
+
span event. `EscalationSummary.to_payload()` returns the
|
|
54
|
+
framework-agnostic dict tenants hand to whatever interrupt
|
|
55
|
+
primitive their orchestrator exposes (LangGraph `interrupt()`,
|
|
56
|
+
Agent Framework checkpoints, a bespoke queue). The SDK owns the
|
|
57
|
+
*local* signal only; the downstream SASF review + signed-verdict
|
|
58
|
+
resume lives in the escalation service (spec 007).
|
|
59
|
+
- Memory write recording: `MemoryKind`, `MemoryRecord`,
|
|
60
|
+
`Decision.remember(kind=..., content=..., key=..., tags=...,
|
|
61
|
+
ttl_seconds=...)`. Tenants perform the actual write against their
|
|
62
|
+
own memory store; the SDK SHA-256s the content locally (raw text
|
|
63
|
+
never hits the span) and emits a `fabric.memory` span event with
|
|
64
|
+
the allowlisted metadata, plus rolling `fabric.memory_write_count`
|
|
65
|
+
and `fabric.memory_kinds` attributes the Telemetry Bridge folds
|
|
66
|
+
into the `DecisionSummary` wire event. Symmetric to
|
|
67
|
+
`record_retrieval` — the Context Graph materializes the write as
|
|
68
|
+
a `Retrieval` node with `source=memory` tied to the owning
|
|
69
|
+
`Decision`.
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from fabric import MemoryKind
|
|
73
|
+
|
|
74
|
+
with fabric.decision(session_id=sess, request_id=req) as decision:
|
|
75
|
+
answer = my_agent.run(user_input)
|
|
76
|
+
my_memory_store.write(key="last_answer", value=answer)
|
|
77
|
+
decision.remember(
|
|
78
|
+
kind=MemoryKind.EPISODIC,
|
|
79
|
+
key="last_answer",
|
|
80
|
+
content=answer,
|
|
81
|
+
tags=("turn", "assistant"),
|
|
82
|
+
)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
When no rails are configured, `guard_input` / `guard_output_*` raise
|
|
86
|
+
`GuardrailNotConfiguredError`. This is a deliberate fail-loud
|
|
87
|
+
posture — a silently passing guardrail is a compliance footgun.
|
|
88
|
+
|
|
89
|
+
### Framework adapters (optional)
|
|
90
|
+
|
|
91
|
+
The core SDK is framework-neutral. Adapters live under
|
|
92
|
+
`fabric.adapters.*` and are each gated behind an install extra so the
|
|
93
|
+
core install does not pull in any orchestration package.
|
|
94
|
+
|
|
95
|
+
- `fabric.adapters.langgraph.escalate(decision, summary)` — records
|
|
96
|
+
the Fabric escalation on the decision span and calls
|
|
97
|
+
`langgraph.types.interrupt(payload)`. Returns whatever the host
|
|
98
|
+
resumes the graph with (typically the signed verdict).
|
|
99
|
+
- `fabric.adapters.agent_framework.request_escalation(ctx, decision,
|
|
100
|
+
summary, *, response_type=...)` — records on span, then
|
|
101
|
+
`await ctx.request_info(request_data=..., response_type=...)`. The
|
|
102
|
+
resumed response is routed to a MAF `@response_handler` method
|
|
103
|
+
(dispatch-based, per MAF design).
|
|
104
|
+
- `fabric.adapters.crewai.attach_callbacks(decision)` returns
|
|
105
|
+
`CrewCallbacks` (step + task callbacks that record CrewAI
|
|
106
|
+
lifecycle events on the decision span).
|
|
107
|
+
`fabric.adapters.crewai.request_escalation(decision, summary)`
|
|
108
|
+
records on span and returns the canonical payload — the tenant
|
|
109
|
+
pairs it with their chosen CrewAI HITL channel (`@human_feedback`
|
|
110
|
+
Flow, `Task(human_input=True)`, or enterprise `/resume`).
|
|
111
|
+
|
|
112
|
+
## Install
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
pip install singleaxis-fabric # core
|
|
116
|
+
pip install "singleaxis-fabric[otlp]" # + OTLP/HTTP exporter
|
|
117
|
+
pip install "singleaxis-fabric[langgraph]" # + LangGraph adapter
|
|
118
|
+
pip install "singleaxis-fabric[agent-framework]" # + MAF adapter
|
|
119
|
+
pip install "singleaxis-fabric[crewai]" # + CrewAI adapter
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Quick start
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
import os
|
|
126
|
+
from fabric import Fabric, install_default_provider
|
|
127
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
128
|
+
|
|
129
|
+
# Host chooses how to export — typically an OTLP endpoint pointing at
|
|
130
|
+
# the Fabric OTel Collector. install_default_provider is a convenience
|
|
131
|
+
# for small agents; production hosts wire the provider themselves.
|
|
132
|
+
install_default_provider(
|
|
133
|
+
service_name="support-bot",
|
|
134
|
+
exporter=OTLPSpanExporter(endpoint=os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"]),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
fabric = Fabric.from_env()
|
|
138
|
+
|
|
139
|
+
with fabric.decision(
|
|
140
|
+
session_id=session.id,
|
|
141
|
+
request_id=req.id,
|
|
142
|
+
user_id=user.id,
|
|
143
|
+
) as decision:
|
|
144
|
+
# real work happens here; the decision span wraps it all
|
|
145
|
+
safe_input = decision.guard_input(req.body)
|
|
146
|
+
output = llm.complete(prompt=safe_input)
|
|
147
|
+
final = decision.guard_output_final(output)
|
|
148
|
+
decision.set_attribute("llm.model", "claude-opus-4-7")
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`guard_input` / `guard_output_*` are no-ops that raise
|
|
152
|
+
`GuardrailNotConfiguredError` unless `FABRIC_PRESIDIO_UNIX_SOCKET` is
|
|
153
|
+
set (or a `PresidioClient` is passed to `Fabric(...)` directly).
|
|
154
|
+
|
|
155
|
+
## Environment variables
|
|
156
|
+
|
|
157
|
+
| Variable | Required | Purpose |
|
|
158
|
+
| --- | --- | --- |
|
|
159
|
+
| `FABRIC_TENANT_ID` | yes | Tenant scope for all emitted events. |
|
|
160
|
+
| `FABRIC_AGENT_ID` | yes | Which agent in the tenant is running. |
|
|
161
|
+
| `FABRIC_PROFILE` | no | Regulatory profile (default `permissive-dev`). |
|
|
162
|
+
| `FABRIC_PRESIDIO_UNIX_SOCKET` | no | Unix socket path to the Presidio sidecar (`/v1/redact`). If unset, the Presidio rail is not installed. |
|
|
163
|
+
| `FABRIC_PRESIDIO_TIMEOUT_SECONDS` | no | Per-call timeout for the sidecar (float, default `0.5`). |
|
|
164
|
+
| `FABRIC_NEMO_UNIX_SOCKET` | no | Unix socket path to the NeMo Colang sidecar (`/v1/check`). If unset, the NeMo rail is not installed. |
|
|
165
|
+
| `FABRIC_NEMO_TIMEOUT_SECONDS` | no | Per-call timeout for the NeMo sidecar (float, default `1.0`). |
|
|
166
|
+
|
|
167
|
+
## Module layout
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
sdk/python/
|
|
171
|
+
├── pyproject.toml
|
|
172
|
+
├── src/fabric/
|
|
173
|
+
│ ├── __init__.py
|
|
174
|
+
│ ├── client.py # Fabric, FabricConfig, from_env
|
|
175
|
+
│ ├── decision.py # Decision context manager
|
|
176
|
+
│ ├── guardrails.py # result + error types
|
|
177
|
+
│ ├── escalation.py # EscalationSummary + EscalationRequested
|
|
178
|
+
│ ├── presidio.py # PresidioClient protocol + UDS impl
|
|
179
|
+
│ ├── nemo.py # NemoClient protocol + UDS impl
|
|
180
|
+
│ ├── retrieval.py # RetrievalSource + RetrievalRecord
|
|
181
|
+
│ ├── _chain.py # GuardrailChain (internal)
|
|
182
|
+
│ ├── _uds.py # HTTP-over-unix-socket transport
|
|
183
|
+
│ ├── tracing.py # OTel helpers
|
|
184
|
+
│ └── py.typed
|
|
185
|
+
└── tests/
|
|
186
|
+
├── conftest.py
|
|
187
|
+
├── _fake_sidecar.py
|
|
188
|
+
├── test_client.py
|
|
189
|
+
├── test_decision.py
|
|
190
|
+
├── test_guardrail_chain.py
|
|
191
|
+
├── test_escalation.py
|
|
192
|
+
├── test_nemo.py
|
|
193
|
+
├── test_presidio.py
|
|
194
|
+
├── test_retrieval.py
|
|
195
|
+
└── test_tracing.py
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Tests
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
python -m venv .venv && . .venv/bin/activate
|
|
202
|
+
pip install -e '.[dev]'
|
|
203
|
+
pytest
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Coverage threshold is 85% at the pyproject level. Current baseline is
|
|
207
|
+
~98% because the Phase 1a surface is narrow; as guardrails and memory
|
|
208
|
+
land, keep the 85% floor honest rather than moving it.
|
|
209
|
+
|
|
210
|
+
## Versioning
|
|
211
|
+
|
|
212
|
+
Independent of the Fabric umbrella version pre-1.0.0. Tenant agents
|
|
213
|
+
pin the SDK; the Control Plane advertises compatibility ranges.
|
|
214
|
+
|
|
215
|
+
## License
|
|
216
|
+
|
|
217
|
+
Apache-2.0. See `LICENSE`.
|