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