aigp-python 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aigp/__init__.py +128 -0
- aigp/attributes.py +129 -0
- aigp/baggage.py +126 -0
- aigp/cloudevents.py +264 -0
- aigp/decorators.py +747 -0
- aigp/events.py +535 -0
- aigp/instrumentor.py +1312 -0
- aigp/openlineage.py +249 -0
- aigp/tracestate.py +149 -0
- aigp_python-1.0.0.dist-info/METADATA +280 -0
- aigp_python-1.0.0.dist-info/RECORD +12 -0
- aigp_python-1.0.0.dist-info/WHEEL +4 -0
aigp/openlineage.py
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AIGP OpenLineage Facet Builder
|
|
3
|
+
==============================
|
|
4
|
+
|
|
5
|
+
Constructs OpenLineage-compatible facet dicts from AIGP governance events.
|
|
6
|
+
No OpenLineage library dependency -- produces plain dicts conforming to
|
|
7
|
+
the OpenLineage custom facet JSON Schema.
|
|
8
|
+
|
|
9
|
+
AIGP defines two custom facets for OpenLineage:
|
|
10
|
+
|
|
11
|
+
1. **AIGPGovernanceRunFacet** (run facet) -- aggregate governance proof
|
|
12
|
+
attached to ``run.facets.aigp_governance``.
|
|
13
|
+
2. **AIGPResourceInputFacet** (input dataset facet) -- per-resource
|
|
14
|
+
governance metadata attached to ``inputs[].inputFacets.aigp_resource``.
|
|
15
|
+
|
|
16
|
+
Usage::
|
|
17
|
+
|
|
18
|
+
from aigp.openlineage import (
|
|
19
|
+
build_governance_run_facet,
|
|
20
|
+
build_resource_input_facets,
|
|
21
|
+
build_openlineage_run_event,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# From an existing AIGP event dict:
|
|
25
|
+
run_facet = build_governance_run_facet(aigp_event)
|
|
26
|
+
input_facets = build_resource_input_facets(aigp_event)
|
|
27
|
+
|
|
28
|
+
# Or build a complete OpenLineage RunEvent:
|
|
29
|
+
ol_event = build_openlineage_run_event(
|
|
30
|
+
aigp_event,
|
|
31
|
+
job_namespace="finco.trading",
|
|
32
|
+
job_name="trading-bot-v2.invoke",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
Architectural note -- Emission Granularity:
|
|
36
|
+
Implementations SHOULD emit at most one OpenLineage RunEvent per
|
|
37
|
+
governance session or task, using ``trace_id`` as the ``runId``.
|
|
38
|
+
Individual agent steps within a session SHOULD be tracked as OTel
|
|
39
|
+
spans, not as separate OpenLineage runs. OpenLineage was designed
|
|
40
|
+
for discrete Job Runs in a DAG; AI agents are conversational and
|
|
41
|
+
iterative -- emitting per-step runs overwhelms lineage backends.
|
|
42
|
+
|
|
43
|
+
Architectural note -- Active vs. Passive Lineage:
|
|
44
|
+
OpenLineage integration is PASSIVE (eventually consistent). It MUST
|
|
45
|
+
NOT be used for real-time enforcement decisions. Enforcement MUST use
|
|
46
|
+
the AIGP + OTel path. When pre-execution lineage context is needed
|
|
47
|
+
for governance, snapshot it, hash it as a ``"context"`` resource in
|
|
48
|
+
the Merkle tree -- making it an active governed artifact.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
import uuid as _uuid
|
|
52
|
+
from datetime import datetime, timezone
|
|
53
|
+
from typing import Any
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
# Constants
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
PRODUCER = "https://github.com/open-aigp/aigp"
|
|
61
|
+
|
|
62
|
+
RUN_FACET_SCHEMA_URL = (
|
|
63
|
+
"https://github.com/open-aigp/aigp/blob/v0.9.0/"
|
|
64
|
+
"integrations/openlineage/facets/AIGPGovernanceRunFacet.json"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
RESOURCE_FACET_SCHEMA_URL = (
|
|
68
|
+
"https://github.com/open-aigp/aigp/blob/v0.9.0/"
|
|
69
|
+
"integrations/openlineage/facets/AIGPResourceInputFacet.json"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
OPENLINEAGE_SCHEMA_URL = (
|
|
73
|
+
"https://openlineage.io/spec/2-0-2/OpenLineage.json#/$defs/RunEvent"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
# Facet Builders
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
def build_governance_run_facet(aigp_event: dict[str, Any]) -> dict[str, Any]:
|
|
82
|
+
"""
|
|
83
|
+
Build an AIGPGovernanceRunFacet from an AIGP event dict.
|
|
84
|
+
|
|
85
|
+
The returned dict conforms to the ``AIGPGovernanceRunFacet`` JSON Schema
|
|
86
|
+
and is ready to be placed in ``run.facets.aigp_governance`` of an
|
|
87
|
+
OpenLineage RunEvent.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
aigp_event: AIGP event dict (from ``create_aigp_event`` or any
|
|
91
|
+
``AIGPInstrumentor`` method).
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Dict conforming to AIGPGovernanceRunFacet schema.
|
|
95
|
+
"""
|
|
96
|
+
merkle_tree = aigp_event.get("governance_merkle_tree")
|
|
97
|
+
leaf_count = merkle_tree["leaf_count"] if merkle_tree else 1
|
|
98
|
+
|
|
99
|
+
facet: dict[str, Any] = {
|
|
100
|
+
"_producer": PRODUCER,
|
|
101
|
+
"_schemaURL": RUN_FACET_SCHEMA_URL,
|
|
102
|
+
"governanceHash": aigp_event.get("governance_hash", ""),
|
|
103
|
+
"hashType": aigp_event.get("hash_type", "sha256"),
|
|
104
|
+
"leafCount": leaf_count,
|
|
105
|
+
"agentId": aigp_event.get("agent_id", ""),
|
|
106
|
+
"traceId": aigp_event.get("trace_id", ""),
|
|
107
|
+
"specVersion": "0.9.0",
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# Infer enforcement result from event type
|
|
111
|
+
event_type = aigp_event.get("event_type", "")
|
|
112
|
+
if "DENIED" in event_type or "VIOLATION" in event_type or "BLOCKED" in event_type:
|
|
113
|
+
facet["enforcementResult"] = "denied"
|
|
114
|
+
elif event_type:
|
|
115
|
+
facet["enforcementResult"] = "allowed"
|
|
116
|
+
|
|
117
|
+
# Optional: data classification
|
|
118
|
+
classification = aigp_event.get("data_classification", "")
|
|
119
|
+
if classification:
|
|
120
|
+
facet["dataClassification"] = classification
|
|
121
|
+
|
|
122
|
+
return facet
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def build_resource_input_facets(
|
|
126
|
+
aigp_event: dict[str, Any],
|
|
127
|
+
) -> list[dict[str, Any]]:
|
|
128
|
+
"""
|
|
129
|
+
Build AIGPResourceInputFacet dicts from an AIGP event's governed resources.
|
|
130
|
+
|
|
131
|
+
If the event has a ``governance_merkle_tree``, produces one facet per leaf.
|
|
132
|
+
Otherwise, produces a single facet from the event's primary resource
|
|
133
|
+
(``policy_name`` or ``prompt_name``).
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
aigp_event: AIGP event dict.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
List of dicts, each conforming to AIGPResourceInputFacet schema.
|
|
140
|
+
Each dict is meant for ``inputs[].inputFacets.aigp_resource``.
|
|
141
|
+
"""
|
|
142
|
+
merkle_tree = aigp_event.get("governance_merkle_tree")
|
|
143
|
+
|
|
144
|
+
if merkle_tree:
|
|
145
|
+
facets: list[dict[str, Any]] = []
|
|
146
|
+
for leaf in merkle_tree.get("leaves", []):
|
|
147
|
+
facets.append({
|
|
148
|
+
"_producer": PRODUCER,
|
|
149
|
+
"_schemaURL": RESOURCE_FACET_SCHEMA_URL,
|
|
150
|
+
"resourceType": leaf["resource_type"],
|
|
151
|
+
"resourceName": leaf["resource_name"],
|
|
152
|
+
"leafHash": leaf["hash"],
|
|
153
|
+
})
|
|
154
|
+
return facets
|
|
155
|
+
|
|
156
|
+
# Single resource: infer from event fields
|
|
157
|
+
facet: dict[str, Any] = {
|
|
158
|
+
"_producer": PRODUCER,
|
|
159
|
+
"_schemaURL": RESOURCE_FACET_SCHEMA_URL,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if aigp_event.get("policy_name"):
|
|
163
|
+
facet["resourceType"] = "policy"
|
|
164
|
+
facet["resourceName"] = aigp_event["policy_name"]
|
|
165
|
+
if aigp_event.get("policy_version"):
|
|
166
|
+
facet["resourceVersion"] = aigp_event["policy_version"]
|
|
167
|
+
elif aigp_event.get("prompt_name"):
|
|
168
|
+
facet["resourceType"] = "prompt"
|
|
169
|
+
facet["resourceName"] = aigp_event["prompt_name"]
|
|
170
|
+
if aigp_event.get("prompt_version"):
|
|
171
|
+
facet["resourceVersion"] = aigp_event["prompt_version"]
|
|
172
|
+
else:
|
|
173
|
+
return [] # No identifiable resource
|
|
174
|
+
|
|
175
|
+
if aigp_event.get("governance_hash"):
|
|
176
|
+
facet["leafHash"] = aigp_event["governance_hash"]
|
|
177
|
+
|
|
178
|
+
return [facet]
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def build_openlineage_run_event(
|
|
182
|
+
aigp_event: dict[str, Any],
|
|
183
|
+
job_namespace: str,
|
|
184
|
+
job_name: str,
|
|
185
|
+
run_id: str = "",
|
|
186
|
+
event_type: str = "COMPLETE",
|
|
187
|
+
) -> dict[str, Any]:
|
|
188
|
+
"""
|
|
189
|
+
Build a complete OpenLineage RunEvent with AIGP governance facets.
|
|
190
|
+
|
|
191
|
+
This convenience function creates a full RunEvent dict that can be
|
|
192
|
+
sent to any OpenLineage-compatible lineage backend.
|
|
193
|
+
|
|
194
|
+
Governed resources become OpenLineage InputDatasets with the
|
|
195
|
+
``aigp_resource`` input facet.
|
|
196
|
+
|
|
197
|
+
**Emission granularity:** Use ``trace_id`` as ``run_id`` to ensure
|
|
198
|
+
one RunEvent per governance session (not per agent step).
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
aigp_event: AIGP event dict.
|
|
202
|
+
job_namespace: OpenLineage job namespace (e.g., ``"finco.trading"``).
|
|
203
|
+
job_name: OpenLineage job name (e.g., ``"trading-bot-v2.invoke"``).
|
|
204
|
+
run_id: Optional run ID (UUID string). If not provided, uses the
|
|
205
|
+
AIGP event's ``trace_id``; falls back to a generated UUID.
|
|
206
|
+
event_type: OpenLineage event type. One of ``"START"``,
|
|
207
|
+
``"RUNNING"``, ``"COMPLETE"``, ``"FAIL"``, ``"ABORT"``.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
Dict conforming to OpenLineage RunEvent schema with AIGP facets.
|
|
211
|
+
"""
|
|
212
|
+
if not run_id:
|
|
213
|
+
# Prefer trace_id as run_id (one RunEvent per session).
|
|
214
|
+
run_id = aigp_event.get("trace_id") or str(_uuid.uuid4())
|
|
215
|
+
|
|
216
|
+
run_facet = build_governance_run_facet(aigp_event)
|
|
217
|
+
resource_facets = build_resource_input_facets(aigp_event)
|
|
218
|
+
|
|
219
|
+
# Each governed resource becomes an OpenLineage InputDataset.
|
|
220
|
+
inputs: list[dict[str, Any]] = []
|
|
221
|
+
for rf in resource_facets:
|
|
222
|
+
inputs.append({
|
|
223
|
+
"namespace": job_namespace,
|
|
224
|
+
"name": rf.get("resourceName", "unknown"),
|
|
225
|
+
"inputFacets": {
|
|
226
|
+
"aigp_resource": rf,
|
|
227
|
+
},
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
now = datetime.now(timezone.utc)
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
"eventType": event_type,
|
|
234
|
+
"eventTime": now.strftime("%Y-%m-%dT%H:%M:%S.") + f"{now.microsecond // 1000:03d}Z",
|
|
235
|
+
"run": {
|
|
236
|
+
"runId": run_id,
|
|
237
|
+
"facets": {
|
|
238
|
+
"aigp_governance": run_facet,
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
"job": {
|
|
242
|
+
"namespace": job_namespace,
|
|
243
|
+
"name": job_name,
|
|
244
|
+
},
|
|
245
|
+
"inputs": inputs,
|
|
246
|
+
"outputs": [],
|
|
247
|
+
"producer": PRODUCER,
|
|
248
|
+
"schemaURL": OPENLINEAGE_SCHEMA_URL,
|
|
249
|
+
}
|
aigp/tracestate.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AIGP W3C tracestate Vendor Key
|
|
3
|
+
===============================
|
|
4
|
+
|
|
5
|
+
Manages the `aigp` vendor key in the W3C tracestate header for
|
|
6
|
+
lightweight governance signaling that survives through proxies
|
|
7
|
+
and load balancers.
|
|
8
|
+
|
|
9
|
+
Format:
|
|
10
|
+
tracestate: aigp=cls:{classification};pol:{policy_name};ver:{policy_version}
|
|
11
|
+
|
|
12
|
+
Unlike Baggage, tracestate is part of the W3C Trace Context standard
|
|
13
|
+
and is preserved by all compliant tracing infrastructure.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from aigp.attributes import AIGPAttributes
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AIGPTraceState:
|
|
20
|
+
"""
|
|
21
|
+
Encodes and decodes AIGP governance context for the W3C tracestate header.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
VENDOR_KEY = "aigp"
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def encode(
|
|
28
|
+
data_classification: str = "",
|
|
29
|
+
policy_name: str = "",
|
|
30
|
+
policy_version: int = 0,
|
|
31
|
+
) -> str:
|
|
32
|
+
"""
|
|
33
|
+
Encode AIGP governance context as a tracestate vendor value.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
data_classification: AIGP classification level.
|
|
37
|
+
policy_name: AGRN policy name.
|
|
38
|
+
policy_version: Policy version number.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Encoded vendor value string (e.g., "cls:con;pol:policy.trading-limits;ver:4").
|
|
42
|
+
"""
|
|
43
|
+
parts = []
|
|
44
|
+
|
|
45
|
+
if data_classification:
|
|
46
|
+
abbrev = AIGPAttributes.CLASSIFICATION_ABBREV.get(
|
|
47
|
+
data_classification, data_classification[:3]
|
|
48
|
+
)
|
|
49
|
+
parts.append(f"cls:{abbrev}")
|
|
50
|
+
|
|
51
|
+
if policy_name:
|
|
52
|
+
parts.append(f"pol:{policy_name}")
|
|
53
|
+
|
|
54
|
+
if policy_version > 0:
|
|
55
|
+
parts.append(f"ver:{policy_version}")
|
|
56
|
+
|
|
57
|
+
return ";".join(parts)
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def decode(vendor_value: str) -> dict[str, str]:
|
|
61
|
+
"""
|
|
62
|
+
Decode AIGP governance context from a tracestate vendor value.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
vendor_value: The value portion of the aigp tracestate entry
|
|
66
|
+
(e.g., "cls:con;pol:policy.trading-limits;ver:4").
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Dict with decoded governance context.
|
|
70
|
+
"""
|
|
71
|
+
result = {}
|
|
72
|
+
abbrev_reverse = {v: k for k, v in AIGPAttributes.CLASSIFICATION_ABBREV.items()}
|
|
73
|
+
|
|
74
|
+
for part in vendor_value.split(";"):
|
|
75
|
+
if ":" not in part:
|
|
76
|
+
continue
|
|
77
|
+
key, value = part.split(":", 1)
|
|
78
|
+
|
|
79
|
+
if key == "cls":
|
|
80
|
+
result["data_classification"] = abbrev_reverse.get(value, value)
|
|
81
|
+
elif key == "pol":
|
|
82
|
+
result["policy_name"] = value
|
|
83
|
+
elif key == "ver":
|
|
84
|
+
result["policy_version"] = value
|
|
85
|
+
|
|
86
|
+
return result
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def inject_into_tracestate(
|
|
90
|
+
existing_tracestate: str,
|
|
91
|
+
data_classification: str = "",
|
|
92
|
+
policy_name: str = "",
|
|
93
|
+
policy_version: int = 0,
|
|
94
|
+
) -> str:
|
|
95
|
+
"""
|
|
96
|
+
Inject AIGP vendor entry into an existing tracestate header value.
|
|
97
|
+
|
|
98
|
+
Per W3C spec, the most recently updated vendor entry moves to the front.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
existing_tracestate: Current tracestate header value.
|
|
102
|
+
data_classification: AIGP classification level.
|
|
103
|
+
policy_name: AGRN policy name.
|
|
104
|
+
policy_version: Policy version number.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Updated tracestate header value with aigp entry prepended.
|
|
108
|
+
"""
|
|
109
|
+
aigp_value = AIGPTraceState.encode(
|
|
110
|
+
data_classification=data_classification,
|
|
111
|
+
policy_name=policy_name,
|
|
112
|
+
policy_version=policy_version,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if not aigp_value:
|
|
116
|
+
return existing_tracestate
|
|
117
|
+
|
|
118
|
+
aigp_entry = f"{AIGPTraceState.VENDOR_KEY}={aigp_value}"
|
|
119
|
+
|
|
120
|
+
# Remove existing aigp entry if present
|
|
121
|
+
if existing_tracestate:
|
|
122
|
+
entries = [
|
|
123
|
+
e.strip()
|
|
124
|
+
for e in existing_tracestate.split(",")
|
|
125
|
+
if not e.strip().startswith(f"{AIGPTraceState.VENDOR_KEY}=")
|
|
126
|
+
]
|
|
127
|
+
if entries:
|
|
128
|
+
return f"{aigp_entry},{','.join(entries)}"
|
|
129
|
+
|
|
130
|
+
return aigp_entry
|
|
131
|
+
|
|
132
|
+
@staticmethod
|
|
133
|
+
def extract_from_tracestate(tracestate: str) -> dict[str, str]:
|
|
134
|
+
"""
|
|
135
|
+
Extract AIGP governance context from a tracestate header value.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
tracestate: The full tracestate header value.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Dict with decoded governance context, or empty dict if no aigp entry.
|
|
142
|
+
"""
|
|
143
|
+
for entry in tracestate.split(","):
|
|
144
|
+
entry = entry.strip()
|
|
145
|
+
if entry.startswith(f"{AIGPTraceState.VENDOR_KEY}="):
|
|
146
|
+
vendor_value = entry[len(f"{AIGPTraceState.VENDOR_KEY}="):]
|
|
147
|
+
return AIGPTraceState.decode(vendor_value)
|
|
148
|
+
|
|
149
|
+
return {}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aigp-python
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: AIGP — AI Governance Proof. Open standard for proving your AI agents used the approved policies, prompts, and tools.
|
|
5
|
+
Project-URL: Homepage, https://github.com/open-aigp/aigp
|
|
6
|
+
Project-URL: Documentation, https://github.com/open-aigp/aigp/tree/main/sdks/python
|
|
7
|
+
Project-URL: Repository, https://github.com/open-aigp/aigp
|
|
8
|
+
Project-URL: Issues, https://github.com/open-aigp/aigp/issues
|
|
9
|
+
Author: AIGP Community
|
|
10
|
+
License-Expression: Apache-2.0
|
|
11
|
+
Keywords: ai-agents,ai-governance,aigp,audit,governance,lineage,merkle-proof,openlineage,opentelemetry,tracing
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
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: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Topic :: System :: Monitoring
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: opentelemetry-api>=1.20.0
|
|
24
|
+
Requires-Dist: opentelemetry-sdk>=1.20.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
28
|
+
Provides-Extra: exporters
|
|
29
|
+
Requires-Dist: opentelemetry-exporter-otlp>=1.20.0; extra == 'exporters'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# AIGP-OpenTelemetry Python SDK
|
|
33
|
+
|
|
34
|
+
[](../../LICENSE)
|
|
35
|
+
[](https://www.python.org/)
|
|
36
|
+
|
|
37
|
+
**Bridge between AIGP governance events and OpenTelemetry spans.**
|
|
38
|
+
|
|
39
|
+
AIGP is the governance-proof semantic payload. OpenTelemetry is the transport and correlation layer. This SDK handles dual-emit: every governance action produces both an AIGP event (compliance store) and an OTel span event (observability backend).
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install opentelemetry-api opentelemetry-sdk
|
|
47
|
+
|
|
48
|
+
# Then add this package to your project
|
|
49
|
+
# (from the AIGP repo root)
|
|
50
|
+
pip install -e sdks/python/
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Quick Start
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from opentelemetry import trace
|
|
57
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
58
|
+
from opentelemetry.sdk.resources import Resource
|
|
59
|
+
|
|
60
|
+
from aigp import AIGPInstrumentor
|
|
61
|
+
|
|
62
|
+
# 1. Initialize with AIGP Resource attributes
|
|
63
|
+
instrumentor = AIGPInstrumentor(
|
|
64
|
+
agent_id="agent.trading-bot-v2",
|
|
65
|
+
agent_name="Trading Bot",
|
|
66
|
+
org_id="org.finco",
|
|
67
|
+
event_callback=send_to_store, # your AI governance store
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
resource = Resource.create({
|
|
71
|
+
**instrumentor.get_resource_attributes(),
|
|
72
|
+
"service.name": "trading-bot-v2",
|
|
73
|
+
})
|
|
74
|
+
provider = TracerProvider(resource=resource)
|
|
75
|
+
trace.set_tracer_provider(provider)
|
|
76
|
+
tracer = trace.get_tracer("aigp.example")
|
|
77
|
+
|
|
78
|
+
# 2. Emit governance events within OTel spans
|
|
79
|
+
with tracer.start_as_current_span("invoke_agent") as span:
|
|
80
|
+
event = instrumentor.inject_success(
|
|
81
|
+
policy_name="policy.trading-limits",
|
|
82
|
+
policy_version=4,
|
|
83
|
+
content="Max position: $10M...",
|
|
84
|
+
data_classification="confidential",
|
|
85
|
+
)
|
|
86
|
+
# -> AIGP event sent to AI governance store (compliance)
|
|
87
|
+
# -> OTel span event with aigp.* attributes (observability)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Features
|
|
91
|
+
|
|
92
|
+
### Dual-Emit Architecture
|
|
93
|
+
|
|
94
|
+
Every call produces two outputs automatically:
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
instrumentor.inject_success(...)
|
|
98
|
+
|
|
|
99
|
+
+---> AIGP Event (JSON) ---> event_callback (AI governance store)
|
|
100
|
+
|
|
|
101
|
+
+---> OTel Span Event -----> OTel-compatible observability backend
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Supported Event Types
|
|
105
|
+
|
|
106
|
+
| Method | AIGP Event Type | OTel Span Event |
|
|
107
|
+
|--------|----------------|-----------------|
|
|
108
|
+
| `inject_success()` | `INJECT_SUCCESS` | `aigp.inject.success` |
|
|
109
|
+
| `inject_denied()` | `INJECT_DENIED` | `aigp.inject.denied` |
|
|
110
|
+
| `prompt_used()` | `PROMPT_USED` | `aigp.prompt.used` |
|
|
111
|
+
| `prompt_denied()` | `PROMPT_DENIED` | `aigp.prompt.denied` |
|
|
112
|
+
| `policy_violation()` | `POLICY_VIOLATION` | `aigp.policy.violation` |
|
|
113
|
+
| `a2a_call()` | `A2A_CALL` | `aigp.a2a.call` |
|
|
114
|
+
| `governance_proof()` | `GOVERNANCE_PROOF` | `aigp.governance.proof` |
|
|
115
|
+
| `multi_policy_inject()` | `INJECT_SUCCESS` | `aigp.inject.success` (with array attributes) |
|
|
116
|
+
| `multi_resource_governance_proof()` | `GOVERNANCE_PROOF` | `aigp.governance.proof` (with Merkle tree) |
|
|
117
|
+
|
|
118
|
+
### Multi-Policy / Multi-Prompt Support
|
|
119
|
+
|
|
120
|
+
When an agent is governed by multiple policies simultaneously:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
event = instrumentor.multi_policy_inject(
|
|
124
|
+
policies=[
|
|
125
|
+
{"name": "policy.trading-limits", "version": 4},
|
|
126
|
+
{"name": "policy.risk-controls", "version": 2},
|
|
127
|
+
],
|
|
128
|
+
content="Combined governed content...",
|
|
129
|
+
data_classification="confidential",
|
|
130
|
+
)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
This produces OTel array-valued attributes:
|
|
134
|
+
```
|
|
135
|
+
aigp.policies.names = ["policy.trading-limits", "policy.risk-controls"]
|
|
136
|
+
aigp.policies.versions = [4, 2]
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Merkle Tree Governance Hash
|
|
140
|
+
|
|
141
|
+
When an agent is governed by multiple resources (policies, prompts, tools, contexts, lineage), compute a Merkle tree for per-resource verification:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from aigp.events import compute_merkle_governance_hash
|
|
145
|
+
|
|
146
|
+
resources = [
|
|
147
|
+
("policy", "policy.refund-limits", "Refund max: $500..."),
|
|
148
|
+
("prompt", "prompt.customer-support-v3", "You are a helpful..."),
|
|
149
|
+
("tool", "tool.order-lookup", '{"name": "order-lookup", "scope": "read"}'),
|
|
150
|
+
("context", "context.env-config", '{"env": "production", "region": "us-east-1"}'),
|
|
151
|
+
("lineage", "lineage.upstream-orders", '{"datasets": ["orders", "customers"]}'),
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
root_hash, merkle_tree = compute_merkle_governance_hash(resources)
|
|
155
|
+
# root_hash: "a3f2b8..." (Merkle root, used as governance_hash)
|
|
156
|
+
# merkle_tree: {"algorithm": "sha256", "leaf_count": 5, "leaves": [...]}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Or use the instrumentor for triple-emit with Merkle:
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
event = instrumentor.multi_resource_governance_proof(
|
|
163
|
+
resources=[
|
|
164
|
+
("policy", "policy.refund-limits", "Refund max: $500..."),
|
|
165
|
+
("prompt", "prompt.customer-support-v3", "You are a helpful..."),
|
|
166
|
+
("tool", "tool.order-lookup", '{"name": "order-lookup"}'),
|
|
167
|
+
("context", "context.env-config", '{"env": "production"}'),
|
|
168
|
+
("lineage", "lineage.upstream-orders", '{"datasets": ["orders"]}'),
|
|
169
|
+
],
|
|
170
|
+
data_classification="confidential",
|
|
171
|
+
)
|
|
172
|
+
# governance_hash is the Merkle root
|
|
173
|
+
# hash_type is "merkle-sha256"
|
|
174
|
+
# governance_merkle_tree contains per-resource leaf hashes
|
|
175
|
+
# OTel span event carries aigp.governance.merkle.leaf_count
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Single-resource calls continue to produce flat SHA-256 hashes for full backward compatibility.
|
|
179
|
+
|
|
180
|
+
### Baggage Propagation (Agent-to-Agent)
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
from aigp.baggage import AIGPBaggage
|
|
184
|
+
|
|
185
|
+
# Calling agent: inject governance context
|
|
186
|
+
ctx = AIGPBaggage.inject(
|
|
187
|
+
policy_name="policy.trading-limits",
|
|
188
|
+
data_classification="confidential",
|
|
189
|
+
org_id="org.finco",
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Receiving agent: extract governance context
|
|
193
|
+
extracted = AIGPBaggage.extract()
|
|
194
|
+
# {'aigp.policy.name': 'policy.trading-limits', ...}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### W3C tracestate Vendor Key
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
from aigp.tracestate import AIGPTraceState
|
|
201
|
+
|
|
202
|
+
# Encode AIGP into tracestate
|
|
203
|
+
tracestate = AIGPTraceState.inject_into_tracestate(
|
|
204
|
+
existing_tracestate="dd=s:1",
|
|
205
|
+
data_classification="confidential",
|
|
206
|
+
policy_name="policy.trading-limits",
|
|
207
|
+
policy_version=4,
|
|
208
|
+
)
|
|
209
|
+
# "aigp=cls:con;pol:policy.trading-limits;ver:4,dd=s:1"
|
|
210
|
+
|
|
211
|
+
# Decode on receiving side
|
|
212
|
+
context = AIGPTraceState.extract_from_tracestate(tracestate)
|
|
213
|
+
# {'data_classification': 'confidential', 'policy_name': 'policy.trading-limits', ...}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### OpenLineage Facet Builder
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
from aigp.openlineage import build_openlineage_run_event
|
|
220
|
+
from aigp.events import compute_merkle_governance_hash, create_aigp_event
|
|
221
|
+
|
|
222
|
+
# Context + lineage resources as governed inputs
|
|
223
|
+
resources = [
|
|
224
|
+
("policy", "policy.fair-lending", policy_content),
|
|
225
|
+
("prompt", "prompt.scoring-v3", prompt_content),
|
|
226
|
+
("context", "context.env-config", env_config_json),
|
|
227
|
+
("lineage", "lineage.upstream-orders", lineage_snapshot_json),
|
|
228
|
+
]
|
|
229
|
+
root, tree = compute_merkle_governance_hash(resources)
|
|
230
|
+
|
|
231
|
+
aigp_event = create_aigp_event(
|
|
232
|
+
event_type="GOVERNANCE_PROOF",
|
|
233
|
+
event_category="governance-proof",
|
|
234
|
+
agent_id="agent.credit-scorer-v2",
|
|
235
|
+
trace_id="abc123...",
|
|
236
|
+
governance_hash=root,
|
|
237
|
+
hash_type="merkle-sha256",
|
|
238
|
+
governance_merkle_tree=tree,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# Build OpenLineage RunEvent (zero OL dependency)
|
|
242
|
+
ol_event = build_openlineage_run_event(
|
|
243
|
+
aigp_event,
|
|
244
|
+
job_namespace="finco.scoring",
|
|
245
|
+
job_name="credit-scorer-v2.invoke",
|
|
246
|
+
)
|
|
247
|
+
# Send to any OpenLineage-compatible lineage backend
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Modules
|
|
251
|
+
|
|
252
|
+
| Module | Purpose |
|
|
253
|
+
|--------|---------|
|
|
254
|
+
| `aigp.instrumentor` | Core triple-emit bridge (`AIGPInstrumentor`) |
|
|
255
|
+
| `aigp.attributes` | `aigp.*` semantic attribute constants |
|
|
256
|
+
| `aigp.events` | AIGP event creation, hash computation, and Merkle tree |
|
|
257
|
+
| `aigp.openlineage` | OpenLineage facet builder (zero OL dependency) |
|
|
258
|
+
| `aigp.baggage` | OTel Baggage propagation for A2A calls |
|
|
259
|
+
| `aigp.tracestate` | W3C tracestate vendor key encode/decode |
|
|
260
|
+
|
|
261
|
+
## Running Tests
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
cd sdks/python
|
|
265
|
+
pip install opentelemetry-api opentelemetry-sdk pytest
|
|
266
|
+
PYTHONPATH=. pytest tests/ -v
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Running the End-to-End Example
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
cd sdks/python
|
|
273
|
+
PYTHONPATH=. python examples/end_to_end.py
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Related Documentation
|
|
277
|
+
|
|
278
|
+
- [AIGP Specification](../../spec/aigp-spec.md) (Sections 11.4-11.7)
|
|
279
|
+
- [AIGP OTel Semantic Conventions](../../integrations/opentelemetry/semantic-conventions.md)
|
|
280
|
+
- [OTel Collector Reference Config](../../integrations/opentelemetry/collector-config.yaml)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
aigp/__init__.py,sha256=ie0jSZRTFaQwYn2BPse1OBTnZWp-bgXecEhyQQyfOOk,3824
|
|
2
|
+
aigp/attributes.py,sha256=RI04tuQJMLEQTfKj3ELqIJDDqClXEHcRbd6Fy_LceuA,5345
|
|
3
|
+
aigp/baggage.py,sha256=u3pUVWDNbNra1P_qUkFs-NhkcGs3w3cG6sIugP8DP_s,3820
|
|
4
|
+
aigp/cloudevents.py,sha256=ltokwfuB5Qm5Vc9y8K_s20wsCUN05aVAM4KcNexIVrY,7718
|
|
5
|
+
aigp/decorators.py,sha256=e6w9nNGF0F8Qqa3ip0ALUwyICvTnEvbStzwKlA2xTxc,26129
|
|
6
|
+
aigp/events.py,sha256=tuDXWL5j5qGMmDeDfJMFKPILkpMoYU3cncHnFpn0fnI,19392
|
|
7
|
+
aigp/instrumentor.py,sha256=zKJzZCBMAuxB7evD2AAvw1zSkl6qz3eOlKAGNvp-hxs,50179
|
|
8
|
+
aigp/openlineage.py,sha256=fEtXa75caZualGZcJvbdZlMRaiYtr2IPNplHQkFNPw4,8669
|
|
9
|
+
aigp/tracestate.py,sha256=cim3Bl2KFp0HQuUduXpH21qLDO86NS81og967HpTGQM,4581
|
|
10
|
+
aigp_python-1.0.0.dist-info/METADATA,sha256=FqaIkQ9Di_ZlkAYyYgCsjz5aeoCK_ZxVmLtcY0miqio,9520
|
|
11
|
+
aigp_python-1.0.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
12
|
+
aigp_python-1.0.0.dist-info/RECORD,,
|