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,267 @@
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
+ """Abstract base class for LLM providers."""
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import logging
12
+ from abc import ABC, abstractmethod
13
+ from typing import Any, Dict, List, Optional
14
+
15
+ from mannf.product.llm.prompts import EDGE_CASE_PROMPT, NL_TEST_AUTHORING_PROMPT, TEST_PLAN_PROMPT
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class LLMProvider(ABC):
21
+ """Abstract base class for LLM providers.
22
+
23
+ All concrete implementations must be safe to use in an async context and
24
+ must never raise exceptions that would abort a scan — errors are logged and
25
+ an empty list is returned instead.
26
+ """
27
+
28
+ @property
29
+ @abstractmethod
30
+ def provider_name(self) -> str:
31
+ """Human-readable provider name (e.g. ``"openai"``)."""
32
+
33
+ @abstractmethod
34
+ def is_available(self) -> bool:
35
+ """Return ``True`` if an API key is configured and the provider can be used."""
36
+
37
+ @abstractmethod
38
+ async def generate(self, prompt: str, **kwargs: Any) -> str:
39
+ """Send *prompt* to the LLM and return the raw response text."""
40
+
41
+ async def generate_test_cases(
42
+ self,
43
+ endpoint_info: Dict[str, Any],
44
+ spec_context: Dict[str, Any],
45
+ n: int = 5,
46
+ ) -> List[Dict[str, Any]]:
47
+ """Generate *n* structured test cases for the given endpoint.
48
+
49
+ Parameters
50
+ ----------
51
+ endpoint_info:
52
+ Dict with keys ``method``, ``path``, ``description``,
53
+ ``parameters``, ``schema``.
54
+ spec_context:
55
+ Additional context from the spec (servers, info, etc.).
56
+ n:
57
+ Number of test cases to request.
58
+
59
+ Returns
60
+ -------
61
+ list[dict]
62
+ Each dict has at least ``inputs`` and ``expected_behavior`` keys.
63
+ Returns an empty list on any error.
64
+ """
65
+ if not self.is_available():
66
+ return []
67
+
68
+ prompt = EDGE_CASE_PROMPT.format(
69
+ n=n,
70
+ method=endpoint_info.get("method", "GET"),
71
+ path=endpoint_info.get("path", "/"),
72
+ description=endpoint_info.get("description", ""),
73
+ parameters=json.dumps(endpoint_info.get("parameters", []), indent=2),
74
+ schema=json.dumps(endpoint_info.get("schema", {}), indent=2),
75
+ )
76
+
77
+ try:
78
+ raw = await self.generate(prompt)
79
+ return self._parse_test_cases(raw)
80
+ except Exception as exc: # noqa: BLE001
81
+ logger.warning("%s: generate_test_cases failed: %s", self.provider_name, exc)
82
+ return []
83
+
84
+ async def generate_test_plan(
85
+ self,
86
+ api_summary: Dict[str, Any],
87
+ ) -> Optional["TestPlan"]:
88
+ """Generate a structured test plan for an entire API specification.
89
+
90
+ Parameters
91
+ ----------
92
+ api_summary:
93
+ Structured dict produced by the ingestor pipeline containing
94
+ endpoint groups, parameter descriptions, auth requirements, and
95
+ schema information.
96
+
97
+ Returns
98
+ -------
99
+ TestPlan | None
100
+ A parsed :class:`~mannf.product.llm.test_plan_models.TestPlan`
101
+ model on success, or ``None`` on error / unavailable provider.
102
+ """
103
+ from mannf.product.llm.test_plan_models import EndpointGroup, Priority, TestPlan # noqa: PLC0415
104
+
105
+ if not self.is_available():
106
+ return None
107
+
108
+ prompt = TEST_PLAN_PROMPT.format(
109
+ api_summary=json.dumps(api_summary, indent=2),
110
+ )
111
+
112
+ try:
113
+ raw = await self.generate(prompt)
114
+ plan_dict = self._parse_json_object(raw)
115
+ if not isinstance(plan_dict, dict):
116
+ logger.warning("%s: generate_test_plan returned non-dict JSON", self.provider_name)
117
+ return None
118
+
119
+ groups: List[EndpointGroup] = []
120
+ for g in plan_dict.get("endpoint_groups", []):
121
+ if not isinstance(g, dict):
122
+ continue
123
+ try:
124
+ groups.append(
125
+ EndpointGroup(
126
+ name=g.get("name", "Unnamed Group"),
127
+ endpoints=g.get("endpoints", []),
128
+ priority=Priority(g.get("priority", "medium")),
129
+ risk_assessment=g.get("risk_assessment", ""),
130
+ suggested_strategies=g.get("suggested_strategies", []),
131
+ edge_cases=g.get("edge_cases", []),
132
+ )
133
+ )
134
+ except Exception as exc: # noqa: BLE001
135
+ logger.debug("%s: skipping malformed endpoint group: %s", self.provider_name, exc)
136
+
137
+ import uuid # noqa: PLC0415
138
+ from datetime import datetime, timezone # noqa: PLC0415
139
+ return TestPlan(
140
+ id=str(uuid.uuid4()),
141
+ created_at=datetime.now(timezone.utc).isoformat(),
142
+ spec_source=api_summary.get("spec_source", ""),
143
+ endpoint_groups=groups,
144
+ estimated_coverage=plan_dict.get("estimated_coverage"),
145
+ )
146
+ except Exception as exc: # noqa: BLE001
147
+ logger.warning("%s: generate_test_plan failed: %s", self.provider_name, exc)
148
+ return None
149
+
150
+ async def generate_from_natural_language(
151
+ self,
152
+ description: str,
153
+ context: Optional[Dict[str, Any]] = None,
154
+ ) -> List[Dict[str, Any]]:
155
+ """Translate a plain-English test description into executable scenarios.
156
+
157
+ Parameters
158
+ ----------
159
+ description:
160
+ Free-form natural-language description of what to test.
161
+ context:
162
+ Optional dict with keys ``base_url``, ``auth_type``, and
163
+ ``known_endpoints`` (list of ``"METHOD /path"`` strings).
164
+
165
+ Returns
166
+ -------
167
+ list[dict]
168
+ Each dict is an executable test scenario compatible with
169
+ :class:`~mannf.core.functional_orchestrator.FunctionalTestOrchestrator`
170
+ (browser type) or
171
+ :class:`~mannf.product.security.scanner.SecurityScanner` (api type).
172
+ Returns an empty list on any error or if the provider is unavailable.
173
+ """
174
+ if not self.is_available():
175
+ return []
176
+
177
+ ctx = context or {}
178
+ known_endpoints = ctx.get("known_endpoints") or []
179
+ if isinstance(known_endpoints, list):
180
+ known_endpoints_str = ", ".join(known_endpoints) if known_endpoints else "none"
181
+ else:
182
+ known_endpoints_str = str(known_endpoints)
183
+
184
+ prompt = NL_TEST_AUTHORING_PROMPT.format(
185
+ description=description,
186
+ base_url=ctx.get("base_url", ""),
187
+ auth_type=ctx.get("auth_type", "none"),
188
+ known_endpoints=known_endpoints_str,
189
+ )
190
+
191
+ try:
192
+ raw = await self.generate(prompt)
193
+ return self._parse_test_cases(raw)
194
+ except Exception as exc: # noqa: BLE001
195
+ logger.warning(
196
+ "%s: generate_from_natural_language failed: %s",
197
+ self.provider_name,
198
+ exc,
199
+ )
200
+ return []
201
+
202
+ # ------------------------------------------------------------------
203
+ # Helpers
204
+ # ------------------------------------------------------------------
205
+
206
+ @staticmethod
207
+ def _parse_test_cases(raw: str) -> List[Dict[str, Any]]:
208
+ """Parse LLM response text into a list of test case dicts.
209
+
210
+ Attempts JSON parsing first; falls back to extracting any JSON array
211
+ found within the text. Returns an empty list if nothing parseable is
212
+ found.
213
+ """
214
+ if not raw or not raw.strip():
215
+ return []
216
+
217
+ # Try direct JSON parse
218
+ try:
219
+ parsed = json.loads(raw.strip())
220
+ if isinstance(parsed, list):
221
+ return [tc for tc in parsed if isinstance(tc, dict)]
222
+ return []
223
+ except json.JSONDecodeError:
224
+ pass
225
+
226
+ # Fallback: find the first [...] block in the text
227
+ start = raw.find("[")
228
+ end = raw.rfind("]")
229
+ if start != -1 and end != -1 and end > start:
230
+ try:
231
+ parsed = json.loads(raw[start : end + 1])
232
+ if isinstance(parsed, list):
233
+ return [tc for tc in parsed if isinstance(tc, dict)]
234
+ except json.JSONDecodeError:
235
+ pass
236
+
237
+ logger.debug("LLMProvider: could not parse response as JSON array")
238
+ return []
239
+
240
+ @staticmethod
241
+ def _parse_json_object(raw: str) -> Any:
242
+ """Parse LLM response text into a JSON object (dict or list).
243
+
244
+ Attempts direct JSON parsing first; falls back to extracting the first
245
+ ``{…}`` block found within the text. Returns ``None`` if nothing
246
+ parseable is found.
247
+ """
248
+ if not raw or not raw.strip():
249
+ return None
250
+
251
+ # Try direct parse
252
+ try:
253
+ return json.loads(raw.strip())
254
+ except json.JSONDecodeError:
255
+ pass
256
+
257
+ # Fallback: find the first {...} block in the text
258
+ start = raw.find("{")
259
+ end = raw.rfind("}")
260
+ if start != -1 and end != -1 and end > start:
261
+ try:
262
+ return json.loads(raw[start : end + 1])
263
+ except json.JSONDecodeError:
264
+ pass
265
+
266
+ logger.debug("LLMProvider: could not parse response as JSON object")
267
+ return None
@@ -0,0 +1,48 @@
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 configuration dataclass."""
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from typing import Optional
12
+
13
+
14
+ @dataclass
15
+ class LLMConfig:
16
+ """Configuration for an LLM provider.
17
+
18
+ Parameters
19
+ ----------
20
+ provider:
21
+ Provider name: ``"openai"`` or ``"anthropic"``.
22
+ model:
23
+ Model identifier (e.g. ``"gpt-4o-mini"`` or ``"claude-3-haiku-20240307"``).
24
+ api_key:
25
+ API key. When ``None``, the provider reads from the appropriate
26
+ environment variable (``OPENAI_API_KEY`` / ``ANTHROPIC_API_KEY``).
27
+ max_tokens:
28
+ Maximum number of tokens to generate per response.
29
+ temperature:
30
+ Sampling temperature (0–2 for OpenAI, 0–1 for Anthropic).
31
+ timeout:
32
+ HTTP request timeout in seconds.
33
+ max_retries:
34
+ Number of retry attempts on transient errors.
35
+ """
36
+
37
+ provider: str = "openai"
38
+ model: str = ""
39
+ api_key: Optional[str] = None
40
+ max_tokens: int = 1024
41
+ temperature: float = 0.7
42
+ timeout: float = 30.0
43
+ max_retries: int = 3
44
+
45
+ def __post_init__(self) -> None:
46
+ if not self.model:
47
+ defaults = {"openai": "gpt-4o-mini", "anthropic": "claude-3-haiku-20240307"}
48
+ self.model = defaults.get(self.provider, "gpt-4o-mini")
@@ -0,0 +1,42 @@
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
+ """Factory function for creating LLM provider instances."""
7
+
8
+ from __future__ import annotations
9
+
10
+ from mannf.product.llm.base import LLMProvider
11
+ from mannf.product.llm.config import LLMConfig
12
+
13
+
14
+ def get_provider(config: LLMConfig) -> LLMProvider:
15
+ """Return an :class:`~mannf.product.llm.base.LLMProvider` instance for *config*.
16
+
17
+ Parameters
18
+ ----------
19
+ config:
20
+ Provider configuration.
21
+
22
+ Returns
23
+ -------
24
+ LLMProvider
25
+ Concrete provider instance.
26
+
27
+ Raises
28
+ ------
29
+ ValueError
30
+ If ``config.provider`` is not a recognised provider name.
31
+ """
32
+ name = (config.provider or "").lower()
33
+ if name == "openai":
34
+ from mannf.product.llm.openai_provider import OpenAIProvider
35
+ return OpenAIProvider(config)
36
+ if name == "anthropic":
37
+ from mannf.product.llm.anthropic_provider import AnthropicProvider
38
+ return AnthropicProvider(config)
39
+ raise ValueError(
40
+ f"Unknown LLM provider {config.provider!r}. "
41
+ "Supported providers: 'openai', 'anthropic'."
42
+ )
@@ -0,0 +1,93 @@
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
+ """OpenAI LLM provider (httpx-based, no openai SDK dependency)."""
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import json
12
+ import logging
13
+ import os
14
+ from typing import Any, Optional
15
+
16
+ import httpx
17
+
18
+ from mannf.product.llm.base import LLMProvider
19
+ from mannf.product.llm.config import LLMConfig
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ _OPENAI_API_URL = "https://api.openai.com/v1/chat/completions"
24
+
25
+
26
+ class OpenAIProvider(LLMProvider):
27
+ """LLM provider that calls the OpenAI Chat Completions API.
28
+
29
+ Uses ``httpx`` directly — no ``openai`` SDK dependency required.
30
+
31
+ Parameters
32
+ ----------
33
+ config:
34
+ :class:`~mannf.product.llm.config.LLMConfig` instance. When ``None``, a
35
+ default config is created (reads ``OPENAI_API_KEY`` from env).
36
+ """
37
+
38
+ def __init__(self, config: Optional[LLMConfig] = None) -> None:
39
+ if config is None:
40
+ config = LLMConfig(provider="openai")
41
+ self._config = config
42
+ self._api_key: str = config.api_key or os.environ.get("OPENAI_API_KEY", "")
43
+
44
+ @property
45
+ def provider_name(self) -> str:
46
+ return "openai"
47
+
48
+ def is_available(self) -> bool:
49
+ return bool(self._api_key)
50
+
51
+ async def generate(self, prompt: str, **kwargs: Any) -> str:
52
+ """Send *prompt* to the OpenAI Chat Completions API and return the response text."""
53
+ if not self.is_available():
54
+ logger.warning("OpenAIProvider: no API key configured")
55
+ return ""
56
+
57
+ headers = {
58
+ "Authorization": f"Bearer {self._api_key}",
59
+ "Content-Type": "application/json",
60
+ }
61
+ payload = {
62
+ "model": self._config.model,
63
+ "messages": [{"role": "user", "content": prompt}],
64
+ "max_tokens": self._config.max_tokens,
65
+ "temperature": self._config.temperature,
66
+ }
67
+
68
+ last_exc: Exception = RuntimeError("No attempts made")
69
+ for attempt in range(self._config.max_retries):
70
+ try:
71
+ async with httpx.AsyncClient(timeout=self._config.timeout) as client:
72
+ resp = await client.post(_OPENAI_API_URL, headers=headers, json=payload)
73
+ resp.raise_for_status()
74
+ data = resp.json()
75
+ return data["choices"][0]["message"]["content"]
76
+ except (httpx.HTTPStatusError, httpx.RequestError, KeyError, json.JSONDecodeError) as exc:
77
+ last_exc = exc
78
+ if isinstance(exc, httpx.HTTPStatusError) and exc.response.status_code in (400, 401, 403):
79
+ # Non-retryable
80
+ logger.error("OpenAIProvider: non-retryable error %s", exc)
81
+ return ""
82
+ wait = 2 ** attempt
83
+ logger.warning(
84
+ "OpenAIProvider: attempt %d/%d failed (%s), retrying in %ds",
85
+ attempt + 1,
86
+ self._config.max_retries,
87
+ exc,
88
+ wait,
89
+ )
90
+ await asyncio.sleep(wait)
91
+
92
+ logger.error("OpenAIProvider: all retries exhausted: %s", last_exc)
93
+ return ""