agent_os_kernel 3.1.0__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.
- agent_control_plane/__init__.py +662 -0
- agent_control_plane/a2a_adapter.py +543 -0
- agent_control_plane/adapter.py +417 -0
- agent_control_plane/agent_hibernation.py +394 -0
- agent_control_plane/agent_kernel.py +470 -0
- agent_control_plane/compliance.py +720 -0
- agent_control_plane/constraint_graphs.py +478 -0
- agent_control_plane/control_plane.py +854 -0
- agent_control_plane/example_executors.py +195 -0
- agent_control_plane/execution_engine.py +231 -0
- agent_control_plane/flight_recorder.py +846 -0
- agent_control_plane/governance_layer.py +435 -0
- agent_control_plane/hf_utils.py +563 -0
- agent_control_plane/interfaces/__init__.py +55 -0
- agent_control_plane/interfaces/kernel_interface.py +361 -0
- agent_control_plane/interfaces/plugin_interface.py +497 -0
- agent_control_plane/interfaces/protocol_interfaces.py +387 -0
- agent_control_plane/kernel_space.py +1009 -0
- agent_control_plane/langchain_adapter.py +424 -0
- agent_control_plane/lifecycle.py +3113 -0
- agent_control_plane/mcp_adapter.py +653 -0
- agent_control_plane/ml_safety.py +563 -0
- agent_control_plane/multimodal.py +727 -0
- agent_control_plane/mute_agent.py +422 -0
- agent_control_plane/observability.py +787 -0
- agent_control_plane/orchestrator.py +482 -0
- agent_control_plane/plugin_registry.py +750 -0
- agent_control_plane/policy_engine.py +954 -0
- agent_control_plane/process_isolation.py +777 -0
- agent_control_plane/shadow_mode.py +310 -0
- agent_control_plane/signals.py +493 -0
- agent_control_plane/supervisor_agents.py +430 -0
- agent_control_plane/time_travel_debugger.py +557 -0
- agent_control_plane/tool_registry.py +452 -0
- agent_control_plane/vfs.py +697 -0
- agent_kernel/__init__.py +69 -0
- agent_kernel/analyzer.py +435 -0
- agent_kernel/auditor.py +36 -0
- agent_kernel/completeness_auditor.py +237 -0
- agent_kernel/detector.py +203 -0
- agent_kernel/kernel.py +744 -0
- agent_kernel/memory_manager.py +85 -0
- agent_kernel/models.py +374 -0
- agent_kernel/nudge_mechanism.py +263 -0
- agent_kernel/outcome_analyzer.py +338 -0
- agent_kernel/patcher.py +582 -0
- agent_kernel/semantic_analyzer.py +316 -0
- agent_kernel/semantic_purge.py +349 -0
- agent_kernel/simulator.py +449 -0
- agent_kernel/teacher.py +85 -0
- agent_kernel/triage.py +152 -0
- agent_os/__init__.py +409 -0
- agent_os/_adversarial_impl.py +200 -0
- agent_os/_circuit_breaker_impl.py +232 -0
- agent_os/_mcp_metrics.py +193 -0
- agent_os/adversarial.py +20 -0
- agent_os/agents_compat.py +490 -0
- agent_os/audit_logger.py +135 -0
- agent_os/base_agent.py +651 -0
- agent_os/circuit_breaker.py +34 -0
- agent_os/cli/__init__.py +659 -0
- agent_os/cli/cmd_audit.py +128 -0
- agent_os/cli/cmd_init.py +152 -0
- agent_os/cli/cmd_policy.py +41 -0
- agent_os/cli/cmd_policy_gen.py +180 -0
- agent_os/cli/cmd_validate.py +258 -0
- agent_os/cli/mcp_scan.py +265 -0
- agent_os/cli/output.py +192 -0
- agent_os/cli/policy_checker.py +330 -0
- agent_os/compat.py +74 -0
- agent_os/constraint_graph.py +234 -0
- agent_os/content_governance.py +140 -0
- agent_os/context_budget.py +305 -0
- agent_os/credential_redactor.py +224 -0
- agent_os/diff_policy.py +89 -0
- agent_os/egress_policy.py +159 -0
- agent_os/escalation.py +276 -0
- agent_os/event_bus.py +124 -0
- agent_os/exceptions.py +180 -0
- agent_os/execution_context_policy.py +141 -0
- agent_os/github_enterprise.py +96 -0
- agent_os/health.py +20 -0
- agent_os/integrations/__init__.py +279 -0
- agent_os/integrations/a2a_adapter.py +279 -0
- agent_os/integrations/agent_lightning/__init__.py +30 -0
- agent_os/integrations/anthropic_adapter.py +420 -0
- agent_os/integrations/autogen_adapter.py +620 -0
- agent_os/integrations/base.py +1137 -0
- agent_os/integrations/compat.py +229 -0
- agent_os/integrations/config.py +98 -0
- agent_os/integrations/conversation_guardian.py +957 -0
- agent_os/integrations/crewai_adapter.py +467 -0
- agent_os/integrations/drift_detector.py +425 -0
- agent_os/integrations/dry_run.py +124 -0
- agent_os/integrations/escalation.py +582 -0
- agent_os/integrations/gemini_adapter.py +364 -0
- agent_os/integrations/google_adk_adapter.py +633 -0
- agent_os/integrations/guardrails_adapter.py +394 -0
- agent_os/integrations/health.py +197 -0
- agent_os/integrations/langchain_adapter.py +654 -0
- agent_os/integrations/llamafirewall.py +343 -0
- agent_os/integrations/llamaindex_adapter.py +188 -0
- agent_os/integrations/logging.py +191 -0
- agent_os/integrations/maf_adapter.py +631 -0
- agent_os/integrations/mistral_adapter.py +365 -0
- agent_os/integrations/openai_adapter.py +816 -0
- agent_os/integrations/openai_agents_sdk.py +406 -0
- agent_os/integrations/policy_compose.py +171 -0
- agent_os/integrations/profiling.py +144 -0
- agent_os/integrations/pydantic_ai_adapter.py +420 -0
- agent_os/integrations/rate_limiter.py +130 -0
- agent_os/integrations/rbac.py +143 -0
- agent_os/integrations/registry.py +113 -0
- agent_os/integrations/scope_guard.py +303 -0
- agent_os/integrations/semantic_kernel_adapter.py +769 -0
- agent_os/integrations/smolagents_adapter.py +629 -0
- agent_os/integrations/templates.py +178 -0
- agent_os/integrations/token_budget.py +134 -0
- agent_os/integrations/tool_aliases.py +190 -0
- agent_os/integrations/webhooks.py +177 -0
- agent_os/lite.py +208 -0
- agent_os/mcp_gateway.py +385 -0
- agent_os/mcp_message_signer.py +273 -0
- agent_os/mcp_protocols.py +161 -0
- agent_os/mcp_response_scanner.py +232 -0
- agent_os/mcp_security.py +924 -0
- agent_os/mcp_session_auth.py +231 -0
- agent_os/mcp_sliding_rate_limiter.py +184 -0
- agent_os/memory_guard.py +409 -0
- agent_os/metrics.py +134 -0
- agent_os/mute.py +428 -0
- agent_os/mute_agent.py +209 -0
- agent_os/policies/__init__.py +77 -0
- agent_os/policies/async_evaluator.py +275 -0
- agent_os/policies/backends.py +670 -0
- agent_os/policies/bridge.py +169 -0
- agent_os/policies/budget.py +85 -0
- agent_os/policies/cli.py +294 -0
- agent_os/policies/conflict_resolution.py +270 -0
- agent_os/policies/data_classification.py +252 -0
- agent_os/policies/evaluator.py +239 -0
- agent_os/policies/policy_schema.json +228 -0
- agent_os/policies/rate_limiting.py +145 -0
- agent_os/policies/schema.py +115 -0
- agent_os/policies/shared.py +331 -0
- agent_os/prompt_injection.py +694 -0
- agent_os/providers.py +182 -0
- agent_os/py.typed +0 -0
- agent_os/retry.py +81 -0
- agent_os/reversibility.py +251 -0
- agent_os/sandbox.py +432 -0
- agent_os/sandbox_provider.py +140 -0
- agent_os/secure_codegen.py +525 -0
- agent_os/security_skills.py +538 -0
- agent_os/semantic_policy.py +422 -0
- agent_os/server/__init__.py +15 -0
- agent_os/server/__main__.py +25 -0
- agent_os/server/app.py +277 -0
- agent_os/server/models.py +104 -0
- agent_os/shift_left_metrics.py +130 -0
- agent_os/stateless.py +742 -0
- agent_os/supervisor.py +148 -0
- agent_os/task_outcome.py +148 -0
- agent_os/transparency.py +181 -0
- agent_os/trust_root.py +128 -0
- agent_os_kernel-3.1.0.dist-info/METADATA +1269 -0
- agent_os_kernel-3.1.0.dist-info/RECORD +337 -0
- agent_os_kernel-3.1.0.dist-info/WHEEL +4 -0
- agent_os_kernel-3.1.0.dist-info/entry_points.txt +2 -0
- agent_os_kernel-3.1.0.dist-info/licenses/LICENSE +21 -0
- agent_os_observability/__init__.py +27 -0
- agent_os_observability/dashboards.py +898 -0
- agent_os_observability/metrics.py +398 -0
- agent_os_observability/server.py +223 -0
- agent_os_observability/tracer.py +232 -0
- agent_primitives/__init__.py +24 -0
- agent_primitives/failures.py +84 -0
- agent_primitives/py.typed +0 -0
- amb_core/__init__.py +177 -0
- amb_core/adapters/__init__.py +57 -0
- amb_core/adapters/aws_sqs_broker.py +376 -0
- amb_core/adapters/azure_servicebus_broker.py +340 -0
- amb_core/adapters/kafka_broker.py +260 -0
- amb_core/adapters/nats_broker.py +285 -0
- amb_core/adapters/rabbitmq_broker.py +235 -0
- amb_core/adapters/redis_broker.py +262 -0
- amb_core/broker.py +145 -0
- amb_core/bus.py +481 -0
- amb_core/cloudevents.py +509 -0
- amb_core/dlq.py +345 -0
- amb_core/hf_utils.py +536 -0
- amb_core/memory_broker.py +410 -0
- amb_core/models.py +141 -0
- amb_core/persistence.py +529 -0
- amb_core/schema.py +294 -0
- amb_core/tracing.py +358 -0
- atr/__init__.py +640 -0
- atr/access.py +348 -0
- atr/composition.py +645 -0
- atr/decorator.py +357 -0
- atr/executor.py +384 -0
- atr/health.py +557 -0
- atr/hf_utils.py +449 -0
- atr/injection.py +422 -0
- atr/metrics.py +440 -0
- atr/policies.py +403 -0
- atr/py.typed +2 -0
- atr/registry.py +452 -0
- atr/schema.py +480 -0
- atr/tools/safe/__init__.py +75 -0
- atr/tools/safe/calculator.py +467 -0
- atr/tools/safe/datetime_tool.py +443 -0
- atr/tools/safe/file_reader.py +402 -0
- atr/tools/safe/http_client.py +316 -0
- atr/tools/safe/json_parser.py +374 -0
- atr/tools/safe/text_tool.py +537 -0
- atr/tools/safe/toolkit.py +175 -0
- caas/__init__.py +162 -0
- caas/api/__init__.py +7 -0
- caas/api/server.py +1328 -0
- caas/caching.py +834 -0
- caas/cli.py +210 -0
- caas/conversation.py +223 -0
- caas/decay.py +72 -0
- caas/detection/__init__.py +9 -0
- caas/detection/detector.py +238 -0
- caas/enrichment.py +130 -0
- caas/gateway/__init__.py +27 -0
- caas/gateway/trust_gateway.py +474 -0
- caas/hf_utils.py +479 -0
- caas/ingestion/__init__.py +23 -0
- caas/ingestion/processors.py +253 -0
- caas/ingestion/structure_parser.py +188 -0
- caas/models.py +356 -0
- caas/pragmatic_truth.py +444 -0
- caas/routing/__init__.py +10 -0
- caas/routing/heuristic_router.py +58 -0
- caas/storage/__init__.py +9 -0
- caas/storage/store.py +389 -0
- caas/triad.py +213 -0
- caas/tuning/__init__.py +9 -0
- caas/tuning/tuner.py +329 -0
- caas/vfs/__init__.py +14 -0
- caas/vfs/filesystem.py +452 -0
- cmvk/__init__.py +218 -0
- cmvk/audit.py +402 -0
- cmvk/benchmarks.py +478 -0
- cmvk/constitutional.py +904 -0
- cmvk/hf_utils.py +301 -0
- cmvk/metrics.py +473 -0
- cmvk/profiles.py +300 -0
- cmvk/py.typed +0 -0
- cmvk/types.py +12 -0
- cmvk/verification.py +956 -0
- emk/__init__.py +89 -0
- emk/causal.py +352 -0
- emk/hf_utils.py +421 -0
- emk/indexer.py +83 -0
- emk/py.typed +0 -0
- emk/schema.py +204 -0
- emk/sleep_cycle.py +347 -0
- emk/store.py +281 -0
- iatp/__init__.py +166 -0
- iatp/attestation.py +461 -0
- iatp/cli.py +317 -0
- iatp/hf_utils.py +472 -0
- iatp/ipc_pipes.py +580 -0
- iatp/main.py +412 -0
- iatp/models/__init__.py +447 -0
- iatp/policy_engine.py +337 -0
- iatp/py.typed +2 -0
- iatp/recovery.py +321 -0
- iatp/security/__init__.py +270 -0
- iatp/sidecar/__init__.py +519 -0
- iatp/telemetry/__init__.py +164 -0
- iatp/tests/__init__.py +1 -0
- iatp/tests/test_attestation.py +370 -0
- iatp/tests/test_cli.py +131 -0
- iatp/tests/test_ed25519_attestation.py +211 -0
- iatp/tests/test_models.py +130 -0
- iatp/tests/test_policy_engine.py +347 -0
- iatp/tests/test_recovery.py +281 -0
- iatp/tests/test_security.py +222 -0
- iatp/tests/test_sidecar.py +167 -0
- iatp/tests/test_telemetry.py +175 -0
- mcp_kernel_server/__init__.py +28 -0
- mcp_kernel_server/cli.py +274 -0
- mcp_kernel_server/resources.py +217 -0
- mcp_kernel_server/server.py +564 -0
- mcp_kernel_server/tools.py +1174 -0
- mute_agent/__init__.py +68 -0
- mute_agent/core/__init__.py +1 -0
- mute_agent/core/execution_agent.py +166 -0
- mute_agent/core/handshake_protocol.py +201 -0
- mute_agent/core/reasoning_agent.py +238 -0
- mute_agent/knowledge_graph/__init__.py +1 -0
- mute_agent/knowledge_graph/graph_elements.py +65 -0
- mute_agent/knowledge_graph/multidimensional_graph.py +170 -0
- mute_agent/knowledge_graph/subgraph.py +224 -0
- mute_agent/listener/__init__.py +43 -0
- mute_agent/listener/adapters/__init__.py +31 -0
- mute_agent/listener/adapters/base_adapter.py +189 -0
- mute_agent/listener/adapters/caas_adapter.py +344 -0
- mute_agent/listener/adapters/control_plane_adapter.py +436 -0
- mute_agent/listener/adapters/iatp_adapter.py +332 -0
- mute_agent/listener/adapters/scak_adapter.py +251 -0
- mute_agent/listener/listener.py +610 -0
- mute_agent/listener/state_observer.py +436 -0
- mute_agent/listener/threshold_config.py +313 -0
- mute_agent/super_system/__init__.py +1 -0
- mute_agent/super_system/router.py +204 -0
- mute_agent/visualization/__init__.py +10 -0
- mute_agent/visualization/graph_debugger.py +502 -0
- nexus/README.md +60 -0
- nexus/__init__.py +51 -0
- nexus/arbiter.py +359 -0
- nexus/client.py +466 -0
- nexus/dmz.py +444 -0
- nexus/escrow.py +430 -0
- nexus/exceptions.py +286 -0
- nexus/pyproject.toml +36 -0
- nexus/registry.py +393 -0
- nexus/reputation.py +425 -0
- nexus/schemas/__init__.py +51 -0
- nexus/schemas/compliance.py +276 -0
- nexus/schemas/escrow.py +251 -0
- nexus/schemas/manifest.py +225 -0
- nexus/schemas/receipt.py +208 -0
- nexus/tests/__init__.py +0 -0
- nexus/tests/conftest.py +146 -0
- nexus/tests/test_arbiter.py +192 -0
- nexus/tests/test_dmz.py +194 -0
- nexus/tests/test_escrow.py +276 -0
- nexus/tests/test_exceptions.py +225 -0
- nexus/tests/test_registry.py +232 -0
- nexus/tests/test_reputation.py +328 -0
- nexus/tests/test_schemas.py +295 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""NATS broker adapter for AMB.
|
|
4
|
+
|
|
5
|
+
NATS is a lightweight, cloud-native messaging system ideal for
|
|
6
|
+
microservices, IoT, and edge computing scenarios.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import json
|
|
11
|
+
import uuid
|
|
12
|
+
from typing import Dict, List, Optional, Callable
|
|
13
|
+
|
|
14
|
+
from amb_core.broker import BrokerAdapter, MessageHandler
|
|
15
|
+
from amb_core.models import Message
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
import nats
|
|
19
|
+
from nats.aio.client import Client as NATS
|
|
20
|
+
from nats.js.api import StreamConfig, ConsumerConfig
|
|
21
|
+
except ImportError:
|
|
22
|
+
raise ImportError(
|
|
23
|
+
"NATS adapter requires 'nats-py' package. "
|
|
24
|
+
"Install it with: pip install amb-core[nats]"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class NATSBroker(BrokerAdapter):
|
|
29
|
+
"""
|
|
30
|
+
NATS-based broker adapter.
|
|
31
|
+
|
|
32
|
+
This adapter uses NATS for lightweight, high-performance messaging.
|
|
33
|
+
Supports both core NATS (fire-and-forget) and JetStream (persistent).
|
|
34
|
+
|
|
35
|
+
Example:
|
|
36
|
+
```python
|
|
37
|
+
from amb_core.adapters import NATSBroker
|
|
38
|
+
|
|
39
|
+
broker = NATSBroker(servers=["nats://localhost:4222"])
|
|
40
|
+
await broker.connect()
|
|
41
|
+
|
|
42
|
+
# Subscribe
|
|
43
|
+
async def handler(msg):
|
|
44
|
+
print(f"Received: {msg.payload}")
|
|
45
|
+
|
|
46
|
+
await broker.subscribe("agent.tasks", handler)
|
|
47
|
+
|
|
48
|
+
# Publish
|
|
49
|
+
await broker.publish(Message(topic="agent.tasks", payload={"task": "analyze"}))
|
|
50
|
+
```
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
servers: List[str] = None,
|
|
56
|
+
use_jetstream: bool = False,
|
|
57
|
+
stream_name: str = "AMB_STREAM"
|
|
58
|
+
):
|
|
59
|
+
"""
|
|
60
|
+
Initialize NATS broker.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
servers: List of NATS server URLs (default: ["nats://localhost:4222"])
|
|
64
|
+
use_jetstream: Enable JetStream for persistence (default: False)
|
|
65
|
+
stream_name: JetStream stream name (default: "AMB_STREAM")
|
|
66
|
+
"""
|
|
67
|
+
self.servers = servers or ["nats://localhost:4222"]
|
|
68
|
+
self.use_jetstream = use_jetstream
|
|
69
|
+
self.stream_name = stream_name
|
|
70
|
+
|
|
71
|
+
self._nc: Optional[NATS] = None
|
|
72
|
+
self._js = None # JetStream context
|
|
73
|
+
self._subscriptions: Dict[str, object] = {} # subscription_id -> subscription
|
|
74
|
+
self._handlers: Dict[str, MessageHandler] = {} # topic -> handler
|
|
75
|
+
self._running = False
|
|
76
|
+
|
|
77
|
+
async def connect(self) -> None:
|
|
78
|
+
"""Connect to NATS server(s)."""
|
|
79
|
+
self._nc = await nats.connect(servers=self.servers)
|
|
80
|
+
self._running = True
|
|
81
|
+
|
|
82
|
+
if self.use_jetstream:
|
|
83
|
+
self._js = self._nc.jetstream()
|
|
84
|
+
# Create stream if it doesn't exist
|
|
85
|
+
try:
|
|
86
|
+
await self._js.add_stream(
|
|
87
|
+
name=self.stream_name,
|
|
88
|
+
subjects=["amb.>"], # All AMB subjects
|
|
89
|
+
retention="limits",
|
|
90
|
+
max_msgs=100000,
|
|
91
|
+
max_bytes=100 * 1024 * 1024 # 100MB
|
|
92
|
+
)
|
|
93
|
+
except Exception:
|
|
94
|
+
# Stream may already exist
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
async def disconnect(self) -> None:
|
|
98
|
+
"""Disconnect from NATS."""
|
|
99
|
+
self._running = False
|
|
100
|
+
|
|
101
|
+
# Unsubscribe all
|
|
102
|
+
for sub_id in list(self._subscriptions.keys()):
|
|
103
|
+
await self.unsubscribe(sub_id)
|
|
104
|
+
|
|
105
|
+
if self._nc:
|
|
106
|
+
await self._nc.drain()
|
|
107
|
+
await self._nc.close()
|
|
108
|
+
|
|
109
|
+
async def publish(self, message: Message, wait_for_confirmation: bool = False) -> Optional[str]:
|
|
110
|
+
"""
|
|
111
|
+
Publish message to NATS subject.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
message: Message to publish
|
|
115
|
+
wait_for_confirmation: Wait for acknowledgment (JetStream only)
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Message ID
|
|
119
|
+
"""
|
|
120
|
+
if not self._nc:
|
|
121
|
+
raise ConnectionError("Not connected to NATS")
|
|
122
|
+
|
|
123
|
+
# Convert topic to NATS subject format (replace / with .)
|
|
124
|
+
subject = f"amb.{message.topic.replace('/', '.')}"
|
|
125
|
+
|
|
126
|
+
# Serialize message
|
|
127
|
+
message_bytes = message.model_dump_json().encode()
|
|
128
|
+
|
|
129
|
+
if self.use_jetstream and self._js:
|
|
130
|
+
# Publish to JetStream for persistence
|
|
131
|
+
ack = await self._js.publish(subject, message_bytes)
|
|
132
|
+
if wait_for_confirmation:
|
|
133
|
+
return message.id
|
|
134
|
+
else:
|
|
135
|
+
# Core NATS publish (fire-and-forget)
|
|
136
|
+
await self._nc.publish(subject, message_bytes)
|
|
137
|
+
|
|
138
|
+
return message.id
|
|
139
|
+
|
|
140
|
+
async def subscribe(self, topic: str, handler: MessageHandler) -> str:
|
|
141
|
+
"""
|
|
142
|
+
Subscribe to a NATS subject.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
topic: Topic to subscribe to
|
|
146
|
+
handler: Message handler
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Subscription ID
|
|
150
|
+
"""
|
|
151
|
+
if not self._nc:
|
|
152
|
+
raise ConnectionError("Not connected to NATS")
|
|
153
|
+
|
|
154
|
+
subscription_id = str(uuid.uuid4())
|
|
155
|
+
|
|
156
|
+
# Convert topic to NATS subject format
|
|
157
|
+
subject = f"amb.{topic.replace('/', '.')}"
|
|
158
|
+
|
|
159
|
+
# Create message callback
|
|
160
|
+
async def message_callback(msg):
|
|
161
|
+
try:
|
|
162
|
+
# Parse message
|
|
163
|
+
data = msg.data.decode('utf-8')
|
|
164
|
+
amb_message = Message.model_validate_json(data)
|
|
165
|
+
|
|
166
|
+
# Call handler
|
|
167
|
+
await handler(amb_message)
|
|
168
|
+
|
|
169
|
+
except Exception as e:
|
|
170
|
+
print(f"Error handling NATS message: {e}")
|
|
171
|
+
|
|
172
|
+
# Subscribe
|
|
173
|
+
if self.use_jetstream and self._js:
|
|
174
|
+
# JetStream subscription with durable consumer
|
|
175
|
+
sub = await self._js.subscribe(
|
|
176
|
+
subject,
|
|
177
|
+
cb=message_callback,
|
|
178
|
+
durable=f"amb-{subscription_id[:8]}"
|
|
179
|
+
)
|
|
180
|
+
else:
|
|
181
|
+
# Core NATS subscription
|
|
182
|
+
sub = await self._nc.subscribe(subject, cb=message_callback)
|
|
183
|
+
|
|
184
|
+
self._subscriptions[subscription_id] = sub
|
|
185
|
+
self._handlers[topic] = handler
|
|
186
|
+
|
|
187
|
+
return subscription_id
|
|
188
|
+
|
|
189
|
+
async def unsubscribe(self, subscription_id: str) -> None:
|
|
190
|
+
"""
|
|
191
|
+
Unsubscribe from a subject.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
subscription_id: Subscription ID
|
|
195
|
+
"""
|
|
196
|
+
if subscription_id in self._subscriptions:
|
|
197
|
+
sub = self._subscriptions[subscription_id]
|
|
198
|
+
await sub.unsubscribe()
|
|
199
|
+
del self._subscriptions[subscription_id]
|
|
200
|
+
|
|
201
|
+
async def request(self, message: Message, timeout: float = 30.0) -> Message:
|
|
202
|
+
"""
|
|
203
|
+
Send request and wait for response using NATS request-reply.
|
|
204
|
+
|
|
205
|
+
NATS has native request-reply support which makes this efficient.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
message: Request message
|
|
209
|
+
timeout: Timeout in seconds
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
Response message
|
|
213
|
+
"""
|
|
214
|
+
if not self._nc:
|
|
215
|
+
raise ConnectionError("Not connected to NATS")
|
|
216
|
+
|
|
217
|
+
# Generate correlation ID
|
|
218
|
+
if not message.correlation_id:
|
|
219
|
+
message.correlation_id = str(uuid.uuid4())
|
|
220
|
+
|
|
221
|
+
# Convert topic to NATS subject
|
|
222
|
+
subject = f"amb.{message.topic.replace('/', '.')}"
|
|
223
|
+
|
|
224
|
+
# Serialize message
|
|
225
|
+
message_bytes = message.model_dump_json().encode()
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
# NATS native request-reply
|
|
229
|
+
response = await self._nc.request(
|
|
230
|
+
subject,
|
|
231
|
+
message_bytes,
|
|
232
|
+
timeout=timeout
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Parse response
|
|
236
|
+
response_data = response.data.decode('utf-8')
|
|
237
|
+
return Message.model_validate_json(response_data)
|
|
238
|
+
|
|
239
|
+
except asyncio.TimeoutError:
|
|
240
|
+
raise TimeoutError(f"No response received within {timeout} seconds")
|
|
241
|
+
|
|
242
|
+
async def get_pending_messages(self, topic: str, limit: int = 10) -> List[Message]:
|
|
243
|
+
"""
|
|
244
|
+
Get pending messages (JetStream only).
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
topic: Topic to get messages from
|
|
248
|
+
limit: Maximum messages to retrieve
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
List of messages
|
|
252
|
+
"""
|
|
253
|
+
if not self.use_jetstream or not self._js:
|
|
254
|
+
return [] # Core NATS doesn't persist messages
|
|
255
|
+
|
|
256
|
+
subject = f"amb.{topic.replace('/', '.')}"
|
|
257
|
+
messages = []
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
# Create pull consumer
|
|
261
|
+
psub = await self._js.pull_subscribe(
|
|
262
|
+
subject,
|
|
263
|
+
durable=f"pending-{uuid.uuid4().hex[:8]}"
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
# Fetch messages
|
|
267
|
+
try:
|
|
268
|
+
fetched = await psub.fetch(limit, timeout=1.0)
|
|
269
|
+
for msg in fetched:
|
|
270
|
+
data = msg.data.decode('utf-8')
|
|
271
|
+
amb_message = Message.model_validate_json(data)
|
|
272
|
+
messages.append(amb_message)
|
|
273
|
+
except asyncio.TimeoutError:
|
|
274
|
+
pass # No more messages
|
|
275
|
+
|
|
276
|
+
await psub.unsubscribe()
|
|
277
|
+
|
|
278
|
+
except Exception as e:
|
|
279
|
+
print(f"Error fetching pending messages: {e}")
|
|
280
|
+
|
|
281
|
+
return messages
|
|
282
|
+
|
|
283
|
+
async def health_check(self) -> bool:
|
|
284
|
+
"""Check if connected to NATS."""
|
|
285
|
+
return self._nc is not None and self._nc.is_connected
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""RabbitMQ broker adapter for AMB."""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import uuid
|
|
7
|
+
from typing import Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
from amb_core.broker import BrokerAdapter, MessageHandler
|
|
10
|
+
from amb_core.models import Message
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
from aio_pika import ExchangeType, connect_robust
|
|
14
|
+
from aio_pika import Message as PikaMessage
|
|
15
|
+
from aio_pika.abc import AbstractChannel, AbstractConnection, AbstractQueue
|
|
16
|
+
except ImportError:
|
|
17
|
+
raise ImportError(
|
|
18
|
+
"RabbitMQ adapter requires 'aio-pika' package. "
|
|
19
|
+
"Install it with: pip install amb-core[rabbitmq]"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class RabbitMQBroker(BrokerAdapter):
|
|
24
|
+
"""
|
|
25
|
+
RabbitMQ-based broker adapter.
|
|
26
|
+
|
|
27
|
+
This adapter uses RabbitMQ's topic exchanges for flexible routing
|
|
28
|
+
and direct exchanges for request-response patterns.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, url: str = "amqp://guest:guest@localhost/"):
|
|
32
|
+
"""
|
|
33
|
+
Initialize RabbitMQ broker.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
url: RabbitMQ connection URL
|
|
37
|
+
"""
|
|
38
|
+
self.url = url
|
|
39
|
+
self._connection: Optional[AbstractConnection] = None
|
|
40
|
+
self._channel: Optional[AbstractChannel] = None
|
|
41
|
+
self._subscriptions: Dict[str, AbstractQueue] = {}
|
|
42
|
+
self._response_queues: Dict[str, asyncio.Queue] = {}
|
|
43
|
+
self._tasks: set = set()
|
|
44
|
+
|
|
45
|
+
async def connect(self) -> None:
|
|
46
|
+
"""Connect to RabbitMQ."""
|
|
47
|
+
self._connection = await connect_robust(self.url)
|
|
48
|
+
self._channel = await self._connection.channel()
|
|
49
|
+
|
|
50
|
+
# Declare topic exchange for pub/sub
|
|
51
|
+
await self._channel.declare_exchange(
|
|
52
|
+
"amb.topic",
|
|
53
|
+
ExchangeType.TOPIC,
|
|
54
|
+
durable=True
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
async def disconnect(self) -> None:
|
|
58
|
+
"""Disconnect from RabbitMQ."""
|
|
59
|
+
# Cancel all tasks
|
|
60
|
+
for task in self._tasks:
|
|
61
|
+
if not task.done():
|
|
62
|
+
task.cancel()
|
|
63
|
+
|
|
64
|
+
if self._tasks:
|
|
65
|
+
await asyncio.gather(*self._tasks, return_exceptions=True)
|
|
66
|
+
|
|
67
|
+
self._tasks.clear()
|
|
68
|
+
|
|
69
|
+
if self._channel:
|
|
70
|
+
await self._channel.close()
|
|
71
|
+
|
|
72
|
+
if self._connection:
|
|
73
|
+
await self._connection.close()
|
|
74
|
+
|
|
75
|
+
async def publish(self, message: Message, wait_for_confirmation: bool = False) -> Optional[str]:
|
|
76
|
+
"""
|
|
77
|
+
Publish message to RabbitMQ exchange.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
message: Message to publish
|
|
81
|
+
wait_for_confirmation: Wait for broker confirmation
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Message ID
|
|
85
|
+
"""
|
|
86
|
+
if not self._channel:
|
|
87
|
+
raise ConnectionError("Not connected to RabbitMQ")
|
|
88
|
+
|
|
89
|
+
exchange = await self._channel.get_exchange("amb.topic")
|
|
90
|
+
|
|
91
|
+
# Serialize message
|
|
92
|
+
message_json = message.model_dump_json()
|
|
93
|
+
|
|
94
|
+
# Create AMQP message
|
|
95
|
+
amqp_message = PikaMessage(
|
|
96
|
+
body=message_json.encode(),
|
|
97
|
+
content_type="application/json",
|
|
98
|
+
message_id=message.id,
|
|
99
|
+
correlation_id=message.correlation_id,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Publish
|
|
103
|
+
await exchange.publish(
|
|
104
|
+
amqp_message,
|
|
105
|
+
routing_key=message.topic,
|
|
106
|
+
mandatory=wait_for_confirmation
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
return message.id
|
|
110
|
+
|
|
111
|
+
async def subscribe(self, topic: str, handler: MessageHandler) -> str:
|
|
112
|
+
"""
|
|
113
|
+
Subscribe to a RabbitMQ topic.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
topic: Topic pattern to subscribe to (supports wildcards: * and #)
|
|
117
|
+
handler: Message handler
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Subscription ID
|
|
121
|
+
"""
|
|
122
|
+
if not self._channel:
|
|
123
|
+
raise ConnectionError("Not connected to RabbitMQ")
|
|
124
|
+
|
|
125
|
+
subscription_id = str(uuid.uuid4())
|
|
126
|
+
|
|
127
|
+
# Declare queue
|
|
128
|
+
queue = await self._channel.declare_queue(
|
|
129
|
+
f"amb.queue.{subscription_id}",
|
|
130
|
+
auto_delete=True
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Bind to exchange
|
|
134
|
+
exchange = await self._channel.get_exchange("amb.topic")
|
|
135
|
+
await queue.bind(exchange, routing_key=topic)
|
|
136
|
+
|
|
137
|
+
# Start consuming
|
|
138
|
+
async def on_message(message):
|
|
139
|
+
async with message.process():
|
|
140
|
+
# Parse message
|
|
141
|
+
msg = Message.model_validate_json(message.body.decode())
|
|
142
|
+
|
|
143
|
+
# Call handler
|
|
144
|
+
await handler(msg)
|
|
145
|
+
|
|
146
|
+
await queue.consume(on_message)
|
|
147
|
+
|
|
148
|
+
self._subscriptions[subscription_id] = queue
|
|
149
|
+
|
|
150
|
+
return subscription_id
|
|
151
|
+
|
|
152
|
+
async def unsubscribe(self, subscription_id: str) -> None:
|
|
153
|
+
"""
|
|
154
|
+
Unsubscribe from a topic.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
subscription_id: Subscription ID
|
|
158
|
+
"""
|
|
159
|
+
if subscription_id in self._subscriptions:
|
|
160
|
+
queue = self._subscriptions[subscription_id]
|
|
161
|
+
await queue.delete()
|
|
162
|
+
del self._subscriptions[subscription_id]
|
|
163
|
+
|
|
164
|
+
async def request(self, message: Message, timeout: float = 30.0) -> Message:
|
|
165
|
+
"""
|
|
166
|
+
Send request and wait for response using RabbitMQ RPC pattern.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
message: Request message
|
|
170
|
+
timeout: Timeout in seconds
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Response message
|
|
174
|
+
"""
|
|
175
|
+
if not self._channel:
|
|
176
|
+
raise ConnectionError("Not connected to RabbitMQ")
|
|
177
|
+
|
|
178
|
+
# Generate correlation ID
|
|
179
|
+
if not message.correlation_id:
|
|
180
|
+
message.correlation_id = str(uuid.uuid4())
|
|
181
|
+
|
|
182
|
+
# Declare callback queue
|
|
183
|
+
callback_queue = await self._channel.declare_queue(
|
|
184
|
+
exclusive=True,
|
|
185
|
+
auto_delete=True
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Set up response queue
|
|
189
|
+
response_queue: asyncio.Queue = asyncio.Queue()
|
|
190
|
+
self._response_queues[message.correlation_id] = response_queue
|
|
191
|
+
|
|
192
|
+
# Start consuming responses
|
|
193
|
+
async def on_response(response_msg):
|
|
194
|
+
async with response_msg.process():
|
|
195
|
+
if response_msg.correlation_id in self._response_queues:
|
|
196
|
+
msg = Message.model_validate_json(response_msg.body.decode())
|
|
197
|
+
await self._response_queues[response_msg.correlation_id].put(msg)
|
|
198
|
+
|
|
199
|
+
await callback_queue.consume(on_response)
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
# Set reply_to
|
|
203
|
+
message.reply_to = callback_queue.name
|
|
204
|
+
|
|
205
|
+
# Publish request
|
|
206
|
+
await self.publish(message, wait_for_confirmation=False)
|
|
207
|
+
|
|
208
|
+
# Wait for response
|
|
209
|
+
try:
|
|
210
|
+
response = await asyncio.wait_for(response_queue.get(), timeout=timeout)
|
|
211
|
+
return response
|
|
212
|
+
except asyncio.TimeoutError:
|
|
213
|
+
raise TimeoutError(f"No response received within {timeout} seconds")
|
|
214
|
+
|
|
215
|
+
finally:
|
|
216
|
+
# Clean up
|
|
217
|
+
if message.correlation_id in self._response_queues:
|
|
218
|
+
del self._response_queues[message.correlation_id]
|
|
219
|
+
|
|
220
|
+
await callback_queue.delete()
|
|
221
|
+
|
|
222
|
+
async def get_pending_messages(self, topic: str, limit: int = 10) -> List[Message]:
|
|
223
|
+
"""
|
|
224
|
+
Get pending messages (limited support in RabbitMQ).
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
topic: Topic to get messages from
|
|
228
|
+
limit: Maximum messages to retrieve
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
Empty list (RabbitMQ doesn't support this easily)
|
|
232
|
+
"""
|
|
233
|
+
# RabbitMQ doesn't support getting pending messages easily
|
|
234
|
+
# Would need to implement message persistence differently
|
|
235
|
+
return []
|