From 8c27d935b93defe0e2cec49f4bd8b541b2e52906 Mon Sep 17 00:00:00 2001 From: Yash Wagle Date: Thu, 12 Nov 2026 15:02:41 -0800 Subject: [PATCH 1/2] feat(memory): keep per-field semanticScore in memory search results LLMOps passes ECS's per-field semanticScore through on memory search, but MemoryMatchField did not declare it and pydantic's default extra="ignore" dropped it. Callers and the agent's "Apply dynamic few shot" span saw only the rank-scale field score. Add semantic_score (optional) to MemoryMatchField and let the search response models keep unknown keys, so new server fields are no longer silently discarded. Bump uipath-platform to 0.3.7. Co-Authored-By: Claude Opus 5.5 --- packages/uipath-platform/pyproject.toml | 2 +- .../src/uipath/platform/memory/memory.py | 13 ++++-- .../tests/services/test_memory_service.py | 41 +++++++++++++++++++ packages/uipath-platform/uv.lock | 2 +- packages/uipath/uv.lock | 2 +- 5 files changed, 53 insertions(+), 7 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index b80e51351..413a6c63c 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.3.6" +version = "0.3.7" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/memory/memory.py b/packages/uipath-platform/src/uipath/platform/memory/memory.py index e0ec5d34c..e6ce6ddb8 100644 --- a/packages/uipath-platform/src/uipath/platform/memory/memory.py +++ b/packages/uipath-platform/src/uipath/platform/memory/memory.py @@ -72,14 +72,19 @@ class SearchSettings(BaseModel): class MemoryMatchField(BaseModel): - """A field within a search result, with scoring details.""" + """A field within a search result, with scoring details. - model_config = ConfigDict(populate_by_name=True) + ``semantic_score`` is the field's vector similarity, as distinct from + ``score``. Unknown keys the server adds are kept, not dropped. + """ + + model_config = ConfigDict(populate_by_name=True, extra="allow") key_path: List[str] = Field(..., alias="keyPath") value: str = Field(..., alias="value") weight: float = Field(..., alias="weight") score: float = Field(..., alias="score") + semantic_score: Optional[float] = Field(None, alias="semanticScore") weighted_score: float = Field(..., alias="weightedScore") @@ -144,7 +149,7 @@ class MemorySearchRequest(BaseModel): class MemoryMatch(BaseModel): """A single matched memory from a search operation (LLMOps).""" - model_config = ConfigDict(populate_by_name=True) + model_config = ConfigDict(populate_by_name=True, extra="allow") memory_item_id: str = Field(..., alias="memoryItemId") score: float = Field(..., alias="score") @@ -158,7 +163,7 @@ class MemoryMatch(BaseModel): class MemorySearchResponse(BaseModel): """Response from LLMOps search, including system prompt injection.""" - model_config = ConfigDict(populate_by_name=True) + model_config = ConfigDict(populate_by_name=True, extra="allow") results: List[MemoryMatch] = Field(default_factory=list, alias="results") metadata: Dict[str, str] = Field(default_factory=dict, alias="metadata") diff --git a/packages/uipath-platform/tests/services/test_memory_service.py b/packages/uipath-platform/tests/services/test_memory_service.py index 084fe2587..54a95467c 100644 --- a/packages/uipath-platform/tests/services/test_memory_service.py +++ b/packages/uipath-platform/tests/services/test_memory_service.py @@ -76,6 +76,7 @@ def service( "value": "What is the capital of France?", "weight": 1.0, "score": 0.95, + "semanticScore": 0.91, "weightedScore": 0.95, } ], @@ -284,6 +285,7 @@ def test_search_memory( assert result.results[0].memory_item_id == "item-001" assert result.results[0].score == 0.95 assert isinstance(result.results[0].fields[0], MemoryMatchField) + assert result.results[0].fields[0].semantic_score == 0.91 assert ( result.system_prompt_injection == "Based on past interactions: Paris is the capital." @@ -298,6 +300,45 @@ def test_search_memory( assert body["settings"]["fieldMatching"] == "Or" assert body["definitionSystemPrompt"] == "You are a helpful assistant." + def test_search_response_keeps_field_semantic_score_in_dump(self) -> None: + response = MemorySearchResponse.model_validate(SAMPLE_SEARCH_RESPONSE) + + dumped = response.model_dump(by_alias=True, exclude_none=True) + + field = dumped["results"][0]["fields"][0] + assert field["semanticScore"] == 0.91 + assert field["score"] == 0.95 + + def test_search_response_field_semantic_score_is_optional(self) -> None: + field = MemoryMatchField.model_validate( + { + "keyPath": ["agent-input", "topic"], + "value": "python", + "weight": 1.0, + "score": 0.5, + "weightedScore": 0.5, + } + ) + + assert field.semantic_score is None + assert "semanticScore" not in field.model_dump( + by_alias=True, exclude_none=True + ) + + def test_search_response_keeps_unknown_server_keys(self) -> None: + payload = json.loads(json.dumps(SAMPLE_SEARCH_RESPONSE)) + payload["searchId"] = "search-1" + payload["results"][0]["rank"] = 1 + payload["results"][0]["fields"][0]["matched"] = True + + dumped = MemorySearchResponse.model_validate(payload).model_dump( + by_alias=True, exclude_none=True + ) + + assert dumped["searchId"] == "search-1" + assert dumped["results"][0]["rank"] == 1 + assert dumped["results"][0]["fields"][0]["matched"] is True + @pytest.mark.parametrize( ("raw", "expected"), [ diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 2d859d395..4530e2ed5 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1095,7 +1095,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.3.6" +version = "0.3.7" source = { editable = "." } dependencies = [ { name = "anyio" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 73de2e882..6b4d34ce2 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2762,7 +2762,7 @@ wheels = [ [[package]] name = "uipath-platform" -version = "0.3.6" +version = "0.3.7" source = { editable = "../uipath-platform" } dependencies = [ { name = "anyio" }, From 2524b021ee48cf47f111ba0cf622b93fec84023c Mon Sep 17 00:00:00 2001 From: Yash Wagle Date: Thu, 12 Nov 2026 15:13:50 -0800 Subject: [PATCH 2/2] refactor(memory): model semanticScore explicitly instead of keeping unknown keys Drop extra="allow" from the search response models. Unknown keys would flow into the agent's "Apply dynamic few shot" span and the SDK's memory_search span without review, so new server fields should be added deliberately, as semantic_score is. Co-Authored-By: Claude Opus 5.5 --- .../src/uipath/platform/memory/memory.py | 8 ++++---- .../tests/services/test_memory_service.py | 14 -------------- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/packages/uipath-platform/src/uipath/platform/memory/memory.py b/packages/uipath-platform/src/uipath/platform/memory/memory.py index e6ce6ddb8..b1973d33e 100644 --- a/packages/uipath-platform/src/uipath/platform/memory/memory.py +++ b/packages/uipath-platform/src/uipath/platform/memory/memory.py @@ -75,10 +75,10 @@ class MemoryMatchField(BaseModel): """A field within a search result, with scoring details. ``semantic_score`` is the field's vector similarity, as distinct from - ``score``. Unknown keys the server adds are kept, not dropped. + ``score``. """ - model_config = ConfigDict(populate_by_name=True, extra="allow") + model_config = ConfigDict(populate_by_name=True) key_path: List[str] = Field(..., alias="keyPath") value: str = Field(..., alias="value") @@ -149,7 +149,7 @@ class MemorySearchRequest(BaseModel): class MemoryMatch(BaseModel): """A single matched memory from a search operation (LLMOps).""" - model_config = ConfigDict(populate_by_name=True, extra="allow") + model_config = ConfigDict(populate_by_name=True) memory_item_id: str = Field(..., alias="memoryItemId") score: float = Field(..., alias="score") @@ -163,7 +163,7 @@ class MemoryMatch(BaseModel): class MemorySearchResponse(BaseModel): """Response from LLMOps search, including system prompt injection.""" - model_config = ConfigDict(populate_by_name=True, extra="allow") + model_config = ConfigDict(populate_by_name=True) results: List[MemoryMatch] = Field(default_factory=list, alias="results") metadata: Dict[str, str] = Field(default_factory=dict, alias="metadata") diff --git a/packages/uipath-platform/tests/services/test_memory_service.py b/packages/uipath-platform/tests/services/test_memory_service.py index 54a95467c..9a5245ab6 100644 --- a/packages/uipath-platform/tests/services/test_memory_service.py +++ b/packages/uipath-platform/tests/services/test_memory_service.py @@ -325,20 +325,6 @@ def test_search_response_field_semantic_score_is_optional(self) -> None: by_alias=True, exclude_none=True ) - def test_search_response_keeps_unknown_server_keys(self) -> None: - payload = json.loads(json.dumps(SAMPLE_SEARCH_RESPONSE)) - payload["searchId"] = "search-1" - payload["results"][0]["rank"] = 1 - payload["results"][0]["fields"][0]["matched"] = True - - dumped = MemorySearchResponse.model_validate(payload).model_dump( - by_alias=True, exclude_none=True - ) - - assert dumped["searchId"] == "search-1" - assert dumped["results"][0]["rank"] == 1 - assert dumped["results"][0]["fields"][0]["matched"] is True - @pytest.mark.parametrize( ("raw", "expected"), [