planar 0.10.0__py3-none-any.whl → 0.11.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 (60) hide show
  1. planar/app.py +18 -6
  2. planar/routers/info.py +79 -36
  3. planar/scaffold_templates/pyproject.toml.j2 +1 -1
  4. planar/testing/fixtures.py +7 -4
  5. {planar-0.10.0.dist-info → planar-0.11.0.dist-info}/METADATA +9 -1
  6. {planar-0.10.0.dist-info → planar-0.11.0.dist-info}/RECORD +8 -60
  7. planar/ai/test_agent_serialization.py +0 -229
  8. planar/ai/test_agent_tool_step_display.py +0 -78
  9. planar/data/test_dataset.py +0 -358
  10. planar/files/storage/test_azure_blob.py +0 -435
  11. planar/files/storage/test_local_directory.py +0 -162
  12. planar/files/storage/test_s3.py +0 -299
  13. planar/files/test_files.py +0 -282
  14. planar/human/test_human.py +0 -385
  15. planar/logging/test_formatter.py +0 -327
  16. planar/modeling/mixins/test_auditable.py +0 -97
  17. planar/modeling/mixins/test_timestamp.py +0 -134
  18. planar/modeling/mixins/test_uuid_primary_key.py +0 -52
  19. planar/routers/test_agents_router.py +0 -174
  20. planar/routers/test_dataset_router.py +0 -429
  21. planar/routers/test_files_router.py +0 -49
  22. planar/routers/test_object_config_router.py +0 -367
  23. planar/routers/test_routes_security.py +0 -168
  24. planar/routers/test_rule_router.py +0 -470
  25. planar/routers/test_workflow_router.py +0 -564
  26. planar/rules/test_data/account_dormancy_management.json +0 -223
  27. planar/rules/test_data/airline_loyalty_points_calculator.json +0 -262
  28. planar/rules/test_data/applicant_risk_assessment.json +0 -435
  29. planar/rules/test_data/booking_fraud_detection.json +0 -407
  30. planar/rules/test_data/cellular_data_rollover_system.json +0 -258
  31. planar/rules/test_data/clinical_trial_eligibility_screener.json +0 -437
  32. planar/rules/test_data/customer_lifetime_value.json +0 -143
  33. planar/rules/test_data/import_duties_calculator.json +0 -289
  34. planar/rules/test_data/insurance_prior_authorization.json +0 -443
  35. planar/rules/test_data/online_check_in_eligibility_system.json +0 -254
  36. planar/rules/test_data/order_consolidation_system.json +0 -375
  37. planar/rules/test_data/portfolio_risk_monitor.json +0 -471
  38. planar/rules/test_data/supply_chain_risk.json +0 -253
  39. planar/rules/test_data/warehouse_cross_docking.json +0 -237
  40. planar/rules/test_rules.py +0 -1494
  41. planar/security/tests/test_auth_middleware.py +0 -162
  42. planar/security/tests/test_authorization_context.py +0 -78
  43. planar/security/tests/test_cedar_basics.py +0 -41
  44. planar/security/tests/test_cedar_policies.py +0 -158
  45. planar/security/tests/test_jwt_principal_context.py +0 -179
  46. planar/test_app.py +0 -142
  47. planar/test_cli.py +0 -394
  48. planar/test_config.py +0 -515
  49. planar/test_object_config.py +0 -527
  50. planar/test_object_registry.py +0 -14
  51. planar/test_sqlalchemy.py +0 -193
  52. planar/test_utils.py +0 -105
  53. planar/testing/test_memory_storage.py +0 -143
  54. planar/workflows/test_concurrency_detection.py +0 -120
  55. planar/workflows/test_lock_timeout.py +0 -140
  56. planar/workflows/test_serialization.py +0 -1203
  57. planar/workflows/test_suspend_deserialization.py +0 -231
  58. planar/workflows/test_workflow.py +0 -2005
  59. {planar-0.10.0.dist-info → planar-0.11.0.dist-info}/WHEEL +0 -0
  60. {planar-0.10.0.dist-info → planar-0.11.0.dist-info}/entry_points.txt +0 -0
@@ -1,1494 +0,0 @@
1
- import json
2
- from datetime import datetime, timezone
3
- from enum import Enum
4
- from operator import itemgetter
5
- from pathlib import Path
6
- from typing import Any, Dict, cast
7
- from unittest.mock import patch
8
- from uuid import UUID
9
-
10
- import pytest
11
- from pydantic import BaseModel, Field, ValidationError
12
- from sqlmodel import select
13
- from sqlmodel.ext.asyncio.session import AsyncSession
14
-
15
- from planar.object_registry import ObjectRegistry
16
- from planar.rules.decorator import RULE_REGISTRY, rule, serialize_for_rule_evaluation
17
- from planar.rules.models import JDMGraph, Rule, RuleEngineConfig, create_jdm_graph
18
- from planar.rules.rule_configuration import rule_configuration
19
- from planar.rules.runner import EvaluateError, EvaluateResponse, evaluate_rule
20
- from planar.workflows.decorators import workflow
21
- from planar.workflows.execution import lock_and_execute
22
- from planar.workflows.models import StepType, WorkflowStatus, WorkflowStep
23
-
24
-
25
- # Test Enums
26
- class CustomerTier(str, Enum):
27
- """Customer tier enumeration."""
28
-
29
- STANDARD = "standard"
30
- PREMIUM = "premium"
31
- VIP = "vip"
32
-
33
-
34
- # Test data models
35
- class PriceCalculationInput(BaseModel):
36
- """Input for a price calculation rule."""
37
-
38
- product_id: str = Field(description="Product identifier")
39
- base_price: float = Field(description="Base price of the product")
40
- quantity: int = Field(description="Quantity ordered")
41
- customer_tier: CustomerTier = Field(description="Customer tier")
42
-
43
-
44
- class PriceCalculationOutput(BaseModel):
45
- """Output from a price calculation rule."""
46
-
47
- final_price: float = Field(description="Final calculated price")
48
- discount_applied: float = Field(description="Discount percentage applied")
49
- discount_reason: str = Field(description="Reason for the discount")
50
-
51
-
52
- # Default rule implementation for testing
53
- DEFAULT_PRICE_CALCULATION = PriceCalculationOutput(
54
- final_price=95.0, discount_applied=5.0, discount_reason="Standard 5% discount"
55
- )
56
-
57
-
58
- # Sample JDM graph for overriding the rule
59
- PRICE_RULE_JDM_OVERRIDE = {
60
- "nodes": [
61
- {
62
- "id": "input-node",
63
- "type": "inputNode",
64
- "name": "Input",
65
- "content": {
66
- "schema": json.dumps(PriceCalculationInput.model_json_schema())
67
- },
68
- "position": {"x": 100, "y": 100},
69
- },
70
- {
71
- "id": "output-node",
72
- "type": "outputNode",
73
- "name": "Output",
74
- "content": {
75
- "schema": json.dumps(PriceCalculationOutput.model_json_schema())
76
- },
77
- "position": {"x": 700, "y": 100},
78
- },
79
- {
80
- "id": "function-node",
81
- "type": "functionNode",
82
- "name": "Custom Pricing Logic",
83
- "content": {
84
- "source": """
85
- export const handler = async (input) => {
86
- let discount = 0;
87
- let reason = "No discount applied";
88
-
89
- if (input.customer_tier === "premium") {
90
- discount = 10;
91
- reason = "Premium customer discount";
92
- } else if (input.customer_tier === "vip") {
93
- discount = 15;
94
- reason = "VIP customer discount";
95
- }
96
-
97
- if (input.quantity > 10) {
98
- discount += 5;
99
- reason += " + bulk order discount";
100
- }
101
-
102
- const finalPrice = input.base_price * input.quantity * (1 - discount/100);
103
-
104
- return {
105
- final_price: finalPrice,
106
- discount_applied: discount,
107
- discount_reason: reason
108
- };
109
- };
110
- """
111
- },
112
- "position": {"x": 400, "y": 100},
113
- },
114
- ],
115
- "edges": [
116
- {
117
- "id": "edge1",
118
- "sourceId": "input-node",
119
- "targetId": "function-node",
120
- "type": "edge",
121
- },
122
- {
123
- "id": "edge2",
124
- "sourceId": "function-node",
125
- "targetId": "output-node",
126
- "type": "edge",
127
- },
128
- ],
129
- }
130
-
131
-
132
- @pytest.fixture
133
- def price_calculation_rule():
134
- """Returns a rule definition for price calculation testing."""
135
-
136
- @rule(
137
- description="Calculate the final price based on product, quantity, and customer tier"
138
- )
139
- def calculate_price(input: PriceCalculationInput) -> PriceCalculationOutput:
140
- # In a real implementation, this would contain business logic
141
- # For testing, simply return the default output
142
- return DEFAULT_PRICE_CALCULATION
143
-
144
- ObjectRegistry.get_instance().register(calculate_price.__rule__) # type: ignore
145
-
146
- return calculate_price
147
-
148
-
149
- @pytest.fixture
150
- def price_calculation_rule_with_body_variables():
151
- """Returns a rule definition for price calculation testing."""
152
-
153
- @rule(
154
- description="Calculate the final price based on product, quantity, and customer tier"
155
- )
156
- def calculate_price(input: PriceCalculationInput) -> PriceCalculationOutput:
157
- some_variable = 10
158
- return PriceCalculationOutput(
159
- final_price=input.base_price * some_variable,
160
- discount_applied=0,
161
- discount_reason="No discount applied",
162
- )
163
-
164
- return calculate_price
165
-
166
-
167
- @pytest.fixture
168
- def price_calculation_input():
169
- """Returns sample price calculation input for testing."""
170
- return {
171
- "product_id": "PROD-123",
172
- "base_price": 100.0,
173
- "quantity": 1,
174
- "customer_tier": "standard",
175
- }
176
-
177
-
178
- async def test_rule_initialization():
179
- """Test that a rule function is properly initialized with the @rule decorator."""
180
-
181
- @rule(description="Test rule initialization")
182
- def test_rule(input: PriceCalculationInput) -> PriceCalculationOutput:
183
- return DEFAULT_PRICE_CALCULATION
184
-
185
- # The rule should be registered in the RULE_REGISTRY
186
- assert "test_rule" in RULE_REGISTRY
187
- registered_rule = RULE_REGISTRY["test_rule"]
188
-
189
- # Verify initialization
190
- assert registered_rule.name == "test_rule"
191
- assert registered_rule.description == "Test rule initialization"
192
- assert registered_rule.input == PriceCalculationInput
193
- assert registered_rule.output == PriceCalculationOutput
194
-
195
-
196
- async def test_rule_type_validation():
197
- """Test that the rule decorator properly validates input and output types."""
198
-
199
- # Should raise ValueError when input type is not a Pydantic model
200
- with pytest.raises(ValueError):
201
- # Using Any to avoid the actual type check in pytest itself
202
- # The validation function in the decorator will still catch this
203
- @rule(description="Invalid input type")
204
- def invalid_input_rule(input: Any) -> PriceCalculationOutput:
205
- return DEFAULT_PRICE_CALCULATION
206
-
207
- # Should raise ValueError when output type is not a Pydantic model
208
- with pytest.raises(ValueError):
209
- # Using Any to avoid the actual type check in pytest itself
210
- @rule(description="Invalid output type")
211
- def invalid_output_rule(input: PriceCalculationInput) -> Any:
212
- return "Invalid"
213
-
214
- # Should raise ValueError when missing type annotations
215
- with pytest.raises(ValueError):
216
- # Missing type annotation for input
217
- @rule(description="Missing annotations")
218
- def missing_annotations_rule(input):
219
- return DEFAULT_PRICE_CALCULATION
220
-
221
- # Should raise ValueError when missing return type
222
- with pytest.raises(ValueError):
223
- # The decorator function should catch this
224
- @rule(description="Missing return type")
225
- def missing_return_type(input: PriceCalculationInput):
226
- return DEFAULT_PRICE_CALCULATION
227
-
228
-
229
- async def test_rule_in_workflow(session: AsyncSession, price_calculation_rule):
230
- """Test that a rule can be used in a workflow."""
231
-
232
- @workflow()
233
- async def pricing_workflow(input_data: Dict):
234
- input_model = PriceCalculationInput(**input_data)
235
- result = await price_calculation_rule(input_model)
236
- return result
237
-
238
- # Start the workflow and run it
239
- input_data = {
240
- "product_id": "PROD-123",
241
- "base_price": 100.0,
242
- "quantity": 1,
243
- "customer_tier": "standard",
244
- }
245
-
246
- wf = await pricing_workflow.start(input_data)
247
- result = await lock_and_execute(wf)
248
-
249
- # Verify workflow completed successfully
250
- assert wf.status == WorkflowStatus.SUCCEEDED
251
- assert wf.result == DEFAULT_PRICE_CALCULATION.model_dump()
252
-
253
- assert isinstance(result, PriceCalculationOutput)
254
- assert result.final_price == DEFAULT_PRICE_CALCULATION.final_price
255
- assert result.discount_applied == DEFAULT_PRICE_CALCULATION.discount_applied
256
- assert result.discount_reason == DEFAULT_PRICE_CALCULATION.discount_reason
257
-
258
- # Verify steps were recorded correctly
259
- steps = (
260
- await session.exec(
261
- select(WorkflowStep).where(WorkflowStep.workflow_id == wf.id)
262
- )
263
- ).all()
264
- assert len(steps) >= 1
265
-
266
- # Find the rule step
267
- rule_step = next((step for step in steps if step.step_type == StepType.RULE), None)
268
- assert rule_step is not None
269
- assert price_calculation_rule.__name__ in rule_step.function_name
270
-
271
-
272
- async def test_rule_in_workflow_with_body_variables(
273
- session: AsyncSession, price_calculation_rule_with_body_variables
274
- ):
275
- """Test that a rule can be used in a workflow."""
276
-
277
- @workflow()
278
- async def pricing_workflow(input_data: Dict):
279
- input_model = PriceCalculationInput(**input_data)
280
- result = await price_calculation_rule_with_body_variables(input_model)
281
- return result
282
-
283
- # Start the workflow and run it
284
- input_data = {
285
- "product_id": "PROD-123",
286
- "base_price": 10.0,
287
- "quantity": 1,
288
- "customer_tier": "standard",
289
- }
290
-
291
- wf = await pricing_workflow.start(input_data)
292
- result = await lock_and_execute(wf)
293
-
294
- # Verify workflow completed successfully
295
- assert wf.status == WorkflowStatus.SUCCEEDED
296
- assert (
297
- wf.result
298
- == PriceCalculationOutput(
299
- final_price=100.0, discount_applied=0, discount_reason="No discount applied"
300
- ).model_dump()
301
- )
302
-
303
- assert isinstance(result, PriceCalculationOutput)
304
- assert result.final_price == 100.0
305
- assert result.discount_applied == 0
306
- assert result.discount_reason == "No discount applied"
307
-
308
-
309
- async def test_rule_override(session: AsyncSession, price_calculation_rule):
310
- """Test that a rule can be overridden with a JDM graph."""
311
-
312
- # Create and save an override
313
- override = RuleEngineConfig(jdm=JDMGraph.model_validate(PRICE_RULE_JDM_OVERRIDE))
314
-
315
- cfg = await rule_configuration.write_config(
316
- price_calculation_rule.__name__, override
317
- )
318
- await rule_configuration.promote_config(cfg.id)
319
-
320
- @workflow()
321
- async def pricing_workflow(input_data: Dict):
322
- input_model = PriceCalculationInput(**input_data)
323
- result = await price_calculation_rule(input_model)
324
- return result
325
-
326
- # Start the workflow with premium customer input
327
- premium_input = {
328
- "product_id": "PROD-456",
329
- "base_price": 100.0,
330
- "quantity": 5,
331
- "customer_tier": "premium",
332
- }
333
-
334
- wf = await pricing_workflow.start(premium_input)
335
- _ = await lock_and_execute(wf)
336
-
337
- # Verify the workflow used the override logic
338
- assert wf.status == WorkflowStatus.SUCCEEDED
339
- assert wf.result is not None
340
- assert wf.result != DEFAULT_PRICE_CALCULATION.model_dump()
341
- assert wf.result["discount_applied"] == 10.0
342
- assert "Premium customer discount" in wf.result["discount_reason"]
343
-
344
- # Now test with VIP customer and bulk order
345
- vip_bulk_input = {
346
- "product_id": "PROD-789",
347
- "base_price": 100.0,
348
- "quantity": 15,
349
- "customer_tier": "vip",
350
- }
351
-
352
- wf2 = await pricing_workflow.start(vip_bulk_input)
353
- _ = await lock_and_execute(wf2)
354
-
355
- # Verify the workflow used the override logic with both discounts
356
- assert wf2.status == WorkflowStatus.SUCCEEDED
357
- assert wf2.result is not None
358
- assert wf2.result["discount_applied"] == 20.0 # 15% VIP + 5% bulk
359
- assert "VIP customer discount" in wf2.result["discount_reason"]
360
- assert "bulk order discount" in wf2.result["discount_reason"]
361
-
362
-
363
- async def test_evaluate_rule_function():
364
- """Test the evaluate_rule function directly."""
365
-
366
- # Create test input data
367
- input_data = {
368
- "product_id": "PROD-123",
369
- "base_price": 100.0,
370
- "quantity": 5,
371
- "customer_tier": "premium",
372
- }
373
-
374
- # Test error handling
375
- with patch("planar.rules.runner.ZenEngine") as MockZenEngine:
376
- mock_decision = MockZenEngine.return_value.create_decision.return_value
377
- error_json = json.dumps(
378
- {
379
- "type": "RuleEvaluationError",
380
- "source": json.dumps({"error": "Invalid rule logic"}),
381
- "nodeId": "decision-table-node",
382
- }
383
- )
384
- mock_decision.evaluate.side_effect = RuntimeError(error_json)
385
-
386
- result = evaluate_rule(
387
- JDMGraph.model_validate(PRICE_RULE_JDM_OVERRIDE), input_data
388
- )
389
-
390
- assert isinstance(result, EvaluateError)
391
- assert result.success is False
392
- assert result.title == "RuleEvaluationError"
393
- assert result.message == {"error": "Invalid rule logic"}
394
- assert result.data["nodeId"] == "decision-table-node"
395
-
396
-
397
- async def test_rule_override_validation(session: AsyncSession, price_calculation_rule):
398
- """Test validation when creating a rule override."""
399
-
400
- ObjectRegistry.get_instance().register(price_calculation_rule.__rule__)
401
-
402
- # Test with valid JDMGraph
403
- valid_jdm = create_jdm_graph(price_calculation_rule.__rule__)
404
- valid_override = RuleEngineConfig(jdm=valid_jdm)
405
- assert valid_override is not None
406
- assert isinstance(valid_override.jdm, JDMGraph)
407
- await rule_configuration.write_config(
408
- price_calculation_rule.__name__, valid_override
409
- )
410
-
411
- # Query back and verify
412
- configs = await rule_configuration._read_configs(price_calculation_rule.__name__)
413
- assert len(configs) == 1
414
- assert configs[0].object_name == price_calculation_rule.__name__
415
- assert JDMGraph.model_validate(configs[0].data.jdm) == valid_jdm
416
-
417
- # Test with invalid JDMGraph (missing required fields)
418
- with pytest.raises(ValidationError):
419
- # Test with incomplete dictionary
420
- invalid_dict = {"invalid": "structure"}
421
- JDMGraph.model_validate(invalid_dict)
422
-
423
- # Test with invalid JDMGraph type
424
- with pytest.raises(ValidationError):
425
- # Test with completely wrong type
426
- RuleEngineConfig(jdm="invalid_string") # type: ignore
427
-
428
-
429
- def test_serialize_for_rule_evaluation_dict():
430
- """Test serialization of dictionaries with nested datetime and UUID objects."""
431
-
432
- test_uuid = UUID("12345678-1234-5678-1234-567812345678")
433
- naive_dt = datetime(2023, 12, 25, 14, 30, 45)
434
- aware_dt = datetime(2023, 12, 25, 14, 30, 45, tzinfo=timezone.utc)
435
-
436
- test_dict = {
437
- "id": test_uuid,
438
- "created_at": naive_dt,
439
- "updated_at": aware_dt,
440
- "name": "test_item",
441
- "count": 42,
442
- "nested": {"another_id": test_uuid, "another_date": naive_dt},
443
- }
444
-
445
- serialized = serialize_for_rule_evaluation(test_dict)
446
-
447
- assert serialized["id"] == "12345678-1234-5678-1234-567812345678"
448
- assert serialized["created_at"] == "2023-12-25T14:30:45Z"
449
- assert serialized["updated_at"] == "2023-12-25T14:30:45+00:00"
450
- assert serialized["name"] == "test_item"
451
- assert serialized["count"] == 42
452
- assert serialized["nested"]["another_id"] == "12345678-1234-5678-1234-567812345678"
453
- assert serialized["nested"]["another_date"] == "2023-12-25T14:30:45Z"
454
-
455
-
456
- def test_serialize_for_rule_evaluation():
457
- """Test serialization of complex nested structures."""
458
-
459
- test_uuid1 = UUID("12345678-1234-5678-1234-567812345678")
460
- test_uuid2 = UUID("87654321-4321-8765-4321-876543218765")
461
- naive_dt = datetime(2023, 12, 25, 14, 30, 45, 123456)
462
- aware_dt = datetime(2023, 12, 25, 14, 30, 45, 123456, timezone.utc)
463
-
464
- complex_data = {
465
- "metadata": {
466
- "id": test_uuid1,
467
- "created_at": naive_dt,
468
- "updated_at": aware_dt,
469
- "tags": ["tag1", "tag2", test_uuid2],
470
- },
471
- "items": [
472
- {
473
- "item_id": test_uuid1,
474
- "timestamp": naive_dt,
475
- "values": (1, 2, 3, aware_dt),
476
- },
477
- {
478
- "item_id": test_uuid2,
479
- "timestamp": aware_dt,
480
- "nested_list": [{"deep_uuid": test_uuid1, "deep_date": naive_dt}],
481
- },
482
- ],
483
- "enum_values": [CustomerTier.STANDARD],
484
- "simple_values": [1, "test", True, None],
485
- }
486
-
487
- serialized = serialize_for_rule_evaluation(complex_data)
488
-
489
- # Verify metadata
490
- assert serialized["metadata"]["id"] == "12345678-1234-5678-1234-567812345678"
491
- assert serialized["metadata"]["created_at"] == "2023-12-25T14:30:45.123456Z"
492
- assert serialized["metadata"]["updated_at"] == "2023-12-25T14:30:45.123456+00:00"
493
- assert serialized["metadata"]["tags"][2] == "87654321-4321-8765-4321-876543218765"
494
-
495
- # Verify items
496
- assert serialized["items"][0]["item_id"] == "12345678-1234-5678-1234-567812345678"
497
- assert serialized["items"][0]["timestamp"] == "2023-12-25T14:30:45.123456Z"
498
- assert serialized["items"][0]["values"][3] == "2023-12-25T14:30:45.123456+00:00"
499
-
500
- assert serialized["items"][1]["item_id"] == "87654321-4321-8765-4321-876543218765"
501
- assert serialized["items"][1]["timestamp"] == "2023-12-25T14:30:45.123456+00:00"
502
- assert (
503
- serialized["items"][1]["nested_list"][0]["deep_uuid"]
504
- == "12345678-1234-5678-1234-567812345678"
505
- )
506
- assert (
507
- serialized["items"][1]["nested_list"][0]["deep_date"]
508
- == "2023-12-25T14:30:45.123456Z"
509
- )
510
-
511
- # Verify simple values remain unchanged
512
- assert serialized["simple_values"] == [1, "test", True, None]
513
-
514
-
515
- class DateTimeTestModel(BaseModel):
516
- """Test model with datetime fields for integration testing."""
517
-
518
- id: UUID = Field(description="Unique identifier")
519
- created_at: datetime = Field(description="Creation timestamp")
520
- updated_at: datetime | None = Field(default=None, description="Update timestamp")
521
- name: str = Field(description="Name of the item")
522
-
523
-
524
- def test_serialize_pydantic_model_with_datetime():
525
- """Test serialization of Pydantic models containing datetime fields."""
526
-
527
- test_uuid = UUID("12345678-1234-5678-1234-567812345678")
528
- naive_dt = datetime(2023, 12, 25, 14, 30, 45, 123456)
529
- aware_dt = datetime(2023, 12, 25, 14, 30, 45, 123456, timezone.utc)
530
-
531
- model = DateTimeTestModel(
532
- id=test_uuid, created_at=naive_dt, updated_at=aware_dt, name="test_model"
533
- )
534
-
535
- # Serialize the model's dict representation
536
- model_dict = model.model_dump()
537
- serialized = serialize_for_rule_evaluation(model_dict)
538
-
539
- assert serialized["id"] == "12345678-1234-5678-1234-567812345678"
540
- assert serialized["created_at"] == "2023-12-25T14:30:45.123456Z"
541
- assert serialized["updated_at"] == "2023-12-25T14:30:45.123456+00:00"
542
- assert serialized["name"] == "test_model"
543
-
544
-
545
- async def test_rule_with_complex_types_serialization(session: AsyncSession):
546
- """Integration test: Test that complex types serialization works in rule evaluation."""
547
-
548
- class ComplexTypesInput(BaseModel):
549
- event_id: UUID
550
- event_time: datetime
551
- event_name: str
552
- enum_value: CustomerTier
553
-
554
- class ComplexTypesOutput(BaseModel):
555
- processed_id: UUID
556
- processed_time: datetime
557
- enum_value: CustomerTier
558
- message: str
559
-
560
- @rule(description="Process datetime input")
561
- def process_datetime_rule(input: ComplexTypesInput) -> ComplexTypesOutput:
562
- # Should actually be using the rule override below.
563
- return ComplexTypesOutput(
564
- processed_id=UUID("12345678-1234-5678-1234-567812345678"),
565
- processed_time=datetime.now(timezone.utc),
566
- enum_value=CustomerTier.STANDARD,
567
- message="Should not be using this default rule",
568
- )
569
-
570
- ObjectRegistry.get_instance().register(process_datetime_rule.__rule__) # type: ignore
571
-
572
- # Create a JDM override that uses the datetime fields
573
- datetime_jdm_override = {
574
- "nodes": [
575
- {
576
- "id": "input-node",
577
- "type": "inputNode",
578
- "name": "Input",
579
- "content": {
580
- "schema": json.dumps(ComplexTypesInput.model_json_schema())
581
- },
582
- "position": {"x": 100, "y": 100},
583
- },
584
- {
585
- "id": "output-node",
586
- "type": "outputNode",
587
- "name": "Output",
588
- "content": {
589
- "schema": json.dumps(ComplexTypesOutput.model_json_schema())
590
- },
591
- "position": {"x": 700, "y": 100},
592
- },
593
- {
594
- "id": "function-node",
595
- "type": "functionNode",
596
- "name": "DateTime Processing",
597
- "content": {
598
- "source": """
599
- export const handler = async (input) => {
600
- return {
601
- processed_id: input.event_id,
602
- processed_time: input.event_time,
603
- enum_value: input.enum_value,
604
- message: `Override processed ${input.event_name}`
605
- };
606
- };
607
- """
608
- },
609
- "position": {"x": 400, "y": 100},
610
- },
611
- ],
612
- "edges": [
613
- {
614
- "id": "edge1",
615
- "sourceId": "input-node",
616
- "targetId": "function-node",
617
- "type": "edge",
618
- },
619
- {
620
- "id": "edge2",
621
- "sourceId": "function-node",
622
- "targetId": "output-node",
623
- "type": "edge",
624
- },
625
- ],
626
- }
627
-
628
- # Create and save an override
629
- override = RuleEngineConfig(jdm=JDMGraph.model_validate(datetime_jdm_override))
630
- cfg = await rule_configuration.write_config(
631
- process_datetime_rule.__name__, override
632
- )
633
- await rule_configuration.promote_config(cfg.id)
634
-
635
- @workflow()
636
- async def datetime_workflow(input: ComplexTypesInput):
637
- result = await process_datetime_rule(input)
638
- return result
639
-
640
- # Test with naive datetime
641
- test_uuid = UUID("12345678-1234-5678-1234-567812345678")
642
- naive_dt = datetime(2023, 12, 25, 14, 30, 45, 123456)
643
-
644
- input = ComplexTypesInput(
645
- event_id=test_uuid,
646
- event_time=naive_dt,
647
- event_name="test_event",
648
- enum_value=CustomerTier.STANDARD,
649
- )
650
-
651
- wf = await datetime_workflow.start(input)
652
- await lock_and_execute(wf)
653
-
654
- # Verify the workflow completed successfully
655
- assert wf.status == WorkflowStatus.SUCCEEDED
656
- assert wf.result is not None
657
- assert ComplexTypesOutput.model_validate(wf.result) == ComplexTypesOutput(
658
- processed_id=test_uuid,
659
- processed_time=naive_dt.replace(tzinfo=timezone.utc),
660
- enum_value=CustomerTier.STANDARD,
661
- message="Override processed test_event",
662
- )
663
-
664
-
665
- async def test_create_jdm_graph():
666
- """Test JDM graph generation from rule schemas."""
667
- rule = Rule(
668
- name="test_price_rule",
669
- description="Test price calculation rule",
670
- input=PriceCalculationInput,
671
- output=PriceCalculationOutput,
672
- )
673
-
674
- # Generate the JDM graph
675
- jdm_graph = create_jdm_graph(rule)
676
-
677
- # Verify the structure
678
- assert len(jdm_graph.nodes) == 3 # input, decision table, output
679
- assert len(jdm_graph.edges) == 2 # input->table, table->output
680
-
681
- # Verify node types
682
- node_types = {node.type for node in jdm_graph.nodes}
683
- assert node_types == {"inputNode", "decisionTableNode", "outputNode"}
684
-
685
- # Find the decision table node
686
- decision_table = next(
687
- node for node in jdm_graph.nodes if node.type == "decisionTableNode"
688
- )
689
-
690
- # Verify output columns match the output schema
691
- output_columns = decision_table.content.outputs
692
- assert len(output_columns) == 3 # final_price, discount_applied, discount_reason
693
-
694
- output_fields = {col.field for col in output_columns}
695
- assert output_fields == {"final_price", "discount_applied", "discount_reason"}
696
-
697
- # Verify rule values have correct default types
698
- rule_values = decision_table.content.rules[0]
699
-
700
- # Find column IDs for each field
701
- final_price_col = next(col for col in output_columns if col.field == "final_price")
702
- discount_applied_col = next(
703
- col for col in output_columns if col.field == "discount_applied"
704
- )
705
- discount_reason_col = next(
706
- col for col in output_columns if col.field == "discount_reason"
707
- )
708
-
709
- assert rule_values[final_price_col.id] == "0" # number default
710
- assert rule_values[discount_applied_col.id] == "0" # number default
711
- assert rule_values[discount_reason_col.id] == '"default value"' # string default
712
-
713
- # Verify input and output nodes have proper schemas
714
- input_node = next(node for node in jdm_graph.nodes if node.type == "inputNode")
715
- output_node = next(node for node in jdm_graph.nodes if node.type == "outputNode")
716
-
717
- input_schema = json.loads(input_node.content.schema_)
718
- output_schema = json.loads(output_node.content.schema_)
719
-
720
- assert input_schema == PriceCalculationInput.model_json_schema()
721
- assert output_schema == PriceCalculationOutput.model_json_schema()
722
-
723
-
724
- async def test_jdm_graph_evaluation():
725
- """Test evaluating a JDM graph with a simple rule."""
726
-
727
- # Create a rule and generate its JDM graph
728
- @rule(description="Test JDM evaluation")
729
- def simple_rule(input: PriceCalculationInput) -> PriceCalculationOutput:
730
- return DEFAULT_PRICE_CALCULATION
731
-
732
- jdm_graph = create_jdm_graph(RULE_REGISTRY[simple_rule.__name__])
733
-
734
- # Test input data
735
- test_input = {
736
- "product_id": "PROD-EVAL",
737
- "base_price": 200.0,
738
- "quantity": 2,
739
- "customer_tier": "vip",
740
- }
741
-
742
- # Evaluate the rule
743
- result = evaluate_rule(jdm_graph, test_input)
744
-
745
- # Verify the result
746
- assert isinstance(result, EvaluateResponse)
747
- assert result.success is True
748
- assert result.result["final_price"] == 0.0
749
- assert result.result["discount_applied"] == 0.0
750
- assert "default value" in result.result["discount_reason"]
751
-
752
-
753
- def test_evalute_rule_with_airline_loyalty_points_calculator_rule():
754
- airline_loyalty_points_calculator_path = (
755
- Path(__file__).parent / "test_data" / "airline_loyalty_points_calculator.json"
756
- )
757
- with open(airline_loyalty_points_calculator_path, "r", encoding="utf-8") as f:
758
- jdm_dict = json.load(f)
759
-
760
- input_data = {
761
- "booking": {
762
- "fareClass": "Business",
763
- "routeType": "International",
764
- "distance": 3500,
765
- "isSeasonalPromotion": True,
766
- },
767
- "member": {
768
- "status": "Gold",
769
- "id": "MEM12345",
770
- "name": "John Smith",
771
- "enrollmentDate": "2020-05-15",
772
- },
773
- }
774
-
775
- result = evaluate_rule(JDMGraph.model_validate(jdm_dict), input_data)
776
-
777
- assert result.success
778
- assert cast(EvaluateResponse, result).result == {
779
- "calculatedPoints": 9000,
780
- "seasonalPromotion": 1.5,
781
- "totalPoints": 9000,
782
- }
783
-
784
-
785
- def test_evalute_rule_with_account_dormancy_management_rule():
786
- account_dormancy_management_path = (
787
- Path(__file__).parent / "test_data" / "account_dormancy_management.json"
788
- )
789
- with open(account_dormancy_management_path, "r", encoding="utf-8") as f:
790
- jdm_dict = json.load(f)
791
-
792
- input_data = {
793
- "accountId": "ACC98765432",
794
- "accountType": "savings",
795
- "customerTier": "premium",
796
- "lastActivityDate": "2024-11-15",
797
- "dormancyThreshold": 180,
798
- "accountBalance": 25750.45,
799
- "currency": "USD",
800
- "region": "NORTH_AMERICA",
801
- "contactPreference": "email",
802
- "customerEmail": "customer@example.com",
803
- "customerPhone": "+15551234567",
804
- "regulatoryJurisdiction": "US-NY",
805
- }
806
-
807
- result = evaluate_rule(JDMGraph.model_validate(jdm_dict), input_data)
808
-
809
- assert result.success
810
- assert cast(EvaluateResponse, result).result == {
811
- "actionPriority": "high",
812
- "recommendedAction": "fee_waiver",
813
- }
814
-
815
-
816
- def test_evalute_rule_with_clinical_trial_eligibility_screener_rule():
817
- clinical_trial_eligibility_screener_path = (
818
- Path(__file__).parent / "test_data" / "clinical_trial_eligibility_screener.json"
819
- )
820
- with open(clinical_trial_eligibility_screener_path, "r", encoding="utf-8") as f:
821
- jdm_dict = json.load(f)
822
-
823
- input_data = {
824
- "patient": {
825
- "id": "P67890",
826
- "name": "John Smith",
827
- "age": 68,
828
- "diagnosis": "lung_cancer",
829
- "diseaseStage": "IV",
830
- "currentMedications": ["immunosuppressants", "albuterol", "omeprazole"],
831
- "priorTreatments": 3,
832
- "comorbidities": ["autoimmune_disease", "COPD"],
833
- "lastLabResults": {"wbc": 3.8, "hgb": 10.9, "plt": 150, "creatinine": 1.2},
834
- }
835
- }
836
-
837
- result = evaluate_rule(JDMGraph.model_validate(jdm_dict), input_data)
838
-
839
- expected_result = {
840
- "decisionSummary": "Patient is not eligible for clinical trial",
841
- "eligibilityReasons": [
842
- {
843
- "flag": False,
844
- "reason": "Stage IV patients excluded from trial",
845
- },
846
- {
847
- "flag": True,
848
- "reason": "Diagnosis matches trial criteria",
849
- },
850
- {
851
- "flag": True,
852
- "reason": "Age within eligible range",
853
- },
854
- {
855
- "flag": False,
856
- "reason": "Excluded comorbidity present",
857
- },
858
- {
859
- "flag": False,
860
- "reason": "Patient taking excluded medications",
861
- },
862
- {
863
- "flag": False,
864
- "reason": "Too many prior treatments",
865
- },
866
- ],
867
- "failedCriteria": [
868
- "stage",
869
- "comorbidity",
870
- "medication",
871
- "priorTreatment",
872
- ],
873
- "isEligible": False,
874
- }
875
-
876
- result = cast(EvaluateResponse, result)
877
-
878
- def sort_reasons(reasons: list[dict[str, Any]]) -> list[dict[str, Any]]:
879
- return sorted(reasons, key=itemgetter("reason"))
880
-
881
- assert result.success
882
- assert result.result["decisionSummary"] == expected_result["decisionSummary"]
883
- assert sort_reasons(result.result["eligibilityReasons"]) == sort_reasons(
884
- expected_result["eligibilityReasons"]
885
- )
886
- assert sorted(result.result["failedCriteria"]) == sorted(
887
- expected_result["failedCriteria"]
888
- )
889
- assert result.result["isEligible"] == expected_result["isEligible"]
890
-
891
-
892
- def test_evalute_rule_with_customer_lifetime_value_rule():
893
- customer_lifetime_value_path = (
894
- Path(__file__).parent / "test_data" / "customer_lifetime_value.json"
895
- )
896
- with open(customer_lifetime_value_path, "r", encoding="utf-8") as f:
897
- jdm_dict = json.load(f)
898
-
899
- input_data = {
900
- "customer": {
901
- "id": "CUST-12345",
902
- "name": "John Doe",
903
- "segment": "retail",
904
- "acquisitionCost": 150,
905
- "acquisitionChannel": "paid_search",
906
- },
907
- "purchaseHistory": {
908
- "orderValues": [120, 89, 245, 78, 310],
909
- "customerDurationMonths": 18,
910
- "averageGrossMarginPercent": 35,
911
- "retentionRate": 85,
912
- },
913
- }
914
-
915
- result = evaluate_rule(JDMGraph.model_validate(jdm_dict), input_data)
916
-
917
- assert result.success
918
- assert cast(EvaluateResponse, result).result == {
919
- "acquisitionCostRatio": 0.009543603664743808,
920
- "adjustedLTV": 15567.333333333334,
921
- "averageOrderValue": 168.4,
922
- "basicLTV": 15717.333333333334,
923
- "customer": {
924
- "acquisitionChannel": "paid_search",
925
- "acquisitionCost": 150,
926
- "id": "CUST-12345",
927
- "name": "John Doe",
928
- "segment": "retail",
929
- },
930
- "customerInsights": {
931
- "recommendedStrategy": "High-touch service, premium offers, exclusive events",
932
- "tier": "platinum",
933
- },
934
- "customerLifetimeMonths": 80,
935
- "grossMargin": 0.35,
936
- "purchaseFrequency": 3.3333333333333335,
937
- "purchaseHistory": {
938
- "averageGrossMarginPercent": 35,
939
- "customerDurationMonths": 18,
940
- "orderValues": [120, 89, 245, 78, 310],
941
- "retentionRate": 85,
942
- },
943
- }
944
-
945
-
946
- def test_evaluate_rule_with_supply_chain_risk_assessment_rule():
947
- supply_chain_risk_assessment_path = (
948
- Path(__file__).parent / "test_data" / "supply_chain_risk.json"
949
- )
950
- with open(supply_chain_risk_assessment_path, "r", encoding="utf-8") as f:
951
- jdm_dict = json.load(f)
952
-
953
- input_data = {
954
- "supplier": {
955
- "name": "GlobalTech Supplies Inc.",
956
- "location": "medium_risk_region",
957
- "performanceScore": 82,
958
- "alternateSourcesCount": 2,
959
- "products": [
960
- {"id": "P123", "name": "Semiconductor Chip", "criticalComponent": True}
961
- ],
962
- "relationshipDurationMonths": 36,
963
- },
964
- "geopoliticalTensions": True,
965
- "marketVolatility": "medium",
966
- "supplyCategory": "electronics",
967
- "leadTimeData": {"averageDays": 45, "historicalVariance": 8},
968
- }
969
-
970
- result = evaluate_rule(JDMGraph.model_validate(jdm_dict), input_data)
971
-
972
- assert result.success
973
- assert cast(EvaluateResponse, result).result == {
974
- "adjustedRiskScore": 57,
975
- "assessment": {
976
- "baseRiskCategory": "medium",
977
- "baseRiskScore": 40,
978
- "recommendedAction": "Monitor supplier performance and conduct quarterly reviews",
979
- },
980
- "finalAssessment": {
981
- "leadTimeImpact": "minor",
982
- "priorityLevel": "medium",
983
- "riskCategory": "medium",
984
- },
985
- "geopoliticalFactor": 1.3,
986
- "geopoliticalTensions": True,
987
- "leadTimeData": {"averageDays": 45, "historicalVariance": 8},
988
- "marketVolatility": "medium",
989
- "marketVolatilityFactor": 1.1,
990
- "supplier": {
991
- "alternateSourcesCount": 2,
992
- "location": "medium_risk_region",
993
- "name": "GlobalTech Supplies Inc.",
994
- "performanceScore": 82,
995
- "products": [
996
- {"criticalComponent": True, "id": "P123", "name": "Semiconductor Chip"}
997
- ],
998
- "relationshipDurationMonths": 36,
999
- },
1000
- "supplyCategory": "electronics",
1001
- }
1002
-
1003
-
1004
- def test_evaluate_rule_with_import_duties_calculator_rule():
1005
- import_duties_calculator_path = (
1006
- Path(__file__).parent / "test_data" / "import_duties_calculator.json"
1007
- )
1008
- with open(import_duties_calculator_path, "r", encoding="utf-8") as f:
1009
- jdm_dict = json.load(f)
1010
-
1011
- input_data = {
1012
- "product": {
1013
- "category": "electronics",
1014
- "value": 1200,
1015
- "weight": 0.8,
1016
- "hsCode": "851712",
1017
- },
1018
- "origin": {"country": "CN", "hasFTA": False, "preferentialTreatment": False},
1019
- "destination": {"country": "US"},
1020
- }
1021
-
1022
- result = evaluate_rule(JDMGraph.model_validate(jdm_dict), input_data)
1023
-
1024
- assert result.success
1025
- assert cast(EvaluateResponse, result).result == {
1026
- "additionalFees": 0,
1027
- "baseDuty": 180,
1028
- "countryAdjustment": 225,
1029
- "dutyRate": 0.1875,
1030
- "minDuty": 225,
1031
- "preferentialDiscount": 225,
1032
- "totalDuty": 225,
1033
- }
1034
-
1035
-
1036
- def test_evaluate_rule_with_cellular_data_rollover_system_rule():
1037
- cellular_data_rollover_system_path = (
1038
- Path(__file__).parent / "test_data" / "cellular_data_rollover_system.json"
1039
- )
1040
- with open(cellular_data_rollover_system_path, "r", encoding="utf-8") as f:
1041
- jdm_dict = json.load(f)
1042
-
1043
- input_data = {
1044
- "plan": {
1045
- "type": "premium",
1046
- "monthlyDataAllowance": 50,
1047
- "rolloverEligible": True,
1048
- },
1049
- "currentBillingCycle": {
1050
- "dataUsed": 35,
1051
- "consecutiveRollovers": 1,
1052
- "rolloverData": 5,
1053
- },
1054
- }
1055
-
1056
- result = evaluate_rule(JDMGraph.model_validate(jdm_dict), input_data)
1057
-
1058
- assert result.success
1059
- assert cast(EvaluateResponse, result).result == {
1060
- "nextBillingCycle": {"consecutiveRollovers": 2, "status": "approved"},
1061
- "responseMessage": "Rollover successful. You have 20 GB of rollover data available for your next billing cycle.",
1062
- }
1063
-
1064
-
1065
- def test_evaluate_rule_with_online_check_in_eligibility_system_rule():
1066
- online_check_in_eligibility_system_path = (
1067
- Path(__file__).parent / "test_data" / "online_check_in_eligibility_system.json"
1068
- )
1069
- with open(online_check_in_eligibility_system_path, "r", encoding="utf-8") as f:
1070
- jdm_dict = json.load(f)
1071
-
1072
- input_data = {
1073
- "passenger": {
1074
- "id": "P12345678",
1075
- "name": "John Smith",
1076
- "hasValidPassport": True,
1077
- "hasValidVisa": True,
1078
- "requiresSpecialAssistance": False,
1079
- "frequentFlyerStatus": "gold",
1080
- },
1081
- "flight": {
1082
- "flightNumber": "BA123",
1083
- "departureTime": "2025-03-20T10:30:00Z",
1084
- "origin": "LHR",
1085
- "destination": "JFK",
1086
- "requiresVisa": True,
1087
- "hasSeatSelection": True,
1088
- "allowsExtraBaggage": True,
1089
- "hasSpecialMealOptions": True,
1090
- },
1091
- }
1092
-
1093
- result = evaluate_rule(JDMGraph.model_validate(jdm_dict), input_data)
1094
-
1095
- assert result.success
1096
- assert cast(EvaluateResponse, result).result == {
1097
- "canAddBaggage": True,
1098
- "canSelectSeat": True,
1099
- "isEligible": False,
1100
- "message": "You are eligible for online check-in.",
1101
- "statusCode": "eligible",
1102
- }
1103
-
1104
-
1105
- def test_evaluate_rule_with_warehouse_cross_docking_rule():
1106
- warehouse_cross_docking_path = (
1107
- Path(__file__).parent / "test_data" / "warehouse_cross_docking.json"
1108
- )
1109
- with open(warehouse_cross_docking_path, "r", encoding="utf-8") as f:
1110
- jdm_dict = json.load(f)
1111
-
1112
- input_data = {
1113
- "inboundShipmentId": "IN-12345",
1114
- "inboundShipmentTime": "2025-03-19T10:00:00Z",
1115
- "outboundShipmentTime": "2025-03-20T09:00:00Z",
1116
- "matchingOutboundOrders": [
1117
- {
1118
- "orderId": "ORD-789",
1119
- "customerPriority": "standard",
1120
- "destinationZone": "East",
1121
- },
1122
- {
1123
- "orderId": "ORD-790",
1124
- "customerPriority": "premium",
1125
- "destinationZone": "East",
1126
- },
1127
- ],
1128
- "inboundShipmentItems": [
1129
- {"sku": "ITEM-001", "quantity": 50, "category": "Electronics"},
1130
- {"sku": "ITEM-002", "quantity": 30, "category": "Home Goods"},
1131
- ],
1132
- "currentStorageUsed": 7500,
1133
- "totalStorageCapacity": 10000,
1134
- "crossDockingBayAssignment": "Bay-E4",
1135
- }
1136
-
1137
- result = evaluate_rule(JDMGraph.model_validate(jdm_dict), input_data)
1138
-
1139
- assert result.success
1140
- assert cast(EvaluateResponse, result).result == {
1141
- "crossDockDecision": "cross-dock",
1142
- "crossDockingBayAssignment": "Bay-E4",
1143
- "currentStorageUsed": 7500,
1144
- "decisionReason": "Matching orders available within 48 hours",
1145
- "dockingBay": "Bay-E4",
1146
- "estimatedProcessingTime": 30,
1147
- "hasMatchingOutboundOrders": True,
1148
- "inboundShipmentId": "IN-12345",
1149
- "inboundShipmentItems": [
1150
- {"category": "Electronics", "quantity": 50, "sku": "ITEM-001"},
1151
- {"category": "Home Goods", "quantity": 30, "sku": "ITEM-002"},
1152
- ],
1153
- "inboundShipmentTime": "2025-03-19T10:00:00Z",
1154
- "matchingOutboundOrders": [
1155
- {
1156
- "customerPriority": "standard",
1157
- "destinationZone": "East",
1158
- "orderId": "ORD-789",
1159
- },
1160
- {
1161
- "customerPriority": "premium",
1162
- "destinationZone": "East",
1163
- "orderId": "ORD-790",
1164
- },
1165
- ],
1166
- "outboundShipmentTime": "2025-03-20T09:00:00Z",
1167
- "priority": "normal",
1168
- "timeDifferenceHours": 23,
1169
- "totalStorageCapacity": 10000,
1170
- "warehouseCapacityPercentage": 75,
1171
- }
1172
-
1173
-
1174
- def test_evaluate_rule_with_booking_fraud_detection_rule():
1175
- booking_fraud_detection_path = (
1176
- Path(__file__).parent / "test_data" / "booking_fraud_detection.json"
1177
- )
1178
- with open(booking_fraud_detection_path, "r", encoding="utf-8") as f:
1179
- jdm_dict = json.load(f)
1180
-
1181
- input_data = {
1182
- "booking": {
1183
- "payment_method": "prepaid_card",
1184
- "amount": 2500,
1185
- "ip_country": "US",
1186
- },
1187
- "account": {"country": "US", "bookings_last_24h": 6},
1188
- }
1189
-
1190
- result = evaluate_rule(JDMGraph.model_validate(jdm_dict), input_data)
1191
-
1192
- assert result.success
1193
- assert cast(EvaluateResponse, result).result == {
1194
- "flags": {"manual_review": True, "requires_verification": True},
1195
- }
1196
-
1197
-
1198
- def test_evaluate_rule_with_applicant_risk_assessment_rule():
1199
- applicant_risk_assessment_path = (
1200
- Path(__file__).parent / "test_data" / "applicant_risk_assessment.json"
1201
- )
1202
- with open(applicant_risk_assessment_path, "r", encoding="utf-8") as f:
1203
- jdm_dict = json.load(f)
1204
-
1205
- input_data = {
1206
- "applicant": {
1207
- "creditScore": 710,
1208
- "latePayments": 1,
1209
- "creditHistoryMonths": 48,
1210
- "employmentMonths": 36,
1211
- "incomeVerification": "complete",
1212
- "bankAccountStanding": "good",
1213
- "debtToIncomeRatio": 0.35,
1214
- "outstandingLoans": 2,
1215
- }
1216
- }
1217
-
1218
- result = evaluate_rule(JDMGraph.model_validate(jdm_dict), input_data)
1219
-
1220
- assert result.success
1221
- assert cast(EvaluateResponse, result).result == {
1222
- "applicant": {
1223
- "bankAccountStanding": "good",
1224
- "creditHistoryMonths": 48,
1225
- "creditScore": 710,
1226
- "debtToIncomeRatio": 0.35,
1227
- "employmentMonths": 36,
1228
- "incomeVerification": "complete",
1229
- "latePayments": 1,
1230
- "outstandingLoans": 2,
1231
- },
1232
- "approvalStatus": "manual-review",
1233
- "interestRateModifier": 0,
1234
- "negativeFactors": [],
1235
- "negativeFactorsCount": 0,
1236
- "riskCategory": "medium",
1237
- "scores": {"creditHistory": 20, "debtToIncome": 15, "incomeStability": 20},
1238
- "totalRiskScore": 55,
1239
- }
1240
-
1241
-
1242
- def test_evaluate_rule_with_insurance_prior_authorization_rule():
1243
- insurance_prior_authorization_path = (
1244
- Path(__file__).parent / "test_data" / "insurance_prior_authorization.json"
1245
- )
1246
- with open(insurance_prior_authorization_path, "r", encoding="utf-8") as f:
1247
- jdm_dict = json.load(f)
1248
-
1249
- input_data = {
1250
- "patientInfo": {"insuranceType": "Commercial"},
1251
- "diagnosisCodes": ["M54.5", "M51.26"],
1252
- "serviceType": "Imaging",
1253
- "serviceDetails": {
1254
- "code": "70551",
1255
- "cost": 1200,
1256
- "isEmergency": False,
1257
- },
1258
- }
1259
-
1260
- result = evaluate_rule(JDMGraph.model_validate(jdm_dict), input_data)
1261
-
1262
- assert result.success
1263
- assert cast(EvaluateResponse, result).result["requiresAuthorization"]
1264
- assert (
1265
- cast(EvaluateResponse, result).result["reason"]
1266
- == "Advanced imaging requires prior authorization"
1267
- )
1268
-
1269
-
1270
- def test_evaluate_rule_with_portfolio_risk_monitor_rule():
1271
- portfolio_risk_monitor_path = (
1272
- Path(__file__).parent / "test_data" / "portfolio_risk_monitor.json"
1273
- )
1274
- with open(portfolio_risk_monitor_path, "r", encoding="utf-8") as f:
1275
- jdm_dict = json.load(f)
1276
-
1277
- input_data = {
1278
- "customer": {
1279
- "id": "cust-78945",
1280
- "name": "John Smith",
1281
- "riskTolerance": "moderate",
1282
- "investmentHorizon": "long-term",
1283
- "preferences": {
1284
- "allowAutomaticAdjustments": True,
1285
- "alertThreshold": "moderate",
1286
- "communicationPreference": "email",
1287
- },
1288
- },
1289
- "portfolio": {
1290
- "id": "port-12345",
1291
- "name": "Retirement Portfolio",
1292
- "totalValue": 750000,
1293
- "creationDate": "2019-05-12",
1294
- "lastRebalance": 95,
1295
- "volatility": 22.5,
1296
- "highRiskPercentage": 35,
1297
- "currentAllocation": {"equity": 65, "bonds": 25, "cash": 10},
1298
- "targetAllocation": {"equity": 60, "bonds": 35, "cash": 5},
1299
- "holdings": [
1300
- {
1301
- "symbol": "VTI",
1302
- "category": "equity",
1303
- "percentage": 30,
1304
- "value": 225000,
1305
- },
1306
- {
1307
- "symbol": "VXUS",
1308
- "category": "equity",
1309
- "percentage": 20,
1310
- "value": 150000,
1311
- },
1312
- {
1313
- "symbol": "VGT",
1314
- "category": "equity",
1315
- "percentage": 15,
1316
- "value": 112500,
1317
- },
1318
- {
1319
- "symbol": "BND",
1320
- "category": "bonds",
1321
- "percentage": 25,
1322
- "value": 187500,
1323
- },
1324
- {
1325
- "symbol": "CASH",
1326
- "category": "cash",
1327
- "percentage": 10,
1328
- "value": 75000,
1329
- },
1330
- ],
1331
- },
1332
- "market": {
1333
- "volatilityIndex": 28.5,
1334
- "trendPercentage": -12.5,
1335
- "interestRate": 3.75,
1336
- "sectorPerformance": {
1337
- "technology": -15.2,
1338
- "healthcare": -5.1,
1339
- "financials": -18.4,
1340
- "consumerStaples": -3.2,
1341
- "utilities": 1.5,
1342
- },
1343
- "economicIndicators": {
1344
- "gdpGrowth": 0.8,
1345
- "inflation": 4.2,
1346
- "unemploymentRate": 4.1,
1347
- },
1348
- },
1349
- }
1350
-
1351
- result = evaluate_rule(JDMGraph.model_validate(jdm_dict), input_data)
1352
-
1353
- assert result.success
1354
-
1355
- assert cast(EvaluateResponse, result).result["action"] == "rebalance"
1356
- assert cast(EvaluateResponse, result).result["outcome"] == {
1357
- "riskScore": 0.53,
1358
- "status": "rebalance_suggested",
1359
- "timestamp": cast(EvaluateResponse, result).result["outcome"]["timestamp"],
1360
- }
1361
-
1362
- assert cast(EvaluateResponse, result).result["rebalanceDetails"] == {
1363
- "currentAllocation": {"bonds": 25, "cash": 10, "equity": 65},
1364
- "customerId": "cust-78945",
1365
- "date": cast(EvaluateResponse, result).result["rebalanceDetails"]["date"],
1366
- "driftPercentage": "5.0",
1367
- "message": "Rebalancing recommended: Portfolio has drifted 5.0% from target allocation.",
1368
- "portfolioId": "port-12345",
1369
- "riskCategory": "high",
1370
- "riskScore": 0.53,
1371
- "suggestedChanges": {"bonds": 10, "cash": -5, "equity": -5},
1372
- "targetAllocation": {"bonds": 35, "cash": 5, "equity": 60},
1373
- }
1374
-
1375
-
1376
- def test_evaluate_rule_with_order_consolidation_system_rule():
1377
- order_consolidation_system_path = (
1378
- Path(__file__).parent / "test_data" / "order_consolidation_system.json"
1379
- )
1380
- with open(order_consolidation_system_path, "r", encoding="utf-8") as f:
1381
- jdm_dict = json.load(f)
1382
-
1383
- input_data = {
1384
- "orders": [
1385
- {
1386
- "orderId": "ORD-12345",
1387
- "customerName": "John Smith",
1388
- "deliveryAddress": {
1389
- "street": "123 Main St",
1390
- "city": "Springfield",
1391
- "state": "IL",
1392
- "zipCode": "62704",
1393
- "coordinates": {"latitude": 39.7817, "longitude": -89.6501},
1394
- },
1395
- "requestedDeliveryDate": "2025-03-25T14:00:00Z",
1396
- "orderWeight": 12.5,
1397
- "orderItems": 3,
1398
- },
1399
- {
1400
- "orderId": "ORD-12346",
1401
- "customerName": "Jane Doe",
1402
- "deliveryAddress": {
1403
- "street": "456 Oak Ave",
1404
- "city": "Springfield",
1405
- "state": "IL",
1406
- "zipCode": "62702",
1407
- "coordinates": {"latitude": 39.8021, "longitude": -89.6443},
1408
- },
1409
- "requestedDeliveryDate": "2025-03-25T16:00:00Z",
1410
- "orderWeight": 8.2,
1411
- "orderItems": 2,
1412
- },
1413
- ],
1414
- "distanceKm": 35,
1415
- "deliveryWindowDifferenceHours": 24,
1416
- "availableCarrierCapacity": 4,
1417
- "orderWeight1": 12.5,
1418
- "orderWeight2": 8.2,
1419
- "carrierDetails": {
1420
- "carrierId": "CAR-789",
1421
- "maxCapacity": 500,
1422
- "currentLoad": 320,
1423
- },
1424
- }
1425
-
1426
- result = evaluate_rule(JDMGraph.model_validate(jdm_dict), input_data)
1427
-
1428
- assert result.success
1429
-
1430
- assert cast(EvaluateResponse, result).result["canConsolidate"]
1431
- assert cast(EvaluateResponse, result).result["schedulingPriority"] == "high"
1432
- assert cast(EvaluateResponse, result).result["availableCarrierCapacity"] == 4
1433
- assert cast(EvaluateResponse, result).result["carrierDetails"] == {
1434
- "carrierId": "CAR-789",
1435
- "currentLoad": 320,
1436
- "maxCapacity": 500,
1437
- }
1438
- assert (
1439
- cast(EvaluateResponse, result).result["consolidationAction"]
1440
- == "immediate_consolidation"
1441
- )
1442
- assert cast(EvaluateResponse, result).result["consolidationPriority"] == "high"
1443
- assert cast(EvaluateResponse, result).result["consolidationWeight"] == 20.7
1444
- assert cast(EvaluateResponse, result).result["costSavingEstimate"] == 23.75
1445
- assert cast(EvaluateResponse, result).result["costSavingsReport"] == {
1446
- "fuelSavings": 5.25,
1447
- "laborSavings": 23.75,
1448
- "totalSavings": 29,
1449
- }
1450
-
1451
- assert cast(EvaluateResponse, result).result["deliverySchedule"] == {
1452
- "estimatedDeliveryTime": None,
1453
- "notificationRequired": False,
1454
- "type": "consolidated",
1455
- }
1456
- assert cast(EvaluateResponse, result).result["deliveryWindowDifferenceHours"] == 24
1457
- assert cast(EvaluateResponse, result).result["distanceKm"] == 35
1458
- assert cast(EvaluateResponse, result).result["expectedFuelSavings"] == 5.25
1459
- assert (
1460
- cast(EvaluateResponse, result).result["explanation"]
1461
- == "Orders are nearby, delivery window compatible, and carrier has capacity"
1462
- )
1463
- assert cast(EvaluateResponse, result).result["orderWeight1"] == 12.5
1464
- assert cast(EvaluateResponse, result).result["orderWeight2"] == 8.2
1465
- assert cast(EvaluateResponse, result).result["orders"] == [
1466
- {
1467
- "customerName": "John Smith",
1468
- "deliveryAddress": {
1469
- "city": "Springfield",
1470
- "coordinates": {"latitude": 39.7817, "longitude": -89.6501},
1471
- "state": "IL",
1472
- "street": "123 Main St",
1473
- "zipCode": "62704",
1474
- },
1475
- "orderId": "ORD-12345",
1476
- "orderItems": 3,
1477
- "orderWeight": 12.5,
1478
- "requestedDeliveryDate": "2025-03-25T14:00:00Z",
1479
- },
1480
- {
1481
- "customerName": "Jane Doe",
1482
- "deliveryAddress": {
1483
- "city": "Springfield",
1484
- "coordinates": {"latitude": 39.8021, "longitude": -89.6443},
1485
- "state": "IL",
1486
- "street": "456 Oak Ave",
1487
- "zipCode": "62702",
1488
- },
1489
- "orderId": "ORD-12346",
1490
- "orderItems": 2,
1491
- "orderWeight": 8.2,
1492
- "requestedDeliveryDate": "2025-03-25T16:00:00Z",
1493
- },
1494
- ]