lexsi-sdk 0.1.16__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lexsi_sdk/__init__.py +5 -0
- lexsi_sdk/client/__init__.py +0 -0
- lexsi_sdk/client/client.py +176 -0
- lexsi_sdk/common/__init__.py +0 -0
- lexsi_sdk/common/config/.env.prod +3 -0
- lexsi_sdk/common/constants.py +143 -0
- lexsi_sdk/common/enums.py +8 -0
- lexsi_sdk/common/environment.py +49 -0
- lexsi_sdk/common/monitoring.py +81 -0
- lexsi_sdk/common/trigger.py +75 -0
- lexsi_sdk/common/types.py +122 -0
- lexsi_sdk/common/utils.py +93 -0
- lexsi_sdk/common/validation.py +110 -0
- lexsi_sdk/common/xai_uris.py +197 -0
- lexsi_sdk/core/__init__.py +0 -0
- lexsi_sdk/core/agent.py +62 -0
- lexsi_sdk/core/alert.py +56 -0
- lexsi_sdk/core/case.py +618 -0
- lexsi_sdk/core/dashboard.py +131 -0
- lexsi_sdk/core/guardrails/__init__.py +0 -0
- lexsi_sdk/core/guardrails/guard_template.py +299 -0
- lexsi_sdk/core/guardrails/guardrail_autogen.py +554 -0
- lexsi_sdk/core/guardrails/guardrails_langgraph.py +525 -0
- lexsi_sdk/core/guardrails/guardrails_openai.py +541 -0
- lexsi_sdk/core/guardrails/openai_runner.py +1328 -0
- lexsi_sdk/core/model_summary.py +110 -0
- lexsi_sdk/core/organization.py +549 -0
- lexsi_sdk/core/project.py +5131 -0
- lexsi_sdk/core/synthetic.py +387 -0
- lexsi_sdk/core/text.py +595 -0
- lexsi_sdk/core/tracer.py +208 -0
- lexsi_sdk/core/utils.py +36 -0
- lexsi_sdk/core/workspace.py +325 -0
- lexsi_sdk/core/wrapper.py +766 -0
- lexsi_sdk/core/xai.py +306 -0
- lexsi_sdk/version.py +34 -0
- lexsi_sdk-0.1.16.dist-info/METADATA +100 -0
- lexsi_sdk-0.1.16.dist-info/RECORD +40 -0
- lexsi_sdk-0.1.16.dist-info/WHEEL +5 -0
- lexsi_sdk-0.1.16.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Dict, Any, Optional, Callable, List, Union
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from autogen import ConversableAgent, UserProxyAgent
|
|
5
|
+
from autogen_agentchat.agents import AssistantAgent
|
|
6
|
+
import time
|
|
7
|
+
from lexsi_sdk.core.project import Project
|
|
8
|
+
from lexsi_sdk.common.xai_uris import (
|
|
9
|
+
RUN_GUARDRAILS_URI,
|
|
10
|
+
)
|
|
11
|
+
from opentelemetry import trace, context
|
|
12
|
+
from .guard_template import Guard
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class GuardrailRunResult(Dict[str, Any]):
|
|
16
|
+
"""Dictionary describing the outcome of a guardrail execution."""
|
|
17
|
+
pass # TypedDict not needed for runtime, but can add if desired
|
|
18
|
+
|
|
19
|
+
class GuardrailSupervisor:
|
|
20
|
+
"""Pluggable class to monitor and control agent behavior using API-based guardrails."""
|
|
21
|
+
def __init__(self,
|
|
22
|
+
guards: Union[List[Dict[str, Any]], Dict[str, Any], None] = None,
|
|
23
|
+
apply_to: str = 'both',
|
|
24
|
+
action: str = "block",
|
|
25
|
+
project: Optional[Project] = None,
|
|
26
|
+
llm: Optional[Any] = None,
|
|
27
|
+
):
|
|
28
|
+
"""Initialize supervisor with guard specifications and runtime context."""
|
|
29
|
+
if apply_to not in ['input', 'output', 'both']:
|
|
30
|
+
raise ValueError("apply_to must be one of 'input', 'output', 'both'")
|
|
31
|
+
self.apply_to = apply_to
|
|
32
|
+
if isinstance(guards, dict):
|
|
33
|
+
guards = [guards]
|
|
34
|
+
self.guards = guards or []
|
|
35
|
+
if action not in ['block', 'retry', 'warn']:
|
|
36
|
+
raise ValueError("action must be one of 'block', 'retry', 'warn'")
|
|
37
|
+
self.action = action
|
|
38
|
+
if project is not None:
|
|
39
|
+
self.api_client = project.api_client
|
|
40
|
+
self.project_name = project.project_name
|
|
41
|
+
self.llm = llm
|
|
42
|
+
self.max_retries = 1
|
|
43
|
+
self.retry_delay = 1.0
|
|
44
|
+
self.tracer = trace.get_tracer("autogen-app") # Standardized tracer name
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _process_content(
|
|
48
|
+
self,
|
|
49
|
+
content: str,
|
|
50
|
+
agent_id: str,
|
|
51
|
+
content_type: str,
|
|
52
|
+
action: str,
|
|
53
|
+
guards: List[Dict[str, Any]],
|
|
54
|
+
) -> str:
|
|
55
|
+
"""Run configured guards sequentially over provided content."""
|
|
56
|
+
if not guards:
|
|
57
|
+
return content
|
|
58
|
+
|
|
59
|
+
current_content = content
|
|
60
|
+
for guard in guards:
|
|
61
|
+
if isinstance(guard, str):
|
|
62
|
+
guard_spec: Dict[str, Any] = {"name": guard}
|
|
63
|
+
else:
|
|
64
|
+
guard_spec = dict(guard)
|
|
65
|
+
|
|
66
|
+
current_content = self._apply_guardrail_with_retry(
|
|
67
|
+
content=current_content,
|
|
68
|
+
guard_spec=guard_spec,
|
|
69
|
+
agent_id=agent_id,
|
|
70
|
+
content_type=content_type,
|
|
71
|
+
action=action,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
return current_content
|
|
75
|
+
|
|
76
|
+
def _apply_guardrail_with_retry(
|
|
77
|
+
self,
|
|
78
|
+
content: str,
|
|
79
|
+
guard_spec: Dict[str, Any],
|
|
80
|
+
agent_id: str,
|
|
81
|
+
content_type: str,
|
|
82
|
+
action: str,
|
|
83
|
+
) -> str:
|
|
84
|
+
"""Apply a guardrail with optional retries and sanitization."""
|
|
85
|
+
current_content = content
|
|
86
|
+
retry_count = 0
|
|
87
|
+
|
|
88
|
+
if action == "retry":
|
|
89
|
+
while retry_count <= self.max_retries:
|
|
90
|
+
run_result = self._call_run_guardrail(current_content, guard_spec, content_type)
|
|
91
|
+
validation_passed = bool(run_result.get("validation_passed", True))
|
|
92
|
+
detected_issue = not validation_passed or not run_result.get("success", True)
|
|
93
|
+
|
|
94
|
+
if detected_issue and self.llm is not None and retry_count < self.max_retries:
|
|
95
|
+
retry_count += 1
|
|
96
|
+
time.sleep(self.retry_delay)
|
|
97
|
+
continue
|
|
98
|
+
else:
|
|
99
|
+
return self._handle_action(
|
|
100
|
+
original=current_content,
|
|
101
|
+
run_result=run_result,
|
|
102
|
+
action=f"retry_{retry_count}" if retry_count > 0 else action,
|
|
103
|
+
agent_id=agent_id,
|
|
104
|
+
content_type=content_type,
|
|
105
|
+
guard_name=guard_spec.get("name", "unknown"),
|
|
106
|
+
)
|
|
107
|
+
else:
|
|
108
|
+
run_result = self._call_run_guardrail(current_content, guard_spec, content_type)
|
|
109
|
+
return self._handle_action(
|
|
110
|
+
original=current_content,
|
|
111
|
+
run_result=run_result,
|
|
112
|
+
action=action,
|
|
113
|
+
agent_id=agent_id,
|
|
114
|
+
content_type=content_type,
|
|
115
|
+
guard_name=guard_spec.get("name", "unknown"),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def _call_run_guardrail(self, input_data: str, guard: Dict[str, Any], content_type: str) -> GuardrailRunResult:
|
|
119
|
+
"""Call guardrail API for a single guard specification."""
|
|
120
|
+
start_time = datetime.now()
|
|
121
|
+
try:
|
|
122
|
+
body = {"input_data": input_data, "guard": guard}
|
|
123
|
+
data = self.api_client.post(RUN_GUARDRAILS_URI, body)
|
|
124
|
+
# print(data , "api ran")
|
|
125
|
+
end_time = datetime.now()
|
|
126
|
+
details = data.get("details", {}) if isinstance(data, dict) else {}
|
|
127
|
+
result: GuardrailRunResult = {
|
|
128
|
+
"success": bool(data.get("success", False)) if isinstance(data, dict) else False,
|
|
129
|
+
"details": details if isinstance(details, dict) else {},
|
|
130
|
+
"start_time": start_time.isoformat(),
|
|
131
|
+
"end_time": end_time.isoformat(),
|
|
132
|
+
}
|
|
133
|
+
if "duration" not in details:
|
|
134
|
+
result["duration"] = (end_time - start_time).total_seconds()
|
|
135
|
+
if isinstance(details, dict):
|
|
136
|
+
result.update({k: v for k, v in details.items() if k in [
|
|
137
|
+
"validated_output", "validation_passed", "sanitized_output", "duration", "latency"
|
|
138
|
+
]})
|
|
139
|
+
result["retry_count"] = 0
|
|
140
|
+
result["max_retries"] = self.max_retries
|
|
141
|
+
result["response"] = data
|
|
142
|
+
result["input"] = input_data
|
|
143
|
+
return result
|
|
144
|
+
except Exception as exc:
|
|
145
|
+
end_time = datetime.now()
|
|
146
|
+
result = {
|
|
147
|
+
"success": False,
|
|
148
|
+
"details": {},
|
|
149
|
+
"start_time": start_time.isoformat(),
|
|
150
|
+
"end_time": end_time.isoformat(),
|
|
151
|
+
"duration": (end_time - start_time).total_seconds(),
|
|
152
|
+
"error": str(exc)
|
|
153
|
+
}
|
|
154
|
+
return result
|
|
155
|
+
|
|
156
|
+
def _handle_action(
|
|
157
|
+
self,
|
|
158
|
+
original: str,
|
|
159
|
+
run_result: GuardrailRunResult,
|
|
160
|
+
action: str,
|
|
161
|
+
agent_id: str,
|
|
162
|
+
content_type: str,
|
|
163
|
+
guard_name: str,
|
|
164
|
+
) -> str:
|
|
165
|
+
"""Handle guardrail outcome according to configured action."""
|
|
166
|
+
validation_passed = bool(run_result.get("validation_passed", True))
|
|
167
|
+
detected_issue = not validation_passed or not run_result.get("success", True)
|
|
168
|
+
sanitized_output = run_result.get("sanitized_output")
|
|
169
|
+
|
|
170
|
+
status = "passed" if not detected_issue else "failed"
|
|
171
|
+
self._log_event(
|
|
172
|
+
agent_id=agent_id,
|
|
173
|
+
stage=content_type,
|
|
174
|
+
data=original,
|
|
175
|
+
status=status,
|
|
176
|
+
error=run_result.get("error", ""),
|
|
177
|
+
details={
|
|
178
|
+
"guard": guard_name,
|
|
179
|
+
"action": action,
|
|
180
|
+
"detected": detected_issue,
|
|
181
|
+
"duration": run_result.get("duration", 0.0),
|
|
182
|
+
"input": self._safe_str(run_result.get("input")),
|
|
183
|
+
"output": self._safe_str(run_result.get("response")),
|
|
184
|
+
}
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
if detected_issue:
|
|
188
|
+
on_fail_action = action
|
|
189
|
+
if on_fail_action == "block":
|
|
190
|
+
raise ValueError(f"Guardrail '{guard_name}' detected an issue in {content_type}. Operation blocked.")
|
|
191
|
+
elif "retry" in on_fail_action:
|
|
192
|
+
return original # Return last content (would be sanitized if implemented)
|
|
193
|
+
else: # warn
|
|
194
|
+
return original
|
|
195
|
+
return original
|
|
196
|
+
|
|
197
|
+
@staticmethod
|
|
198
|
+
def _safe_str(value: Any) -> str:
|
|
199
|
+
"""Safely stringify values for logging/telemetry."""
|
|
200
|
+
try:
|
|
201
|
+
if isinstance(value, (str, int, float, bool)) or value is None:
|
|
202
|
+
s = str(value)
|
|
203
|
+
return s
|
|
204
|
+
if hasattr(value, "content"):
|
|
205
|
+
s = str(getattr(value, "content", ""))
|
|
206
|
+
return s
|
|
207
|
+
|
|
208
|
+
if isinstance(value, (list, tuple)):
|
|
209
|
+
parts = []
|
|
210
|
+
for item in value:
|
|
211
|
+
parts.append(Guard._safe_str(item) if hasattr(Guard, "_safe_str") else str(item))
|
|
212
|
+
s = ", ".join(parts)
|
|
213
|
+
return s
|
|
214
|
+
|
|
215
|
+
if isinstance(value, dict):
|
|
216
|
+
safe_dict: Dict[str, Any] = {}
|
|
217
|
+
for k, v in value.items():
|
|
218
|
+
key = str(k)
|
|
219
|
+
if isinstance(v, (str, int, float, bool)) or v is None:
|
|
220
|
+
safe_dict[key] = v
|
|
221
|
+
elif hasattr(v, "content"):
|
|
222
|
+
safe_dict[key] = str(getattr(v, "content", ""))
|
|
223
|
+
else:
|
|
224
|
+
safe_dict[key] = str(v)
|
|
225
|
+
s = json.dumps(safe_dict, ensure_ascii=False)
|
|
226
|
+
return s
|
|
227
|
+
s = str(value)
|
|
228
|
+
return s
|
|
229
|
+
except Exception:
|
|
230
|
+
return "<unserializable>"
|
|
231
|
+
|
|
232
|
+
def instrument_agents(self, agents: List[Union[ConversableAgent, AssistantAgent]]) -> List[Union[ConversableAgent, AssistantAgent]]:
|
|
233
|
+
"""
|
|
234
|
+
Instruments a list of agents to apply guardrails.
|
|
235
|
+
|
|
236
|
+
This method iterates through a list of agents and applies the appropriate
|
|
237
|
+
instrumentation to intercept their message generation or run methods.
|
|
238
|
+
It handles different agent types like `AssistantAgent` and `ConversableAgent`.
|
|
239
|
+
|
|
240
|
+
:param agents: List of agents to be instrumented.
|
|
241
|
+
:return: The list of instrumented agents.
|
|
242
|
+
"""
|
|
243
|
+
for agent in agents:
|
|
244
|
+
# It's important to check for the more specific subclass first.
|
|
245
|
+
if isinstance(agent, AssistantAgent):
|
|
246
|
+
self.instrument_agent(agent)
|
|
247
|
+
return agents
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
async def _execute_guarded_run(self, agent, original_run, args, kwargs, current_context):
|
|
251
|
+
"""Execute the guarded run with proper context for guardrails."""
|
|
252
|
+
# Extract task argument and ensure proper argument handling
|
|
253
|
+
task = kwargs.get('task', None)
|
|
254
|
+
if task is None and len(args) > 0:
|
|
255
|
+
task = args[0]
|
|
256
|
+
|
|
257
|
+
# Process input for guardrails - make them direct children of current agent span
|
|
258
|
+
if self.apply_to in ['input', 'both'] and task:
|
|
259
|
+
request_content = self._extract_task_content(task)
|
|
260
|
+
if request_content:
|
|
261
|
+
# Set input content attribute on the current agent span
|
|
262
|
+
current_span = trace.get_current_span()
|
|
263
|
+
if current_span.is_recording():
|
|
264
|
+
current_span.set_attribute("guardrail.input_content", self._safe_str(request_content))
|
|
265
|
+
|
|
266
|
+
self._apply_input_guardrails(
|
|
267
|
+
request_content, agent.name, current_context
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# Call original run method with proper argument handling
|
|
271
|
+
try:
|
|
272
|
+
reply = await original_run(*args, **kwargs)
|
|
273
|
+
|
|
274
|
+
except Exception as e:
|
|
275
|
+
raise ValueError(f"Error generating response: {str(e)}")
|
|
276
|
+
|
|
277
|
+
# Process output through guardrails - make them direct children of current agent span
|
|
278
|
+
if self.apply_to in ['output', 'both'] and reply:
|
|
279
|
+
response_content = self._extract_response_content(reply)
|
|
280
|
+
if response_content:
|
|
281
|
+
# Set output content attribute on the current agent span
|
|
282
|
+
self._apply_output_guardrails(
|
|
283
|
+
response_content, agent.name, current_context
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# Ensure reply has required format for AutoGen consistency
|
|
287
|
+
reply = self._format_reply(reply, agent)
|
|
288
|
+
|
|
289
|
+
return reply
|
|
290
|
+
|
|
291
|
+
def instrument_agent(self, agent) -> None:
|
|
292
|
+
"""Wrap AssistantAgent to intercept run method for guardrails (AutoGen 0.4+)."""
|
|
293
|
+
original_run = agent.run
|
|
294
|
+
# print(f"Guardrail running on {agent.__class__.__name__}")
|
|
295
|
+
|
|
296
|
+
async def wrapped_run(*args, **kwargs):
|
|
297
|
+
"""Execute the agent run while applying guardrail checks."""
|
|
298
|
+
# print("Guardrail intercepted run method")
|
|
299
|
+
|
|
300
|
+
# Get the current span and context
|
|
301
|
+
current_span = trace.get_current_span()
|
|
302
|
+
current_context = context.get_current()
|
|
303
|
+
|
|
304
|
+
# If no parent span is active, start one for the agent run
|
|
305
|
+
if not current_span.is_recording():
|
|
306
|
+
with self.tracer.start_as_current_span(f"{agent.name}_run") as agent_span:
|
|
307
|
+
# Update current_context to include the new parent
|
|
308
|
+
current_context = context.get_current()
|
|
309
|
+
return await self._execute_guarded_run(agent, original_run, args, kwargs, current_context)
|
|
310
|
+
else:
|
|
311
|
+
return await self._execute_guarded_run(agent, original_run, args, kwargs, current_context)
|
|
312
|
+
|
|
313
|
+
# Replace the run method with proper binding
|
|
314
|
+
agent.run = wrapped_run
|
|
315
|
+
return agent
|
|
316
|
+
|
|
317
|
+
def _extract_task_content(self, task) -> str:
|
|
318
|
+
"""Extract content from task parameter for processing."""
|
|
319
|
+
if isinstance(task, str):
|
|
320
|
+
return task
|
|
321
|
+
elif isinstance(task, list):
|
|
322
|
+
# Handle list of messages/tasks
|
|
323
|
+
content_parts = []
|
|
324
|
+
for item in task:
|
|
325
|
+
if isinstance(item, dict):
|
|
326
|
+
# Handle message dict format
|
|
327
|
+
if item.get('role') == 'user':
|
|
328
|
+
content_parts.append(item.get('content', ''))
|
|
329
|
+
elif 'content' in item:
|
|
330
|
+
content_parts.append(item.get('content', ''))
|
|
331
|
+
else:
|
|
332
|
+
content_parts.append(str(item))
|
|
333
|
+
elif hasattr(item, 'content'):
|
|
334
|
+
content_parts.append(str(item.content))
|
|
335
|
+
elif hasattr(item, 'role') and hasattr(item, 'content'):
|
|
336
|
+
if item.role == 'user':
|
|
337
|
+
content_parts.append(str(item.content))
|
|
338
|
+
else:
|
|
339
|
+
content_parts.append(str(item))
|
|
340
|
+
return ' '.join(filter(None, content_parts))
|
|
341
|
+
elif isinstance(task, dict):
|
|
342
|
+
# Handle single message dict
|
|
343
|
+
return task.get('content', str(task))
|
|
344
|
+
elif hasattr(task, 'content'):
|
|
345
|
+
return str(task.content)
|
|
346
|
+
else:
|
|
347
|
+
return str(task)
|
|
348
|
+
|
|
349
|
+
def _extract_response_content(self, reply) -> str:
|
|
350
|
+
"""Extract content from agent response for guardrail processing."""
|
|
351
|
+
if isinstance(reply, str):
|
|
352
|
+
return reply
|
|
353
|
+
elif isinstance(reply, dict):
|
|
354
|
+
return reply.get("content", "")
|
|
355
|
+
elif hasattr(reply, "content"):
|
|
356
|
+
return str(reply.content)
|
|
357
|
+
elif hasattr(reply, "text"):
|
|
358
|
+
return str(reply.text)
|
|
359
|
+
else:
|
|
360
|
+
return str(reply)
|
|
361
|
+
|
|
362
|
+
def _apply_input_guardrails(self, content: str, agent_name: str, ctx) -> None:
|
|
363
|
+
"""Apply input guardrails with telemetry tracking as direct children of agent span."""
|
|
364
|
+
for guard in self.guards:
|
|
365
|
+
guard_name = guard.get("name", "unknown")
|
|
366
|
+
|
|
367
|
+
# Create guardrail span as direct child of the current agent execution span
|
|
368
|
+
with self.tracer.start_as_current_span(f"guardrail: {guard_name}", context=ctx) as guard_span:
|
|
369
|
+
# Set comprehensive attributes linking this guardrail to the specific agent
|
|
370
|
+
guard_span.set_attribute("component", agent_name)
|
|
371
|
+
guard_span.set_attribute("guard", guard_name)
|
|
372
|
+
guard_span.set_attribute("content_type", "input")
|
|
373
|
+
|
|
374
|
+
try:
|
|
375
|
+
start_time = datetime.now()
|
|
376
|
+
run_result = self._call_run_guardrail(content, guard, "input")
|
|
377
|
+
end_time = datetime.now()
|
|
378
|
+
|
|
379
|
+
# Set comprehensive telemetry attributes
|
|
380
|
+
guard_span.set_attribute("input.value", self._safe_str(content))
|
|
381
|
+
guard_span.set_attribute("output.value",
|
|
382
|
+
self._safe_str(run_result.get("response", "")))
|
|
383
|
+
guard_span.set_attribute("start_time", start_time.isoformat())
|
|
384
|
+
guard_span.set_attribute("end_time", end_time.isoformat())
|
|
385
|
+
guard_span.set_attribute("duration",
|
|
386
|
+
(end_time - start_time).total_seconds())
|
|
387
|
+
|
|
388
|
+
# Check validation results
|
|
389
|
+
validation_passed = bool(run_result.get("validation_passed", True))
|
|
390
|
+
success = bool(run_result.get("success", True))
|
|
391
|
+
detected_issue = not validation_passed or not success
|
|
392
|
+
|
|
393
|
+
guard_span.set_attribute("detected", detected_issue)
|
|
394
|
+
|
|
395
|
+
# Handle guardrail violations based on your policy
|
|
396
|
+
if detected_issue:
|
|
397
|
+
guard_span.set_attribute("action", self.action)
|
|
398
|
+
error_msg = run_result.get("error_message",
|
|
399
|
+
f"Input guardrail '{guard_name}' detected an issue for agent '{agent_name}'")
|
|
400
|
+
guard_span.add_event("input_guardrail_violation", {
|
|
401
|
+
"agent": agent_name,
|
|
402
|
+
"guard": guard_name,
|
|
403
|
+
"error_message": error_msg
|
|
404
|
+
})
|
|
405
|
+
guard_span.set_attribute("violation.message", error_msg)
|
|
406
|
+
guard_span.set_attribute("violation.agent", agent_name)
|
|
407
|
+
# print(f"Input guardrail violation on {agent_name}: {error_msg}")
|
|
408
|
+
# Uncomment if you want to raise exceptions on violations:
|
|
409
|
+
# raise ValueError(error_msg)
|
|
410
|
+
else:
|
|
411
|
+
guard_span.set_attribute("action", "passed")
|
|
412
|
+
guard_span.add_event("input_guardrail_passed", {
|
|
413
|
+
"agent": agent_name,
|
|
414
|
+
"guard": guard_name
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
except Exception as e:
|
|
418
|
+
guard_span.record_exception(e)
|
|
419
|
+
guard_span.set_attribute("execution.error", True)
|
|
420
|
+
guard_span.set_attribute("error.message", str(e))
|
|
421
|
+
guard_span.set_attribute("error.agent", agent_name)
|
|
422
|
+
guard_span.add_event("input_guardrail_failed", {
|
|
423
|
+
"agent": agent_name,
|
|
424
|
+
"guard": guard_name,
|
|
425
|
+
"error": str(e)
|
|
426
|
+
})
|
|
427
|
+
# print(f"Error in input guardrail '{guard_name}' for agent '{agent_name}': {str(e)}")
|
|
428
|
+
# Re-raise if you want strict enforcement
|
|
429
|
+
# raise
|
|
430
|
+
|
|
431
|
+
def _apply_output_guardrails(self, content: str, agent_name: str, parent_context) -> None:
|
|
432
|
+
"""Apply output guardrails with telemetry tracking as direct children of agent span."""
|
|
433
|
+
for guard in self.guards:
|
|
434
|
+
guard_name = guard.get("name", "unknown")
|
|
435
|
+
|
|
436
|
+
# Create guardrail span as direct child of the current agent execution span
|
|
437
|
+
with self.tracer.start_as_current_span(
|
|
438
|
+
f"guardrail:{guard_name}",
|
|
439
|
+
context=parent_context
|
|
440
|
+
) as guard_span:
|
|
441
|
+
|
|
442
|
+
# Set comprehensive attributes linking this guardrail to the specific agent
|
|
443
|
+
guard_span.set_attribute("component", agent_name)
|
|
444
|
+
guard_span.set_attribute("guard", guard_name)
|
|
445
|
+
guard_span.set_attribute("content_type", "output")
|
|
446
|
+
|
|
447
|
+
try:
|
|
448
|
+
start_time = datetime.now()
|
|
449
|
+
guard_span.add_event("output_guardrail_started", {
|
|
450
|
+
"agent": agent_name,
|
|
451
|
+
"guard": guard_name
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
run_result = self._call_run_guardrail(content, guard, "output")
|
|
455
|
+
|
|
456
|
+
end_time = datetime.now()
|
|
457
|
+
guard_span.add_event("output_guardrail_completed", {
|
|
458
|
+
"agent": agent_name,
|
|
459
|
+
"guard": guard_name
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
# Set comprehensive telemetry attributes
|
|
463
|
+
guard_span.set_attribute("input.value", self._safe_str(content))
|
|
464
|
+
guard_span.set_attribute("output.value",
|
|
465
|
+
self._safe_str(run_result.get("response", "")))
|
|
466
|
+
guard_span.set_attribute("start_time", start_time.isoformat())
|
|
467
|
+
guard_span.set_attribute("end_time", end_time.isoformat())
|
|
468
|
+
guard_span.set_attribute("duration",
|
|
469
|
+
(end_time - start_time).total_seconds())
|
|
470
|
+
|
|
471
|
+
# Check validation results
|
|
472
|
+
validation_passed = bool(run_result.get("validation_passed", True))
|
|
473
|
+
success = bool(run_result.get("success", True))
|
|
474
|
+
detected_issue = not validation_passed or not success
|
|
475
|
+
|
|
476
|
+
guard_span.set_attribute("detected", detected_issue)
|
|
477
|
+
|
|
478
|
+
# Handle guardrail violations
|
|
479
|
+
if detected_issue:
|
|
480
|
+
guard_span.set_attribute("action", self.action)
|
|
481
|
+
error_msg = run_result.get("error_message",
|
|
482
|
+
f"Output guardrail '{guard_name}' detected an issue for agent '{agent_name}'")
|
|
483
|
+
guard_span.add_event("output_guardrail_violation", {
|
|
484
|
+
"agent": agent_name,
|
|
485
|
+
"guard": guard_name,
|
|
486
|
+
"error_message": error_msg
|
|
487
|
+
})
|
|
488
|
+
guard_span.set_attribute("violation.message", error_msg)
|
|
489
|
+
guard_span.set_attribute("violation.agent", agent_name)
|
|
490
|
+
# print(f"Output guardrail violation on {agent_name}: {error_msg}")
|
|
491
|
+
# Uncomment if you want to raise exceptions on violations:
|
|
492
|
+
# raise ValueError(error_msg)
|
|
493
|
+
else:
|
|
494
|
+
guard_span.set_attribute("action", "passed")
|
|
495
|
+
guard_span.add_event("output_guardrail_passed", {
|
|
496
|
+
"agent": agent_name,
|
|
497
|
+
"guard": guard_name
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
except Exception as e:
|
|
501
|
+
guard_span.record_exception(e)
|
|
502
|
+
guard_span.set_attribute("execution.error", True)
|
|
503
|
+
guard_span.set_attribute("error.message", str(e))
|
|
504
|
+
guard_span.set_attribute("error.agent", agent_name)
|
|
505
|
+
guard_span.add_event("output_guardrail_failed", {
|
|
506
|
+
"agent": agent_name,
|
|
507
|
+
"guard": guard_name,
|
|
508
|
+
"error": str(e)
|
|
509
|
+
})
|
|
510
|
+
# print(f"Error in output guardrail '{guard_name}' for agent '{agent_name}': {str(e)}")
|
|
511
|
+
# Re-raise if you want strict enforcement
|
|
512
|
+
# raise
|
|
513
|
+
|
|
514
|
+
def _format_reply(self, reply, agent) -> Dict[str, Any]:
|
|
515
|
+
"""Ensure reply has consistent format for AutoGen compatibility."""
|
|
516
|
+
agent_name = getattr(agent, 'name', 'assistant')
|
|
517
|
+
|
|
518
|
+
if isinstance(reply, str):
|
|
519
|
+
return {
|
|
520
|
+
"role": "assistant",
|
|
521
|
+
"content": reply,
|
|
522
|
+
"name": agent_name
|
|
523
|
+
}
|
|
524
|
+
elif isinstance(reply, dict):
|
|
525
|
+
# Ensure required fields exist
|
|
526
|
+
formatted_reply = reply.copy()
|
|
527
|
+
if "role" not in formatted_reply:
|
|
528
|
+
formatted_reply["role"] = "assistant"
|
|
529
|
+
if "name" not in formatted_reply:
|
|
530
|
+
formatted_reply["name"] = agent_name
|
|
531
|
+
if "content" not in formatted_reply and reply:
|
|
532
|
+
# Try to extract content from the reply
|
|
533
|
+
content = self._extract_response_content(reply)
|
|
534
|
+
if content:
|
|
535
|
+
formatted_reply["content"] = content
|
|
536
|
+
return formatted_reply
|
|
537
|
+
else:
|
|
538
|
+
# Handle other response types
|
|
539
|
+
content = self._extract_response_content(reply)
|
|
540
|
+
return {
|
|
541
|
+
"role": "assistant",
|
|
542
|
+
"content": content,
|
|
543
|
+
"name": agent_name
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
def _safe_str(self, value, max_length: int = 1000) -> str:
|
|
547
|
+
"""Safely convert value to string with length limit for telemetry."""
|
|
548
|
+
if value is None:
|
|
549
|
+
return ""
|
|
550
|
+
|
|
551
|
+
str_value = str(value)
|
|
552
|
+
if len(str_value) > max_length:
|
|
553
|
+
return str_value[:max_length] + "... [truncated]"
|
|
554
|
+
return str_value
|