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,25 @@
|
|
|
1
|
+
"""Public API for PII-related utilities.
|
|
2
|
+
|
|
3
|
+
This subpackage groups all helpers for anonymizing/deanonymizing PII used by
|
|
4
|
+
agents and tools.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from aip_agents.utils.pii.pii_handler import ToolPIIHandler
|
|
8
|
+
from aip_agents.utils.pii.pii_helper import (
|
|
9
|
+
add_pii_mappings,
|
|
10
|
+
anonymize_final_response_content,
|
|
11
|
+
deanonymize_final_response_content,
|
|
12
|
+
extract_pii_mapping_from_agent_response,
|
|
13
|
+
normalize_enable_pii,
|
|
14
|
+
)
|
|
15
|
+
from aip_agents.utils.pii.uuid_deanonymizer_mapping import UUIDDeanonymizerMapping
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"ToolPIIHandler",
|
|
19
|
+
"add_pii_mappings",
|
|
20
|
+
"anonymize_final_response_content",
|
|
21
|
+
"deanonymize_final_response_content",
|
|
22
|
+
"extract_pii_mapping_from_agent_response",
|
|
23
|
+
"normalize_enable_pii",
|
|
24
|
+
"UUIDDeanonymizerMapping",
|
|
25
|
+
]
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
"""PII handler for masking/demasking at the tool calling level.
|
|
2
|
+
|
|
3
|
+
This module provides the ToolPIIHandler class for handling PII operations
|
|
4
|
+
during tool execution in LangGraph agents. Tag replacement works with the
|
|
5
|
+
mapping supplied by the runner, while advanced NER-powered detection is only
|
|
6
|
+
enabled when NER_API_URL and NER_API_KEY environment variables are set.
|
|
7
|
+
|
|
8
|
+
Authors:
|
|
9
|
+
Fachriza Adhiatma (fachriza.d.adhiatma@gdplabs.id)
|
|
10
|
+
|
|
11
|
+
References:
|
|
12
|
+
1. https://gdplabs.gitbook.io/sdk/tutorials/security-and-privacy/pii-masking#anonymizer-with-ner
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
from enum import Enum
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
from gllm_privacy.pii_detector import TextAnalyzer, TextAnonymizer
|
|
21
|
+
from gllm_privacy.pii_detector.anonymizer import Operation
|
|
22
|
+
from gllm_privacy.pii_detector.constants import GLLM_PRIVACY_ENTITIES, Entities
|
|
23
|
+
from gllm_privacy.pii_detector.recognizer.gdplabs_ner_api_remote_recognizer import (
|
|
24
|
+
GDPLabsNerApiRemoteRecognizer,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
_HAS_GLLM_PRIVACY = True
|
|
28
|
+
except ImportError: # pragma: no cover
|
|
29
|
+
TextAnalyzer = Any # type: ignore[assignment]
|
|
30
|
+
TextAnonymizer = Any # type: ignore[assignment]
|
|
31
|
+
GDPLabsNerApiRemoteRecognizer = Any # type: ignore[assignment]
|
|
32
|
+
|
|
33
|
+
class _Operation(str, Enum):
|
|
34
|
+
ANONYMIZE = "ANONYMIZE"
|
|
35
|
+
DEANONYMIZE = "DEANONYMIZE"
|
|
36
|
+
|
|
37
|
+
Operation = _Operation
|
|
38
|
+
|
|
39
|
+
GLLM_PRIVACY_ENTITIES = [] # type: ignore[assignment]
|
|
40
|
+
Entities = None # type: ignore[assignment]
|
|
41
|
+
_HAS_GLLM_PRIVACY = False
|
|
42
|
+
|
|
43
|
+
from aip_agents.utils.logger import LoggerManager
|
|
44
|
+
from aip_agents.utils.pii.uuid_deanonymizer_mapping import UUIDDeanonymizerMapping
|
|
45
|
+
|
|
46
|
+
logger = LoggerManager().get_logger(__name__)
|
|
47
|
+
|
|
48
|
+
NER_API_URL_ENV_VAR = "NER_API_URL"
|
|
49
|
+
NER_API_KEY_ENV_VAR = "NER_API_KEY"
|
|
50
|
+
NER_API_TIMEOUT = 10
|
|
51
|
+
if _HAS_GLLM_PRIVACY:
|
|
52
|
+
EXCLUDED_ENTITIES = [Entities.URL.value]
|
|
53
|
+
DEFAULT_SUPPORTED_ENTITIES = [entity for entity in GLLM_PRIVACY_ENTITIES if entity not in EXCLUDED_ENTITIES]
|
|
54
|
+
else:
|
|
55
|
+
EXCLUDED_ENTITIES = []
|
|
56
|
+
DEFAULT_SUPPORTED_ENTITIES = []
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ToolPIIHandler:
|
|
60
|
+
"""Handles PII masking/demasking for tool calling.
|
|
61
|
+
|
|
62
|
+
Tag replacement based on runner-provided mappings always works. Optional
|
|
63
|
+
NER-powered masking/de-masking is only enabled when NER_API_URL and
|
|
64
|
+
NER_API_KEY environment variables are set.
|
|
65
|
+
|
|
66
|
+
Attributes:
|
|
67
|
+
flat_pii_mapping: Flat mapping from runner service (tag → value)
|
|
68
|
+
text_analyzer: GLLM Privacy TextAnalyzer instance
|
|
69
|
+
text_anonymizer: GLLM Privacy TextAnonymizer instance
|
|
70
|
+
enable_ner: Whether NER is enabled
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
pii_mapping: dict[str, str] | None = None,
|
|
76
|
+
ner_api_url: str | None = None,
|
|
77
|
+
ner_api_key: str | None = None,
|
|
78
|
+
) -> None:
|
|
79
|
+
"""Initialize PII handler (private - use create_if_enabled() instead).
|
|
80
|
+
|
|
81
|
+
Initializes GLLM Privacy components (TextAnalyzer, TextAnonymizer) if NER credentials
|
|
82
|
+
are provided. Creates dual recognizers for Indonesian and English languages.
|
|
83
|
+
Pre-loads any existing PII mappings into the anonymizer's internal state.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
pii_mapping: Existing PII mapping from runner service (flat format: tag -> value)
|
|
87
|
+
ner_api_url: NER API endpoint URL
|
|
88
|
+
ner_api_key: NER API authentication key
|
|
89
|
+
"""
|
|
90
|
+
self.flat_pii_mapping: dict[str, str] = pii_mapping or {}
|
|
91
|
+
self.enable_ner: bool = bool(ner_api_url and ner_api_key)
|
|
92
|
+
self.text_analyzer: TextAnalyzer | None = None
|
|
93
|
+
self.text_anonymizer: TextAnonymizer | None = None
|
|
94
|
+
|
|
95
|
+
if self.enable_ner and not _HAS_GLLM_PRIVACY:
|
|
96
|
+
logger.warning(
|
|
97
|
+
"NER is configured (NER_API_URL/NER_API_KEY present) but optional dependency 'gllm-privacy' "
|
|
98
|
+
"is not installed. Continuing with NER disabled."
|
|
99
|
+
)
|
|
100
|
+
self.enable_ner = False
|
|
101
|
+
|
|
102
|
+
if self.enable_ner:
|
|
103
|
+
try:
|
|
104
|
+
headers = {"X-Api-Key": ner_api_key}
|
|
105
|
+
|
|
106
|
+
id_recognizer = GDPLabsNerApiRemoteRecognizer(
|
|
107
|
+
api_url=ner_api_url,
|
|
108
|
+
supported_language="id",
|
|
109
|
+
api_headers=headers,
|
|
110
|
+
api_timeout=NER_API_TIMEOUT,
|
|
111
|
+
)
|
|
112
|
+
en_recognizer = GDPLabsNerApiRemoteRecognizer(
|
|
113
|
+
api_url=ner_api_url,
|
|
114
|
+
supported_language="en",
|
|
115
|
+
api_headers=headers,
|
|
116
|
+
api_timeout=NER_API_TIMEOUT,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
self.text_analyzer = TextAnalyzer(additional_recognizers=[id_recognizer, en_recognizer])
|
|
120
|
+
|
|
121
|
+
# Initialize with UUID-based deanonymizer mapping
|
|
122
|
+
uuid_mapping = UUIDDeanonymizerMapping(uuid_length=8)
|
|
123
|
+
self.text_anonymizer = TextAnonymizer(
|
|
124
|
+
text_analyzer=self.text_analyzer,
|
|
125
|
+
deanonymizer_mapping=uuid_mapping,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if self.flat_pii_mapping:
|
|
129
|
+
gllm_mapping = self._convert_flat_to_gllm_format(self.flat_pii_mapping)
|
|
130
|
+
self.text_anonymizer._deanonymizer_mapping.update(gllm_mapping, use_uuid_suffix=False)
|
|
131
|
+
|
|
132
|
+
except Exception as e: # noqa: BLE001
|
|
133
|
+
logger.warning(f"Failed to initialize GLLM Privacy components: {e}")
|
|
134
|
+
self.enable_ner = False
|
|
135
|
+
|
|
136
|
+
@classmethod
|
|
137
|
+
def create_if_enabled(cls, pii_mapping: dict[str, str] | None = None) -> "ToolPIIHandler | None":
|
|
138
|
+
"""Create ToolPIIHandler when mappings or NER configuration exist.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
pii_mapping: Existing PII mapping from runner service
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
ToolPIIHandler instance when mapping or NER config is available, None otherwise
|
|
145
|
+
"""
|
|
146
|
+
ner_api_url = os.getenv(NER_API_URL_ENV_VAR)
|
|
147
|
+
ner_api_key = os.getenv(NER_API_KEY_ENV_VAR)
|
|
148
|
+
|
|
149
|
+
if ner_api_url and ner_api_key:
|
|
150
|
+
return cls(pii_mapping, ner_api_url, ner_api_key)
|
|
151
|
+
|
|
152
|
+
if pii_mapping:
|
|
153
|
+
return cls(pii_mapping)
|
|
154
|
+
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
@classmethod
|
|
158
|
+
def create_mapping_only(cls, pii_mapping: dict[str, str] | None = None) -> "ToolPIIHandler | None":
|
|
159
|
+
"""Create ToolPIIHandler in mapping-only mode (no NER).
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
pii_mapping: Existing PII mapping from runner service
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
ToolPIIHandler instance when mapping exists, None otherwise
|
|
166
|
+
"""
|
|
167
|
+
if not pii_mapping:
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
return cls(pii_mapping)
|
|
171
|
+
|
|
172
|
+
def deanonymize_tool_args(self, args: dict[str, Any]) -> dict[str, Any]:
|
|
173
|
+
"""Replace PII tags in tool arguments with real values.
|
|
174
|
+
|
|
175
|
+
Recursively processes dictionaries, lists, and strings to replace all PII tags
|
|
176
|
+
(e.g., '<EMAIL_1>') with their corresponding real values from flat_pii_mapping.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
args: Tool arguments that may contain PII tags
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Arguments with tags replaced by real values
|
|
183
|
+
"""
|
|
184
|
+
return self._process_value(args, operation=Operation.DEANONYMIZE)
|
|
185
|
+
|
|
186
|
+
def anonymize_tool_output(self, output: Any) -> tuple[Any, dict[str, str]]:
|
|
187
|
+
"""Mask PII values in tool output.
|
|
188
|
+
|
|
189
|
+
Handles string and dictionary outputs. For strings, uses two-phase anonymization:
|
|
190
|
+
first masks known PII, then detects new PII via NER. For dictionaries, recursively
|
|
191
|
+
processes all string values. Returns updated mapping with any newly discovered PII.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
output: Tool output that may contain PII values (string, dict, or other)
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Tuple of (anonymized_output, updated_flat_pii_mapping)
|
|
198
|
+
"""
|
|
199
|
+
if isinstance(output, str):
|
|
200
|
+
anonymized, updated_mapping = self._anonymize_text(output)
|
|
201
|
+
return anonymized, updated_mapping
|
|
202
|
+
elif isinstance(output, dict):
|
|
203
|
+
anonymized, updated_mapping = self._anonymize_dict(output)
|
|
204
|
+
return anonymized, updated_mapping
|
|
205
|
+
else:
|
|
206
|
+
# For non-string, non-dict outputs, return as-is
|
|
207
|
+
return output, self.flat_pii_mapping
|
|
208
|
+
|
|
209
|
+
@staticmethod
|
|
210
|
+
def _convert_flat_to_gllm_format(
|
|
211
|
+
flat_mapping: dict[str, str],
|
|
212
|
+
) -> dict[str, dict[str, str]]:
|
|
213
|
+
"""Convert flat PII mapping to GLLM Privacy nested format.
|
|
214
|
+
|
|
215
|
+
Transforms flat format {'<PERSON_1>': 'Alice'} to nested format
|
|
216
|
+
{'PERSON': {'<PERSON_1>': 'Alice'}} required by GLLM Privacy's internal state.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
flat_mapping: Flat mapping (tag → value)
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Nested mapping organized by entity type
|
|
223
|
+
"""
|
|
224
|
+
gllm_mapping: dict[str, dict[str, str]] = {}
|
|
225
|
+
|
|
226
|
+
for tag, value in flat_mapping.items():
|
|
227
|
+
tag_content = tag.strip("<>")
|
|
228
|
+
parts = tag_content.rsplit("_", 1)
|
|
229
|
+
entity_type = parts[0] if parts else "UNKNOWN"
|
|
230
|
+
|
|
231
|
+
if entity_type not in gllm_mapping:
|
|
232
|
+
gllm_mapping[entity_type] = {}
|
|
233
|
+
|
|
234
|
+
gllm_mapping[entity_type][tag] = value
|
|
235
|
+
|
|
236
|
+
return gllm_mapping
|
|
237
|
+
|
|
238
|
+
@staticmethod
|
|
239
|
+
def _convert_gllm_to_flat_format(
|
|
240
|
+
gllm_mapping: dict[str, dict[str, str]],
|
|
241
|
+
) -> dict[str, str]:
|
|
242
|
+
"""Convert GLLM Privacy nested format back to flat format.
|
|
243
|
+
|
|
244
|
+
Inverse of _convert_flat_to_gllm_format. Flattens nested structure
|
|
245
|
+
{'PERSON': {'<PERSON_1>': 'Alice'}} back to {'<PERSON_1>': 'Alice'}.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
gllm_mapping: Nested mapping from GLLM Privacy
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
Flat mapping (tag → value)
|
|
252
|
+
"""
|
|
253
|
+
flat_mapping: dict[str, str] = {}
|
|
254
|
+
|
|
255
|
+
for entity_type, tags_dict in gllm_mapping.items():
|
|
256
|
+
for tag, value in tags_dict.items():
|
|
257
|
+
flat_mapping[tag] = value
|
|
258
|
+
|
|
259
|
+
return flat_mapping
|
|
260
|
+
|
|
261
|
+
def _deanonymize_text(self, text: str) -> str:
|
|
262
|
+
"""Deanonymize a single text string by replacing PII tags with real values.
|
|
263
|
+
|
|
264
|
+
Uses GLLM Privacy's TextAnonymizer.deanonymize() when NER is enabled,
|
|
265
|
+
otherwise falls back to simple string replacement. GLLM Privacy's deanonymize()
|
|
266
|
+
doesn't require entities parameter as they're configured during TextAnalyzer init.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
text: Text containing PII tags (e.g., '<EMAIL_1>', '<PERSON_1>')
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
Text with PII tags replaced by their real values from flat_pii_mapping
|
|
273
|
+
"""
|
|
274
|
+
if self.enable_ner and self.text_anonymizer:
|
|
275
|
+
try:
|
|
276
|
+
restored_text = self.text_anonymizer.deanonymize(text=text)
|
|
277
|
+
return restored_text
|
|
278
|
+
except Exception as e:
|
|
279
|
+
logger.warning(f"GLLM Privacy deanonymization failed: {e}, falling back to simple replacement")
|
|
280
|
+
return self._replace_tags_in_text(text)
|
|
281
|
+
else:
|
|
282
|
+
return self._replace_tags_in_text(text)
|
|
283
|
+
|
|
284
|
+
def _anonymize_text(self, text: str) -> tuple[str, dict[str, str]]:
|
|
285
|
+
"""Anonymize PII in text using a two-phase approach.
|
|
286
|
+
|
|
287
|
+
Phase 1: Masks known PII values using existing flat_pii_mapping (simple replacement).
|
|
288
|
+
Phase 2: Uses NER (if enabled) to detect and mask NEW PII values not in mapping.
|
|
289
|
+
Phase 3: Extracts newly discovered PII from GLLM Privacy's internal state.
|
|
290
|
+
Phase 4-5: Converts and merges new mappings into flat_pii_mapping.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
text: Text that may contain real PII values to be masked
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
Tuple of (anonymized_text, updated_flat_pii_mapping including new discoveries)
|
|
297
|
+
"""
|
|
298
|
+
anonymized = self._mask_with_existing_mapping(text)
|
|
299
|
+
|
|
300
|
+
if self.enable_ner and self.text_anonymizer:
|
|
301
|
+
try:
|
|
302
|
+
result = self.text_anonymizer.anonymize(text=anonymized, entities=DEFAULT_SUPPORTED_ENTITIES)
|
|
303
|
+
if hasattr(result, "text"):
|
|
304
|
+
anonymized = result.text
|
|
305
|
+
else:
|
|
306
|
+
anonymized = result
|
|
307
|
+
|
|
308
|
+
gllm_mapping = self.text_anonymizer.deanonymizer_mapping
|
|
309
|
+
if gllm_mapping:
|
|
310
|
+
new_flat_mapping = self._convert_gllm_to_flat_format(gllm_mapping)
|
|
311
|
+
self.flat_pii_mapping.update(new_flat_mapping)
|
|
312
|
+
except Exception as e:
|
|
313
|
+
logger.warning(f"GLLM Privacy anonymization failed: {e}, continuing with masked text")
|
|
314
|
+
|
|
315
|
+
return anonymized, self.flat_pii_mapping
|
|
316
|
+
|
|
317
|
+
def _anonymize_dict(self, data: dict[str, Any]) -> tuple[dict[str, Any], dict[str, str]]:
|
|
318
|
+
"""Anonymize PII in dictionary recursively.
|
|
319
|
+
|
|
320
|
+
Processes string values with _anonymize_text(), recursively handles nested dicts,
|
|
321
|
+
and processes list items. Non-string/dict/list values are preserved as-is.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
data: Dictionary that may contain PII values
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
Tuple of (anonymized_dict, updated_mapping)
|
|
328
|
+
"""
|
|
329
|
+
anonymized = {}
|
|
330
|
+
|
|
331
|
+
for key, value in data.items():
|
|
332
|
+
if isinstance(value, str):
|
|
333
|
+
anonymized_value, _ = self._anonymize_text(value)
|
|
334
|
+
anonymized[key] = anonymized_value
|
|
335
|
+
elif isinstance(value, dict):
|
|
336
|
+
anonymized[key], _ = self._anonymize_dict(value)
|
|
337
|
+
elif isinstance(value, list):
|
|
338
|
+
anonymized[key] = [self._anonymize_text(item)[0] if isinstance(item, str) else item for item in value]
|
|
339
|
+
else:
|
|
340
|
+
anonymized[key] = value
|
|
341
|
+
|
|
342
|
+
return anonymized, self.flat_pii_mapping
|
|
343
|
+
|
|
344
|
+
def _mask_with_existing_mapping(self, text: str) -> str:
|
|
345
|
+
"""Mask PII using existing flat mapping.
|
|
346
|
+
|
|
347
|
+
Iterates through mapping in reverse order of value length to handle overlapping
|
|
348
|
+
PII values correctly (longer values are replaced first).
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
text: Text to mask
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
Text with known PII values replaced by tags
|
|
355
|
+
"""
|
|
356
|
+
masked = text
|
|
357
|
+
for tag, value in sorted(self.flat_pii_mapping.items(), key=lambda x: len(x[1]), reverse=True):
|
|
358
|
+
if value in masked:
|
|
359
|
+
masked = masked.replace(value, tag)
|
|
360
|
+
return masked
|
|
361
|
+
|
|
362
|
+
def _replace_tags_in_text(self, text: str) -> str:
|
|
363
|
+
"""Replace PII tags with real values in text using flat mapping.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
text: Text containing PII tags
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
Text with tags replaced by values
|
|
370
|
+
"""
|
|
371
|
+
replaced = text
|
|
372
|
+
for tag, value in self.flat_pii_mapping.items():
|
|
373
|
+
if tag in replaced:
|
|
374
|
+
replaced = replaced.replace(tag, value)
|
|
375
|
+
return replaced
|
|
376
|
+
|
|
377
|
+
def _process_value(self, value: Any, operation: Operation) -> Any:
|
|
378
|
+
"""Process a value recursively based on operation type.
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
value: Value to process
|
|
382
|
+
operation: Operation enum value (ANONYMIZE or DEANONYMIZE)
|
|
383
|
+
|
|
384
|
+
Returns:
|
|
385
|
+
Processed value
|
|
386
|
+
"""
|
|
387
|
+
if isinstance(value, str):
|
|
388
|
+
if operation == Operation.DEANONYMIZE:
|
|
389
|
+
return self._deanonymize_text(value)
|
|
390
|
+
else:
|
|
391
|
+
return self._anonymize_text(value)[0]
|
|
392
|
+
elif isinstance(value, dict):
|
|
393
|
+
return {k: self._process_value(v, operation) for k, v in value.items()}
|
|
394
|
+
elif isinstance(value, list):
|
|
395
|
+
return [self._process_value(item, operation) for item in value]
|
|
396
|
+
else:
|
|
397
|
+
return value
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""PII mapping helper functions for LangGraph agent state management.
|
|
2
|
+
|
|
3
|
+
This module provides reducer functions and extraction utilities for managing
|
|
4
|
+
PII mappings across tool execution and multi-agent delegation scenarios.
|
|
5
|
+
|
|
6
|
+
Authors:
|
|
7
|
+
Fachriza Adhiatma (fachriza.d.adhiatma@gdplabs.id)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from aip_agents.utils.logger import LoggerManager
|
|
13
|
+
|
|
14
|
+
logger = LoggerManager().get_logger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def normalize_enable_pii(enable_pii: Any) -> bool | None:
|
|
18
|
+
"""Normalize enable_pii value from agent configuration.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
enable_pii: Raw enable_pii value from agent configuration.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
The normalized enable_pii flag when explicitly set (True/False), otherwise None.
|
|
25
|
+
"""
|
|
26
|
+
if enable_pii is None:
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
if isinstance(enable_pii, bool):
|
|
30
|
+
return enable_pii
|
|
31
|
+
|
|
32
|
+
logger.warning("Ignoring invalid enable_pii value from agent config: %s", enable_pii)
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _get_pii_mapping_from_metadata(metadata: dict[str, Any] | None) -> dict[str, str] | None:
|
|
37
|
+
"""Extract the pii_mapping dictionary from metadata structures.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
metadata: Metadata payload that may contain a pii_mapping key directly or nested
|
|
41
|
+
inside another metadata dictionary.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
A dict containing tag-to-value mappings when found, otherwise None.
|
|
45
|
+
"""
|
|
46
|
+
if not isinstance(metadata, dict):
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
metadata_dict = metadata
|
|
50
|
+
if "pii_mapping" not in metadata_dict and isinstance(metadata.get("metadata"), dict):
|
|
51
|
+
metadata_dict = metadata["metadata"]
|
|
52
|
+
|
|
53
|
+
pii_mapping = metadata_dict.get("pii_mapping")
|
|
54
|
+
if isinstance(pii_mapping, dict) and pii_mapping:
|
|
55
|
+
return pii_mapping # type: ignore[return-value]
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _replace_content_segments(content: str, replacements: list[tuple[str, str]]) -> str:
|
|
60
|
+
"""Apply sequential placeholder replacements on a response string.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
content: Original response content containing placeholders.
|
|
64
|
+
replacements: List of (placeholder, actual_value) tuples to apply in order.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
The content string with all replacements applied.
|
|
68
|
+
"""
|
|
69
|
+
if not replacements:
|
|
70
|
+
return content
|
|
71
|
+
|
|
72
|
+
result = content
|
|
73
|
+
for placeholder, actual in replacements:
|
|
74
|
+
result = result.replace(placeholder, actual)
|
|
75
|
+
return result
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def add_pii_mappings(
|
|
79
|
+
left: dict[str, str] | None,
|
|
80
|
+
right: dict[str, str] | None,
|
|
81
|
+
) -> dict[str, str]:
|
|
82
|
+
"""Reducer function to merge PII mappings from multiple sources.
|
|
83
|
+
|
|
84
|
+
This is a LangGraph reducer function that merges PII mappings from:
|
|
85
|
+
- Parent agent's initial mapping
|
|
86
|
+
- Tool outputs with newly discovered PII
|
|
87
|
+
- Subagent responses with their discovered PII
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
left: Existing PII mapping (or None)
|
|
91
|
+
right: New PII mapping to merge (or None)
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Merged PII mapping dictionary
|
|
95
|
+
|
|
96
|
+
Note:
|
|
97
|
+
- Right (new) mappings take precedence over left (existing)
|
|
98
|
+
- Handles None/non-dict cases gracefully
|
|
99
|
+
- Preserves all unique PII tags
|
|
100
|
+
- Returns empty dict if both inputs are None/empty
|
|
101
|
+
"""
|
|
102
|
+
# Handle None/non-dict inputs
|
|
103
|
+
left_dict = left if isinstance(left, dict) else {}
|
|
104
|
+
right_dict = right if isinstance(right, dict) else {}
|
|
105
|
+
|
|
106
|
+
# Merge: right takes precedence
|
|
107
|
+
merged = {**left_dict, **right_dict}
|
|
108
|
+
|
|
109
|
+
return merged if merged else {}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def extract_pii_mapping_from_agent_response(result: Any) -> dict[str, str] | None:
|
|
113
|
+
"""Extract PII mapping from subagent response.
|
|
114
|
+
|
|
115
|
+
Used by DelegationToolManager to propagate PII mappings from subagents
|
|
116
|
+
back to parent agents.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
result: The result returned by the delegated agent
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
PII mapping dictionary if found, None otherwise
|
|
123
|
+
|
|
124
|
+
Note:
|
|
125
|
+
- Checks if result is a dict
|
|
126
|
+
- Extracts 'full_final_state' from result
|
|
127
|
+
- Extracts 'pii_mapping' from full_final_state
|
|
128
|
+
- Validates mapping is a non-empty dict
|
|
129
|
+
- Returns None if any step fails
|
|
130
|
+
"""
|
|
131
|
+
# Validate result is a dict
|
|
132
|
+
if not isinstance(result, dict):
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
# Extract full_final_state
|
|
136
|
+
full_final_state = result.get("full_final_state")
|
|
137
|
+
if not isinstance(full_final_state, dict):
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
# Extract pii_mapping
|
|
141
|
+
pii_mapping = full_final_state.get("pii_mapping")
|
|
142
|
+
if not isinstance(pii_mapping, dict) or not pii_mapping:
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
logger.info(f"Extracted PII mapping with {len(pii_mapping)} entries from agent response")
|
|
146
|
+
return pii_mapping # type: ignore
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def deanonymize_final_response_content(
|
|
150
|
+
content: str,
|
|
151
|
+
is_final_response: bool,
|
|
152
|
+
metadata: dict[str, Any] | None,
|
|
153
|
+
) -> str:
|
|
154
|
+
"""Deanonymize final response content using PII mapping from metadata.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
content: Final response content that may contain PII tags.
|
|
158
|
+
is_final_response: Flag indicating whether this message is a final response.
|
|
159
|
+
metadata: Optional metadata dict (or event payload containing ``metadata``) with
|
|
160
|
+
``pii_mapping`` tag-to-value mapping.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Content string with PII tags replaced by real values when applicable.
|
|
164
|
+
"""
|
|
165
|
+
if not is_final_response:
|
|
166
|
+
return content
|
|
167
|
+
|
|
168
|
+
pii_mapping = _get_pii_mapping_from_metadata(metadata)
|
|
169
|
+
if not pii_mapping:
|
|
170
|
+
return content
|
|
171
|
+
|
|
172
|
+
replacements = [
|
|
173
|
+
(tag, value) for tag, value in pii_mapping.items() if isinstance(tag, str) and isinstance(value, str) and tag
|
|
174
|
+
]
|
|
175
|
+
|
|
176
|
+
return _replace_content_segments(content, replacements)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def anonymize_final_response_content(content: str, metadata: dict[str, Any] | None) -> str:
|
|
180
|
+
"""Anonymize final response content using PII mapping from metadata.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
content: Final response content that may contain real PII values.
|
|
184
|
+
metadata: Metadata dict (or event payload containing ``metadata``) with
|
|
185
|
+
``pii_mapping`` tag-to-value mapping.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Content string with real PII values replaced by their PII tags when mapping is present.
|
|
189
|
+
"""
|
|
190
|
+
if not isinstance(content, str) or not content:
|
|
191
|
+
return content
|
|
192
|
+
|
|
193
|
+
pii_mapping = _get_pii_mapping_from_metadata(metadata)
|
|
194
|
+
if not pii_mapping:
|
|
195
|
+
return content
|
|
196
|
+
|
|
197
|
+
replacements = [
|
|
198
|
+
(tag, value) for tag, value in pii_mapping.items() if isinstance(tag, str) and isinstance(value, str) and value
|
|
199
|
+
]
|
|
200
|
+
if not replacements:
|
|
201
|
+
return content
|
|
202
|
+
|
|
203
|
+
# Replace longer values first to avoid partial replacements (e.g., "John" before "John Smith").
|
|
204
|
+
replacements.sort(key=lambda item: len(item[1]), reverse=True)
|
|
205
|
+
normalized_pairs = [(value, tag) for tag, value in replacements]
|
|
206
|
+
|
|
207
|
+
return _replace_content_segments(content, normalized_pairs)
|