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.
Files changed (40) hide show
  1. lexsi_sdk/__init__.py +5 -0
  2. lexsi_sdk/client/__init__.py +0 -0
  3. lexsi_sdk/client/client.py +176 -0
  4. lexsi_sdk/common/__init__.py +0 -0
  5. lexsi_sdk/common/config/.env.prod +3 -0
  6. lexsi_sdk/common/constants.py +143 -0
  7. lexsi_sdk/common/enums.py +8 -0
  8. lexsi_sdk/common/environment.py +49 -0
  9. lexsi_sdk/common/monitoring.py +81 -0
  10. lexsi_sdk/common/trigger.py +75 -0
  11. lexsi_sdk/common/types.py +122 -0
  12. lexsi_sdk/common/utils.py +93 -0
  13. lexsi_sdk/common/validation.py +110 -0
  14. lexsi_sdk/common/xai_uris.py +197 -0
  15. lexsi_sdk/core/__init__.py +0 -0
  16. lexsi_sdk/core/agent.py +62 -0
  17. lexsi_sdk/core/alert.py +56 -0
  18. lexsi_sdk/core/case.py +618 -0
  19. lexsi_sdk/core/dashboard.py +131 -0
  20. lexsi_sdk/core/guardrails/__init__.py +0 -0
  21. lexsi_sdk/core/guardrails/guard_template.py +299 -0
  22. lexsi_sdk/core/guardrails/guardrail_autogen.py +554 -0
  23. lexsi_sdk/core/guardrails/guardrails_langgraph.py +525 -0
  24. lexsi_sdk/core/guardrails/guardrails_openai.py +541 -0
  25. lexsi_sdk/core/guardrails/openai_runner.py +1328 -0
  26. lexsi_sdk/core/model_summary.py +110 -0
  27. lexsi_sdk/core/organization.py +549 -0
  28. lexsi_sdk/core/project.py +5131 -0
  29. lexsi_sdk/core/synthetic.py +387 -0
  30. lexsi_sdk/core/text.py +595 -0
  31. lexsi_sdk/core/tracer.py +208 -0
  32. lexsi_sdk/core/utils.py +36 -0
  33. lexsi_sdk/core/workspace.py +325 -0
  34. lexsi_sdk/core/wrapper.py +766 -0
  35. lexsi_sdk/core/xai.py +306 -0
  36. lexsi_sdk/version.py +34 -0
  37. lexsi_sdk-0.1.16.dist-info/METADATA +100 -0
  38. lexsi_sdk-0.1.16.dist-info/RECORD +40 -0
  39. lexsi_sdk-0.1.16.dist-info/WHEEL +5 -0
  40. 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