Expose product length calculation through MCP

This commit is contained in:
2026-09-25 21:21:20 +02:00
parent 580e9caa5e
commit e6a7d4da72
10 changed files with 357 additions and 7 deletions
+21
View File
@@ -63,6 +63,27 @@ class CalculationApiTests(unittest.TestCase):
self.assertEqual(result["state"]["request"]["roll_length_m"], 80.0)
self.assertEqual(result["calculation"]["effective_roll_length_m"], 80.0)
def test_product_length_endpoint_uses_the_domain_calculation(self):
response = self.client.post(
"/api/calculations/product-length",
json={
"target_roll_diameter_mm": 600,
"core_diameter_mm": 140,
"thickness_mm": 4.311611,
"thickness_stddev_mm": 0.249999,
"width_m": 6.0,
"area_weight_g_m2": 495.395833,
},
headers=self.headers,
)
result = response.get_json()
self.assertEqual(response.status_code, 200)
self.assertEqual(result["status"], "success")
self.assertAlmostEqual(
result["calculation"]["average_product_length_m"], 62.0068774341
)
def test_transport_endpoint_delegates_to_shared_domain_service(self):
payload = {
"length_m": 10,
+44
View File
@@ -8,12 +8,14 @@ import mcp_server
from roll_calculation import (
ArticleRepository,
calculate_material_weight,
calculate_product_length,
calculate_roll,
get_article,
)
from rollcalc_mcp_tools import (
analyze_transport_capacity_result,
calculate_material_weight_result,
calculate_product_length_result,
calculate_roll_diameter_result,
get_article_result,
search_articles_result,
@@ -87,6 +89,23 @@ class McpAdapterTests(unittest.TestCase):
self.assertEqual(result["weight_scope"], "material_only")
self.assertAlmostEqual(result["material_weight_kg"], 1455.0239028)
def test_product_length_adapter_delegates_with_article_218500_values(self):
arguments = {
"target_roll_diameter_mm": 600.0,
"core_diameter_mm": 140.0,
"thickness_mm": 4.311611,
"thickness_stddev_mm": 0.249999,
"width_m": 6.0,
"area_weight_g_m2": 495.395833,
}
result = calculate_product_length_result(**arguments)
self.assertEqual(result, calculate_product_length(**arguments))
self.assertAlmostEqual(
result["calculation"]["average_product_length_m"], 62.0068774341
)
def test_calculate_roll_diameter_delegates_for_normal_weighted_and_invalid_cases(self):
for arguments in (
{
@@ -178,6 +197,7 @@ class McpAdapterTests(unittest.TestCase):
"get_article",
"search_articles",
"calculate_material_weight",
"calculate_product_length",
"calculate_roll_diameter",
"analyze_transport_capacity",
],
@@ -188,6 +208,7 @@ class McpAdapterTests(unittest.TestCase):
tools = instance._tool_manager._tools
search_tool = tools["search_articles"]
material_weight_tool = tools["calculate_material_weight"]
product_length_tool = tools["calculate_product_length"]
roll_schema = tools["calculate_roll_diameter"].parameters
transport_tool = tools["analyze_transport_capacity"]
transport_schema = transport_tool.parameters
@@ -202,6 +223,19 @@ class McpAdapterTests(unittest.TestCase):
["roll_length_m", "width_m", "area_weight_g_m2"],
)
self.assertNotIn("core_diameter_mm", material_weight_schema["properties"])
product_length_schema = product_length_tool.parameters
self.assertEqual(
product_length_schema["required"],
[
"target_roll_diameter_mm",
"core_diameter_mm",
"thickness_mm",
],
)
self.assertEqual(
product_length_schema["properties"]["target_roll_diameter_mm"]["type"],
"number",
)
self.assertNotIn("include_roll_weight", roll_schema["properties"])
preset_schema = transport_schema["properties"]["transport_preset"]
@@ -294,6 +328,16 @@ class McpAdapterTests(unittest.TestCase):
self.assertIn("do not infer or default one", roll_description)
self.assertIn("call calculate_material_weight instead", roll_description)
product_length_description = product_length_tool.description
for text in (
"required product/roll length for a target outer roll diameter",
"do not infer a missing core diameter, thickness, width, area weight, or article",
"minimum, average, and maximum length ranges",
"material-weight ranges",
"use calculate_roll_diameter for the reverse direction",
):
self.assertIn(text, product_length_description)
if __name__ == "__main__":
unittest.main()
+32
View File
@@ -9,6 +9,7 @@ from roll_calculation import (
_canonical_name,
article_production_site,
calculate_material_weight,
calculate_product_length,
calculate_roll,
get_article,
modify_calculation,
@@ -212,6 +213,37 @@ class RollCalculationTests(unittest.TestCase):
item["field"] for item in result["invalid"]
])
def test_product_length_characterizes_article_218500_with_ranges_and_weight(self):
result = calculate_product_length(
target_roll_diameter_mm=600,
core_diameter_mm=140,
thickness_mm=4.311611,
thickness_stddev_mm=0.249999,
width_m=6.0,
area_weight_g_m2=495.395833,
)
self.assertEqual(result["status"], "success")
calculation = result["calculation"]
self.assertAlmostEqual(calculation["minimum_product_length_m"], 55.5634372661)
self.assertAlmostEqual(calculation["average_product_length_m"], 62.0068774341)
self.assertAlmostEqual(calculation["maximum_product_length_m"], 70.1407868061)
self.assertAlmostEqual(
calculation["material_weights_kg"]["average_kg"], 184.3076921892
)
def test_product_length_rejects_target_diameter_not_larger_than_core(self):
result = calculate_product_length(
target_roll_diameter_mm=140,
core_diameter_mm=140,
thickness_mm=4,
)
self.assertEqual(result["status"], "invalid_parameter")
self.assertEqual(
result["invalid"][0]["field"], "target_roll_diameter_mm"
)
def test_diameter_only_calculation_does_not_require_width(self):
result = self.calculate_fixture(
{
+5
View File
@@ -17,6 +17,11 @@ class TemplatePdfIntegrationTests(unittest.TestCase):
self.assertIn("fetch('/api/calculations/roll'", self.template)
self.assertIn("calculation.average_diameter_mm", self.template)
self.assertIn("calculation.effective_roll_length_m", self.template)
def test_product_length_ui_uses_the_shared_domain_endpoint(self):
self.assertIn("fetch('/api/calculations/product-length'", self.template)
self.assertIn("calculation.average_product_length_m", self.template)
self.assertNotIn("const L_calc = (Math.PI / (4 * t))", self.template)
self.assertNotIn("function calculateDiameterResult", self.template)
def test_optional_weight_requires_both_weight_inputs(self):