coreason-manifest 0.7.0__py3-none-any.whl → 0.10.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 +85 -4
- coreason_manifest/definitions/__init__.py +60 -0
- coreason_manifest/definitions/agent.py +177 -38
- coreason_manifest/definitions/audit.py +178 -4
- coreason_manifest/definitions/base.py +47 -0
- coreason_manifest/definitions/events.py +423 -0
- coreason_manifest/definitions/message.py +188 -0
- coreason_manifest/definitions/simulation.py +69 -9
- coreason_manifest/definitions/simulation_config.py +46 -0
- coreason_manifest/definitions/topology.py +210 -9
- coreason_manifest/recipes.py +53 -7
- coreason_manifest/schemas/__init__.py +9 -1
- coreason_manifest/schemas/agent.schema.json +855 -27
- coreason_manifest/schemas/recipe.schema.json +813 -0
- coreason_manifest/utils/__init__.py +10 -0
- coreason_manifest/utils/logger.py +10 -0
- coreason_manifest/v1/__init__.py +15 -0
- {coreason_manifest-0.7.0.dist-info → coreason_manifest-0.10.0.dist-info}/METADATA +91 -29
- coreason_manifest-0.10.0.dist-info/RECORD +22 -0
- {coreason_manifest-0.7.0.dist-info → coreason_manifest-0.10.0.dist-info}/WHEEL +1 -1
- coreason_manifest-0.7.0.dist-info/RECORD +0 -16
- {coreason_manifest-0.7.0.dist-info → coreason_manifest-0.10.0.dist-info}/licenses/LICENSE +0 -0
- {coreason_manifest-0.7.0.dist-info → coreason_manifest-0.10.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -0,0 +1,47 @@
|
|
|
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 typing import Any, Dict
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, ConfigDict
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CoReasonBaseModel(BaseModel):
|
|
17
|
+
"""Base model for all CoReason Pydantic models with enhanced serialization.
|
|
18
|
+
|
|
19
|
+
This base class addresses JSON serialization challenges in Pydantic v2 (e.g., UUID, datetime)
|
|
20
|
+
by providing standardized methods (`dump`, `to_json`) with optimal configuration.
|
|
21
|
+
|
|
22
|
+
For a detailed rationale, see `docs/coreason_base_model_rationale.md`.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
26
|
+
|
|
27
|
+
def dump(self, **kwargs: Any) -> Dict[str, Any]:
|
|
28
|
+
"""Serialize the model to a JSON-compatible dictionary.
|
|
29
|
+
|
|
30
|
+
Uses mode='json' to ensure types like UUID and datetime are serialized to strings.
|
|
31
|
+
Defaults to by_alias=True and exclude_none=True.
|
|
32
|
+
"""
|
|
33
|
+
# Set defaults but allow overrides
|
|
34
|
+
kwargs.setdefault("mode", "json")
|
|
35
|
+
kwargs.setdefault("by_alias", True)
|
|
36
|
+
kwargs.setdefault("exclude_none", True)
|
|
37
|
+
return self.model_dump(**kwargs)
|
|
38
|
+
|
|
39
|
+
def to_json(self, **kwargs: Any) -> str:
|
|
40
|
+
"""Serialize the model to a JSON string.
|
|
41
|
+
|
|
42
|
+
Defaults to by_alias=True and exclude_none=True.
|
|
43
|
+
"""
|
|
44
|
+
# Set defaults but allow overrides
|
|
45
|
+
kwargs.setdefault("by_alias", True)
|
|
46
|
+
kwargs.setdefault("exclude_none", True)
|
|
47
|
+
return self.model_dump_json(**kwargs)
|
|
@@ -0,0 +1,423 @@
|
|
|
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
|
+
)
|
|
@@ -0,0 +1,188 @@
|
|
|
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 functools
|
|
12
|
+
import json
|
|
13
|
+
import warnings
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from typing import Annotated, Any, Dict, List, Literal, Optional, Union, cast
|
|
16
|
+
|
|
17
|
+
from pydantic import ConfigDict, Field, model_validator
|
|
18
|
+
|
|
19
|
+
from coreason_manifest.definitions.base import CoReasonBaseModel
|
|
20
|
+
|
|
21
|
+
# --- Enums ---
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Role(str, Enum):
|
|
25
|
+
SYSTEM = "system"
|
|
26
|
+
USER = "user"
|
|
27
|
+
ASSISTANT = "assistant"
|
|
28
|
+
TOOL = "tool"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Modality(str, Enum):
|
|
32
|
+
TEXT = "text"
|
|
33
|
+
IMAGE = "image"
|
|
34
|
+
AUDIO = "audio"
|
|
35
|
+
VIDEO = "video"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# --- Message Parts ---
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TextPart(CoReasonBaseModel):
|
|
42
|
+
"""Represents text content sent to or received from the model."""
|
|
43
|
+
|
|
44
|
+
model_config = ConfigDict(extra="ignore")
|
|
45
|
+
type: Literal["text"] = "text"
|
|
46
|
+
content: str
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class BlobPart(CoReasonBaseModel):
|
|
50
|
+
"""Represents blob binary data sent inline to the model."""
|
|
51
|
+
|
|
52
|
+
model_config = ConfigDict(extra="ignore")
|
|
53
|
+
type: Literal["blob"] = "blob"
|
|
54
|
+
content: str # Base64 encoded string
|
|
55
|
+
modality: Modality
|
|
56
|
+
mime_type: Optional[str] = None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class FilePart(CoReasonBaseModel):
|
|
60
|
+
"""Represents an external referenced file sent to the model by file id."""
|
|
61
|
+
|
|
62
|
+
model_config = ConfigDict(extra="ignore")
|
|
63
|
+
type: Literal["file"] = "file"
|
|
64
|
+
file_id: str
|
|
65
|
+
modality: Modality
|
|
66
|
+
mime_type: Optional[str] = None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class UriPart(CoReasonBaseModel):
|
|
70
|
+
"""Represents an external referenced file sent to the model by URI."""
|
|
71
|
+
|
|
72
|
+
model_config = ConfigDict(extra="ignore")
|
|
73
|
+
type: Literal["uri"] = "uri"
|
|
74
|
+
uri: str
|
|
75
|
+
modality: Modality
|
|
76
|
+
mime_type: Optional[str] = None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class ToolCallRequestPart(CoReasonBaseModel):
|
|
80
|
+
"""Represents a tool call requested by the model."""
|
|
81
|
+
|
|
82
|
+
model_config = ConfigDict(extra="ignore")
|
|
83
|
+
type: Literal["tool_call"] = "tool_call"
|
|
84
|
+
name: str
|
|
85
|
+
arguments: Union[Dict[str, Any], str] # Structured arguments or JSON string
|
|
86
|
+
id: Optional[str] = None
|
|
87
|
+
|
|
88
|
+
@functools.cached_property
|
|
89
|
+
def parsed_arguments(self) -> Dict[str, Any]:
|
|
90
|
+
"""Return arguments as a dictionary, parsing JSON if necessary."""
|
|
91
|
+
if isinstance(self.arguments, dict):
|
|
92
|
+
return self.arguments
|
|
93
|
+
try:
|
|
94
|
+
result = json.loads(self.arguments)
|
|
95
|
+
return cast(Dict[str, Any], result) if isinstance(result, dict) else {}
|
|
96
|
+
except (json.JSONDecodeError, TypeError):
|
|
97
|
+
return {}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class ToolCallResponsePart(CoReasonBaseModel):
|
|
101
|
+
"""Represents a tool call result sent to the model."""
|
|
102
|
+
|
|
103
|
+
model_config = ConfigDict(extra="ignore")
|
|
104
|
+
type: Literal["tool_call_response"] = "tool_call_response"
|
|
105
|
+
response: Any # The result of the tool call
|
|
106
|
+
id: Optional[str] = None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class ReasoningPart(CoReasonBaseModel):
|
|
110
|
+
"""Represents reasoning/thinking content received from the model."""
|
|
111
|
+
|
|
112
|
+
model_config = ConfigDict(extra="ignore")
|
|
113
|
+
type: Literal["reasoning"] = "reasoning"
|
|
114
|
+
content: str
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# --- Union of All Parts ---
|
|
118
|
+
|
|
119
|
+
Part = Annotated[
|
|
120
|
+
Union[TextPart, BlobPart, FilePart, UriPart, ToolCallRequestPart, ToolCallResponsePart, ReasoningPart],
|
|
121
|
+
Field(discriminator="type"),
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
# --- Main Message Model ---
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class ChatMessage(CoReasonBaseModel):
|
|
128
|
+
"""Represents a message in a conversation with an LLM."""
|
|
129
|
+
|
|
130
|
+
model_config = ConfigDict(extra="ignore")
|
|
131
|
+
|
|
132
|
+
role: Role
|
|
133
|
+
parts: List[Part] = Field(..., description="List of message parts that make up the message content.")
|
|
134
|
+
name: Optional[str] = None
|
|
135
|
+
|
|
136
|
+
@classmethod
|
|
137
|
+
def user(cls, content: str, name: Optional[str] = None) -> "ChatMessage":
|
|
138
|
+
"""Factory method to create a user message with text content."""
|
|
139
|
+
return cls(role=Role.USER, parts=[TextPart(content=content)], name=name)
|
|
140
|
+
|
|
141
|
+
@classmethod
|
|
142
|
+
def assistant(cls, content: str, name: Optional[str] = None) -> "ChatMessage":
|
|
143
|
+
"""Factory method to create an assistant message with text content."""
|
|
144
|
+
return cls(role=Role.ASSISTANT, parts=[TextPart(content=content)], name=name)
|
|
145
|
+
|
|
146
|
+
@classmethod
|
|
147
|
+
def tool(cls, tool_call_id: str, content: Any) -> "ChatMessage":
|
|
148
|
+
"""Factory method to create a tool message with the result."""
|
|
149
|
+
return cls(role=Role.TOOL, parts=[ToolCallResponsePart(id=tool_call_id, response=content)])
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# --- Backward Compatibility ---
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class FunctionCall(CoReasonBaseModel):
|
|
156
|
+
"""Deprecated: Use ToolCallRequestPart instead."""
|
|
157
|
+
|
|
158
|
+
name: str
|
|
159
|
+
arguments: str
|
|
160
|
+
|
|
161
|
+
@model_validator(mode="after")
|
|
162
|
+
def warn_deprecated(self) -> "FunctionCall":
|
|
163
|
+
warnings.warn(
|
|
164
|
+
"FunctionCall is deprecated. Use ToolCallRequestPart instead.",
|
|
165
|
+
DeprecationWarning,
|
|
166
|
+
stacklevel=2,
|
|
167
|
+
)
|
|
168
|
+
return self
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class ToolCall(CoReasonBaseModel):
|
|
172
|
+
"""Deprecated: Use ToolCallRequestPart instead."""
|
|
173
|
+
|
|
174
|
+
id: str
|
|
175
|
+
type: str = "function"
|
|
176
|
+
function: FunctionCall
|
|
177
|
+
|
|
178
|
+
@model_validator(mode="after")
|
|
179
|
+
def warn_deprecated(self) -> "ToolCall":
|
|
180
|
+
warnings.warn(
|
|
181
|
+
"ToolCall is deprecated. Use ToolCallRequestPart instead.",
|
|
182
|
+
DeprecationWarning,
|
|
183
|
+
stacklevel=2,
|
|
184
|
+
)
|
|
185
|
+
return self
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
Message = ChatMessage
|