From f9d147572ec6144845a7402081d22638aada7f6b Mon Sep 17 00:00:00 2001 From: Mayank Jha Date: Tue, 19 May 2026 21:27:30 -0700 Subject: [PATCH] Log escalation memory search parameters --- .../uipath/platform/memory/_memory_service.py | 94 ++++++++++++- .../tests/services/test_memory_service.py | 125 ++++++++++++++++++ 2 files changed, 217 insertions(+), 2 deletions(-) diff --git a/packages/uipath-platform/src/uipath/platform/memory/_memory_service.py b/packages/uipath-platform/src/uipath/platform/memory/_memory_service.py index 73d788f80..a71eb9d7a 100644 --- a/packages/uipath-platform/src/uipath/platform/memory/_memory_service.py +++ b/packages/uipath-platform/src/uipath/platform/memory/_memory_service.py @@ -5,6 +5,9 @@ enriches traces/feedback before forwarding to ECS. """ +import hashlib +import json +import logging from typing import Any, Optional from uipath.core.tracing import traced @@ -29,6 +32,8 @@ _MEMORY_SPACES_BASE = "/ecs_/v2/episodicmemories" _LLMOPS_AGENT_BASE = "/llmopstenant_/api/Agent/memory" +logger = logging.getLogger(__name__) + class MemoryService(FolderContext, BaseService): """Service for Agent Memory Spaces. @@ -276,10 +281,18 @@ def escalation_search( EscalationMemorySearchResponse: Matched escalation outcomes. """ spec = self._escalation_search_spec(memory_space_id, folder_key, folder_path) + request_body = request.model_dump(by_alias=True, exclude_none=True) + _log_escalation_search_request( + memory_space_id=memory_space_id, + folder_key=folder_key, + folder_path=folder_path, + headers=spec.headers, + request_body=request_body, + ) response = self.request( spec.method, spec.endpoint, - json=request.model_dump(by_alias=True, exclude_none=True), + json=request_body, headers=spec.headers, ).json() return EscalationMemorySearchResponse.model_validate(response) @@ -307,11 +320,19 @@ async def escalation_search_async( EscalationMemorySearchResponse: Matched escalation outcomes. """ spec = self._escalation_search_spec(memory_space_id, folder_key, folder_path) + request_body = request.model_dump(by_alias=True, exclude_none=True) + _log_escalation_search_request( + memory_space_id=memory_space_id, + folder_key=folder_key, + folder_path=folder_path, + headers=spec.headers, + request_body=request_body, + ) response = ( await self.request_async( spec.method, spec.endpoint, - json=request.model_dump(by_alias=True, exclude_none=True), + json=request_body, headers=spec.headers, ) ).json() @@ -491,3 +512,72 @@ def _escalation_ingest_spec( ), headers={**header_folder(folder_key, None)}, ) + + +def _log_escalation_search_request( + *, + memory_space_id: str, + folder_key: Optional[str], + folder_path: Optional[str], + headers: dict[str, str], + request_body: dict[str, Any], +) -> None: + logger.info( + "Escalation memory search request parameters: %s", + json.dumps( + _build_escalation_search_log_payload( + memory_space_id=memory_space_id, + folder_key=folder_key, + folder_path=folder_path, + headers=headers, + request_body=request_body, + ), + default=str, + sort_keys=True, + ), + ) + + +def _build_escalation_search_log_payload( + *, + memory_space_id: str, + folder_key: Optional[str], + folder_path: Optional[str], + headers: dict[str, str], + request_body: dict[str, Any], +) -> dict[str, Any]: + fields = request_body.get("fields") or [] + definition_system_prompt = request_body.get("definitionSystemPrompt") + + return { + "memorySpaceId": memory_space_id, + "folderKey": folder_key, + "folderPath": folder_path, + "resolvedHeaders": headers, + "request": { + "definitionSystemPrompt": _safe_value_summary(definition_system_prompt), + "fieldCount": len(fields), + "fields": [_safe_search_field(field) for field in fields], + "settings": request_body.get("settings"), + }, + } + + +def _safe_search_field(field: Any) -> dict[str, Any]: + if not isinstance(field, dict): + return {"field": str(type(field)), "value": _safe_value_summary(field)} + + safe_field = {key: value for key, value in field.items() if key != "value"} + safe_field["value"] = _safe_value_summary(field.get("value")) + return safe_field + + +def _safe_value_summary(value: Any) -> dict[str, Any] | None: + if value is None: + return None + + string_value = str(value) + return { + "length": len(string_value), + "sha256": hashlib.sha256(string_value.encode("utf-8")).hexdigest(), + } diff --git a/packages/uipath-platform/tests/services/test_memory_service.py b/packages/uipath-platform/tests/services/test_memory_service.py index 716e3438c..3a2b2c359 100644 --- a/packages/uipath-platform/tests/services/test_memory_service.py +++ b/packages/uipath-platform/tests/services/test_memory_service.py @@ -1,6 +1,8 @@ """Unit tests for MemoryService with HTTP mocking.""" +import hashlib import json +import logging import pytest from pytest_httpx import HTTPXMock @@ -330,6 +332,129 @@ def test_search_sends_folder_header( assert sent.headers.get("x-uipath-folderkey") == "custom-folder" class TestEscalationSearch: + def test_escalation_search_logs_request_parameters( + self, + httpx_mock: HTTPXMock, + service: MemoryService, + base_url: str, + org: str, + tenant: str, + caplog, + ) -> None: + memory_space_id = "aaaa-bbbb-cccc-dddd" + field_value = "approval request with customer details" + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/llmopstenant_/api/Agent/memory/{memory_space_id}/escalation/search", + status_code=200, + json=SAMPLE_ESCALATION_SEARCH_RESPONSE, + ) + + request = MemorySearchRequest( + fields=[ + SearchField( + key_path=["escalation-input", "request_details"], + value=field_value, + ) + ], + settings=SearchSettings( + threshold=0.25, + result_count=1, + search_mode=SearchMode.Semantic, + ), + ) + + with caplog.at_level( + logging.INFO, + logger="uipath.platform.memory._memory_service", + ): + service.escalation_search( + memory_space_id=memory_space_id, + request=request, + folder_key="custom-folder", + ) + + log_record = next( + record + for record in caplog.records + if record.message.startswith( + "Escalation memory search request parameters: " + ) + ) + payload = json.loads(log_record.message.split(": ", 1)[1]) + + assert payload["memorySpaceId"] == memory_space_id + assert payload["folderKey"] == "custom-folder" + assert payload["resolvedHeaders"] == {"x-uipath-folderkey": "custom-folder"} + assert payload["request"]["settings"] == { + "threshold": 0.25, + "resultCount": 1, + "searchMode": "Semantic", + } + assert payload["request"]["fieldCount"] == 1 + assert payload["request"]["fields"][0]["keyPath"] == [ + "escalation-input", + "request_details", + ] + assert payload["request"]["fields"][0]["value"] == { + "length": len(field_value), + "sha256": hashlib.sha256(field_value.encode("utf-8")).hexdigest(), + } + assert field_value not in log_record.message + + async def test_escalation_search_async_logs_request_parameters( + self, + httpx_mock: HTTPXMock, + service: MemoryService, + base_url: str, + org: str, + tenant: str, + caplog, + ) -> None: + memory_space_id = "aaaa-bbbb-cccc-dddd" + field_value = "async approval request" + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/llmopstenant_/api/Agent/memory/{memory_space_id}/escalation/search", + status_code=200, + json=SAMPLE_ESCALATION_SEARCH_RESPONSE, + ) + + request = MemorySearchRequest( + fields=[SearchField(key_path=["escalation-input"], value=field_value)], + settings=SearchSettings( + threshold=0.0, + result_count=1, + search_mode=SearchMode.Hybrid, + ), + ) + + with caplog.at_level( + logging.INFO, + logger="uipath.platform.memory._memory_service", + ): + await service.escalation_search_async( + memory_space_id=memory_space_id, + request=request, + ) + + log_record = next( + record + for record in caplog.records + if record.message.startswith( + "Escalation memory search request parameters: " + ) + ) + payload = json.loads(log_record.message.split(": ", 1)[1]) + + assert payload["memorySpaceId"] == memory_space_id + assert payload["resolvedHeaders"] == { + "x-uipath-folderkey": "test-folder-key" + } + assert payload["request"]["fields"][0]["value"] == { + "length": len(field_value), + "sha256": hashlib.sha256(field_value.encode("utf-8")).hexdigest(), + } + assert field_value not in log_record.message + def test_escalation_search( self, httpx_mock: HTTPXMock,