coreason-manifest 0.10.0__py3-none-any.whl → 0.12.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 (30) hide show
  1. coreason_manifest/__init__.py +35 -82
  2. coreason_manifest/{definitions/base.py → common.py} +18 -1
  3. coreason_manifest/governance.py +83 -0
  4. coreason_manifest/schemas/coreason-v2.schema.json +462 -0
  5. coreason_manifest/v2/__init__.py +1 -0
  6. coreason_manifest/v2/governance.py +144 -0
  7. coreason_manifest/v2/io.py +132 -0
  8. coreason_manifest/v2/resolver.py +67 -0
  9. coreason_manifest/v2/spec/__init__.py +1 -0
  10. coreason_manifest/v2/spec/contracts.py +34 -0
  11. coreason_manifest/v2/spec/definitions.py +196 -0
  12. coreason_manifest/v2/validator.py +48 -0
  13. {coreason_manifest-0.10.0.dist-info → coreason_manifest-0.12.0.dist-info}/METADATA +48 -71
  14. coreason_manifest-0.12.0.dist-info/RECORD +20 -0
  15. coreason_manifest/definitions/__init__.py +0 -60
  16. coreason_manifest/definitions/agent.py +0 -370
  17. coreason_manifest/definitions/audit.py +0 -181
  18. coreason_manifest/definitions/events.py +0 -423
  19. coreason_manifest/definitions/message.py +0 -188
  20. coreason_manifest/definitions/simulation.py +0 -79
  21. coreason_manifest/definitions/simulation_config.py +0 -46
  22. coreason_manifest/definitions/topology.py +0 -341
  23. coreason_manifest/recipes.py +0 -84
  24. coreason_manifest/schemas/agent.schema.json +0 -1051
  25. coreason_manifest/schemas/recipe.schema.json +0 -813
  26. coreason_manifest/v1/__init__.py +0 -15
  27. coreason_manifest-0.10.0.dist-info/RECORD +0 -22
  28. {coreason_manifest-0.10.0.dist-info → coreason_manifest-0.12.0.dist-info}/WHEEL +0 -0
  29. {coreason_manifest-0.10.0.dist-info → coreason_manifest-0.12.0.dist-info}/licenses/LICENSE +0 -0
  30. {coreason_manifest-0.10.0.dist-info → coreason_manifest-0.12.0.dist-info}/licenses/NOTICE +0 -0
@@ -1,181 +0,0 @@
1
- # Copyright (c) 2025 CoReason, Inc.
2
- #
3
- # This software is proprietary and dual-licensed.
4
- # Licensed under the Prosperity Public License 3.0 (the "License").
5
- # A copy of the license is available at https://prosperitylicense.com/versions/3.0.0
6
- # For details, see the LICENSE file.
7
- # Commercial use beyond a 30-day trial requires a separate license.
8
- #
9
- # Source Code: https://github.com/CoReason-AI/coreason-manifest
10
-
11
- import hashlib
12
- import json
13
- import uuid
14
- from datetime import datetime
15
- from enum import Enum
16
- from typing import Any, Dict, List, Optional
17
- from uuid import UUID
18
-
19
- from pydantic import ConfigDict, Field
20
-
21
- from coreason_manifest.definitions.base import CoReasonBaseModel
22
-
23
- from .message import ChatMessage
24
-
25
-
26
- class GenAITokenUsage(CoReasonBaseModel):
27
- """Token consumption stats aligned with OTel conventions."""
28
-
29
- model_config = ConfigDict(extra="ignore")
30
-
31
- input: int = Field(0, description="Number of input tokens (prompt).")
32
- output: int = Field(0, description="Number of output tokens (completion).")
33
- total: int = Field(0, description="Total number of tokens used.")
34
-
35
- # Backward compatibility fields (mapped to new fields in logic if needed, but kept for schema)
36
- prompt_tokens: int = 0
37
- completion_tokens: int = 0
38
- total_tokens: int = 0
39
- details: Dict[str, Any] = Field(default_factory=dict)
40
-
41
- def __add__(self, other: "GenAITokenUsage") -> "GenAITokenUsage":
42
- """Add two TokenUsage objects."""
43
- new_details = self.details.copy()
44
- new_details.update(other.details)
45
- return GenAITokenUsage(
46
- input=self.input + other.input,
47
- output=self.output + other.output,
48
- total=self.total + other.total,
49
- prompt_tokens=self.prompt_tokens + other.prompt_tokens,
50
- completion_tokens=self.completion_tokens + other.completion_tokens,
51
- total_tokens=self.total_tokens + other.total_tokens,
52
- details=new_details,
53
- )
54
-
55
- def __iadd__(self, other: "GenAITokenUsage") -> "GenAITokenUsage":
56
- """In-place add two TokenUsage objects."""
57
- self.input += other.input
58
- self.output += other.output
59
- self.total += other.total
60
- self.prompt_tokens += other.prompt_tokens
61
- self.completion_tokens += other.completion_tokens
62
- self.total_tokens += other.total_tokens
63
- self.details.update(other.details)
64
- return self
65
-
66
-
67
- class GenAIOperation(CoReasonBaseModel):
68
- """An atomic operation in the reasoning process (e.g., one LLM call), aligning with OTel Spans."""
69
-
70
- model_config = ConfigDict(extra="ignore")
71
-
72
- span_id: str = Field(..., description="Unique identifier for the operation/span.")
73
- trace_id: str = Field(..., description="Trace ID this operation belongs to.")
74
- parent_id: Optional[str] = Field(None, description="Parent span ID.")
75
-
76
- operation_name: str = Field(..., description="Name of the operation (e.g., chat, embedding).")
77
- provider: str = Field(..., description="GenAI provider (e.g., openai, anthropic).")
78
- model: str = Field(..., description="Model name used.")
79
-
80
- start_time: datetime = Field(default_factory=datetime.now)
81
- end_time: Optional[datetime] = None
82
- duration_ms: float = 0.0
83
-
84
- # Context
85
- input_messages: List[ChatMessage] = Field(default_factory=list)
86
- output_messages: List[ChatMessage] = Field(default_factory=list)
87
-
88
- # Metrics
89
- token_usage: Optional[GenAITokenUsage] = None
90
-
91
- # Metadata
92
- status: str = "pending" # success, error
93
- error_type: Optional[str] = None
94
- metadata: Dict[str, Any] = Field(default_factory=dict)
95
-
96
- @classmethod
97
- def thought(cls, content: str, **kwargs: Any) -> "GenAIOperation":
98
- """Factory method to create a simplified thought/reasoning step."""
99
- defaults = {
100
- "span_id": str(uuid.uuid4()),
101
- "trace_id": str(uuid.uuid4()),
102
- "operation_name": "thought",
103
- "provider": "internal",
104
- "model": "internal",
105
- }
106
- defaults.update(kwargs)
107
- # Ensure output_messages is not duplicated if passed in kwargs
108
- defaults.pop("output_messages", None)
109
- return cls(
110
- **defaults,
111
- output_messages=[ChatMessage.assistant(content)],
112
- )
113
-
114
-
115
- class ReasoningTrace(CoReasonBaseModel):
116
- """The full audit trail of an Agent's execution session.
117
-
118
- Aligned with OpenTelemetry for trace identification.
119
- """
120
-
121
- model_config = ConfigDict(extra="ignore")
122
-
123
- trace_id: str = Field(..., description="Trace ID (OTel format).")
124
- agent_id: str
125
- session_id: Optional[str] = None
126
-
127
- start_time: datetime
128
- end_time: Optional[datetime] = None
129
-
130
- # The chain of thought (Ordered list of operations)
131
- steps: List[GenAIOperation] = Field(default_factory=list)
132
-
133
- # Final outcome
134
- status: str = "pending" # options: success, failure, pending
135
- final_result: Optional[str] = None
136
- error: Optional[str] = None
137
-
138
- # Aggregated stats
139
- total_tokens: GenAITokenUsage = Field(default_factory=GenAITokenUsage)
140
- total_cost: float = 0.0
141
-
142
- # Flexible metadata
143
- metadata: Dict[str, Any] = Field(default_factory=dict)
144
-
145
-
146
- class AuditEventType(str, Enum):
147
- SYSTEM_CHANGE = "system_change"
148
- PREDICTION = "prediction"
149
- GUARDRAIL_TRIGGER = "guardrail_trigger"
150
-
151
-
152
- class AuditLog(CoReasonBaseModel):
153
- """Tamper-evident legal record.
154
-
155
- IDs aligned with OpenTelemetry:
156
- - audit_id: Unique record ID.
157
- - trace_id: OTel Trace ID.
158
- """
159
-
160
- audit_id: UUID = Field(..., description="Unique identifier.")
161
- trace_id: str = Field(..., description="Trace ID for OTel correlation.")
162
- timestamp: datetime = Field(..., description="ISO8601 timestamp.")
163
- actor: str = Field(..., description="User ID or Agent Component ID.")
164
- event_type: AuditEventType = Field(..., description="Type of event.")
165
- safety_metadata: Dict[str, Any] = Field(..., description="Safety metadata (e.g., PII detected).")
166
- previous_hash: str = Field(..., description="Hash of the previous log entry.")
167
- integrity_hash: str = Field(..., description="SHA256 hash of this record + previous_hash.")
168
-
169
- def compute_hash(self) -> str:
170
- """Computes the integrity hash of the record."""
171
- # Use model_dump to get a dict, but exclude integrity_hash as it is the target
172
- data = self.model_dump(exclude={"integrity_hash"}, mode="json")
173
- # Ensure deterministic serialization
174
- json_str = json.dumps(data, sort_keys=True, default=str)
175
- return hashlib.sha256(json_str.encode("utf-8")).hexdigest()
176
-
177
-
178
- # --- Backward Compatibility ---
179
- # Adapters or Aliases
180
- CognitiveStep = GenAIOperation
181
- TokenUsage = GenAITokenUsage
@@ -1,423 +0,0 @@
1
- # Copyright (c) 2025 CoReason, Inc.
2
- #
3
- # This software is proprietary and dual-licensed.
4
- # Licensed under the Prosperity Public License 3.0 (the "License").
5
- # A copy of the license is available at https://prosperitylicense.com/versions/3.0.0
6
- # For details, see the LICENSE file.
7
- # Commercial use beyond a 30-day trial requires a separate license.
8
- #
9
- # Source Code: https://github.com/CoReason-AI/coreason-manifest
10
-
11
- from datetime import datetime, timezone
12
- from typing import Annotated, Any, Dict, Generic, Literal, Optional, Protocol, TypeVar, Union, runtime_checkable
13
- from uuid import uuid4
14
-
15
- from pydantic import ConfigDict, Field
16
-
17
- from coreason_manifest.definitions.base import CoReasonBaseModel
18
- from coreason_manifest.definitions.topology import RuntimeVisualMetadata
19
-
20
- # --- CloudEvents v1.0 Implementation ---
21
-
22
- T = TypeVar("T")
23
-
24
-
25
- @runtime_checkable
26
- class CloudEventSource(Protocol):
27
- def as_cloud_event_payload(self) -> Any: ...
28
-
29
-
30
- class CloudEvent(CoReasonBaseModel, Generic[T]):
31
- """Standard CloudEvent v1.0 Envelope."""
32
-
33
- model_config = ConfigDict(extra="allow", populate_by_name=True)
34
-
35
- specversion: Literal["1.0"] = "1.0"
36
- type: str = Field(..., description="Type of occurrence (e.g. ai.coreason.node.started)")
37
- source: str = Field(..., description="URI identifying the event producer")
38
- id: str = Field(default_factory=lambda: str(uuid4()), description="Unique event identifier")
39
- time: datetime = Field(
40
- default_factory=lambda: datetime.now(timezone.utc), description="Timestamp of when the occurrence happened"
41
- )
42
- datacontenttype: Literal["application/json"] = "application/json"
43
-
44
- data: Optional[T] = Field(None, description="The event payload")
45
-
46
- # Distributed Tracing Extensions (W3C)
47
- traceparent: Optional[str] = Field(None, description="W3C Trace Context traceparent")
48
- tracestate: Optional[str] = Field(None, description="W3C Trace Context tracestate")
49
-
50
-
51
- # --- OTel Semantic Conventions ---
52
-
53
-
54
- class GenAIUsage(CoReasonBaseModel):
55
- """GenAI Usage metrics."""
56
-
57
- input_tokens: Optional[int] = Field(None, alias="input_tokens")
58
- output_tokens: Optional[int] = Field(None, alias="output_tokens")
59
-
60
- model_config = ConfigDict(populate_by_name=True)
61
-
62
-
63
- class GenAIRequest(CoReasonBaseModel):
64
- """GenAI Request details."""
65
-
66
- model: Optional[str] = None
67
- temperature: Optional[float] = None
68
- top_p: Optional[float] = None
69
-
70
-
71
- class GenAICompletion(CoReasonBaseModel):
72
- """GenAI Completion details."""
73
-
74
- chunk: Optional[str] = None
75
- finish_reason: Optional[str] = None
76
-
77
-
78
- class GenAISemantics(CoReasonBaseModel):
79
- """OpenTelemetry GenAI Semantic Conventions."""
80
-
81
- system: Optional[str] = None
82
- usage: Optional[GenAIUsage] = None
83
- request: Optional[GenAIRequest] = None
84
- completion: Optional[GenAICompletion] = None
85
-
86
-
87
- # --- Base Models ---
88
-
89
-
90
- class BaseNodePayload(CoReasonBaseModel):
91
- """Base model for node-related events."""
92
-
93
- model_config = ConfigDict(extra="ignore")
94
- node_id: str
95
-
96
- # Common OTel attributes that might appear in payload
97
- model: Optional[str] = None
98
- system: Optional[str] = None
99
-
100
- def as_cloud_event_payload(self) -> Any:
101
- return self
102
-
103
-
104
- # --- Payload Models ---
105
-
106
-
107
- class NodeInit(BaseNodePayload):
108
- """Payload for NODE_INIT event."""
109
-
110
- type: str = "DEFAULT"
111
- visual_cue: str = "IDLE"
112
-
113
-
114
- class NodeStarted(BaseNodePayload):
115
- """Payload for NODE_START event."""
116
-
117
- timestamp: float
118
- status: Literal["RUNNING"] = "RUNNING"
119
- visual_cue: str = "PULSE"
120
- input_tokens: Optional[int] = None
121
-
122
- def as_cloud_event_payload(self) -> Any:
123
- semantics = GenAISemantics()
124
- has_semantics = False
125
-
126
- if self.input_tokens is not None:
127
- semantics.usage = GenAIUsage(input_tokens=self.input_tokens)
128
- has_semantics = True
129
-
130
- if self.model:
131
- semantics.request = GenAIRequest(model=self.model)
132
- has_semantics = True
133
-
134
- if self.system:
135
- semantics.system = self.system
136
- has_semantics = True
137
-
138
- return StandardizedNodeStarted(
139
- node_id=self.node_id,
140
- status=self.status,
141
- gen_ai=semantics if has_semantics else None,
142
- )
143
-
144
-
145
- class NodeCompleted(BaseNodePayload):
146
- """Payload for NODE_DONE event."""
147
-
148
- output_summary: str
149
- status: Literal["SUCCESS"] = "SUCCESS"
150
- visual_cue: str = "GREEN_GLOW"
151
- cost: Optional[float] = None
152
-
153
- def as_cloud_event_payload(self) -> Any:
154
- semantics = GenAISemantics()
155
- has_semantics = False
156
-
157
- if self.model:
158
- semantics.request = GenAIRequest(model=self.model)
159
- has_semantics = True
160
-
161
- return StandardizedNodeCompleted(
162
- node_id=self.node_id,
163
- output_summary=self.output_summary,
164
- status=self.status,
165
- gen_ai=semantics if has_semantics else None,
166
- )
167
-
168
-
169
- class NodeRestored(BaseNodePayload):
170
- """Payload for NODE_RESTORED event."""
171
-
172
- output_summary: str
173
- status: Literal["RESTORED"] = "RESTORED"
174
- visual_cue: str = "INSTANT_GREEN"
175
-
176
-
177
- class NodeSkipped(BaseNodePayload):
178
- """Payload for NODE_SKIPPED event."""
179
-
180
- status: Literal["SKIPPED"] = "SKIPPED"
181
- visual_cue: str = "GREY_OUT"
182
-
183
-
184
- class NodeStream(BaseNodePayload):
185
- """Payload for NODE_STREAM event."""
186
-
187
- chunk: str
188
- visual_cue: str = "TEXT_BUBBLE"
189
-
190
- def as_cloud_event_payload(self) -> Any:
191
- semantics = GenAISemantics(completion=GenAICompletion(chunk=self.chunk))
192
- if self.model:
193
- if not semantics.request:
194
- semantics.request = GenAIRequest()
195
- semantics.request.model = self.model
196
-
197
- return StandardizedNodeStream(node_id=self.node_id, gen_ai=semantics)
198
-
199
-
200
- class ArtifactGenerated(BaseNodePayload):
201
- """Payload for ARTIFACT_GENERATED event."""
202
-
203
- artifact_type: str = "PDF"
204
- url: str
205
-
206
-
207
- class EdgeTraversed(CoReasonBaseModel):
208
- """Payload for EDGE_ACTIVE event."""
209
-
210
- model_config = ConfigDict(extra="ignore")
211
- source: str
212
- target: str
213
- animation_speed: str = "FAST"
214
-
215
- def as_cloud_event_payload(self) -> Any:
216
- return self
217
-
218
-
219
- class CouncilVote(BaseNodePayload):
220
- """Payload for COUNCIL_VOTE event."""
221
-
222
- votes: Dict[str, str]
223
-
224
-
225
- class WorkflowError(BaseNodePayload):
226
- """Payload for ERROR event."""
227
-
228
- error_message: str
229
- stack_trace: str
230
- input_snapshot: Dict[str, Any]
231
- status: Literal["ERROR"] = "ERROR"
232
- visual_cue: str = "RED_FLASH"
233
-
234
-
235
- # --- Standardized Payloads ---
236
-
237
-
238
- class StandardizedNodeStarted(BaseNodePayload):
239
- """Standardized payload for node start with OTel support."""
240
-
241
- gen_ai: Optional[GenAISemantics] = None
242
- status: Literal["RUNNING"] = "RUNNING"
243
-
244
-
245
- class StandardizedNodeCompleted(BaseNodePayload):
246
- """Standardized payload for node completion with OTel support."""
247
-
248
- gen_ai: Optional[GenAISemantics] = None
249
- output_summary: str
250
- status: Literal["SUCCESS"] = "SUCCESS"
251
-
252
-
253
- class StandardizedNodeStream(BaseNodePayload):
254
- """Standardized payload for node stream with OTel support."""
255
-
256
- gen_ai: Optional[GenAISemantics] = None
257
-
258
-
259
- # --- Aliases for Backward Compatibility ---
260
- # These ensure that code importing `NodeInitPayload` still works.
261
- NodeInitPayload = NodeInit
262
- NodeStartedPayload = NodeStarted
263
- NodeCompletedPayload = NodeCompleted
264
- NodeSkippedPayload = NodeSkipped
265
- NodeStreamPayload = NodeStream
266
- EdgeTraversedPayload = EdgeTraversed
267
- ArtifactGeneratedPayload = ArtifactGenerated
268
- CouncilVotePayload = CouncilVote
269
- WorkflowErrorPayload = WorkflowError
270
-
271
-
272
- # --- Graph Event Wrapper ---
273
-
274
-
275
- class BaseGraphEvent(CoReasonBaseModel):
276
- """Base class for GraphEvents.
277
-
278
- Standardized IDs:
279
- - run_id: Workflow execution ID.
280
- - trace_id: OpenTelemetry Distributed Trace ID.
281
- """
282
-
283
- model_config = ConfigDict(extra="forbid")
284
-
285
- run_id: str
286
- trace_id: str = Field(
287
- default_factory=lambda: "unknown"
288
- ) # Default for compatibility if missing in some legacy calls
289
- node_id: str # Required per BRD and existing tests
290
- timestamp: float
291
- sequence_id: Optional[int] = None # Optional for internal use
292
-
293
- # Visual Metadata drives the Flutter animation engine
294
- visual_metadata: RuntimeVisualMetadata = Field(
295
- ..., description="Hints for UI: color='#00FF00', animation='pulse', progress='0.5'"
296
- )
297
-
298
-
299
- class GraphEventNodeInit(BaseGraphEvent):
300
- event_type: Literal["NODE_INIT"] = "NODE_INIT"
301
- payload: NodeInit = Field(..., description="The logic output")
302
-
303
-
304
- class GraphEventNodeStart(BaseGraphEvent):
305
- event_type: Literal["NODE_START"] = "NODE_START"
306
- payload: NodeStarted = Field(..., description="The logic output")
307
-
308
-
309
- class GraphEventNodeStream(BaseGraphEvent):
310
- event_type: Literal["NODE_STREAM"] = "NODE_STREAM"
311
- payload: NodeStream = Field(..., description="The logic output")
312
-
313
-
314
- class GraphEventNodeDone(BaseGraphEvent):
315
- event_type: Literal["NODE_DONE"] = "NODE_DONE"
316
- payload: NodeCompleted = Field(..., description="The logic output")
317
-
318
-
319
- class GraphEventNodeSkipped(BaseGraphEvent):
320
- event_type: Literal["NODE_SKIPPED"] = "NODE_SKIPPED"
321
- payload: NodeSkipped = Field(..., description="The logic output")
322
-
323
-
324
- class GraphEventEdgeActive(BaseGraphEvent):
325
- event_type: Literal["EDGE_ACTIVE"] = "EDGE_ACTIVE"
326
- payload: EdgeTraversed = Field(..., description="The logic output")
327
-
328
-
329
- class GraphEventCouncilVote(BaseGraphEvent):
330
- event_type: Literal["COUNCIL_VOTE"] = "COUNCIL_VOTE"
331
- payload: CouncilVote = Field(..., description="The logic output")
332
-
333
-
334
- class GraphEventError(BaseGraphEvent):
335
- event_type: Literal["ERROR"] = "ERROR"
336
- payload: WorkflowError = Field(..., description="The logic output")
337
-
338
-
339
- class GraphEventNodeRestored(BaseGraphEvent):
340
- event_type: Literal["NODE_RESTORED"] = "NODE_RESTORED"
341
- payload: NodeRestored = Field(..., description="The logic output")
342
-
343
-
344
- class GraphEventArtifactGenerated(BaseGraphEvent):
345
- event_type: Literal["ARTIFACT_GENERATED"] = "ARTIFACT_GENERATED"
346
- payload: ArtifactGenerated = Field(..., description="The logic output")
347
-
348
-
349
- GraphEvent = Annotated[
350
- Union[
351
- GraphEventNodeInit,
352
- GraphEventNodeStart,
353
- GraphEventNodeStream,
354
- GraphEventNodeDone,
355
- GraphEventNodeSkipped,
356
- GraphEventEdgeActive,
357
- GraphEventCouncilVote,
358
- GraphEventError,
359
- GraphEventNodeRestored,
360
- GraphEventArtifactGenerated,
361
- ],
362
- Field(discriminator="event_type", description="Polymorphic graph event definition."),
363
- ]
364
-
365
-
366
- # --- Migration Logic ---
367
-
368
-
369
- def migrate_graph_event_to_cloud_event(event: GraphEvent) -> CloudEvent[Any]:
370
- """Migrates a legacy GraphEvent to a CloudEvent v1.0."""
371
-
372
- ce_type_map = {
373
- "NODE_START": "ai.coreason.node.started",
374
- "NODE_STREAM": "ai.coreason.node.stream",
375
- "NODE_DONE": "ai.coreason.node.completed",
376
- }
377
- ce_type = ce_type_map.get(event.event_type, f"ai.coreason.legacy.{event.event_type.lower()}")
378
- ce_source = f"urn:node:{event.node_id}"
379
-
380
- data: Any = None
381
-
382
- # event.payload is already a strictly typed Pydantic model (from GraphEvent union).
383
- # All supported payload models implement CloudEventSource protocol (duck-typed or via BaseNodePayload).
384
- if isinstance(event.payload, CloudEventSource):
385
- data = event.payload.as_cloud_event_payload()
386
- else:
387
- # Fallback for models that might not implement the protocol (e.g. unknown future extensions)
388
- data = event.payload
389
-
390
- # UI Metadata as extension
391
- payload_visual_cue = getattr(event.payload, "visual_cue", None)
392
-
393
- visual_dict = event.visual_metadata.model_dump(exclude_none=True)
394
- extensions = {
395
- "com_coreason_ui_cue": event.visual_metadata.animation or payload_visual_cue,
396
- "com_coreason_ui_metadata": visual_dict,
397
- }
398
-
399
- # Filter out None values in extensions
400
- # For dictionaries, we want to filter out empty dicts too.
401
- filtered_extensions = {}
402
- for k, v in extensions.items():
403
- if v is None:
404
- continue
405
- if isinstance(v, dict) and not v:
406
- continue
407
- if isinstance(v, str) and not v:
408
- continue
409
- # Also check if it's a dict containing only empty strings (recursive check not needed for now, just 1 level)
410
- if isinstance(v, dict) and all(isinstance(val, str) and not val for val in v.values()):
411
- continue
412
-
413
- filtered_extensions[k] = v
414
-
415
- extensions = filtered_extensions
416
-
417
- return CloudEvent(
418
- type=ce_type,
419
- source=ce_source,
420
- time=datetime.fromtimestamp(event.timestamp, timezone.utc),
421
- data=data,
422
- **extensions,
423
- )