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