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.
Files changed (52) hide show
  1. fba_bench_core/__init__.py +11 -0
  2. fba_bench_core/agents/__init__.py +15 -0
  3. fba_bench_core/agents/base.py +83 -0
  4. fba_bench_core/agents/registry.py +16 -0
  5. fba_bench_core/benchmarking/__init__.py +6 -0
  6. fba_bench_core/benchmarking/core/__init__.py +1 -0
  7. fba_bench_core/benchmarking/engine/__init__.py +12 -0
  8. fba_bench_core/benchmarking/engine/core.py +135 -0
  9. fba_bench_core/benchmarking/engine/models.py +62 -0
  10. fba_bench_core/benchmarking/metrics/__init__.py +30 -0
  11. fba_bench_core/benchmarking/metrics/accuracy_score.py +27 -0
  12. fba_bench_core/benchmarking/metrics/aggregate.py +39 -0
  13. fba_bench_core/benchmarking/metrics/completeness.py +38 -0
  14. fba_bench_core/benchmarking/metrics/cost_efficiency.py +32 -0
  15. fba_bench_core/benchmarking/metrics/custom_scriptable.py +17 -0
  16. fba_bench_core/benchmarking/metrics/keyword_coverage.py +41 -0
  17. fba_bench_core/benchmarking/metrics/policy_compliance.py +18 -0
  18. fba_bench_core/benchmarking/metrics/registry.py +57 -0
  19. fba_bench_core/benchmarking/metrics/robustness.py +27 -0
  20. fba_bench_core/benchmarking/metrics/technical_performance.py +16 -0
  21. fba_bench_core/benchmarking/registry.py +48 -0
  22. fba_bench_core/benchmarking/scenarios/__init__.py +1 -0
  23. fba_bench_core/benchmarking/scenarios/base.py +36 -0
  24. fba_bench_core/benchmarking/scenarios/complex_marketplace.py +181 -0
  25. fba_bench_core/benchmarking/scenarios/multiturn_tool_use.py +176 -0
  26. fba_bench_core/benchmarking/scenarios/registry.py +18 -0
  27. fba_bench_core/benchmarking/scenarios/research_summarization.py +141 -0
  28. fba_bench_core/benchmarking/validators/__init__.py +24 -0
  29. fba_bench_core/benchmarking/validators/determinism_check.py +95 -0
  30. fba_bench_core/benchmarking/validators/fairness_balance.py +75 -0
  31. fba_bench_core/benchmarking/validators/outlier_detection.py +53 -0
  32. fba_bench_core/benchmarking/validators/registry.py +57 -0
  33. fba_bench_core/benchmarking/validators/reproducibility_metadata.py +74 -0
  34. fba_bench_core/benchmarking/validators/schema_adherence.py +59 -0
  35. fba_bench_core/benchmarking/validators/structural_consistency.py +74 -0
  36. fba_bench_core/config.py +154 -0
  37. fba_bench_core/domain/__init__.py +75 -0
  38. fba_bench_core/domain/events/__init__.py +230 -0
  39. fba_bench_core/domain/events/analytics.py +69 -0
  40. fba_bench_core/domain/events/base.py +59 -0
  41. fba_bench_core/domain/events/inventory.py +119 -0
  42. fba_bench_core/domain/events/marketing.py +102 -0
  43. fba_bench_core/domain/events/pricing.py +179 -0
  44. fba_bench_core/domain/models.py +296 -0
  45. fba_bench_core/exceptions/__init__.py +9 -0
  46. fba_bench_core/exceptions/base.py +46 -0
  47. fba_bench_core/services/__init__.py +12 -0
  48. fba_bench_core/services/base.py +52 -0
  49. fba_bench_core-1.0.0.dist-info/METADATA +152 -0
  50. fba_bench_core-1.0.0.dist-info/RECORD +52 -0
  51. fba_bench_core-1.0.0.dist-info/WHEEL +4 -0
  52. 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