hyperforge-nucliadb 1.0.0.post99__py3-none-any.whl → 1.0.0.post108__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,6 +3,7 @@ import itertools
3
3
  from typing import Dict, List, Optional, cast
4
4
  from uuid import uuid4
5
5
 
6
+ from hyperforge import logger
6
7
  from hyperforge.configure import agent
7
8
  from hyperforge.interaction import (
8
9
  Feedback,
@@ -109,13 +110,159 @@ class SyncAskAgent(BasicAskAgent):
109
110
  memory: QuestionMemory,
110
111
  manager: Manager,
111
112
  resources: Dict[str, List[str]],
113
+ ) -> Dict[str, Dict[str, List[str]]]:
114
+ connections = self.get_connections(manager)
115
+ filtered_resources: Dict[str, Dict[str, List[str]]] = {}
116
+ for kb_source_id, resource_ids in resources.items():
117
+ if connections[kb_source_id].config.connection_ids:
118
+ source_resources = await self._post_filter_configured_resources(
119
+ memory=memory,
120
+ manager=manager,
121
+ resources={kb_source_id: resource_ids},
122
+ )
123
+ else:
124
+ source_resources = await self._post_filter_hybrid_resources(
125
+ memory=memory,
126
+ manager=manager,
127
+ kb_source_id=kb_source_id,
128
+ resource_ids=resource_ids,
129
+ )
130
+ filtered_resources.update(source_resources)
131
+ return filtered_resources
132
+
133
+ async def _post_filter_hybrid_resources(
134
+ self,
135
+ memory: QuestionMemory,
136
+ manager: Manager,
137
+ kb_source_id: str,
138
+ resource_ids: List[str],
139
+ ) -> Dict[str, Dict[str, List[str]]]:
140
+ driver = self.sources[kb_source_id]
141
+ ndb = get_ndb_driver(manager, kb_source_id)
142
+ resource_objs: List[NucliaDBResource] = await asyncio.gather(
143
+ *[
144
+ ndb.driver.get_resource_by_id(
145
+ kbid=ndb.config.kbid,
146
+ rid=resource_id,
147
+ query_params={"show": [ResourceProperties.ORIGIN.value]},
148
+ )
149
+ for resource_id in resource_ids
150
+ ]
151
+ )
152
+
153
+ public_resource_ids: List[str] = []
154
+ connected_resources: Dict[str, List[str]] = {}
155
+ for resource_obj in resource_objs:
156
+ origin = resource_obj.origin
157
+ if origin is None:
158
+ public_resource_ids.append(resource_obj.id)
159
+ continue
160
+
161
+ if not origin.source_id or not origin.source_id.startswith("sync_config_"):
162
+ if origin.sync_metadata is None:
163
+ public_resource_ids.append(resource_obj.id)
164
+ else:
165
+ logger.warning(
166
+ "Excluding resource with inconsistent Sync origin metadata",
167
+ extra={"resource_id": resource_obj.id},
168
+ )
169
+ continue
170
+
171
+ source_id = origin.source_id
172
+ sync_config_id = source_id.removeprefix("sync_config_")
173
+ if not sync_config_id or origin.sync_metadata is None:
174
+ logger.warning(
175
+ "Excluding synced resource with incomplete origin metadata",
176
+ extra={"resource_id": resource_obj.id, "source_id": source_id},
177
+ )
178
+ continue
179
+ connected_resources.setdefault(sync_config_id, []).append(resource_obj.id)
180
+
181
+ resolved_resource_ids: List[str] = []
182
+ resolved_connection_ids: set[str] = set()
183
+ for sync_config_id, connection_resource_ids in connected_resources.items():
184
+ try:
185
+ await driver.resolve_sync_config(sync_config_id)
186
+ except Exception as exc:
187
+ logger.warning(
188
+ "Excluding resources for an unavailable sync configuration",
189
+ extra={
190
+ "sync_config_id": sync_config_id,
191
+ "resource_count": len(connection_resource_ids),
192
+ "exception_type": type(exc).__name__,
193
+ },
194
+ )
195
+ continue
196
+ resolved_resource_ids.extend(connection_resource_ids)
197
+ resolved_connection_ids.add(sync_config_id)
198
+
199
+ public_resource_id_set = set(public_resource_ids)
200
+ if not resolved_resource_ids:
201
+ return {
202
+ kb_source_id: {
203
+ "__hybrid__": [
204
+ resource_id
205
+ for resource_id in resource_ids
206
+ if resource_id in public_resource_id_set
207
+ ]
208
+ }
209
+ }
210
+
211
+ try:
212
+ connected_filtered_resources = await self._post_filter_configured_resources(
213
+ memory=memory,
214
+ manager=manager,
215
+ resources={kb_source_id: resolved_resource_ids},
216
+ allowed_connection_ids=resolved_connection_ids,
217
+ )
218
+ except Exception as exc:
219
+ logger.warning(
220
+ "Excluding synced resources after authorization failed",
221
+ extra={
222
+ "source_id": kb_source_id,
223
+ "exception_type": type(exc).__name__,
224
+ },
225
+ )
226
+ connected_filtered_resources = {}
227
+
228
+ authorized_resource_ids = {
229
+ resource_id
230
+ for connection_resource_ids in connected_filtered_resources.get(
231
+ kb_source_id, {}
232
+ ).values()
233
+ for resource_id in connection_resource_ids
234
+ }
235
+ return {
236
+ kb_source_id: {
237
+ "__hybrid__": [
238
+ resource_id
239
+ for resource_id in resource_ids
240
+ if resource_id in public_resource_id_set
241
+ or resource_id in authorized_resource_ids
242
+ ]
243
+ }
244
+ }
245
+
246
+ async def _post_filter_configured_resources(
247
+ self,
248
+ memory: QuestionMemory,
249
+ manager: Manager,
250
+ resources: Dict[str, List[str]],
251
+ allowed_connection_ids: Optional[set[str]] = None,
112
252
  ) -> Dict[str, Dict[str, List[str]]]:
113
253
  """We have a list of ARAG resources per KB source. Now we need to filter them by connection"""
114
254
  filtered_resources: Dict[str, Dict[str, List[str]]] = {}
115
- connections_by_resource: Dict[str, List[str]] = {}
116
- sync_metadata_by_resource: Dict[str, SyncMetadata] = {}
117
255
  connections = self.get_connections(manager)
118
256
  for kb_source_id, resource_ids in resources.items():
257
+ driver = self.sources[kb_source_id]
258
+ configured_connection_ids = (
259
+ allowed_connection_ids
260
+ if allowed_connection_ids is not None
261
+ else set(driver.config.connection_ids)
262
+ )
263
+ connections_by_resource: Dict[str, List[str]] = {}
264
+ sync_metadata_by_resource: Dict[str, SyncMetadata] = {}
265
+
119
266
  # Get resource source connection for each resource
120
267
  ndb = get_ndb_driver(manager, kb_source_id)
121
268
  get_resource_ids_tasks = []
@@ -131,24 +278,28 @@ class SyncAskAgent(BasicAskAgent):
131
278
  *get_resource_ids_tasks
132
279
  )
133
280
  for resource_obj in resource_objs:
281
+ origin = resource_obj.origin
134
282
  if (
135
- resource_obj.origin is not None
136
- and resource_obj.origin.source_id is not None
283
+ origin is None
284
+ or not origin.source_id
285
+ or not origin.source_id.startswith("sync_config_")
286
+ or origin.sync_metadata is None
137
287
  ):
138
- source_id = resource_obj.origin.source_id.replace(
139
- "sync_config_", ""
140
- )
141
- connections_by_resource.setdefault(source_id, []).append(
142
- resource_obj.id
143
- )
144
- if resource_obj.origin.sync_metadata is not None:
145
- sync_metadata_by_resource[resource_obj.id] = (
146
- resource_obj.origin.sync_metadata
147
- )
288
+ continue
289
+
290
+ source_id = origin.source_id.removeprefix("sync_config_")
291
+ if source_id not in configured_connection_ids:
292
+ continue
293
+
294
+ connections_by_resource.setdefault(source_id, []).append(
295
+ resource_obj.id
296
+ )
297
+ sync_metadata_by_resource[resource_obj.id] = origin.sync_metadata
148
298
 
149
299
  # Get providers needed for the resources allocated
150
300
  needed_providers_ids = list(connections_by_resource.keys())
151
- driver = self.sources[kb_source_id]
301
+ if not needed_providers_ids:
302
+ continue
152
303
 
153
304
  creds_providers = {}
154
305
  for sync_config_id in needed_providers_ids:
@@ -247,6 +398,8 @@ class SyncAskAgent(BasicAskAgent):
247
398
  connection_id,
248
399
  connection_credentials,
249
400
  ) in existing_credentials.items():
401
+ if connection_id not in connections_by_resource:
402
+ continue
250
403
  source = connections[kb_source_id]
251
404
  if (
252
405
  connection_id not in source.sync_configs
@@ -258,13 +411,19 @@ class SyncAskAgent(BasicAskAgent):
258
411
  raise Exception("Connection not found")
259
412
 
260
413
  credential = connection_credentials[inner_connection_id]
414
+ connection_resource_ids = connections_by_resource[connection_id]
415
+ connection_sync_metadata = {
416
+ resource_id: sync_metadata_by_resource[resource_id]
417
+ for resource_id in connection_resource_ids
418
+ if resource_id in sync_metadata_by_resource
419
+ }
261
420
  filtered_resource_ids = await self.validate_resources_by_connection(
262
421
  connection=source,
263
422
  credentials=credential,
264
- resource_ids=resource_ids,
423
+ resource_ids=connection_resource_ids,
265
424
  connection_id=inner_connection_id,
266
425
  sync_config_id=connection_id,
267
- sync_metadata_by_resource=sync_metadata_by_resource,
426
+ sync_metadata_by_resource=connection_sync_metadata,
268
427
  )
269
428
 
270
429
  filtered_resources.setdefault(kb_source_id, {})[connection_id] = (
@@ -279,13 +438,24 @@ class SyncAskAgent(BasicAskAgent):
279
438
  self,
280
439
  catalog_filter: Optional[CatalogFilterExpression] = None,
281
440
  ):
441
+ if any(
442
+ not self.sources[source].config.connection_ids
443
+ for source in self.config.sources
444
+ ):
445
+ return catalog_filter
446
+
447
+ connection_ids = [
448
+ connection_id
449
+ for source in self.config.sources
450
+ for connection_id in self.sources[source].config.connection_ids
451
+ ]
452
+
282
453
  if catalog_filter is None:
283
454
  catalog_filter = CatalogFilterExpression(
284
455
  resource=Or(
285
456
  operands=[
286
457
  OriginSource(id=f"sync_config_{connection_id}")
287
- for source in self.config.sources
288
- for connection_id in self.sources[source].sync_configs.keys()
458
+ for connection_id in connection_ids
289
459
  ]
290
460
  )
291
461
  )
@@ -296,10 +466,7 @@ class SyncAskAgent(BasicAskAgent):
296
466
  Or(
297
467
  operands=[
298
468
  OriginSource(id=f"sync_config_{connection_id}")
299
- for source in self.config.sources
300
- for connection_id in self.sources[
301
- source
302
- ].sync_configs.keys()
469
+ for connection_id in connection_ids
303
470
  ]
304
471
  ),
305
472
  ]
@@ -316,6 +483,7 @@ class SyncAskAgent(BasicAskAgent):
316
483
  catalog_filter: Optional[CatalogFilterExpression] = None,
317
484
  **kwargs,
318
485
  ) -> Dict[str, List[str]]:
486
+ self.get_connections(manager)
319
487
  catalog_filter = self.enrich_catalog_filter(catalog_filter)
320
488
  resources = await super().search_by_title(
321
489
  memory=memory,
@@ -2,6 +2,7 @@ from typing import Literal
2
2
 
3
3
  from httpx import Timeout
4
4
  from hyperforge.driver import DriverConfig
5
+ from pydantic import Field, field_validator
5
6
  from pydantic.config import ConfigDict
6
7
 
7
8
  from hyperforge_nucliadb.driver_config import (
@@ -12,7 +13,21 @@ SYNC_HTTP_TIMEOUT = Timeout(connect=5.0, read=30.0, write=10.0, pool=5.0)
12
13
 
13
14
 
14
15
  class SyncConnection(NucliaDBConnection):
15
- connection_ids: list[str]
16
+ connection_ids: list[str] = Field(
17
+ default_factory=list,
18
+ description=(
19
+ "Sync configuration IDs to restrict searches to. When omitted or empty, "
20
+ "the entire knowledge box is searched and synced resources are authorized "
21
+ "individually."
22
+ ),
23
+ )
24
+
25
+ @field_validator("connection_ids")
26
+ @classmethod
27
+ def validate_connection_ids(cls, connection_ids: list[str]) -> list[str]:
28
+ if any(not connection_id.strip() for connection_id in connection_ids):
29
+ raise ValueError("connection_ids cannot contain blank values")
30
+ return connection_ids
16
31
 
17
32
  @property
18
33
  def kb_url(self) -> str:
@@ -90,29 +90,47 @@ class SyncDriver(NucliaDBDriver):
90
90
  async def init(cls, driver: Any) -> "SyncDriver":
91
91
  sync_driver = cast(SyncDriverConfig, driver)
92
92
  client = await sync_connect(sync_driver.config)
93
- information = {}
94
- sync_configs: Dict[str, list[str]] = {}
95
- for sync_config_id in sync_driver.config.connection_ids:
96
- response = await client.get(f"/sync_config/{sync_config_id}")
97
- response.raise_for_status()
98
- data = response.json()
99
- connection_id = data["external_connection"]["id"]
100
- response = await client.get(f"/external_connection/{connection_id}")
101
- response.raise_for_status()
102
- sync_configs.setdefault(sync_config_id, []).append(connection_id)
103
- data = response.json()
104
- information[connection_id] = ExternalConnectionOutput(**data)
105
- return cls(
93
+ instance = cls(
106
94
  provider=sync_driver.provider,
107
95
  name=sync_driver.name,
108
96
  async_driver=client,
109
- information=information,
110
- sync_configs=sync_configs,
97
+ information={},
98
+ sync_configs={},
111
99
  config=sync_driver.config,
112
100
  driver=await connect(cast(NucliaDBConnection, sync_driver.config)),
113
101
  manager=await manager_connect(cast(NucliaDBConnection, sync_driver.config)),
114
102
  _synonyms=None,
115
103
  )
104
+ for sync_config_id in sync_driver.config.connection_ids:
105
+ await instance.resolve_sync_config(sync_config_id)
106
+ return instance
107
+
108
+ async def resolve_sync_config(
109
+ self, sync_config_id: str
110
+ ) -> ExternalConnectionOutput:
111
+ if (
112
+ self.config.connection_ids
113
+ and sync_config_id not in self.config.connection_ids
114
+ ):
115
+ raise ValueError(
116
+ f"Sync configuration {sync_config_id} is not configured for this driver"
117
+ )
118
+
119
+ existing_connection_ids = self.sync_configs.get(sync_config_id, [])
120
+ if existing_connection_ids:
121
+ return self.information[existing_connection_ids[0]]
122
+
123
+ UUID(sync_config_id)
124
+ response = await self.async_driver.get(f"/sync_config/{sync_config_id}")
125
+ response.raise_for_status()
126
+ connection_id = response.json()["external_connection"]["id"]
127
+
128
+ response = await self.async_driver.get(f"/external_connection/{connection_id}")
129
+ response.raise_for_status()
130
+ information = ExternalConnectionOutput(**response.json())
131
+ self.sync_configs[sync_config_id] = [connection_id]
132
+ self.information[connection_id] = information
133
+ return information
116
134
 
117
135
  async def get_oauth_url(
118
136
  self,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperforge_nucliadb
3
- Version: 1.0.0.post99
3
+ Version: 1.0.0.post108
4
4
  Summary: NucliaDB Hyperforge agent
5
5
  Author-email: Nuclia <nucliadb@nuclia.com>
6
6
  License-Expression: Apache-2.0
@@ -22,11 +22,11 @@ hyperforge_nucliadb/ask/query_analysis.py,sha256=pg_0MKLR-oVkt_mo8pm4fMEdW5ZMMqd
22
22
  hyperforge_nucliadb/ask/rerank.py,sha256=6fjjZ_B_d86RilohNCIac_cniuMTvXzFipel_WPfEMg,1326
23
23
  hyperforge_nucliadb/ask/utils.py,sha256=LxvFo3M1lRDN9hBvsZGh5DU-G9U0ZZ7a13z0zMjrymE,1102
24
24
  hyperforge_nucliadb/sync/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
- hyperforge_nucliadb/sync/agent.py,sha256=iyijdMa8UwkJ1R6fPxeZUJovvGe_8vvIf-PZPXKTwr4,18356
25
+ hyperforge_nucliadb/sync/agent.py,sha256=3WFjZ2Xv1YL9XmcOLeaGM5qJbeWw1KDW85VpLEQTmzg,24816
26
26
  hyperforge_nucliadb/sync/config.py,sha256=kmf6mM-IFlnlmsXqrZHzTUIa3LCLipBCH7tp8brVyuo,319
27
- hyperforge_nucliadb/sync/config_driver.py,sha256=wfUtgCEoQC4xh4mo0ASFi4kGrWXy11pokL2aBk3_xHs,641
28
- hyperforge_nucliadb/sync/driver.py,sha256=hi4ROnZSfUCsCiyIhMObTGpIk55s2w82f9W5xglzLOI,9288
29
- hyperforge_nucliadb-1.0.0.post99.dist-info/METADATA,sha256=ymt63DoIw2C9VmYGsPTWxa-25ki9BQ3cA4_OhgiQ1go,1012
30
- hyperforge_nucliadb-1.0.0.post99.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
31
- hyperforge_nucliadb-1.0.0.post99.dist-info/top_level.txt,sha256=jtg2wNgOFs27BzB5Gu5xGfoeL2txFEZTzr0QCHzNROk,20
32
- hyperforge_nucliadb-1.0.0.post99.dist-info/RECORD,,
27
+ hyperforge_nucliadb/sync/config_driver.py,sha256=msBONAAQw2qfVrpyL3Ew9wWXXhwJZgdQ033sWZg5isk,1284
28
+ hyperforge_nucliadb/sync/driver.py,sha256=jPWQNMBOQVsZV-tFXNg2Qp1e_fX2hzIvJawLPAW2wTw,9859
29
+ hyperforge_nucliadb-1.0.0.post108.dist-info/METADATA,sha256=xeJs-BPyKOs0D7QQsah3Fi8joj2muZ2kP5ZYzKwgK78,1013
30
+ hyperforge_nucliadb-1.0.0.post108.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
31
+ hyperforge_nucliadb-1.0.0.post108.dist-info/top_level.txt,sha256=jtg2wNgOFs27BzB5Gu5xGfoeL2txFEZTzr0QCHzNROk,20
32
+ hyperforge_nucliadb-1.0.0.post108.dist-info/RECORD,,