spanforge 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.
Files changed (174) hide show
  1. spanforge/__init__.py +815 -0
  2. spanforge/_ansi.py +93 -0
  3. spanforge/_batch_exporter.py +409 -0
  4. spanforge/_cli.py +2094 -0
  5. spanforge/_cli_audit.py +639 -0
  6. spanforge/_cli_compliance.py +711 -0
  7. spanforge/_cli_cost.py +243 -0
  8. spanforge/_cli_ops.py +791 -0
  9. spanforge/_cli_phase11.py +356 -0
  10. spanforge/_hooks.py +337 -0
  11. spanforge/_server.py +1708 -0
  12. spanforge/_span.py +1036 -0
  13. spanforge/_store.py +288 -0
  14. spanforge/_stream.py +664 -0
  15. spanforge/_trace.py +335 -0
  16. spanforge/_tracer.py +254 -0
  17. spanforge/actor.py +141 -0
  18. spanforge/alerts.py +469 -0
  19. spanforge/auto.py +464 -0
  20. spanforge/baseline.py +335 -0
  21. spanforge/cache.py +635 -0
  22. spanforge/compliance.py +325 -0
  23. spanforge/config.py +532 -0
  24. spanforge/consent.py +228 -0
  25. spanforge/consumer.py +377 -0
  26. spanforge/core/__init__.py +5 -0
  27. spanforge/core/compliance_mapping.py +1254 -0
  28. spanforge/cost.py +600 -0
  29. spanforge/debug.py +548 -0
  30. spanforge/deprecations.py +205 -0
  31. spanforge/drift.py +482 -0
  32. spanforge/egress.py +58 -0
  33. spanforge/eval.py +648 -0
  34. spanforge/event.py +1064 -0
  35. spanforge/exceptions.py +240 -0
  36. spanforge/explain.py +178 -0
  37. spanforge/export/__init__.py +69 -0
  38. spanforge/export/append_only.py +337 -0
  39. spanforge/export/cloud.py +357 -0
  40. spanforge/export/datadog.py +497 -0
  41. spanforge/export/grafana.py +320 -0
  42. spanforge/export/jsonl.py +195 -0
  43. spanforge/export/openinference.py +158 -0
  44. spanforge/export/otel_bridge.py +294 -0
  45. spanforge/export/otlp.py +811 -0
  46. spanforge/export/otlp_bridge.py +233 -0
  47. spanforge/export/redis_backend.py +282 -0
  48. spanforge/export/siem_schema.py +98 -0
  49. spanforge/export/siem_splunk.py +264 -0
  50. spanforge/export/siem_syslog.py +212 -0
  51. spanforge/export/webhook.py +299 -0
  52. spanforge/exporters/__init__.py +30 -0
  53. spanforge/exporters/console.py +271 -0
  54. spanforge/exporters/jsonl.py +144 -0
  55. spanforge/exporters/sqlite.py +142 -0
  56. spanforge/gate.py +1150 -0
  57. spanforge/governance.py +181 -0
  58. spanforge/hitl.py +295 -0
  59. spanforge/http.py +187 -0
  60. spanforge/inspect.py +427 -0
  61. spanforge/integrations/__init__.py +45 -0
  62. spanforge/integrations/_pricing.py +280 -0
  63. spanforge/integrations/anthropic.py +388 -0
  64. spanforge/integrations/azure_openai.py +133 -0
  65. spanforge/integrations/bedrock.py +292 -0
  66. spanforge/integrations/crewai.py +251 -0
  67. spanforge/integrations/gemini.py +351 -0
  68. spanforge/integrations/groq.py +442 -0
  69. spanforge/integrations/langchain.py +349 -0
  70. spanforge/integrations/langgraph.py +306 -0
  71. spanforge/integrations/llamaindex.py +373 -0
  72. spanforge/integrations/ollama.py +287 -0
  73. spanforge/integrations/openai.py +368 -0
  74. spanforge/integrations/together.py +483 -0
  75. spanforge/io.py +214 -0
  76. spanforge/lint.py +322 -0
  77. spanforge/metrics.py +417 -0
  78. spanforge/metrics_export.py +343 -0
  79. spanforge/migrate.py +402 -0
  80. spanforge/model_registry.py +278 -0
  81. spanforge/models.py +389 -0
  82. spanforge/namespaces/__init__.py +254 -0
  83. spanforge/namespaces/audit.py +256 -0
  84. spanforge/namespaces/cache.py +237 -0
  85. spanforge/namespaces/chain.py +77 -0
  86. spanforge/namespaces/confidence.py +72 -0
  87. spanforge/namespaces/consent.py +92 -0
  88. spanforge/namespaces/cost.py +179 -0
  89. spanforge/namespaces/decision.py +143 -0
  90. spanforge/namespaces/diff.py +157 -0
  91. spanforge/namespaces/drift.py +80 -0
  92. spanforge/namespaces/eval_.py +251 -0
  93. spanforge/namespaces/feedback.py +241 -0
  94. spanforge/namespaces/fence.py +193 -0
  95. spanforge/namespaces/guard.py +105 -0
  96. spanforge/namespaces/hitl.py +91 -0
  97. spanforge/namespaces/latency.py +72 -0
  98. spanforge/namespaces/prompt.py +190 -0
  99. spanforge/namespaces/redact.py +173 -0
  100. spanforge/namespaces/retrieval.py +379 -0
  101. spanforge/namespaces/runtime_governance.py +494 -0
  102. spanforge/namespaces/template.py +208 -0
  103. spanforge/namespaces/tool_call.py +77 -0
  104. spanforge/namespaces/trace.py +1029 -0
  105. spanforge/normalizer.py +171 -0
  106. spanforge/plugins.py +82 -0
  107. spanforge/presidio_backend.py +349 -0
  108. spanforge/processor.py +258 -0
  109. spanforge/prompt_registry.py +418 -0
  110. spanforge/py.typed +0 -0
  111. spanforge/redact.py +914 -0
  112. spanforge/regression.py +192 -0
  113. spanforge/runtime_policy.py +159 -0
  114. spanforge/sampling.py +511 -0
  115. spanforge/schema.py +183 -0
  116. spanforge/schemas/v1.0/schema.json +170 -0
  117. spanforge/schemas/v2.0/schema.json +536 -0
  118. spanforge/sdk/__init__.py +625 -0
  119. spanforge/sdk/_base.py +584 -0
  120. spanforge/sdk/_base.pyi +71 -0
  121. spanforge/sdk/_exceptions.py +1096 -0
  122. spanforge/sdk/_types.py +2184 -0
  123. spanforge/sdk/alert.py +1514 -0
  124. spanforge/sdk/alert.pyi +56 -0
  125. spanforge/sdk/audit.py +1196 -0
  126. spanforge/sdk/audit.pyi +67 -0
  127. spanforge/sdk/cec.py +1215 -0
  128. spanforge/sdk/cec.pyi +37 -0
  129. spanforge/sdk/config.py +641 -0
  130. spanforge/sdk/config.pyi +55 -0
  131. spanforge/sdk/enterprise.py +714 -0
  132. spanforge/sdk/enterprise.pyi +79 -0
  133. spanforge/sdk/explain.py +170 -0
  134. spanforge/sdk/fallback.py +432 -0
  135. spanforge/sdk/feedback.py +351 -0
  136. spanforge/sdk/gate.py +874 -0
  137. spanforge/sdk/gate.pyi +51 -0
  138. spanforge/sdk/identity.py +2114 -0
  139. spanforge/sdk/identity.pyi +47 -0
  140. spanforge/sdk/lineage.py +175 -0
  141. spanforge/sdk/observe.py +1065 -0
  142. spanforge/sdk/observe.pyi +50 -0
  143. spanforge/sdk/operator.py +338 -0
  144. spanforge/sdk/pii.py +1473 -0
  145. spanforge/sdk/pii.pyi +119 -0
  146. spanforge/sdk/pipelines.py +458 -0
  147. spanforge/sdk/pipelines.pyi +39 -0
  148. spanforge/sdk/policy.py +930 -0
  149. spanforge/sdk/rag.py +594 -0
  150. spanforge/sdk/rbac.py +280 -0
  151. spanforge/sdk/registry.py +430 -0
  152. spanforge/sdk/registry.pyi +46 -0
  153. spanforge/sdk/scope.py +279 -0
  154. spanforge/sdk/secrets.py +293 -0
  155. spanforge/sdk/secrets.pyi +25 -0
  156. spanforge/sdk/security.py +560 -0
  157. spanforge/sdk/security.pyi +57 -0
  158. spanforge/sdk/trust.py +472 -0
  159. spanforge/sdk/trust.pyi +41 -0
  160. spanforge/secrets.py +799 -0
  161. spanforge/signing.py +1179 -0
  162. spanforge/stats.py +100 -0
  163. spanforge/stream.py +560 -0
  164. spanforge/testing.py +378 -0
  165. spanforge/testing_mocks.py +1052 -0
  166. spanforge/trace.py +199 -0
  167. spanforge/types.py +696 -0
  168. spanforge/ulid.py +300 -0
  169. spanforge/validate.py +379 -0
  170. spanforge-1.0.0.dist-info/METADATA +1509 -0
  171. spanforge-1.0.0.dist-info/RECORD +174 -0
  172. spanforge-1.0.0.dist-info/WHEEL +4 -0
  173. spanforge-1.0.0.dist-info/entry_points.txt +5 -0
  174. spanforge-1.0.0.dist-info/licenses/LICENSE +128 -0
@@ -0,0 +1,208 @@
1
+ """spanforge.namespaces.template — Template payload types (RFC-0001).
2
+
3
+ Classes
4
+ -------
5
+ TemplateRegisteredPayload llm.template.registered
6
+ TemplateVariableBoundPayload llm.template.variable.bound
7
+ TemplateValidationFailedPayload llm.template.validation.failed
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass, field
13
+ from typing import Any
14
+
15
+ __all__ = [
16
+ "TemplateRegisteredPayload",
17
+ "TemplateValidationFailedPayload",
18
+ "TemplateVariableBoundPayload",
19
+ ]
20
+
21
+ _VALID_VALUE_TYPES = frozenset({"string", "integer", "float", "boolean", "array", "object", "null"})
22
+ _VALID_FAILURE_TYPES = frozenset(
23
+ {
24
+ "missing_variable",
25
+ "type_mismatch",
26
+ "hash_mismatch",
27
+ "version_not_found",
28
+ "syntax_error",
29
+ "schema_violation",
30
+ }
31
+ )
32
+
33
+ _SHA256_HEX_LEN = 64 # SHA-256 hex digest length (characters)
34
+
35
+
36
+ @dataclass
37
+ class TemplateRegisteredPayload:
38
+ """RFC-0001 — A prompt template was registered in the registry."""
39
+
40
+ template_id: str
41
+ version: str
42
+ template_hash: str # 64 lowercase hex chars, SHA-256 of template source
43
+ variable_names: list[str] = field(default_factory=list)
44
+ variable_count: int | None = None
45
+ language: str | None = None
46
+ char_count: int | None = None
47
+ registered_by: str | None = None
48
+ is_active: bool | None = None
49
+ tags: dict[str, str] | None = None
50
+
51
+ def __post_init__(self) -> None:
52
+ if not self.template_id:
53
+ raise ValueError("TemplateRegisteredPayload.template_id must be non-empty")
54
+ if not self.version:
55
+ raise ValueError("TemplateRegisteredPayload.version must be non-empty")
56
+ if not self.template_hash or len(self.template_hash) != _SHA256_HEX_LEN:
57
+ raise ValueError(
58
+ "TemplateRegisteredPayload.template_hash must be 64 hex chars (SHA-256)"
59
+ )
60
+
61
+ def to_dict(self) -> dict[str, Any]:
62
+ """Serialise the payload to a plain ``dict``."""
63
+ d: dict[str, Any] = {
64
+ "template_id": self.template_id,
65
+ "version": self.version,
66
+ "template_hash": self.template_hash,
67
+ }
68
+ if self.variable_names:
69
+ d["variable_names"] = list(self.variable_names)
70
+ if self.variable_count is not None:
71
+ d["variable_count"] = self.variable_count
72
+ if self.language is not None:
73
+ d["language"] = self.language
74
+ if self.char_count is not None:
75
+ d["char_count"] = self.char_count
76
+ if self.registered_by is not None:
77
+ d["registered_by"] = self.registered_by
78
+ if self.is_active is not None:
79
+ d["is_active"] = self.is_active
80
+ if self.tags is not None:
81
+ d["tags"] = dict(self.tags)
82
+ return d
83
+
84
+ @classmethod
85
+ def from_dict(cls, data: dict[str, Any]) -> TemplateRegisteredPayload:
86
+ """Deserialise from a plain ``dict``."""
87
+ return cls(
88
+ template_id=data["template_id"],
89
+ version=data["version"],
90
+ template_hash=data["template_hash"],
91
+ variable_names=list(data.get("variable_names", [])),
92
+ variable_count=int(data["variable_count"]) if "variable_count" in data else None,
93
+ language=data.get("language"),
94
+ char_count=int(data["char_count"]) if "char_count" in data else None,
95
+ registered_by=data.get("registered_by"),
96
+ is_active=bool(data["is_active"]) if "is_active" in data else None,
97
+ tags=dict(data["tags"]) if "tags" in data else None,
98
+ )
99
+
100
+
101
+ @dataclass
102
+ class TemplateVariableBoundPayload:
103
+ """RFC-0001 — A variable was bound to a value for template rendering.
104
+
105
+ ``value_hash`` stores a SHA-256 hash of the value. For sensitive variables,
106
+ the raw value MUST NOT be stored.
107
+ """
108
+
109
+ template_id: str
110
+ version: str
111
+ variable_name: str
112
+ value_type: str | None = None # "string"|"integer"|"float"|"boolean"|"array"|"object"|"null"
113
+ value_length: int | None = None
114
+ value_hash: str | None = None # 64 hex chars, SHA-256
115
+ is_sensitive: bool | None = None
116
+ span_id: str | None = None
117
+
118
+ def __post_init__(self) -> None:
119
+ if not self.template_id:
120
+ raise ValueError("TemplateVariableBoundPayload.template_id must be non-empty")
121
+ if not self.version:
122
+ raise ValueError("TemplateVariableBoundPayload.version must be non-empty")
123
+ if not self.variable_name:
124
+ raise ValueError("TemplateVariableBoundPayload.variable_name must be non-empty")
125
+ if self.value_type is not None and self.value_type not in _VALID_VALUE_TYPES:
126
+ raise ValueError(
127
+ f"TemplateVariableBoundPayload.value_type must be one of {sorted(_VALID_VALUE_TYPES)}"
128
+ )
129
+ if self.value_hash is not None and len(self.value_hash) != _SHA256_HEX_LEN:
130
+ raise ValueError(
131
+ "TemplateVariableBoundPayload.value_hash must be 64 hex chars (SHA-256)"
132
+ )
133
+
134
+ def to_dict(self) -> dict[str, Any]:
135
+ """Serialise the payload to a plain ``dict``."""
136
+ d: dict[str, Any] = {
137
+ "template_id": self.template_id,
138
+ "version": self.version,
139
+ "variable_name": self.variable_name,
140
+ }
141
+ if self.value_type is not None:
142
+ d["value_type"] = self.value_type
143
+ if self.value_length is not None:
144
+ d["value_length"] = self.value_length
145
+ if self.value_hash is not None:
146
+ d["value_hash"] = self.value_hash
147
+ if self.is_sensitive is not None:
148
+ d["is_sensitive"] = self.is_sensitive
149
+ if self.span_id is not None:
150
+ d["span_id"] = self.span_id
151
+ return d
152
+
153
+ @classmethod
154
+ def from_dict(cls, data: dict[str, Any]) -> TemplateVariableBoundPayload:
155
+ """Deserialise from a plain ``dict``."""
156
+ return cls(
157
+ template_id=data["template_id"],
158
+ version=data["version"],
159
+ variable_name=data["variable_name"],
160
+ value_type=data.get("value_type"),
161
+ value_length=int(data["value_length"]) if "value_length" in data else None,
162
+ value_hash=data.get("value_hash"),
163
+ is_sensitive=bool(data["is_sensitive"]) if "is_sensitive" in data else None,
164
+ span_id=data.get("span_id"),
165
+ )
166
+
167
+
168
+ @dataclass
169
+ class TemplateValidationFailedPayload:
170
+ """RFC-0001 — Template validation failed during rendering or registration."""
171
+
172
+ template_id: str
173
+ version: str
174
+ failure_reason: str
175
+ failure_type: str | None = None
176
+
177
+ def __post_init__(self) -> None:
178
+ if not self.template_id:
179
+ raise ValueError("TemplateValidationFailedPayload.template_id must be non-empty")
180
+ if not self.version:
181
+ raise ValueError("TemplateValidationFailedPayload.version must be non-empty")
182
+ if not self.failure_reason:
183
+ raise ValueError("TemplateValidationFailedPayload.failure_reason must be non-empty")
184
+ if self.failure_type is not None and self.failure_type not in _VALID_FAILURE_TYPES:
185
+ raise ValueError(
186
+ f"TemplateValidationFailedPayload.failure_type must be one of {sorted(_VALID_FAILURE_TYPES)}"
187
+ )
188
+
189
+ def to_dict(self) -> dict[str, Any]:
190
+ """Serialise the payload to a plain ``dict``."""
191
+ d: dict[str, Any] = {
192
+ "template_id": self.template_id,
193
+ "version": self.version,
194
+ "failure_reason": self.failure_reason,
195
+ }
196
+ if self.failure_type is not None:
197
+ d["failure_type"] = self.failure_type
198
+ return d
199
+
200
+ @classmethod
201
+ def from_dict(cls, data: dict[str, Any]) -> TemplateValidationFailedPayload:
202
+ """Deserialise from a plain ``dict``."""
203
+ return cls(
204
+ template_id=data["template_id"],
205
+ version=data["version"],
206
+ failure_reason=data["failure_reason"],
207
+ failure_type=data.get("failure_type"),
208
+ )
@@ -0,0 +1,77 @@
1
+ """spanforge.namespaces.tool_call \u2014 Tool call namespace payload types (RFC-0001 SPANFORGE).
2
+
3
+ Classes
4
+ -------
5
+ ToolCallPayload tool_call.invoked / tool_call.completed / tool_call.failed
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from typing import Any, Literal
12
+
13
+ __all__ = ["ToolCallPayload"]
14
+
15
+ _VALID_STATUSES = frozenset({"success", "failure", "timeout"})
16
+
17
+
18
+ @dataclass
19
+ class ToolCallPayload:
20
+ """RFC-0001 SPANFORGE \u2014 payload for tool_call.* events.
21
+
22
+ Captures all external tool invocations with inputs, outputs, latency, and
23
+ consent-check status (U \u2014 User Rights).
24
+ """
25
+
26
+ call_id: str
27
+ tool_name: str
28
+ latency_ms: float
29
+ status: Literal["success", "failure", "timeout"]
30
+ consent_checked: bool
31
+ tool_version: str | None = None
32
+ inputs: dict[str, Any] = field(default_factory=dict)
33
+ outputs: dict[str, Any] | None = None
34
+ error_message: str | None = None
35
+
36
+ def __post_init__(self) -> None:
37
+ if not self.call_id:
38
+ raise ValueError("ToolCallPayload.call_id must be non-empty")
39
+ if not self.tool_name:
40
+ raise ValueError("ToolCallPayload.tool_name must be non-empty")
41
+ if self.status not in _VALID_STATUSES:
42
+ raise ValueError(f"ToolCallPayload.status must be one of {sorted(_VALID_STATUSES)}")
43
+ if self.latency_ms < 0:
44
+ raise ValueError("ToolCallPayload.latency_ms must be >= 0")
45
+
46
+ def to_dict(self) -> dict[str, Any]:
47
+ """Serialise to a plain dict."""
48
+ d: dict[str, Any] = {
49
+ "call_id": self.call_id,
50
+ "tool_name": self.tool_name,
51
+ "latency_ms": self.latency_ms,
52
+ "status": self.status,
53
+ "consent_checked": self.consent_checked,
54
+ "inputs": self.inputs,
55
+ }
56
+ if self.tool_version is not None:
57
+ d["tool_version"] = self.tool_version
58
+ if self.outputs is not None:
59
+ d["outputs"] = self.outputs
60
+ if self.error_message is not None:
61
+ d["error_message"] = self.error_message
62
+ return d
63
+
64
+ @classmethod
65
+ def from_dict(cls, data: dict[str, Any]) -> ToolCallPayload:
66
+ """Deserialise from a plain dict."""
67
+ return cls(
68
+ call_id=data["call_id"],
69
+ tool_name=data["tool_name"],
70
+ latency_ms=float(data["latency_ms"]),
71
+ status=data["status"],
72
+ consent_checked=bool(data["consent_checked"]),
73
+ tool_version=data.get("tool_version"),
74
+ inputs=dict(data.get("inputs", {})),
75
+ outputs=data.get("outputs"),
76
+ error_message=data.get("error_message"),
77
+ )