planar 0.5.0__py3-none-any.whl → 0.8.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 (211) hide show
  1. planar/_version.py +1 -1
  2. planar/ai/agent.py +155 -283
  3. planar/ai/agent_base.py +170 -0
  4. planar/ai/agent_utils.py +7 -0
  5. planar/ai/pydantic_ai.py +638 -0
  6. planar/ai/test_agent_serialization.py +1 -1
  7. planar/app.py +64 -20
  8. planar/cli.py +39 -27
  9. planar/config.py +45 -36
  10. planar/db/db.py +2 -1
  11. planar/files/storage/azure_blob.py +343 -0
  12. planar/files/storage/base.py +7 -0
  13. planar/files/storage/config.py +70 -7
  14. planar/files/storage/s3.py +6 -6
  15. planar/files/storage/test_azure_blob.py +435 -0
  16. planar/logging/formatter.py +17 -4
  17. planar/logging/test_formatter.py +327 -0
  18. planar/registry_items.py +2 -1
  19. planar/routers/agents_router.py +3 -1
  20. planar/routers/files.py +11 -2
  21. planar/routers/models.py +14 -1
  22. planar/routers/test_agents_router.py +1 -1
  23. planar/routers/test_files_router.py +49 -0
  24. planar/routers/test_routes_security.py +5 -7
  25. planar/routers/test_workflow_router.py +270 -3
  26. planar/routers/workflow.py +95 -36
  27. planar/rules/models.py +36 -39
  28. planar/rules/test_data/account_dormancy_management.json +223 -0
  29. planar/rules/test_data/airline_loyalty_points_calculator.json +262 -0
  30. planar/rules/test_data/applicant_risk_assessment.json +435 -0
  31. planar/rules/test_data/booking_fraud_detection.json +407 -0
  32. planar/rules/test_data/cellular_data_rollover_system.json +258 -0
  33. planar/rules/test_data/clinical_trial_eligibility_screener.json +437 -0
  34. planar/rules/test_data/customer_lifetime_value.json +143 -0
  35. planar/rules/test_data/import_duties_calculator.json +289 -0
  36. planar/rules/test_data/insurance_prior_authorization.json +443 -0
  37. planar/rules/test_data/online_check_in_eligibility_system.json +254 -0
  38. planar/rules/test_data/order_consolidation_system.json +375 -0
  39. planar/rules/test_data/portfolio_risk_monitor.json +471 -0
  40. planar/rules/test_data/supply_chain_risk.json +253 -0
  41. planar/rules/test_data/warehouse_cross_docking.json +237 -0
  42. planar/rules/test_rules.py +750 -6
  43. planar/scaffold_templates/planar.dev.yaml.j2 +6 -6
  44. planar/scaffold_templates/planar.prod.yaml.j2 +9 -5
  45. planar/scaffold_templates/pyproject.toml.j2 +1 -1
  46. planar/security/auth_context.py +21 -0
  47. planar/security/{jwt_middleware.py → auth_middleware.py} +70 -17
  48. planar/security/authorization.py +9 -15
  49. planar/security/tests/test_auth_middleware.py +162 -0
  50. planar/sse/proxy.py +4 -9
  51. planar/test_app.py +92 -1
  52. planar/test_cli.py +81 -59
  53. planar/test_config.py +17 -14
  54. planar/testing/fixtures.py +325 -0
  55. planar/testing/planar_test_client.py +5 -2
  56. planar/utils.py +41 -1
  57. planar/workflows/execution.py +1 -1
  58. planar/workflows/orchestrator.py +5 -0
  59. planar/workflows/serialization.py +12 -6
  60. planar/workflows/step_core.py +3 -1
  61. planar/workflows/test_serialization.py +9 -1
  62. {planar-0.5.0.dist-info → planar-0.8.0.dist-info}/METADATA +30 -5
  63. planar-0.8.0.dist-info/RECORD +166 -0
  64. planar/.__init__.py.un~ +0 -0
  65. planar/._version.py.un~ +0 -0
  66. planar/.app.py.un~ +0 -0
  67. planar/.cli.py.un~ +0 -0
  68. planar/.config.py.un~ +0 -0
  69. planar/.context.py.un~ +0 -0
  70. planar/.db.py.un~ +0 -0
  71. planar/.di.py.un~ +0 -0
  72. planar/.engine.py.un~ +0 -0
  73. planar/.files.py.un~ +0 -0
  74. planar/.log_context.py.un~ +0 -0
  75. planar/.log_metadata.py.un~ +0 -0
  76. planar/.logging.py.un~ +0 -0
  77. planar/.object_registry.py.un~ +0 -0
  78. planar/.otel.py.un~ +0 -0
  79. planar/.server.py.un~ +0 -0
  80. planar/.session.py.un~ +0 -0
  81. planar/.sqlalchemy.py.un~ +0 -0
  82. planar/.task_local.py.un~ +0 -0
  83. planar/.test_app.py.un~ +0 -0
  84. planar/.test_config.py.un~ +0 -0
  85. planar/.test_object_config.py.un~ +0 -0
  86. planar/.test_sqlalchemy.py.un~ +0 -0
  87. planar/.test_utils.py.un~ +0 -0
  88. planar/.util.py.un~ +0 -0
  89. planar/.utils.py.un~ +0 -0
  90. planar/ai/.__init__.py.un~ +0 -0
  91. planar/ai/._models.py.un~ +0 -0
  92. planar/ai/.agent.py.un~ +0 -0
  93. planar/ai/.agent_utils.py.un~ +0 -0
  94. planar/ai/.events.py.un~ +0 -0
  95. planar/ai/.files.py.un~ +0 -0
  96. planar/ai/.models.py.un~ +0 -0
  97. planar/ai/.providers.py.un~ +0 -0
  98. planar/ai/.pydantic_ai.py.un~ +0 -0
  99. planar/ai/.pydantic_ai_agent.py.un~ +0 -0
  100. planar/ai/.pydantic_ai_provider.py.un~ +0 -0
  101. planar/ai/.step.py.un~ +0 -0
  102. planar/ai/.test_agent.py.un~ +0 -0
  103. planar/ai/.test_agent_serialization.py.un~ +0 -0
  104. planar/ai/.test_providers.py.un~ +0 -0
  105. planar/ai/.utils.py.un~ +0 -0
  106. planar/ai/providers.py +0 -1088
  107. planar/ai/test_agent.py +0 -1298
  108. planar/ai/test_providers.py +0 -463
  109. planar/db/.db.py.un~ +0 -0
  110. planar/files/.config.py.un~ +0 -0
  111. planar/files/.local.py.un~ +0 -0
  112. planar/files/.local_filesystem.py.un~ +0 -0
  113. planar/files/.model.py.un~ +0 -0
  114. planar/files/.models.py.un~ +0 -0
  115. planar/files/.s3.py.un~ +0 -0
  116. planar/files/.storage.py.un~ +0 -0
  117. planar/files/.test_files.py.un~ +0 -0
  118. planar/files/storage/.__init__.py.un~ +0 -0
  119. planar/files/storage/.base.py.un~ +0 -0
  120. planar/files/storage/.config.py.un~ +0 -0
  121. planar/files/storage/.context.py.un~ +0 -0
  122. planar/files/storage/.local_directory.py.un~ +0 -0
  123. planar/files/storage/.test_local_directory.py.un~ +0 -0
  124. planar/files/storage/.test_s3.py.un~ +0 -0
  125. planar/human/.human.py.un~ +0 -0
  126. planar/human/.test_human.py.un~ +0 -0
  127. planar/logging/.__init__.py.un~ +0 -0
  128. planar/logging/.attributes.py.un~ +0 -0
  129. planar/logging/.formatter.py.un~ +0 -0
  130. planar/logging/.logger.py.un~ +0 -0
  131. planar/logging/.otel.py.un~ +0 -0
  132. planar/logging/.tracer.py.un~ +0 -0
  133. planar/modeling/.mixin.py.un~ +0 -0
  134. planar/modeling/.storage.py.un~ +0 -0
  135. planar/modeling/orm/.planar_base_model.py.un~ +0 -0
  136. planar/object_config/.object_config.py.un~ +0 -0
  137. planar/routers/.__init__.py.un~ +0 -0
  138. planar/routers/.agents_router.py.un~ +0 -0
  139. planar/routers/.crud.py.un~ +0 -0
  140. planar/routers/.decision.py.un~ +0 -0
  141. planar/routers/.event.py.un~ +0 -0
  142. planar/routers/.file_attachment.py.un~ +0 -0
  143. planar/routers/.files.py.un~ +0 -0
  144. planar/routers/.files_router.py.un~ +0 -0
  145. planar/routers/.human.py.un~ +0 -0
  146. planar/routers/.info.py.un~ +0 -0
  147. planar/routers/.models.py.un~ +0 -0
  148. planar/routers/.object_config_router.py.un~ +0 -0
  149. planar/routers/.rule.py.un~ +0 -0
  150. planar/routers/.test_object_config_router.py.un~ +0 -0
  151. planar/routers/.test_workflow_router.py.un~ +0 -0
  152. planar/routers/.workflow.py.un~ +0 -0
  153. planar/rules/.decorator.py.un~ +0 -0
  154. planar/rules/.runner.py.un~ +0 -0
  155. planar/rules/.test_rules.py.un~ +0 -0
  156. planar/security/.jwt_middleware.py.un~ +0 -0
  157. planar/sse/.constants.py.un~ +0 -0
  158. planar/sse/.example.html.un~ +0 -0
  159. planar/sse/.hub.py.un~ +0 -0
  160. planar/sse/.model.py.un~ +0 -0
  161. planar/sse/.proxy.py.un~ +0 -0
  162. planar/testing/.client.py.un~ +0 -0
  163. planar/testing/.memory_storage.py.un~ +0 -0
  164. planar/testing/.planar_test_client.py.un~ +0 -0
  165. planar/testing/.predictable_tracer.py.un~ +0 -0
  166. planar/testing/.synchronizable_tracer.py.un~ +0 -0
  167. planar/testing/.test_memory_storage.py.un~ +0 -0
  168. planar/testing/.workflow_observer.py.un~ +0 -0
  169. planar/workflows/.__init__.py.un~ +0 -0
  170. planar/workflows/.builtin_steps.py.un~ +0 -0
  171. planar/workflows/.concurrency_tracing.py.un~ +0 -0
  172. planar/workflows/.context.py.un~ +0 -0
  173. planar/workflows/.contrib.py.un~ +0 -0
  174. planar/workflows/.decorators.py.un~ +0 -0
  175. planar/workflows/.durable_test.py.un~ +0 -0
  176. planar/workflows/.errors.py.un~ +0 -0
  177. planar/workflows/.events.py.un~ +0 -0
  178. planar/workflows/.exceptions.py.un~ +0 -0
  179. planar/workflows/.execution.py.un~ +0 -0
  180. planar/workflows/.human.py.un~ +0 -0
  181. planar/workflows/.lock.py.un~ +0 -0
  182. planar/workflows/.misc.py.un~ +0 -0
  183. planar/workflows/.model.py.un~ +0 -0
  184. planar/workflows/.models.py.un~ +0 -0
  185. planar/workflows/.notifications.py.un~ +0 -0
  186. planar/workflows/.orchestrator.py.un~ +0 -0
  187. planar/workflows/.runtime.py.un~ +0 -0
  188. planar/workflows/.serialization.py.un~ +0 -0
  189. planar/workflows/.step.py.un~ +0 -0
  190. planar/workflows/.step_core.py.un~ +0 -0
  191. planar/workflows/.sub_workflow_runner.py.un~ +0 -0
  192. planar/workflows/.sub_workflow_scheduler.py.un~ +0 -0
  193. planar/workflows/.test_concurrency.py.un~ +0 -0
  194. planar/workflows/.test_concurrency_detection.py.un~ +0 -0
  195. planar/workflows/.test_human.py.un~ +0 -0
  196. planar/workflows/.test_lock_timeout.py.un~ +0 -0
  197. planar/workflows/.test_orchestrator.py.un~ +0 -0
  198. planar/workflows/.test_race_conditions.py.un~ +0 -0
  199. planar/workflows/.test_serialization.py.un~ +0 -0
  200. planar/workflows/.test_suspend_deserialization.py.un~ +0 -0
  201. planar/workflows/.test_workflow.py.un~ +0 -0
  202. planar/workflows/.tracing.py.un~ +0 -0
  203. planar/workflows/.types.py.un~ +0 -0
  204. planar/workflows/.util.py.un~ +0 -0
  205. planar/workflows/.utils.py.un~ +0 -0
  206. planar/workflows/.workflow.py.un~ +0 -0
  207. planar/workflows/.workflow_wrapper.py.un~ +0 -0
  208. planar/workflows/.wrappers.py.un~ +0 -0
  209. planar-0.5.0.dist-info/RECORD +0 -289
  210. {planar-0.5.0.dist-info → planar-0.8.0.dist-info}/WHEEL +0 -0
  211. {planar-0.5.0.dist-info → planar-0.8.0.dist-info}/entry_points.txt +0 -0
@@ -1,7 +1,9 @@
1
1
  import json
2
2
  from datetime import datetime, timezone
3
3
  from enum import Enum
4
- from typing import Any, Dict
4
+ from operator import itemgetter
5
+ from pathlib import Path
6
+ from typing import Any, Dict, cast
5
7
  from unittest.mock import patch
6
8
  from uuid import UUID
7
9
 
@@ -704,11 +706,9 @@ async def test_create_jdm_graph():
704
706
  col for col in output_columns if col.field == "discount_reason"
705
707
  )
706
708
 
707
- assert getattr(rule_values, final_price_col.id) == "0" # number default
708
- assert getattr(rule_values, discount_applied_col.id) == "0" # number default
709
- assert (
710
- getattr(rule_values, discount_reason_col.id) == '"default value"'
711
- ) # string default
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
712
 
713
713
  # Verify input and output nodes have proper schemas
714
714
  input_node = next(node for node in jdm_graph.nodes if node.type == "inputNode")
@@ -748,3 +748,747 @@ async def test_jdm_graph_evaluation():
748
748
  assert result.result["final_price"] == 0.0
749
749
  assert result.result["discount_applied"] == 0.0
750
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
+ ]