aip-agents-binary 0.5.20__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.
- aip_agents/__init__.py +65 -0
- aip_agents/a2a/__init__.py +19 -0
- aip_agents/a2a/server/__init__.py +10 -0
- aip_agents/a2a/server/base_executor.py +1086 -0
- aip_agents/a2a/server/google_adk_executor.py +198 -0
- aip_agents/a2a/server/langflow_executor.py +180 -0
- aip_agents/a2a/server/langgraph_executor.py +270 -0
- aip_agents/a2a/types.py +232 -0
- aip_agents/agent/__init__.py +27 -0
- aip_agents/agent/base_agent.py +970 -0
- aip_agents/agent/base_langgraph_agent.py +2942 -0
- aip_agents/agent/google_adk_agent.py +926 -0
- aip_agents/agent/google_adk_constants.py +6 -0
- aip_agents/agent/hitl/__init__.py +24 -0
- aip_agents/agent/hitl/config.py +28 -0
- aip_agents/agent/hitl/langgraph_hitl_mixin.py +515 -0
- aip_agents/agent/hitl/manager.py +532 -0
- aip_agents/agent/hitl/models.py +18 -0
- aip_agents/agent/hitl/prompt/__init__.py +9 -0
- aip_agents/agent/hitl/prompt/base.py +42 -0
- aip_agents/agent/hitl/prompt/deferred.py +73 -0
- aip_agents/agent/hitl/registry.py +149 -0
- aip_agents/agent/interface.py +138 -0
- aip_agents/agent/interfaces.py +65 -0
- aip_agents/agent/langflow_agent.py +464 -0
- aip_agents/agent/langgraph_memory_enhancer_agent.py +433 -0
- aip_agents/agent/langgraph_react_agent.py +2514 -0
- aip_agents/agent/system_instruction_context.py +34 -0
- aip_agents/clients/__init__.py +10 -0
- aip_agents/clients/langflow/__init__.py +10 -0
- aip_agents/clients/langflow/client.py +477 -0
- aip_agents/clients/langflow/types.py +18 -0
- aip_agents/constants.py +23 -0
- aip_agents/credentials/manager.py +132 -0
- aip_agents/examples/__init__.py +5 -0
- aip_agents/examples/compare_streaming_client.py +783 -0
- aip_agents/examples/compare_streaming_server.py +142 -0
- aip_agents/examples/demo_memory_recall.py +401 -0
- aip_agents/examples/hello_world_a2a_google_adk_client.py +49 -0
- aip_agents/examples/hello_world_a2a_google_adk_client_agent.py +48 -0
- aip_agents/examples/hello_world_a2a_google_adk_client_streaming.py +60 -0
- aip_agents/examples/hello_world_a2a_google_adk_server.py +79 -0
- aip_agents/examples/hello_world_a2a_langchain_client.py +39 -0
- aip_agents/examples/hello_world_a2a_langchain_client_agent.py +39 -0
- aip_agents/examples/hello_world_a2a_langchain_client_lm_invoker.py +37 -0
- aip_agents/examples/hello_world_a2a_langchain_client_streaming.py +41 -0
- aip_agents/examples/hello_world_a2a_langchain_reference_client_streaming.py +60 -0
- aip_agents/examples/hello_world_a2a_langchain_reference_server.py +105 -0
- aip_agents/examples/hello_world_a2a_langchain_server.py +79 -0
- aip_agents/examples/hello_world_a2a_langchain_server_lm_invoker.py +78 -0
- aip_agents/examples/hello_world_a2a_langflow_client.py +83 -0
- aip_agents/examples/hello_world_a2a_langflow_server.py +82 -0
- aip_agents/examples/hello_world_a2a_langgraph_artifact_client.py +73 -0
- aip_agents/examples/hello_world_a2a_langgraph_artifact_client_streaming.py +76 -0
- aip_agents/examples/hello_world_a2a_langgraph_artifact_server.py +92 -0
- aip_agents/examples/hello_world_a2a_langgraph_client.py +54 -0
- aip_agents/examples/hello_world_a2a_langgraph_client_agent.py +54 -0
- aip_agents/examples/hello_world_a2a_langgraph_client_agent_lm_invoker.py +32 -0
- aip_agents/examples/hello_world_a2a_langgraph_client_streaming.py +50 -0
- aip_agents/examples/hello_world_a2a_langgraph_client_streaming_lm_invoker.py +44 -0
- aip_agents/examples/hello_world_a2a_langgraph_client_streaming_tool_streaming.py +92 -0
- aip_agents/examples/hello_world_a2a_langgraph_server.py +84 -0
- aip_agents/examples/hello_world_a2a_langgraph_server_lm_invoker.py +79 -0
- aip_agents/examples/hello_world_a2a_langgraph_server_tool_streaming.py +132 -0
- aip_agents/examples/hello_world_a2a_mcp_langgraph.py +196 -0
- aip_agents/examples/hello_world_a2a_three_level_agent_hierarchy_client.py +244 -0
- aip_agents/examples/hello_world_a2a_three_level_agent_hierarchy_server.py +251 -0
- aip_agents/examples/hello_world_a2a_with_metadata_langchain_client.py +57 -0
- aip_agents/examples/hello_world_a2a_with_metadata_langchain_server_lm_invoker.py +80 -0
- aip_agents/examples/hello_world_google_adk.py +41 -0
- aip_agents/examples/hello_world_google_adk_mcp_http.py +34 -0
- aip_agents/examples/hello_world_google_adk_mcp_http_stream.py +40 -0
- aip_agents/examples/hello_world_google_adk_mcp_sse.py +44 -0
- aip_agents/examples/hello_world_google_adk_mcp_sse_stream.py +48 -0
- aip_agents/examples/hello_world_google_adk_mcp_stdio.py +44 -0
- aip_agents/examples/hello_world_google_adk_mcp_stdio_stream.py +48 -0
- aip_agents/examples/hello_world_google_adk_stream.py +44 -0
- aip_agents/examples/hello_world_langchain.py +28 -0
- aip_agents/examples/hello_world_langchain_lm_invoker.py +15 -0
- aip_agents/examples/hello_world_langchain_mcp_http.py +34 -0
- aip_agents/examples/hello_world_langchain_mcp_http_interactive.py +130 -0
- aip_agents/examples/hello_world_langchain_mcp_http_stream.py +42 -0
- aip_agents/examples/hello_world_langchain_mcp_multi_server.py +155 -0
- aip_agents/examples/hello_world_langchain_mcp_sse.py +34 -0
- aip_agents/examples/hello_world_langchain_mcp_sse_stream.py +40 -0
- aip_agents/examples/hello_world_langchain_mcp_stdio.py +30 -0
- aip_agents/examples/hello_world_langchain_mcp_stdio_stream.py +41 -0
- aip_agents/examples/hello_world_langchain_stream.py +36 -0
- aip_agents/examples/hello_world_langchain_stream_lm_invoker.py +39 -0
- aip_agents/examples/hello_world_langflow_agent.py +163 -0
- aip_agents/examples/hello_world_langgraph.py +39 -0
- aip_agents/examples/hello_world_langgraph_bosa_twitter.py +41 -0
- aip_agents/examples/hello_world_langgraph_mcp_http.py +31 -0
- aip_agents/examples/hello_world_langgraph_mcp_http_stream.py +34 -0
- aip_agents/examples/hello_world_langgraph_mcp_sse.py +35 -0
- aip_agents/examples/hello_world_langgraph_mcp_sse_stream.py +50 -0
- aip_agents/examples/hello_world_langgraph_mcp_stdio.py +35 -0
- aip_agents/examples/hello_world_langgraph_mcp_stdio_stream.py +50 -0
- aip_agents/examples/hello_world_langgraph_stream.py +43 -0
- aip_agents/examples/hello_world_langgraph_stream_lm_invoker.py +37 -0
- aip_agents/examples/hello_world_model_switch_cli.py +210 -0
- aip_agents/examples/hello_world_multi_agent_adk.py +75 -0
- aip_agents/examples/hello_world_multi_agent_langchain.py +54 -0
- aip_agents/examples/hello_world_multi_agent_langgraph.py +66 -0
- aip_agents/examples/hello_world_multi_agent_langgraph_lm_invoker.py +69 -0
- aip_agents/examples/hello_world_pii_logger.py +21 -0
- aip_agents/examples/hello_world_sentry.py +133 -0
- aip_agents/examples/hello_world_step_limits.py +273 -0
- aip_agents/examples/hello_world_stock_a2a_server.py +103 -0
- aip_agents/examples/hello_world_tool_output_client.py +46 -0
- aip_agents/examples/hello_world_tool_output_server.py +114 -0
- aip_agents/examples/hitl_demo.py +724 -0
- aip_agents/examples/mcp_configs/configs.py +63 -0
- aip_agents/examples/mcp_servers/common.py +76 -0
- aip_agents/examples/mcp_servers/mcp_name.py +29 -0
- aip_agents/examples/mcp_servers/mcp_server_http.py +19 -0
- aip_agents/examples/mcp_servers/mcp_server_sse.py +19 -0
- aip_agents/examples/mcp_servers/mcp_server_stdio.py +19 -0
- aip_agents/examples/mcp_servers/mcp_time.py +10 -0
- aip_agents/examples/pii_demo_langgraph_client.py +69 -0
- aip_agents/examples/pii_demo_langgraph_server.py +126 -0
- aip_agents/examples/pii_demo_multi_agent_client.py +80 -0
- aip_agents/examples/pii_demo_multi_agent_server.py +247 -0
- aip_agents/examples/todolist_planning_a2a_langchain_client.py +70 -0
- aip_agents/examples/todolist_planning_a2a_langgraph_server.py +88 -0
- aip_agents/examples/tools/__init__.py +27 -0
- aip_agents/examples/tools/adk_arithmetic_tools.py +36 -0
- aip_agents/examples/tools/adk_weather_tool.py +60 -0
- aip_agents/examples/tools/data_generator_tool.py +103 -0
- aip_agents/examples/tools/data_visualization_tool.py +312 -0
- aip_agents/examples/tools/image_artifact_tool.py +136 -0
- aip_agents/examples/tools/langchain_arithmetic_tools.py +26 -0
- aip_agents/examples/tools/langchain_currency_exchange_tool.py +88 -0
- aip_agents/examples/tools/langchain_graph_artifact_tool.py +172 -0
- aip_agents/examples/tools/langchain_weather_tool.py +48 -0
- aip_agents/examples/tools/langgraph_streaming_tool.py +130 -0
- aip_agents/examples/tools/mock_retrieval_tool.py +56 -0
- aip_agents/examples/tools/pii_demo_tools.py +189 -0
- aip_agents/examples/tools/random_chart_tool.py +142 -0
- aip_agents/examples/tools/serper_tool.py +202 -0
- aip_agents/examples/tools/stock_tools.py +82 -0
- aip_agents/examples/tools/table_generator_tool.py +167 -0
- aip_agents/examples/tools/time_tool.py +82 -0
- aip_agents/examples/tools/weather_forecast_tool.py +38 -0
- aip_agents/executor/agent_executor.py +473 -0
- aip_agents/executor/base.py +48 -0
- aip_agents/mcp/__init__.py +1 -0
- aip_agents/mcp/client/__init__.py +14 -0
- aip_agents/mcp/client/base_mcp_client.py +369 -0
- aip_agents/mcp/client/connection_manager.py +193 -0
- aip_agents/mcp/client/google_adk/__init__.py +11 -0
- aip_agents/mcp/client/google_adk/client.py +381 -0
- aip_agents/mcp/client/langchain/__init__.py +11 -0
- aip_agents/mcp/client/langchain/client.py +265 -0
- aip_agents/mcp/client/persistent_session.py +359 -0
- aip_agents/mcp/client/session_pool.py +351 -0
- aip_agents/mcp/client/transports.py +215 -0
- aip_agents/mcp/utils/__init__.py +7 -0
- aip_agents/mcp/utils/config_validator.py +139 -0
- aip_agents/memory/__init__.py +14 -0
- aip_agents/memory/adapters/__init__.py +10 -0
- aip_agents/memory/adapters/base_adapter.py +717 -0
- aip_agents/memory/adapters/mem0.py +84 -0
- aip_agents/memory/base.py +84 -0
- aip_agents/memory/constants.py +49 -0
- aip_agents/memory/factory.py +86 -0
- aip_agents/memory/guidance.py +20 -0
- aip_agents/memory/simple_memory.py +47 -0
- aip_agents/middleware/__init__.py +17 -0
- aip_agents/middleware/base.py +88 -0
- aip_agents/middleware/manager.py +128 -0
- aip_agents/middleware/todolist.py +274 -0
- aip_agents/schema/__init__.py +69 -0
- aip_agents/schema/a2a.py +56 -0
- aip_agents/schema/agent.py +111 -0
- aip_agents/schema/hitl.py +157 -0
- aip_agents/schema/langgraph.py +37 -0
- aip_agents/schema/model_id.py +97 -0
- aip_agents/schema/step_limit.py +108 -0
- aip_agents/schema/storage.py +40 -0
- aip_agents/sentry/__init__.py +11 -0
- aip_agents/sentry/sentry.py +151 -0
- aip_agents/storage/__init__.py +41 -0
- aip_agents/storage/base.py +85 -0
- aip_agents/storage/clients/__init__.py +12 -0
- aip_agents/storage/clients/minio_client.py +318 -0
- aip_agents/storage/config.py +62 -0
- aip_agents/storage/providers/__init__.py +15 -0
- aip_agents/storage/providers/base.py +106 -0
- aip_agents/storage/providers/memory.py +114 -0
- aip_agents/storage/providers/object_storage.py +214 -0
- aip_agents/tools/__init__.py +33 -0
- aip_agents/tools/bosa_tools.py +105 -0
- aip_agents/tools/browser_use/__init__.py +82 -0
- aip_agents/tools/browser_use/action_parser.py +103 -0
- aip_agents/tools/browser_use/browser_use_tool.py +1112 -0
- aip_agents/tools/browser_use/llm_config.py +120 -0
- aip_agents/tools/browser_use/minio_storage.py +198 -0
- aip_agents/tools/browser_use/schemas.py +119 -0
- aip_agents/tools/browser_use/session.py +76 -0
- aip_agents/tools/browser_use/session_errors.py +132 -0
- aip_agents/tools/browser_use/steel_session_recording.py +317 -0
- aip_agents/tools/browser_use/streaming.py +813 -0
- aip_agents/tools/browser_use/structured_data_parser.py +257 -0
- aip_agents/tools/browser_use/structured_data_recovery.py +204 -0
- aip_agents/tools/browser_use/types.py +78 -0
- aip_agents/tools/code_sandbox/__init__.py +26 -0
- aip_agents/tools/code_sandbox/constant.py +13 -0
- aip_agents/tools/code_sandbox/e2b_cloud_sandbox_extended.py +257 -0
- aip_agents/tools/code_sandbox/e2b_sandbox_tool.py +411 -0
- aip_agents/tools/constants.py +165 -0
- aip_agents/tools/document_loader/__init__.py +44 -0
- aip_agents/tools/document_loader/base_reader.py +302 -0
- aip_agents/tools/document_loader/docx_reader_tool.py +68 -0
- aip_agents/tools/document_loader/excel_reader_tool.py +171 -0
- aip_agents/tools/document_loader/pdf_reader_tool.py +79 -0
- aip_agents/tools/document_loader/pdf_splitter.py +169 -0
- aip_agents/tools/gl_connector/__init__.py +5 -0
- aip_agents/tools/gl_connector/tool.py +351 -0
- aip_agents/tools/memory_search/__init__.py +22 -0
- aip_agents/tools/memory_search/base.py +200 -0
- aip_agents/tools/memory_search/mem0.py +258 -0
- aip_agents/tools/memory_search/schema.py +48 -0
- aip_agents/tools/memory_search_tool.py +26 -0
- aip_agents/tools/time_tool.py +117 -0
- aip_agents/tools/tool_config_injector.py +300 -0
- aip_agents/tools/web_search/__init__.py +15 -0
- aip_agents/tools/web_search/serper_tool.py +187 -0
- aip_agents/types/__init__.py +70 -0
- aip_agents/types/a2a_events.py +13 -0
- aip_agents/utils/__init__.py +79 -0
- aip_agents/utils/a2a_connector.py +1757 -0
- aip_agents/utils/artifact_helpers.py +502 -0
- aip_agents/utils/constants.py +22 -0
- aip_agents/utils/datetime/__init__.py +34 -0
- aip_agents/utils/datetime/normalization.py +231 -0
- aip_agents/utils/datetime/timezone.py +206 -0
- aip_agents/utils/env_loader.py +27 -0
- aip_agents/utils/event_handler_registry.py +58 -0
- aip_agents/utils/file_prompt_utils.py +176 -0
- aip_agents/utils/final_response_builder.py +211 -0
- aip_agents/utils/formatter_llm_client.py +231 -0
- aip_agents/utils/langgraph/__init__.py +19 -0
- aip_agents/utils/langgraph/converter.py +128 -0
- aip_agents/utils/langgraph/tool_managers/__init__.py +15 -0
- aip_agents/utils/langgraph/tool_managers/a2a_tool_manager.py +99 -0
- aip_agents/utils/langgraph/tool_managers/base_tool_manager.py +66 -0
- aip_agents/utils/langgraph/tool_managers/delegation_tool_manager.py +1071 -0
- aip_agents/utils/langgraph/tool_output_management.py +967 -0
- aip_agents/utils/logger.py +195 -0
- aip_agents/utils/metadata/__init__.py +27 -0
- aip_agents/utils/metadata/activity_metadata_helper.py +407 -0
- aip_agents/utils/metadata/activity_narrative/__init__.py +35 -0
- aip_agents/utils/metadata/activity_narrative/builder.py +817 -0
- aip_agents/utils/metadata/activity_narrative/constants.py +51 -0
- aip_agents/utils/metadata/activity_narrative/context.py +49 -0
- aip_agents/utils/metadata/activity_narrative/formatters.py +230 -0
- aip_agents/utils/metadata/activity_narrative/utils.py +35 -0
- aip_agents/utils/metadata/schemas/__init__.py +16 -0
- aip_agents/utils/metadata/schemas/activity_schema.py +29 -0
- aip_agents/utils/metadata/schemas/thinking_schema.py +31 -0
- aip_agents/utils/metadata/thinking_metadata_helper.py +38 -0
- aip_agents/utils/metadata_helper.py +358 -0
- aip_agents/utils/name_preprocessor/__init__.py +17 -0
- aip_agents/utils/name_preprocessor/base_name_preprocessor.py +73 -0
- aip_agents/utils/name_preprocessor/google_name_preprocessor.py +100 -0
- aip_agents/utils/name_preprocessor/name_preprocessor.py +87 -0
- aip_agents/utils/name_preprocessor/openai_name_preprocessor.py +48 -0
- aip_agents/utils/pii/__init__.py +25 -0
- aip_agents/utils/pii/pii_handler.py +397 -0
- aip_agents/utils/pii/pii_helper.py +207 -0
- aip_agents/utils/pii/uuid_deanonymizer_mapping.py +195 -0
- aip_agents/utils/reference_helper.py +273 -0
- aip_agents/utils/sse_chunk_transformer.py +831 -0
- aip_agents/utils/step_limit_manager.py +265 -0
- aip_agents/utils/token_usage_helper.py +156 -0
- aip_agents_binary-0.5.20.dist-info/METADATA +681 -0
- aip_agents_binary-0.5.20.dist-info/RECORD +280 -0
- aip_agents_binary-0.5.20.dist-info/WHEEL +5 -0
- aip_agents_binary-0.5.20.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
"""HITL ApprovalManager for coordinating approval workflow.
|
|
2
|
+
|
|
3
|
+
This module provides the core approval management logic that coordinates
|
|
4
|
+
between configuration, requests, and CLI prompting.
|
|
5
|
+
|
|
6
|
+
Author:
|
|
7
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import json
|
|
12
|
+
from collections.abc import Callable, Iterable
|
|
13
|
+
from datetime import datetime, timedelta
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from aip_agents.agent.hitl.config import ToolApprovalConfig
|
|
17
|
+
from aip_agents.agent.hitl.prompt import BasePromptHandler, DeferredPromptHandler
|
|
18
|
+
from aip_agents.agent.hitl.registry import hitl_registry
|
|
19
|
+
from aip_agents.schema.hitl import ApprovalDecision, ApprovalDecisionType, ApprovalLogEntry, ApprovalRequest
|
|
20
|
+
|
|
21
|
+
# Constants
|
|
22
|
+
MAX_ARGUMENTS_PREVIEW_LENGTH = 2048
|
|
23
|
+
|
|
24
|
+
# Decision message templates for different HITL outcomes
|
|
25
|
+
DECISION_MESSAGE_MAP = {
|
|
26
|
+
ApprovalDecisionType.SKIPPED: (
|
|
27
|
+
"Tool execution was skipped by the human operator. Consider alternative approaches or gather additional "
|
|
28
|
+
"context before retrying."
|
|
29
|
+
),
|
|
30
|
+
ApprovalDecisionType.TIMEOUT_SKIP: (
|
|
31
|
+
"Tool execution timed out waiting for human approval. Proceed without running the tool and surface a "
|
|
32
|
+
"follow-up plan."
|
|
33
|
+
),
|
|
34
|
+
ApprovalDecisionType.REJECTED: (
|
|
35
|
+
"Tool execution was explicitly rejected by the human operator. Abort this action and provide a safe "
|
|
36
|
+
"alternative or explanation."
|
|
37
|
+
),
|
|
38
|
+
ApprovalDecisionType.PENDING: (
|
|
39
|
+
"Awaiting human approval for request '{request_id}'. Invoke "
|
|
40
|
+
"ApprovalManager.resolve_pending_request using this identifier to continue execution."
|
|
41
|
+
),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# Decisions that block tool execution (all except APPROVED)
|
|
45
|
+
TOOL_EXECUTION_BLOCKING_DECISIONS = {
|
|
46
|
+
ApprovalDecisionType.REJECTED,
|
|
47
|
+
ApprovalDecisionType.SKIPPED,
|
|
48
|
+
ApprovalDecisionType.TIMEOUT_SKIP,
|
|
49
|
+
ApprovalDecisionType.PENDING,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ApprovalManager:
|
|
54
|
+
"""Manages the HITL approval workflow for tools.
|
|
55
|
+
|
|
56
|
+
This class coordinates the approval process from configuration through
|
|
57
|
+
to final decision, handling timeouts and cleanup.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(self, prompt_handler: BasePromptHandler | None = None):
|
|
61
|
+
"""Initialize the approval manager.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
prompt_handler: Optional prompt handler for user interaction.
|
|
65
|
+
Defaults to DeferredPromptHandler which exposes pending requests
|
|
66
|
+
for out-of-band resolution.
|
|
67
|
+
"""
|
|
68
|
+
self._active_requests: dict[str, ApprovalRequest] = {}
|
|
69
|
+
self._waiters: dict[str, Any] = {}
|
|
70
|
+
self._prompt_handler: BasePromptHandler = prompt_handler or DeferredPromptHandler()
|
|
71
|
+
# Allow handlers that need manager access to attach it
|
|
72
|
+
try:
|
|
73
|
+
self._prompt_handler.attach_manager(self) # type: ignore[attr-defined]
|
|
74
|
+
except Exception:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
def create_approval_request(
|
|
78
|
+
self,
|
|
79
|
+
tool_name: str,
|
|
80
|
+
arguments: dict[str, Any],
|
|
81
|
+
config: ToolApprovalConfig,
|
|
82
|
+
context: dict[str, str] | None = None,
|
|
83
|
+
) -> ApprovalRequest:
|
|
84
|
+
"""Create a new approval request for a tool call.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
tool_name: Name of the tool being called.
|
|
88
|
+
arguments: Tool arguments (will be JSON serialized).
|
|
89
|
+
config: Approval configuration for this tool.
|
|
90
|
+
context: Optional context metadata. Defaults to None.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
The created approval request.
|
|
94
|
+
"""
|
|
95
|
+
# Create truncated JSON preview (limit to 2KB)
|
|
96
|
+
try:
|
|
97
|
+
arguments_json = json.dumps(arguments, indent=2)
|
|
98
|
+
if len(arguments_json) > MAX_ARGUMENTS_PREVIEW_LENGTH:
|
|
99
|
+
arguments_json = arguments_json[: MAX_ARGUMENTS_PREVIEW_LENGTH - 3] + "..."
|
|
100
|
+
except (TypeError, ValueError):
|
|
101
|
+
# Handle non-serializable arguments with a safe fallback
|
|
102
|
+
# Try to provide more specific information about what's not serializable
|
|
103
|
+
non_serializable_info = self._get_non_serializable_info(arguments)
|
|
104
|
+
arguments_json = f"<Non-serializable arguments: {non_serializable_info}>"
|
|
105
|
+
|
|
106
|
+
request = ApprovalRequest.create(tool_name=tool_name, arguments_preview=arguments_json, context=context)
|
|
107
|
+
|
|
108
|
+
# Set timeout and register the request
|
|
109
|
+
self._set_timeout_based_on_config(request, config)
|
|
110
|
+
|
|
111
|
+
return request
|
|
112
|
+
|
|
113
|
+
def _get_non_serializable_info(self, arguments: Any) -> str:
|
|
114
|
+
"""Get information about non-serializable content in arguments.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
arguments: The arguments that failed JSON serialization.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
String describing the non-serializable content.
|
|
121
|
+
"""
|
|
122
|
+
# Handle dictionary arguments
|
|
123
|
+
if isinstance(arguments, dict):
|
|
124
|
+
return self._get_dict_serializable_info(arguments)
|
|
125
|
+
|
|
126
|
+
# Handle list/tuple arguments
|
|
127
|
+
if isinstance(arguments, list | tuple):
|
|
128
|
+
return self._get_sequence_serializable_info(arguments)
|
|
129
|
+
|
|
130
|
+
# For other types, just return the type name
|
|
131
|
+
return type(arguments).__name__
|
|
132
|
+
|
|
133
|
+
def _get_dict_serializable_info(self, arguments: dict[str, Any]) -> str:
|
|
134
|
+
"""Get serializable info for dictionary arguments.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
arguments (dict[str, Any]): The dictionary arguments to analyze.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
str: Description of the arguments' serializability.
|
|
141
|
+
"""
|
|
142
|
+
non_serializable_items = []
|
|
143
|
+
|
|
144
|
+
for key, value in arguments.items():
|
|
145
|
+
if not self._is_json_serializable(value):
|
|
146
|
+
non_serializable_items.append(f"{key}={type(value).__name__}")
|
|
147
|
+
|
|
148
|
+
if non_serializable_items:
|
|
149
|
+
return f"dict with non-serializable keys: {', '.join(non_serializable_items)}"
|
|
150
|
+
|
|
151
|
+
return "dict"
|
|
152
|
+
|
|
153
|
+
def _get_sequence_serializable_info(self, arguments: list | tuple) -> str:
|
|
154
|
+
"""Get serializable info for list/tuple arguments.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
arguments (list | tuple): The sequence arguments to analyze.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
str: Description of the arguments' serializability.
|
|
161
|
+
"""
|
|
162
|
+
non_serializable_items = []
|
|
163
|
+
|
|
164
|
+
for i, item in enumerate(arguments):
|
|
165
|
+
if not self._is_json_serializable(item):
|
|
166
|
+
non_serializable_items.append(f"[{i}]={type(item).__name__}")
|
|
167
|
+
|
|
168
|
+
if non_serializable_items:
|
|
169
|
+
return f"{type(arguments).__name__} with non-serializable items: {', '.join(non_serializable_items)}"
|
|
170
|
+
|
|
171
|
+
return type(arguments).__name__
|
|
172
|
+
|
|
173
|
+
def _is_json_serializable(self, value: Any) -> bool:
|
|
174
|
+
"""Check if a value is JSON serializable.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
value (Any): The value to check for JSON serializability.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
bool: True if the value is JSON serializable, False otherwise.
|
|
181
|
+
"""
|
|
182
|
+
try:
|
|
183
|
+
json.dumps(value)
|
|
184
|
+
return True
|
|
185
|
+
except (TypeError, ValueError):
|
|
186
|
+
return False
|
|
187
|
+
|
|
188
|
+
# Set timeout based on config
|
|
189
|
+
def _set_timeout_based_on_config(self, request: ApprovalRequest, config: ToolApprovalConfig) -> None:
|
|
190
|
+
"""Set timeout on the approval request based on configuration.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
request: The approval request to set timeout on.
|
|
194
|
+
config: The approval configuration.
|
|
195
|
+
"""
|
|
196
|
+
if request.created_at:
|
|
197
|
+
request.timeout_at = request.created_at + timedelta(seconds=config.timeout_seconds)
|
|
198
|
+
|
|
199
|
+
self._active_requests[request.request_id] = request
|
|
200
|
+
|
|
201
|
+
# Register ownership in global registry for hierarchical routing
|
|
202
|
+
hitl_registry.register(request.request_id, self)
|
|
203
|
+
|
|
204
|
+
def get_pending_request(self, request_id: str) -> ApprovalRequest | None:
|
|
205
|
+
"""Get a pending approval request by ID.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
request_id: The unique identifier of the approval request.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
The pending approval request if found, None otherwise.
|
|
212
|
+
"""
|
|
213
|
+
return self._active_requests.get(request_id)
|
|
214
|
+
|
|
215
|
+
def approve_request(self, request: ApprovalRequest, operator_input: str = "approved") -> ApprovalDecision:
|
|
216
|
+
"""Record an approval decision for a request.
|
|
217
|
+
|
|
218
|
+
This method creates an approval decision, calculates latency metrics,
|
|
219
|
+
and removes the request from the active requests queue.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
request: The approval request to approve.
|
|
223
|
+
operator_input: Raw operator input that led to this decision. Defaults to "approved".
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
The recorded approval decision.
|
|
227
|
+
"""
|
|
228
|
+
return self._finalize_request(request, ApprovalDecisionType.APPROVED, operator_input)
|
|
229
|
+
|
|
230
|
+
def reject_request(self, request: ApprovalRequest, operator_input: str = "rejected") -> ApprovalDecision:
|
|
231
|
+
"""Record a rejection decision for a request.
|
|
232
|
+
|
|
233
|
+
This method creates a rejection decision, calculates latency metrics,
|
|
234
|
+
and removes the request from the active requests queue.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
request: The approval request to reject.
|
|
238
|
+
operator_input: Raw operator input that led to this decision. Defaults to "rejected".
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
The recorded rejection decision.
|
|
242
|
+
"""
|
|
243
|
+
return self._finalize_request(request, ApprovalDecisionType.REJECTED, operator_input)
|
|
244
|
+
|
|
245
|
+
def skip_request(self, request: ApprovalRequest, operator_input: str = "skipped") -> ApprovalDecision:
|
|
246
|
+
"""Record a skip decision for a request.
|
|
247
|
+
|
|
248
|
+
This method creates a skip decision, calculates latency metrics,
|
|
249
|
+
and removes the request from the active requests queue.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
request: The approval request to skip.
|
|
253
|
+
operator_input: Raw operator input that led to this decision. Defaults to "skipped".
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
The recorded skip decision.
|
|
257
|
+
"""
|
|
258
|
+
return self._finalize_request(request, ApprovalDecisionType.SKIPPED, operator_input)
|
|
259
|
+
|
|
260
|
+
def timeout_request(self, request: ApprovalRequest, operator_input: str = "TIMEOUT") -> ApprovalDecision:
|
|
261
|
+
"""Record a timeout decision for a request, always skipping the tool.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
request (ApprovalRequest): The approval request that timed out.
|
|
265
|
+
operator_input (str, optional): Input from the operator (defaults to "TIMEOUT").
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
ApprovalDecision: The timeout decision.
|
|
269
|
+
"""
|
|
270
|
+
decision_type = ApprovalDecisionType.TIMEOUT_SKIP
|
|
271
|
+
return self._finalize_request(request, decision_type, operator_input)
|
|
272
|
+
|
|
273
|
+
def check_timeout(self, request: ApprovalRequest) -> ApprovalDecision | None:
|
|
274
|
+
"""Check if a request has timed out and return timeout decision if so.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
request: The approval request to check.
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
Timeout decision if timed out, None otherwise.
|
|
281
|
+
"""
|
|
282
|
+
now = datetime.now()
|
|
283
|
+
if request.timeout_at and now >= request.timeout_at:
|
|
284
|
+
return self.timeout_request(request)
|
|
285
|
+
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
def _finalize_request(
|
|
289
|
+
self,
|
|
290
|
+
request: ApprovalRequest,
|
|
291
|
+
decision_type: ApprovalDecisionType,
|
|
292
|
+
operator_input: str,
|
|
293
|
+
) -> ApprovalDecision:
|
|
294
|
+
"""Create a decision, record latency, and remove the request.
|
|
295
|
+
|
|
296
|
+
This is a helper method used by all decision recording methods to
|
|
297
|
+
create the ApprovalDecision object, calculate latency metrics, and
|
|
298
|
+
clean up the request from the active queue.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
request: The approval request being finalized.
|
|
302
|
+
decision_type: The type of decision (approved, rejected, etc.).
|
|
303
|
+
operator_input: The raw input that led to this decision.
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
The completed approval decision with latency metrics.
|
|
307
|
+
"""
|
|
308
|
+
decision = ApprovalDecision(
|
|
309
|
+
request_id=request.request_id,
|
|
310
|
+
decision=decision_type,
|
|
311
|
+
operator_input=operator_input,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
if request.created_at and decision.decided_at:
|
|
315
|
+
latency = decision.decided_at - request.created_at
|
|
316
|
+
decision.latency_ms = int(latency.total_seconds() * 1000)
|
|
317
|
+
|
|
318
|
+
self._active_requests.pop(request.request_id, None)
|
|
319
|
+
|
|
320
|
+
# Unregister from global registry when finalized
|
|
321
|
+
hitl_registry.unregister(request.request_id)
|
|
322
|
+
|
|
323
|
+
return decision
|
|
324
|
+
|
|
325
|
+
async def prompt_for_decision(
|
|
326
|
+
self,
|
|
327
|
+
request: ApprovalRequest,
|
|
328
|
+
timeout_seconds: int,
|
|
329
|
+
context_keys: list[str] | None = None,
|
|
330
|
+
) -> ApprovalDecision:
|
|
331
|
+
"""Prompt for a decision using the configured handler.
|
|
332
|
+
|
|
333
|
+
This method delegates to the configured prompt handler to obtain
|
|
334
|
+
an approval decision from the operator. The handler may be interactive
|
|
335
|
+
(CLI) or deferred (programmatic, resume-in-place).
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
request: The approval request to prompt for.
|
|
339
|
+
timeout_seconds: How long to wait for input.
|
|
340
|
+
context_keys: Optional keys to display from context. Defaults to None.
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
The operator's decision on the request.
|
|
344
|
+
"""
|
|
345
|
+
return await self._prompt_handler.prompt_for_decision(
|
|
346
|
+
request=request,
|
|
347
|
+
timeout_seconds=timeout_seconds,
|
|
348
|
+
context_keys=context_keys,
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
def list_pending_requests(self) -> Iterable[ApprovalRequest]:
|
|
352
|
+
"""Return a snapshot of currently pending requests.
|
|
353
|
+
|
|
354
|
+
This method provides a thread-safe snapshot of all currently active
|
|
355
|
+
approval requests that are waiting for operator decisions.
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
An iterable of all pending approval requests.
|
|
359
|
+
"""
|
|
360
|
+
return list(self._active_requests.values())
|
|
361
|
+
|
|
362
|
+
def _resolve_decision_handler(
|
|
363
|
+
self, decision: str
|
|
364
|
+
) -> tuple[ApprovalDecisionType, Callable[[ApprovalRequest, str], ApprovalDecision]]:
|
|
365
|
+
"""Return the canonical decision enum and finalize callable.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
decision (str): The decision string to resolve.
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
tuple[ApprovalDecisionType, Callable[[ApprovalRequest, str], ApprovalDecision]]: The decision type and handler function.
|
|
372
|
+
"""
|
|
373
|
+
normalized = decision.strip().lower()
|
|
374
|
+
try:
|
|
375
|
+
decision_type = ApprovalDecisionType(normalized)
|
|
376
|
+
except ValueError as exc:
|
|
377
|
+
raise ValueError(f"Unsupported decision '{decision}' for pending request resolution") from exc
|
|
378
|
+
|
|
379
|
+
decision_map: dict[ApprovalDecisionType, Callable[[ApprovalRequest, str], ApprovalDecision]] = {
|
|
380
|
+
ApprovalDecisionType.APPROVED: self.approve_request,
|
|
381
|
+
ApprovalDecisionType.REJECTED: self.reject_request,
|
|
382
|
+
ApprovalDecisionType.SKIPPED: self.skip_request,
|
|
383
|
+
}
|
|
384
|
+
handler = decision_map.get(decision_type)
|
|
385
|
+
if handler is None:
|
|
386
|
+
raise ValueError(f"Unsupported decision '{decision}' for pending request resolution")
|
|
387
|
+
return decision_type, handler
|
|
388
|
+
|
|
389
|
+
def resolve_pending_request(self, request_id: str, decision: str, operator_input: str = "") -> ApprovalDecision:
|
|
390
|
+
"""Resolve a pending request with an explicit decision.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
request_id: The unique identifier of the pending approval request.
|
|
394
|
+
decision: The decision string (e.g., "approved", "rejected", "skipped").
|
|
395
|
+
operator_input: Optional raw operator input, defaults to the decision string. Defaults to "".
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
The recorded approval decision.
|
|
399
|
+
|
|
400
|
+
Raises:
|
|
401
|
+
KeyError: If no pending request exists with the given request_id.
|
|
402
|
+
ValueError: If the decision string is not supported.
|
|
403
|
+
"""
|
|
404
|
+
request = self.get_pending_request(request_id)
|
|
405
|
+
if not request:
|
|
406
|
+
raise KeyError(f"No pending approval request with id '{request_id}'")
|
|
407
|
+
|
|
408
|
+
applied_input = operator_input or decision
|
|
409
|
+
decision_type, finalize = self._resolve_decision_handler(decision)
|
|
410
|
+
|
|
411
|
+
waiter = self._waiters.pop(request_id, None)
|
|
412
|
+
if waiter is not None:
|
|
413
|
+
waiter.set_result((decision_type.value, applied_input))
|
|
414
|
+
return ApprovalDecision(
|
|
415
|
+
request_id=request_id,
|
|
416
|
+
decision=ApprovalDecisionType.PENDING,
|
|
417
|
+
operator_input=applied_input,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
return finalize(request, applied_input)
|
|
421
|
+
|
|
422
|
+
def create_log_entry(
|
|
423
|
+
self,
|
|
424
|
+
decision: ApprovalDecision,
|
|
425
|
+
tool_name: str,
|
|
426
|
+
agent_id: str | None = None,
|
|
427
|
+
thread_id: str | None = None,
|
|
428
|
+
additional_context: dict[str, Any] | None = None,
|
|
429
|
+
) -> ApprovalLogEntry:
|
|
430
|
+
"""Create a log entry for a decision.
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
decision: The approval decision.
|
|
434
|
+
tool_name: Name of the tool.
|
|
435
|
+
agent_id: Optional agent identifier. Defaults to None.
|
|
436
|
+
thread_id: Optional thread/session identifier. Defaults to None.
|
|
437
|
+
additional_context: Optional additional logging context. Defaults to None.
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
Structured log entry.
|
|
441
|
+
"""
|
|
442
|
+
return ApprovalLogEntry(
|
|
443
|
+
request_id=decision.request_id,
|
|
444
|
+
tool_name=tool_name,
|
|
445
|
+
decision=decision.decision,
|
|
446
|
+
agent_id=agent_id,
|
|
447
|
+
thread_id=thread_id,
|
|
448
|
+
additional_context=additional_context,
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
def cleanup_expired_requests(self) -> int:
|
|
452
|
+
"""Clean up expired requests and return count of cleaned requests.
|
|
453
|
+
|
|
454
|
+
This method removes all approval requests that have exceeded their
|
|
455
|
+
timeout period from the active requests queue. This is typically
|
|
456
|
+
called periodically to prevent memory leaks from abandoned requests.
|
|
457
|
+
|
|
458
|
+
Returns:
|
|
459
|
+
The number of expired requests that were cleaned up.
|
|
460
|
+
"""
|
|
461
|
+
now = datetime.now()
|
|
462
|
+
expired_ids = []
|
|
463
|
+
|
|
464
|
+
for request_id, request in self._active_requests.items():
|
|
465
|
+
if request.timeout_at and now >= request.timeout_at:
|
|
466
|
+
expired_ids.append(request_id)
|
|
467
|
+
|
|
468
|
+
for request_id in expired_ids:
|
|
469
|
+
self._active_requests.pop(request_id, None)
|
|
470
|
+
# Unregister expired requests from global registry
|
|
471
|
+
hitl_registry.unregister(request_id)
|
|
472
|
+
|
|
473
|
+
return len(expired_ids)
|
|
474
|
+
|
|
475
|
+
# Deferred waiter helpers ----------------------------codex---------------------
|
|
476
|
+
def register_waiter(self, request_id: str, future: Any) -> None:
|
|
477
|
+
"""Register a waiter future for an approval request.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
request_id: The id for which to wait.
|
|
481
|
+
future: An asyncio.Future that will receive a tuple(decision, operator_input).
|
|
482
|
+
"""
|
|
483
|
+
self._waiters[request_id] = future
|
|
484
|
+
|
|
485
|
+
def unregister_waiter(self, request_id: str) -> None:
|
|
486
|
+
"""Unregister a waiter if present (used on timeout/cancellation).
|
|
487
|
+
|
|
488
|
+
Args:
|
|
489
|
+
request_id (str): The ID of the request to unregister the waiter for.
|
|
490
|
+
"""
|
|
491
|
+
self._waiters.pop(request_id, None)
|
|
492
|
+
|
|
493
|
+
async def wait_for_pending_decision(self, request: ApprovalRequest, timeout_seconds: int) -> ApprovalDecision:
|
|
494
|
+
"""Wait for a pending request to be resolved and return the final decision.
|
|
495
|
+
|
|
496
|
+
Args:
|
|
497
|
+
request: The pending approval request to wait for.
|
|
498
|
+
timeout_seconds: Maximum time to wait before treating as timeout.
|
|
499
|
+
|
|
500
|
+
Returns:
|
|
501
|
+
The finalized approval decision.
|
|
502
|
+
"""
|
|
503
|
+
future = self._waiters.get(request.request_id)
|
|
504
|
+
if future is None:
|
|
505
|
+
loop = asyncio.get_running_loop()
|
|
506
|
+
future = loop.create_future()
|
|
507
|
+
self.register_waiter(request.request_id, future)
|
|
508
|
+
|
|
509
|
+
try:
|
|
510
|
+
normalized_decision, operator_input = await asyncio.wait_for(future, timeout_seconds)
|
|
511
|
+
except TimeoutError:
|
|
512
|
+
self.unregister_waiter(request.request_id)
|
|
513
|
+
return self.timeout_request(request)
|
|
514
|
+
|
|
515
|
+
_decision_type, handler = self._resolve_decision_handler(normalized_decision)
|
|
516
|
+
return handler(request, operator_input)
|
|
517
|
+
|
|
518
|
+
@staticmethod
|
|
519
|
+
def get_decision_message(decision: ApprovalDecision) -> str:
|
|
520
|
+
"""Get the appropriate message for a HITL decision.
|
|
521
|
+
|
|
522
|
+
Args:
|
|
523
|
+
decision: The approval decision to get a message for.
|
|
524
|
+
|
|
525
|
+
Returns:
|
|
526
|
+
A human-readable message explaining the decision outcome.
|
|
527
|
+
"""
|
|
528
|
+
template = DECISION_MESSAGE_MAP.get(decision.decision)
|
|
529
|
+
if template:
|
|
530
|
+
return template.format(request_id=decision.request_id)
|
|
531
|
+
else:
|
|
532
|
+
return f"Tool execution {decision.decision} by human approval."
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Backward-compatible exports for HITL approval schema.
|
|
2
|
+
|
|
3
|
+
Prefer importing from ``aip_agents.schema.hitl`` in new code.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from aip_agents.schema.hitl import (
|
|
7
|
+
ApprovalDecision,
|
|
8
|
+
ApprovalDecisionType,
|
|
9
|
+
ApprovalLogEntry,
|
|
10
|
+
ApprovalRequest,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"ApprovalDecisionType",
|
|
15
|
+
"ApprovalRequest",
|
|
16
|
+
"ApprovalDecision",
|
|
17
|
+
"ApprovalLogEntry",
|
|
18
|
+
]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Prompt handler implementations for HITL approvals."""
|
|
2
|
+
|
|
3
|
+
from aip_agents.agent.hitl.prompt.base import BasePromptHandler
|
|
4
|
+
from aip_agents.agent.hitl.prompt.deferred import DeferredPromptHandler
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"BasePromptHandler",
|
|
8
|
+
"DeferredPromptHandler",
|
|
9
|
+
]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Shared base classes for HITL prompt handlers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from aip_agents.schema.hitl import ApprovalDecision, ApprovalRequest
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING: # pragma: no cover - typing-only import
|
|
11
|
+
from aip_agents.agent.hitl.manager import ApprovalManager
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BasePromptHandler(ABC):
|
|
15
|
+
"""Abstract base class for prompt handlers used in HITL flows."""
|
|
16
|
+
|
|
17
|
+
def attach_manager(self, manager: ApprovalManager) -> None:
|
|
18
|
+
"""Optionally attach the ``ApprovalManager`` coordinating approvals.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
manager (ApprovalManager): The approval manager instance to attach.
|
|
22
|
+
"""
|
|
23
|
+
# Default implementation stores nothing; subclasses override as needed.
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
async def prompt_for_decision(
|
|
28
|
+
self,
|
|
29
|
+
request: ApprovalRequest,
|
|
30
|
+
timeout_seconds: int,
|
|
31
|
+
context_keys: list[str] | None = None,
|
|
32
|
+
) -> ApprovalDecision:
|
|
33
|
+
"""Collect and return a decision for the given approval request.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
request (ApprovalRequest): The approval request to prompt for.
|
|
37
|
+
timeout_seconds (int): Maximum time to wait for a decision in seconds.
|
|
38
|
+
context_keys (list[str] | None, optional): Optional keys for additional context. Defaults to None.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
ApprovalDecision: The decision made for the approval request.
|
|
42
|
+
"""
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Deferred prompt handler that waits for external approval resolution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from aip_agents.agent.hitl.prompt.base import BasePromptHandler
|
|
10
|
+
from aip_agents.schema.hitl import ApprovalDecision, ApprovalDecisionType, ApprovalRequest
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING: # pragma: no cover - typing-only import
|
|
13
|
+
from aip_agents.agent.hitl.manager import ApprovalManager
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DeferredPromptHandler(BasePromptHandler):
|
|
17
|
+
"""Prompt handler that defers tool execution until an external decision is received."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, notify: Callable[[ApprovalRequest], None] | None = None) -> None:
|
|
20
|
+
"""Initialize the deferred prompt handler.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
notify: Optional callback function to notify when an approval request is made.
|
|
24
|
+
"""
|
|
25
|
+
self._notify = notify
|
|
26
|
+
self._manager: ApprovalManager | None = None
|
|
27
|
+
|
|
28
|
+
def attach_manager(self, manager: ApprovalManager) -> None:
|
|
29
|
+
"""Attach the ApprovalManager orchestrating approvals.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
manager: The ApprovalManager instance to attach for handling approval decisions.
|
|
33
|
+
"""
|
|
34
|
+
self._manager = manager
|
|
35
|
+
|
|
36
|
+
async def prompt_for_decision(
|
|
37
|
+
self,
|
|
38
|
+
request: ApprovalRequest,
|
|
39
|
+
timeout_seconds: int,
|
|
40
|
+
context_keys: list[str] | None = None,
|
|
41
|
+
) -> ApprovalDecision:
|
|
42
|
+
"""Register a waiter and return a pending decision sentinel.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
request: The approval request containing the tool call details and context.
|
|
46
|
+
timeout_seconds: Number of seconds to wait for approval before timing out.
|
|
47
|
+
context_keys: Optional list of context keys to include in the approval request.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
ApprovalDecision with PENDING status and registered waiter for external resolution.
|
|
51
|
+
"""
|
|
52
|
+
if self._notify:
|
|
53
|
+
try:
|
|
54
|
+
self._notify(request)
|
|
55
|
+
except Exception:
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
if self._manager is None:
|
|
59
|
+
return ApprovalDecision(
|
|
60
|
+
request_id=request.request_id,
|
|
61
|
+
decision=ApprovalDecisionType.PENDING,
|
|
62
|
+
operator_input="PENDING",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
loop = asyncio.get_running_loop()
|
|
66
|
+
waiter = loop.create_future()
|
|
67
|
+
self._manager.register_waiter(request.request_id, waiter)
|
|
68
|
+
|
|
69
|
+
return ApprovalDecision(
|
|
70
|
+
request_id=request.request_id,
|
|
71
|
+
decision=ApprovalDecisionType.PENDING,
|
|
72
|
+
operator_input="PENDING",
|
|
73
|
+
)
|