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,340 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""Azure Service Bus broker adapter for AMB.
|
|
4
|
+
|
|
5
|
+
Azure Service Bus provides enterprise-grade messaging for cloud applications
|
|
6
|
+
with features like dead-letter queues, sessions, and transactions.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import json
|
|
11
|
+
import uuid
|
|
12
|
+
from typing import Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
from amb_core.broker import BrokerAdapter, MessageHandler
|
|
15
|
+
from amb_core.models import Message
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
from azure.servicebus.aio import ServiceBusClient, ServiceBusSender, ServiceBusReceiver
|
|
19
|
+
from azure.servicebus import ServiceBusMessage
|
|
20
|
+
except ImportError:
|
|
21
|
+
raise ImportError(
|
|
22
|
+
"Azure Service Bus adapter requires 'azure-servicebus' package. "
|
|
23
|
+
"Install it with: pip install amb-core[azure]"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AzureServiceBusBroker(BrokerAdapter):
|
|
28
|
+
"""
|
|
29
|
+
Azure Service Bus broker adapter.
|
|
30
|
+
|
|
31
|
+
This adapter uses Azure Service Bus topics and subscriptions for
|
|
32
|
+
enterprise-grade messaging with guaranteed delivery.
|
|
33
|
+
|
|
34
|
+
Example:
|
|
35
|
+
```python
|
|
36
|
+
from amb_core.adapters import AzureServiceBusBroker
|
|
37
|
+
|
|
38
|
+
broker = AzureServiceBusBroker(
|
|
39
|
+
connection_string="Endpoint=sb://...",
|
|
40
|
+
topic_name="agent-messages"
|
|
41
|
+
)
|
|
42
|
+
await broker.connect()
|
|
43
|
+
|
|
44
|
+
# Subscribe
|
|
45
|
+
async def handler(msg):
|
|
46
|
+
print(f"Received: {msg.payload}")
|
|
47
|
+
|
|
48
|
+
sub_id = await broker.subscribe("agent.tasks", handler)
|
|
49
|
+
|
|
50
|
+
# Publish
|
|
51
|
+
await broker.publish(Message(topic="agent.tasks", payload={"task": "analyze"}))
|
|
52
|
+
```
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
connection_string: str,
|
|
58
|
+
topic_name: str = "amb-messages",
|
|
59
|
+
subscription_name: str = "amb-subscription"
|
|
60
|
+
):
|
|
61
|
+
"""
|
|
62
|
+
Initialize Azure Service Bus broker.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
connection_string: Azure Service Bus connection string
|
|
66
|
+
topic_name: Service Bus topic name
|
|
67
|
+
subscription_name: Default subscription name
|
|
68
|
+
"""
|
|
69
|
+
self.connection_string = connection_string
|
|
70
|
+
self.topic_name = topic_name
|
|
71
|
+
self.subscription_name = subscription_name
|
|
72
|
+
|
|
73
|
+
self._client: Optional[ServiceBusClient] = None
|
|
74
|
+
self._sender: Optional[ServiceBusSender] = None
|
|
75
|
+
self._receivers: Dict[str, ServiceBusReceiver] = {}
|
|
76
|
+
self._subscriptions: Dict[str, str] = {} # subscription_id -> topic
|
|
77
|
+
self._handlers: Dict[str, MessageHandler] = {}
|
|
78
|
+
self._tasks: set = set()
|
|
79
|
+
self._running = False
|
|
80
|
+
|
|
81
|
+
async def connect(self) -> None:
|
|
82
|
+
"""Connect to Azure Service Bus."""
|
|
83
|
+
self._client = ServiceBusClient.from_connection_string(
|
|
84
|
+
conn_str=self.connection_string
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Create sender for the topic
|
|
88
|
+
self._sender = self._client.get_topic_sender(topic_name=self.topic_name)
|
|
89
|
+
self._running = True
|
|
90
|
+
|
|
91
|
+
async def disconnect(self) -> None:
|
|
92
|
+
"""Disconnect from Azure Service Bus."""
|
|
93
|
+
self._running = False
|
|
94
|
+
|
|
95
|
+
# Cancel all tasks
|
|
96
|
+
for task in self._tasks:
|
|
97
|
+
if not task.done():
|
|
98
|
+
task.cancel()
|
|
99
|
+
|
|
100
|
+
if self._tasks:
|
|
101
|
+
await asyncio.gather(*self._tasks, return_exceptions=True)
|
|
102
|
+
|
|
103
|
+
self._tasks.clear()
|
|
104
|
+
|
|
105
|
+
# Close receivers
|
|
106
|
+
for receiver in self._receivers.values():
|
|
107
|
+
await receiver.close()
|
|
108
|
+
|
|
109
|
+
self._receivers.clear()
|
|
110
|
+
|
|
111
|
+
# Close sender
|
|
112
|
+
if self._sender:
|
|
113
|
+
await self._sender.close()
|
|
114
|
+
|
|
115
|
+
# Close client
|
|
116
|
+
if self._client:
|
|
117
|
+
await self._client.close()
|
|
118
|
+
|
|
119
|
+
async def publish(self, message: Message, wait_for_confirmation: bool = False) -> Optional[str]:
|
|
120
|
+
"""
|
|
121
|
+
Publish message to Azure Service Bus topic.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
message: Message to publish
|
|
125
|
+
wait_for_confirmation: Wait for broker acknowledgment
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Message ID
|
|
129
|
+
"""
|
|
130
|
+
if not self._sender:
|
|
131
|
+
raise ConnectionError("Not connected to Azure Service Bus")
|
|
132
|
+
|
|
133
|
+
# Create Service Bus message
|
|
134
|
+
sb_message = ServiceBusMessage(
|
|
135
|
+
body=message.model_dump_json(),
|
|
136
|
+
message_id=message.id,
|
|
137
|
+
correlation_id=message.correlation_id,
|
|
138
|
+
subject=message.topic, # Use subject for filtering
|
|
139
|
+
application_properties={
|
|
140
|
+
"topic": message.topic,
|
|
141
|
+
"source": message.source,
|
|
142
|
+
"target": message.target or "",
|
|
143
|
+
"message_type": message.message_type.value if hasattr(message.message_type, 'value') else str(message.message_type)
|
|
144
|
+
}
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Send message
|
|
148
|
+
await self._sender.send_messages(sb_message)
|
|
149
|
+
|
|
150
|
+
return message.id
|
|
151
|
+
|
|
152
|
+
async def subscribe(self, topic: str, handler: MessageHandler) -> str:
|
|
153
|
+
"""
|
|
154
|
+
Subscribe to messages on a topic.
|
|
155
|
+
|
|
156
|
+
Creates a receiver for the subscription and starts listening.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
topic: Topic to filter messages by
|
|
160
|
+
handler: Message handler
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Subscription ID
|
|
164
|
+
"""
|
|
165
|
+
if not self._client:
|
|
166
|
+
raise ConnectionError("Not connected to Azure Service Bus")
|
|
167
|
+
|
|
168
|
+
subscription_id = str(uuid.uuid4())
|
|
169
|
+
|
|
170
|
+
# Create receiver for the subscription
|
|
171
|
+
receiver = self._client.get_subscription_receiver(
|
|
172
|
+
topic_name=self.topic_name,
|
|
173
|
+
subscription_name=self.subscription_name,
|
|
174
|
+
max_wait_time=5 # seconds
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
self._receivers[subscription_id] = receiver
|
|
178
|
+
self._subscriptions[subscription_id] = topic
|
|
179
|
+
self._handlers[topic] = handler
|
|
180
|
+
|
|
181
|
+
# Start receiver task
|
|
182
|
+
task = asyncio.create_task(self._receive_task(subscription_id, topic, handler))
|
|
183
|
+
self._tasks.add(task)
|
|
184
|
+
task.add_done_callback(self._tasks.discard)
|
|
185
|
+
|
|
186
|
+
return subscription_id
|
|
187
|
+
|
|
188
|
+
async def _receive_task(self, subscription_id: str, topic: str, handler: MessageHandler):
|
|
189
|
+
"""Receive messages from Azure Service Bus."""
|
|
190
|
+
receiver = self._receivers.get(subscription_id)
|
|
191
|
+
if not receiver:
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
while self._running:
|
|
195
|
+
try:
|
|
196
|
+
# Receive messages in batches
|
|
197
|
+
async with receiver:
|
|
198
|
+
messages = await receiver.receive_messages(max_message_count=10, max_wait_time=5)
|
|
199
|
+
|
|
200
|
+
for sb_message in messages:
|
|
201
|
+
try:
|
|
202
|
+
# Parse message
|
|
203
|
+
body = str(sb_message)
|
|
204
|
+
amb_message = Message.model_validate_json(body)
|
|
205
|
+
|
|
206
|
+
# Check if message matches topic filter
|
|
207
|
+
if amb_message.topic == topic or topic == "*":
|
|
208
|
+
await handler(amb_message)
|
|
209
|
+
|
|
210
|
+
# Complete the message
|
|
211
|
+
await receiver.complete_message(sb_message)
|
|
212
|
+
|
|
213
|
+
except Exception as e:
|
|
214
|
+
print(f"Error handling message: {e}")
|
|
215
|
+
# Dead-letter the message for investigation
|
|
216
|
+
await receiver.dead_letter_message(
|
|
217
|
+
sb_message,
|
|
218
|
+
reason="ProcessingError",
|
|
219
|
+
error_description=str(e)
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
except asyncio.CancelledError:
|
|
223
|
+
break
|
|
224
|
+
except Exception as e:
|
|
225
|
+
print(f"Error in Service Bus receiver: {e}")
|
|
226
|
+
await asyncio.sleep(1.0)
|
|
227
|
+
|
|
228
|
+
async def unsubscribe(self, subscription_id: str) -> None:
|
|
229
|
+
"""
|
|
230
|
+
Unsubscribe from messages.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
subscription_id: Subscription ID
|
|
234
|
+
"""
|
|
235
|
+
if subscription_id in self._receivers:
|
|
236
|
+
receiver = self._receivers[subscription_id]
|
|
237
|
+
await receiver.close()
|
|
238
|
+
del self._receivers[subscription_id]
|
|
239
|
+
|
|
240
|
+
if subscription_id in self._subscriptions:
|
|
241
|
+
topic = self._subscriptions[subscription_id]
|
|
242
|
+
del self._subscriptions[subscription_id]
|
|
243
|
+
if topic in self._handlers:
|
|
244
|
+
del self._handlers[topic]
|
|
245
|
+
|
|
246
|
+
async def request(self, message: Message, timeout: float = 30.0) -> Message:
|
|
247
|
+
"""
|
|
248
|
+
Send request and wait for response.
|
|
249
|
+
|
|
250
|
+
Uses correlation ID and reply-to for request-response pattern.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
message: Request message
|
|
254
|
+
timeout: Timeout in seconds
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
Response message
|
|
258
|
+
"""
|
|
259
|
+
if not self._client or not self._sender:
|
|
260
|
+
raise ConnectionError("Not connected to Azure Service Bus")
|
|
261
|
+
|
|
262
|
+
# Generate correlation ID
|
|
263
|
+
if not message.correlation_id:
|
|
264
|
+
message.correlation_id = str(uuid.uuid4())
|
|
265
|
+
|
|
266
|
+
# Create response queue
|
|
267
|
+
response_queue: asyncio.Queue = asyncio.Queue()
|
|
268
|
+
|
|
269
|
+
# Create temporary receiver for response
|
|
270
|
+
response_subscription = f"response-{message.correlation_id[:8]}"
|
|
271
|
+
|
|
272
|
+
async def response_handler(msg: Message):
|
|
273
|
+
if msg.correlation_id == message.correlation_id:
|
|
274
|
+
await response_queue.put(msg)
|
|
275
|
+
|
|
276
|
+
# Subscribe to responses
|
|
277
|
+
sub_id = await self.subscribe(response_subscription, response_handler)
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
# Set reply-to
|
|
281
|
+
message.reply_to = response_subscription
|
|
282
|
+
|
|
283
|
+
# Publish request
|
|
284
|
+
await self.publish(message, wait_for_confirmation=False)
|
|
285
|
+
|
|
286
|
+
# Wait for response
|
|
287
|
+
try:
|
|
288
|
+
response = await asyncio.wait_for(response_queue.get(), timeout=timeout)
|
|
289
|
+
return response
|
|
290
|
+
except asyncio.TimeoutError:
|
|
291
|
+
raise TimeoutError(f"No response received within {timeout} seconds")
|
|
292
|
+
|
|
293
|
+
finally:
|
|
294
|
+
await self.unsubscribe(sub_id)
|
|
295
|
+
|
|
296
|
+
async def get_pending_messages(self, topic: str, limit: int = 10) -> List[Message]:
|
|
297
|
+
"""
|
|
298
|
+
Peek at pending messages without consuming them.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
topic: Topic to filter by
|
|
302
|
+
limit: Maximum messages to retrieve
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
List of messages
|
|
306
|
+
"""
|
|
307
|
+
if not self._client:
|
|
308
|
+
raise ConnectionError("Not connected to Azure Service Bus")
|
|
309
|
+
|
|
310
|
+
messages = []
|
|
311
|
+
|
|
312
|
+
# Create temporary receiver
|
|
313
|
+
receiver = self._client.get_subscription_receiver(
|
|
314
|
+
topic_name=self.topic_name,
|
|
315
|
+
subscription_name=self.subscription_name
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
try:
|
|
319
|
+
async with receiver:
|
|
320
|
+
# Peek messages without completing them
|
|
321
|
+
peeked = await receiver.peek_messages(max_message_count=limit)
|
|
322
|
+
|
|
323
|
+
for sb_message in peeked:
|
|
324
|
+
try:
|
|
325
|
+
body = str(sb_message)
|
|
326
|
+
amb_message = Message.model_validate_json(body)
|
|
327
|
+
|
|
328
|
+
if amb_message.topic == topic or topic == "*":
|
|
329
|
+
messages.append(amb_message)
|
|
330
|
+
except Exception:
|
|
331
|
+
continue
|
|
332
|
+
|
|
333
|
+
finally:
|
|
334
|
+
await receiver.close()
|
|
335
|
+
|
|
336
|
+
return messages
|
|
337
|
+
|
|
338
|
+
async def health_check(self) -> bool:
|
|
339
|
+
"""Check if connected to Azure Service Bus."""
|
|
340
|
+
return self._client is not None and self._sender is not None
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""Kafka broker adapter for AMB."""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import uuid
|
|
8
|
+
from typing import Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
from amb_core.broker import BrokerAdapter, MessageHandler
|
|
11
|
+
from amb_core.models import Message
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from aiokafka import AIOKafkaConsumer, AIOKafkaProducer
|
|
15
|
+
from aiokafka.errors import KafkaError
|
|
16
|
+
except ImportError:
|
|
17
|
+
raise ImportError(
|
|
18
|
+
"Kafka adapter requires 'aiokafka' package. "
|
|
19
|
+
"Install it with: pip install amb-core[kafka]"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class KafkaBroker(BrokerAdapter):
|
|
24
|
+
"""
|
|
25
|
+
Kafka-based broker adapter.
|
|
26
|
+
|
|
27
|
+
This adapter uses Kafka topics for message distribution with
|
|
28
|
+
high throughput and durability.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, bootstrap_servers: str = "localhost:9092"):
|
|
32
|
+
"""
|
|
33
|
+
Initialize Kafka broker.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
bootstrap_servers: Kafka bootstrap servers
|
|
37
|
+
"""
|
|
38
|
+
self.bootstrap_servers = bootstrap_servers
|
|
39
|
+
self._producer: Optional[AIOKafkaProducer] = None
|
|
40
|
+
self._consumers: Dict[str, AIOKafkaConsumer] = {}
|
|
41
|
+
self._subscriptions: Dict[str, str] = {} # subscription_id -> topic
|
|
42
|
+
self._tasks: set = set()
|
|
43
|
+
|
|
44
|
+
async def connect(self) -> None:
|
|
45
|
+
"""Connect to Kafka."""
|
|
46
|
+
self._producer = AIOKafkaProducer(
|
|
47
|
+
bootstrap_servers=self.bootstrap_servers,
|
|
48
|
+
value_serializer=lambda v: json.dumps(v).encode()
|
|
49
|
+
)
|
|
50
|
+
await self._producer.start()
|
|
51
|
+
|
|
52
|
+
async def disconnect(self) -> None:
|
|
53
|
+
"""Disconnect from Kafka."""
|
|
54
|
+
# Cancel all tasks
|
|
55
|
+
for task in self._tasks:
|
|
56
|
+
if not task.done():
|
|
57
|
+
task.cancel()
|
|
58
|
+
|
|
59
|
+
if self._tasks:
|
|
60
|
+
await asyncio.gather(*self._tasks, return_exceptions=True)
|
|
61
|
+
|
|
62
|
+
self._tasks.clear()
|
|
63
|
+
|
|
64
|
+
# Stop consumers
|
|
65
|
+
for consumer in self._consumers.values():
|
|
66
|
+
await consumer.stop()
|
|
67
|
+
|
|
68
|
+
self._consumers.clear()
|
|
69
|
+
|
|
70
|
+
# Stop producer
|
|
71
|
+
if self._producer:
|
|
72
|
+
await self._producer.stop()
|
|
73
|
+
|
|
74
|
+
async def publish(self, message: Message, wait_for_confirmation: bool = False) -> Optional[str]:
|
|
75
|
+
"""
|
|
76
|
+
Publish message to Kafka topic.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
message: Message to publish
|
|
80
|
+
wait_for_confirmation: Wait for broker confirmation
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Message ID
|
|
84
|
+
"""
|
|
85
|
+
if not self._producer:
|
|
86
|
+
raise ConnectionError("Not connected to Kafka")
|
|
87
|
+
|
|
88
|
+
# Serialize message
|
|
89
|
+
message_dict = message.model_dump(mode='json')
|
|
90
|
+
|
|
91
|
+
# Publish
|
|
92
|
+
future = await self._producer.send(
|
|
93
|
+
message.topic,
|
|
94
|
+
value=message_dict,
|
|
95
|
+
key=message.id.encode() if message.id else None
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if wait_for_confirmation:
|
|
99
|
+
# Wait for acknowledgment
|
|
100
|
+
await future
|
|
101
|
+
|
|
102
|
+
return message.id
|
|
103
|
+
|
|
104
|
+
async def subscribe(self, topic: str, handler: MessageHandler) -> str:
|
|
105
|
+
"""
|
|
106
|
+
Subscribe to a Kafka topic.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
topic: Topic to subscribe to
|
|
110
|
+
handler: Message handler
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Subscription ID
|
|
114
|
+
"""
|
|
115
|
+
subscription_id = str(uuid.uuid4())
|
|
116
|
+
|
|
117
|
+
# Create consumer
|
|
118
|
+
consumer = AIOKafkaConsumer(
|
|
119
|
+
topic,
|
|
120
|
+
bootstrap_servers=self.bootstrap_servers,
|
|
121
|
+
value_deserializer=lambda v: json.loads(v.decode()),
|
|
122
|
+
group_id=f"amb-{subscription_id}",
|
|
123
|
+
auto_offset_reset='latest'
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
await consumer.start()
|
|
127
|
+
self._consumers[subscription_id] = consumer
|
|
128
|
+
self._subscriptions[subscription_id] = topic
|
|
129
|
+
|
|
130
|
+
# Start consumer task
|
|
131
|
+
task = asyncio.create_task(self._consume_task(subscription_id, handler))
|
|
132
|
+
self._tasks.add(task)
|
|
133
|
+
task.add_done_callback(self._tasks.discard)
|
|
134
|
+
|
|
135
|
+
return subscription_id
|
|
136
|
+
|
|
137
|
+
async def _consume_task(self, subscription_id: str, handler: MessageHandler):
|
|
138
|
+
"""Consume messages from Kafka."""
|
|
139
|
+
consumer = self._consumers.get(subscription_id)
|
|
140
|
+
if not consumer:
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
async for msg in consumer:
|
|
145
|
+
try:
|
|
146
|
+
# Parse message
|
|
147
|
+
message = Message.model_validate(msg.value)
|
|
148
|
+
|
|
149
|
+
# Call handler
|
|
150
|
+
await handler(message)
|
|
151
|
+
|
|
152
|
+
except Exception as e:
|
|
153
|
+
print(f"Error handling Kafka message: {e}")
|
|
154
|
+
|
|
155
|
+
except asyncio.CancelledError:
|
|
156
|
+
pass
|
|
157
|
+
except Exception as e:
|
|
158
|
+
print(f"Error in Kafka consumer: {e}")
|
|
159
|
+
|
|
160
|
+
async def unsubscribe(self, subscription_id: str) -> None:
|
|
161
|
+
"""
|
|
162
|
+
Unsubscribe from a topic.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
subscription_id: Subscription ID
|
|
166
|
+
"""
|
|
167
|
+
if subscription_id in self._consumers:
|
|
168
|
+
consumer = self._consumers[subscription_id]
|
|
169
|
+
await consumer.stop()
|
|
170
|
+
del self._consumers[subscription_id]
|
|
171
|
+
|
|
172
|
+
if subscription_id in self._subscriptions:
|
|
173
|
+
del self._subscriptions[subscription_id]
|
|
174
|
+
|
|
175
|
+
async def request(self, message: Message, timeout: float = 30.0) -> Message:
|
|
176
|
+
"""
|
|
177
|
+
Send request and wait for response.
|
|
178
|
+
|
|
179
|
+
This is a simplified implementation using temporary topics.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
message: Request message
|
|
183
|
+
timeout: Timeout in seconds
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Response message
|
|
187
|
+
"""
|
|
188
|
+
if not self._producer:
|
|
189
|
+
raise ConnectionError("Not connected to Kafka")
|
|
190
|
+
|
|
191
|
+
# Generate correlation ID
|
|
192
|
+
if not message.correlation_id:
|
|
193
|
+
message.correlation_id = str(uuid.uuid4())
|
|
194
|
+
|
|
195
|
+
# Create temporary response topic
|
|
196
|
+
response_topic = f"response.{message.correlation_id}"
|
|
197
|
+
message.reply_to = response_topic
|
|
198
|
+
|
|
199
|
+
# Create response queue
|
|
200
|
+
response_queue: asyncio.Queue = asyncio.Queue()
|
|
201
|
+
|
|
202
|
+
# Subscribe to response topic
|
|
203
|
+
async def response_handler(msg: Message):
|
|
204
|
+
if msg.correlation_id == message.correlation_id:
|
|
205
|
+
await response_queue.put(msg)
|
|
206
|
+
|
|
207
|
+
sub_id = await self.subscribe(response_topic, response_handler)
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
# Publish request
|
|
211
|
+
await self.publish(message, wait_for_confirmation=False)
|
|
212
|
+
|
|
213
|
+
# Wait for response
|
|
214
|
+
try:
|
|
215
|
+
response = await asyncio.wait_for(response_queue.get(), timeout=timeout)
|
|
216
|
+
return response
|
|
217
|
+
except asyncio.TimeoutError:
|
|
218
|
+
raise TimeoutError(f"No response received within {timeout} seconds")
|
|
219
|
+
|
|
220
|
+
finally:
|
|
221
|
+
# Clean up
|
|
222
|
+
await self.unsubscribe(sub_id)
|
|
223
|
+
|
|
224
|
+
async def get_pending_messages(self, topic: str, limit: int = 10) -> List[Message]:
|
|
225
|
+
"""
|
|
226
|
+
Get pending messages from Kafka topic.
|
|
227
|
+
|
|
228
|
+
This creates a temporary consumer to read recent messages.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
topic: Topic to get messages from
|
|
232
|
+
limit: Maximum messages to retrieve
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
List of messages
|
|
236
|
+
"""
|
|
237
|
+
# Create temporary consumer
|
|
238
|
+
consumer = AIOKafkaConsumer(
|
|
239
|
+
topic,
|
|
240
|
+
bootstrap_servers=self.bootstrap_servers,
|
|
241
|
+
value_deserializer=lambda v: json.loads(v.decode()),
|
|
242
|
+
auto_offset_reset='earliest',
|
|
243
|
+
consumer_timeout_ms=1000
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
try:
|
|
247
|
+
await consumer.start()
|
|
248
|
+
|
|
249
|
+
messages = []
|
|
250
|
+
async for msg in consumer:
|
|
251
|
+
message = Message.model_validate(msg.value)
|
|
252
|
+
messages.append(message)
|
|
253
|
+
|
|
254
|
+
if len(messages) >= limit:
|
|
255
|
+
break
|
|
256
|
+
|
|
257
|
+
return messages
|
|
258
|
+
|
|
259
|
+
finally:
|
|
260
|
+
await consumer.stop()
|