From d9cb3f978ea04546c65a42f47f2e2aa4d91de51c Mon Sep 17 00:00:00 2001 From: Mayank Jha Date: Thu, 19 Mar 2026 17:35:33 -0700 Subject: [PATCH] feat: scaffold MemoryService for Agent Episodic Memory (URT migration) Add MemoryService client backed by ECS (/ecs_/memory/...) endpoints for Agent Episodic Memory. This enables dynamic few-shot retrieval where agents query past episodes at execution start and inject them as examples into the system prompt. New files: - memory.py: Pydantic models (MemoryField, MemoryItem, MemoryQueryRequest, etc.) - _memory_service.py: MemoryService with create, ingest, query, retrieve, delete, list - __init__.py: Module exports Also registers sdk.memory on the UiPath class. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/uipath/platform/_uipath.py | 5 + .../src/uipath/platform/memory/__init__.py | 25 + .../uipath/platform/memory/_memory_service.py | 428 ++++++++++++++++++ .../src/uipath/platform/memory/memory.py | 89 ++++ 4 files changed, 547 insertions(+) create mode 100644 packages/uipath-platform/src/uipath/platform/memory/__init__.py create mode 100644 packages/uipath-platform/src/uipath/platform/memory/_memory_service.py create mode 100644 packages/uipath-platform/src/uipath/platform/memory/memory.py diff --git a/packages/uipath-platform/src/uipath/platform/_uipath.py b/packages/uipath-platform/src/uipath/platform/_uipath.py index 2143e5dd6..c3a3b94a0 100644 --- a/packages/uipath-platform/src/uipath/platform/_uipath.py +++ b/packages/uipath-platform/src/uipath/platform/_uipath.py @@ -17,6 +17,7 @@ from .common.auth import resolve_config_from_env from .connections import ConnectionsService from .context_grounding import ContextGroundingService +from .memory import MemoryService from .documents import DocumentsService from .entities import EntitiesService from .errors import BaseUrlMissingError, SecretMissingError @@ -111,6 +112,10 @@ def context_grounding(self) -> ContextGroundingService: self.buckets, ) + @property + def memory(self) -> MemoryService: + return MemoryService(self._config, self._execution_context) + @property def documents(self) -> DocumentsService: return DocumentsService(self._config, self._execution_context) diff --git a/packages/uipath-platform/src/uipath/platform/memory/__init__.py b/packages/uipath-platform/src/uipath/platform/memory/__init__.py new file mode 100644 index 000000000..70f6f3b1d --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/memory/__init__.py @@ -0,0 +1,25 @@ +"""Init file for memory module.""" + +from ._memory_service import MemoryService +from .memory import ( + MemoryField, + MemoryIngestRequest, + MemoryItem, + MemoryListResponse, + MemoryQueryRequest, + MemoryQueryResponse, + MemoryQueryResult, + MemoryResource, +) + +__all__ = [ + "MemoryField", + "MemoryIngestRequest", + "MemoryItem", + "MemoryListResponse", + "MemoryQueryRequest", + "MemoryQueryResponse", + "MemoryQueryResult", + "MemoryResource", + "MemoryService", +] diff --git a/packages/uipath-platform/src/uipath/platform/memory/_memory_service.py b/packages/uipath-platform/src/uipath/platform/memory/_memory_service.py new file mode 100644 index 000000000..6cd06b4f4 --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/memory/_memory_service.py @@ -0,0 +1,428 @@ +"""Memory service for Agent Episodic Memory backed by ECS.""" + +from typing import Optional + +from uipath.core.tracing import traced + +from ..common._base_service import BaseService +from ..common._config import UiPathApiConfig +from ..common._execution_context import UiPathExecutionContext +from ..common._folder_context import FolderContext, header_folder +from ..common._models import Endpoint, RequestSpec +from .memory import ( + MemoryIngestRequest, + MemoryItem, + MemoryListResponse, + MemoryQueryRequest, + MemoryQueryResponse, + MemoryResource, +) + + +class MemoryService(FolderContext, BaseService): + """Service for Agent Episodic Memory backed by the ECS service. + + Agent Memory allows agents to persist context across jobs using dynamic + few-shot retrieval. Memory resources are folder-scoped (like CG indexes) + and use Index.Create/Read/Update/Delete permissions. + """ + + def __init__( + self, + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, + ) -> None: + super().__init__(config=config, execution_context=execution_context) + + # ── Public methods ──────────────────────────────────────────────── + + @traced(name="memory_create", run_type="uipath") + def create( + self, + name: str, + description: Optional[str] = None, + folder_key: Optional[str] = None, + ) -> MemoryResource: + """Create a new memory resource. + + Args: + name: The name of the memory resource. + description: Optional description. + folder_key: The folder key for the operation. + + Returns: + MemoryResource: The created memory resource. + """ + spec = self._create_spec(name, description, folder_key) + response = self.request( + spec.method, + spec.endpoint, + json=spec.json, + headers=spec.headers, + ).json() + return MemoryResource.model_validate(response) + + @traced(name="memory_create", run_type="uipath") + async def create_async( + self, + name: str, + description: Optional[str] = None, + folder_key: Optional[str] = None, + ) -> MemoryResource: + """Asynchronously create a new memory resource. + + Args: + name: The name of the memory resource. + description: Optional description. + folder_key: The folder key for the operation. + + Returns: + MemoryResource: The created memory resource. + """ + spec = self._create_spec(name, description, folder_key) + response = ( + await self.request_async( + spec.method, + spec.endpoint, + json=spec.json, + headers=spec.headers, + ) + ).json() + return MemoryResource.model_validate(response) + + @traced(name="memory_ingest", run_type="uipath") + def ingest( + self, + name: str, + request: MemoryIngestRequest, + folder_key: Optional[str] = None, + ) -> None: + """Ingest a memory item into the specified memory resource. + + Args: + name: The name of the memory resource. + request: The ingest request payload. + folder_key: The folder key for the operation. + """ + spec = self._ingest_spec(name, folder_key) + self.request( + spec.method, + spec.endpoint, + json=request.model_dump(by_alias=True, exclude_none=True), + headers=spec.headers, + ) + + @traced(name="memory_ingest", run_type="uipath") + async def ingest_async( + self, + name: str, + request: MemoryIngestRequest, + folder_key: Optional[str] = None, + ) -> None: + """Asynchronously ingest a memory item into the specified memory resource. + + Args: + name: The name of the memory resource. + request: The ingest request payload. + folder_key: The folder key for the operation. + """ + spec = self._ingest_spec(name, folder_key) + await self.request_async( + spec.method, + spec.endpoint, + json=request.model_dump(by_alias=True, exclude_none=True), + headers=spec.headers, + ) + + @traced(name="memory_query", run_type="uipath") + def query( + self, + name: str, + request: MemoryQueryRequest, + folder_key: Optional[str] = None, + ) -> MemoryQueryResponse: + """Perform semantic search on memory. + + Args: + name: The name of the memory resource. + request: The query request payload. + folder_key: The folder key for the operation. + + Returns: + MemoryQueryResponse: The query results. + """ + spec = self._query_spec(name, folder_key) + response = self.request( + spec.method, + spec.endpoint, + json=request.model_dump(by_alias=True, exclude_none=True), + headers=spec.headers, + ).json() + return MemoryQueryResponse.model_validate(response) + + @traced(name="memory_query", run_type="uipath") + async def query_async( + self, + name: str, + request: MemoryQueryRequest, + folder_key: Optional[str] = None, + ) -> MemoryQueryResponse: + """Asynchronously perform semantic search on memory. + + Args: + name: The name of the memory resource. + request: The query request payload. + folder_key: The folder key for the operation. + + Returns: + MemoryQueryResponse: The query results. + """ + spec = self._query_spec(name, folder_key) + response = ( + await self.request_async( + spec.method, + spec.endpoint, + json=request.model_dump(by_alias=True, exclude_none=True), + headers=spec.headers, + ) + ).json() + return MemoryQueryResponse.model_validate(response) + + @traced(name="memory_retrieve", run_type="uipath") + def retrieve( + self, + name: str, + memory_id: str, + folder_key: Optional[str] = None, + ) -> MemoryItem: + """Retrieve a single memory item by ID. + + Args: + name: The name of the memory resource. + memory_id: The ID of the memory item to retrieve. + folder_key: The folder key for the operation. + + Returns: + MemoryItem: The retrieved memory item. + """ + spec = self._retrieve_spec(name, memory_id, folder_key) + response = self.request( + spec.method, + spec.endpoint, + headers=spec.headers, + ).json() + return MemoryItem.model_validate(response) + + @traced(name="memory_retrieve", run_type="uipath") + async def retrieve_async( + self, + name: str, + memory_id: str, + folder_key: Optional[str] = None, + ) -> MemoryItem: + """Asynchronously retrieve a single memory item by ID. + + Args: + name: The name of the memory resource. + memory_id: The ID of the memory item to retrieve. + folder_key: The folder key for the operation. + + Returns: + MemoryItem: The retrieved memory item. + """ + spec = self._retrieve_spec(name, memory_id, folder_key) + response = ( + await self.request_async( + spec.method, + spec.endpoint, + headers=spec.headers, + ) + ).json() + return MemoryItem.model_validate(response) + + @traced(name="memory_delete", run_type="uipath") + def delete( + self, + name: str, + memory_id: str, + folder_key: Optional[str] = None, + ) -> None: + """Delete a memory item by ID. + + Args: + name: The name of the memory resource. + memory_id: The ID of the memory item to delete. + folder_key: The folder key for the operation. + """ + spec = self._delete_spec(name, memory_id, folder_key) + self.request( + spec.method, + spec.endpoint, + headers=spec.headers, + ) + + @traced(name="memory_delete", run_type="uipath") + async def delete_async( + self, + name: str, + memory_id: str, + folder_key: Optional[str] = None, + ) -> None: + """Asynchronously delete a memory item by ID. + + Args: + name: The name of the memory resource. + memory_id: The ID of the memory item to delete. + folder_key: The folder key for the operation. + """ + spec = self._delete_spec(name, memory_id, folder_key) + await self.request_async( + spec.method, + spec.endpoint, + headers=spec.headers, + ) + + @traced(name="memory_list", run_type="uipath") + def list( + self, + name: str, + folder_key: Optional[str] = None, + ) -> MemoryListResponse: + """List all memory items in a memory resource. + + Args: + name: The name of the memory resource. + folder_key: The folder key for the operation. + + Returns: + MemoryListResponse: The list of memory items. + """ + spec = self._list_spec(name, folder_key) + response = self.request( + spec.method, + spec.endpoint, + headers=spec.headers, + ).json() + return MemoryListResponse.model_validate(response) + + @traced(name="memory_list", run_type="uipath") + async def list_async( + self, + name: str, + folder_key: Optional[str] = None, + ) -> MemoryListResponse: + """Asynchronously list all memory items in a memory resource. + + Args: + name: The name of the memory resource. + folder_key: The folder key for the operation. + + Returns: + MemoryListResponse: The list of memory items. + """ + spec = self._list_spec(name, folder_key) + response = ( + await self.request_async( + spec.method, + spec.endpoint, + headers=spec.headers, + ) + ).json() + return MemoryListResponse.model_validate(response) + + # ── Private spec builders ───────────────────────────────────────── + + def _resolve_folder(self, folder_key: Optional[str]) -> Optional[str]: + """Resolve the folder key, falling back to the context default.""" + return folder_key or self._folder_key + + def _create_spec( + self, + name: str, + description: Optional[str], + folder_key: Optional[str] = None, + ) -> RequestSpec: + folder_key = self._resolve_folder(folder_key) + return RequestSpec( + method="POST", + endpoint=Endpoint("/ecs_/memory/create"), + json={ + "name": name, + "description": description, + }, + headers={ + **header_folder(folder_key, None), + }, + ) + + def _ingest_spec( + self, + name: str, + folder_key: Optional[str] = None, + ) -> RequestSpec: + folder_key = self._resolve_folder(folder_key) + return RequestSpec( + method="POST", + endpoint=Endpoint(f"/ecs_/memory/{name}/ingest"), + headers={ + **header_folder(folder_key, None), + }, + ) + + def _query_spec( + self, + name: str, + folder_key: Optional[str] = None, + ) -> RequestSpec: + folder_key = self._resolve_folder(folder_key) + return RequestSpec( + method="POST", + endpoint=Endpoint(f"/ecs_/memory/{name}/query"), + headers={ + **header_folder(folder_key, None), + }, + ) + + def _retrieve_spec( + self, + name: str, + memory_id: str, + folder_key: Optional[str] = None, + ) -> RequestSpec: + folder_key = self._resolve_folder(folder_key) + return RequestSpec( + method="GET", + endpoint=Endpoint(f"/ecs_/memory/{name}/{memory_id}"), + headers={ + **header_folder(folder_key, None), + }, + ) + + def _delete_spec( + self, + name: str, + memory_id: str, + folder_key: Optional[str] = None, + ) -> RequestSpec: + folder_key = self._resolve_folder(folder_key) + return RequestSpec( + method="DELETE", + endpoint=Endpoint(f"/ecs_/memory/{name}/{memory_id}"), + headers={ + **header_folder(folder_key, None), + }, + ) + + def _list_spec( + self, + name: str, + folder_key: Optional[str] = None, + ) -> RequestSpec: + folder_key = self._resolve_folder(folder_key) + return RequestSpec( + method="GET", + endpoint=Endpoint(f"/ecs_/memory/{name}/list"), + headers={ + **header_folder(folder_key, None), + }, + ) diff --git a/packages/uipath-platform/src/uipath/platform/memory/memory.py b/packages/uipath-platform/src/uipath/platform/memory/memory.py new file mode 100644 index 000000000..4c9c78989 --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/memory/memory.py @@ -0,0 +1,89 @@ +"""Pydantic models for the Memory API.""" + +from typing import List, Optional + +from pydantic import BaseModel, ConfigDict, Field + + +class MemoryField(BaseModel): + """A field name/value pair used in memory inputs and outputs.""" + + model_config = ConfigDict(populate_by_name=True) + + field_name: str = Field(..., alias="fieldName") + field_value: str = Field(..., alias="fieldValue") + + +class MemoryItem(BaseModel): + """A single memory item containing inputs, outputs, and trace context.""" + + model_config = ConfigDict(populate_by_name=True) + + id: Optional[str] = Field(None, alias="id") + trace_id: Optional[str] = Field(None, alias="traceId") + feedback_item_id: Optional[str] = Field(None, alias="feedbackItemId") + inputs: List[MemoryField] = Field(default_factory=list, alias="inputs") + outputs: List[MemoryField] = Field(default_factory=list, alias="outputs") + abbreviated_trace: Optional[List[str]] = Field(None, alias="abbreviatedTrace") + partition_key: Optional[str] = Field(None, alias="partitionKey") + + +class MemoryQueryRequest(BaseModel): + """Request payload for semantic search on memory.""" + + model_config = ConfigDict(populate_by_name=True) + + inputs: List[MemoryField] = Field(..., alias="inputs") + outputs: Optional[List[MemoryField]] = Field(None, alias="outputs") + abbreviated_trace: Optional[List[str]] = Field(None, alias="abbreviatedTrace") + top_k: int = Field(default=5, alias="topK") + threshold: Optional[float] = Field(None, alias="threshold") + partition_key: Optional[str] = Field(None, alias="partitionKey") + + +class MemoryQueryResult(BaseModel): + """A single result from a memory query.""" + + model_config = ConfigDict(populate_by_name=True) + + memory_item: MemoryItem = Field(..., alias="memoryItem") + score: float = Field(..., alias="score") + + +class MemoryQueryResponse(BaseModel): + """Response from a memory query operation.""" + + model_config = ConfigDict(populate_by_name=True) + + results: List[MemoryQueryResult] = Field(default_factory=list, alias="results") + + +class MemoryIngestRequest(BaseModel): + """Request payload for ingesting a memory item.""" + + model_config = ConfigDict(populate_by_name=True) + + trace_id: Optional[str] = Field(None, alias="traceId") + feedback_item_id: Optional[str] = Field(None, alias="feedbackItemId") + inputs: List[MemoryField] = Field(..., alias="inputs") + outputs: List[MemoryField] = Field(default_factory=list, alias="outputs") + abbreviated_trace: Optional[List[str]] = Field(None, alias="abbreviatedTrace") + partition_key: Optional[str] = Field(None, alias="partitionKey") + + +class MemoryResource(BaseModel): + """A memory resource (folder-scoped, similar to CG indexes).""" + + model_config = ConfigDict(populate_by_name=True) + + id: Optional[str] = Field(None, alias="id") + name: str = Field(..., alias="name") + description: Optional[str] = Field(None, alias="description") + + +class MemoryListResponse(BaseModel): + """Response from listing memory items.""" + + model_config = ConfigDict(populate_by_name=True) + + value: List[MemoryItem] = Field(default_factory=list, alias="value")