coreason-manifest 0.10.0__py3-none-any.whl → 0.13.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.
- coreason_manifest/__init__.py +35 -82
- coreason_manifest/{definitions/base.py → common.py} +18 -1
- coreason_manifest/governance.py +83 -0
- coreason_manifest/schemas/coreason-v2.schema.json +462 -0
- coreason_manifest/utils/logger.py +0 -45
- coreason_manifest/v2/__init__.py +1 -0
- coreason_manifest/v2/governance.py +144 -0
- coreason_manifest/v2/io.py +132 -0
- coreason_manifest/v2/resolver.py +67 -0
- coreason_manifest/v2/spec/__init__.py +1 -0
- coreason_manifest/v2/spec/contracts.py +34 -0
- coreason_manifest/v2/spec/definitions.py +196 -0
- coreason_manifest/v2/validator.py +48 -0
- {coreason_manifest-0.10.0.dist-info → coreason_manifest-0.13.0.dist-info}/METADATA +48 -72
- coreason_manifest-0.13.0.dist-info/RECORD +20 -0
- coreason_manifest/definitions/__init__.py +0 -60
- coreason_manifest/definitions/agent.py +0 -370
- coreason_manifest/definitions/audit.py +0 -181
- coreason_manifest/definitions/events.py +0 -423
- coreason_manifest/definitions/message.py +0 -188
- coreason_manifest/definitions/simulation.py +0 -79
- coreason_manifest/definitions/simulation_config.py +0 -46
- coreason_manifest/definitions/topology.py +0 -341
- coreason_manifest/recipes.py +0 -84
- coreason_manifest/schemas/agent.schema.json +0 -1051
- coreason_manifest/schemas/recipe.schema.json +0 -813
- coreason_manifest/v1/__init__.py +0 -15
- coreason_manifest-0.10.0.dist-info/RECORD +0 -22
- {coreason_manifest-0.10.0.dist-info → coreason_manifest-0.13.0.dist-info}/WHEEL +0 -0
- {coreason_manifest-0.10.0.dist-info → coreason_manifest-0.13.0.dist-info}/licenses/LICENSE +0 -0
- {coreason_manifest-0.10.0.dist-info → coreason_manifest-0.13.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
|
-
)
|