Skip to content

Commit 9892925

Browse files
authored
feat: support Lumen Moderate endpoint in LLM shield (#1026)
1 parent a983367 commit 9892925

1 file changed

Lines changed: 118 additions & 53 deletions

File tree

veadk/tools/builtin_tools/llm_shield.py

Lines changed: 118 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414
import json
1515
import os
16+
import time
1617
import requests
1718
from typing import Optional, List, Dict, Any, Union
1819
from volcenginesdkllmshield.models.llm_shield_sign import request_sign
@@ -82,7 +83,27 @@ def __init__(self, timeout: int = 50) -> None:
8283
107: "Computational Resource Consumption",
8384
}
8485

85-
def _request_llm_shield(self, message: str, role: str) -> Optional[str]:
86+
def _get_session_info(self, tool_context: Any) -> Dict[str, str]:
87+
"""Extract session id and run id from the tool context when available."""
88+
session_info = {"SessionID": "", "RunID": ""}
89+
try:
90+
session = getattr(tool_context, "session", None)
91+
session_id = getattr(session, "id", "") if session else ""
92+
run_id = getattr(tool_context, "invocation_id", "") or ""
93+
session_info = {"SessionID": session_id or "", "RunID": run_id}
94+
except Exception:
95+
logger.debug(
96+
"Failed to extract session info from tool context", exc_info=True
97+
)
98+
return session_info
99+
100+
def _request_llm_shield(
101+
self,
102+
message: str,
103+
role: str,
104+
hook_name: Optional[str] = None,
105+
session_info: Optional[Dict[str, str]] = None,
106+
) -> Optional[str]:
86107
"""
87108
Make a request to the LLM Shield service for content moderation.
88109
@@ -93,6 +114,8 @@ def _request_llm_shield(self, message: str, role: str) -> Optional[str]:
93114
Args:
94115
message (str): The content to be moderated
95116
role (str): The role of the message sender ("user" or "assistant")
117+
hook_name (str, optional): Hook name for the Lumen Moderate endpoint
118+
session_info (dict, optional): Session and run ids for the Lumen endpoint
96119
97120
Returns:
98121
Optional[str]: A blocking message if content violates policies,
@@ -102,64 +125,94 @@ def _request_llm_shield(self, message: str, role: str) -> Optional[str]:
102125
logger.error("LLM Shield app ID not configured")
103126
return None
104127

105-
body = {
106-
"Message": {
107-
"Role": role,
108-
"Content": message,
109-
"ContentType": 1,
110-
},
111-
"Scene": self.appid,
112-
}
113-
114-
body_json = json.dumps(body).encode("utf-8")
115-
116-
path = "/v2/moderate"
117-
action = "Moderate"
118-
version = "2025-08-31"
119-
120-
# Check if using API key authentication
121-
logger.debug(f"API key configured: {bool(self.api_key)}")
122-
if self.api_key and self.api_key != "":
123-
logger.debug("Using API key authentication (no AK/SK signature)")
124-
# Use API key authentication only - match curl command headers exactly
125-
signed_header = {
128+
if "/OpenTOP/V1/Lumen/Moderate" in self.url:
129+
logger.debug("Using Lumen Moderate endpoint")
130+
body = {
131+
"Message": {
132+
"Role": role,
133+
"MultiPart": [
134+
{"Content": message, "ContentType": 1},
135+
],
136+
},
137+
"History": [],
138+
"HookName": hook_name or "before_tool_call",
139+
"IsGroup": False,
140+
"LocalTimestamp": int(time.time()),
141+
"LocalTimestampMS": int(time.time() * 1000),
142+
"SessionInfo": session_info or {"SessionID": "", "RunID": ""},
143+
}
144+
headers = {
126145
"Content-Type": "application/json",
127-
"x-api-key": self.api_key,
146+
"X-Top-Account-Id": "30000001",
147+
"X-Top-Region": "cn-private",
148+
"X-endpoint-Id": self.appid,
149+
"X-endpoint-api-Key": self.api_key,
128150
}
151+
body_json = json.dumps(body).encode("utf-8")
152+
request_url = self.url
153+
params = None
129154
else:
130-
logger.debug("Using AK/SK signature authentication")
131-
# Use AK/SK signature authentication
132-
ak = os.getenv("VOLCENGINE_ACCESS_KEY")
133-
sk = os.getenv("VOLCENGINE_SECRET_KEY")
134-
session_token = ""
135-
if not (ak and sk):
136-
logger.debug("Get AK/SK from environment variables failed.")
137-
credential = get_credential_from_vefaas_iam()
138-
ak = credential.access_key_id
139-
sk = credential.secret_access_key
140-
session_token = credential.session_token
155+
body = {
156+
"Message": {
157+
"Role": role,
158+
"Content": message,
159+
"ContentType": 1,
160+
},
161+
"Scene": self.appid,
162+
}
163+
body_json = json.dumps(body).encode("utf-8")
164+
165+
path = "/v2/moderate"
166+
action = "Moderate"
167+
version = "2025-08-31"
168+
169+
# Check if using API key authentication
170+
logger.debug(f"API key configured: {bool(self.api_key)}")
171+
if self.api_key and self.api_key != "":
172+
logger.debug("Using API key authentication (no AK/SK signature)")
173+
# Use API key authentication only - match curl command headers exactly
174+
signed_header = {
175+
"Content-Type": "application/json",
176+
"x-api-key": self.api_key,
177+
}
141178
else:
142-
logger.debug("Successfully get AK/SK from environment variables.")
179+
logger.debug("Using AK/SK signature authentication")
180+
# Use AK/SK signature authentication
181+
ak = os.getenv("VOLCENGINE_ACCESS_KEY")
182+
sk = os.getenv("VOLCENGINE_SECRET_KEY")
183+
session_token = ""
184+
if not (ak and sk):
185+
logger.debug("Get AK/SK from environment variables failed.")
186+
credential = get_credential_from_vefaas_iam()
187+
ak = credential.access_key_id
188+
sk = credential.secret_access_key
189+
session_token = credential.session_token
190+
else:
191+
logger.debug("Successfully get AK/SK from environment variables.")
192+
193+
header = {"X-Security-Token": session_token}
194+
signed_header = request_sign(
195+
header, ak, sk, self.region, self.url, path, action, body_json
196+
)
143197

144-
header = {"X-Security-Token": session_token}
145-
signed_header = request_sign(
146-
header, ak, sk, self.region, self.url, path, action, body_json
147-
)
198+
signed_header.update(
199+
{
200+
"Content-Type": "application/json",
201+
"X-Top-Service": "llmshield",
202+
"X-Top-Region": self.region,
203+
}
204+
)
148205

149-
signed_header.update(
150-
{
151-
"Content-Type": "application/json",
152-
"X-Top-Service": "llmshield",
153-
"X-Top-Region": self.region,
154-
}
155-
)
206+
headers = signed_header
207+
request_url = self.url + path
208+
params = {"Action": action, "Version": version}
156209

157210
try:
158211
response = requests.post(
159-
self.url + path,
160-
headers=signed_header,
212+
request_url,
213+
headers=headers,
161214
data=body_json,
162-
params={"Action": action, "Version": version},
215+
params=params,
163216
timeout=self.timeout,
164217
)
165218

@@ -261,7 +314,9 @@ def before_model_callback(
261314
if not last_user_message:
262315
return None
263316

264-
response = self._request_llm_shield(message=last_user_message, role="user")
317+
response = self._request_llm_shield(
318+
message=last_user_message, role="user", hook_name="before_model"
319+
)
265320
if response:
266321
logger.debug("LLM Shield triggered in before_model_callback.")
267322
return LlmResponse(
@@ -308,7 +363,7 @@ def after_model_callback(
308363
return None
309364

310365
response = self._request_llm_shield(
311-
message=last_model_message, role="assistant"
366+
message=last_model_message, role="assistant", hook_name="after_model"
312367
)
313368
if response:
314369
logger.debug("LLM Shield triggered in after_model_callback.")
@@ -347,7 +402,12 @@ def before_tool_callback(
347402
args_list.append(f"{key}: {value}")
348403

349404
message = "\n".join(args_list)
350-
response = self._request_llm_shield(message=message, role="user")
405+
response = self._request_llm_shield(
406+
message=message,
407+
role="user",
408+
hook_name="before_tool_call",
409+
session_info=self._get_session_info(tool_context),
410+
)
351411
if response:
352412
logger.debug("LLM Shield triggered in before_tool_callback.")
353413
return {"result": response}
@@ -389,7 +449,12 @@ def after_tool_callback(
389449
for item in tool_response:
390450
message += f"{item}\n"
391451

392-
response = self._request_llm_shield(message=message, role="assistant")
452+
response = self._request_llm_shield(
453+
message=message,
454+
role="assistant",
455+
hook_name="after_tool_call",
456+
session_info=self._get_session_info(tool_context),
457+
)
393458
if response:
394459
logger.debug("LLM Shield triggered in after_tool_callback.")
395460
return {"result": response}

0 commit comments

Comments
 (0)