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,262 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""Redis broker adapter for AMB."""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import time
|
|
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
|
+
import redis.asyncio as aioredis
|
|
15
|
+
except ImportError:
|
|
16
|
+
raise ImportError(
|
|
17
|
+
"Redis adapter requires 'redis' package. "
|
|
18
|
+
"Install it with: pip install amb-core[redis]"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RedisBroker(BrokerAdapter):
|
|
23
|
+
"""
|
|
24
|
+
Redis-based broker adapter using pub/sub.
|
|
25
|
+
|
|
26
|
+
This adapter uses Redis pub/sub for message distribution and
|
|
27
|
+
Redis streams for request-response patterns.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, url: str = "redis://localhost:6379/0"):
|
|
31
|
+
"""
|
|
32
|
+
Initialize Redis broker.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
url: Redis connection URL
|
|
36
|
+
"""
|
|
37
|
+
self.url = url
|
|
38
|
+
self._client: Optional[aioredis.Redis] = None
|
|
39
|
+
self._pubsub: Optional[aioredis.client.PubSub] = None
|
|
40
|
+
self._subscriptions: Dict[str, str] = {} # subscription_id -> topic
|
|
41
|
+
self._handlers: Dict[str, MessageHandler] = {} # topic -> handler
|
|
42
|
+
self._tasks: set = set()
|
|
43
|
+
self._running = False
|
|
44
|
+
|
|
45
|
+
async def connect(self) -> None:
|
|
46
|
+
"""Connect to Redis."""
|
|
47
|
+
self._client = await aioredis.from_url(self.url)
|
|
48
|
+
self._pubsub = self._client.pubsub()
|
|
49
|
+
self._running = True
|
|
50
|
+
|
|
51
|
+
async def disconnect(self) -> None:
|
|
52
|
+
"""Disconnect from Redis."""
|
|
53
|
+
self._running = False
|
|
54
|
+
|
|
55
|
+
# Cancel all tasks
|
|
56
|
+
for task in self._tasks:
|
|
57
|
+
if not task.done():
|
|
58
|
+
task.cancel()
|
|
59
|
+
|
|
60
|
+
if self._tasks:
|
|
61
|
+
await asyncio.gather(*self._tasks, return_exceptions=True)
|
|
62
|
+
|
|
63
|
+
self._tasks.clear()
|
|
64
|
+
|
|
65
|
+
if self._pubsub:
|
|
66
|
+
await self._pubsub.close()
|
|
67
|
+
|
|
68
|
+
if self._client:
|
|
69
|
+
await self._client.close()
|
|
70
|
+
|
|
71
|
+
async def publish(self, message: Message, wait_for_confirmation: bool = False) -> Optional[str]:
|
|
72
|
+
"""
|
|
73
|
+
Publish message to Redis pub/sub.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
message: Message to publish
|
|
77
|
+
wait_for_confirmation: Wait for Redis confirmation
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Message ID
|
|
81
|
+
"""
|
|
82
|
+
if not self._client:
|
|
83
|
+
raise ConnectionError("Not connected to Redis")
|
|
84
|
+
|
|
85
|
+
# Serialize message
|
|
86
|
+
message_json = message.model_dump_json()
|
|
87
|
+
|
|
88
|
+
# Publish to channel
|
|
89
|
+
result = await self._client.publish(message.topic, message_json)
|
|
90
|
+
|
|
91
|
+
# Store in stream for pending messages
|
|
92
|
+
await self._client.xadd(
|
|
93
|
+
f"stream:{message.topic}",
|
|
94
|
+
{"data": message_json},
|
|
95
|
+
maxlen=1000 # Keep last 1000 messages
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if wait_for_confirmation:
|
|
99
|
+
# For Redis pub/sub, confirmation is implicit
|
|
100
|
+
# result contains number of subscribers that received the message
|
|
101
|
+
return message.id
|
|
102
|
+
|
|
103
|
+
return message.id
|
|
104
|
+
|
|
105
|
+
async def subscribe(self, topic: str, handler: MessageHandler) -> str:
|
|
106
|
+
"""
|
|
107
|
+
Subscribe to a Redis channel.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
topic: Topic/channel to subscribe to
|
|
111
|
+
handler: Message handler
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Subscription ID
|
|
115
|
+
"""
|
|
116
|
+
if not self._pubsub:
|
|
117
|
+
raise ConnectionError("Not connected to Redis")
|
|
118
|
+
|
|
119
|
+
subscription_id = str(uuid.uuid4())
|
|
120
|
+
self._subscriptions[subscription_id] = topic
|
|
121
|
+
self._handlers[topic] = handler
|
|
122
|
+
|
|
123
|
+
# Subscribe to channel
|
|
124
|
+
await self._pubsub.subscribe(topic)
|
|
125
|
+
|
|
126
|
+
# Start listener task if not already running
|
|
127
|
+
task = asyncio.create_task(self._listen_task(topic))
|
|
128
|
+
self._tasks.add(task)
|
|
129
|
+
task.add_done_callback(self._tasks.discard)
|
|
130
|
+
|
|
131
|
+
return subscription_id
|
|
132
|
+
|
|
133
|
+
async def _listen_task(self, topic: str):
|
|
134
|
+
"""Listen for messages on a topic."""
|
|
135
|
+
while self._running:
|
|
136
|
+
try:
|
|
137
|
+
message = await self._pubsub.get_message(ignore_subscribe_messages=True, timeout=1.0)
|
|
138
|
+
if message and message['type'] == 'message':
|
|
139
|
+
# Parse message
|
|
140
|
+
data = message['data']
|
|
141
|
+
if isinstance(data, bytes):
|
|
142
|
+
data = data.decode('utf-8')
|
|
143
|
+
|
|
144
|
+
msg = Message.model_validate_json(data)
|
|
145
|
+
|
|
146
|
+
# Call handler
|
|
147
|
+
if topic in self._handlers:
|
|
148
|
+
await self._handlers[topic](msg)
|
|
149
|
+
|
|
150
|
+
except asyncio.CancelledError:
|
|
151
|
+
break
|
|
152
|
+
except Exception as e:
|
|
153
|
+
# Log error but continue
|
|
154
|
+
print(f"Error in Redis listener: {e}")
|
|
155
|
+
await asyncio.sleep(1.0)
|
|
156
|
+
|
|
157
|
+
async def unsubscribe(self, subscription_id: str) -> None:
|
|
158
|
+
"""
|
|
159
|
+
Unsubscribe from a channel.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
subscription_id: Subscription ID
|
|
163
|
+
"""
|
|
164
|
+
if subscription_id not in self._subscriptions:
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
topic = self._subscriptions[subscription_id]
|
|
168
|
+
|
|
169
|
+
if self._pubsub:
|
|
170
|
+
await self._pubsub.unsubscribe(topic)
|
|
171
|
+
|
|
172
|
+
# Clean up
|
|
173
|
+
del self._subscriptions[subscription_id]
|
|
174
|
+
if topic in self._handlers:
|
|
175
|
+
del self._handlers[topic]
|
|
176
|
+
|
|
177
|
+
async def request(self, message: Message, timeout: float = 30.0) -> Message:
|
|
178
|
+
"""
|
|
179
|
+
Send request and wait for response using Redis streams.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
message: Request message
|
|
183
|
+
timeout: Timeout in seconds
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Response message
|
|
187
|
+
"""
|
|
188
|
+
if not self._client:
|
|
189
|
+
raise ConnectionError("Not connected to Redis")
|
|
190
|
+
|
|
191
|
+
# Generate correlation ID
|
|
192
|
+
if not message.correlation_id:
|
|
193
|
+
message.correlation_id = str(uuid.uuid4())
|
|
194
|
+
|
|
195
|
+
# Create response stream
|
|
196
|
+
response_key = f"response:{message.correlation_id}"
|
|
197
|
+
|
|
198
|
+
# Publish request
|
|
199
|
+
await self.publish(message, wait_for_confirmation=False)
|
|
200
|
+
|
|
201
|
+
# Wait for response
|
|
202
|
+
try:
|
|
203
|
+
start_time = time.monotonic()
|
|
204
|
+
while True:
|
|
205
|
+
# Check for timeout
|
|
206
|
+
elapsed = time.monotonic() - start_time
|
|
207
|
+
if elapsed > timeout:
|
|
208
|
+
raise TimeoutError(f"No response received within {timeout} seconds")
|
|
209
|
+
|
|
210
|
+
# Calculate remaining time for blocking
|
|
211
|
+
remaining_time = timeout - elapsed
|
|
212
|
+
block_ms = max(int(remaining_time * 1000), 100) # At least 100ms
|
|
213
|
+
|
|
214
|
+
# Read from response stream
|
|
215
|
+
messages = await self._client.xread(
|
|
216
|
+
{response_key: '0'},
|
|
217
|
+
count=1,
|
|
218
|
+
block=block_ms
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
if messages:
|
|
222
|
+
# Parse response
|
|
223
|
+
stream_name, message_list = messages[0]
|
|
224
|
+
message_id, data = message_list[0]
|
|
225
|
+
response_json = data[b'data'].decode('utf-8')
|
|
226
|
+
|
|
227
|
+
# Clean up
|
|
228
|
+
await self._client.delete(response_key)
|
|
229
|
+
|
|
230
|
+
return Message.model_validate_json(response_json)
|
|
231
|
+
|
|
232
|
+
await asyncio.sleep(0.1)
|
|
233
|
+
|
|
234
|
+
except asyncio.TimeoutError:
|
|
235
|
+
raise TimeoutError(f"No response received within {timeout} seconds")
|
|
236
|
+
|
|
237
|
+
async def get_pending_messages(self, topic: str, limit: int = 10) -> List[Message]:
|
|
238
|
+
"""
|
|
239
|
+
Get pending messages from Redis stream.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
topic: Topic to get messages from
|
|
243
|
+
limit: Maximum messages to retrieve
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
List of messages
|
|
247
|
+
"""
|
|
248
|
+
if not self._client:
|
|
249
|
+
raise ConnectionError("Not connected to Redis")
|
|
250
|
+
|
|
251
|
+
stream_key = f"stream:{topic}"
|
|
252
|
+
|
|
253
|
+
# Read from stream
|
|
254
|
+
messages = await self._client.xrevrange(stream_key, count=limit)
|
|
255
|
+
|
|
256
|
+
result = []
|
|
257
|
+
for message_id, data in messages:
|
|
258
|
+
message_json = data[b'data'].decode('utf-8')
|
|
259
|
+
msg = Message.model_validate_json(message_json)
|
|
260
|
+
result.append(msg)
|
|
261
|
+
|
|
262
|
+
return result
|
amb_core/broker.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""Broker adapter interface for AMB."""
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import Awaitable, Callable, List, Optional
|
|
7
|
+
|
|
8
|
+
from amb_core.models import Message
|
|
9
|
+
|
|
10
|
+
# Type alias for message handler functions
|
|
11
|
+
MessageHandler = Callable[[Message], Awaitable[None]]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BrokerAdapter(ABC):
|
|
15
|
+
"""
|
|
16
|
+
Abstract base class for broker adapters.
|
|
17
|
+
|
|
18
|
+
This provides a broker-agnostic interface that can be implemented
|
|
19
|
+
for different message brokers (Redis, RabbitMQ, Kafka, etc.).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
async def connect(self) -> None:
|
|
24
|
+
"""
|
|
25
|
+
Establish connection to the broker.
|
|
26
|
+
|
|
27
|
+
Raises:
|
|
28
|
+
ConnectionError: If connection fails
|
|
29
|
+
"""
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
async def disconnect(self) -> None:
|
|
34
|
+
"""
|
|
35
|
+
Close connection to the broker.
|
|
36
|
+
"""
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
async def publish(self, message: Message, wait_for_confirmation: bool = False) -> Optional[str]:
|
|
41
|
+
"""
|
|
42
|
+
Publish a message to the broker.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
message: The message to publish
|
|
46
|
+
wait_for_confirmation: If True, wait for broker confirmation (slower but reliable)
|
|
47
|
+
If False, fire and forget (faster but no guarantee)
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Optional message ID or confirmation token if wait_for_confirmation is True
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
PublishError: If publishing fails
|
|
54
|
+
"""
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
@abstractmethod
|
|
58
|
+
async def subscribe(self, topic: str, handler: MessageHandler) -> str:
|
|
59
|
+
"""
|
|
60
|
+
Subscribe to a topic and register a handler.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
topic: Topic to subscribe to
|
|
64
|
+
handler: Async function to handle incoming messages
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Subscription ID that can be used to unsubscribe
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
SubscriptionError: If subscription fails
|
|
71
|
+
"""
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
@abstractmethod
|
|
75
|
+
async def unsubscribe(self, subscription_id: str) -> None:
|
|
76
|
+
"""
|
|
77
|
+
Unsubscribe from a topic.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
subscription_id: The subscription ID returned from subscribe()
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
SubscriptionError: If unsubscription fails
|
|
84
|
+
"""
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
@abstractmethod
|
|
88
|
+
async def request(self, message: Message, timeout: float = 30.0) -> Message:
|
|
89
|
+
"""
|
|
90
|
+
Send a request and wait for a response (request-response pattern).
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
message: The request message
|
|
94
|
+
timeout: Maximum time to wait for response in seconds
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
The response message
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
TimeoutError: If no response received within timeout
|
|
101
|
+
RequestError: If request fails
|
|
102
|
+
"""
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
@abstractmethod
|
|
106
|
+
async def get_pending_messages(self, topic: str, limit: int = 10) -> List[Message]:
|
|
107
|
+
"""
|
|
108
|
+
Get pending messages from a topic (if supported by broker).
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
topic: Topic to get messages from
|
|
112
|
+
limit: Maximum number of messages to retrieve
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
List of pending messages
|
|
116
|
+
"""
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
def get_backpressure_stats(self, topic: Optional[str] = None):
|
|
120
|
+
"""
|
|
121
|
+
Get backpressure statistics (if supported by broker).
|
|
122
|
+
|
|
123
|
+
Optional method - brokers may choose not to implement this.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
topic: Optional topic to get stats for
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Statistics about backpressure events
|
|
130
|
+
"""
|
|
131
|
+
return {}
|
|
132
|
+
|
|
133
|
+
def get_queue_size(self, topic: str) -> int:
|
|
134
|
+
"""
|
|
135
|
+
Get current queue size for a topic (if supported by broker).
|
|
136
|
+
|
|
137
|
+
Optional method - brokers may choose not to implement this.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
topic: The topic to check
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Number of messages in the queue, or 0 if not supported
|
|
144
|
+
"""
|
|
145
|
+
return 0
|