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,150 @@
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
+ """Oracle agent.
7
+
8
+ The :class:`OracleAgent` acts as the *test oracle*: it receives
9
+ :attr:`MessageType.TEST_RESULT` messages from the orchestrator and uses its
10
+ neural network to predict whether a test outcome should be considered a
11
+ genuine defect or an expected/acceptable failure.
12
+
13
+ It publishes :attr:`MessageType.TEST_VERDICT` messages that the orchestrator
14
+ records in the test suite and uses to update the adaptive controller.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import logging
20
+ from typing import Any, Dict, Optional, Set
21
+
22
+ import numpy as np
23
+
24
+ from mannf.core.agents.base import BaseAgent
25
+ from mannf.core.messaging.bus import MessageBus
26
+ from mannf.core.messaging.messages import Message, MessageType
27
+ from mannf.core.neural import NeuralNetwork
28
+ from mannf.core.testing.models import TestCase, TestResult
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+ _INPUT_DIM = 5 # features derived from a TestResult
33
+
34
+
35
+ class OracleAgent(BaseAgent):
36
+ """Classifies test outcomes as genuine defects or expected failures.
37
+
38
+ The oracle uses a binary classifier neural network that is trained online:
39
+ initially it trusts all raw pass/fail flags, but as it accumulates
40
+ experience it learns patterns (e.g. transient latency spikes should not
41
+ be classified as defects).
42
+
43
+ Verdict payload format::
44
+
45
+ {
46
+ "test_case_id": str,
47
+ "is_defect": bool,
48
+ "confidence": float, # [0, 1]
49
+ "raw_passed": bool,
50
+ }
51
+ """
52
+
53
+ def __init__(self, agent_id: str, bus: MessageBus) -> None:
54
+ network = NeuralNetwork(
55
+ layer_sizes=[_INPUT_DIM, 16, 8, 1],
56
+ hidden_activation="relu",
57
+ learning_rate=0.005,
58
+ )
59
+ super().__init__(agent_id, bus, network)
60
+ self._verdict_count = 0
61
+ self._defect_count = 0
62
+
63
+ @property
64
+ def subscribed_types(self) -> Set[MessageType]:
65
+ return {MessageType.TEST_RESULT}
66
+
67
+ # ------------------------------------------------------------------
68
+ # Message handling
69
+ # ------------------------------------------------------------------
70
+
71
+ async def _handle_message(self, message: Message) -> None:
72
+ if message.type == MessageType.TEST_RESULT:
73
+ payload = message.payload
74
+ result: TestResult = payload.get("result") if isinstance(payload, dict) else payload
75
+
76
+ if not isinstance(result, TestResult):
77
+ logger.warning("OracleAgent: unexpected payload type %s", type(result))
78
+ return
79
+
80
+ test_case: Optional[TestCase] = (
81
+ payload.get("test_case") if isinstance(payload, dict) else None
82
+ )
83
+ verdict = self._evaluate(result, test_case)
84
+ await self._publish(
85
+ Message(MessageType.TEST_VERDICT, self.agent_id, verdict)
86
+ )
87
+
88
+ # ------------------------------------------------------------------
89
+ # Evaluation
90
+ # ------------------------------------------------------------------
91
+
92
+ def _evaluate(
93
+ self,
94
+ result: TestResult,
95
+ test_case: Optional[TestCase] = None,
96
+ ) -> Dict[str, Any]:
97
+ """Return a verdict dictionary."""
98
+ features = self._extract_features(result, test_case)
99
+ assert self.network is not None
100
+ raw_score = float(self.network.predict(features)[0])
101
+ # Sigmoid to get probability of being a defect
102
+ confidence = 1.0 / (1.0 + np.exp(-raw_score))
103
+ is_defect = bool(confidence >= 0.5)
104
+
105
+ # Online training: use raw pass/fail as ground truth label
106
+ label = np.array([0.0 if result.passed else 1.0])
107
+ self.network.train_step(features, label)
108
+
109
+ self._verdict_count += 1
110
+ if is_defect:
111
+ self._defect_count += 1
112
+
113
+ return {
114
+ "test_case_id": result.test_case_id,
115
+ "is_defect": is_defect,
116
+ "confidence": round(float(confidence), 4),
117
+ "raw_passed": result.passed,
118
+ "error": result.error,
119
+ }
120
+
121
+ @staticmethod
122
+ def _extract_features(
123
+ result: TestResult,
124
+ test_case: Optional[TestCase],
125
+ ) -> np.ndarray:
126
+ """Encode result + optional test_case into a feature vector."""
127
+ vec = np.zeros(_INPUT_DIM)
128
+ vec[0] = 0.0 if result.passed else 1.0
129
+ vec[1] = 1.0 if result.error else 0.0
130
+ vec[2] = min(result.execution_time_ms / 500.0, 1.0) # normalised
131
+ if test_case is not None:
132
+ vec[3] = test_case.priority
133
+ vec[4] = len(test_case.inputs) / 10.0
134
+ return vec
135
+
136
+ # ------------------------------------------------------------------
137
+ # Stats
138
+ # ------------------------------------------------------------------
139
+
140
+ @property
141
+ def verdict_count(self) -> int:
142
+ return self._verdict_count
143
+
144
+ @property
145
+ def defect_count(self) -> int:
146
+ return self._defect_count
147
+
148
+ @property
149
+ def defect_rate(self) -> float:
150
+ return self._defect_count / self._verdict_count if self._verdict_count else 0.0
@@ -0,0 +1,279 @@
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
+ """PerformanceTestingAgent – BDI agent that measures Web Vitals performance.
7
+
8
+ Listens for :attr:`~mannf.core.messaging.messages.MessageType.INTERACTION_RESULT`
9
+ messages published by browser agents and evaluates any performance metrics
10
+ embedded in the payload. Metrics can also be deposited programmatically via
11
+ :meth:`register_metrics`.
12
+
13
+ After each evaluation the agent publishes:
14
+
15
+ * :attr:`~MessageType.PERFORMANCE_METRICS_CAPTURED` — always, with all metric
16
+ ratings.
17
+ * :attr:`~MessageType.PERFORMANCE_THRESHOLD_VIOLATED` — only when one or more
18
+ metrics are rated ``"poor"``.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import logging
24
+ from typing import Any
25
+
26
+ from mannf.core.agents.bdi_agent import BDIAgent
27
+ from mannf.core.browser.performance_metrics import (
28
+ PerformanceResult,
29
+ evaluate_metrics,
30
+ get_threshold_violations,
31
+ )
32
+ from mannf.core.messaging.bus import MessageBus
33
+ from mannf.core.messaging.messages import Message, MessageType
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+
38
+ class PerformanceTestingAgent(BDIAgent):
39
+ """BDI agent that evaluates Web Vitals performance metrics.
40
+
41
+ Parameters
42
+ ----------
43
+ agent_id:
44
+ Unique identifier for this agent.
45
+ bus:
46
+ Shared message bus.
47
+ service_names:
48
+ Services in the system under test (forwarded to :class:`BDIAgent`).
49
+ custom_thresholds:
50
+ Optional threshold overrides mapping metric name to
51
+ ``(threshold_good, threshold_poor, unit)``. Any metric name absent
52
+ from this dict falls back to :data:`~mannf.core.browser.performance_metrics.DEFAULT_THRESHOLDS`.
53
+ min_performance_score:
54
+ Minimum passing performance score in the range 0–100. Evaluations
55
+ whose :attr:`~PerformanceResult.performance_score` is below this
56
+ value are counted as ``"failed"`` in :meth:`get_performance_report`.
57
+ """
58
+
59
+ def __init__(
60
+ self,
61
+ agent_id: str,
62
+ bus: MessageBus,
63
+ service_names: list[str],
64
+ custom_thresholds: dict[str, tuple[float, float, str]] | None = None,
65
+ min_performance_score: float = 70.0,
66
+ ) -> None:
67
+ super().__init__(agent_id=agent_id, bus=bus, service_names=service_names)
68
+
69
+ self._custom_thresholds = custom_thresholds
70
+ self._min_performance_score = min_performance_score
71
+
72
+ # url -> raw metrics deposited by callers / orchestrator
73
+ self._metrics_registry: dict[str, dict[str, float]] = {}
74
+
75
+ # Evaluated results accumulated across all processed URLs
76
+ self._results: list[PerformanceResult] = []
77
+
78
+ # ------------------------------------------------------------------
79
+ # BDIAgent contract
80
+ # ------------------------------------------------------------------
81
+
82
+ @property
83
+ def subscribed_types(self) -> set[MessageType]:
84
+ return {
85
+ MessageType.INTERACTION_RESULT,
86
+ MessageType.SCREENSHOT_CAPTURED,
87
+ MessageType.BELIEF_UPDATE,
88
+ }
89
+
90
+ async def _handle_message(self, message: Message) -> None:
91
+ if message.type == MessageType.INTERACTION_RESULT:
92
+ await self._on_interaction_result(message)
93
+ elif message.type == MessageType.SCREENSHOT_CAPTURED:
94
+ # No-op — retained for future enrichment.
95
+ pass
96
+ elif message.type == MessageType.BELIEF_UPDATE:
97
+ payload = message.payload or {}
98
+ peer_beliefs = payload.get("beliefs", {})
99
+ if peer_beliefs:
100
+ self._absorb_peer_beliefs(message.sender_id, peer_beliefs)
101
+
102
+ # ------------------------------------------------------------------
103
+ # Metrics registry
104
+ # ------------------------------------------------------------------
105
+
106
+ def register_metrics(self, url: str, raw_metrics: dict[str, float]) -> None:
107
+ """Deposit raw performance metrics for the given URL.
108
+
109
+ The metrics will be evaluated when
110
+ :meth:`evaluate_url_performance` is called, or automatically when an
111
+ :attr:`~MessageType.INTERACTION_RESULT` message arrives for *url*.
112
+
113
+ Parameters
114
+ ----------
115
+ url:
116
+ The page URL the metrics were collected from.
117
+ raw_metrics:
118
+ Dict mapping metric names to measured values.
119
+ """
120
+ self._metrics_registry[url] = raw_metrics
121
+
122
+ # ------------------------------------------------------------------
123
+ # Message handlers
124
+ # ------------------------------------------------------------------
125
+
126
+ async def _on_interaction_result(self, message: Message) -> None:
127
+ payload = message.payload or {}
128
+ url: str = payload.get("url", "")
129
+ performance_data: dict[str, float] | None = payload.get("performance")
130
+
131
+ if performance_data:
132
+ await self.evaluate_url_performance(url, performance_data)
133
+ elif url in self._metrics_registry:
134
+ await self.evaluate_url_performance(url, self._metrics_registry[url])
135
+
136
+ # ------------------------------------------------------------------
137
+ # Core evaluation logic
138
+ # ------------------------------------------------------------------
139
+
140
+ async def evaluate_url_performance(
141
+ self, url: str, raw_metrics: dict[str, float]
142
+ ) -> PerformanceResult:
143
+ """Evaluate metrics for *url* and publish results on the bus.
144
+
145
+ Parameters
146
+ ----------
147
+ url:
148
+ Page URL (used for logging and the published payload).
149
+ raw_metrics:
150
+ Dict mapping metric names to measured values.
151
+
152
+ Returns
153
+ -------
154
+ PerformanceResult
155
+ The evaluation outcome (also stored internally for reporting).
156
+ """
157
+ result = evaluate_metrics(raw_metrics, url=url, custom_thresholds=self._custom_thresholds)
158
+ self._results.append(result)
159
+
160
+ passed = result.performance_score >= self._min_performance_score
161
+ violations = get_threshold_violations(result)
162
+
163
+ logger.info(
164
+ "PerformanceTestingAgent %s: url=%s score=%.1f poor_count=%d %s",
165
+ self.agent_id,
166
+ url,
167
+ result.performance_score,
168
+ result.poor_count,
169
+ "PASS" if passed else "FAIL",
170
+ )
171
+
172
+ await self._publish(
173
+ Message(
174
+ MessageType.PERFORMANCE_METRICS_CAPTURED,
175
+ sender_id=self.agent_id,
176
+ payload={
177
+ "url": url,
178
+ "performance_score": result.performance_score,
179
+ "page_load_time_ms": result.page_load_time_ms,
180
+ "passed": passed,
181
+ "good_count": result.good_count,
182
+ "poor_count": result.poor_count,
183
+ "metrics": [
184
+ {
185
+ "name": m.name,
186
+ "value": m.value,
187
+ "unit": m.unit,
188
+ "rating": m.rating,
189
+ "threshold_good": m.threshold_good,
190
+ "threshold_poor": m.threshold_poor,
191
+ }
192
+ for m in result.metrics
193
+ ],
194
+ },
195
+ )
196
+ )
197
+
198
+ if violations:
199
+ await self._publish(
200
+ Message(
201
+ MessageType.PERFORMANCE_THRESHOLD_VIOLATED,
202
+ sender_id=self.agent_id,
203
+ payload={
204
+ "url": url,
205
+ "violations": [
206
+ {
207
+ "name": m.name,
208
+ "value": m.value,
209
+ "unit": m.unit,
210
+ "threshold_poor": m.threshold_poor,
211
+ }
212
+ for m in violations
213
+ ],
214
+ },
215
+ )
216
+ )
217
+
218
+ return result
219
+
220
+ # ------------------------------------------------------------------
221
+ # Reporting
222
+ # ------------------------------------------------------------------
223
+
224
+ def get_performance_report(self) -> dict[str, Any]:
225
+ """Return a summary of all performance evaluations performed.
226
+
227
+ Returns
228
+ -------
229
+ dict
230
+ ``{total_evaluations, passed, failed, avg_performance_score,
231
+ threshold_violations, results: [...]}``
232
+ """
233
+ total = len(self._results)
234
+ passed_count = sum(
235
+ 1 for r in self._results
236
+ if r.performance_score >= self._min_performance_score
237
+ )
238
+ failed_count = total - passed_count
239
+ avg_score = (
240
+ round(
241
+ sum(r.performance_score for r in self._results) / total,
242
+ 1,
243
+ )
244
+ if total > 0
245
+ else 100.0
246
+ )
247
+ all_violations = [
248
+ {"url": r.url, "metric": m.name, "value": m.value, "unit": m.unit}
249
+ for r in self._results
250
+ for m in get_threshold_violations(r)
251
+ ]
252
+
253
+ return {
254
+ "total_evaluations": total,
255
+ "passed": passed_count,
256
+ "failed": failed_count,
257
+ "avg_performance_score": avg_score,
258
+ "threshold_violations": all_violations,
259
+ "results": [
260
+ {
261
+ "url": r.url,
262
+ "performance_score": r.performance_score,
263
+ "page_load_time_ms": r.page_load_time_ms,
264
+ "passed": r.performance_score >= self._min_performance_score,
265
+ "good_count": r.good_count,
266
+ "poor_count": r.poor_count,
267
+ "metrics": [
268
+ {
269
+ "name": m.name,
270
+ "value": m.value,
271
+ "unit": m.unit,
272
+ "rating": m.rating,
273
+ }
274
+ for m in r.metrics
275
+ ],
276
+ }
277
+ for r in self._results
278
+ ],
279
+ }
@@ -0,0 +1,128 @@
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
+ """Planner Agent – generates candidate test objectives.
7
+
8
+ The PlannerAgent is responsible for formulating testing tasks and submitting
9
+ them to the CoordinatorAgent for allocation. It uses its Predictor network
10
+ and current belief state to identify the most fault-prone services (Thesis
11
+ §4.2, §4.4).
12
+
13
+ Task announcement payload::
14
+
15
+ {
16
+ "task_id": str,
17
+ "target": str, # service name
18
+ "priority": float, # expected informational gain
19
+ "risk_estimate": float, # NN-derived fault-proneness score
20
+ "beliefs": dict, # snapshot of planner's beliefs
21
+ }
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import logging
27
+ import uuid
28
+ from typing import Any, Dict, List, Set
29
+
30
+ import numpy as np
31
+
32
+ from mannf.core.agents.bdi_agent import BDIAgent
33
+ from mannf.core.messaging.bus import MessageBus
34
+ from mannf.core.messaging.messages import Message, MessageType
35
+ from mannf.core.testing.models import TestCase
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+
40
+ class PlannerAgent(BDIAgent):
41
+ """Formulates test objectives and requests CoordinatorAgent to schedule them.
42
+
43
+ On each idle cycle the planner consults its belief state and NN to identify
44
+ high-risk services and emits TASK_ANNOUNCEMENT messages.
45
+ """
46
+
47
+ def __init__(
48
+ self,
49
+ agent_id: str,
50
+ bus: MessageBus,
51
+ service_names: List[str],
52
+ tasks_per_cycle: int = 2,
53
+ ) -> None:
54
+ super().__init__(agent_id, bus, service_names)
55
+ self._tasks_per_cycle = tasks_per_cycle
56
+ self._announced_tasks: Dict[str, Dict[str, Any]] = {}
57
+
58
+ @property
59
+ def subscribed_types(self) -> Set[MessageType]:
60
+ return {
61
+ MessageType.BELIEF_UPDATE, # absorb peer beliefs
62
+ MessageType.CONTRACT_RESULT, # learn from execution outcomes
63
+ }
64
+
65
+ # ------------------------------------------------------------------
66
+ # Message handling
67
+ # ------------------------------------------------------------------
68
+
69
+ async def _handle_message(self, message: Message) -> None:
70
+ if message.type == MessageType.BELIEF_UPDATE:
71
+ payload = message.payload or {}
72
+ self._absorb_peer_beliefs(message.sender_id, payload.get("beliefs", {}))
73
+
74
+ elif message.type == MessageType.CONTRACT_RESULT:
75
+ self._process_contract_result(message.payload)
76
+ # Re-announce tasks immediately after a contract completes
77
+ # so the pipeline stays full without waiting for idle timeout
78
+ await self._announce_tasks()
79
+
80
+ async def _on_idle(self) -> None:
81
+ self._deliberate()
82
+ await self._announce_tasks()
83
+
84
+ # ------------------------------------------------------------------
85
+ # Task announcement
86
+ # ------------------------------------------------------------------
87
+
88
+ async def _announce_tasks(self) -> None:
89
+ """Announce the top-N tasks based on current belief priorities."""
90
+ targets = self.beliefs.top_k(self._tasks_per_cycle)
91
+ for target in targets:
92
+ risk = self.beliefs.fault_likelihood.get(target, 0.5)
93
+ task = {
94
+ "task_id": str(uuid.uuid4()),
95
+ "target": target,
96
+ "priority": risk,
97
+ "risk_estimate": risk,
98
+ "beliefs": self.beliefs.snapshot(),
99
+ "planner_id": self.agent_id,
100
+ }
101
+ self._announced_tasks[task["task_id"]] = task
102
+ await self._publish(
103
+ Message(MessageType.TASK_ANNOUNCEMENT, self.agent_id, task)
104
+ )
105
+ logger.debug(
106
+ "PlannerAgent %s: announced task for %s (risk=%.3f)",
107
+ self.agent_id, target, risk,
108
+ )
109
+
110
+ # ------------------------------------------------------------------
111
+ # Learning from results
112
+ # ------------------------------------------------------------------
113
+
114
+ def _process_contract_result(self, payload: Dict[str, Any]) -> None:
115
+ if not payload:
116
+ return
117
+ target = payload.get("target", "")
118
+ passed = payload.get("passed", True)
119
+ error = payload.get("error")
120
+ if not target:
121
+ return
122
+ # Evidence: failures/errors increase fault-likelihood
123
+ evidence = 0.0 if passed else (0.5 if error else 1.0)
124
+ self._update_belief_from_result(target, evidence)
125
+ logger.debug(
126
+ "PlannerAgent %s: belief for %s updated to %.3f",
127
+ self.agent_id, target, self.beliefs.fault_likelihood.get(target, 0.5),
128
+ )