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,179 @@
|
|
1
|
+
"""Pricing and demand-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 ConfigDict, Field, field_validator, model_validator
|
10
|
+
|
11
|
+
from ..models import CompetitorListing, DemandProfile
|
12
|
+
from .base import BaseEvent, Command
|
13
|
+
|
14
|
+
|
15
|
+
class SaleOccurred(BaseEvent):
|
16
|
+
"""Event emitted when a sale completes for a product.
|
17
|
+
|
18
|
+
Notes / semantics:
|
19
|
+
- `product_id` is the canonical identifier and is preferred; `product_sku` is
|
20
|
+
optional and kept for compatibility with legacy callers.
|
21
|
+
- `gross_margin` is represented as a Decimal fraction of revenue (e.g., 0.35
|
22
|
+
for 35%). Negative values are allowed (loss-making sale) but are bounded
|
23
|
+
by -1.0 for sanity.
|
24
|
+
"""
|
25
|
+
|
26
|
+
event_type: Literal["sale_occurred"] = "sale_occurred"
|
27
|
+
|
28
|
+
order_id: str
|
29
|
+
product_id: str | None = None
|
30
|
+
product_sku: str | None = None
|
31
|
+
quantity: int = Field(..., ge=1)
|
32
|
+
revenue: Decimal = Field(..., gt=Decimal("0"))
|
33
|
+
currency: str = Field("USD", min_length=3, description="ISO currency code")
|
34
|
+
channel: str | None = None
|
35
|
+
customer_segment: str | None = None
|
36
|
+
gross_margin: Decimal | None = Field(
|
37
|
+
None,
|
38
|
+
description="Gross margin expressed as Decimal fraction (0.0 == 0%, 1.0 == 100%)",
|
39
|
+
)
|
40
|
+
|
41
|
+
@field_validator("revenue", "gross_margin", mode="before")
|
42
|
+
@classmethod
|
43
|
+
def _coerce_decimal(cls, v):
|
44
|
+
if v is None:
|
45
|
+
return v
|
46
|
+
if isinstance(v, Decimal):
|
47
|
+
return v
|
48
|
+
try:
|
49
|
+
return Decimal(str(v))
|
50
|
+
except Exception as exc:
|
51
|
+
raise ValueError(f"Invalid monetary/ratio value: {v!r}") from exc
|
52
|
+
|
53
|
+
@model_validator(mode="after")
|
54
|
+
def _validate_gross_margin(self):
|
55
|
+
if self.gross_margin is not None:
|
56
|
+
if self.gross_margin < Decimal("-1") or self.gross_margin > Decimal("1"):
|
57
|
+
raise ValueError("gross_margin must be between -1 and 1 (fractional)")
|
58
|
+
return self
|
59
|
+
|
60
|
+
|
61
|
+
class PriceChangedExternally(BaseEvent):
|
62
|
+
"""Event representing an observed competitor price/listing change.
|
63
|
+
|
64
|
+
Embeds a CompetitorListing to provide structured comparables for repricing logic.
|
65
|
+
"""
|
66
|
+
|
67
|
+
event_type: Literal["price_changed_externally"] = "price_changed_externally"
|
68
|
+
competitor_id: str
|
69
|
+
listing: CompetitorListing
|
70
|
+
|
71
|
+
|
72
|
+
class DemandSpiked(BaseEvent):
|
73
|
+
"""Event signalling an abrupt increase in demand for a product.
|
74
|
+
|
75
|
+
- delta: positive increase in expected demand (units or percentage depending on trigger)
|
76
|
+
- trigger: short text describing why (e.g., 'seasonal', 'media_mention', 'stockout_competitor')
|
77
|
+
- optional demand_profile allows attaching a refreshed DemandProfile for downstream forecasting.
|
78
|
+
"""
|
79
|
+
|
80
|
+
event_type: Literal["demand_spiked"] = "demand_spiked"
|
81
|
+
|
82
|
+
product_id: str
|
83
|
+
delta: Decimal = Field(..., gt=Decimal("0"))
|
84
|
+
trigger: str
|
85
|
+
demand_profile: DemandProfile | None = None
|
86
|
+
|
87
|
+
@field_validator("delta", mode="before")
|
88
|
+
@classmethod
|
89
|
+
def _coerce_delta(cls, v):
|
90
|
+
if isinstance(v, Decimal):
|
91
|
+
return v
|
92
|
+
try:
|
93
|
+
return Decimal(str(v))
|
94
|
+
except Exception as exc:
|
95
|
+
raise ValueError("delta must be numeric") from exc
|
96
|
+
|
97
|
+
|
98
|
+
class CompetitorAction(BaseEvent):
|
99
|
+
"""Event signalling a competitor's strategic action (e.g., price change, promotion launch).
|
100
|
+
|
101
|
+
- action_type: categorized action (e.g., 'price_adjustment', 'promotion_launch', 'inventory_change')
|
102
|
+
- details: optional structured details about the action.
|
103
|
+
"""
|
104
|
+
|
105
|
+
event_type: Literal["competitor_action"] = "competitor_action"
|
106
|
+
|
107
|
+
competitor_id: str
|
108
|
+
action_type: str
|
109
|
+
details: dict[str, Any] | None = None
|
110
|
+
|
111
|
+
|
112
|
+
class AdjustPriceCommand(Command):
|
113
|
+
"""Command to change the price for a product.
|
114
|
+
|
115
|
+
Business rules:
|
116
|
+
- proposed_price uses Decimal for monetary precision and must be >= 0.
|
117
|
+
- `effective_from` indicates when price should take effect (None = immediate).
|
118
|
+
- `channel` is optional to target marketplace/channel-level prices.
|
119
|
+
"""
|
120
|
+
|
121
|
+
command_type: Literal["adjust_price"] = "adjust_price"
|
122
|
+
|
123
|
+
product_id: str | None = None
|
124
|
+
product_sku: str | None = None
|
125
|
+
proposed_price: Decimal = Field(..., ge=Decimal("0"))
|
126
|
+
effective_from: datetime | None = None
|
127
|
+
channel: str | None = None
|
128
|
+
|
129
|
+
@field_validator("proposed_price", mode="before")
|
130
|
+
@classmethod
|
131
|
+
def _coerce_price(cls, v):
|
132
|
+
if isinstance(v, Decimal):
|
133
|
+
return v
|
134
|
+
try:
|
135
|
+
return Decimal(str(v))
|
136
|
+
except Exception as exc:
|
137
|
+
raise ValueError("Invalid proposed_price") from exc
|
138
|
+
|
139
|
+
|
140
|
+
class LaunchPromotionCommand(Command):
|
141
|
+
"""Command instructing the system to launch a promotion.
|
142
|
+
|
143
|
+
- discount_percent is Decimal fraction between 0 and 1.
|
144
|
+
"""
|
145
|
+
|
146
|
+
command_type: Literal["launch_promotion"] = "launch_promotion"
|
147
|
+
|
148
|
+
promotion_id: str
|
149
|
+
product_ids: list[str] | None = None
|
150
|
+
category: str | None = None
|
151
|
+
discount_percent: Decimal = Field(..., ge=Decimal("0"), le=Decimal("1"))
|
152
|
+
start: datetime
|
153
|
+
end: datetime | None = None
|
154
|
+
channels: list[str] | None = None
|
155
|
+
notes: str | None = None
|
156
|
+
|
157
|
+
@field_validator("discount_percent", mode="before")
|
158
|
+
@classmethod
|
159
|
+
def _coerce_discount(cls, v):
|
160
|
+
if isinstance(v, Decimal):
|
161
|
+
return v
|
162
|
+
try:
|
163
|
+
return Decimal(str(v))
|
164
|
+
except Exception as exc:
|
165
|
+
raise ValueError("Invalid discount_percent") from exc
|
166
|
+
|
167
|
+
|
168
|
+
class MonitorCompetitorCommand(Command):
|
169
|
+
"""Command to initiate monitoring of a competitor's activities.
|
170
|
+
|
171
|
+
- monitoring_focus: areas to monitor (e.g., 'pricing', 'inventory', 'promotions').
|
172
|
+
"""
|
173
|
+
|
174
|
+
command_type: Literal["monitor_competitor"] = "monitor_competitor"
|
175
|
+
|
176
|
+
competitor_id: str
|
177
|
+
monitoring_focus: list[str]
|
178
|
+
|
179
|
+
model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")
|
@@ -0,0 +1,296 @@
|
|
1
|
+
"""
|
2
|
+
Domain models for FBA-Bench core domain layer (Phase B).
|
3
|
+
|
4
|
+
These models encode explicit business semantics used throughout the
|
5
|
+
simulation core: monetary precision, inventory invariants, marketplace
|
6
|
+
competitor listings, and demand profiles.
|
7
|
+
|
8
|
+
Design notes (high level):
|
9
|
+
- Monetary values use Decimal to avoid floating-point rounding issues in
|
10
|
+
profitability calculations.
|
11
|
+
- The Product model enforces core invariants (non-negative stock, price >= cost).
|
12
|
+
Exceptions (e.g., temporary loss-leaders) should be modeled by the simulation
|
13
|
+
or application layers, not by the core contract.
|
14
|
+
- InventorySnapshot captures the momentary state of inventory for reconciliation,
|
15
|
+
forecasting, and fulfillment logic.
|
16
|
+
"""
|
17
|
+
|
18
|
+
from __future__ import annotations
|
19
|
+
|
20
|
+
from datetime import datetime
|
21
|
+
from decimal import Decimal
|
22
|
+
|
23
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
24
|
+
|
25
|
+
# Helper constants for documentation / defaults
|
26
|
+
DEFAULT_DECIMAL_PLACES = 2
|
27
|
+
|
28
|
+
|
29
|
+
class Product(BaseModel):
|
30
|
+
"""
|
31
|
+
Product represents the fundamental trade unit in the simulation.
|
32
|
+
|
33
|
+
Business invariants and rationale:
|
34
|
+
- price and cost are Decimal to preserve financial precision.
|
35
|
+
- cost must be > 0 to ensure positive landed costs.
|
36
|
+
- price must be > 0 and >= cost at the domain level to avoid modeling systemic losses.
|
37
|
+
Allowing price == cost supports break-even scenarios; intentional loss-leading
|
38
|
+
promotions (price < cost) are an application/simulation-level decision and
|
39
|
+
should be represented via promotions/events rather than a contract-level rule.
|
40
|
+
- stock is an available on-hand integer and must be >= 0.
|
41
|
+
- max_inventory is optional; when defined it represents a physical or policy
|
42
|
+
ceiling for stockage (used by replenishment logic).
|
43
|
+
- fulfillment_latency (in days) expresses expected time-to-ship from the seller.
|
44
|
+
"""
|
45
|
+
|
46
|
+
# Identifiers and classification
|
47
|
+
product_id: str = Field(..., description="Canonical product identifier (internal).")
|
48
|
+
sku: str | None = Field(
|
49
|
+
None, description="Stock keeping unit; may match marketplace SKU."
|
50
|
+
)
|
51
|
+
name: str | None = Field(None, description="Human-readable product title.")
|
52
|
+
category: str | None = Field(
|
53
|
+
None, description="Category or taxonomy for aggregation/segmentation."
|
54
|
+
)
|
55
|
+
|
56
|
+
# Monetary fields (Decimal for precision)
|
57
|
+
cost: Decimal = Field(
|
58
|
+
..., description="Per-unit landed cost (Decimal). Must be > 0."
|
59
|
+
)
|
60
|
+
price: Decimal = Field(
|
61
|
+
..., description="Per-unit listing price (Decimal). Must be > 0 and >= cost."
|
62
|
+
)
|
63
|
+
|
64
|
+
# Inventory and fulfillment
|
65
|
+
stock: int = Field(
|
66
|
+
0, description="Available on-hand units (must be non-negative integer)."
|
67
|
+
)
|
68
|
+
max_inventory: int | None = Field(
|
69
|
+
None,
|
70
|
+
description="Optional maximum stock allowed; if set, stock must not exceed this ceiling.",
|
71
|
+
)
|
72
|
+
fulfillment_latency: int | None = Field(
|
73
|
+
None,
|
74
|
+
description="Expected fulfillment latency in days (integer). Used by fulfillment & SLAs.",
|
75
|
+
ge=0,
|
76
|
+
)
|
77
|
+
|
78
|
+
# Pydantic model configuration: validate assignments so runtime updates still enforce invariants.
|
79
|
+
model_config = ConfigDict(validate_assignment=True, frozen=True, extra="forbid")
|
80
|
+
|
81
|
+
# Field-level validators
|
82
|
+
@field_validator("cost", "price", mode="before")
|
83
|
+
@classmethod
|
84
|
+
def _coerce_decimal(cls, v):
|
85
|
+
"""
|
86
|
+
Ensure numeric inputs are converted to Decimal in a robust manner.
|
87
|
+
Accept str/int/float but convert cautiously; floats may lose precision so
|
88
|
+
passing strings or Decimal is preferred.
|
89
|
+
"""
|
90
|
+
if isinstance(v, Decimal):
|
91
|
+
return v
|
92
|
+
if v is None:
|
93
|
+
raise ValueError("Monetary fields cannot be None")
|
94
|
+
try:
|
95
|
+
return Decimal(str(v))
|
96
|
+
except Exception as exc:
|
97
|
+
raise ValueError(f"Invalid monetary value: {v!r}") from exc
|
98
|
+
|
99
|
+
@field_validator("cost", "price")
|
100
|
+
@classmethod
|
101
|
+
def _positive_money(cls, v: Decimal):
|
102
|
+
if v <= Decimal("0"):
|
103
|
+
raise ValueError("Monetary values must be positive")
|
104
|
+
return v
|
105
|
+
|
106
|
+
@field_validator("stock", "max_inventory", mode="before")
|
107
|
+
@classmethod
|
108
|
+
def _coerce_ints(cls, v):
|
109
|
+
"""Coerce numeric stock inputs to int and validate sign when possible."""
|
110
|
+
if v is None:
|
111
|
+
return v
|
112
|
+
try:
|
113
|
+
return int(v)
|
114
|
+
except Exception as exc:
|
115
|
+
raise ValueError("Stock and inventory limits must be integers") from exc
|
116
|
+
|
117
|
+
@field_validator("stock")
|
118
|
+
@classmethod
|
119
|
+
def _stock_non_negative(cls, v: int):
|
120
|
+
if v < 0:
|
121
|
+
raise ValueError("stock must be >= 0")
|
122
|
+
return v
|
123
|
+
|
124
|
+
@model_validator(mode="after")
|
125
|
+
def _price_vs_cost_and_inventory_limits(self):
|
126
|
+
# price >= cost invariant
|
127
|
+
if self.price < self.cost:
|
128
|
+
# Domain-level rule: do not permit systemic losses at the model layer.
|
129
|
+
# Applications that want to model discounts or strategic loss-leading
|
130
|
+
# should apply those as transient events or promotion objects.
|
131
|
+
raise ValueError("price must be greater than or equal to cost")
|
132
|
+
|
133
|
+
# If max_inventory is specified, ensure stock is within [0, max_inventory]
|
134
|
+
if self.max_inventory is not None:
|
135
|
+
if self.max_inventory < 0:
|
136
|
+
raise ValueError("max_inventory must be >= 0")
|
137
|
+
if self.stock > self.max_inventory:
|
138
|
+
raise ValueError("stock must not exceed max_inventory")
|
139
|
+
|
140
|
+
return self
|
141
|
+
|
142
|
+
|
143
|
+
class InventorySnapshot(BaseModel):
|
144
|
+
"""
|
145
|
+
Snapshot of inventory for a particular product at a specific time and location.
|
146
|
+
|
147
|
+
Purpose:
|
148
|
+
- Provide a normalized, auditable representation of available and reserved units
|
149
|
+
used by forecasting, fulfillment routing, and reconciliation.
|
150
|
+
"""
|
151
|
+
|
152
|
+
product_id: str = Field(
|
153
|
+
..., description="Canonical product identifier this snapshot refers to."
|
154
|
+
)
|
155
|
+
available_units: int = Field(
|
156
|
+
..., ge=0, description="Units available for sale/fulfillment."
|
157
|
+
)
|
158
|
+
reserved_units: int = Field(
|
159
|
+
0, ge=0, description="Units reserved for pending orders."
|
160
|
+
)
|
161
|
+
warehouse_location: str | None = Field(
|
162
|
+
None,
|
163
|
+
description="Identifier for warehouse/fulfillment center (optional).",
|
164
|
+
)
|
165
|
+
timestamp: datetime = Field(
|
166
|
+
default_factory=datetime.utcnow,
|
167
|
+
description="UTC timestamp of snapshot capture.",
|
168
|
+
)
|
169
|
+
|
170
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
171
|
+
|
172
|
+
@model_validator(mode="after")
|
173
|
+
def _validate_reserved_vs_available(self):
|
174
|
+
if self.reserved_units > (self.available_units + self.reserved_units):
|
175
|
+
# Defensive; reserved cannot exceed total physical units; this simple
|
176
|
+
# check relies on the snapshot representing only on-hand + reserved.
|
177
|
+
raise ValueError("reserved_units cannot exceed the total units represented")
|
178
|
+
if self.available_units < 0 or self.reserved_units < 0:
|
179
|
+
raise ValueError("available_units and reserved_units must be non-negative")
|
180
|
+
return self
|
181
|
+
|
182
|
+
|
183
|
+
class CompetitorListing(BaseModel):
|
184
|
+
"""
|
185
|
+
A single competitor listing describing a comparable SKU offered by a competitor.
|
186
|
+
|
187
|
+
Rationale:
|
188
|
+
- Simulations compare our product's price & fulfillment against competitor listings.
|
189
|
+
- Fulfillment latency and marketplace are important signals for win-rate models.
|
190
|
+
"""
|
191
|
+
|
192
|
+
sku: str | None = Field(None, description="Competitor SKU, if available.")
|
193
|
+
price: Decimal = Field(..., description="Competitor listing price (Decimal).")
|
194
|
+
rating: float | None = Field(
|
195
|
+
None, ge=0.0, le=5.0, description="Optional customer rating (0-5)."
|
196
|
+
)
|
197
|
+
fulfillment_latency: int | None = Field(
|
198
|
+
None, ge=0, description="Fulfillment latency in days."
|
199
|
+
)
|
200
|
+
marketplace: str | None = Field(
|
201
|
+
None, description="Marketplace or channel name (e.g., 'amazon.com')."
|
202
|
+
)
|
203
|
+
|
204
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
205
|
+
|
206
|
+
@field_validator("price", mode="before")
|
207
|
+
@classmethod
|
208
|
+
def _coerce_price(cls, v):
|
209
|
+
if isinstance(v, Decimal):
|
210
|
+
return v
|
211
|
+
try:
|
212
|
+
return Decimal(str(v))
|
213
|
+
except Exception as exc:
|
214
|
+
raise ValueError("Invalid price for CompetitorListing") from exc
|
215
|
+
|
216
|
+
@model_validator(mode="after")
|
217
|
+
def _price_non_negative(self):
|
218
|
+
if self.price < Decimal("0"):
|
219
|
+
raise ValueError("Competitor listing price must be non-negative")
|
220
|
+
return self
|
221
|
+
|
222
|
+
|
223
|
+
class Competitor(BaseModel):
|
224
|
+
"""
|
225
|
+
Represents a competing seller in the marketplace.
|
226
|
+
|
227
|
+
- listings: typed list of CompetitorListing rather than raw Product copies. This
|
228
|
+
prevents accidental reuse of our Product model for external listings and keeps
|
229
|
+
competitor metadata explicit.
|
230
|
+
- operating_regions and primary_marketplace are optional metadata used by
|
231
|
+
marketplace segmentation logic.
|
232
|
+
"""
|
233
|
+
|
234
|
+
competitor_id: str = Field(..., description="Unique identifier for the competitor.")
|
235
|
+
name: str | None = Field(None, description="Display name for the competitor.")
|
236
|
+
listings: list[CompetitorListing] = Field(
|
237
|
+
default_factory=list, description="Listings offered by the competitor."
|
238
|
+
)
|
239
|
+
operating_regions: list[str] | None = Field(
|
240
|
+
None,
|
241
|
+
description="ISO region codes or region identifiers where the competitor operates.",
|
242
|
+
)
|
243
|
+
primary_marketplace: str | None = Field(
|
244
|
+
None, description="Primary marketplace/channel name."
|
245
|
+
)
|
246
|
+
|
247
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
248
|
+
|
249
|
+
@model_validator(mode="after")
|
250
|
+
def _unique_listing_skus(self):
|
251
|
+
skus = [listing.sku for listing in self.listings if listing.sku is not None]
|
252
|
+
if len(skus) != len(set(skus)):
|
253
|
+
raise ValueError("Competitor listings must have unique SKUs when provided")
|
254
|
+
return self
|
255
|
+
|
256
|
+
|
257
|
+
class DemandProfile(BaseModel):
|
258
|
+
"""
|
259
|
+
Simplified demand profile / customer segment demand model.
|
260
|
+
|
261
|
+
Purpose:
|
262
|
+
- Provide a compact representation of stochastic demand assumptions used by
|
263
|
+
assortment and replenishment simulations. Fields are intentionally minimal;
|
264
|
+
complex demand models belong in specialized modules.
|
265
|
+
"""
|
266
|
+
|
267
|
+
product_id: str = Field(..., description="Product this demand profile pertains to.")
|
268
|
+
daily_demand_mean: float = Field(
|
269
|
+
..., ge=0.0, description="Mean expected daily demand (units/day)."
|
270
|
+
)
|
271
|
+
daily_demand_std: float = Field(
|
272
|
+
0.0, ge=0.0, description="Standard deviation for daily demand."
|
273
|
+
)
|
274
|
+
|
275
|
+
segment: str | None = Field(
|
276
|
+
None,
|
277
|
+
description="Optional customer segment identifier (e.g., 'value', 'business').",
|
278
|
+
)
|
279
|
+
|
280
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
281
|
+
|
282
|
+
@model_validator(mode="after")
|
283
|
+
def _validate_stats(self):
|
284
|
+
if self.daily_demand_std < 0:
|
285
|
+
raise ValueError("daily_demand_std must be non-negative")
|
286
|
+
return self
|
287
|
+
|
288
|
+
|
289
|
+
# Export list for explicit imports; keep stable names for external modules
|
290
|
+
__all__ = [
|
291
|
+
"Product",
|
292
|
+
"InventorySnapshot",
|
293
|
+
"CompetitorListing",
|
294
|
+
"Competitor",
|
295
|
+
"DemandProfile",
|
296
|
+
]
|
@@ -0,0 +1,9 @@
|
|
1
|
+
"""Public exports for fba_bench_core.exceptions.
|
2
|
+
|
3
|
+
This package re-exports the base exception types to provide a small,
|
4
|
+
stable public API for consumers.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from .base import AgentError, ConfigurationError, FBABenchException
|
8
|
+
|
9
|
+
__all__ = ["FBABenchException", "ConfigurationError", "AgentError"]
|
@@ -0,0 +1,46 @@
|
|
1
|
+
"""Base exceptions for fba_bench_core.
|
2
|
+
|
3
|
+
This module defines the foundational exception hierarchy used across
|
4
|
+
the fba_bench_core package. It provides a single root exception and
|
5
|
+
a couple of commonly-used subclasses to improve error handling and
|
6
|
+
exception semantics throughout the project.
|
7
|
+
|
8
|
+
Classes:
|
9
|
+
- FBABenchException: Root exception for all fba_bench_core errors.
|
10
|
+
- ConfigurationError: Raised for configuration-related problems.
|
11
|
+
- AgentError: Raised for errors during agent execution or lifecycle.
|
12
|
+
"""
|
13
|
+
|
14
|
+
|
15
|
+
class FBABenchException(Exception):
|
16
|
+
"""Base exception for fba_bench_core.
|
17
|
+
|
18
|
+
Acts as the common ancestor for all library-specific exceptions so
|
19
|
+
callers can catch library errors without accidentally catching other
|
20
|
+
exceptions that inherit directly from Exception.
|
21
|
+
"""
|
22
|
+
|
23
|
+
pass
|
24
|
+
|
25
|
+
|
26
|
+
class ConfigurationError(FBABenchException):
|
27
|
+
"""Raised when there is an issue with configuration or setup.
|
28
|
+
|
29
|
+
Examples include invalid config values, missing required settings, or
|
30
|
+
failure to parse configuration files.
|
31
|
+
"""
|
32
|
+
|
33
|
+
pass
|
34
|
+
|
35
|
+
|
36
|
+
class AgentError(FBABenchException):
|
37
|
+
"""Raised for agent execution or lifecycle failures.
|
38
|
+
|
39
|
+
Use this when an agent encounters an unrecoverable error during
|
40
|
+
planning, execution, or coordination.
|
41
|
+
"""
|
42
|
+
|
43
|
+
pass
|
44
|
+
|
45
|
+
|
46
|
+
__all__ = ["FBABenchException", "ConfigurationError", "AgentError"]
|
@@ -0,0 +1,12 @@
|
|
1
|
+
"""Exports for fba_bench_core.services package.
|
2
|
+
|
3
|
+
Expose the BaseService class and its typed configuration model.
|
4
|
+
"""
|
5
|
+
|
6
|
+
from __future__ import annotations
|
7
|
+
|
8
|
+
from fba_bench_core.config import BaseServiceConfig
|
9
|
+
|
10
|
+
from .base import BaseService
|
11
|
+
|
12
|
+
__all__ = ["BaseService", "BaseServiceConfig"]
|
@@ -0,0 +1,52 @@
|
|
1
|
+
"""Typed BaseService for fba_bench_core (Phase D).
|
2
|
+
|
3
|
+
This module introduces a typed configuration contract for services and a
|
4
|
+
minimal abstract base class that requires a validated configuration object.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from __future__ import annotations
|
8
|
+
|
9
|
+
from abc import ABC, abstractmethod
|
10
|
+
|
11
|
+
from fba_bench_core.config import BaseServiceConfig
|
12
|
+
|
13
|
+
|
14
|
+
class BaseService(ABC):
|
15
|
+
"""Abstract base class for services that receive typed configuration.
|
16
|
+
|
17
|
+
Rationale:
|
18
|
+
As with agents, services should accept explicit, validated configuration
|
19
|
+
objects rather than arbitrary `**kwargs`. This improves discoverability,
|
20
|
+
maintainability, and prevents accidental acceptance of untyped values.
|
21
|
+
|
22
|
+
Initialization:
|
23
|
+
Construct with a `BaseServiceConfig` (or subclass) instance. Access the
|
24
|
+
service id via the `service_id` property and the full configuration via
|
25
|
+
the `config` property.
|
26
|
+
"""
|
27
|
+
|
28
|
+
def __init__(self, config: BaseServiceConfig) -> None:
|
29
|
+
"""Initialize the service with a typed configuration model."""
|
30
|
+
self._config = config
|
31
|
+
self._service_id = config.service_id
|
32
|
+
|
33
|
+
@property
|
34
|
+
def service_id(self) -> str:
|
35
|
+
"""Return the service's unique identifier (from config.service_id)."""
|
36
|
+
return self._service_id
|
37
|
+
|
38
|
+
@property
|
39
|
+
def config(self) -> BaseServiceConfig:
|
40
|
+
"""Return the typed configuration object for this service."""
|
41
|
+
return self._config
|
42
|
+
|
43
|
+
def get_config(self) -> dict:
|
44
|
+
"""Return a serializable shallow mapping of the configuration."""
|
45
|
+
return self._config.model_dump()
|
46
|
+
|
47
|
+
@abstractmethod
|
48
|
+
def start(self) -> None:
|
49
|
+
"""Start the service (synchronous API). Implementations should provide
|
50
|
+
the concrete startup behavior. Use async variants in concrete classes
|
51
|
+
if necessary."""
|
52
|
+
raise NotImplementedError
|