Skip to content

Commit 365c57e

Browse files
committed
Update context grounding vector store to be compatible with gym
1 parent 67329d1 commit 365c57e

File tree

1 file changed

+90
-119
lines changed

1 file changed

+90
-119
lines changed

src/uipath_langchain/vectorstores/context_grounding_vectorstore.py

Lines changed: 90 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -2,66 +2,52 @@
22
Vector store implementation that connects to UiPath Context Grounding as a backend.
33
44
This is a read-only vector store that uses the UiPath Context Grounding API to retrieve documents.
5-
6-
You need to set the following environment variables (also see .env.example):
7-
### - UIPATH_URL="https://alpha.uipath.com/{ORG_ID}/{TENANT_ID}"
8-
### - UIPATH_ACCESS_TOKEN={BEARER_TOKEN_WITH_CONTEXT_GROUNDING_PERMISSIONS}
9-
### - UIPATH_FOLDER_PATH="" - this can be left empty
10-
### - UIPATH_FOLDER_KEY="" - this can be left empty
115
"""
126

137
from collections.abc import Iterable
14-
from typing import Any, Optional, TypeVar
8+
from typing import Any, Self, TypeVar, override
159

1610
from langchain_core.documents import Document
1711
from langchain_core.embeddings import Embeddings
1812
from langchain_core.vectorstores import VectorStore
1913
from uipath import UiPath
20-
21-
VST = TypeVar("VST", bound="ContextGroundingVectorStore")
22-
14+
from uipath.models.context_grounding import ContextGroundingQueryResponse
2315

2416
class ContextGroundingVectorStore(VectorStore):
2517
"""Vector store that uses UiPath Context Grounding (ECS) as a backend.
2618
2719
This class provides a straightforward implementation that connects to the
2820
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-
)
4221
"""
4322

23+
4424
def __init__(
4525
self,
4626
index_name: str,
47-
folder_path: Optional[str] = None,
48-
uipath_sdk: Optional[UiPath] = None,
27+
uipath_sdk: UiPath | None = None,
28+
folder_path: str | None = None,
4929
):
5030
"""Initialize the ContextGroundingVectorStore.
5131
5232
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.
33+
index_name: Name of the context grounding index to use (schema name)
34+
uipath_sdk: Optional UiPath SDK instance.
35+
folder_path: Optional folder path for folder-scoped operations
5536
"""
37+
self.index_name = index_name
38+
self.folder_path = folder_path
39+
5640
self.index_name = index_name
5741
self.folder_path = folder_path
5842
self.sdk = uipath_sdk or UiPath()
5943

44+
# VectorStore implementation methods
45+
46+
@override
6047
def similarity_search_with_score(
6148
self, query: str, k: int = 4, **kwargs: Any
6249
) -> list[tuple[Document, float]]:
6350
"""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.
6551
6652
Args:
6753
query: The query string
@@ -70,52 +56,22 @@ def similarity_search_with_score(
7056
Returns:
7157
list of tuples of (document, score)
7258
"""
73-
# Call the UiPath SDK to perform the search
74-
results = self.sdk.context_grounding.search(
59+
# Use the context grounding service to perform search
60+
results: list[ContextGroundingQueryResponse] = self.sdk.context_grounding.search(
7561
name=self.index_name,
7662
query=query,
7763
number_of_results=k,
7864
folder_path=self.folder_path,
7965
)
8066

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
67+
return self._convert_results_to_documents(results)
11068

69+
@override
11170
def similarity_search_with_relevance_scores(
11271
self, query: str, k: int = 4, **kwargs: Any
11372
) -> list[tuple[Document, float]]:
11473
"""Return documents along with their relevance scores on a scale from 0 to 1.
11574
116-
This directly uses the scores provided by the Context Grounding API,
117-
which are already normalized between 0 and 1.
118-
11975
Args:
12076
query: The query string
12177
k: Number of documents to return (default=4)
@@ -128,6 +84,7 @@ def similarity_search_with_relevance_scores(
12884
for doc, score in self.similarity_search_with_score(query, k, **kwargs)
12985
]
13086

87+
@override
13188
async def asimilarity_search_with_score(
13289
self, query: str, k: int = 4, **kwargs: Any
13390
) -> list[tuple[Document, float]]:
@@ -140,52 +97,23 @@ async def asimilarity_search_with_score(
14097
Returns:
14198
list of tuples of (document, score)
14299
"""
143-
# Call the UiPath SDK to perform the search asynchronously
144-
results = await self.sdk.context_grounding.search_async(
100+
# Use the context grounding service to perform async search
101+
results: list[
102+
ContextGroundingQueryResponse
103+
] = await self.sdk.context_grounding.search_async(
145104
name=self.index_name,
146105
query=query,
147106
number_of_results=k,
148107
folder_path=self.folder_path,
149108
)
150109

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
110+
return self._convert_results_to_documents(results)
181111

112+
@override
182113
async def asimilarity_search_with_relevance_scores(
183114
self, query: str, k: int = 4, **kwargs: Any
184115
) -> 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.
116+
"""Asynchronously return documents along with their relevance scores.
189117
190118
Args:
191119
query: The query string
@@ -196,14 +124,11 @@ async def asimilarity_search_with_relevance_scores(
196124
"""
197125
return [
198126
(doc, 1.0 - score)
199-
for doc, score in await self.asimilarity_search_with_score(
200-
query, k, **kwargs
201-
)
127+
for doc, score in await self.asimilarity_search_with_score(query, k, **kwargs)
202128
]
203129

204-
def similarity_search(
205-
self, query: str, k: int = 4, **kwargs: Any
206-
) -> list[Document]:
130+
@override
131+
def similarity_search(self, query: str, k: int = 4, **kwargs: Any) -> list[Document]:
207132
"""Return documents most similar to the query.
208133
209134
Args:
@@ -216,9 +141,8 @@ def similarity_search(
216141
docs_and_scores = self.similarity_search_with_score(query, k, **kwargs)
217142
return [doc for doc, _ in docs_and_scores]
218143

219-
async def asimilarity_search(
220-
self, query: str, k: int = 4, **kwargs: Any
221-
) -> list[Document]:
144+
@override
145+
async def asimilarity_search(self, query: str, k: int = 4, **kwargs: Any) -> list[Document]:
222146
"""Asynchronously return documents most similar to the query.
223147
224148
Args:
@@ -231,38 +155,85 @@ async def asimilarity_search(
231155
docs_and_scores = await self.asimilarity_search_with_score(query, k, **kwargs)
232156
return [doc for doc, _ in docs_and_scores]
233157

158+
def _convert_results_to_documents(
159+
self, results: list[ContextGroundingQueryResponse]
160+
) -> list[tuple[Document, float]]:
161+
"""Convert API results to Document objects with scores.
162+
163+
Args:
164+
results: List of ContextGroundingQueryResponse objects
165+
166+
Returns:
167+
List of tuples containing (Document, score)
168+
"""
169+
docs_with_scores = []
170+
171+
for result in results:
172+
# Create metadata from result fields
173+
metadata = {}
174+
175+
# Add string fields with proper defaults
176+
if result.source:
177+
metadata["source"] = str(result.source)
178+
if result.reference:
179+
metadata["reference"] = str(result.reference)
180+
if result.page_number:
181+
metadata["page_number"] = str(result.page_number)
182+
if result.source_document_id:
183+
metadata["source_document_id"] = str(result.source_document_id)
184+
if result.caption:
185+
metadata["caption"] = str(result.caption)
186+
187+
# Add any operation metadata if available
188+
if result.metadata:
189+
if result.metadata.operation_id:
190+
metadata["operation_id"] = str(result.metadata.operation_id)
191+
if result.metadata.strategy:
192+
metadata["strategy"] = str(result.metadata.strategy)
193+
194+
# Create a Document with the content and metadata
195+
doc = Document(
196+
page_content=result.content or "",
197+
metadata=metadata,
198+
)
199+
200+
# Convert score to distance (1 - score)
201+
score = 1.0 - float(result.score or 0.0)
202+
203+
docs_with_scores.append((doc, score))
204+
205+
return docs_with_scores
206+
234207
@classmethod
208+
@override
235209
def from_texts(
236-
cls: type[VST],
210+
cls,
237211
texts: list[str],
238212
embedding: Embeddings,
239-
metadatas: Optional[list[dict[str, Any]]] = None,
213+
metadatas: list[dict] | None = None,
240214
**kwargs: Any,
241-
) -> VST:
215+
) -> Self:
242216
"""This method is required by the VectorStore abstract class, but is not supported
243217
by ContextGroundingVectorStore which is read-only.
244218
245219
Raises:
246220
NotImplementedError: This method is not supported by ContextGroundingVectorStore
247221
"""
248222
raise NotImplementedError(
249-
"ContextGroundingVectorStore is a read-only wrapper for UiPath Context Grounding. "
250-
"Creating a vector store from texts is not supported."
223+
"ContextGroundingVectorStore is a read-only wrapper for UiPath Context Grounding."
251224
)
252225

253-
# Other required methods with minimal implementation to satisfy the interface
226+
@override
254227
def add_texts(
255-
self,
256-
texts: Iterable[str],
257-
metadatas: Optional[list[dict[str, Any]]] = None,
258-
**kwargs: Any,
228+
self, texts: Iterable[str], metadatas: list[dict] | None = None, **kwargs: Any
259229
) -> list[str]:
260230
"""Not implemented for ContextGroundingVectorStore as this is a read-only wrapper."""
261231
raise NotImplementedError(
262232
"ContextGroundingVectorStore is a read-only wrapper for UiPath Context Grounding."
263-
)
233+
) # type: ignore[unreachable]
264234

265-
def delete(self, ids: Optional[list[str]] = None, **kwargs: Any) -> Optional[bool]:
235+
@override
236+
def delete(self, ids: list[str] | None = None, **kwargs: Any) -> bool | None:
266237
"""Not implemented for ContextGroundingVectorStore as this is a read-only wrapper."""
267238
raise NotImplementedError(
268239
"ContextGroundingVectorStore is a read-only wrapper for UiPath Context Grounding."

0 commit comments

Comments
 (0)