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,330 @@
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
+ """BDI (Belief–Desire–Intention) agent base class.
7
+
8
+ Extends :class:`BaseAgent` with explicit BDI mental state as described in
9
+ Chapter 3 of the NeuroAgentTest thesis:
10
+
11
+ * **Beliefs** – probabilistic fault-likelihood estimates per service
12
+ (managed by :class:`BeliefState`).
13
+ * **Desires** – high-level testing objectives (e.g. maximise coverage,
14
+ isolate defects) represented as weighted goal dictionaries.
15
+ * **Intentions** – concrete, committed action plans drawn from the
16
+ deliberation cycle.
17
+
18
+ Each BDI agent owns *two* neural networks (Thesis §3.2):
19
+ * ``predictor_net`` – maps static feature vectors to fault-proneness scores.
20
+ * ``oracle_net`` – maps execution-trace features to anomaly scores.
21
+
22
+ The deliberation cycle runs every time the inbox is drained, calling
23
+ :meth:`_deliberate` to revise intentions based on updated beliefs.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import logging
29
+ from abc import abstractmethod
30
+ from typing import Any, Dict, List, Optional, Set
31
+
32
+ from mannf.core.agents.base import BaseAgent
33
+ from mannf.core.agents.belief_state import BeliefState
34
+ from mannf.core.messaging.bus import MessageBus
35
+ from mannf.core.messaging.messages import Message, MessageType
36
+ from mannf.core.neural import NeuralNetwork
37
+
38
+ logger = logging.getLogger(__name__)
39
+
40
+ # Feature dimensions (must match what each network was built with)
41
+ _PREDICTOR_IN = 8 # static code-metric features
42
+ _ORACLE_IN = 6 # runtime execution-trace features
43
+ _HIDDEN = 16
44
+
45
+
46
+ class BDIAgent(BaseAgent):
47
+ """Agent with explicit BDI mental state and dual neural networks.
48
+
49
+ Parameters
50
+ ----------
51
+ agent_id:
52
+ Unique identifier.
53
+ bus:
54
+ Shared message bus.
55
+ service_names:
56
+ All services in the system under test.
57
+ """
58
+
59
+ def __init__(
60
+ self,
61
+ agent_id: str,
62
+ bus: MessageBus,
63
+ service_names: List[str],
64
+ ) -> None:
65
+ # --- Beliefs -------------------------------------------------------
66
+ self.beliefs = BeliefState(services=service_names)
67
+
68
+ # --- Desires -------------------------------------------------------
69
+ # Weighted objective dictionary; subclasses may override
70
+ self.desires: Dict[str, float] = {
71
+ "maximise_coverage": 1.0,
72
+ "isolate_defects": 1.5,
73
+ "reduce_redundancy": 0.8,
74
+ }
75
+
76
+ # --- Intentions ----------------------------------------------------
77
+ # Current committed action plan (list of (action, args) tuples)
78
+ self.intentions: List[Dict[str, Any]] = []
79
+
80
+ # --- Neural networks -----------------------------------------------
81
+ n_services = max(len(service_names), 1)
82
+ # Predictor: static metrics → per-service fault-proneness scores
83
+ self.predictor_net = NeuralNetwork(
84
+ layer_sizes=[_PREDICTOR_IN, _HIDDEN, _HIDDEN, n_services],
85
+ hidden_activation="relu",
86
+ learning_rate=0.008,
87
+ )
88
+ # Oracle: runtime traces → anomaly score (single output)
89
+ self.oracle_net = NeuralNetwork(
90
+ layer_sizes=[_ORACLE_IN, _HIDDEN, _HIDDEN // 2, 1],
91
+ hidden_activation="relu",
92
+ learning_rate=0.005,
93
+ )
94
+
95
+ # Pass None as the single "network" to BaseAgent (we manage ours)
96
+ super().__init__(agent_id, bus, network=None)
97
+
98
+ # ------------------------------------------------------------------
99
+ # BDI deliberation cycle
100
+ # ------------------------------------------------------------------
101
+
102
+ async def _on_idle(self) -> None:
103
+ """Runs each idle cycle – triggers the deliberation step."""
104
+ self._deliberate()
105
+
106
+ def _deliberate(self) -> None:
107
+ """Revise intentions based on current beliefs and desires.
108
+
109
+ Generates a prioritised action list using the Predictor network to
110
+ score each service, then filters by desire weights.
111
+ """
112
+ import numpy as np
113
+
114
+ # Build feature vector for the Predictor network
115
+ x = self._build_predictor_features()
116
+ scores = self.predictor_net.predict(x)
117
+
118
+ # Blend NN scores with belief-state likelihoods (bounded by conf)
119
+ blended: List[tuple[str, float]] = []
120
+ for i, svc in enumerate(self.beliefs.services):
121
+ nn_score = float(scores[i]) if i < len(scores) else 0.0
122
+ belief = self.beliefs.fault_likelihood.get(svc, 0.5)
123
+ conf = self.beliefs.confidence.get(svc, 0.0)
124
+ # Weighted blend: high confidence → trust beliefs more
125
+ blended_score = conf * belief + (1.0 - conf) * max(0.0, nn_score)
126
+ blended.append((svc, blended_score))
127
+
128
+ # Apply desire weights
129
+ isolate_weight = self.desires.get("isolate_defects", 1.0)
130
+ blended.sort(key=lambda t: t[1] * isolate_weight, reverse=True)
131
+
132
+ # Enforce exploration quota: inject low-belief services if entropy too low
133
+ if self.beliefs.exploration_deficit() > 0:
134
+ prioritised = self.beliefs.prioritise()
135
+ # Move the last entry to the front as an exploration push
136
+ if len(prioritised) > 1:
137
+ blended.insert(0, (prioritised[-1], 0.0))
138
+
139
+ self.intentions = [{"action": "test", "target": svc, "score": sc} for svc, sc in blended]
140
+
141
+ # Schedule agent-state telemetry broadcast (non-blocking, fail-safe)
142
+ try:
143
+ import asyncio
144
+ from mannf.dashboard import telemetry as _tel
145
+ loop = asyncio.get_running_loop()
146
+ loop.create_task(_tel.broadcast_agent_state(self))
147
+ except Exception: # noqa: BLE001
148
+ pass
149
+
150
+ def _build_predictor_features(self) -> "np.ndarray":
151
+ """Build the 8-D static feature vector fed into the Predictor network."""
152
+ import numpy as np
153
+
154
+ vec = np.zeros(_PREDICTOR_IN)
155
+ beliefs = list(self.beliefs.fault_likelihood.values())
156
+ if beliefs:
157
+ vec[0] = float(np.mean(beliefs))
158
+ vec[1] = float(np.std(beliefs)) if len(beliefs) > 1 else 0.0
159
+ vec[2] = float(np.max(beliefs))
160
+ vec[3] = float(np.min(beliefs))
161
+ vec[4] = self.beliefs.entropy()
162
+ vec[5] = float(len(self.beliefs.services)) / 20.0
163
+ # Desire weights
164
+ vec[6] = self.desires.get("isolate_defects", 1.0) / 2.0
165
+ vec[7] = self.desires.get("maximise_coverage", 1.0) / 2.0
166
+ return vec
167
+
168
+ # ------------------------------------------------------------------
169
+ # Belief revision helpers
170
+ # ------------------------------------------------------------------
171
+
172
+ def _update_belief_from_result(self, service: str, evidence: float) -> None:
173
+ """Update fault-likelihood belief and retrain the Predictor network."""
174
+ import numpy as np
175
+
176
+ self.beliefs.update(service, evidence)
177
+
178
+ # Train Predictor to align with updated beliefs
179
+ x = self._build_predictor_features()
180
+ target = np.array([self.beliefs.fault_likelihood.get(s, 0.5) for s in self.beliefs.services])
181
+ self.predictor_net.train_step(x, target)
182
+
183
+ def _update_belief_from_user_feedback(self, service: str) -> None:
184
+ """Incorporate a user-dismissed finding as a false-positive (FP) signal.
185
+
186
+ Injects ``evidence=0.0`` to recalibrate the belief downward and
187
+ retrains the Predictor network so that future deliberation reflects
188
+ the corrected signal.
189
+
190
+ Parameters
191
+ ----------
192
+ service:
193
+ Endpoint key (e.g. ``"POST /api/v1/login"``) whose finding was
194
+ dismissed by the user as a false positive.
195
+ """
196
+ self._update_belief_from_result(service, 0.0)
197
+
198
+ def _score_anomaly(self, trace_features: "np.ndarray") -> float:
199
+ """Use the Oracle network to score an execution trace for anomaly."""
200
+ return float(self.oracle_net.predict(trace_features)[0])
201
+
202
+ # ------------------------------------------------------------------
203
+ # Broadcast own beliefs to peers
204
+ # ------------------------------------------------------------------
205
+
206
+ async def _broadcast_beliefs(self) -> None:
207
+ from mannf.core.messaging.messages import Message, MessageType
208
+
209
+ await self._publish(
210
+ Message(
211
+ MessageType.BELIEF_UPDATE,
212
+ sender_id=self.agent_id,
213
+ payload={
214
+ "beliefs": self.beliefs.snapshot(),
215
+ "entropy": self.beliefs.entropy(),
216
+ },
217
+ )
218
+ )
219
+
220
+ # ------------------------------------------------------------------
221
+ # Incorporate peer beliefs
222
+ # ------------------------------------------------------------------
223
+
224
+ def _absorb_peer_beliefs(self, peer_id: str, peer_beliefs: Dict[str, float]) -> None:
225
+ """Merge peer belief snapshot into own beliefs (confidence-weighted)."""
226
+ for svc, peer_val in peer_beliefs.items():
227
+ own_val = self.beliefs.fault_likelihood.get(svc, 0.5)
228
+ own_conf = self.beliefs.confidence.get(svc, 0.0)
229
+ # Weight peer evidence inversely to own confidence
230
+ weight = 0.5 * (1.0 - own_conf)
231
+ blended = own_val + weight * (peer_val - own_val)
232
+ self.beliefs.fault_likelihood[svc] = max(0.01, min(0.99, blended))
233
+
234
+ # ------------------------------------------------------------------
235
+ # Weight persistence helpers
236
+ # ------------------------------------------------------------------
237
+
238
+ def save_weights(self) -> Dict[str, Any]:
239
+ """Return a serialisable snapshot of this agent's network weights.
240
+
241
+ Returns
242
+ -------
243
+ dict
244
+ ``{"role": str, "layers": List[dict]}`` where each layer dict
245
+ contains ``{"W": List, "b": List}`` (numpy arrays converted via
246
+ ``.tolist()``).
247
+ """
248
+ layers = []
249
+ for net_name in ("predictor_net", "oracle_net"):
250
+ net = getattr(self, net_name, None)
251
+ if net is not None:
252
+ for wb in net.get_weights():
253
+ layers.append({"net": net_name, "W": wb["W"].tolist(), "b": wb["b"].tolist()})
254
+ return {"role": self.__class__.__name__.lower().replace("agent", ""), "layers": layers}
255
+
256
+ def load_weights(self, snapshot: Dict[str, Any]) -> None:
257
+ """Restore network weights from a previously saved snapshot.
258
+
259
+ Incompatible shapes are silently skipped with a warning so that a
260
+ partially-compatible snapshot does not crash the agent.
261
+
262
+ Parameters
263
+ ----------
264
+ snapshot:
265
+ Dict as returned by :meth:`save_weights` (``{"layers": [...]}``)
266
+ or the ``{"W": ndarray, "b": ndarray}`` layer format used by
267
+ :meth:`~mannf.core.neural.NeuralNetwork.set_weights`.
268
+ """
269
+ import numpy as np
270
+
271
+ layers_raw = snapshot.get("layers", [])
272
+
273
+ # Group layers back by network name
274
+ predictor_layers: List[Dict[str, Any]] = []
275
+ oracle_layers: List[Dict[str, Any]] = []
276
+
277
+ for layer in layers_raw:
278
+ net_name = layer.get("net", "")
279
+ entry = {
280
+ "W": np.array(layer["W"], dtype=float) if not isinstance(layer["W"], np.ndarray) else layer["W"],
281
+ "b": np.array(layer["b"], dtype=float) if not isinstance(layer["b"], np.ndarray) else layer["b"],
282
+ }
283
+ if net_name == "oracle_net":
284
+ oracle_layers.append(entry)
285
+ else:
286
+ predictor_layers.append(entry)
287
+
288
+ for net_name, layers in (("predictor_net", predictor_layers), ("oracle_net", oracle_layers)):
289
+ net = getattr(self, net_name, None)
290
+ if net is None or not layers:
291
+ continue
292
+ if len(layers) != len(net.layers):
293
+ logger.warning(
294
+ "Agent %s: skipping %s weight restore — layer count mismatch "
295
+ "(file: %d, network: %d)",
296
+ self.agent_id, net_name, len(layers), len(net.layers),
297
+ )
298
+ continue
299
+ shape_ok = all(
300
+ layers[i]["W"].shape == net.layers[i].W.shape
301
+ and layers[i]["b"].shape == net.layers[i].b.shape
302
+ for i in range(len(layers))
303
+ )
304
+ if not shape_ok:
305
+ logger.warning(
306
+ "Agent %s: skipping %s weight restore — shape mismatch",
307
+ self.agent_id, net_name,
308
+ )
309
+ continue
310
+ try:
311
+ net.set_weights(layers)
312
+ logger.debug("Agent %s: restored %s weights", self.agent_id, net_name)
313
+ except (ValueError, AttributeError) as exc:
314
+ logger.warning(
315
+ "Agent %s: could not restore %s weights: %s",
316
+ self.agent_id, net_name, exc,
317
+ )
318
+
319
+ # ------------------------------------------------------------------
320
+ # Subclass contract
321
+ # ------------------------------------------------------------------
322
+
323
+ @property
324
+ @abstractmethod
325
+ def subscribed_types(self) -> Set[MessageType]: # type: ignore[override]
326
+ ...
327
+
328
+ @abstractmethod
329
+ async def _handle_message(self, message: Message) -> None:
330
+ ...
@@ -0,0 +1,202 @@
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
+ """BDI Belief State for NAT agents.
7
+
8
+ Implements the probabilistic belief revision model described in Chapter 3 of
9
+ the NeuroAgentTest (NAT) thesis. Beliefs represent fault-likelihood
10
+ estimates per service; they are updated with *bounded* delta rules that
11
+ prevent rapid oscillation (a known failure mode documented in Appendix A.3).
12
+
13
+ Key design choices (directly from the thesis):
14
+ * Belief updates are *weighted* by prior confidence.
15
+ * Maximum per-cycle update magnitude is capped at MAX_DELTA.
16
+ * Confidence increases monotonically with evidence accumulation.
17
+ * Entropy thresholds are enforced to prevent premature convergence.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import math
23
+ from dataclasses import dataclass, field
24
+ from typing import Any, Dict, List, Optional
25
+
26
+ import numpy as np
27
+
28
+
29
+ # Thesis-defined safeguards (Section 4.5 & Appendix A.1)
30
+ _MAX_DELTA: float = 0.20 # maximum per-cycle belief change
31
+ _MIN_ENTROPY: float = 0.10 # minimum Shannon entropy (exploration quota)
32
+ _MOMENTUM: float = 0.15 # momentum for smoothing belief updates
33
+ _CONFIDENCE_SHAPE: float = 1.0 # controls how fast confidence grows
34
+
35
+
36
+ @dataclass
37
+ class BeliefState:
38
+ """Probabilistic BDI beliefs about service fault likelihood.
39
+
40
+ Parameters
41
+ ----------
42
+ services:
43
+ Names of all known services in the system under test.
44
+ initial_belief:
45
+ Starting fault-likelihood estimate for all services (0.5 = uncertain).
46
+ """
47
+
48
+ services: List[str]
49
+ initial_belief: float = 0.5
50
+
51
+ # Fault-likelihood per service (0 = definitely safe, 1 = definitely faulty)
52
+ fault_likelihood: Dict[str, float] = field(default_factory=dict)
53
+
54
+ # Confidence in each belief: grows with evidence count
55
+ confidence: Dict[str, float] = field(default_factory=dict)
56
+
57
+ # Update momentum (exponential moving average of deltas per service)
58
+ _momentum: Dict[str, float] = field(default_factory=dict)
59
+
60
+ # Evidence counts per service
61
+ _counts: Dict[str, int] = field(default_factory=dict)
62
+
63
+ def __post_init__(self) -> None:
64
+ for svc in self.services:
65
+ self.fault_likelihood[svc] = self.initial_belief
66
+ self.confidence[svc] = 0.0
67
+ self._momentum[svc] = 0.0
68
+ self._counts[svc] = 0
69
+
70
+ # ------------------------------------------------------------------
71
+ # Belief revision
72
+ # ------------------------------------------------------------------
73
+
74
+ def update(self, service: str, evidence: float, learning_rate: float = 0.15) -> float:
75
+ """Update fault-likelihood belief for *service* with new *evidence*.
76
+
77
+ Parameters
78
+ ----------
79
+ service:
80
+ Name of the service the evidence pertains to.
81
+ evidence:
82
+ Observed fault signal in [0, 1] (1 = fault detected).
83
+ learning_rate:
84
+ Base learning step (will be modulated by confidence).
85
+
86
+ Returns
87
+ -------
88
+ float
89
+ The updated belief value.
90
+ """
91
+ if service not in self.fault_likelihood:
92
+ # Dynamically register unknown service
93
+ self.fault_likelihood[service] = self.initial_belief
94
+ self.confidence[service] = 0.0
95
+ self._momentum[service] = 0.0
96
+ self._counts[service] = 0
97
+
98
+ prior = self.fault_likelihood[service]
99
+ conf = self.confidence[service]
100
+
101
+ # Scale step by (1 - confidence) so high-confidence beliefs change slowly
102
+ effective_lr = learning_rate * (1.0 - conf * 0.8)
103
+
104
+ # Raw gradient toward evidence
105
+ raw_delta = effective_lr * (evidence - prior)
106
+
107
+ # Apply momentum for smooth updates (mitigates oscillation)
108
+ self._momentum[service] = (
109
+ _MOMENTUM * self._momentum[service] + (1.0 - _MOMENTUM) * raw_delta
110
+ )
111
+ delta = self._momentum[service]
112
+
113
+ # Hard cap (Thesis Section 4.5: belief caps limit rapid oscillation)
114
+ delta = max(-_MAX_DELTA, min(_MAX_DELTA, delta))
115
+
116
+ new_belief = max(0.01, min(0.99, prior + delta))
117
+ self.fault_likelihood[service] = new_belief
118
+
119
+ # Update confidence: grows as √evidence_count, capped at 0.95
120
+ n = self._counts[service] + 1
121
+ self._counts[service] = n
122
+ self.confidence[service] = min(0.95, 1.0 - 1.0 / math.sqrt(n + _CONFIDENCE_SHAPE))
123
+
124
+ return new_belief
125
+
126
+ # ------------------------------------------------------------------
127
+ # Prioritisation helpers
128
+ # ------------------------------------------------------------------
129
+
130
+ def prioritise(self) -> List[str]:
131
+ """Return services sorted by descending fault likelihood.
132
+
133
+ Enforces the *exploration quota* from Thesis Section 4.5: services
134
+ with belief below entropy threshold are always included so the system
135
+ doesn't over-exploit high-risk areas.
136
+ """
137
+ ranked = sorted(
138
+ self.services, key=lambda s: self.fault_likelihood.get(s, 0.5), reverse=True
139
+ )
140
+ return ranked
141
+
142
+ def entropy(self) -> float:
143
+ """Shannon entropy of the fault-likelihood distribution.
144
+
145
+ Higher entropy means beliefs are spread evenly (exploratory);
146
+ lower entropy means the system is focused on specific services.
147
+ """
148
+ beliefs = [self.fault_likelihood.get(s, 0.5) for s in self.services]
149
+ if not beliefs:
150
+ return 0.0
151
+ total = sum(beliefs)
152
+ if total == 0:
153
+ return 0.0
154
+ probs = [b / total for b in beliefs]
155
+ return float(-sum(p * math.log(p + 1e-12) for p in probs))
156
+
157
+ def exploration_deficit(self) -> float:
158
+ """How far entropy is below the minimum threshold (0 = healthy)."""
159
+ return max(0.0, _MIN_ENTROPY - self.entropy())
160
+
161
+ def snapshot(self) -> Dict[str, float]:
162
+ """Return a copy of the current fault-likelihood beliefs."""
163
+ return dict(self.fault_likelihood)
164
+
165
+ def export_snapshot(
166
+ self,
167
+ run_id: str = "",
168
+ iteration: int = 0,
169
+ timestamp: Optional[str] = None,
170
+ ) -> Dict[str, Any]:
171
+ """Return a JSON-serialisable snapshot suitable for persistence.
172
+
173
+ Parameters
174
+ ----------
175
+ run_id:
176
+ Autonomous run identifier.
177
+ iteration:
178
+ Loop iteration number when the snapshot was taken.
179
+ timestamp:
180
+ ISO-8601 timestamp string. When omitted, the current UTC time
181
+ is used.
182
+
183
+ Returns
184
+ -------
185
+ dict
186
+ ``{run_id, iteration, timestamp, beliefs: {page_key: fault_likelihood}}``
187
+ """
188
+ import datetime # noqa: PLC0415
189
+
190
+ ts = timestamp or datetime.datetime.now(datetime.timezone.utc).isoformat()
191
+ return {
192
+ "run_id": run_id,
193
+ "iteration": iteration,
194
+ "timestamp": ts,
195
+ "beliefs": dict(self.fault_likelihood),
196
+ "confidence": dict(self.confidence),
197
+ "momentum": dict(self._momentum),
198
+ }
199
+
200
+ def top_k(self, k: int) -> List[str]:
201
+ """Return the *k* services with the highest fault likelihood."""
202
+ return self.prioritise()[:k]