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,273 @@
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
+ """LLM-powered root cause analysis for autonomous loop failures (Phase 6.5).
7
+
8
+ Calls the configured LLM provider with :data:`ROOT_CAUSE_ANALYSIS_PROMPT`
9
+ (defined in :mod:`mannf.product.llm.prompts`) to suggest likely root causes
10
+ for new failures.
11
+
12
+ Output schema (JSON returned by LLM, validated here)::
13
+
14
+ {
15
+ "likely_cause": "...",
16
+ "suggested_fix": "...",
17
+ "confidence": 0.8,
18
+ "files_to_check": ["path/to/file.py", ...]
19
+ }
20
+
21
+ Missing or invalid fields are filled with safe defaults so the caller always
22
+ gets a usable dict.
23
+
24
+ The analyzer is gated by:
25
+ - LLM provider availability (returns ``None`` immediately when not configured)
26
+ - Severity threshold (only called for failures whose ``fault_likelihood``
27
+ exceeds *min_fault_likelihood*)
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import json
33
+ import logging
34
+ from typing import Any, Dict, List, Optional
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+ # Minimum fault_likelihood to trigger root cause analysis (avoids burning LLM
39
+ # tokens on low-probability noise).
40
+ _DEFAULT_MIN_FAULT_LIKELIHOOD = 0.6
41
+
42
+
43
+ class RootCauseResult:
44
+ """Structured result from the LLM root cause analysis call.
45
+
46
+ Attributes
47
+ ----------
48
+ likely_cause : str
49
+ suggested_fix : str
50
+ confidence : float
51
+ files_to_check : list[str]
52
+ raw_response : str
53
+ The raw string returned by the LLM (for debugging / auditing).
54
+ """
55
+
56
+ def __init__(
57
+ self,
58
+ likely_cause: str,
59
+ suggested_fix: str,
60
+ confidence: float,
61
+ files_to_check: List[str],
62
+ raw_response: str = "",
63
+ ) -> None:
64
+ self.likely_cause = likely_cause
65
+ self.suggested_fix = suggested_fix
66
+ self.confidence = max(0.0, min(1.0, float(confidence)))
67
+ self.files_to_check = files_to_check
68
+ self.raw_response = raw_response
69
+
70
+ def to_dict(self) -> Dict[str, Any]:
71
+ return {
72
+ "likely_cause": self.likely_cause,
73
+ "suggested_fix": self.suggested_fix,
74
+ "confidence": self.confidence,
75
+ "files_to_check": self.files_to_check,
76
+ }
77
+
78
+
79
+ def _parse_llm_response(raw: str) -> Dict[str, Any]:
80
+ """Extract the JSON object from the LLM response string."""
81
+ # Strip markdown fences if present
82
+ text = raw.strip()
83
+ if text.startswith("```"):
84
+ lines = text.splitlines()
85
+ # Drop first line (```json or ```) and last line (```)
86
+ inner = [l for l in lines[1:] if l.strip() != "```"]
87
+ text = "\n".join(inner).strip()
88
+ return json.loads(text)
89
+
90
+
91
+ class RootCauseAnalyzer:
92
+ """Analyse failures with an LLM to suggest root causes.
93
+
94
+ Parameters
95
+ ----------
96
+ llm_provider : object or None
97
+ An initialised LLM provider instance that exposes an
98
+ ``acomplete(prompt: str) -> str`` (async) or
99
+ ``complete(prompt: str) -> str`` (sync) coroutine / method.
100
+ When ``None``, :meth:`analyze` returns ``None`` immediately.
101
+ min_fault_likelihood : float
102
+ Minimum fault likelihood required to trigger analysis.
103
+ """
104
+
105
+ def __init__(
106
+ self,
107
+ llm_provider: Optional[Any] = None,
108
+ min_fault_likelihood: float = _DEFAULT_MIN_FAULT_LIKELIHOOD,
109
+ ) -> None:
110
+ self._provider = llm_provider
111
+ self._min_fault_likelihood = min_fault_likelihood
112
+
113
+ # ------------------------------------------------------------------
114
+ # Public API
115
+ # ------------------------------------------------------------------
116
+
117
+ async def analyze(
118
+ self,
119
+ failure: Dict[str, Any],
120
+ fault_likelihood: float = 1.0,
121
+ belief_history: Optional[List[Dict[str, Any]]] = None,
122
+ dom_snapshot: str = "",
123
+ scenario_steps: Optional[List[str]] = None,
124
+ page_metadata: Optional[Dict[str, Any]] = None,
125
+ ) -> Optional[RootCauseResult]:
126
+ """Analyse a single failure and return a :class:`RootCauseResult`.
127
+
128
+ Returns ``None`` when LLM is unavailable or fault_likelihood is below
129
+ threshold.
130
+
131
+ Parameters
132
+ ----------
133
+ failure:
134
+ Failure dict with at least ``url``, ``error``, ``task_id`` keys.
135
+ fault_likelihood:
136
+ Current belief state fault_likelihood for the page (0–1).
137
+ belief_history:
138
+ Optional list of prior belief snapshots for this page, each a
139
+ dict with ``iteration``, ``fault_likelihood``, ``timestamp``.
140
+ dom_snapshot:
141
+ Optional DOM/HTML snapshot string for context.
142
+ scenario_steps:
143
+ Optional list of step descriptions from the failing scenario.
144
+ page_metadata:
145
+ Optional metadata dict about the page.
146
+ """
147
+ if self._provider is None:
148
+ return None
149
+
150
+ if fault_likelihood < self._min_fault_likelihood:
151
+ return None
152
+
153
+ prompt = self._build_prompt(
154
+ failure=failure,
155
+ fault_likelihood=fault_likelihood,
156
+ belief_history=belief_history or [],
157
+ dom_snapshot=dom_snapshot,
158
+ scenario_steps=scenario_steps or [],
159
+ page_metadata=page_metadata or {},
160
+ )
161
+
162
+ try:
163
+ raw = await self._call_llm(prompt)
164
+ return self._parse_result(raw)
165
+ except Exception as exc: # noqa: BLE001
166
+ logger.warning("RootCauseAnalyzer: LLM call failed — %s", exc)
167
+ return None
168
+
169
+ def analyze_sync(
170
+ self,
171
+ failure: Dict[str, Any],
172
+ fault_likelihood: float = 1.0,
173
+ belief_history: Optional[List[Dict[str, Any]]] = None,
174
+ dom_snapshot: str = "",
175
+ scenario_steps: Optional[List[str]] = None,
176
+ page_metadata: Optional[Dict[str, Any]] = None,
177
+ ) -> Optional[RootCauseResult]:
178
+ """Synchronous variant of :meth:`analyze`."""
179
+ if self._provider is None:
180
+ return None
181
+ if fault_likelihood < self._min_fault_likelihood:
182
+ return None
183
+
184
+ prompt = self._build_prompt(
185
+ failure=failure,
186
+ fault_likelihood=fault_likelihood,
187
+ belief_history=belief_history or [],
188
+ dom_snapshot=dom_snapshot,
189
+ scenario_steps=scenario_steps or [],
190
+ page_metadata=page_metadata or {},
191
+ )
192
+
193
+ try:
194
+ raw = self._call_llm_sync(prompt)
195
+ return self._parse_result(raw)
196
+ except Exception as exc: # noqa: BLE001
197
+ logger.warning("RootCauseAnalyzer: sync LLM call failed — %s", exc)
198
+ return None
199
+
200
+ # ------------------------------------------------------------------
201
+ # Internals
202
+ # ------------------------------------------------------------------
203
+
204
+ def _build_prompt(
205
+ self,
206
+ failure: Dict[str, Any],
207
+ fault_likelihood: float,
208
+ belief_history: List[Dict[str, Any]],
209
+ dom_snapshot: str,
210
+ scenario_steps: List[str],
211
+ page_metadata: Dict[str, Any],
212
+ ) -> str:
213
+ from mannf.product.llm.prompts import ROOT_CAUSE_ANALYSIS_PROMPT # noqa: PLC0415
214
+
215
+ belief_history_str = (
216
+ json.dumps(belief_history, indent=2) if belief_history else "No prior history available"
217
+ )
218
+ scenario_steps_str = (
219
+ "\n".join(f" {i + 1}. {s}" for i, s in enumerate(scenario_steps))
220
+ if scenario_steps
221
+ else " (no steps recorded)"
222
+ )
223
+ dom_snippet = (dom_snapshot or "")[:2000] # cap DOM context length
224
+ page_meta_str = json.dumps(page_metadata) if page_metadata else "{}"
225
+
226
+ return ROOT_CAUSE_ANALYSIS_PROMPT.format(
227
+ url=failure.get("url", ""),
228
+ page=failure.get("page", ""),
229
+ error=failure.get("error", ""),
230
+ task_id=failure.get("task_id", ""),
231
+ fault_likelihood=round(fault_likelihood, 3),
232
+ belief_history=belief_history_str,
233
+ dom_snapshot=dom_snippet,
234
+ scenario_steps=scenario_steps_str,
235
+ page_metadata=page_meta_str,
236
+ )
237
+
238
+ async def _call_llm(self, prompt: str) -> str:
239
+ """Invoke the LLM provider (async)."""
240
+ provider = self._provider
241
+ if hasattr(provider, "acomplete"):
242
+ return await provider.acomplete(prompt)
243
+ if hasattr(provider, "complete"):
244
+ result = provider.complete(prompt)
245
+ # Handle both sync and coroutine returns
246
+ if hasattr(result, "__await__"):
247
+ return await result
248
+ return result
249
+ raise AttributeError("LLM provider has no 'acomplete' or 'complete' method")
250
+
251
+ def _call_llm_sync(self, prompt: str) -> str:
252
+ """Invoke the LLM provider (sync)."""
253
+ provider = self._provider
254
+ if hasattr(provider, "complete"):
255
+ return provider.complete(prompt)
256
+ raise AttributeError("LLM provider has no 'complete' method for sync use")
257
+
258
+ @staticmethod
259
+ def _parse_result(raw: str) -> RootCauseResult:
260
+ """Parse the LLM JSON response into a :class:`RootCauseResult`."""
261
+ try:
262
+ data = _parse_llm_response(raw)
263
+ except (json.JSONDecodeError, ValueError):
264
+ logger.warning("RootCauseAnalyzer: could not parse LLM response as JSON")
265
+ data = {}
266
+
267
+ return RootCauseResult(
268
+ likely_cause=str(data.get("likely_cause", "Unable to determine root cause")),
269
+ suggested_fix=str(data.get("suggested_fix", "Investigate the error manually")),
270
+ confidence=float(data.get("confidence", 0.0)),
271
+ files_to_check=list(data.get("files_to_check", [])),
272
+ raw_response=raw,
273
+ )
@@ -0,0 +1,16 @@
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
+ """Distributed sub-package for the Multi-Agent Neural Network Framework."""
7
+
8
+ from mannf.core.distributed.endpoint import Endpoint, EndpointRegistry
9
+ from mannf.core.distributed.system_under_test import SystemUnderTest, MockDistributedSystem
10
+
11
+ __all__ = [
12
+ "Endpoint",
13
+ "EndpointRegistry",
14
+ "SystemUnderTest",
15
+ "MockDistributedSystem",
16
+ ]
@@ -0,0 +1,139 @@
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
+ """Distributed endpoint model.
7
+
8
+ An :class:`Endpoint` represents one service/component in the system under test.
9
+ It stores connection metadata and exposes a uniform *invoke* interface.
10
+
11
+ Supported protocol values include:
12
+
13
+ * ``"http"`` / ``"https"`` — REST HTTP APIs
14
+ * ``"grpc"`` — gRPC services (plaintext)
15
+ * ``"grpcs"`` — gRPC services (TLS)
16
+ * ``"ws"`` — WebSocket connections (plaintext)
17
+ * ``"wss"`` — WebSocket connections (TLS)
18
+ * ``"kafka"`` — Kafka broker
19
+ * ``"tcp"`` — generic TCP
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from dataclasses import dataclass, field
25
+ from typing import Any, Callable, Dict, List, Optional
26
+
27
+ # Protocol families for quick categorisation
28
+ _HTTP_PROTOCOLS = frozenset({"http", "https"})
29
+ _GRPC_PROTOCOLS = frozenset({"grpc", "grpcs"})
30
+ _WS_PROTOCOLS = frozenset({"ws", "wss", "websocket", "websockets"})
31
+ _ASYNC_PROTOCOLS = frozenset({"kafka", "amqp", "amqps", "mqtt"})
32
+
33
+
34
+ @dataclass
35
+ class Endpoint:
36
+ """Metadata about one distributed service endpoint.
37
+
38
+ Parameters
39
+ ----------
40
+ name:
41
+ Logical name of the service (e.g. ``"auth-service"``).
42
+ host:
43
+ Hostname or IP address.
44
+ port:
45
+ TCP port.
46
+ protocol:
47
+ Application protocol — one of ``"http"``, ``"https"``, ``"grpc"``,
48
+ ``"grpcs"``, ``"ws"``, ``"wss"``, ``"kafka"``, ``"tcp"``.
49
+ health_check_path:
50
+ Path used to verify the service is alive (HTTP/WebSocket only).
51
+ tags:
52
+ Arbitrary labels for filtering/grouping.
53
+ """
54
+
55
+ name: str
56
+ host: str
57
+ port: int
58
+ protocol: str = "http"
59
+ health_check_path: str = "/health"
60
+ tags: Dict[str, str] = field(default_factory=dict)
61
+
62
+ # ------------------------------------------------------------------
63
+ # Protocol family helpers
64
+ # ------------------------------------------------------------------
65
+
66
+ @property
67
+ def is_http(self) -> bool:
68
+ """Return True for HTTP/HTTPS endpoints."""
69
+ return self.protocol in _HTTP_PROTOCOLS
70
+
71
+ @property
72
+ def is_grpc(self) -> bool:
73
+ """Return True for gRPC endpoints (plain or TLS)."""
74
+ return self.protocol in _GRPC_PROTOCOLS
75
+
76
+ @property
77
+ def is_websocket(self) -> bool:
78
+ """Return True for WebSocket endpoints (plain or TLS)."""
79
+ return self.protocol in _WS_PROTOCOLS
80
+
81
+ @property
82
+ def is_async(self) -> bool:
83
+ """Return True for async/event-driven endpoints (Kafka, AMQP, MQTT)."""
84
+ return self.protocol in _ASYNC_PROTOCOLS
85
+
86
+ # ------------------------------------------------------------------
87
+ # URL builders
88
+ # ------------------------------------------------------------------
89
+
90
+ @property
91
+ def base_url(self) -> str:
92
+ if self.protocol == "http":
93
+ return f"http://{self.host}:{self.port}"
94
+ if self.protocol == "https":
95
+ return f"https://{self.host}:{self.port}"
96
+ if self.protocol in ("grpc", "grpcs"):
97
+ return f"{self.host}:{self.port}"
98
+ if self.protocol == "ws":
99
+ return f"ws://{self.host}:{self.port}"
100
+ if self.protocol in ("wss", "websocket", "websockets"):
101
+ return f"wss://{self.host}:{self.port}"
102
+ return f"{self.protocol}://{self.host}:{self.port}"
103
+
104
+ def __str__(self) -> str:
105
+ return f"{self.name}@{self.base_url}"
106
+
107
+
108
+ @dataclass
109
+ class EndpointRegistry:
110
+ """Holds all known endpoints for the system under test."""
111
+
112
+ _endpoints: Dict[str, Endpoint] = field(default_factory=dict)
113
+
114
+ def register(self, endpoint: Endpoint) -> None:
115
+ self._endpoints[endpoint.name] = endpoint
116
+
117
+ def get(self, name: str) -> Optional[Endpoint]:
118
+ return self._endpoints.get(name)
119
+
120
+ def all(self) -> list[Endpoint]:
121
+ return list(self._endpoints.values())
122
+
123
+ def by_protocol(self, protocol: str) -> list[Endpoint]:
124
+ """Return all endpoints with the given *protocol*."""
125
+ return [e for e in self._endpoints.values() if e.protocol == protocol]
126
+
127
+ def grpc_endpoints(self) -> list[Endpoint]:
128
+ """Return all gRPC endpoints."""
129
+ return [e for e in self._endpoints.values() if e.is_grpc]
130
+
131
+ def websocket_endpoints(self) -> list[Endpoint]:
132
+ """Return all WebSocket endpoints."""
133
+ return [e for e in self._endpoints.values() if e.is_websocket]
134
+
135
+ def by_tag(self, key: str, value: str) -> list[Endpoint]:
136
+ return [e for e in self._endpoints.values() if e.tags.get(key) == value]
137
+
138
+ def __len__(self) -> int:
139
+ return len(self._endpoints)
@@ -0,0 +1,207 @@
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
+ """System-Under-Test (SUT) abstraction.
7
+
8
+ :class:`SystemUnderTest` is the interface the agents use to interact with the
9
+ distributed system being tested. The concrete class provided here is
10
+ :class:`MockDistributedSystem`, a fully in-process simulation of a small
11
+ microservice landscape that is ready to use without any network infrastructure.
12
+
13
+ The mock deliberately injects configurable faults so the framework can
14
+ demonstrate its ability to find them adaptively.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import asyncio
20
+ import logging
21
+ import random
22
+ import time
23
+ from abc import ABC, abstractmethod
24
+ from dataclasses import dataclass, field
25
+ from typing import Any, Dict, List, Optional
26
+
27
+ from mannf.core.distributed.endpoint import Endpoint, EndpointRegistry
28
+ from mannf.core.testing.models import TestCase, TestResult
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # Abstract interface
35
+ # ---------------------------------------------------------------------------
36
+
37
+ class SystemUnderTest(ABC):
38
+ """Abstract base class for any system the framework will test."""
39
+
40
+ @abstractmethod
41
+ async def execute(self, test_case: TestCase) -> TestResult:
42
+ """Execute *test_case* and return its result."""
43
+
44
+ @property
45
+ @abstractmethod
46
+ def registry(self) -> EndpointRegistry:
47
+ """Return the endpoint registry for this system."""
48
+
49
+
50
+ # ---------------------------------------------------------------------------
51
+ # Mock distributed system
52
+ # ---------------------------------------------------------------------------
53
+
54
+ @dataclass
55
+ class _ServiceConfig:
56
+ """Configuration for one simulated microservice."""
57
+
58
+ name: str
59
+ failure_rate: float = 0.0 # probability of returning a failure response
60
+ error_rate: float = 0.0 # probability of raising an exception
61
+ latency_ms: float = 10.0 # base latency
62
+ latency_jitter_ms: float = 5.0 # latency jitter (uniform)
63
+
64
+
65
+ class MockDistributedSystem(SystemUnderTest):
66
+ """An in-process simulation of a multi-service distributed system.
67
+
68
+ Services can be added with configurable failure/error rates so that the
69
+ adaptive testing agents can demonstrate their ability to converge on
70
+ high-failure areas.
71
+
72
+ Parameters
73
+ ----------
74
+ seed:
75
+ Optional random seed for reproducible simulations.
76
+ """
77
+
78
+ # Default service topology
79
+ _DEFAULT_SERVICES: List[_ServiceConfig] = [
80
+ _ServiceConfig("api-gateway", failure_rate=0.05, error_rate=0.01, latency_ms=15),
81
+ _ServiceConfig("auth-service", failure_rate=0.10, error_rate=0.02, latency_ms=20),
82
+ _ServiceConfig("user-service", failure_rate=0.03, error_rate=0.01, latency_ms=10),
83
+ _ServiceConfig("order-service", failure_rate=0.15, error_rate=0.03, latency_ms=25),
84
+ _ServiceConfig("payment-service", failure_rate=0.20, error_rate=0.05, latency_ms=30),
85
+ _ServiceConfig("inventory-service",failure_rate=0.08, error_rate=0.02, latency_ms=12),
86
+ _ServiceConfig("notification-svc", failure_rate=0.04, error_rate=0.01, latency_ms=8),
87
+ _ServiceConfig("analytics-service",failure_rate=0.06, error_rate=0.01, latency_ms=50),
88
+ ]
89
+
90
+ def __init__(
91
+ self,
92
+ services: Optional[List[_ServiceConfig]] = None,
93
+ seed: Optional[int] = None,
94
+ ) -> None:
95
+ if seed is not None:
96
+ random.seed(seed)
97
+
98
+ self._services: Dict[str, _ServiceConfig] = {}
99
+ self._registry = EndpointRegistry()
100
+
101
+ for svc in services or self._DEFAULT_SERVICES:
102
+ self._services[svc.name] = svc
103
+ self._registry.register(
104
+ Endpoint(
105
+ name=svc.name,
106
+ host="localhost",
107
+ port=8000 + len(self._services),
108
+ protocol="http",
109
+ )
110
+ )
111
+
112
+ self._call_log: List[Dict[str, Any]] = []
113
+
114
+ # -- SystemUnderTest interface ----------------------------------------
115
+
116
+ @property
117
+ def registry(self) -> EndpointRegistry:
118
+ return self._registry
119
+
120
+ async def execute(self, test_case: TestCase) -> TestResult:
121
+ """Simulate executing *test_case* against the target service."""
122
+ start = time.monotonic()
123
+
124
+ service = self._services.get(test_case.target)
125
+ if service is None:
126
+ return TestResult(
127
+ test_case_id=test_case.id,
128
+ passed=False,
129
+ error=f"Unknown service: {test_case.target}",
130
+ execution_time_ms=(time.monotonic() - start) * 1000,
131
+ )
132
+
133
+ # Simulate network latency
134
+ jitter = random.uniform(0, service.latency_jitter_ms)
135
+ await asyncio.sleep((service.latency_ms + jitter) / 1000.0)
136
+
137
+ execution_time_ms = (time.monotonic() - start) * 1000
138
+
139
+ # Simulate errors (exceptions)
140
+ if random.random() < service.error_rate:
141
+ error_msg = f"Simulated error in {service.name}: connection timeout"
142
+ self._log(test_case, passed=False, error=error_msg)
143
+ return TestResult(
144
+ test_case_id=test_case.id,
145
+ passed=False,
146
+ error=error_msg,
147
+ execution_time_ms=execution_time_ms,
148
+ )
149
+
150
+ # Simulate functional failures
151
+ passed = random.random() >= service.failure_rate
152
+
153
+ output = {
154
+ "service": service.name,
155
+ "status": "ok" if passed else "error",
156
+ "inputs_received": test_case.inputs,
157
+ }
158
+ self._log(test_case, passed=passed)
159
+ return TestResult(
160
+ test_case_id=test_case.id,
161
+ passed=passed,
162
+ actual_output=output,
163
+ execution_time_ms=execution_time_ms,
164
+ )
165
+
166
+ # -- Introspection helpers --------------------------------------------
167
+
168
+ def _log(
169
+ self,
170
+ test_case: TestCase,
171
+ passed: bool,
172
+ error: Optional[str] = None,
173
+ ) -> None:
174
+ self._call_log.append(
175
+ {
176
+ "target": test_case.target,
177
+ "passed": passed,
178
+ "error": error,
179
+ }
180
+ )
181
+
182
+ def call_log(self) -> List[Dict[str, Any]]:
183
+ """Return a copy of the call log (for testing / observability)."""
184
+ return list(self._call_log)
185
+
186
+ def failure_counts(self) -> Dict[str, int]:
187
+ """Return per-service failure+error counts."""
188
+ counts: Dict[str, int] = {}
189
+ for entry in self._call_log:
190
+ if not entry["passed"] or entry["error"]:
191
+ counts[entry["target"]] = counts.get(entry["target"], 0) + 1
192
+ return counts
193
+
194
+ def service_names(self) -> List[str]:
195
+ return list(self._services.keys())
196
+
197
+ def add_service(self, config: _ServiceConfig) -> None:
198
+ """Dynamically register an additional simulated service."""
199
+ self._services[config.name] = config
200
+ self._registry.register(
201
+ Endpoint(
202
+ name=config.name,
203
+ host="localhost",
204
+ port=8000 + len(self._services),
205
+ protocol="http",
206
+ )
207
+ )