forked from lightspeed-core/lightspeed-stack
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathshields.py
More file actions
235 lines (194 loc) · 8.14 KB
/
Copy pathshields.py
File metadata and controls
235 lines (194 loc) · 8.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
"""Utility helpers for shield override validation and conversation persistence."""
from typing import Optional
from fastapi import HTTPException
from ogx_client import APIConnectionError, APIStatusError, AsyncOgxClient
from pydantic_ai.exceptions import AgentRunError
from configuration import AppConfig
from log import get_logger
from models.api.requests import QueryRequest
from models.api.responses.error import (
InternalServerErrorResponse,
NotFoundResponse,
ServiceUnavailableResponse,
UnprocessableEntityResponse,
)
from models.common.moderation import (
ShieldModerationPassed,
ShieldModerationResult,
)
from models.config import QuestionValidityConfig, RedactionConfig, ShieldConfiguration
from pydantic_ai_lightspeed.capabilities.base import AbstractSafetyCapability
from pydantic_ai_lightspeed.capabilities.question_validity._capability import (
QuestionValidity,
)
from pydantic_ai_lightspeed.capabilities.redaction._capability import (
PiiRedactionCapability,
)
from utils.agents.error_handler import map_agent_inference_error
logger = get_logger(__name__)
def validate_shield_ids_override(
query_request: QueryRequest, config: AppConfig
) -> None:
"""
Validate that shield_ids override is allowed by configuration.
If configuration disables shield_ids override
(config.customization.disable_shield_ids_override) and the incoming
query_request contains shield_ids, an HTTP 422 Unprocessable Entity
is raised instructing the client to remove the field.
Parameters:
----------
query_request: The incoming query payload; may contain shield_ids.
config: Application configuration which may include customization flags.
Raises:
------
HTTPException: If shield_ids override is disabled but shield_ids is provided.
"""
shield_ids_override_disabled = (
config.customization is not None
and config.customization.disable_shield_ids_override
)
if shield_ids_override_disabled and query_request.shield_ids is not None:
response = UnprocessableEntityResponse(
response="Shield IDs customization is disabled",
cause=(
"This instance does not support customizing shield IDs in the "
"query request (disable_shield_ids_override is set). Please remove the "
"shield_ids field from your request."
),
)
raise HTTPException(**response.model_dump())
async def run_shield_moderation_v2(
input_text: str,
shield_configs: list[ShieldConfiguration],
selected_shield_ids: Optional[list[str]] = None,
) -> ShieldModerationResult:
"""Run v2 shield moderation on input text.
Iterates through configured shields and runs moderation checks.
Parameters:
input_text: The text to moderate.
shield_configs: List of shield configurations to evaluate.
selected_shield_ids: Optional list of shield names to filter by.
Returns:
Result indicating if content was blocked or passed.
"""
selected_shield_configs = get_shields_for_request(
shield_configs, selected_shield_ids
)
for shield_config in selected_shield_configs:
shield = build_shield(shield_config)
try:
shield_result = await shield.run(input_text)
# APIConnectionError and APIStatusError from ogx should not be raised from model_request,
# because they will be caught inside AsyncOpenAI and transferred into openai's
# APIConnectionError. The openai's exceptions will further transferred into ModelHTTPError
# or ModelAPIError by _map_api_errors in OpenAIResponseModel.
except (AgentRunError, RuntimeError) as exc:
model_id = getattr(shield_config.config, "model_id", "unknown-shield-model")
response = map_agent_inference_error(exc, model_id)
raise HTTPException(**response.model_dump()) from exc
if shield_result.decision == "blocked":
return shield_result
return ShieldModerationPassed()
def build_shield(shield_config: ShieldConfiguration) -> AbstractSafetyCapability:
"""Build a safety capability instance from a shield configuration.
Parameters:
shield_config: The shield configuration to build from.
Returns:
The constructed safety capability.
"""
match shield_config.config:
case QuestionValidityConfig():
return QuestionValidity(shield_config.config)
case RedactionConfig():
return PiiRedactionCapability(shield_config.config)
async def run_shield_moderation(
_client: AsyncOgxClient,
_input_text: str,
_endpoint_path: str,
_shield_ids: Optional[list[str]] = None,
) -> ShieldModerationResult:
"""
Run shield moderation on input text.
Iterates through configured shields and runs moderation checks.
Raises HTTPException if shield model is not found.
Parameters:
----------
client: The Llama Stack client.
input_text: The text to moderate.
endpoint_path: The API endpoint path for metric labeling.
shield_ids: Optional list of shield IDs to use. If None, uses all shields.
If empty list, skips all shields.
Returns:
-------
ShieldModerationResult: Result indicating if content was blocked and the message.
Raises:
------
HTTPException: If shield's provider_resource_id is not configured or model not found.
"""
# Currently stubbed to always pass until LCS-owned input shields are wired.
return ShieldModerationPassed()
async def append_turn_to_conversation(
client: AsyncOgxClient,
conversation_id: str,
user_message: str,
assistant_message: str,
) -> None:
"""
Append a user/assistant turn to a conversation after shield violation.
Used to record the conversation turn when a shield blocks the request,
storing both the user's original message and the violation response.
Parameters:
----------
client: The Llama Stack client.
conversation_id: The Llama Stack conversation ID.
user_message: The user's input message.
assistant_message: The shield violation response message.
"""
try:
await client.conversations.items.create(
conversation_id,
items=[
{"type": "message", "role": "user", "content": user_message},
{"type": "message", "role": "assistant", "content": assistant_message},
],
)
except APIConnectionError as e:
error_response = ServiceUnavailableResponse(
backend_name="OGX",
cause=str(e),
)
raise HTTPException(**error_response.model_dump()) from e
except APIStatusError as e:
error_response = InternalServerErrorResponse.generic()
raise HTTPException(**error_response.model_dump()) from e
def get_shields_for_request(
shields: list[ShieldConfiguration],
shield_ids: Optional[list[str]] = None,
) -> list[ShieldConfiguration]:
"""Return configured shields, optionally filtered by request ``shield_ids``.
Shield identifiers in the request map to each shield's configured ``name``.
Args:
shields: Configured LCS shields.
shield_ids: Optional list of shield names. If ``None``, all ``shields``
are returned. An empty list skips all shields. Otherwise only
shields whose ``name`` is in this list are returned.
Returns:
list[ShieldConfiguration]: Shield configurations to run for this request.
Raises:
HTTPException: 404 if ``shield_ids`` is provided and any requested
shield name is not present in ``shields``.
"""
if shield_ids is None:
return list(shields)
if shield_ids == []:
return []
requested = set(shield_ids)
configured_ids = {shield.name for shield in shields}
missing = requested - configured_ids
if missing:
response = NotFoundResponse(
resource=f"Shield{'s' if len(missing) > 1 else ''}",
resource_id=", ".join(sorted(missing)),
)
raise HTTPException(**response.model_dump())
return [shield for shield in shields if shield.name in requested]