fba-bench-core 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.
- fba_bench_core/__init__.py +11 -0
- fba_bench_core/agents/__init__.py +15 -0
- fba_bench_core/agents/base.py +83 -0
- fba_bench_core/agents/registry.py +16 -0
- fba_bench_core/benchmarking/__init__.py +6 -0
- fba_bench_core/benchmarking/core/__init__.py +1 -0
- fba_bench_core/benchmarking/engine/__init__.py +12 -0
- fba_bench_core/benchmarking/engine/core.py +135 -0
- fba_bench_core/benchmarking/engine/models.py +62 -0
- fba_bench_core/benchmarking/metrics/__init__.py +30 -0
- fba_bench_core/benchmarking/metrics/accuracy_score.py +27 -0
- fba_bench_core/benchmarking/metrics/aggregate.py +39 -0
- fba_bench_core/benchmarking/metrics/completeness.py +38 -0
- fba_bench_core/benchmarking/metrics/cost_efficiency.py +32 -0
- fba_bench_core/benchmarking/metrics/custom_scriptable.py +17 -0
- fba_bench_core/benchmarking/metrics/keyword_coverage.py +41 -0
- fba_bench_core/benchmarking/metrics/policy_compliance.py +18 -0
- fba_bench_core/benchmarking/metrics/registry.py +57 -0
- fba_bench_core/benchmarking/metrics/robustness.py +27 -0
- fba_bench_core/benchmarking/metrics/technical_performance.py +16 -0
- fba_bench_core/benchmarking/registry.py +48 -0
- fba_bench_core/benchmarking/scenarios/__init__.py +1 -0
- fba_bench_core/benchmarking/scenarios/base.py +36 -0
- fba_bench_core/benchmarking/scenarios/complex_marketplace.py +181 -0
- fba_bench_core/benchmarking/scenarios/multiturn_tool_use.py +176 -0
- fba_bench_core/benchmarking/scenarios/registry.py +18 -0
- fba_bench_core/benchmarking/scenarios/research_summarization.py +141 -0
- fba_bench_core/benchmarking/validators/__init__.py +24 -0
- fba_bench_core/benchmarking/validators/determinism_check.py +95 -0
- fba_bench_core/benchmarking/validators/fairness_balance.py +75 -0
- fba_bench_core/benchmarking/validators/outlier_detection.py +53 -0
- fba_bench_core/benchmarking/validators/registry.py +57 -0
- fba_bench_core/benchmarking/validators/reproducibility_metadata.py +74 -0
- fba_bench_core/benchmarking/validators/schema_adherence.py +59 -0
- fba_bench_core/benchmarking/validators/structural_consistency.py +74 -0
- fba_bench_core/config.py +154 -0
- fba_bench_core/domain/__init__.py +75 -0
- fba_bench_core/domain/events/__init__.py +230 -0
- fba_bench_core/domain/events/analytics.py +69 -0
- fba_bench_core/domain/events/base.py +59 -0
- fba_bench_core/domain/events/inventory.py +119 -0
- fba_bench_core/domain/events/marketing.py +102 -0
- fba_bench_core/domain/events/pricing.py +179 -0
- fba_bench_core/domain/models.py +296 -0
- fba_bench_core/exceptions/__init__.py +9 -0
- fba_bench_core/exceptions/base.py +46 -0
- fba_bench_core/services/__init__.py +12 -0
- fba_bench_core/services/base.py +52 -0
- fba_bench_core-1.0.0.dist-info/METADATA +152 -0
- fba_bench_core-1.0.0.dist-info/RECORD +52 -0
- fba_bench_core-1.0.0.dist-info/WHEEL +4 -0
- fba_bench_core-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,75 @@
|
|
1
|
+
"""Domain package exports for fba_bench_core.
|
2
|
+
|
3
|
+
This module exposes the core domain contracts and the expanded event/command
|
4
|
+
vocabulary introduced in Phase C.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from .events import ( # Base types; Events; Commands; helpers
|
8
|
+
AdjustFulfillmentLatencyCommand,
|
9
|
+
AdjustPriceCommand,
|
10
|
+
AnomalyDetected,
|
11
|
+
AnyCommand,
|
12
|
+
AnyEvent,
|
13
|
+
BaseEvent,
|
14
|
+
Command,
|
15
|
+
CustomerComplaintLogged,
|
16
|
+
DemandSpiked,
|
17
|
+
ForecastUpdated,
|
18
|
+
FulfillmentDelayed,
|
19
|
+
LaunchPromotionCommand,
|
20
|
+
PlaceReplenishmentOrderCommand,
|
21
|
+
PriceChangedExternally,
|
22
|
+
PromotionLaunched,
|
23
|
+
ReforecastDemandCommand,
|
24
|
+
ResolveCustomerIssueCommand,
|
25
|
+
SaleOccurred,
|
26
|
+
StartCustomerOutreachCommand,
|
27
|
+
StockDepleted,
|
28
|
+
StockReplenished,
|
29
|
+
TransferInventoryCommand,
|
30
|
+
UpdateSafetyStockCommand,
|
31
|
+
get_command_class_for_type,
|
32
|
+
get_event_class_for_type,
|
33
|
+
)
|
34
|
+
from .models import (
|
35
|
+
Competitor,
|
36
|
+
CompetitorListing,
|
37
|
+
DemandProfile,
|
38
|
+
InventorySnapshot,
|
39
|
+
Product,
|
40
|
+
)
|
41
|
+
|
42
|
+
__all__ = [
|
43
|
+
# models
|
44
|
+
"Product",
|
45
|
+
"InventorySnapshot",
|
46
|
+
"CompetitorListing",
|
47
|
+
"Competitor",
|
48
|
+
"DemandProfile",
|
49
|
+
# events & commands
|
50
|
+
"BaseEvent",
|
51
|
+
"Command",
|
52
|
+
"SaleOccurred",
|
53
|
+
"PriceChangedExternally",
|
54
|
+
"DemandSpiked",
|
55
|
+
"StockReplenished",
|
56
|
+
"StockDepleted",
|
57
|
+
"FulfillmentDelayed",
|
58
|
+
"PromotionLaunched",
|
59
|
+
"CustomerComplaintLogged",
|
60
|
+
"ForecastUpdated",
|
61
|
+
"AnomalyDetected",
|
62
|
+
"AnyEvent",
|
63
|
+
"AdjustPriceCommand",
|
64
|
+
"LaunchPromotionCommand",
|
65
|
+
"PlaceReplenishmentOrderCommand",
|
66
|
+
"TransferInventoryCommand",
|
67
|
+
"UpdateSafetyStockCommand",
|
68
|
+
"ResolveCustomerIssueCommand",
|
69
|
+
"StartCustomerOutreachCommand",
|
70
|
+
"ReforecastDemandCommand",
|
71
|
+
"AdjustFulfillmentLatencyCommand",
|
72
|
+
"AnyCommand",
|
73
|
+
"get_event_class_for_type",
|
74
|
+
"get_command_class_for_type",
|
75
|
+
]
|
@@ -0,0 +1,230 @@
|
|
1
|
+
"""Compatibility layer for fba_bench_core.domain.events.
|
2
|
+
|
3
|
+
This module re-exports all event classes, command classes, unions (AnyEvent, AnyCommand),
|
4
|
+
EventType enum, and helper functions from the modular domain/events submodules to
|
5
|
+
provide full backward compatibility. Existing code importing from fba_bench_core.domain.events
|
6
|
+
will continue to work unchanged.
|
7
|
+
|
8
|
+
Do not add new definitions here; maintain only re-exports and registry logic.
|
9
|
+
"""
|
10
|
+
|
11
|
+
from __future__ import annotations
|
12
|
+
|
13
|
+
import re
|
14
|
+
from enum import Enum
|
15
|
+
|
16
|
+
from .analytics import (
|
17
|
+
AdjustFulfillmentLatencyCommand,
|
18
|
+
AnomalyDetected,
|
19
|
+
ForecastUpdated,
|
20
|
+
NegotiateSupplyCommand,
|
21
|
+
ReforecastDemandCommand,
|
22
|
+
)
|
23
|
+
from .base import BaseEvent, Command
|
24
|
+
from .inventory import (
|
25
|
+
AdjustInventoryCommand,
|
26
|
+
FulfillmentDelayed,
|
27
|
+
PlaceReplenishmentOrderCommand,
|
28
|
+
StockDepleted,
|
29
|
+
StockReplenished,
|
30
|
+
SupplyDisruption,
|
31
|
+
TransferInventoryCommand,
|
32
|
+
UpdateSafetyStockCommand,
|
33
|
+
)
|
34
|
+
from .marketing import (
|
35
|
+
CustomerComplaintLogged,
|
36
|
+
PromotionLaunched,
|
37
|
+
ResolveCustomerIssueCommand,
|
38
|
+
RespondToComplaintCommand,
|
39
|
+
StartCustomerOutreachCommand,
|
40
|
+
)
|
41
|
+
from .pricing import (
|
42
|
+
AdjustPriceCommand,
|
43
|
+
CompetitorAction,
|
44
|
+
DemandSpiked,
|
45
|
+
LaunchPromotionCommand,
|
46
|
+
MonitorCompetitorCommand,
|
47
|
+
PriceChangedExternally,
|
48
|
+
SaleOccurred,
|
49
|
+
)
|
50
|
+
|
51
|
+
# Unions for type checking
|
52
|
+
AnyEvent = (
|
53
|
+
SaleOccurred
|
54
|
+
| PriceChangedExternally
|
55
|
+
| DemandSpiked
|
56
|
+
| CompetitorAction
|
57
|
+
| StockReplenished
|
58
|
+
| StockDepleted
|
59
|
+
| FulfillmentDelayed
|
60
|
+
| SupplyDisruption
|
61
|
+
| PromotionLaunched
|
62
|
+
| CustomerComplaintLogged
|
63
|
+
| ForecastUpdated
|
64
|
+
| AnomalyDetected
|
65
|
+
)
|
66
|
+
|
67
|
+
AnyCommand = (
|
68
|
+
AdjustPriceCommand
|
69
|
+
| LaunchPromotionCommand
|
70
|
+
| PlaceReplenishmentOrderCommand
|
71
|
+
| TransferInventoryCommand
|
72
|
+
| UpdateSafetyStockCommand
|
73
|
+
| AdjustInventoryCommand
|
74
|
+
| ResolveCustomerIssueCommand
|
75
|
+
| StartCustomerOutreachCommand
|
76
|
+
| RespondToComplaintCommand
|
77
|
+
| ReforecastDemandCommand
|
78
|
+
| AdjustFulfillmentLatencyCommand
|
79
|
+
| NegotiateSupplyCommand
|
80
|
+
| MonitorCompetitorCommand
|
81
|
+
)
|
82
|
+
|
83
|
+
|
84
|
+
# Registry setup (moved from original events.py)
|
85
|
+
def _camel_to_snake(name: str) -> str:
|
86
|
+
s1 = re.sub(r"(.)([A-Z][a-z]+)", r"\1_\2", name)
|
87
|
+
s2 = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", s1)
|
88
|
+
return s2.replace("__", "_").lower()
|
89
|
+
|
90
|
+
|
91
|
+
def _extract_event_type_from_class(cls: type[BaseEvent]) -> str:
|
92
|
+
val = cls.__dict__.get("event_type", None)
|
93
|
+
if isinstance(val, str):
|
94
|
+
return val
|
95
|
+
return _camel_to_snake(cls.__name__)
|
96
|
+
|
97
|
+
|
98
|
+
_event_registry: dict[str, type[BaseEvent]] = {
|
99
|
+
_extract_event_type_from_class(cls): cls
|
100
|
+
for cls in (
|
101
|
+
SaleOccurred,
|
102
|
+
PriceChangedExternally,
|
103
|
+
DemandSpiked,
|
104
|
+
CompetitorAction,
|
105
|
+
StockReplenished,
|
106
|
+
StockDepleted,
|
107
|
+
FulfillmentDelayed,
|
108
|
+
SupplyDisruption,
|
109
|
+
PromotionLaunched,
|
110
|
+
CustomerComplaintLogged,
|
111
|
+
ForecastUpdated,
|
112
|
+
AnomalyDetected,
|
113
|
+
)
|
114
|
+
}
|
115
|
+
|
116
|
+
|
117
|
+
def _extract_command_type_from_class(cls: type[Command]) -> str:
|
118
|
+
val = cls.__dict__.get("command_type", None)
|
119
|
+
if isinstance(val, str):
|
120
|
+
return val
|
121
|
+
return _camel_to_snake(cls.__name__)
|
122
|
+
|
123
|
+
|
124
|
+
_command_registry: dict[str, type[Command]] = {}
|
125
|
+
for cls in (
|
126
|
+
AdjustPriceCommand,
|
127
|
+
LaunchPromotionCommand,
|
128
|
+
PlaceReplenishmentOrderCommand,
|
129
|
+
TransferInventoryCommand,
|
130
|
+
UpdateSafetyStockCommand,
|
131
|
+
AdjustInventoryCommand,
|
132
|
+
ResolveCustomerIssueCommand,
|
133
|
+
StartCustomerOutreachCommand,
|
134
|
+
RespondToComplaintCommand,
|
135
|
+
ReforecastDemandCommand,
|
136
|
+
AdjustFulfillmentLatencyCommand,
|
137
|
+
NegotiateSupplyCommand,
|
138
|
+
MonitorCompetitorCommand,
|
139
|
+
):
|
140
|
+
literal = cls.__dict__.get("command_type")
|
141
|
+
if isinstance(literal, str):
|
142
|
+
_command_registry[literal] = cls
|
143
|
+
|
144
|
+
derived = _camel_to_snake(cls.__name__)
|
145
|
+
_command_registry.setdefault(derived, cls)
|
146
|
+
|
147
|
+
if derived.endswith("_command"):
|
148
|
+
short = derived[: -len("_command")]
|
149
|
+
_command_registry.setdefault(short, cls)
|
150
|
+
|
151
|
+
|
152
|
+
# EventType enum (dynamic, derived from the runtime registry)
|
153
|
+
def _safe_member_name(s: str) -> str:
|
154
|
+
name = re.sub(r"\W+", "_", s).upper()
|
155
|
+
if not name:
|
156
|
+
name = "UNKNOWN"
|
157
|
+
if name[0].isdigit():
|
158
|
+
name = "_" + name
|
159
|
+
return name
|
160
|
+
|
161
|
+
|
162
|
+
_event_type_members = {_safe_member_name(k): k for k in _event_registry.keys()}
|
163
|
+
|
164
|
+
EventType = Enum("EventType", _event_type_members, type=str)
|
165
|
+
|
166
|
+
# Attach metadata to enum members
|
167
|
+
for member in EventType:
|
168
|
+
cls = _event_registry.get(member.value)
|
169
|
+
setattr(member, "event_class", cls)
|
170
|
+
setattr(
|
171
|
+
member,
|
172
|
+
"metadata",
|
173
|
+
{
|
174
|
+
"event_class": cls,
|
175
|
+
"doc": getattr(cls, "__doc__", None),
|
176
|
+
"event_type": member.value,
|
177
|
+
},
|
178
|
+
)
|
179
|
+
|
180
|
+
|
181
|
+
# Helper functions
|
182
|
+
def get_event_class_for_type(event_type: str) -> type[BaseEvent] | None:
|
183
|
+
"""Return the event class for a given event_type or None if unknown."""
|
184
|
+
return _event_registry.get(event_type)
|
185
|
+
|
186
|
+
|
187
|
+
def get_command_class_for_type(command_type: str) -> type[Command] | None:
|
188
|
+
"""Return the command class for a given command_type or None if unknown."""
|
189
|
+
return _command_registry.get(command_type)
|
190
|
+
|
191
|
+
|
192
|
+
__all__ = [
|
193
|
+
# Base types
|
194
|
+
"BaseEvent",
|
195
|
+
"Command",
|
196
|
+
# Events
|
197
|
+
"SaleOccurred",
|
198
|
+
"PriceChangedExternally",
|
199
|
+
"DemandSpiked",
|
200
|
+
"CompetitorAction",
|
201
|
+
"StockReplenished",
|
202
|
+
"StockDepleted",
|
203
|
+
"FulfillmentDelayed",
|
204
|
+
"SupplyDisruption",
|
205
|
+
"PromotionLaunched",
|
206
|
+
"CustomerComplaintLogged",
|
207
|
+
"ForecastUpdated",
|
208
|
+
"AnomalyDetected",
|
209
|
+
"AnyEvent",
|
210
|
+
# Commands
|
211
|
+
"AdjustPriceCommand",
|
212
|
+
"LaunchPromotionCommand",
|
213
|
+
"PlaceReplenishmentOrderCommand",
|
214
|
+
"TransferInventoryCommand",
|
215
|
+
"UpdateSafetyStockCommand",
|
216
|
+
"AdjustInventoryCommand",
|
217
|
+
"ResolveCustomerIssueCommand",
|
218
|
+
"StartCustomerOutreachCommand",
|
219
|
+
"RespondToComplaintCommand",
|
220
|
+
"ReforecastDemandCommand",
|
221
|
+
"AdjustFulfillmentLatencyCommand",
|
222
|
+
"NegotiateSupplyCommand",
|
223
|
+
"MonitorCompetitorCommand",
|
224
|
+
"AnyCommand",
|
225
|
+
# Registries / helpers
|
226
|
+
"get_event_class_for_type",
|
227
|
+
"get_command_class_for_type",
|
228
|
+
# EventType enum
|
229
|
+
"EventType",
|
230
|
+
]
|
@@ -0,0 +1,69 @@
|
|
1
|
+
"""Analytics and system events and commands."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from typing import Any, Literal
|
6
|
+
|
7
|
+
from pydantic import ConfigDict, Field
|
8
|
+
|
9
|
+
from ..models import DemandProfile
|
10
|
+
from .base import BaseEvent, Command
|
11
|
+
|
12
|
+
|
13
|
+
class ForecastUpdated(BaseEvent):
|
14
|
+
"""Event emitted when a product forecast/demand profile is updated."""
|
15
|
+
|
16
|
+
event_type: Literal["forecast_updated"] = "forecast_updated"
|
17
|
+
|
18
|
+
product_id: str
|
19
|
+
demand_profile: DemandProfile
|
20
|
+
|
21
|
+
|
22
|
+
class AnomalyDetected(BaseEvent):
|
23
|
+
"""Generic anomaly detection event used by monitoring/analytics.
|
24
|
+
|
25
|
+
- summary: short text describing the anomaly type.
|
26
|
+
- metrics: optional structured metrics that explain the anomaly (small dict).
|
27
|
+
"""
|
28
|
+
|
29
|
+
event_type: Literal["anomaly_detected"] = "anomaly_detected"
|
30
|
+
|
31
|
+
summary: str
|
32
|
+
metrics: dict[str, Any] | None = None
|
33
|
+
severity: Literal["low", "medium", "high", "critical"] | None = "low"
|
34
|
+
|
35
|
+
|
36
|
+
class ReforecastDemandCommand(Command):
|
37
|
+
"""Request to recompute demand forecasts for a product over a timeframe."""
|
38
|
+
|
39
|
+
command_type: Literal["reforecast_demand"] = "reforecast_demand"
|
40
|
+
|
41
|
+
product_id: str
|
42
|
+
timeframe_days: int = Field(..., gt=0)
|
43
|
+
reason: str | None = None
|
44
|
+
|
45
|
+
|
46
|
+
class AdjustFulfillmentLatencyCommand(Command):
|
47
|
+
"""Command to set or adjust fulfillment latency targets (in days)."""
|
48
|
+
|
49
|
+
command_type: Literal["adjust_fulfillment_latency"] = "adjust_fulfillment_latency"
|
50
|
+
|
51
|
+
product_id: str
|
52
|
+
new_latency_days: int = Field(..., ge=0)
|
53
|
+
|
54
|
+
model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")
|
55
|
+
|
56
|
+
|
57
|
+
class NegotiateSupplyCommand(Command):
|
58
|
+
"""Command to negotiate supply terms with a supplier.
|
59
|
+
|
60
|
+
- negotiation_terms: structured terms being negotiated (e.g., price, lead_time).
|
61
|
+
"""
|
62
|
+
|
63
|
+
command_type: Literal["negotiate_supply"] = "negotiate_supply"
|
64
|
+
|
65
|
+
supplier_id: str
|
66
|
+
product_id: str
|
67
|
+
negotiation_terms: dict[str, Any]
|
68
|
+
|
69
|
+
model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")
|
@@ -0,0 +1,59 @@
|
|
1
|
+
"""Base classes for domain events and commands."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from datetime import datetime
|
6
|
+
from typing import Any
|
7
|
+
|
8
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
9
|
+
|
10
|
+
|
11
|
+
class BaseEvent(BaseModel):
|
12
|
+
"""Base contract for all domain events.
|
13
|
+
|
14
|
+
Attributes:
|
15
|
+
- event_type: Literal discriminator provided by subclasses.
|
16
|
+
- timestamp: UTC timestamp when the event was recorded (defaults to now).
|
17
|
+
- tick: non-negative simulation or system tick.
|
18
|
+
- correlation_id: optional id to trace this event across services and workflows.
|
19
|
+
"""
|
20
|
+
|
21
|
+
model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")
|
22
|
+
|
23
|
+
event_type: str
|
24
|
+
timestamp: datetime = Field(default_factory=datetime.utcnow)
|
25
|
+
tick: int = Field(0, ge=0)
|
26
|
+
correlation_id: str | None = None
|
27
|
+
|
28
|
+
|
29
|
+
class Command(BaseModel):
|
30
|
+
"""Base contract for commands (intent to change system state).
|
31
|
+
|
32
|
+
Commands are issued by agents or systems. Include optional metadata to
|
33
|
+
enable observability and intent tracing:
|
34
|
+
- issued_by: human or system identifier that created the command.
|
35
|
+
- reason: free-text explanation for auditing.
|
36
|
+
- correlation_id: align with events for traceability.
|
37
|
+
- metadata: structured map for small typed values (avoid open-ended blobs).
|
38
|
+
We deliberately use extra="forbid" at the model level to prevent accidental
|
39
|
+
arbitrary attributes; metadata is the supported extensibility point.
|
40
|
+
"""
|
41
|
+
|
42
|
+
model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")
|
43
|
+
|
44
|
+
command_type: str
|
45
|
+
issued_by: str | None = None
|
46
|
+
reason: str | None = None
|
47
|
+
correlation_id: str | None = None
|
48
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
49
|
+
|
50
|
+
@field_validator("metadata")
|
51
|
+
@classmethod
|
52
|
+
def _validate_metadata(cls, v: dict[str, Any]):
|
53
|
+
# Keep metadata shallow and keyed by str to encourage typed schemas.
|
54
|
+
if not isinstance(v, dict):
|
55
|
+
raise TypeError("metadata must be a dict[str, Any]")
|
56
|
+
for k in v.keys():
|
57
|
+
if not isinstance(k, str):
|
58
|
+
raise TypeError("metadata keys must be strings")
|
59
|
+
return v
|
@@ -0,0 +1,119 @@
|
|
1
|
+
"""Inventory and fulfillment-related events and commands."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from typing import Literal
|
6
|
+
|
7
|
+
from pydantic import Field, model_validator
|
8
|
+
|
9
|
+
from ..models import InventorySnapshot
|
10
|
+
from .base import BaseEvent, Command
|
11
|
+
|
12
|
+
|
13
|
+
class StockReplenished(BaseEvent):
|
14
|
+
"""Event emitted when stock is replenished at a warehouse.
|
15
|
+
|
16
|
+
Either provide `snapshot_before` and `snapshot_after` (InventorySnapshot) or
|
17
|
+
provide `quantity_added` and `warehouse_location`. `quantity_added` must be > 0.
|
18
|
+
"""
|
19
|
+
|
20
|
+
event_type: Literal["stock_replenished"] = "stock_replenished"
|
21
|
+
|
22
|
+
product_id: str
|
23
|
+
snapshot_before: InventorySnapshot | None = None
|
24
|
+
snapshot_after: InventorySnapshot | None = None
|
25
|
+
warehouse_location: str | None = None
|
26
|
+
quantity_added: int | None = Field(None, gt=0)
|
27
|
+
|
28
|
+
@model_validator(mode="after")
|
29
|
+
def _validate_snapshots_or_quantity(self):
|
30
|
+
if not (self.snapshot_before or self.snapshot_after or self.quantity_added):
|
31
|
+
raise ValueError(
|
32
|
+
"Provide snapshot_before/after or quantity_added to describe the replenishment"
|
33
|
+
)
|
34
|
+
return self
|
35
|
+
|
36
|
+
|
37
|
+
class StockDepleted(BaseEvent):
|
38
|
+
"""Event triggered when inventory reaches zero or falls below safety stock.
|
39
|
+
|
40
|
+
- safety_stock: optional configured safety stock level (int)
|
41
|
+
- current_snapshot: optional InventorySnapshot for reconciliation
|
42
|
+
"""
|
43
|
+
|
44
|
+
event_type: Literal["stock_depleted"] = "stock_depleted"
|
45
|
+
|
46
|
+
product_id: str
|
47
|
+
safety_stock: int | None = None
|
48
|
+
current_snapshot: InventorySnapshot | None = None
|
49
|
+
|
50
|
+
|
51
|
+
class FulfillmentDelayed(BaseEvent):
|
52
|
+
"""Event emitted when an order fulfillment is delayed beyond SLA."""
|
53
|
+
|
54
|
+
event_type: Literal["fulfillment_delayed"] = "fulfillment_delayed"
|
55
|
+
|
56
|
+
order_id: str
|
57
|
+
delay_hours: float = Field(..., ge=0.0)
|
58
|
+
reason: str | None = None
|
59
|
+
|
60
|
+
|
61
|
+
class SupplyDisruption(BaseEvent):
|
62
|
+
"""Event emitted when a supply chain disruption occurs (e.g., supplier delay, shortage).
|
63
|
+
|
64
|
+
- disruption_type: type of disruption (e.g., 'supplier_delay', 'material_shortage', 'logistics_issue')
|
65
|
+
- impact_description: optional details on the impact.
|
66
|
+
"""
|
67
|
+
|
68
|
+
event_type: Literal["supply_disruption"] = "supply_disruption"
|
69
|
+
|
70
|
+
product_id: str
|
71
|
+
supplier_id: str
|
72
|
+
disruption_type: str
|
73
|
+
impact_description: str | None = None
|
74
|
+
|
75
|
+
|
76
|
+
class PlaceReplenishmentOrderCommand(Command):
|
77
|
+
"""Command to place a replenishment order with a supplier."""
|
78
|
+
|
79
|
+
command_type: Literal["place_replenishment_order"] = "place_replenishment_order"
|
80
|
+
|
81
|
+
product_id: str
|
82
|
+
quantity: int = Field(..., gt=0)
|
83
|
+
supplier_id: str
|
84
|
+
target_warehouse: str | None = None
|
85
|
+
priority: Literal["low", "normal", "high", "urgent"] | None = "normal"
|
86
|
+
|
87
|
+
|
88
|
+
class TransferInventoryCommand(Command):
|
89
|
+
"""Command to transfer inventory between warehouses."""
|
90
|
+
|
91
|
+
command_type: Literal["transfer_inventory"] = "transfer_inventory"
|
92
|
+
|
93
|
+
product_id: str
|
94
|
+
from_warehouse: str
|
95
|
+
to_warehouse: str
|
96
|
+
quantity: int = Field(..., gt=0)
|
97
|
+
|
98
|
+
|
99
|
+
class UpdateSafetyStockCommand(Command):
|
100
|
+
"""Command to update safety stock thresholds for a product."""
|
101
|
+
|
102
|
+
command_type: Literal["update_safety_stock"] = "update_safety_stock"
|
103
|
+
|
104
|
+
product_id: str
|
105
|
+
new_safety_stock: int = Field(..., ge=0)
|
106
|
+
|
107
|
+
|
108
|
+
class AdjustInventoryCommand(Command):
|
109
|
+
"""Command to adjust inventory levels for a product (e.g., manual correction, write-off).
|
110
|
+
|
111
|
+
- adjustment_quantity: positive for increase, negative for decrease.
|
112
|
+
- warehouse_location: optional specific warehouse.
|
113
|
+
"""
|
114
|
+
|
115
|
+
command_type: Literal["adjust_inventory"] = "adjust_inventory"
|
116
|
+
|
117
|
+
product_id: str
|
118
|
+
adjustment_quantity: int
|
119
|
+
warehouse_location: str | None = None
|
@@ -0,0 +1,102 @@
|
|
1
|
+
"""Marketing and customer engagement-related events and commands."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from datetime import datetime
|
6
|
+
from decimal import Decimal
|
7
|
+
from typing import Any, Literal
|
8
|
+
|
9
|
+
from pydantic import Field, field_validator
|
10
|
+
|
11
|
+
from .base import BaseEvent, Command
|
12
|
+
|
13
|
+
|
14
|
+
class PromotionLaunched(BaseEvent):
|
15
|
+
"""Event signalling that a promotion has been launched for products or categories.
|
16
|
+
|
17
|
+
- discount_percent is expressed as Decimal fraction (0.0 - 1.0).
|
18
|
+
"""
|
19
|
+
|
20
|
+
event_type: Literal["promotion_launched"] = "promotion_launched"
|
21
|
+
|
22
|
+
promotion_id: str
|
23
|
+
product_ids: list[str] | None = None
|
24
|
+
category: str | None = None
|
25
|
+
discount_percent: Decimal = Field(..., ge=Decimal("0"), le=Decimal("1"))
|
26
|
+
start: datetime
|
27
|
+
end: datetime | None = None
|
28
|
+
channels: list[str] | None = None
|
29
|
+
|
30
|
+
@field_validator("discount_percent", mode="before")
|
31
|
+
@classmethod
|
32
|
+
def _coerce_discount(cls, v):
|
33
|
+
if isinstance(v, Decimal):
|
34
|
+
return v
|
35
|
+
try:
|
36
|
+
return Decimal(str(v))
|
37
|
+
except Exception as exc:
|
38
|
+
raise ValueError("Invalid discount_percent") from exc
|
39
|
+
|
40
|
+
|
41
|
+
class CustomerComplaintLogged(BaseEvent):
|
42
|
+
"""Event representing a logged customer complaint tied to an order."""
|
43
|
+
|
44
|
+
event_type: Literal["customer_complaint_logged"] = "customer_complaint_logged"
|
45
|
+
|
46
|
+
complaint_id: str
|
47
|
+
order_id: str | None = None
|
48
|
+
product_id: str | None = None
|
49
|
+
issue_type: str
|
50
|
+
details: str | None = None
|
51
|
+
resolution_deadline: datetime | None = None
|
52
|
+
|
53
|
+
|
54
|
+
class ResolveCustomerIssueCommand(Command):
|
55
|
+
"""Command for customer service agents to resolve a logged complaint.
|
56
|
+
|
57
|
+
- refund_amount uses Decimal for monetary values and must be >= 0.
|
58
|
+
"""
|
59
|
+
|
60
|
+
command_type: Literal["resolve_customer_issue"] = "resolve_customer_issue"
|
61
|
+
|
62
|
+
complaint_id: str | None = None
|
63
|
+
order_id: str | None = None
|
64
|
+
resolution_action: str
|
65
|
+
refund_amount: Decimal | None = Field(None, ge=Decimal("0"))
|
66
|
+
|
67
|
+
@field_validator("refund_amount", mode="before")
|
68
|
+
@classmethod
|
69
|
+
def _coerce_refund(cls, v):
|
70
|
+
if v is None:
|
71
|
+
return v
|
72
|
+
if isinstance(v, Decimal):
|
73
|
+
return v
|
74
|
+
try:
|
75
|
+
return Decimal(str(v))
|
76
|
+
except Exception as exc:
|
77
|
+
raise ValueError("Invalid refund_amount") from exc
|
78
|
+
|
79
|
+
|
80
|
+
class StartCustomerOutreachCommand(Command):
|
81
|
+
"""Command to start an outreach/campaign targeted at a customer segment."""
|
82
|
+
|
83
|
+
command_type: Literal["start_customer_outreach"] = "start_customer_outreach"
|
84
|
+
|
85
|
+
segment: str
|
86
|
+
message_template: str
|
87
|
+
goal_metrics: dict[str, Any] | None = None
|
88
|
+
channels: list[str] | None = None
|
89
|
+
|
90
|
+
|
91
|
+
class RespondToComplaintCommand(Command):
|
92
|
+
"""Command to respond to a customer complaint.
|
93
|
+
|
94
|
+
- response_action: type of response (e.g., 'apology', 'refund', 'replacement')
|
95
|
+
- response_message: optional detailed message to the customer.
|
96
|
+
"""
|
97
|
+
|
98
|
+
command_type: Literal["respond_to_complaint"] = "respond_to_complaint"
|
99
|
+
|
100
|
+
complaint_id: str
|
101
|
+
response_action: str
|
102
|
+
response_message: str | None = None
|