Skip to content

Commit 189c162

Browse files
committed
Update context grounding vector store to be compatible with gym
1 parent aefbe61 commit 189c162

File tree

1 file changed

+114
-111
lines changed

1 file changed

+114
-111
lines changed

src/uipath_langchain/vectorstores/context_grounding_vectorstore.py

Lines changed: 114 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -11,57 +11,69 @@
1111
"""
1212

1313
from collections.abc import Iterable
14-
from typing import Any, Optional, TypeVar
14+
from typing import Any, Self, override
1515

1616
from langchain_core.documents import Document
1717
from langchain_core.embeddings import Embeddings
1818
from langchain_core.vectorstores import VectorStore
1919
from 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

2429
class 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

Comments
 (0)