nat-engine 1__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 (299) hide show
  1. mannf/__init__.py +33 -0
  2. mannf/__main__.py +10 -0
  3. mannf/_version.py +8 -0
  4. mannf/agents/__init__.py +7 -0
  5. mannf/agents/analyzer_agent.py +9 -0
  6. mannf/agents/base.py +9 -0
  7. mannf/agents/bdi_agent.py +9 -0
  8. mannf/agents/belief_state.py +9 -0
  9. mannf/agents/coordinator_agent.py +9 -0
  10. mannf/agents/executor_agent.py +9 -0
  11. mannf/agents/monitor_agent.py +9 -0
  12. mannf/agents/oracle_agent.py +9 -0
  13. mannf/agents/planner_agent.py +9 -0
  14. mannf/agents/test_agent.py +9 -0
  15. mannf/anomaly/__init__.py +7 -0
  16. mannf/anomaly/enhanced_detector.py +9 -0
  17. mannf/cli.py +9 -0
  18. mannf/core/__init__.py +26 -0
  19. mannf/core/agents/__init__.py +52 -0
  20. mannf/core/agents/accessibility_scanner_agent.py +245 -0
  21. mannf/core/agents/analyzer_agent.py +224 -0
  22. mannf/core/agents/autonomous_loop_agent.py +1086 -0
  23. mannf/core/agents/autonomous_loop_models.py +62 -0
  24. mannf/core/agents/autonomous_run_differ.py +427 -0
  25. mannf/core/agents/base.py +128 -0
  26. mannf/core/agents/bdi_agent.py +330 -0
  27. mannf/core/agents/belief_state.py +202 -0
  28. mannf/core/agents/browser_coordinator_agent.py +224 -0
  29. mannf/core/agents/browser_executor_agent.py +410 -0
  30. mannf/core/agents/coordinator_agent.py +262 -0
  31. mannf/core/agents/executor_agent.py +222 -0
  32. mannf/core/agents/monitor_agent.py +188 -0
  33. mannf/core/agents/oracle_agent.py +150 -0
  34. mannf/core/agents/performance_testing_agent.py +279 -0
  35. mannf/core/agents/planner_agent.py +128 -0
  36. mannf/core/agents/test_agent.py +249 -0
  37. mannf/core/agents/visual_regression_agent.py +311 -0
  38. mannf/core/agents/web_crawler_agent.py +510 -0
  39. mannf/core/agents/worker_pool.py +366 -0
  40. mannf/core/anomaly/__init__.py +14 -0
  41. mannf/core/anomaly/enhanced_detector.py +541 -0
  42. mannf/core/browser/__init__.py +63 -0
  43. mannf/core/browser/accessibility_scanner.py +424 -0
  44. mannf/core/browser/discovery_model.py +178 -0
  45. mannf/core/browser/dom_snapshot.py +349 -0
  46. mannf/core/browser/ingestor_bridge.py +371 -0
  47. mannf/core/browser/performance_metrics.py +217 -0
  48. mannf/core/browser/reflection_analyzer.py +442 -0
  49. mannf/core/browser/scenario_generator.py +1100 -0
  50. mannf/core/browser/security_scenario_generator.py +695 -0
  51. mannf/core/browser/visual_comparer.py +159 -0
  52. mannf/core/diagnostics/__init__.py +28 -0
  53. mannf/core/diagnostics/failure_clusterer.py +211 -0
  54. mannf/core/diagnostics/flake_detector.py +233 -0
  55. mannf/core/diagnostics/root_cause_analyzer.py +273 -0
  56. mannf/core/distributed/__init__.py +16 -0
  57. mannf/core/distributed/endpoint.py +139 -0
  58. mannf/core/distributed/system_under_test.py +207 -0
  59. mannf/core/functional_orchestrator.py +428 -0
  60. mannf/core/messaging/__init__.py +11 -0
  61. mannf/core/messaging/bus.py +113 -0
  62. mannf/core/messaging/messages.py +89 -0
  63. mannf/core/nat_orchestrator.py +342 -0
  64. mannf/core/neural/__init__.py +183 -0
  65. mannf/core/orchestrator.py +272 -0
  66. mannf/core/prioritization/__init__.py +17 -0
  67. mannf/core/prioritization/adaptive_controller.py +509 -0
  68. mannf/core/prioritization/belief_prioritizer.py +231 -0
  69. mannf/core/prioritization/risk_scorer.py +430 -0
  70. mannf/core/reporting/__init__.py +12 -0
  71. mannf/core/reporting/unified_report.py +664 -0
  72. mannf/core/testing/__init__.py +17 -0
  73. mannf/core/testing/adaptive_controller.py +149 -0
  74. mannf/core/testing/models.py +179 -0
  75. mannf/core/validation/__init__.py +10 -0
  76. mannf/core/validation/self_validation_runner.py +180 -0
  77. mannf/dashboard/__init__.py +7 -0
  78. mannf/dashboard/app.py +9 -0
  79. mannf/dashboard/models.py +9 -0
  80. mannf/dashboard/static/index.html +2538 -0
  81. mannf/dashboard/telemetry.py +9 -0
  82. mannf/distributed/__init__.py +7 -0
  83. mannf/distributed/endpoint.py +9 -0
  84. mannf/distributed/system_under_test.py +9 -0
  85. mannf/healing/__init__.py +7 -0
  86. mannf/healing/graphql_schema_diff.py +9 -0
  87. mannf/healing/healer.py +9 -0
  88. mannf/healing/models.py +9 -0
  89. mannf/healing/schema_diff.py +9 -0
  90. mannf/integrations/__init__.py +7 -0
  91. mannf/integrations/auth.py +9 -0
  92. mannf/integrations/graphql_parser.py +9 -0
  93. mannf/integrations/graphql_sut.py +9 -0
  94. mannf/integrations/http_sut.py +9 -0
  95. mannf/integrations/openapi_parser.py +9 -0
  96. mannf/integrations/postman_parser.py +9 -0
  97. mannf/llm/__init__.py +7 -0
  98. mannf/llm/anthropic_provider.py +9 -0
  99. mannf/llm/base.py +9 -0
  100. mannf/llm/config.py +9 -0
  101. mannf/llm/factory.py +9 -0
  102. mannf/llm/openai_provider.py +9 -0
  103. mannf/llm/prompts.py +9 -0
  104. mannf/messaging/__init__.py +7 -0
  105. mannf/messaging/bus.py +9 -0
  106. mannf/messaging/messages.py +9 -0
  107. mannf/nat_orchestrator.py +9 -0
  108. mannf/neural/__init__.py +7 -0
  109. mannf/orchestrator.py +9 -0
  110. mannf/prioritization/__init__.py +7 -0
  111. mannf/prioritization/adaptive_controller.py +9 -0
  112. mannf/prioritization/belief_prioritizer.py +9 -0
  113. mannf/prioritization/risk_scorer.py +9 -0
  114. mannf/product/__init__.py +29 -0
  115. mannf/product/admin/__init__.py +3 -0
  116. mannf/product/admin/routes.py +514 -0
  117. mannf/product/auth/__init__.py +5 -0
  118. mannf/product/auth/saml.py +212 -0
  119. mannf/product/billing/__init__.py +5 -0
  120. mannf/product/billing/audit.py +160 -0
  121. mannf/product/billing/feature_gates.py +180 -0
  122. mannf/product/billing/metering.py +179 -0
  123. mannf/product/billing/notifications.py +181 -0
  124. mannf/product/billing/plans.py +133 -0
  125. mannf/product/billing/rate_limits.py +35 -0
  126. mannf/product/billing/stripe_billing.py +906 -0
  127. mannf/product/billing/tenant_auth.py +233 -0
  128. mannf/product/billing/tenant_manager.py +873 -0
  129. mannf/product/cli.py +3900 -0
  130. mannf/product/cli_admin.py +408 -0
  131. mannf/product/dashboard/__init__.py +61 -0
  132. mannf/product/dashboard/app.py +3567 -0
  133. mannf/product/dashboard/models.py +460 -0
  134. mannf/product/dashboard/static/index.html +6347 -0
  135. mannf/product/dashboard/static/manifest.json +25 -0
  136. mannf/product/dashboard/static/pwa-icon-192.png +0 -0
  137. mannf/product/dashboard/static/pwa-icon-512.png +0 -0
  138. mannf/product/dashboard/static/sw.js +64 -0
  139. mannf/product/dashboard/telemetry.py +547 -0
  140. mannf/product/database.py +145 -0
  141. mannf/product/demo.py +844 -0
  142. mannf/product/doctor.py +509 -0
  143. mannf/product/exporters/__init__.py +65 -0
  144. mannf/product/exporters/azuredevops_exporter.py +257 -0
  145. mannf/product/exporters/base.py +307 -0
  146. mannf/product/exporters/bugzilla_exporter.py +200 -0
  147. mannf/product/exporters/dedup.py +275 -0
  148. mannf/product/exporters/finding_adapter.py +216 -0
  149. mannf/product/exporters/github_exporter.py +197 -0
  150. mannf/product/exporters/gitlab_exporter.py +215 -0
  151. mannf/product/exporters/jira_exporter.py +180 -0
  152. mannf/product/exporters/linear_exporter.py +195 -0
  153. mannf/product/exporters/loader.py +233 -0
  154. mannf/product/exporters/pagerduty_exporter.py +363 -0
  155. mannf/product/exporters/sentry_exporter.py +322 -0
  156. mannf/product/exporters/servicenow_exporter.py +240 -0
  157. mannf/product/exporters/shortcut_exporter.py +231 -0
  158. mannf/product/exporters/webhook_exporter.py +383 -0
  159. mannf/product/formatters/__init__.py +18 -0
  160. mannf/product/formatters/allure_formatter.py +161 -0
  161. mannf/product/formatters/ctrf_formatter.py +149 -0
  162. mannf/product/healing/__init__.py +30 -0
  163. mannf/product/healing/graphql_schema_diff.py +152 -0
  164. mannf/product/healing/healer.py +141 -0
  165. mannf/product/healing/models.py +175 -0
  166. mannf/product/healing/schema_diff.py +251 -0
  167. mannf/product/ingestors/__init__.py +77 -0
  168. mannf/product/ingestors/base.py +256 -0
  169. mannf/product/ingestors/bgstm_ingestor.py +764 -0
  170. mannf/product/ingestors/curl_ingestor.py +1019 -0
  171. mannf/product/ingestors/cypress_ingestor.py +487 -0
  172. mannf/product/ingestors/gherkin_ingestor.py +967 -0
  173. mannf/product/ingestors/graphql_ingestor.py +845 -0
  174. mannf/product/ingestors/grpc_ingestor.py +591 -0
  175. mannf/product/ingestors/har_ingestor.py +976 -0
  176. mannf/product/ingestors/loader.py +284 -0
  177. mannf/product/ingestors/models.py +146 -0
  178. mannf/product/ingestors/openapi_ingestor.py +606 -0
  179. mannf/product/ingestors/playwright_ingestor.py +449 -0
  180. mannf/product/ingestors/postman_ingestor.py +631 -0
  181. mannf/product/ingestors/traffic_ingestor.py +679 -0
  182. mannf/product/ingestors/websocket_ingestor.py +526 -0
  183. mannf/product/integrations/__init__.py +21 -0
  184. mannf/product/integrations/auth.py +190 -0
  185. mannf/product/integrations/graphql_parser.py +436 -0
  186. mannf/product/integrations/graphql_sut.py +247 -0
  187. mannf/product/integrations/grpc_sut.py +469 -0
  188. mannf/product/integrations/http_sut.py +237 -0
  189. mannf/product/integrations/kafka_adapter.py +342 -0
  190. mannf/product/integrations/openapi_parser.py +513 -0
  191. mannf/product/integrations/postman_parser.py +467 -0
  192. mannf/product/integrations/webhook_receiver.py +344 -0
  193. mannf/product/integrations/websocket_sut.py +434 -0
  194. mannf/product/llm/__init__.py +25 -0
  195. mannf/product/llm/anthropic_provider.py +94 -0
  196. mannf/product/llm/base.py +267 -0
  197. mannf/product/llm/config.py +48 -0
  198. mannf/product/llm/factory.py +42 -0
  199. mannf/product/llm/openai_provider.py +93 -0
  200. mannf/product/llm/prompts.py +403 -0
  201. mannf/product/llm/root_cause_service.py +311 -0
  202. mannf/product/llm/test_plan_models.py +78 -0
  203. mannf/product/metrics.py +149 -0
  204. mannf/product/middleware/__init__.py +3 -0
  205. mannf/product/middleware/audit_middleware.py +112 -0
  206. mannf/product/middleware/tenant_isolation.py +114 -0
  207. mannf/product/models.py +347 -0
  208. mannf/product/notifications/__init__.py +24 -0
  209. mannf/product/notifications/dispatcher.py +411 -0
  210. mannf/product/onboarding.py +190 -0
  211. mannf/product/orchestration/__init__.py +39 -0
  212. mannf/product/orchestration/ingest_scan_orchestrator.py +339 -0
  213. mannf/product/orchestration/pipeline.py +401 -0
  214. mannf/product/orchestrator.py +987 -0
  215. mannf/product/orchestrator_models.py +269 -0
  216. mannf/product/regression/__init__.py +36 -0
  217. mannf/product/regression/differ.py +172 -0
  218. mannf/product/regression/masking.py +100 -0
  219. mannf/product/regression/models.py +232 -0
  220. mannf/product/regression/recorder.py +124 -0
  221. mannf/product/regression/replayer.py +168 -0
  222. mannf/product/reports/__init__.py +10 -0
  223. mannf/product/reports/pdf.py +132 -0
  224. mannf/product/scheduling/__init__.py +57 -0
  225. mannf/product/scheduling/cron_utils.py +251 -0
  226. mannf/product/scheduling/engine.py +473 -0
  227. mannf/product/scheduling/models.py +86 -0
  228. mannf/product/scheduling/queue.py +894 -0
  229. mannf/product/scheduling/store.py +235 -0
  230. mannf/product/security/__init__.py +21 -0
  231. mannf/product/security/belief_guided.py +143 -0
  232. mannf/product/security/checks/__init__.py +55 -0
  233. mannf/product/security/checks/base.py +69 -0
  234. mannf/product/security/checks/bfla.py +77 -0
  235. mannf/product/security/checks/bola.py +77 -0
  236. mannf/product/security/checks/bopla.py +80 -0
  237. mannf/product/security/checks/broken_auth.py +86 -0
  238. mannf/product/security/checks/graphql_security.py +299 -0
  239. mannf/product/security/checks/inventory.py +70 -0
  240. mannf/product/security/checks/misconfig.py +158 -0
  241. mannf/product/security/checks/resource_consumption.py +70 -0
  242. mannf/product/security/checks/sensitive_flows.py +80 -0
  243. mannf/product/security/checks/ssrf.py +101 -0
  244. mannf/product/security/checks/unsafe_consumption.py +120 -0
  245. mannf/product/security/models.py +92 -0
  246. mannf/product/security/plugin_loader.py +182 -0
  247. mannf/product/security/reporter.py +92 -0
  248. mannf/product/security/scanner.py +183 -0
  249. mannf/product/server.py +6220 -0
  250. mannf/product/setup_wizard.py +873 -0
  251. mannf/product/status.py +404 -0
  252. mannf/product/storage/__init__.py +10 -0
  253. mannf/product/storage/artifact_store.py +343 -0
  254. mannf/product/telemetry.py +300 -0
  255. mannf/product/uninstall.py +169 -0
  256. mannf/product/upgrade.py +139 -0
  257. mannf/product/weights/__init__.py +13 -0
  258. mannf/product/weights/blob_store.py +299 -0
  259. mannf/product/weights/factory.py +42 -0
  260. mannf/product/weights/registry.py +159 -0
  261. mannf/product/weights/store.py +210 -0
  262. mannf/regression/__init__.py +7 -0
  263. mannf/regression/differ.py +9 -0
  264. mannf/regression/masking.py +9 -0
  265. mannf/regression/models.py +9 -0
  266. mannf/regression/recorder.py +9 -0
  267. mannf/regression/replayer.py +9 -0
  268. mannf/security/__init__.py +7 -0
  269. mannf/security/belief_guided.py +9 -0
  270. mannf/security/checks/__init__.py +7 -0
  271. mannf/security/checks/base.py +9 -0
  272. mannf/security/checks/bfla.py +9 -0
  273. mannf/security/checks/bola.py +9 -0
  274. mannf/security/checks/bopla.py +9 -0
  275. mannf/security/checks/broken_auth.py +9 -0
  276. mannf/security/checks/graphql_security.py +9 -0
  277. mannf/security/checks/inventory.py +9 -0
  278. mannf/security/checks/misconfig.py +9 -0
  279. mannf/security/checks/resource_consumption.py +9 -0
  280. mannf/security/checks/sensitive_flows.py +9 -0
  281. mannf/security/checks/ssrf.py +9 -0
  282. mannf/security/checks/unsafe_consumption.py +9 -0
  283. mannf/security/models.py +9 -0
  284. mannf/security/reporter.py +9 -0
  285. mannf/security/scanner.py +9 -0
  286. mannf/server.py +9 -0
  287. mannf/testing/__init__.py +7 -0
  288. mannf/testing/adaptive_controller.py +9 -0
  289. mannf/testing/models.py +9 -0
  290. mannf/weights/__init__.py +7 -0
  291. mannf/weights/registry.py +9 -0
  292. mannf/weights/store.py +9 -0
  293. nat_engine-1.dist-info/METADATA +555 -0
  294. nat_engine-1.dist-info/RECORD +299 -0
  295. nat_engine-1.dist-info/WHEEL +5 -0
  296. nat_engine-1.dist-info/entry_points.txt +4 -0
  297. nat_engine-1.dist-info/licenses/LICENSE +651 -0
  298. nat_engine-1.dist-info/licenses/NOTICE +178 -0
  299. nat_engine-1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,342 @@
1
+ # Copyright (C) 2026 Brad Guider
2
+ # This file is part of NAT (Neural Agent Testing Framework).
3
+ # Licensed under the AGPL-3.0. See LICENSE for details.
4
+ # Commercial licensing available — see COMMERCIAL_LICENSE.md.
5
+
6
+ """NAT Orchestrator – full NeuroAgentTest BDI/ECNP architecture.
7
+
8
+ This orchestrator implements the complete NAT framework as described in
9
+ Chapter 4 of the thesis, using:
10
+
11
+ * **PlannerAgents** – formulate test objectives from belief state
12
+ * **ExecutorAgents** – bid on and execute contracts via ECNP
13
+ * **AnalyzerAgents** – process results, update beliefs, detect anomalies
14
+ * **CoordinatorAgent** – allocates tasks via Extended Contract Net Protocol
15
+
16
+ All agents run concurrently as ``asyncio`` tasks and communicate through the
17
+ shared :class:`MessageBus`. The orchestrator drives agent lifecycle and
18
+ produces the final report.
19
+
20
+ Emergence: unlike the simple round-robin :class:`Orchestrator`, this
21
+ implementation produces emergent specialisation through ECNP negotiation –
22
+ services with higher fault likelihood attract more testing attention without
23
+ any explicit assignment.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import asyncio
29
+ import logging
30
+ import time
31
+ from typing import Any, Dict, List, Optional
32
+
33
+ from mannf.core.agents.analyzer_agent import AnalyzerAgent
34
+ from mannf.core.agents.coordinator_agent import CoordinatorAgent
35
+ from mannf.core.agents.executor_agent import ExecutorAgent
36
+ from mannf.core.agents.planner_agent import PlannerAgent
37
+ from mannf.core.anomaly.enhanced_detector import EnhancedAnomalyDetector
38
+ from mannf.core.distributed.system_under_test import SystemUnderTest
39
+ from mannf.core.messaging.bus import MessageBus
40
+ from mannf.core.messaging.messages import Message, MessageType
41
+ from mannf.core.prioritization.belief_prioritizer import BeliefPrioritizer
42
+ from mannf.core.prioritization.risk_scorer import RiskScorer
43
+ from mannf.core.testing.models import TestResult, TestSuite
44
+
45
+ logger = logging.getLogger(__name__)
46
+
47
+
48
+ def _infer_status_code(result: TestResult, actual: Any) -> int:
49
+ """Infer an HTTP status code from a test result for anomaly detection."""
50
+ if result.error:
51
+ return 500
52
+ if isinstance(actual, dict) and "status_code" in actual:
53
+ try:
54
+ return int(actual["status_code"])
55
+ except (ValueError, TypeError):
56
+ pass
57
+ return 200 if result.passed else 400
58
+
59
+
60
+ class NATOrchestrator:
61
+ """Full NeuroAgentTest orchestrator using BDI agents and ECNP.
62
+
63
+ Parameters
64
+ ----------
65
+ sut:
66
+ The system under test.
67
+ num_planners:
68
+ Number of PlannerAgent instances.
69
+ num_executors:
70
+ Number of ExecutorAgent instances.
71
+ num_analyzers:
72
+ Number of AnalyzerAgent instances.
73
+ max_contracts:
74
+ Stop after this many contracts have been completed.
75
+ suite_name:
76
+ Label for the test suite in the final report.
77
+ log_interval:
78
+ Log a progress summary every *log_interval* completions.
79
+ bid_timeout_s:
80
+ ECNP bid collection window (seconds).
81
+ """
82
+
83
+ def __init__(
84
+ self,
85
+ sut: SystemUnderTest,
86
+ num_planners: int = 2,
87
+ num_executors: int = 3,
88
+ num_analyzers: int = 1,
89
+ max_contracts: int = 100,
90
+ suite_name: str = "NAT Run",
91
+ log_interval: int = 10,
92
+ bid_timeout_s: float = 0.05,
93
+ belief_prioritizer: Optional[BeliefPrioritizer] = None,
94
+ anomaly_detector: Optional[EnhancedAnomalyDetector] = None,
95
+ ) -> None:
96
+ self.sut = sut
97
+ self.max_contracts = max_contracts
98
+ self.log_interval = log_interval
99
+ self.belief_prioritizer = belief_prioritizer
100
+ self.anomaly_detector = anomaly_detector
101
+
102
+ self._bus = MessageBus()
103
+ self._suite = TestSuite(name=suite_name)
104
+
105
+ service_names = [e.name for e in sut.registry.all()]
106
+
107
+ # Risk scorer is always active; it feeds get_risk_report()
108
+ self._risk_scorer = RiskScorer()
109
+
110
+ # Create agents
111
+ self._planners: List[PlannerAgent] = [
112
+ PlannerAgent(f"planner-{i}", self._bus, service_names)
113
+ for i in range(num_planners)
114
+ ]
115
+ self._executors: List[ExecutorAgent] = [
116
+ ExecutorAgent(f"executor-{i}", self._bus, service_names, sut)
117
+ for i in range(num_executors)
118
+ ]
119
+ self._analyzers: List[AnalyzerAgent] = [
120
+ AnalyzerAgent(f"analyzer-{i}", self._bus, service_names)
121
+ for i in range(num_analyzers)
122
+ ]
123
+ self._coordinator = CoordinatorAgent(
124
+ "coordinator-0", self._bus, service_names, bid_timeout_s=bid_timeout_s
125
+ )
126
+
127
+ # Subscribe orchestrator to monitor completion
128
+ # TEST_RESULT is published once per execution by ExecutorAgents
129
+ self._result_queue = self._bus.subscribe(
130
+ "nat-orchestrator",
131
+ {MessageType.TEST_RESULT},
132
+ )
133
+
134
+ self._completed = 0
135
+ self._start_time: Optional[float] = None
136
+
137
+ # ------------------------------------------------------------------
138
+ # Main entry point
139
+ # ------------------------------------------------------------------
140
+
141
+ async def run(self) -> TestSuite:
142
+ """Launch all agents and wait for max_contracts completions."""
143
+ self._start_time = time.monotonic()
144
+ logger.info("NATOrchestrator: starting (max_contracts=%d)", self.max_contracts)
145
+
146
+ all_agents = (
147
+ self._planners + self._executors + self._analyzers + [self._coordinator]
148
+ )
149
+ tasks = [asyncio.create_task(a.run()) for a in all_agents]
150
+
151
+ # Give agents time to subscribe and start their run() loops
152
+ await asyncio.sleep(0)
153
+
154
+ # Kickstart the pipeline: trigger initial task announcements from all
155
+ # planners so executors have something to bid on immediately
156
+ for planner in self._planners:
157
+ asyncio.get_running_loop().create_task(planner._announce_tasks())
158
+
159
+ # Allow announcements to propagate before monitoring begins
160
+ await asyncio.sleep(0.01)
161
+
162
+ try:
163
+ await self._monitor_completions()
164
+ finally:
165
+ for a in all_agents:
166
+ a._running = False
167
+ await asyncio.gather(*tasks, return_exceptions=True)
168
+
169
+ elapsed = time.monotonic() - (self._start_time or 0)
170
+ logger.info(
171
+ "NATOrchestrator: finished %d contracts in %.1fs",
172
+ self._completed, elapsed,
173
+ )
174
+ return self._suite
175
+
176
+ # ------------------------------------------------------------------
177
+ # Completion monitoring
178
+ # ------------------------------------------------------------------
179
+
180
+ async def _monitor_completions(self) -> None:
181
+ """Read CONTRACT_RESULT / TEST_RESULT messages and build the suite."""
182
+ while self._completed < self.max_contracts:
183
+ try:
184
+ msg = await asyncio.wait_for(self._result_queue.get(), timeout=5.0)
185
+ except asyncio.TimeoutError:
186
+ logger.warning(
187
+ "NATOrchestrator: no results after 5s (completed=%d)",
188
+ self._completed,
189
+ )
190
+ break
191
+
192
+ payload = msg.payload
193
+ result: Optional[TestResult] = None
194
+ test_case = None
195
+
196
+ if isinstance(payload, dict):
197
+ result = payload.get("result")
198
+ test_case = payload.get("test_case")
199
+ elif isinstance(payload, TestResult):
200
+ result = payload
201
+
202
+ if result is None:
203
+ continue
204
+
205
+ if test_case is not None:
206
+ self._suite.add_case(test_case)
207
+ self._suite.record_result(result)
208
+ self._completed += 1
209
+
210
+ # Update belief prioritizer and risk scorer with the result
211
+ target = test_case.target if test_case is not None else "unknown"
212
+ if self.belief_prioritizer is not None:
213
+ self.belief_prioritizer.record_result(result, target)
214
+
215
+ belief_states = [a.beliefs for a in self._analyzers]
216
+ self._risk_scorer.record(target, result, belief_states)
217
+
218
+ # Run anomaly detection on the result
219
+ if self.anomaly_detector is not None:
220
+ actual = result.actual_output
221
+ self.anomaly_detector.observe(
222
+ endpoint=target,
223
+ latency_ms=result.execution_time_ms,
224
+ status_code=_infer_status_code(result, actual),
225
+ payload=actual,
226
+ is_error=bool(result.error),
227
+ )
228
+
229
+ if self._completed % self.log_interval == 0:
230
+ self._log_progress()
231
+
232
+ def _log_progress(self) -> None:
233
+ s = self._suite.summary()
234
+ elapsed = time.monotonic() - (self._start_time or time.monotonic())
235
+ # Aggregate belief state from all analyzers
236
+ avg_entropy = (
237
+ sum(a.beliefs.entropy() for a in self._analyzers) / len(self._analyzers)
238
+ if self._analyzers else 0.0
239
+ )
240
+ logger.info(
241
+ "NAT %4d/%d | passed=%d failed=%d errored=%d "
242
+ "| defect_rate=%.1f%% | avg_belief_entropy=%.3f "
243
+ "| contracts_awarded=%d | elapsed=%.1fs",
244
+ self._completed, self.max_contracts,
245
+ s["passed"], s["failed"], s["errored"],
246
+ s["defect_detection_rate"] * 100,
247
+ avg_entropy,
248
+ self._coordinator.contracts_awarded,
249
+ elapsed,
250
+ )
251
+
252
+ # ------------------------------------------------------------------
253
+ # Report
254
+ # ------------------------------------------------------------------
255
+
256
+ def report(self) -> Dict[str, Any]:
257
+ """Return a structured summary of the completed NAT run."""
258
+ elapsed = time.monotonic() - (self._start_time or 0)
259
+ suite_summary = self._suite.summary()
260
+
261
+ # Aggregate belief state across analyzers
262
+ avg_entropy = (
263
+ sum(a.beliefs.entropy() for a in self._analyzers) / len(self._analyzers)
264
+ if self._analyzers else 0.0
265
+ )
266
+
267
+ # Per-service failure counts from each executor
268
+ failure_counts: Dict[str, int] = {}
269
+ for exc in self._executors:
270
+ for svc, count in exc.beliefs.fault_likelihood.items():
271
+ pass # fault_likelihood is beliefs, not counts
272
+ # Use SUT call log for ground truth
273
+ failure_counts = self.sut.failure_counts() if hasattr(self.sut, "failure_counts") else {}
274
+
275
+ # Belief state from analyzers
276
+ combined_beliefs: Dict[str, float] = {}
277
+ for analyzer in self._analyzers:
278
+ for svc, val in analyzer.beliefs.fault_likelihood.items():
279
+ combined_beliefs[svc] = (combined_beliefs.get(svc, 0.0) + val)
280
+ if self._analyzers:
281
+ combined_beliefs = {k: v / len(self._analyzers) for k, v in combined_beliefs.items()}
282
+
283
+ return {
284
+ "suite": suite_summary,
285
+ "coordinator": {
286
+ "contracts_awarded": self._coordinator.contracts_awarded,
287
+ "contracts_completed": self._coordinator.contracts_completed,
288
+ },
289
+ "executors": [
290
+ {
291
+ "id": e.agent_id,
292
+ "executions": e.execution_count,
293
+ "avg_latency_ms": round(e.avg_latency_ms, 2),
294
+ }
295
+ for e in self._executors
296
+ ],
297
+ "analyzers": [
298
+ {
299
+ "id": a.agent_id,
300
+ "total_results": a.total_results,
301
+ "belief_entropy": round(a.beliefs.entropy(), 4),
302
+ "top_risk_services": a.beliefs.top_k(3),
303
+ }
304
+ for a in self._analyzers
305
+ ],
306
+ "beliefs": {
307
+ k: round(v, 4)
308
+ for k, v in sorted(
309
+ combined_beliefs.items(), key=lambda x: x[1], reverse=True
310
+ )
311
+ },
312
+ "avg_belief_entropy": round(avg_entropy, 4),
313
+ "actual_failure_counts": failure_counts,
314
+ "agent_count": (
315
+ len(self._planners) + len(self._executors)
316
+ + len(self._analyzers) + 1
317
+ ),
318
+ "elapsed_seconds": round(elapsed, 2),
319
+ "risk_report": self.get_risk_report(),
320
+ "anomaly_alerts": (
321
+ self.anomaly_detector.get_alerts_dict()
322
+ if self.anomaly_detector is not None
323
+ else []
324
+ ),
325
+ }
326
+
327
+ def get_risk_report(self) -> List[Dict[str, Any]]:
328
+ """Return the current per-endpoint risk report from the risk scorer."""
329
+ belief_states = [a.beliefs for a in self._analyzers]
330
+ return self._risk_scorer.get_risk_report(belief_states)
331
+
332
+ @property
333
+ def suite(self) -> TestSuite:
334
+ return self._suite
335
+
336
+ @property
337
+ def coordinator(self) -> CoordinatorAgent:
338
+ return self._coordinator
339
+
340
+ @property
341
+ def risk_scorer(self) -> RiskScorer:
342
+ return self._risk_scorer
@@ -0,0 +1,183 @@
1
+ # Copyright (C) 2026 Brad Guider
2
+ # This file is part of NAT (Neural Agent Testing Framework).
3
+ # Licensed under the AGPL-3.0. See LICENSE for details.
4
+ # Commercial licensing available — see COMMERCIAL_LICENSE.md.
5
+
6
+ """Neural network primitives built on NumPy.
7
+
8
+ Provides a lightweight, dependency-free implementation of a fully-connected
9
+ feedforward network with backpropagation. Each agent in the framework owns
10
+ one or more of these networks to guide its decision-making.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import numpy as np
16
+ from typing import List, Sequence
17
+
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # Activation functions
21
+ # ---------------------------------------------------------------------------
22
+
23
+ def relu(x: np.ndarray) -> np.ndarray:
24
+ return np.maximum(0.0, x)
25
+
26
+
27
+ def relu_deriv(x: np.ndarray) -> np.ndarray:
28
+ return (x > 0.0).astype(float)
29
+
30
+
31
+ def sigmoid(x: np.ndarray) -> np.ndarray:
32
+ return 1.0 / (1.0 + np.exp(-np.clip(x, -500, 500)))
33
+
34
+
35
+ def sigmoid_deriv(x: np.ndarray) -> np.ndarray:
36
+ s = sigmoid(x)
37
+ return s * (1.0 - s)
38
+
39
+
40
+ def softmax(x: np.ndarray) -> np.ndarray:
41
+ e = np.exp(x - np.max(x))
42
+ return e / e.sum()
43
+
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # Network layer
47
+ # ---------------------------------------------------------------------------
48
+
49
+ class Layer:
50
+ """Single fully-connected layer with weight matrix and bias vector."""
51
+
52
+ def __init__(self, n_in: int, n_out: int, activation: str = "relu") -> None:
53
+ self.activation = activation
54
+ # Xavier / Glorot initialisation
55
+ scale = np.sqrt(2.0 / n_in)
56
+ self.W: np.ndarray = np.random.randn(n_in, n_out) * scale
57
+ self.b: np.ndarray = np.zeros(n_out)
58
+
59
+ # Cache for backprop
60
+ self._x: np.ndarray | None = None
61
+ self._z: np.ndarray | None = None
62
+
63
+ # -- forward ----------------------------------------------------------
64
+
65
+ def forward(self, x: np.ndarray) -> np.ndarray:
66
+ self._x = x
67
+ self._z = x @ self.W + self.b
68
+ return self._activate(self._z)
69
+
70
+ def _activate(self, z: np.ndarray) -> np.ndarray:
71
+ if self.activation == "relu":
72
+ return relu(z)
73
+ if self.activation == "sigmoid":
74
+ return sigmoid(z)
75
+ if self.activation == "tanh":
76
+ return np.tanh(z)
77
+ if self.activation == "linear":
78
+ return z
79
+ raise ValueError(f"Unknown activation: {self.activation}")
80
+
81
+ def _activate_deriv(self, z: np.ndarray) -> np.ndarray:
82
+ if self.activation == "relu":
83
+ return relu_deriv(z)
84
+ if self.activation == "sigmoid":
85
+ return sigmoid_deriv(z)
86
+ if self.activation == "tanh":
87
+ return 1.0 - np.tanh(z) ** 2
88
+ if self.activation == "linear":
89
+ return np.ones_like(z)
90
+ raise ValueError(f"Unknown activation: {self.activation}")
91
+
92
+ # -- backward ---------------------------------------------------------
93
+
94
+ def backward(self, grad_out: np.ndarray) -> np.ndarray:
95
+ """Return gradient w.r.t. input; cache weight/bias gradients."""
96
+ assert self._z is not None and self._x is not None
97
+ grad_z = grad_out * self._activate_deriv(self._z)
98
+ self._grad_W = self._x[:, None] * grad_z[None, :] # outer product
99
+ self._grad_b = grad_z
100
+ return grad_z @ self.W.T # propagate to previous layer
101
+
102
+ # -- parameter update -------------------------------------------------
103
+
104
+ def apply_gradients(self, lr: float) -> None:
105
+ assert hasattr(self, "_grad_W"), "backward() must be called first"
106
+ self.W -= lr * self._grad_W
107
+ self.b -= lr * self._grad_b
108
+
109
+
110
+ # ---------------------------------------------------------------------------
111
+ # Fully-connected network
112
+ # ---------------------------------------------------------------------------
113
+
114
+ class NeuralNetwork:
115
+ """Multi-layer perceptron with MSE loss and online gradient descent.
116
+
117
+ Parameters
118
+ ----------
119
+ layer_sizes:
120
+ Sequence of integers specifying the width of each layer,
121
+ e.g. ``[8, 16, 8, 4]`` creates three layers.
122
+ hidden_activation:
123
+ Activation used for all hidden layers. Output layer uses *linear*
124
+ activation so the network can regress arbitrary real values.
125
+ learning_rate:
126
+ Step size for stochastic gradient descent updates.
127
+ """
128
+
129
+ def __init__(
130
+ self,
131
+ layer_sizes: Sequence[int],
132
+ hidden_activation: str = "relu",
133
+ learning_rate: float = 0.01,
134
+ ) -> None:
135
+ if len(layer_sizes) < 2:
136
+ raise ValueError("layer_sizes must contain at least two elements (in, out)")
137
+ self.lr = learning_rate
138
+ self.layers: List[Layer] = []
139
+ for i in range(len(layer_sizes) - 1):
140
+ act = hidden_activation if i < len(layer_sizes) - 2 else "linear"
141
+ self.layers.append(Layer(layer_sizes[i], layer_sizes[i + 1], activation=act))
142
+
143
+ # -- forward pass -----------------------------------------------------
144
+
145
+ def predict(self, x: np.ndarray) -> np.ndarray:
146
+ """Return network output for input vector *x*."""
147
+ out = x.astype(float)
148
+ for layer in self.layers:
149
+ out = layer.forward(out)
150
+ return out
151
+
152
+ # -- training step ----------------------------------------------------
153
+
154
+ def train_step(self, x: np.ndarray, target: np.ndarray) -> float:
155
+ """Forward pass, MSE loss, backprop, and SGD update.
156
+
157
+ Returns the scalar MSE loss value.
158
+ """
159
+ output = self.predict(x)
160
+ loss = float(np.mean((output - target) ** 2))
161
+
162
+ # Gradient of MSE loss w.r.t. output
163
+ grad = 2.0 * (output - target) / len(output)
164
+
165
+ for layer in reversed(self.layers):
166
+ grad = layer.backward(grad)
167
+
168
+ for layer in self.layers:
169
+ layer.apply_gradients(self.lr)
170
+
171
+ return loss
172
+
173
+ # -- persistence helpers ----------------------------------------------
174
+
175
+ def get_weights(self) -> List[dict]:
176
+ """Return a snapshot of all weight matrices and bias vectors."""
177
+ return [{"W": layer.W.copy(), "b": layer.b.copy()} for layer in self.layers]
178
+
179
+ def set_weights(self, weights: List[dict]) -> None:
180
+ """Restore weights from a previous snapshot."""
181
+ for layer, wb in zip(self.layers, weights):
182
+ layer.W = wb["W"].copy()
183
+ layer.b = wb["b"].copy()