1111"""
1212
1313from collections .abc import Iterable
14- from typing import Any , Optional , TypeVar
14+ from typing import Any , Self , override
1515
1616from langchain_core .documents import Document
1717from langchain_core .embeddings import Embeddings
1818from langchain_core .vectorstores import VectorStore
1919from uipath import UiPath
20+ from uipath ._config import Config
21+ from uipath ._execution_context import ExecutionContext
22+ from uipath ._services .context_grounding_service import ContextGroundingService
23+ from uipath .models .context_grounding import ContextGroundingQueryResponse
2024
21- VST = TypeVar ( "VST" , bound = "ContextGroundingVectorStore" )
25+ from uipath_agents_gym . tools . ecs . types import ECSMetadata
2226
27+ VST = TypeVar ("VST" , bound = "ContextGroundingVectorStore" )
2328
2429class ContextGroundingVectorStore (VectorStore ):
2530 """Vector store that uses UiPath Context Grounding (ECS) as a backend.
2631
2732 This class provides a straightforward implementation that connects to the
2833 UiPath Context Grounding API for semantic searching.
29-
30- Example:
31- .. code-block:: python
32-
33- from uipath_agents_gym.tools.ecs_vectorstore import ContextGroundingVectorStore
34-
35- # Initialize the vector store with an index name
36- vectorstore = ContextGroundingVectorStore(index_name="ECCN")
37-
38- # Perform similarity search
39- docs_with_scores = vectorstore.similarity_search_with_score(
40- "How do I process an invoice?", k=5
41- )
4234 """
4335
4436 def __init__ (
4537 self ,
4638 index_name : str ,
47- folder_path : Optional [str ] = None ,
48- uipath_sdk : Optional [UiPath ] = None ,
39+ uipath_url : str ,
40+ access_token : str ,
41+ folder_key : str | None = None ,
42+ folder_path : str | None = None ,
4943 ):
5044 """Initialize the ContextGroundingVectorStore.
5145
5246 Args:
53- index_name: Name of the context grounding index to use
54- uipath_sdk: Optional SDK instance to use. If not provided, a new instance will be created.
47+ index_name: Name of the context grounding index to use (schema name)
48+ uipath_url: UiPath URL (e.g., "https://alpha.uipath.com/organizationId/tenantId")
49+ access_token: Static access token for authentication
50+ folder_key: Optional folder key for folder-scoped operations
51+ folder_path: Optional folder path for folder-scoped operations
5552 """
5653 self .index_name = index_name
54+ self .uipath_url = uipath_url
55+ self .folder_key = folder_key
5756 self .folder_path = folder_path
58- self .sdk = uipath_sdk or UiPath ()
5957
58+ # Create Config and ExecutionContext with static token
59+ config = Config (base_url = self .uipath_url , secret = access_token )
60+ execution_context = ExecutionContext ()
61+
62+ # Create a temporary UiPath SDK instance to get the required services
63+ sdk = UiPath (base_url = self .uipath_url , secret = access_token )
64+
65+ # Create ContextGroundingService instance (composition over inheritance)
66+ self ._context_grounding_service = ContextGroundingService (
67+ config , execution_context , sdk .folders , sdk .buckets
68+ )
69+
70+ # VectorStore implementation methods
71+
72+ @override
6073 def similarity_search_with_score (
6174 self , query : str , k : int = 4 , ** kwargs : Any
6275 ) -> list [tuple [Document , float ]]:
6376 """Return documents most similar to the query along with the distances.
64- The distance is 1 - score, where score is the relevance score returned by the Context Grounding API.
6577
6678 Args:
6779 query: The query string
@@ -70,52 +82,23 @@ def similarity_search_with_score(
7082 Returns:
7183 list of tuples of (document, score)
7284 """
73- # Call the UiPath SDK to perform the search
74- results = self .sdk . context_grounding .search (
85+ # Use the context grounding service to perform search
86+ results : list [ ContextGroundingQueryResponse ] = self ._context_grounding_service .search (
7587 name = self .index_name ,
7688 query = query ,
7789 number_of_results = k ,
90+ folder_key = self .folder_key ,
7891 folder_path = self .folder_path ,
7992 )
8093
81- # Convert the results to Documents with scores
82- docs_with_scores = []
83- for result in results :
84- # Create metadata from result fields
85- metadata = {
86- "source" : result .source ,
87- "id" : result .id ,
88- "reference" : result .reference ,
89- "page_number" : result .page_number ,
90- "source_document_id" : result .source_document_id ,
91- "caption" : result .caption ,
92- }
93-
94- # Add any operation metadata if available
95- if result .metadata :
96- metadata ["operation_id" ] = result .metadata .operation_id
97- metadata ["strategy" ] = result .metadata .strategy
98-
99- # Create a Document with the content and metadata
100- doc = Document (
101- page_content = result .content ,
102- metadata = metadata ,
103- )
104-
105- score = 1.0 - float (result .score )
106-
107- docs_with_scores .append ((doc , score ))
108-
109- return docs_with_scores
94+ return self ._convert_results_to_documents (results )
11095
96+ @override
11197 def similarity_search_with_relevance_scores (
11298 self , query : str , k : int = 4 , ** kwargs : Any
11399 ) -> list [tuple [Document , float ]]:
114100 """Return documents along with their relevance scores on a scale from 0 to 1.
115101
116- This directly uses the scores provided by the Context Grounding API,
117- which are already normalized between 0 and 1.
118-
119102 Args:
120103 query: The query string
121104 k: Number of documents to return (default=4)
@@ -128,6 +111,7 @@ def similarity_search_with_relevance_scores(
128111 for doc , score in self .similarity_search_with_score (query , k , ** kwargs )
129112 ]
130113
114+ @override
131115 async def asimilarity_search_with_score (
132116 self , query : str , k : int = 4 , ** kwargs : Any
133117 ) -> list [tuple [Document , float ]]:
@@ -140,52 +124,24 @@ async def asimilarity_search_with_score(
140124 Returns:
141125 list of tuples of (document, score)
142126 """
143- # Call the UiPath SDK to perform the search asynchronously
144- results = await self .sdk .context_grounding .search_async (
127+ # Use the context grounding service to perform async search
128+ results : list [
129+ ContextGroundingQueryResponse
130+ ] = await self ._context_grounding_service .search_async (
145131 name = self .index_name ,
146132 query = query ,
147133 number_of_results = k ,
134+ folder_key = self .folder_key ,
148135 folder_path = self .folder_path ,
149136 )
150137
151- # Convert the results to Documents with scores
152- docs_with_scores = []
153- for result in results :
154- # Create metadata from result fields
155- metadata = {
156- "source" : result .source ,
157- "id" : result .id ,
158- "reference" : result .reference ,
159- "page_number" : result .page_number ,
160- "source_document_id" : result .source_document_id ,
161- "caption" : result .caption ,
162- }
163-
164- # Add any operation metadata if available
165- if result .metadata :
166- metadata ["operation_id" ] = result .metadata .operation_id
167- metadata ["strategy" ] = result .metadata .strategy
168-
169- # Create a Document with the content and metadata
170- doc = Document (
171- page_content = result .content ,
172- metadata = metadata ,
173- )
174-
175- # Get the distance score as 1 - ecs_score
176- score = 1.0 - float (result .score )
177-
178- docs_with_scores .append ((doc , score ))
179-
180- return docs_with_scores
138+ return self ._convert_results_to_documents (results )
181139
140+ @override
182141 async def asimilarity_search_with_relevance_scores (
183142 self , query : str , k : int = 4 , ** kwargs : Any
184143 ) -> list [tuple [Document , float ]]:
185- """Asynchronously return documents along with their relevance scores on a scale from 0 to 1.
186-
187- This directly uses the scores provided by the Context Grounding API,
188- which are already normalized between 0 and 1.
144+ """Asynchronously return documents along with their relevance scores.
189145
190146 Args:
191147 query: The query string
@@ -196,14 +152,11 @@ async def asimilarity_search_with_relevance_scores(
196152 """
197153 return [
198154 (doc , 1.0 - score )
199- for doc , score in await self .asimilarity_search_with_score (
200- query , k , ** kwargs
201- )
155+ for doc , score in await self .asimilarity_search_with_score (query , k , ** kwargs )
202156 ]
203157
204- def similarity_search (
205- self , query : str , k : int = 4 , ** kwargs : Any
206- ) -> list [Document ]:
158+ @override
159+ def similarity_search (self , query : str , k : int = 4 , ** kwargs : Any ) -> list [Document ]:
207160 """Return documents most similar to the query.
208161
209162 Args:
@@ -216,9 +169,8 @@ def similarity_search(
216169 docs_and_scores = self .similarity_search_with_score (query , k , ** kwargs )
217170 return [doc for doc , _ in docs_and_scores ]
218171
219- async def asimilarity_search (
220- self , query : str , k : int = 4 , ** kwargs : Any
221- ) -> list [Document ]:
172+ @override
173+ async def asimilarity_search (self , query : str , k : int = 4 , ** kwargs : Any ) -> list [Document ]:
222174 """Asynchronously return documents most similar to the query.
223175
224176 Args:
@@ -231,14 +183,67 @@ async def asimilarity_search(
231183 docs_and_scores = await self .asimilarity_search_with_score (query , k , ** kwargs )
232184 return [doc for doc , _ in docs_and_scores ]
233185
186+ def _convert_results_to_documents (
187+ self , results : list [ContextGroundingQueryResponse ]
188+ ) -> list [tuple [Document , float ]]:
189+ """Convert API results to Document objects with scores.
190+
191+ Args:
192+ results: List of ContextGroundingQueryResponse objects
193+
194+ Returns:
195+ List of tuples containing (Document, score)
196+ """
197+ docs_with_scores = []
198+
199+ for result in results :
200+ # Create metadata from result fields using Pydantic model
201+ metadata_dict = {}
202+
203+ # Add required string fields with proper defaults
204+ if result .source :
205+ metadata_dict ["source" ] = str (result .source )
206+ if result .reference :
207+ metadata_dict ["reference" ] = str (result .reference )
208+ if result .page_number :
209+ metadata_dict ["page_number" ] = str (result .page_number )
210+ if result .source_document_id :
211+ metadata_dict ["source_document_id" ] = str (result .source_document_id )
212+ if result .caption :
213+ metadata_dict ["caption" ] = str (result .caption )
214+
215+ # Add any operation metadata if available
216+ if result .metadata :
217+ if result .metadata .operation_id :
218+ metadata_dict ["operation_id" ] = str (result .metadata .operation_id )
219+ if result .metadata .strategy :
220+ metadata_dict ["strategy" ] = str (result .metadata .strategy )
221+
222+ # Create and validate metadata using Pydantic model
223+ metadata = ECSMetadata (** metadata_dict )
224+
225+ # Create a Document with the content and metadata
226+ doc = Document (
227+ page_content = result .content or "" ,
228+ metadata = metadata .model_dump (exclude_none = True ),
229+ )
230+
231+ # Convert score to distance (1 - score)
232+ score = 1.0 - float (result .score or 0.0 )
233+
234+ docs_with_scores .append ((doc , score ))
235+
236+ return docs_with_scores
237+
234238 @classmethod
239+ @override
235240 def from_texts (
236- cls : type [ VST ] ,
241+ cls ,
237242 texts : list [str ],
238243 embedding : Embeddings ,
239- metadatas : Optional [ list [dict [ str , Any ]]] = None ,
244+ metadatas : list [dict ] | None = None ,
240245 ** kwargs : Any ,
241- ) -> VST :
246+ ) -> Self :
242247 """This method is required by the VectorStore abstract class, but is not supported
243248 by ContextGroundingVectorStore which is read-only.
244249
@@ -247,22 +252,20 @@ def from_texts(
247252 """
248253 raise NotImplementedError (
249254 "ContextGroundingVectorStore is a read-only wrapper for UiPath Context Grounding. "
250- "Creating a vector store from texts is not supported."
255+ + "Creating a vector store from texts is not supported."
251256 )
252257
253- # Other required methods with minimal implementation to satisfy the interface
258+ @ override
254259 def add_texts (
255- self ,
256- texts : Iterable [str ],
257- metadatas : Optional [list [dict [str , Any ]]] = None ,
258- ** kwargs : Any ,
260+ self , texts : Iterable [str ], metadatas : list [dict ] | None = None , ** kwargs : Any
259261 ) -> list [str ]:
260262 """Not implemented for ContextGroundingVectorStore as this is a read-only wrapper."""
261263 raise NotImplementedError (
262264 "ContextGroundingVectorStore is a read-only wrapper for UiPath Context Grounding."
263- )
265+ ) # type: ignore[unreachable]
264266
265- def delete (self , ids : Optional [list [str ]] = None , ** kwargs : Any ) -> Optional [bool ]:
267+ @override
268+ def delete (self , ids : list [str ] | None = None , ** kwargs : Any ) -> bool | None :
266269 """Not implemented for ContextGroundingVectorStore as this is a read-only wrapper."""
267270 raise NotImplementedError (
268271 "ContextGroundingVectorStore is a read-only wrapper for UiPath Context Grounding."
0 commit comments