Skip to content

Commit 34e1ed2

Browse files
committed
feat : refactored ContextGroundingVectorStore to improve type safety
1 parent 868fd91 commit 34e1ed2

File tree

2 files changed

+90
-110
lines changed

2 files changed

+90
-110
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath-langchain"
3-
version = "0.0.146"
3+
version = "0.0.147"
44
description = "UiPath Langchain"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.10"

src/uipath_langchain/vectorstores/context_grounding_vectorstore.py

Lines changed: 89 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -2,66 +2,50 @@
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
159

1610
from langchain_core.documents import Document
1711
from langchain_core.embeddings import Embeddings
1812
from langchain_core.vectorstores import VectorStore
13+
from typing_extensions import override
1914
from uipath import UiPath
20-
21-
VST = TypeVar("VST", bound="ContextGroundingVectorStore")
15+
from uipath.models.context_grounding import ContextGroundingQueryResponse
2216

2317

2418
class ContextGroundingVectorStore(VectorStore):
2519
"""Vector store that uses UiPath Context Grounding (ECS) as a backend.
2620
2721
This class provides a straightforward implementation that connects to the
2822
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-
)
4223
"""
4324

4425
def __init__(
4526
self,
4627
index_name: str,
47-
folder_path: Optional[str] = None,
48-
uipath_sdk: Optional[UiPath] = None,
28+
uipath_sdk: UiPath | None = None,
29+
folder_path: str | None = None,
4930
):
5031
"""Initialize the ContextGroundingVectorStore.
5132
5233
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.
34+
index_name: Name of the context grounding index to use (schema name)
35+
uipath_sdk: Optional UiPath SDK instance.
36+
folder_path: Optional folder path for folder-scoped operations
5537
"""
5638
self.index_name = index_name
5739
self.folder_path = folder_path
5840
self.sdk = uipath_sdk or UiPath()
5941

42+
# VectorStore implementation methods
43+
44+
@override
6045
def similarity_search_with_score(
6146
self, query: str, k: int = 4, **kwargs: Any
6247
) -> list[tuple[Document, float]]:
6348
"""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.
6549
6650
Args:
6751
query: The query string
@@ -70,52 +54,24 @@ def similarity_search_with_score(
7054
Returns:
7155
list of tuples of (document, score)
7256
"""
73-
# Call the UiPath SDK to perform the search
74-
results = self.sdk.context_grounding.search(
75-
name=self.index_name,
76-
query=query,
77-
number_of_results=k,
78-
folder_path=self.folder_path,
79-
)
80-
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,
57+
# Use the context grounding service to perform search
58+
results: list[ContextGroundingQueryResponse] = (
59+
self.sdk.context_grounding.search(
60+
name=self.index_name,
61+
query=query,
62+
number_of_results=k,
63+
folder_path=self.folder_path,
10364
)
65+
)
10466

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
@@ -201,6 +129,7 @@ async def asimilarity_search_with_relevance_scores(
201129
)
202130
]
203131

132+
@override
204133
def similarity_search(
205134
self, query: str, k: int = 4, **kwargs: Any
206135
) -> list[Document]:
@@ -216,6 +145,7 @@ def similarity_search(
216145
docs_and_scores = self.similarity_search_with_score(query, k, **kwargs)
217146
return [doc for doc, _ in docs_and_scores]
218147

148+
@override
219149
async def asimilarity_search(
220150
self, query: str, k: int = 4, **kwargs: Any
221151
) -> list[Document]:
@@ -231,38 +161,88 @@ async def asimilarity_search(
231161
docs_and_scores = await self.asimilarity_search_with_score(query, k, **kwargs)
232162
return [doc for doc, _ in docs_and_scores]
233163

164+
def _convert_results_to_documents(
165+
self, results: list[ContextGroundingQueryResponse]
166+
) -> list[tuple[Document, float]]:
167+
"""Convert API results to Document objects with scores.
168+
169+
Args:
170+
results: List of ContextGroundingQueryResponse objects
171+
172+
Returns:
173+
List of tuples containing (Document, score)
174+
"""
175+
docs_with_scores = []
176+
177+
for result in results:
178+
# Create metadata from result fields
179+
metadata = {}
180+
181+
# Add string fields with proper defaults
182+
if result.source:
183+
metadata["source"] = str(result.source)
184+
if result.reference:
185+
metadata["reference"] = str(result.reference)
186+
if result.page_number:
187+
metadata["page_number"] = str(result.page_number)
188+
if result.source_document_id:
189+
metadata["source_document_id"] = str(result.source_document_id)
190+
if result.caption:
191+
metadata["caption"] = str(result.caption)
192+
193+
# Add any operation metadata if available
194+
if result.metadata:
195+
if result.metadata.operation_id:
196+
metadata["operation_id"] = str(result.metadata.operation_id)
197+
if result.metadata.strategy:
198+
metadata["strategy"] = str(result.metadata.strategy)
199+
200+
# Create a Document with the content and metadata
201+
doc = Document(
202+
page_content=result.content or "",
203+
metadata=metadata,
204+
)
205+
206+
# Convert score to distance (1 - score)
207+
score = 1.0 - float(result.score or 0.0)
208+
209+
docs_with_scores.append((doc, score))
210+
211+
return docs_with_scores
212+
234213
@classmethod
214+
@override
235215
def from_texts(
236-
cls: type[VST],
216+
cls,
237217
texts: list[str],
238218
embedding: Embeddings,
239-
metadatas: Optional[list[dict[str, Any]]] = None,
219+
metadatas: list[dict[str, Any]] | None = None,
240220
**kwargs: Any,
241-
) -> VST:
221+
) -> Self:
242222
"""This method is required by the VectorStore abstract class, but is not supported
243223
by ContextGroundingVectorStore which is read-only.
244224
245225
Raises:
246226
NotImplementedError: This method is not supported by ContextGroundingVectorStore
247227
"""
248228
raise NotImplementedError(
249-
"ContextGroundingVectorStore is a read-only wrapper for UiPath Context Grounding. "
250-
"Creating a vector store from texts is not supported."
229+
"ContextGroundingVectorStore is a read-only wrapper for UiPath Context Grounding."
251230
)
252231

253-
# Other required methods with minimal implementation to satisfy the interface
232+
@override
254233
def add_texts(
255234
self,
256235
texts: Iterable[str],
257-
metadatas: Optional[list[dict[str, Any]]] = None,
236+
metadatas: list[dict[str, Any]] | None = None,
258237
**kwargs: Any,
259238
) -> list[str]:
260239
"""Not implemented for ContextGroundingVectorStore as this is a read-only wrapper."""
261240
raise NotImplementedError(
262241
"ContextGroundingVectorStore is a read-only wrapper for UiPath Context Grounding."
263242
)
264243

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

0 commit comments

Comments
 (0)