From c244b35e00384fdcaa2396c8464df501849b1c69 Mon Sep 17 00:00:00 2001 From: Jorge Juarez Date: Fri, 13 Mar 2026 00:24:51 +0000 Subject: [PATCH] feat_dev(guardrail): externalize labels and tighten censorship logic --- config.yaml | 8 ++++++ src/va_agent/config.py | 11 ++++++-- src/va_agent/governance.py | 6 ++--- src/va_agent/session.py | 55 ++++++++++++++++++++------------------ 4 files changed, 49 insertions(+), 31 deletions(-) diff --git a/config.yaml b/config.yaml index c77eefb..635c75d 100644 --- a/config.yaml +++ b/config.yaml @@ -13,6 +13,7 @@ mcp_audience: "https://ap01194-orq-cog-rag-connector-1007577023101.us-central1.r agent_name: VAia agent_model: gemini-2.5-flash + agent_instructions: | Eres VAia, el asistente virtual de VA en WhatsApp. VA es la opción digital de Banorte para los jóvenes. Fuiste creado por el equipo de inteligencia artifical de Banorte. Tu rol es resolver dudas sobre educación financiera y los productos/servicios de VA. Hablas como un amigo que sabe de finanzas: siempre vas directo al grano, con calidez y sin rodeos. @@ -50,6 +51,13 @@ agent_instructions: | El teléfono de centro de contacto de VA es: +52 1 55 5140 5655 +# Guardrail config +guardrail_censored_user_message: "[pregunta mala]" +guardrail_censored_model_response: "[respuesta de adversidad]" +guardrail_blocked_label: "[GUARDRAIL_BLOCKED]" +guardrail_passed_label: "[GUARDRAIL_PASSED]" +guardrail_error_label: "[GUARDRAIL_ERROR]" + guardrail_instruction: | Eres una capa de seguridad y protección de marca para VAia, el asistente virtual de VA en WhatsApp. VAia es un asistente de educación financiera y productos/servicios de VA (la opción digital de Banorte para jóvenes) diff --git a/src/va_agent/config.py b/src/va_agent/config.py index e869aff..49e24ea 100644 --- a/src/va_agent/config.py +++ b/src/va_agent/config.py @@ -21,9 +21,16 @@ class AgentSettings(BaseSettings): # Agent configuration agent_name: str - agent_instructions: str - guardrail_instruction: str agent_model: str + agent_instructions: str + + # Guardrail configuration + guardrail_censored_user_message: str + guardrail_censored_model_response: str + guardrail_blocked_label: str + guardrail_passed_label: str + guardrail_error_label: str + guardrail_instruction: str # Firestore configuration firestore_db: str diff --git a/src/va_agent/governance.py b/src/va_agent/governance.py index 87a9201..fb8ab26 100644 --- a/src/va_agent/governance.py +++ b/src/va_agent/governance.py @@ -171,19 +171,19 @@ class GovernancePlugin: if decision == "unsafe": callback_context.state["guardrail_blocked"] = True - callback_context.state["guardrail_message"] = "[GUARDRAIL_BLOCKED]" + callback_context.state["guardrail_message"] = settings.guardrail_blocked_label callback_context.state["guardrail_reasoning"] = reasoning return LlmResponse( content=Content(role="model", parts=[Part(text=blocking_response)]), usage_metadata=resp.usage_metadata or None, ) callback_context.state["guardrail_blocked"] = False - callback_context.state["guardrail_message"] = "[GUARDRAIL_PASSED]" + callback_context.state["guardrail_message"] = settings.guardrail_passed_label callback_context.state["guardrail_reasoning"] = reasoning except Exception: # Fail safe: block with a generic error response and mark the reason - callback_context.state["guardrail_message"] = "[GUARDRAIL_ERROR]" + callback_context.state["guardrail_message"] = settings.guardrail_error_label logger.exception("Guardrail check failed") return LlmResponse( content=Content( diff --git a/src/va_agent/session.py b/src/va_agent/session.py index dcf3910..bca3788 100644 --- a/src/va_agent/session.py +++ b/src/va_agent/session.py @@ -25,12 +25,13 @@ from google.cloud.firestore_v1.field_path import FieldPath from google.genai.types import Content, Part from .compaction import SessionCompactor +from .config import settings if TYPE_CHECKING: from google import genai from google.cloud.firestore_v1.async_client import AsyncClient -logger = logging.getLogger("google_adk." + __name__) +logger = logging.getLogger(__name__) class FirestoreSessionService(BaseSessionService): @@ -382,7 +383,7 @@ class FirestoreSessionService(BaseSessionService): # Determine if we need to censor this event (model response when guardrail blocked) should_censor_model = ( session.state.get("guardrail_blocked", False) - and event.author == app_name + and event.author != "user" and hasattr(event, "content") and event.content and event.content.parts @@ -396,34 +397,36 @@ class FirestoreSessionService(BaseSessionService): # Create a censored version of the model response event_to_save = copy.deepcopy(event) - event_to_save.content.parts[0].text = "[respuesta de adversidad]" + event_to_save.content.parts[0].text = settings.guardrail_censored_model_response event_data = event_to_save.model_dump(mode="json", exclude_none=True) # Also censor the previous user message in Firestore - # Find the last user event in the session (skip the current model event we just added) - for i in range(len(session.events) - 2, -1, -1): - prev_event = session.events[i] - if ( - prev_event.author == "user" - and prev_event.content - and prev_event.content.parts - ): - # Update this event in Firestore with censored content - censored_user_content = Content( - role="user", parts=[Part(text="[pregunta mala]")] + # Find the last user event in the session + prev_user_event = next( + ( + e + for e in reversed(session.events[:-1]) + if e.author == "user" and e.content and e.content.parts + ), + None, + ) + if prev_user_event: + # Update this event in Firestore with censored content + censored_user_content = Content( + role="user", + parts=[Part(text=settings.guardrail_censored_user_message)], + ) + await ( + self._events_col(app_name, user_id, session_id) + .document(prev_user_event.id) + .update( + { + "content": censored_user_content.model_dump( + mode="json", exclude_none=True + ) + } ) - await ( - self._events_col(app_name, user_id, session_id) - .document(prev_event.id) - .update( - { - "content": censored_user_content.model_dump( - mode="json", exclude_none=True - ) - } - ) - ) - break + ) else: event_data = event.model_dump(mode="json", exclude_none=True)