omnibase_infra 0.2.1__py3-none-any.whl → 0.2.3__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.
Files changed (161) hide show
  1. omnibase_infra/__init__.py +1 -1
  2. omnibase_infra/adapters/adapter_onex_tool_execution.py +451 -0
  3. omnibase_infra/capabilities/__init__.py +15 -0
  4. omnibase_infra/capabilities/capability_inference_rules.py +211 -0
  5. omnibase_infra/capabilities/contract_capability_extractor.py +221 -0
  6. omnibase_infra/capabilities/intent_type_extractor.py +160 -0
  7. omnibase_infra/cli/commands.py +1 -1
  8. omnibase_infra/configs/widget_mapping.yaml +176 -0
  9. omnibase_infra/contracts/handlers/filesystem/handler_contract.yaml +5 -2
  10. omnibase_infra/contracts/handlers/mcp/handler_contract.yaml +5 -2
  11. omnibase_infra/enums/__init__.py +6 -0
  12. omnibase_infra/enums/enum_handler_error_type.py +10 -0
  13. omnibase_infra/enums/enum_handler_source_mode.py +72 -0
  14. omnibase_infra/enums/enum_kafka_acks.py +99 -0
  15. omnibase_infra/errors/error_compute_registry.py +4 -1
  16. omnibase_infra/errors/error_event_bus_registry.py +4 -1
  17. omnibase_infra/errors/error_infra.py +3 -1
  18. omnibase_infra/errors/error_policy_registry.py +4 -1
  19. omnibase_infra/event_bus/event_bus_kafka.py +1 -1
  20. omnibase_infra/event_bus/models/config/model_kafka_event_bus_config.py +59 -10
  21. omnibase_infra/handlers/__init__.py +8 -1
  22. omnibase_infra/handlers/handler_consul.py +7 -1
  23. omnibase_infra/handlers/handler_db.py +10 -3
  24. omnibase_infra/handlers/handler_graph.py +10 -5
  25. omnibase_infra/handlers/handler_http.py +8 -2
  26. omnibase_infra/handlers/handler_intent.py +387 -0
  27. omnibase_infra/handlers/handler_mcp.py +745 -63
  28. omnibase_infra/handlers/handler_vault.py +11 -5
  29. omnibase_infra/handlers/mixins/mixin_consul_kv.py +4 -3
  30. omnibase_infra/handlers/mixins/mixin_consul_service.py +2 -1
  31. omnibase_infra/handlers/registration_storage/handler_registration_storage_postgres.py +7 -0
  32. omnibase_infra/handlers/service_discovery/handler_service_discovery_consul.py +308 -4
  33. omnibase_infra/handlers/service_discovery/models/model_service_info.py +10 -0
  34. omnibase_infra/mixins/mixin_async_circuit_breaker.py +3 -2
  35. omnibase_infra/mixins/mixin_node_introspection.py +42 -7
  36. omnibase_infra/mixins/mixin_retry_execution.py +1 -1
  37. omnibase_infra/models/discovery/model_introspection_config.py +11 -0
  38. omnibase_infra/models/handlers/__init__.py +48 -5
  39. omnibase_infra/models/handlers/model_bootstrap_handler_descriptor.py +162 -0
  40. omnibase_infra/models/handlers/model_contract_discovery_result.py +6 -4
  41. omnibase_infra/models/handlers/model_handler_descriptor.py +15 -0
  42. omnibase_infra/models/handlers/model_handler_source_config.py +220 -0
  43. omnibase_infra/models/mcp/__init__.py +15 -0
  44. omnibase_infra/models/mcp/model_mcp_contract_config.py +80 -0
  45. omnibase_infra/models/mcp/model_mcp_server_config.py +67 -0
  46. omnibase_infra/models/mcp/model_mcp_tool_definition.py +73 -0
  47. omnibase_infra/models/mcp/model_mcp_tool_parameter.py +35 -0
  48. omnibase_infra/models/registration/model_node_capabilities.py +11 -0
  49. omnibase_infra/models/registration/model_node_introspection_event.py +9 -0
  50. omnibase_infra/models/runtime/model_handler_contract.py +25 -9
  51. omnibase_infra/models/runtime/model_loaded_handler.py +9 -0
  52. omnibase_infra/nodes/architecture_validator/contract_architecture_validator.yaml +0 -5
  53. omnibase_infra/nodes/architecture_validator/registry/registry_infra_architecture_validator.py +17 -10
  54. omnibase_infra/nodes/effects/contract.yaml +0 -5
  55. omnibase_infra/nodes/node_registration_orchestrator/contract.yaml +7 -0
  56. omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_introspected.py +86 -1
  57. omnibase_infra/nodes/node_registration_orchestrator/introspection_event_router.py +3 -3
  58. omnibase_infra/nodes/node_registration_orchestrator/plugin.py +1 -1
  59. omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +9 -8
  60. omnibase_infra/nodes/node_registration_orchestrator/timeout_coordinator.py +4 -3
  61. omnibase_infra/nodes/node_registration_orchestrator/wiring.py +14 -13
  62. omnibase_infra/nodes/node_registration_storage_effect/contract.yaml +0 -5
  63. omnibase_infra/nodes/node_registration_storage_effect/node.py +4 -1
  64. omnibase_infra/nodes/node_registration_storage_effect/registry/registry_infra_registration_storage.py +47 -26
  65. omnibase_infra/nodes/node_registry_effect/contract.yaml +0 -5
  66. omnibase_infra/nodes/node_registry_effect/handlers/handler_partial_retry.py +2 -1
  67. omnibase_infra/nodes/node_service_discovery_effect/registry/registry_infra_service_discovery.py +28 -20
  68. omnibase_infra/plugins/examples/plugin_json_normalizer.py +2 -2
  69. omnibase_infra/plugins/examples/plugin_json_normalizer_error_handling.py +2 -2
  70. omnibase_infra/plugins/plugin_compute_base.py +16 -2
  71. omnibase_infra/protocols/__init__.py +2 -0
  72. omnibase_infra/protocols/protocol_container_aware.py +200 -0
  73. omnibase_infra/protocols/protocol_event_projector.py +1 -1
  74. omnibase_infra/runtime/__init__.py +90 -1
  75. omnibase_infra/runtime/binding_config_resolver.py +102 -37
  76. omnibase_infra/runtime/constants_notification.py +75 -0
  77. omnibase_infra/runtime/contract_handler_discovery.py +6 -1
  78. omnibase_infra/runtime/handler_bootstrap_source.py +507 -0
  79. omnibase_infra/runtime/handler_contract_config_loader.py +603 -0
  80. omnibase_infra/runtime/handler_contract_source.py +267 -186
  81. omnibase_infra/runtime/handler_identity.py +81 -0
  82. omnibase_infra/runtime/handler_plugin_loader.py +19 -2
  83. omnibase_infra/runtime/handler_registry.py +11 -3
  84. omnibase_infra/runtime/handler_source_resolver.py +326 -0
  85. omnibase_infra/runtime/mixin_semver_cache.py +25 -1
  86. omnibase_infra/runtime/mixins/__init__.py +7 -0
  87. omnibase_infra/runtime/mixins/mixin_projector_notification_publishing.py +566 -0
  88. omnibase_infra/runtime/mixins/mixin_projector_sql_operations.py +31 -10
  89. omnibase_infra/runtime/models/__init__.py +24 -0
  90. omnibase_infra/runtime/models/model_health_check_result.py +2 -1
  91. omnibase_infra/runtime/models/model_projector_notification_config.py +171 -0
  92. omnibase_infra/runtime/models/model_transition_notification_outbox_config.py +112 -0
  93. omnibase_infra/runtime/models/model_transition_notification_outbox_metrics.py +140 -0
  94. omnibase_infra/runtime/models/model_transition_notification_publisher_metrics.py +357 -0
  95. omnibase_infra/runtime/projector_plugin_loader.py +1 -1
  96. omnibase_infra/runtime/projector_shell.py +229 -1
  97. omnibase_infra/runtime/protocol_lifecycle_executor.py +6 -6
  98. omnibase_infra/runtime/protocols/__init__.py +10 -0
  99. omnibase_infra/runtime/registry/registry_protocol_binding.py +16 -15
  100. omnibase_infra/runtime/registry_contract_source.py +693 -0
  101. omnibase_infra/runtime/registry_policy.py +9 -326
  102. omnibase_infra/runtime/secret_resolver.py +4 -2
  103. omnibase_infra/runtime/service_kernel.py +11 -3
  104. omnibase_infra/runtime/service_message_dispatch_engine.py +4 -2
  105. omnibase_infra/runtime/service_runtime_host_process.py +589 -106
  106. omnibase_infra/runtime/transition_notification_outbox.py +1190 -0
  107. omnibase_infra/runtime/transition_notification_publisher.py +764 -0
  108. omnibase_infra/runtime/util_container_wiring.py +6 -5
  109. omnibase_infra/runtime/util_wiring.py +17 -4
  110. omnibase_infra/schemas/schema_transition_notification_outbox.sql +245 -0
  111. omnibase_infra/services/__init__.py +21 -0
  112. omnibase_infra/services/corpus_capture.py +7 -1
  113. omnibase_infra/services/mcp/__init__.py +31 -0
  114. omnibase_infra/services/mcp/mcp_server_lifecycle.py +449 -0
  115. omnibase_infra/services/mcp/service_mcp_tool_discovery.py +411 -0
  116. omnibase_infra/services/mcp/service_mcp_tool_registry.py +329 -0
  117. omnibase_infra/services/mcp/service_mcp_tool_sync.py +547 -0
  118. omnibase_infra/services/registry_api/__init__.py +40 -0
  119. omnibase_infra/services/registry_api/main.py +261 -0
  120. omnibase_infra/services/registry_api/models/__init__.py +66 -0
  121. omnibase_infra/services/registry_api/models/model_capability_widget_mapping.py +38 -0
  122. omnibase_infra/services/registry_api/models/model_pagination_info.py +48 -0
  123. omnibase_infra/services/registry_api/models/model_registry_discovery_response.py +73 -0
  124. omnibase_infra/services/registry_api/models/model_registry_health_response.py +49 -0
  125. omnibase_infra/services/registry_api/models/model_registry_instance_view.py +88 -0
  126. omnibase_infra/services/registry_api/models/model_registry_node_view.py +88 -0
  127. omnibase_infra/services/registry_api/models/model_registry_summary.py +60 -0
  128. omnibase_infra/services/registry_api/models/model_response_list_instances.py +43 -0
  129. omnibase_infra/services/registry_api/models/model_response_list_nodes.py +51 -0
  130. omnibase_infra/services/registry_api/models/model_warning.py +49 -0
  131. omnibase_infra/services/registry_api/models/model_widget_defaults.py +28 -0
  132. omnibase_infra/services/registry_api/models/model_widget_mapping.py +51 -0
  133. omnibase_infra/services/registry_api/routes.py +371 -0
  134. omnibase_infra/services/registry_api/service.py +837 -0
  135. omnibase_infra/services/service_capability_query.py +4 -4
  136. omnibase_infra/services/service_health.py +3 -2
  137. omnibase_infra/services/service_timeout_emitter.py +20 -3
  138. omnibase_infra/services/service_timeout_scanner.py +7 -3
  139. omnibase_infra/services/session/__init__.py +56 -0
  140. omnibase_infra/services/session/config_consumer.py +120 -0
  141. omnibase_infra/services/session/config_store.py +139 -0
  142. omnibase_infra/services/session/consumer.py +1007 -0
  143. omnibase_infra/services/session/protocol_session_aggregator.py +117 -0
  144. omnibase_infra/services/session/store.py +997 -0
  145. omnibase_infra/utils/__init__.py +19 -0
  146. omnibase_infra/utils/util_atomic_file.py +261 -0
  147. omnibase_infra/utils/util_db_transaction.py +239 -0
  148. omnibase_infra/utils/util_dsn_validation.py +1 -1
  149. omnibase_infra/utils/util_retry_optimistic.py +281 -0
  150. omnibase_infra/validation/__init__.py +3 -19
  151. omnibase_infra/validation/contracts/security.validation.yaml +114 -0
  152. omnibase_infra/validation/infra_validators.py +35 -24
  153. omnibase_infra/validation/validation_exemptions.yaml +140 -9
  154. omnibase_infra/validation/validator_chain_propagation.py +2 -2
  155. omnibase_infra/validation/validator_runtime_shape.py +1 -1
  156. omnibase_infra/validation/validator_security.py +473 -370
  157. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.3.dist-info}/METADATA +3 -3
  158. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.3.dist-info}/RECORD +161 -98
  159. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.3.dist-info}/WHEEL +0 -0
  160. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.3.dist-info}/entry_points.txt +0 -0
  161. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,387 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Intent Handler - Temporary demo wiring for intent graph operations.
4
+
5
+ Wraps HandlerGraph to provide intent-specific graph operations for the demo.
6
+ This is temporary hardcoded routing that will be replaced by contract-driven
7
+ handler routing in production.
8
+
9
+ Supported Operations:
10
+ - intent.store: Store an intent as a graph node with label "Intent"
11
+ - intent.query_session: Query intents by session_id property
12
+ - intent.query_distribution: Get intent count/statistics
13
+
14
+ Note:
15
+ This is TEMPORARY demo wiring. Keep it simple and focused on the demo use case.
16
+ Production implementation should use contract-driven handler routing.
17
+ """
18
+
19
+ # TODO(OMN-1515): Remove demo wiring after intent routing is contract-driven
20
+
21
+ from __future__ import annotations
22
+
23
+ import logging
24
+ from uuid import UUID, uuid4
25
+
26
+ from omnibase_core.container import ModelONEXContainer
27
+ from omnibase_core.types import JsonType
28
+ from omnibase_infra.enums import EnumInfraTransportType
29
+ from omnibase_infra.errors import (
30
+ ModelInfraErrorContext,
31
+ RuntimeHostError,
32
+ )
33
+ from omnibase_infra.handlers.handler_graph import HandlerGraph
34
+ from omnibase_infra.mixins import MixinEnvelopeExtraction
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+ HANDLER_ID_INTENT: str = "intent-handler"
39
+ _SUPPORTED_OPERATIONS: frozenset[str] = frozenset(
40
+ {
41
+ "intent.store",
42
+ "intent.query_session",
43
+ "intent.query_distribution",
44
+ }
45
+ )
46
+
47
+
48
+ class HandlerIntent(MixinEnvelopeExtraction): # DEMO ONLY
49
+ """Intent handler wrapping HandlerGraph for intent-specific operations.
50
+
51
+ This handler provides a simplified interface for storing and querying
52
+ intents in the graph database. It wraps HandlerGraph and translates
53
+ intent-specific operations to graph operations.
54
+
55
+ Note:
56
+ This is temporary demo wiring. The handler assumes HandlerGraph
57
+ is already initialized and passed via config during initialize().
58
+
59
+ Idempotency:
60
+ - intent.store: NOT idempotent (creates new node each call)
61
+ - intent.query_session: Idempotent (read-only query)
62
+ - intent.query_distribution: Idempotent (read-only aggregation)
63
+ """
64
+
65
+ def __init__(self, container: ModelONEXContainer) -> None:
66
+ """Initialize HandlerIntent with ONEX container for dependency injection.
67
+
68
+ Args:
69
+ container: ONEX container for dependency injection.
70
+ """
71
+ self._container = container
72
+ self._graph_handler: HandlerGraph | None = None
73
+ self._initialized: bool = False
74
+
75
+ async def initialize(self, config: dict[str, object]) -> None:
76
+ """Initialize the intent handler.
77
+
78
+ Args:
79
+ config: Configuration dict containing:
80
+ - graph_handler: Pre-initialized HandlerGraph instance (required)
81
+
82
+ Raises:
83
+ RuntimeHostError: If graph_handler is missing or invalid.
84
+ """
85
+ init_correlation_id = uuid4()
86
+
87
+ logger.info(
88
+ "Initializing %s",
89
+ self.__class__.__name__,
90
+ extra={
91
+ "handler": self.__class__.__name__,
92
+ "correlation_id": str(init_correlation_id),
93
+ },
94
+ )
95
+
96
+ graph_handler = config.get("graph_handler")
97
+ if not isinstance(graph_handler, HandlerGraph):
98
+ ctx = ModelInfraErrorContext.with_correlation(
99
+ transport_type=EnumInfraTransportType.GRAPH,
100
+ operation="initialize",
101
+ target_name="intent_handler",
102
+ correlation_id=init_correlation_id,
103
+ )
104
+ raise RuntimeHostError(
105
+ "Missing or invalid 'graph_handler' in config - "
106
+ "must be an initialized HandlerGraph instance",
107
+ context=ctx,
108
+ )
109
+
110
+ self._graph_handler = graph_handler
111
+ self._initialized = True
112
+
113
+ logger.info(
114
+ "%s initialized successfully",
115
+ self.__class__.__name__,
116
+ extra={
117
+ "handler": self.__class__.__name__,
118
+ "correlation_id": str(init_correlation_id),
119
+ },
120
+ )
121
+
122
+ async def shutdown(self) -> None:
123
+ """Shutdown the intent handler.
124
+
125
+ Note:
126
+ This handler does not own the graph handler, so we do not
127
+ shut it down here. The caller is responsible for managing
128
+ the graph handler lifecycle.
129
+ """
130
+ self._graph_handler = None
131
+ self._initialized = False
132
+ logger.info("HandlerIntent shutdown complete")
133
+
134
+ async def execute(self, envelope: dict[str, object]) -> dict[str, object]:
135
+ """Execute intent operation from envelope.
136
+
137
+ Args:
138
+ envelope: Request envelope containing:
139
+ - operation: Intent operation (intent.store, intent.query_session, etc.)
140
+ - payload: dict with operation-specific parameters
141
+ - correlation_id: Optional correlation ID for tracing
142
+ - envelope_id: Optional envelope ID for causality tracking
143
+
144
+ Returns:
145
+ dict containing operation result with:
146
+ - success: bool indicating operation success
147
+ - data: Operation-specific result data
148
+ - correlation_id: UUID string for tracing
149
+
150
+ Raises:
151
+ RuntimeHostError: If handler not initialized or invalid input.
152
+ """
153
+ correlation_id = self._extract_correlation_id(envelope)
154
+
155
+ if not self._initialized or self._graph_handler is None:
156
+ ctx = ModelInfraErrorContext.with_correlation(
157
+ transport_type=EnumInfraTransportType.GRAPH,
158
+ operation="execute",
159
+ target_name="intent_handler",
160
+ correlation_id=correlation_id,
161
+ )
162
+ raise RuntimeHostError(
163
+ "HandlerIntent not initialized. Call initialize() first.",
164
+ context=ctx,
165
+ )
166
+
167
+ operation = envelope.get("operation")
168
+ if not isinstance(operation, str):
169
+ ctx = ModelInfraErrorContext.with_correlation(
170
+ transport_type=EnumInfraTransportType.GRAPH,
171
+ operation="execute",
172
+ target_name="intent_handler",
173
+ correlation_id=correlation_id,
174
+ )
175
+ raise RuntimeHostError(
176
+ "Missing or invalid 'operation' in envelope",
177
+ context=ctx,
178
+ )
179
+
180
+ if operation not in _SUPPORTED_OPERATIONS:
181
+ ctx = ModelInfraErrorContext.with_correlation(
182
+ transport_type=EnumInfraTransportType.GRAPH,
183
+ operation=operation,
184
+ target_name="intent_handler",
185
+ correlation_id=correlation_id,
186
+ )
187
+ raise RuntimeHostError(
188
+ f"Operation '{operation}' not supported. "
189
+ f"Available: {', '.join(sorted(_SUPPORTED_OPERATIONS))}",
190
+ context=ctx,
191
+ )
192
+
193
+ payload = envelope.get("payload")
194
+ if not isinstance(payload, dict):
195
+ ctx = ModelInfraErrorContext.with_correlation(
196
+ transport_type=EnumInfraTransportType.GRAPH,
197
+ operation=operation,
198
+ target_name="intent_handler",
199
+ correlation_id=correlation_id,
200
+ )
201
+ raise RuntimeHostError(
202
+ "Missing or invalid 'payload' in envelope",
203
+ context=ctx,
204
+ )
205
+
206
+ # Route to appropriate handler method
207
+ if operation == "intent.store":
208
+ return await self._store_intent(payload, correlation_id)
209
+ elif operation == "intent.query_session":
210
+ return await self._query_session(payload, correlation_id)
211
+ else: # intent.query_distribution
212
+ return await self._query_distribution(correlation_id)
213
+
214
+ async def _store_intent(
215
+ self, payload: dict[str, object], correlation_id: UUID
216
+ ) -> dict[str, object]:
217
+ """Store an intent as a graph node.
218
+
219
+ Args:
220
+ payload: Intent data to store. Should contain:
221
+ - intent_type: Type of intent (required)
222
+ - session_id: Session identifier (optional)
223
+ - Additional properties as needed
224
+
225
+ Returns:
226
+ dict with created node details.
227
+ """
228
+ # Note: _graph_handler is guaranteed non-None by execute() validation
229
+ assert self._graph_handler is not None # Type narrowing for mypy
230
+
231
+ # Extract intent properties - use JsonType for graph compatibility
232
+ properties: dict[str, JsonType] = {
233
+ "correlation_id": str(correlation_id),
234
+ }
235
+
236
+ # Copy all payload properties to node properties
237
+ for key, value in payload.items():
238
+ # Convert non-primitive types to strings for graph storage
239
+ # NOTE: Using tuple form for isinstance to avoid union validator flag
240
+ if isinstance(value, (str, int, float, bool)) or value is None:
241
+ properties[key] = value
242
+ else:
243
+ properties[key] = str(value)
244
+
245
+ # Create the intent node
246
+ node = await self._graph_handler.create_node(
247
+ labels=["Intent"],
248
+ properties=properties,
249
+ )
250
+
251
+ return {
252
+ "success": True,
253
+ "data": {
254
+ "node_id": node.id,
255
+ "element_id": node.element_id,
256
+ "labels": node.labels,
257
+ "properties": node.properties,
258
+ },
259
+ "correlation_id": str(correlation_id),
260
+ }
261
+
262
+ async def _query_session(
263
+ self, payload: dict[str, object], correlation_id: UUID
264
+ ) -> dict[str, object]:
265
+ """Query intents by session_id.
266
+
267
+ Args:
268
+ payload: Query parameters. Should contain:
269
+ - session_id: Session identifier to filter by (required)
270
+
271
+ Returns:
272
+ dict with matching intent nodes.
273
+ """
274
+ # Note: _graph_handler is guaranteed non-None by execute() validation
275
+ assert self._graph_handler is not None # Type narrowing for mypy
276
+
277
+ session_id = payload.get("session_id")
278
+ if not session_id:
279
+ ctx = ModelInfraErrorContext.with_correlation(
280
+ transport_type=EnumInfraTransportType.GRAPH,
281
+ operation="intent.query_session",
282
+ target_name="intent_handler",
283
+ correlation_id=correlation_id,
284
+ )
285
+ raise RuntimeHostError(
286
+ "Missing 'session_id' in payload",
287
+ context=ctx,
288
+ )
289
+
290
+ # Query intents by session_id
291
+ # SECURITY: Using parameterized query ($session_id) to prevent Cypher injection
292
+ query = """
293
+ MATCH (i:Intent {session_id: $session_id})
294
+ RETURN i, elementId(i) as eid, id(i) as nid
295
+ ORDER BY i.created_at DESC
296
+ """
297
+
298
+ result = await self._graph_handler.execute_query(
299
+ query=query,
300
+ parameters={"session_id": str(session_id)},
301
+ )
302
+
303
+ # Transform records to intent data
304
+ intents = []
305
+ for record in result.records:
306
+ node = record.get("i")
307
+ if node:
308
+ intents.append(
309
+ {
310
+ "node_id": str(record.get("nid", "")),
311
+ "element_id": str(record.get("eid", "")),
312
+ "properties": dict(node) if isinstance(node, dict) else {},
313
+ }
314
+ )
315
+
316
+ return {
317
+ "success": True,
318
+ "data": {
319
+ "session_id": str(session_id),
320
+ "intents": intents,
321
+ "count": len(intents),
322
+ },
323
+ "correlation_id": str(correlation_id),
324
+ }
325
+
326
+ async def _query_distribution(self, correlation_id: UUID) -> dict[str, object]:
327
+ """Query intent distribution/statistics.
328
+
329
+ Returns:
330
+ dict with intent statistics including counts by intent_type.
331
+ """
332
+ # Note: _graph_handler is guaranteed non-None by execute() validation
333
+ assert self._graph_handler is not None # Type narrowing for mypy
334
+
335
+ # Query total count
336
+ count_query = "MATCH (i:Intent) RETURN count(i) as total"
337
+ count_result = await self._graph_handler.execute_query(query=count_query)
338
+
339
+ total_count = 0
340
+ if count_result.records:
341
+ raw_total = count_result.records[0].get("total", 0)
342
+ total_count = int(raw_total) if isinstance(raw_total, int | float) else 0
343
+
344
+ # Query distribution by intent_type
345
+ distribution_query = """
346
+ MATCH (i:Intent)
347
+ RETURN i.intent_type as intent_type, count(i) as count
348
+ ORDER BY count DESC
349
+ """
350
+ distribution_result = await self._graph_handler.execute_query(
351
+ query=distribution_query
352
+ )
353
+
354
+ # Build distribution dict
355
+ distribution: dict[str, int] = {}
356
+ for record in distribution_result.records:
357
+ intent_type = record.get("intent_type")
358
+ raw_count = record.get("count", 0)
359
+ if intent_type:
360
+ count_val = int(raw_count) if isinstance(raw_count, int | float) else 0
361
+ distribution[str(intent_type)] = count_val
362
+
363
+ return {
364
+ "success": True,
365
+ "data": {
366
+ "total_count": total_count,
367
+ "distribution": distribution,
368
+ },
369
+ "correlation_id": str(correlation_id),
370
+ }
371
+
372
+ def describe(self) -> dict[str, object]:
373
+ """Return handler metadata and capabilities.
374
+
375
+ Returns:
376
+ dict containing handler information.
377
+ """
378
+ return {
379
+ "handler_id": HANDLER_ID_INTENT,
380
+ "handler_type": "intent_handler",
381
+ "supported_operations": sorted(_SUPPORTED_OPERATIONS),
382
+ "initialized": self._initialized,
383
+ "version": "0.1.0-demo",
384
+ }
385
+
386
+
387
+ __all__: list[str] = ["HandlerIntent", "HANDLER_ID_INTENT"]