Compare commits

..

7 Commits

4 changed files with 87 additions and 1 deletions

View File

@@ -13,6 +13,7 @@ dependencies = [
"google-cloud-firestore>=2.23.0", "google-cloud-firestore>=2.23.0",
"pydantic-settings[yaml]>=2.13.1", "pydantic-settings[yaml]>=2.13.1",
"google-auth>=2.34.0", "google-auth>=2.34.0",
"google-genai>=1.64.0",
] ]
[build-system] [build-system]

View File

@@ -15,6 +15,7 @@ from va_agent.config import settings
from va_agent.dynamic_instruction import provide_dynamic_instruction from va_agent.dynamic_instruction import provide_dynamic_instruction
from va_agent.notifications import NotificationService from va_agent.notifications import NotificationService
from va_agent.session import FirestoreSessionService from va_agent.session import FirestoreSessionService
from va_agent.governance import GovernancePlugin
# MCP Toolset for RAG knowledge search # MCP Toolset for RAG knowledge search
toolset = McpToolset( toolset = McpToolset(
@@ -22,6 +23,7 @@ toolset = McpToolset(
header_provider=auth_headers_provider, header_provider=auth_headers_provider,
) )
# Shared Firestore client for session service and notifications # Shared Firestore client for session service and notifications
firestore_db = AsyncClient(database=settings.firestore_db) firestore_db = AsyncClient(database=settings.firestore_db)
@@ -40,6 +42,7 @@ notification_service = NotificationService(
) )
# Agent with static and dynamic instructions # Agent with static and dynamic instructions
governance = GovernancePlugin()
agent = Agent( agent = Agent(
model=settings.agent_model, model=settings.agent_model,
name=settings.agent_name, name=settings.agent_name,
@@ -47,8 +50,9 @@ agent = Agent(
role="user", role="user",
parts=[Part(text=settings.agent_instructions)], parts=[Part(text=settings.agent_instructions)],
), ),
instruction=partial(provide_dynamic_instruction, notification_service), instruction=settings.agent_instructions,
tools=[toolset], tools=[toolset],
after_model_callback=governance.after_model_callback,
) )
# Runner # Runner

View File

@@ -0,0 +1,79 @@
"""GovernancePlugin: Guardrails for VAia, the virtual assistant for VA."""
import logging
import re
from google.adk.agents.callback_context import CallbackContext
from google.adk.models import LlmResponse
logger = logging.getLogger(__name__)
FORBIDDEN_EMOJIS = [
"🥵","🔪","🎰","🎲","🃏","😤","🤬","😡","😠","🩸","🧨","🪓","☠️","💀",
"💣","🔫","👗","💦","🍑","🍆","👄","👅","🫦","💩","⚖️","⚔️","✝️","🕍",
"🕌","","🍻","🍸","🥃","🍷","🍺","🚬","👹","👺","👿","😈","🤡","🧙",
"🧙‍♀️", "🧙‍♂️", "🧛", "🧛‍♀️", "🧛‍♂️", "🔞","🧿","💊", "💏"
]
class GovernancePlugin:
"""Guardrail executor for VAia requests as a Agent engine callbacks."""
def __init__(self) -> None:
"""Initialize guardrail model (structured output), prompt and emojis patterns."""
self._combined_pattern = self._get_combined_pattern()
def _get_combined_pattern(self):
person_pattern = r"(?:🧑|👩|👨)"
tone_pattern = r"[\U0001F3FB-\U0001F3FF]?"
# Unique pattern that combines all forbidden emojis, including complex ones with skin tones
combined_pattern = re.compile(
rf"{person_pattern}{tone_pattern}\u200d❤?\u200d💋\u200d{person_pattern}{tone_pattern}" # kiss
rf"|{person_pattern}{tone_pattern}\u200d❤?\u200d{person_pattern}{tone_pattern}" # lovers
rf"|🖕{tone_pattern}" # middle finger with all skin tone variations
rf"|{'|'.join(map(re.escape, sorted(FORBIDDEN_EMOJIS, key=len, reverse=True)))}" # simple emojis
rf"|\u200d|\uFE0F" # residual ZWJ and variation selectors
)
return combined_pattern
def _remove_emojis(self, text: str) -> tuple[str, list[str]]:
removed = self._combined_pattern.findall(text)
text = self._combined_pattern.sub("", text)
return text.strip(), removed
def after_model_callback(
self,
callback_context: CallbackContext | None = None,
llm_response: LlmResponse | None = None,
) -> None:
"""Guardrail post-processing.
Remove forbidden emojis from the model response.
"""
try:
text_out = ""
if llm_response and llm_response.content:
content = llm_response.content
parts = getattr(content, "parts", None)
if parts:
part = parts[0]
text_value = getattr(part, "text", "")
if isinstance(text_value, str):
text_out = text_value
if text_out:
new_text, deleted = self._remove_emojis(text_out)
if llm_response and llm_response.content and llm_response.content.parts:
llm_response.content.parts[0].text = new_text
if deleted:
if callback_context:
callback_context.state["removed_emojis"] = deleted
logger.warning(
"Removed forbidden emojis from response: %s",
deleted,
)
except Exception:
logger.exception("Error in after_model_callback")

2
uv.lock generated
View File

@@ -1924,6 +1924,7 @@ dependencies = [
{ name = "google-adk" }, { name = "google-adk" },
{ name = "google-auth" }, { name = "google-auth" },
{ name = "google-cloud-firestore" }, { name = "google-cloud-firestore" },
{ name = "google-genai" },
{ name = "pydantic-settings", extra = ["yaml"] }, { name = "pydantic-settings", extra = ["yaml"] },
] ]
@@ -1941,6 +1942,7 @@ requires-dist = [
{ name = "google-adk", specifier = ">=1.14.1" }, { name = "google-adk", specifier = ">=1.14.1" },
{ name = "google-auth", specifier = ">=2.34.0" }, { name = "google-auth", specifier = ">=2.34.0" },
{ name = "google-cloud-firestore", specifier = ">=2.23.0" }, { name = "google-cloud-firestore", specifier = ">=2.23.0" },
{ name = "google-genai", specifier = ">=1.64.0" },
{ name = "pydantic-settings", extras = ["yaml"], specifier = ">=2.13.1" }, { name = "pydantic-settings", extras = ["yaml"], specifier = ">=2.13.1" },
] ]