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.
- mannf/__init__.py +33 -0
- mannf/__main__.py +10 -0
- mannf/_version.py +8 -0
- mannf/agents/__init__.py +7 -0
- mannf/agents/analyzer_agent.py +9 -0
- mannf/agents/base.py +9 -0
- mannf/agents/bdi_agent.py +9 -0
- mannf/agents/belief_state.py +9 -0
- mannf/agents/coordinator_agent.py +9 -0
- mannf/agents/executor_agent.py +9 -0
- mannf/agents/monitor_agent.py +9 -0
- mannf/agents/oracle_agent.py +9 -0
- mannf/agents/planner_agent.py +9 -0
- mannf/agents/test_agent.py +9 -0
- mannf/anomaly/__init__.py +7 -0
- mannf/anomaly/enhanced_detector.py +9 -0
- mannf/cli.py +9 -0
- mannf/core/__init__.py +26 -0
- mannf/core/agents/__init__.py +52 -0
- mannf/core/agents/accessibility_scanner_agent.py +245 -0
- mannf/core/agents/analyzer_agent.py +224 -0
- mannf/core/agents/autonomous_loop_agent.py +1086 -0
- mannf/core/agents/autonomous_loop_models.py +62 -0
- mannf/core/agents/autonomous_run_differ.py +427 -0
- mannf/core/agents/base.py +128 -0
- mannf/core/agents/bdi_agent.py +330 -0
- mannf/core/agents/belief_state.py +202 -0
- mannf/core/agents/browser_coordinator_agent.py +224 -0
- mannf/core/agents/browser_executor_agent.py +410 -0
- mannf/core/agents/coordinator_agent.py +262 -0
- mannf/core/agents/executor_agent.py +222 -0
- mannf/core/agents/monitor_agent.py +188 -0
- mannf/core/agents/oracle_agent.py +150 -0
- mannf/core/agents/performance_testing_agent.py +279 -0
- mannf/core/agents/planner_agent.py +128 -0
- mannf/core/agents/test_agent.py +249 -0
- mannf/core/agents/visual_regression_agent.py +311 -0
- mannf/core/agents/web_crawler_agent.py +510 -0
- mannf/core/agents/worker_pool.py +366 -0
- mannf/core/anomaly/__init__.py +14 -0
- mannf/core/anomaly/enhanced_detector.py +541 -0
- mannf/core/browser/__init__.py +63 -0
- mannf/core/browser/accessibility_scanner.py +424 -0
- mannf/core/browser/discovery_model.py +178 -0
- mannf/core/browser/dom_snapshot.py +349 -0
- mannf/core/browser/ingestor_bridge.py +371 -0
- mannf/core/browser/performance_metrics.py +217 -0
- mannf/core/browser/reflection_analyzer.py +442 -0
- mannf/core/browser/scenario_generator.py +1100 -0
- mannf/core/browser/security_scenario_generator.py +695 -0
- mannf/core/browser/visual_comparer.py +159 -0
- mannf/core/diagnostics/__init__.py +28 -0
- mannf/core/diagnostics/failure_clusterer.py +211 -0
- mannf/core/diagnostics/flake_detector.py +233 -0
- mannf/core/diagnostics/root_cause_analyzer.py +273 -0
- mannf/core/distributed/__init__.py +16 -0
- mannf/core/distributed/endpoint.py +139 -0
- mannf/core/distributed/system_under_test.py +207 -0
- mannf/core/functional_orchestrator.py +428 -0
- mannf/core/messaging/__init__.py +11 -0
- mannf/core/messaging/bus.py +113 -0
- mannf/core/messaging/messages.py +89 -0
- mannf/core/nat_orchestrator.py +342 -0
- mannf/core/neural/__init__.py +183 -0
- mannf/core/orchestrator.py +272 -0
- mannf/core/prioritization/__init__.py +17 -0
- mannf/core/prioritization/adaptive_controller.py +509 -0
- mannf/core/prioritization/belief_prioritizer.py +231 -0
- mannf/core/prioritization/risk_scorer.py +430 -0
- mannf/core/reporting/__init__.py +12 -0
- mannf/core/reporting/unified_report.py +664 -0
- mannf/core/testing/__init__.py +17 -0
- mannf/core/testing/adaptive_controller.py +149 -0
- mannf/core/testing/models.py +179 -0
- mannf/core/validation/__init__.py +10 -0
- mannf/core/validation/self_validation_runner.py +180 -0
- mannf/dashboard/__init__.py +7 -0
- mannf/dashboard/app.py +9 -0
- mannf/dashboard/models.py +9 -0
- mannf/dashboard/static/index.html +2538 -0
- mannf/dashboard/telemetry.py +9 -0
- mannf/distributed/__init__.py +7 -0
- mannf/distributed/endpoint.py +9 -0
- mannf/distributed/system_under_test.py +9 -0
- mannf/healing/__init__.py +7 -0
- mannf/healing/graphql_schema_diff.py +9 -0
- mannf/healing/healer.py +9 -0
- mannf/healing/models.py +9 -0
- mannf/healing/schema_diff.py +9 -0
- mannf/integrations/__init__.py +7 -0
- mannf/integrations/auth.py +9 -0
- mannf/integrations/graphql_parser.py +9 -0
- mannf/integrations/graphql_sut.py +9 -0
- mannf/integrations/http_sut.py +9 -0
- mannf/integrations/openapi_parser.py +9 -0
- mannf/integrations/postman_parser.py +9 -0
- mannf/llm/__init__.py +7 -0
- mannf/llm/anthropic_provider.py +9 -0
- mannf/llm/base.py +9 -0
- mannf/llm/config.py +9 -0
- mannf/llm/factory.py +9 -0
- mannf/llm/openai_provider.py +9 -0
- mannf/llm/prompts.py +9 -0
- mannf/messaging/__init__.py +7 -0
- mannf/messaging/bus.py +9 -0
- mannf/messaging/messages.py +9 -0
- mannf/nat_orchestrator.py +9 -0
- mannf/neural/__init__.py +7 -0
- mannf/orchestrator.py +9 -0
- mannf/prioritization/__init__.py +7 -0
- mannf/prioritization/adaptive_controller.py +9 -0
- mannf/prioritization/belief_prioritizer.py +9 -0
- mannf/prioritization/risk_scorer.py +9 -0
- mannf/product/__init__.py +29 -0
- mannf/product/admin/__init__.py +3 -0
- mannf/product/admin/routes.py +514 -0
- mannf/product/auth/__init__.py +5 -0
- mannf/product/auth/saml.py +212 -0
- mannf/product/billing/__init__.py +5 -0
- mannf/product/billing/audit.py +160 -0
- mannf/product/billing/feature_gates.py +180 -0
- mannf/product/billing/metering.py +179 -0
- mannf/product/billing/notifications.py +181 -0
- mannf/product/billing/plans.py +133 -0
- mannf/product/billing/rate_limits.py +35 -0
- mannf/product/billing/stripe_billing.py +906 -0
- mannf/product/billing/tenant_auth.py +233 -0
- mannf/product/billing/tenant_manager.py +873 -0
- mannf/product/cli.py +3900 -0
- mannf/product/cli_admin.py +408 -0
- mannf/product/dashboard/__init__.py +61 -0
- mannf/product/dashboard/app.py +3567 -0
- mannf/product/dashboard/models.py +460 -0
- mannf/product/dashboard/static/index.html +6347 -0
- mannf/product/dashboard/static/manifest.json +25 -0
- mannf/product/dashboard/static/pwa-icon-192.png +0 -0
- mannf/product/dashboard/static/pwa-icon-512.png +0 -0
- mannf/product/dashboard/static/sw.js +64 -0
- mannf/product/dashboard/telemetry.py +547 -0
- mannf/product/database.py +145 -0
- mannf/product/demo.py +844 -0
- mannf/product/doctor.py +509 -0
- mannf/product/exporters/__init__.py +65 -0
- mannf/product/exporters/azuredevops_exporter.py +257 -0
- mannf/product/exporters/base.py +307 -0
- mannf/product/exporters/bugzilla_exporter.py +200 -0
- mannf/product/exporters/dedup.py +275 -0
- mannf/product/exporters/finding_adapter.py +216 -0
- mannf/product/exporters/github_exporter.py +197 -0
- mannf/product/exporters/gitlab_exporter.py +215 -0
- mannf/product/exporters/jira_exporter.py +180 -0
- mannf/product/exporters/linear_exporter.py +195 -0
- mannf/product/exporters/loader.py +233 -0
- mannf/product/exporters/pagerduty_exporter.py +363 -0
- mannf/product/exporters/sentry_exporter.py +322 -0
- mannf/product/exporters/servicenow_exporter.py +240 -0
- mannf/product/exporters/shortcut_exporter.py +231 -0
- mannf/product/exporters/webhook_exporter.py +383 -0
- mannf/product/formatters/__init__.py +18 -0
- mannf/product/formatters/allure_formatter.py +161 -0
- mannf/product/formatters/ctrf_formatter.py +149 -0
- mannf/product/healing/__init__.py +30 -0
- mannf/product/healing/graphql_schema_diff.py +152 -0
- mannf/product/healing/healer.py +141 -0
- mannf/product/healing/models.py +175 -0
- mannf/product/healing/schema_diff.py +251 -0
- mannf/product/ingestors/__init__.py +77 -0
- mannf/product/ingestors/base.py +256 -0
- mannf/product/ingestors/bgstm_ingestor.py +764 -0
- mannf/product/ingestors/curl_ingestor.py +1019 -0
- mannf/product/ingestors/cypress_ingestor.py +487 -0
- mannf/product/ingestors/gherkin_ingestor.py +967 -0
- mannf/product/ingestors/graphql_ingestor.py +845 -0
- mannf/product/ingestors/grpc_ingestor.py +591 -0
- mannf/product/ingestors/har_ingestor.py +976 -0
- mannf/product/ingestors/loader.py +284 -0
- mannf/product/ingestors/models.py +146 -0
- mannf/product/ingestors/openapi_ingestor.py +606 -0
- mannf/product/ingestors/playwright_ingestor.py +449 -0
- mannf/product/ingestors/postman_ingestor.py +631 -0
- mannf/product/ingestors/traffic_ingestor.py +679 -0
- mannf/product/ingestors/websocket_ingestor.py +526 -0
- mannf/product/integrations/__init__.py +21 -0
- mannf/product/integrations/auth.py +190 -0
- mannf/product/integrations/graphql_parser.py +436 -0
- mannf/product/integrations/graphql_sut.py +247 -0
- mannf/product/integrations/grpc_sut.py +469 -0
- mannf/product/integrations/http_sut.py +237 -0
- mannf/product/integrations/kafka_adapter.py +342 -0
- mannf/product/integrations/openapi_parser.py +513 -0
- mannf/product/integrations/postman_parser.py +467 -0
- mannf/product/integrations/webhook_receiver.py +344 -0
- mannf/product/integrations/websocket_sut.py +434 -0
- mannf/product/llm/__init__.py +25 -0
- mannf/product/llm/anthropic_provider.py +94 -0
- mannf/product/llm/base.py +267 -0
- mannf/product/llm/config.py +48 -0
- mannf/product/llm/factory.py +42 -0
- mannf/product/llm/openai_provider.py +93 -0
- mannf/product/llm/prompts.py +403 -0
- mannf/product/llm/root_cause_service.py +311 -0
- mannf/product/llm/test_plan_models.py +78 -0
- mannf/product/metrics.py +149 -0
- mannf/product/middleware/__init__.py +3 -0
- mannf/product/middleware/audit_middleware.py +112 -0
- mannf/product/middleware/tenant_isolation.py +114 -0
- mannf/product/models.py +347 -0
- mannf/product/notifications/__init__.py +24 -0
- mannf/product/notifications/dispatcher.py +411 -0
- mannf/product/onboarding.py +190 -0
- mannf/product/orchestration/__init__.py +39 -0
- mannf/product/orchestration/ingest_scan_orchestrator.py +339 -0
- mannf/product/orchestration/pipeline.py +401 -0
- mannf/product/orchestrator.py +987 -0
- mannf/product/orchestrator_models.py +269 -0
- mannf/product/regression/__init__.py +36 -0
- mannf/product/regression/differ.py +172 -0
- mannf/product/regression/masking.py +100 -0
- mannf/product/regression/models.py +232 -0
- mannf/product/regression/recorder.py +124 -0
- mannf/product/regression/replayer.py +168 -0
- mannf/product/reports/__init__.py +10 -0
- mannf/product/reports/pdf.py +132 -0
- mannf/product/scheduling/__init__.py +57 -0
- mannf/product/scheduling/cron_utils.py +251 -0
- mannf/product/scheduling/engine.py +473 -0
- mannf/product/scheduling/models.py +86 -0
- mannf/product/scheduling/queue.py +894 -0
- mannf/product/scheduling/store.py +235 -0
- mannf/product/security/__init__.py +21 -0
- mannf/product/security/belief_guided.py +143 -0
- mannf/product/security/checks/__init__.py +55 -0
- mannf/product/security/checks/base.py +69 -0
- mannf/product/security/checks/bfla.py +77 -0
- mannf/product/security/checks/bola.py +77 -0
- mannf/product/security/checks/bopla.py +80 -0
- mannf/product/security/checks/broken_auth.py +86 -0
- mannf/product/security/checks/graphql_security.py +299 -0
- mannf/product/security/checks/inventory.py +70 -0
- mannf/product/security/checks/misconfig.py +158 -0
- mannf/product/security/checks/resource_consumption.py +70 -0
- mannf/product/security/checks/sensitive_flows.py +80 -0
- mannf/product/security/checks/ssrf.py +101 -0
- mannf/product/security/checks/unsafe_consumption.py +120 -0
- mannf/product/security/models.py +92 -0
- mannf/product/security/plugin_loader.py +182 -0
- mannf/product/security/reporter.py +92 -0
- mannf/product/security/scanner.py +183 -0
- mannf/product/server.py +6220 -0
- mannf/product/setup_wizard.py +873 -0
- mannf/product/status.py +404 -0
- mannf/product/storage/__init__.py +10 -0
- mannf/product/storage/artifact_store.py +343 -0
- mannf/product/telemetry.py +300 -0
- mannf/product/uninstall.py +169 -0
- mannf/product/upgrade.py +139 -0
- mannf/product/weights/__init__.py +13 -0
- mannf/product/weights/blob_store.py +299 -0
- mannf/product/weights/factory.py +42 -0
- mannf/product/weights/registry.py +159 -0
- mannf/product/weights/store.py +210 -0
- mannf/regression/__init__.py +7 -0
- mannf/regression/differ.py +9 -0
- mannf/regression/masking.py +9 -0
- mannf/regression/models.py +9 -0
- mannf/regression/recorder.py +9 -0
- mannf/regression/replayer.py +9 -0
- mannf/security/__init__.py +7 -0
- mannf/security/belief_guided.py +9 -0
- mannf/security/checks/__init__.py +7 -0
- mannf/security/checks/base.py +9 -0
- mannf/security/checks/bfla.py +9 -0
- mannf/security/checks/bola.py +9 -0
- mannf/security/checks/bopla.py +9 -0
- mannf/security/checks/broken_auth.py +9 -0
- mannf/security/checks/graphql_security.py +9 -0
- mannf/security/checks/inventory.py +9 -0
- mannf/security/checks/misconfig.py +9 -0
- mannf/security/checks/resource_consumption.py +9 -0
- mannf/security/checks/sensitive_flows.py +9 -0
- mannf/security/checks/ssrf.py +9 -0
- mannf/security/checks/unsafe_consumption.py +9 -0
- mannf/security/models.py +9 -0
- mannf/security/reporter.py +9 -0
- mannf/security/scanner.py +9 -0
- mannf/server.py +9 -0
- mannf/testing/__init__.py +7 -0
- mannf/testing/adaptive_controller.py +9 -0
- mannf/testing/models.py +9 -0
- mannf/weights/__init__.py +7 -0
- mannf/weights/registry.py +9 -0
- mannf/weights/store.py +9 -0
- nat_engine-1.dist-info/METADATA +555 -0
- nat_engine-1.dist-info/RECORD +299 -0
- nat_engine-1.dist-info/WHEEL +5 -0
- nat_engine-1.dist-info/entry_points.txt +4 -0
- nat_engine-1.dist-info/licenses/LICENSE +651 -0
- nat_engine-1.dist-info/licenses/NOTICE +178 -0
- 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 ""
|