Skip to content

Commit 2b0c10c

Browse files
aditik0303claude
andcommitted
fix(llamaindex): address remaining review minors (pages 4-5)
Follow-up to the #360 review pass — the earlier commit covered the blockers (rebind + uninstall) + trace_id; these are the payload-extraction minors: - event_handler.py:236 — _coerce_args no longer drops non-dict args: a list-shaped arg (common with MCP tools) is preserved under `_`, and malformed JSON is kept raw under `_raw` instead of {} — a payload governance can't parse must not slip past arg-based policies. - event_handler.py:224 — _message_text now walks a multimodal ChatMessage's text blocks when `.content` is empty, instead of str(message) (which serialized a pydantic repr into the scanned blob). Acknowledged, intentionally unchanged (noted for consistency / follow-up): - event_handler.py:170 (sync handle): deliberate synchronous gate — a BEFORE_MODEL/TOOL_CALL decision must complete before the call proceeds. Async governance is a protocol-wide change (evaluator is sync) → follow-up. - event_handler.py:186 (warning vs exception): kept logger.warning for the swallow path to match LangChain #899 and the other 4 adapters. - latest-message-only scan: same documented tradeoff as #899 (avoids re-firing on prior turns); kept for parity. Tests: list-shaped/malformed _coerce_args + block-extraction fallback. 130 pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c6f7d17 commit 2b0c10c

2 files changed

Lines changed: 43 additions & 9 deletions

File tree

packages/uipath-llamaindex/src/uipath_llamaindex/governance/event_handler.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -281,15 +281,25 @@ def _latest_message_text(messages: Any) -> str:
281281

282282

283283
def _message_text(message: Any) -> str:
284-
"""Pull text from a ``ChatMessage`` (``.content``) or a bare string."""
284+
"""Pull text from a ``ChatMessage`` (``.content`` / ``.blocks``) or a str."""
285285
if message is None:
286286
return ""
287287
if isinstance(message, str):
288288
return message[:_BEFORE_MODEL_TEXT_CAP]
289289
content = getattr(message, "content", None)
290290
if isinstance(content, str) and content:
291291
return content[:_BEFORE_MODEL_TEXT_CAP]
292-
# Newer ChatMessage carries typed blocks; fall back to str().
292+
# Multimodal ChatMessage carries typed blocks. Walk them for text (a
293+
# TextBlock exposes ``.text``) rather than ``str(message)``, which would
294+
# serialize the pydantic repr — dict-syntax noise that pollutes the
295+
# regex-scanned blob. Non-text blocks (image/binary) have no scannable text.
296+
blocks = getattr(message, "blocks", None)
297+
if isinstance(blocks, (list, tuple)):
298+
texts = [
299+
t for b in blocks if isinstance((t := getattr(b, "text", None)), str) and t
300+
]
301+
if texts:
302+
return "\n".join(texts)[:_BEFORE_MODEL_TEXT_CAP]
293303
return str(message)[:_BEFORE_MODEL_TEXT_CAP]
294304

295305

@@ -307,10 +317,14 @@ def _response_text(response: Any) -> str:
307317

308318

309319
def _coerce_args(arguments: Any) -> Dict[str, Any]:
310-
"""Normalise tool arguments (JSON string / Mapping / None) to a dict.
311-
312-
``AgentToolCallEvent.arguments`` is a JSON-encoded string; other call
313-
sites may hand a dict directly.
320+
"""Normalise tool arguments (JSON string / Mapping / list / None) to a dict.
321+
322+
``AgentToolCallEvent.arguments`` is usually a JSON-encoded string; other
323+
call sites may hand a dict directly. Non-dict payloads are preserved (not
324+
dropped) so an arg-based policy can still scan them: a list-shaped arg
325+
(common with MCP tools) is wrapped under ``_``, and malformed JSON is kept
326+
raw under ``_raw`` — a payload governance can't parse must not be a way to
327+
slip past it.
314328
"""
315329
if arguments is None:
316330
return {}
@@ -321,8 +335,9 @@ def _coerce_args(arguments: Any) -> Dict[str, Any]:
321335
parsed = json.loads(arguments)
322336
return parsed if isinstance(parsed, dict) else {"_": parsed}
323337
except (TypeError, ValueError):
324-
return {}
325-
return {}
338+
return {"_raw": arguments}
339+
# list / tuple / other structured args — preserve rather than drop to {}.
340+
return {"_": arguments}
326341

327342

328343
__all__: List[str] = [

packages/uipath-llamaindex/tests/governance/test_event_handler.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,26 @@ def test_coerce_args_dict_passthrough():
295295

296296
def test_coerce_args_none_and_bad():
297297
assert _coerce_args(None) == {}
298-
assert _coerce_args("not json") == {}
298+
# malformed JSON is preserved raw (not dropped) so policies can still scan it
299+
assert _coerce_args("not json") == {"_raw": "not json"}
300+
301+
302+
def test_coerce_args_preserves_list_shaped_args():
303+
# list-shaped tool args (common with MCP tools) must not be dropped to {}
304+
assert _coerce_args(["a", "b"]) == {"_": ["a", "b"]}
305+
assert _coerce_args('["a", "b"]') == {"_": ["a", "b"]}
306+
307+
308+
def test_message_text_walks_blocks_when_content_empty():
309+
# a multimodal message whose .content is empty falls back to its text
310+
# blocks, not str(message) (which would serialize a pydantic repr)
311+
from uipath_llamaindex.governance.event_handler import _message_text
312+
313+
msg = SimpleNamespace(
314+
content=None,
315+
blocks=[SimpleNamespace(text="block one"), SimpleNamespace(text="block two")],
316+
)
317+
assert _message_text(msg) == "block one\nblock two"
299318

300319

301320
# --------------------------------------------------------------------------

0 commit comments

Comments
 (0)