Skip to content

Commit b2f3dc6

Browse files
committed
feat!: enforce single-path completeness operations
1 parent a478dd0 commit b2f3dc6

11 files changed

Lines changed: 122 additions & 149 deletions

File tree

CHANGELOG.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [2.0.0] - 2026-03-04
11+
12+
> **Release**: `v2.0.0`
13+
> **Theme**: Single-path completeness operations (breaking compatibility removal)
14+
15+
### Changed
16+
17+
- Product consistency validation service now uses a **single canonical operation path**:
18+
- schema-driven completeness evaluation only
19+
- no legacy 4-field validator path
20+
- no legacy `product-events` compatibility subscription
21+
22+
### Removed
23+
24+
- Legacy compatibility components removed from `product-management-consistency-validation`:
25+
- legacy validator logic (`missing_name`, `negative_price`, `missing_currency`, `missing_image`)
26+
- legacy event processing module for `product-events`
27+
- legacy MCP tools:
28+
- `/product/consistency/check`
29+
- `/product/consistency/product`
30+
31+
### Added
32+
33+
- Canonical MCP operation for completeness:
34+
- `/product/completeness/evaluate`
35+
36+
### Migration Notes
37+
38+
- Any integration relying on the removed consistency MCP tools must switch to `/product/completeness/evaluate`.
39+
- Event-driven completeness processing now requires publishing to `completeness-jobs` (consumer group `completeness-engine`).
40+
- This release intentionally removes backward compatibility to guarantee a single operation model.
41+
1042
## [1.1.0] - 2026-03-03
1143

1244
> **Release**: [v1.1.0](https://github.com/Azure-Samples/holiday-peak-hub/releases/tag/v1.1.0)

apps/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ Every agent service follows an identical pattern:
6767
|-----|-------------|
6868
| [product-management-acp-transformation](product-management-acp-transformation/) | Transforms catalog products into ACP-compliant (Agentic Commerce Protocol) payloads, ensuring all required fields are populated and flagging missing data for remediation. Subscribes to `product-events`. |
6969
| [product-management-assortment-optimization](product-management-assortment-optimization/) | Ranks products by performance indicators (rating, price, demand) and recommends optimal assortment sets of a target size, explaining keep/drop trade-offs. Subscribes to `order-events` and `product-events`. |
70-
| [product-management-consistency-validation](product-management-consistency-validation/) | Validates catalog product consistency — checks field completeness, detects data quality issues, and provides remediation steps for invalid items. Subscribes to `product-events`. |
70+
| [product-management-consistency-validation](product-management-consistency-validation/) | Evaluates schema-driven product completeness with weighted scoring, gap reports, and enrichment triggers. Uses a single completeness operation model across `/invoke`, MCP, and events. Subscribes to `completeness-jobs`. |
7171
| [product-management-normalization-classification](product-management-normalization-classification/) | Normalizes product names, categories, and tags into a canonical taxonomy, assigns classifications, and highlights missing attributes that need enrichment. Subscribes to `product-events`. |
7272

7373
## Infrastructure

apps/product-management-consistency-validation/src/product_management_consistency_validation/adapters.py

Lines changed: 3 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
from holiday_peak_lib.adapters.mock_adapters import MockProductAdapter
99
from holiday_peak_lib.adapters.product_adapter import ProductConnector
10-
from holiday_peak_lib.schemas.product import CatalogProduct
1110

1211
from .completeness_engine import CategorySchema, GapReport
1312

@@ -71,42 +70,18 @@ async def store_gap_report(self, report: GapReport) -> None:
7170

7271
@dataclass
7372
class ProductConsistencyAdapters:
74-
"""Container for product consistency validation adapters."""
73+
"""Container for completeness evaluation adapters."""
7574

7675
products: ProductConnector
77-
validator: "ProductConsistencyValidator"
7876
completeness: CompletenessStorageAdapter = field(default_factory=CompletenessStorageAdapter)
7977

8078

81-
class ProductConsistencyValidator:
82-
"""Validate product data for completeness and consistency."""
83-
84-
async def validate(self, product: CatalogProduct) -> dict[str, Any]:
85-
issues = []
86-
if not product.name:
87-
issues.append("missing_name")
88-
if product.price is not None and product.price < 0:
89-
issues.append("negative_price")
90-
if product.price is not None and not product.currency:
91-
issues.append("missing_currency")
92-
if not product.image_url:
93-
issues.append("missing_image")
94-
return {
95-
"sku": product.sku,
96-
"issues": issues,
97-
"status": "invalid" if issues else "valid",
98-
}
99-
100-
10179
def build_consistency_adapters(
10280
*,
10381
product_connector: Optional[ProductConnector] = None,
10482
completeness_adapter: Optional[CompletenessStorageAdapter] = None,
10583
) -> ProductConsistencyAdapters:
106-
"""Create adapters for product consistency validation workflows."""
84+
"""Create adapters for schema-driven completeness workflows."""
10785
products = product_connector or ProductConnector(adapter=MockProductAdapter())
108-
validator = ProductConsistencyValidator()
10986
completeness = completeness_adapter or CompletenessStorageAdapter()
110-
return ProductConsistencyAdapters(
111-
products=products, validator=validator, completeness=completeness
112-
)
87+
return ProductConsistencyAdapters(products=products, completeness=completeness)

apps/product-management-consistency-validation/src/product_management_consistency_validation/agents.py

Lines changed: 73 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,83 +6,119 @@
66
from typing import Any
77

88
from holiday_peak_lib.adapters import BaseCRUDAdapter
9-
from holiday_peak_lib.adapters.acp_mapper import AcpCatalogMapper
109
from holiday_peak_lib.agents import BaseRetailAgent
1110
from holiday_peak_lib.agents.fastapi_mcp import FastAPIMCPServer
1211

1312
from .adapters import ProductConsistencyAdapters, build_consistency_adapters
13+
from .completeness_engine import CompletenessEngine
1414

1515

1616
class ProductConsistencyAgent(BaseRetailAgent):
17-
"""Agent that validates catalog product consistency."""
17+
"""Agent that evaluates products with the schema-driven completeness engine."""
1818

1919
def __init__(self, config, *args: Any, **kwargs: Any) -> None:
2020
super().__init__(config, *args, **kwargs)
2121
self._adapters = build_consistency_adapters()
22+
self._engine = CompletenessEngine()
2223

2324
@property
2425
def adapters(self) -> ProductConsistencyAdapters:
2526
return self._adapters
2627

28+
async def evaluate_completeness(
29+
self, sku: str, category_id: str | None = None
30+
) -> dict[str, Any]:
31+
product = await self.adapters.products.get_product(str(sku))
32+
if not product:
33+
return {"error": "sku not found", "sku": sku}
34+
35+
resolved_category = category_id or product.category or "default"
36+
schema = await self.adapters.completeness.get_schema(resolved_category)
37+
if schema is None:
38+
return {
39+
"error": "schema not found",
40+
"sku": sku,
41+
"category_id": resolved_category,
42+
}
43+
44+
report = self._engine.evaluate(str(sku), product.model_dump(), schema)
45+
await self.adapters.completeness.store_gap_report(report)
46+
47+
return {
48+
"sku": sku,
49+
"category_id": resolved_category,
50+
"completeness": report.model_dump(mode="json"),
51+
"needs_enrichment": bool(report.enrichable_gaps),
52+
}
53+
2754
async def handle(self, request: dict[str, Any]) -> dict[str, Any]:
2855
sku = request.get("sku")
2956
if not sku:
3057
return {"error": "sku is required"}
31-
product = await self.adapters.products.get_product(str(sku))
32-
if not product:
33-
return {"error": "sku not found", "sku": sku}
3458

35-
validation = await self.adapters.validator.validate(product)
36-
acp_product = AcpCatalogMapper().to_acp_product(product, availability="unknown")
59+
result = await self.evaluate_completeness(
60+
sku=str(sku), category_id=request.get("category_id")
61+
)
62+
63+
if "error" in result:
64+
return result
3765

3866
if self.slm or self.llm:
3967
messages = [
4068
{"role": "system", "content": _consistency_instructions()},
4169
{
4270
"role": "user",
43-
"content": {
44-
"sku": sku,
45-
"product": product.model_dump(),
46-
"acp_product": acp_product,
47-
"validation": validation,
48-
},
71+
"content": result,
4972
},
5073
]
5174
return await self.invoke_model(request=request, messages=messages)
5275

53-
return {
54-
"service": self.service_name,
55-
"sku": sku,
56-
"product": product.model_dump(),
57-
"acp_product": acp_product,
58-
"validation": validation,
59-
}
76+
return {"service": self.service_name, **result}
6077

6178

6279
def register_mcp_tools(mcp: FastAPIMCPServer, agent: BaseRetailAgent) -> None:
63-
"""Expose MCP tools for product consistency validation workflows."""
64-
adapters = getattr(agent, "adapters", build_consistency_adapters())
80+
"""Expose MCP tools for schema-driven completeness workflows."""
81+
completeness_agent = agent if isinstance(agent, ProductConsistencyAgent) else None
82+
adapters = (
83+
completeness_agent.adapters
84+
if completeness_agent is not None
85+
else build_consistency_adapters()
86+
)
87+
engine = CompletenessEngine()
6588

66-
async def validate_product(payload: dict[str, Any]) -> dict[str, Any]:
89+
async def evaluate_product_completeness(payload: dict[str, Any]) -> dict[str, Any]:
6790
sku = payload.get("sku")
6891
if not sku:
6992
return {"error": "sku is required"}
93+
category_id = payload.get("category_id")
94+
if completeness_agent is not None:
95+
return await completeness_agent.evaluate_completeness(
96+
sku=str(sku), category_id=category_id
97+
)
98+
7099
product = await adapters.products.get_product(str(sku))
71100
if not product:
72101
return {"error": "sku not found", "sku": sku}
73-
validation = await adapters.validator.validate(product)
74-
acp_product = AcpCatalogMapper().to_acp_product(product, availability="unknown")
75-
return {"validation": validation, "acp_product": acp_product}
76102

77-
async def get_product(payload: dict[str, Any]) -> dict[str, Any]:
78-
sku = payload.get("sku")
79-
if not sku:
80-
return {"error": "sku is required"}
81-
product = await adapters.products.get_product(str(sku))
82-
return {"product": product.model_dump() if product else None}
103+
resolved_category = category_id or product.category or "default"
104+
schema = await adapters.completeness.get_schema(resolved_category)
105+
if schema is None:
106+
return {
107+
"error": "schema not found",
108+
"sku": sku,
109+
"category_id": resolved_category,
110+
}
111+
112+
report = engine.evaluate(str(sku), product.model_dump(), schema)
113+
await adapters.completeness.store_gap_report(report)
114+
return {
115+
"sku": str(sku),
116+
"category_id": resolved_category,
117+
"completeness": report.model_dump(mode="json"),
118+
"needs_enrichment": bool(report.enrichable_gaps),
119+
}
83120

84-
mcp.add_tool("/product/consistency/check", validate_product)
85-
mcp.add_tool("/product/consistency/product", get_product)
121+
mcp.add_tool("/product/completeness/evaluate", evaluate_product_completeness)
86122
_register_crud_tools(mcp)
87123

88124

@@ -95,7 +131,7 @@ def _register_crud_tools(mcp: FastAPIMCPServer) -> None:
95131

96132
def _consistency_instructions() -> str:
97133
return (
98-
"You are a product consistency validation agent. "
99-
"Check catalog completeness and highlight missing fields. "
100-
"Provide remediation steps for invalid items."
134+
"You are a schema-driven completeness engine agent. "
135+
"Evaluate products against category schemas and return weighted completeness scores. "
136+
"Highlight missing or invalid fields and enrichment candidates."
101137
)

apps/product-management-consistency-validation/src/product_management_consistency_validation/event_handlers.py

Lines changed: 0 additions & 44 deletions
This file was deleted.

apps/product-management-consistency-validation/src/product_management_consistency_validation/main.py

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,6 @@
1414
from product_management_consistency_validation.event_consumer import (
1515
build_completeness_event_handlers,
1616
)
17-
from product_management_consistency_validation.event_handlers import (
18-
build_event_handlers,
19-
)
2017

2118
SERVICE_NAME = "product-management-consistency-validation"
2219
memory_settings = MemorySettings()
@@ -52,13 +49,9 @@
5249
else None
5350
)
5451

55-
# Merge product-events handlers (backward compat) with completeness-jobs handlers
56-
_all_handlers = {
57-
**build_event_handlers(),
58-
**build_completeness_event_handlers(
59-
completeness_threshold=float(os.getenv("COMPLETENESS_THRESHOLD", "0.7"))
60-
),
61-
}
52+
_all_handlers = build_completeness_event_handlers(
53+
completeness_threshold=float(os.getenv("COMPLETENESS_THRESHOLD", "0.7"))
54+
)
6255

6356
app = build_service_app(
6457
SERVICE_NAME,
@@ -87,7 +80,6 @@
8780
lifespan=create_eventhub_lifespan(
8881
service_name=SERVICE_NAME,
8982
subscriptions=[
90-
EventHubSubscription("product-events", "validation-group"),
9183
EventHubSubscription("completeness-jobs", "completeness-engine"),
9284
],
9385
handlers=_all_handlers,

apps/product-management-consistency-validation/src/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "product-management-consistency-validation"
3-
version = "0.1.0"
3+
version = "1.0.0"
44
description = "Product consistency validation"
55
authors = [{name = "Ricardo Cataldi", email = "rcataldi@microsoft.com"}]
66
requires-python = ">=3.13"

0 commit comments

Comments
 (0)