chore: import upstream snapshot with attribution
This commit is contained in:
+205
@@ -0,0 +1,205 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from google.adk.tools import ToolContext
|
||||
from app.state_schema import ComplianceStep
|
||||
# NOTE: This module is retained as a reference implementation showing the
|
||||
# resilience pattern: timeout, retry, and fail-close to MANUAL_REVIEW. The live
|
||||
# cockpit path calls the Go compliance agent explicitly from fast_api_app.py so
|
||||
# the handoff is observable in the browser. agent.py keeps the ADK
|
||||
# RemoteA2aAgent architecture reference.
|
||||
|
||||
|
||||
async def invoke_a2a_compliance_check(tool_context):
|
||||
"""Placeholder for the legacy A2A compliance check.
|
||||
|
||||
In the original implementation, this function was imported from
|
||||
a2a_client.py which used hand-rolled HTTP calls to the Go agent.
|
||||
The live cockpit now uses fast_api_app.invoke_go_compliance_service().
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"Legacy A2A client removed. Use fast_api_app.invoke_go_compliance_service()."
|
||||
)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _log_structured_json(severity: str, message: str, **kwargs) -> None:
|
||||
"""Standardized structured JSON logger for GCP/Cloud Logging visibility.
|
||||
|
||||
Rule 53 Enforcement: Logs diagnostic messages in parseable JSON structures,
|
||||
ensuring sensitive session tokens are never printed.
|
||||
"""
|
||||
payload = {
|
||||
"severity": severity,
|
||||
"message": message,
|
||||
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S%Z"),
|
||||
**kwargs
|
||||
}
|
||||
logger.info(json.dumps(payload))
|
||||
|
||||
|
||||
async def invoke_resilient_compliance_check(
|
||||
tool_context: ToolContext,
|
||||
timeout_sec: float = 30.0,
|
||||
max_retries: int = 3,
|
||||
backoff_factor: float = 1.5,
|
||||
) -> dict:
|
||||
"""Wraps A2A task audit dispatching in high-resilience triggers:
|
||||
|
||||
1. Handles 503 Crashed Server exceptions using exponential retry loops.
|
||||
2. Enforces a strict 30-second timeout boundary.
|
||||
3. Triggers safe fail-close path migrations (transitions step to MANUAL_REVIEW).
|
||||
"""
|
||||
state = tool_context.state
|
||||
case_id = state.get("case_id", "local-case")
|
||||
|
||||
# Check if a custom timeout is toggled in memory settings
|
||||
simulated_timeout = state.get("simulated_timeout", timeout_sec)
|
||||
|
||||
_log_structured_json(
|
||||
severity="INFO",
|
||||
message=f"Resilience Handler: Initializing compliance check for case {case_id}",
|
||||
event="compliance_audit_start",
|
||||
case_id=case_id,
|
||||
timeout_boundary_sec=simulated_timeout
|
||||
)
|
||||
|
||||
attempt = 0
|
||||
backoff_delay = 1.0
|
||||
|
||||
while attempt < max_retries:
|
||||
attempt += 1
|
||||
try:
|
||||
# Wrap the A2A submission and check in a strict timeout boundary
|
||||
_log_structured_json(
|
||||
severity="INFO",
|
||||
message=f"Dispatching A2A audit request (Attempt {attempt} of {max_retries})",
|
||||
event="a2a_dispatch_attempt",
|
||||
case_id=case_id,
|
||||
attempt=attempt
|
||||
)
|
||||
|
||||
# Execute A2A client validation with timeout caps
|
||||
res = await asyncio.wait_for(
|
||||
invoke_a2a_compliance_check(tool_context),
|
||||
timeout=simulated_timeout
|
||||
)
|
||||
|
||||
# Check response status
|
||||
if res.get("status") == "dormant_paused":
|
||||
_log_structured_json(
|
||||
severity="INFO",
|
||||
message="External connection reports lag. Task submitted and pipeline entered dormant state.",
|
||||
event="dormancy_entered",
|
||||
case_id=case_id,
|
||||
task_id=res.get("task_id")
|
||||
)
|
||||
return res
|
||||
|
||||
_log_structured_json(
|
||||
severity="INFO",
|
||||
message="A2A compliance validation audit finished successfully.",
|
||||
event="compliance_audit_completed",
|
||||
case_id=case_id
|
||||
)
|
||||
return res
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
# 30-second timeout reached
|
||||
_log_structured_json(
|
||||
severity="WARNING",
|
||||
message=f"A2A connection timed out after {simulated_timeout} seconds limit.",
|
||||
event="timeout_reached",
|
||||
case_id=case_id,
|
||||
timeout_limit_sec=simulated_timeout
|
||||
)
|
||||
break # Move straight to MANUAL_REVIEW fallback
|
||||
|
||||
except ConnectionError as e:
|
||||
# 503 connection error or socket faults -> retry loop triggers
|
||||
_log_structured_json(
|
||||
severity="WARNING",
|
||||
message=f"A2A connection failed on attempt {attempt}: {e!s}",
|
||||
event="connection_fault",
|
||||
case_id=case_id,
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
if attempt >= max_retries:
|
||||
_log_structured_json(
|
||||
severity="ERROR",
|
||||
message="Max A2A delivery retries exhausted. Triggering safety manual routing.",
|
||||
event="retries_exhausted",
|
||||
case_id=case_id
|
||||
)
|
||||
break
|
||||
|
||||
# Perform backoff
|
||||
logger.info(f"Retrying compliance check after {backoff_delay} seconds delay...")
|
||||
await asyncio.sleep(backoff_delay)
|
||||
backoff_delay *= backoff_factor
|
||||
|
||||
except Exception as e:
|
||||
# Non-retryable errors — fail immediately
|
||||
_log_structured_json(
|
||||
severity="ERROR",
|
||||
message=f"A2A audit failed with non-retryable error: {e!s}",
|
||||
event="non_retryable_error",
|
||||
case_id=case_id,
|
||||
error=str(e)
|
||||
)
|
||||
break
|
||||
|
||||
# --- FALLBACK GATEWAY (Fail Safe Transition to MANUAL_REVIEW) ---
|
||||
_log_structured_json(
|
||||
severity="ALERT",
|
||||
message="Resilience Gate triggered: Transitioning case to MANUAL_REVIEW checkpoint.",
|
||||
event="fallback_recovery_triggered",
|
||||
case_id=case_id
|
||||
)
|
||||
|
||||
# Update persistent state metrics safely
|
||||
state["current_step"] = ComplianceStep.MANUAL_REVIEW
|
||||
state["pending_signals"] = []
|
||||
|
||||
fallback_verdict = {
|
||||
"passed": False,
|
||||
"violations": [
|
||||
"SYSTEM TIMEOUT: External compliance service failed to respond within 30-second threshold.",
|
||||
"FAIL-SAFE ACTION: Document routed for legal manager manual verification."
|
||||
],
|
||||
"verdict_timestamp": time.strftime("%Y-%m-%d %H:%M:%S %Z")
|
||||
}
|
||||
state["compliance_verdict"] = fallback_verdict
|
||||
|
||||
# Append trace event logs
|
||||
state["trace_logs"] = state.get("trace_logs", [])
|
||||
state["trace_logs"].append({
|
||||
"span": "resilience_fallback_gate",
|
||||
"service": "python-extraction-agent",
|
||||
"duration_ms": 450,
|
||||
"status": "manual_review_routed"
|
||||
})
|
||||
|
||||
return {
|
||||
"status": "fallback_manual_review",
|
||||
"verdict": fallback_verdict,
|
||||
"message": "Resilience safety check triggered. Case successfully routed for manual legal review."
|
||||
}
|
||||
Reference in New Issue
Block a user