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,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