hyperforge-nucliadb 1.0.0.post22__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.
@@ -0,0 +1,477 @@
1
+ import asyncio
2
+ import itertools
3
+ from typing import Dict, List, Optional, cast
4
+ from uuid import uuid4
5
+
6
+ from hyperforge.configure import agent
7
+ from hyperforge.interaction import (
8
+ Feedback,
9
+ OAuthAuthenticateURL,
10
+ OAuthFeedbackReturnSchema,
11
+ )
12
+ from hyperforge.manager import Manager
13
+ from hyperforge.memory import QuestionMemory
14
+ from hyperforge.models import Context, Source
15
+ from hyperforge.settings import OAuthSettings
16
+ from nucliadb_models import SyncMetadata
17
+ from nucliadb_models.filters import (
18
+ And,
19
+ CatalogFilterExpression,
20
+ FilterExpression,
21
+ Or,
22
+ OriginSource,
23
+ )
24
+ from nucliadb_models.resource import Resource as NucliaDBResource
25
+ from nucliadb_models.search import (
26
+ ResourceProperties,
27
+ )
28
+
29
+ from hyperforge_nucliadb.basic_ask_agent import (
30
+ BasicAskAgent,
31
+ get_ndb_driver,
32
+ )
33
+ from hyperforge_nucliadb.sync.config import SyncAskAgentConfig
34
+ from hyperforge_nucliadb.sync.driver import SyncDriver
35
+
36
+
37
+ @agent(
38
+ id="sync",
39
+ agent_type="context",
40
+ title="Sync Service Ask Agent",
41
+ description="Provide answer questions from synched resources. This agent is responsible for handling the authentication flow and resource validation for synched resources, and then providing the valid resources to the Basic Ask Agent to answer questions.",
42
+ config_schema=SyncAskAgentConfig,
43
+ )
44
+ class SyncAskAgent(BasicAskAgent): # type: ignore
45
+ sources: Dict[str, SyncDriver]
46
+ settings: OAuthSettings = OAuthSettings()
47
+
48
+ def __init__(
49
+ self, config: SyncAskAgentConfig, agent_id: Optional[str] = None
50
+ ) -> None:
51
+ super().__init__(config, agent_id)
52
+ self.sources = {}
53
+
54
+ def get_connections(self, manager: Manager) -> Dict[str, SyncDriver]:
55
+ for source in self.config.sources:
56
+ driver = manager.drivers.get(source)
57
+ driver = cast(SyncDriver, driver)
58
+ self.sources[source] = driver
59
+ return self.sources
60
+
61
+ async def get_oauth_url(
62
+ self,
63
+ manager: Manager,
64
+ connection_ids: List[str],
65
+ rao_redirect_url: str,
66
+ oauth_uuid: str,
67
+ kb_source_id: str,
68
+ question_id: str,
69
+ ) -> Dict[str, str]:
70
+ urls = {}
71
+ driver = cast(SyncDriver, manager.drivers.get(kb_source_id))
72
+ for connection_id in connection_ids:
73
+ if (
74
+ connection_id in driver.sync_configs
75
+ and len(driver.sync_configs[connection_id]) > 0
76
+ ):
77
+ real_connection_id = driver.sync_configs[connection_id][0]
78
+ url = await driver.get_oauth_url(
79
+ connection_id=real_connection_id,
80
+ rao_redirect_url=rao_redirect_url,
81
+ oauth_uuid=oauth_uuid,
82
+ sync_config=connection_id,
83
+ question_id=question_id,
84
+ )
85
+ urls[connection_id] = url
86
+ return urls
87
+
88
+ async def validate_resources_by_connection(
89
+ self,
90
+ credentials: str,
91
+ resource_ids: List[str],
92
+ connection: SyncDriver,
93
+ connection_id: str,
94
+ sync_config_id: str,
95
+ sync_metadata_by_resource: Dict[str, SyncMetadata],
96
+ ) -> List[str]:
97
+ # Validate resources
98
+ validated_resources = await connection.validate_resources(
99
+ resource_ids,
100
+ credentials=credentials,
101
+ connection_id=connection_id,
102
+ sync_config_id=sync_config_id,
103
+ sync_metadata_by_resource=sync_metadata_by_resource,
104
+ )
105
+ return validated_resources
106
+
107
+ async def post_filter_resources_by_connection(
108
+ self,
109
+ memory: QuestionMemory,
110
+ manager: Manager,
111
+ resources: Dict[str, List[str]],
112
+ ) -> Dict[str, Dict[str, List[str]]]:
113
+ """We have a list of ARAG resources per KB source. Now we need to filter them by connection"""
114
+ 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
+ connections = self.get_connections(manager)
118
+ for kb_source_id, resource_ids in resources.items():
119
+ # Get resource source connection for each resource
120
+ ndb = get_ndb_driver(manager, kb_source_id)
121
+ get_resource_ids_tasks = []
122
+ for resource in resource_ids:
123
+ get_resource_ids_tasks.append(
124
+ ndb.driver.get_resource_by_id(
125
+ kbid=ndb.config.kbid,
126
+ rid=resource,
127
+ query_params={"show": [ResourceProperties.ORIGIN.value]},
128
+ )
129
+ )
130
+ resource_objs: List[NucliaDBResource] = await asyncio.gather(
131
+ *get_resource_ids_tasks
132
+ )
133
+ for resource_obj in resource_objs:
134
+ if (
135
+ resource_obj.origin is not None
136
+ and resource_obj.origin.source_id is not None
137
+ ):
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
+ )
148
+
149
+ # Get providers needed for the resources allocated
150
+ needed_providers_ids = list(connections_by_resource.keys())
151
+ driver = self.sources[kb_source_id]
152
+
153
+ creds_providers = {}
154
+ for sync_config_id in needed_providers_ids:
155
+ if (
156
+ sync_config_id not in driver.sync_configs
157
+ or len(driver.sync_configs[sync_config_id]) == 0
158
+ ):
159
+ raise Exception(
160
+ f"Connection ID {sync_config_id} not found in driver sync configs"
161
+ )
162
+ inner_connection_id = driver.sync_configs[sync_config_id][0]
163
+ creds_providers[sync_config_id] = driver.information[
164
+ inner_connection_id
165
+ ].provider
166
+
167
+ # First message, ask for OAuth credentials
168
+ oauth_credentials_feedback = Feedback(
169
+ question="Get credentials",
170
+ data=None,
171
+ get_credentials=creds_providers,
172
+ module="oauth",
173
+ request_id=memory.get_session_id(),
174
+ agent_id=self.agent_id,
175
+ response_schema=OAuthFeedbackReturnSchema.model_json_schema(),
176
+ )
177
+ answer = await memory.send_feedback(oauth_credentials_feedback)
178
+ if answer is None or answer.request_id is None:
179
+ raise Exception("No answer received for OAuth initiation")
180
+ assert answer.request_id == memory.get_session_id()
181
+ # Validate answer schema
182
+ try:
183
+ oauth_feedback_return = OAuthFeedbackReturnSchema.model_validate_json(
184
+ answer.response
185
+ )
186
+ except Exception as e:
187
+ raise Exception(f"Invalid OAuth feedback response: {e}")
188
+
189
+ existing_credentials: Dict[str, Dict[str, str]] = {}
190
+ if (
191
+ oauth_feedback_return.existing_credentials is None
192
+ or len(oauth_feedback_return.existing_credentials) == 0
193
+ ):
194
+ # The frontend does not have credentials yet, we need to perform the OAuth flow
195
+
196
+ oauth_uuid = uuid4().hex
197
+ rao_redirect_url = self.settings.rao_redirect_url.format(
198
+ agent_id=memory.get_agent_id(),
199
+ session_id=memory.get_session_id(),
200
+ oauth_uuid=oauth_uuid,
201
+ workflow_id=memory.get_workflow_id(),
202
+ )
203
+ # Get all the authorize URLs from all connections on this KB
204
+ urls = await self.get_oauth_url(
205
+ manager,
206
+ needed_providers_ids,
207
+ rao_redirect_url=rao_redirect_url,
208
+ oauth_uuid=oauth_uuid,
209
+ kb_source_id=kb_source_id,
210
+ question_id=memory.original_question_uuid,
211
+ )
212
+ for connection_id, url in urls.items():
213
+ # For each connection send the OAuth URL to the frontend
214
+ oauth_authenticate_url = OAuthAuthenticateURL(oauth_url=url)
215
+ await memory.send_oauth(oauth=oauth_authenticate_url)
216
+
217
+ credential = await memory.recv_oauth_callback(
218
+ question_id=memory.original_question_uuid, oauth_uuid=oauth_uuid
219
+ )
220
+ if credential is not None:
221
+ inner_conn_id = driver.sync_configs[connection_id][0]
222
+ existing_credentials[connection_id] = {
223
+ inner_conn_id: credential
224
+ }
225
+
226
+ # Send OAuth initiation feedback
227
+ oauth_credentials_feedback = Feedback(
228
+ question="Send credentials",
229
+ data=None,
230
+ get_credentials=None,
231
+ credentials=existing_credentials,
232
+ module="oauth",
233
+ request_id=memory.get_session_id(),
234
+ agent_id=self.agent_id,
235
+ response_schema=OAuthFeedbackReturnSchema.model_json_schema(),
236
+ )
237
+ answer = await memory.send_feedback(oauth_credentials_feedback)
238
+ if answer is None or answer.request_id is None:
239
+ raise Exception("No answer received for OAuth initiation")
240
+ assert answer.request_id == memory.get_session_id()
241
+ elif oauth_feedback_return.existing_credentials is not None:
242
+ existing_credentials = oauth_feedback_return.existing_credentials
243
+ else:
244
+ raise Exception("No credentials provided")
245
+
246
+ for (
247
+ connection_id,
248
+ connection_credentials,
249
+ ) in existing_credentials.items():
250
+ source = connections[kb_source_id]
251
+ if (
252
+ connection_id not in source.sync_configs
253
+ or len(source.sync_configs[connection_id]) == 0
254
+ ):
255
+ raise Exception("Connection / source not found")
256
+ inner_connection_id = source.sync_configs[connection_id][0]
257
+ if inner_connection_id is None or connection_credentials is None:
258
+ raise Exception("Connection not found")
259
+
260
+ credential = connection_credentials[inner_connection_id]
261
+ filtered_resource_ids = await self.validate_resources_by_connection(
262
+ connection=source,
263
+ credentials=credential,
264
+ resource_ids=resource_ids,
265
+ connection_id=inner_connection_id,
266
+ sync_config_id=connection_id,
267
+ sync_metadata_by_resource=sync_metadata_by_resource,
268
+ )
269
+
270
+ filtered_resources.setdefault(kb_source_id, {})[connection_id] = (
271
+ filtered_resource_ids
272
+ )
273
+ return filtered_resources
274
+
275
+ def enrich_filter(self) -> FilterExpression:
276
+ return FilterExpression()
277
+
278
+ def enrich_catalog_filter(
279
+ self,
280
+ catalog_filter: Optional[CatalogFilterExpression] = None,
281
+ ):
282
+ if catalog_filter is None:
283
+ catalog_filter = CatalogFilterExpression(
284
+ resource=Or(
285
+ operands=[
286
+ 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()
289
+ ]
290
+ )
291
+ )
292
+ else:
293
+ catalog_filter.resource = And(
294
+ operands=[
295
+ catalog_filter.resource,
296
+ Or(
297
+ operands=[
298
+ 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()
303
+ ]
304
+ ),
305
+ ]
306
+ )
307
+ return catalog_filter
308
+
309
+ # PUBLIC METHODS USED BY THE BASIC ASK AGENT FRAMEWORK
310
+ async def search_by_title(
311
+ self,
312
+ memory: QuestionMemory,
313
+ manager: Manager,
314
+ title: str,
315
+ filters: Optional[List[str]] = None,
316
+ catalog_filter: Optional[CatalogFilterExpression] = None,
317
+ **kwargs,
318
+ ) -> Dict[str, List[str]]:
319
+ catalog_filter = self.enrich_catalog_filter(catalog_filter)
320
+ resources = await super().search_by_title(
321
+ memory=memory,
322
+ manager=manager,
323
+ title=title,
324
+ filters=filters,
325
+ catalog_filter=catalog_filter,
326
+ **kwargs,
327
+ )
328
+ resources_by_connection = await self.post_filter_resources_by_connection(
329
+ memory=memory,
330
+ manager=manager,
331
+ resources=resources,
332
+ )
333
+ final_resources: Dict[str, List[str]] = {}
334
+ for source_id, connections in resources_by_connection.items():
335
+ for _, resource_ids in connections.items():
336
+ final_resources.setdefault(source_id, []).extend(resource_ids)
337
+ return final_resources
338
+
339
+ async def inner_ask_by_title(
340
+ self,
341
+ source_id: str,
342
+ manager: Manager,
343
+ memory: QuestionMemory,
344
+ question: str,
345
+ resource_ids: List[str],
346
+ ) -> Context:
347
+ if len(resource_ids) == 0:
348
+ resource_ids = await self.retrieve(manager, source_id, question)
349
+
350
+ resources_by_connection = await self.post_filter_resources_by_connection(
351
+ memory=memory,
352
+ manager=manager,
353
+ resources={source_id: resource_ids},
354
+ )
355
+ resources_by_connection_for_source = resources_by_connection.get(
356
+ source_id, {}
357
+ )
358
+ resource_ids = list(
359
+ itertools.chain(*resources_by_connection_for_source.values())
360
+ )
361
+ if resource_ids is None or len(resource_ids) == 0:
362
+ raise Exception("No valid resources found after OAuth validation")
363
+
364
+ return await super().inner_ask_by_title(
365
+ source_id=source_id,
366
+ manager=manager,
367
+ memory=memory,
368
+ question=question,
369
+ resource_ids=resource_ids,
370
+ )
371
+
372
+ async def inner_rag(
373
+ self,
374
+ source_obj: Source,
375
+ manager: Manager,
376
+ memory: QuestionMemory,
377
+ question: str,
378
+ question_uuid: Optional[str] = None,
379
+ keyword_filters: List[str] = [],
380
+ and_filters: Optional[List[str]] = None,
381
+ or_filters: Optional[List[str]] = None,
382
+ full_resource: bool = False,
383
+ resource_filters: Optional[List[str]] = None,
384
+ ) -> Context:
385
+ # Retrieve resources as usual, but then filter them by connection and validate them with the SyncDriver
386
+ catalog_filter = self.enrich_filter()
387
+ resources = await self.retrieve(
388
+ manager=manager,
389
+ source_id=source_obj.id,
390
+ question=question,
391
+ keyword_filters=keyword_filters,
392
+ and_filters=and_filters,
393
+ or_filters=or_filters,
394
+ catalog_filter=catalog_filter,
395
+ )
396
+ resources_by_connection = await self.post_filter_resources_by_connection(
397
+ memory=memory,
398
+ manager=manager,
399
+ resources={source_obj.id: resources},
400
+ )
401
+ resources_by_connection_for_source = resources_by_connection.get(
402
+ source_obj.id, {}
403
+ )
404
+ resource_ids = list(
405
+ itertools.chain(*resources_by_connection_for_source.values())
406
+ )
407
+ if resource_ids is None or len(resource_ids) == 0:
408
+ raise Exception("No valid resources found after OAuth validation")
409
+
410
+ context = await super().inner_rag(
411
+ source_obj=source_obj,
412
+ manager=manager,
413
+ memory=memory,
414
+ question=question,
415
+ question_uuid=question_uuid,
416
+ keyword_filters=keyword_filters,
417
+ and_filters=and_filters,
418
+ or_filters=or_filters,
419
+ full_resource=full_resource,
420
+ resource_filters=resource_ids,
421
+ )
422
+ return context
423
+
424
+ async def rag(
425
+ self,
426
+ source_obj: Source,
427
+ question_uuid: str,
428
+ question: str,
429
+ memory: QuestionMemory,
430
+ manager: Manager,
431
+ flow_id: str,
432
+ ) -> tuple[str, str] | None:
433
+ context = await self.inner_rag(
434
+ source_obj=source_obj,
435
+ manager=manager,
436
+ memory=memory,
437
+ question=question,
438
+ question_uuid=question_uuid,
439
+ )
440
+ missing = await self.save_ctx_and_return_missing(
441
+ context=context,
442
+ question=question,
443
+ memory=memory,
444
+ manager=manager,
445
+ flow_id=flow_id,
446
+ )
447
+ return missing
448
+
449
+ async def inner_facets_search(
450
+ self,
451
+ memory: QuestionMemory,
452
+ manager: Manager,
453
+ question: str,
454
+ source: Source,
455
+ idx: int,
456
+ ) -> Context:
457
+ raise NotImplementedError("Facets search not implemented for SyncAskAgent")
458
+
459
+ async def inner_facets(
460
+ self,
461
+ memory: QuestionMemory,
462
+ manager: Manager,
463
+ question: str,
464
+ source: Source,
465
+ idx: int,
466
+ ) -> Context:
467
+ raise NotImplementedError("Facets search not implemented for SyncAskAgent")
468
+
469
+ async def inner_catalog_search(
470
+ self,
471
+ memory: QuestionMemory,
472
+ manager: Manager,
473
+ question: str,
474
+ source: Source,
475
+ idx: int,
476
+ ) -> Context:
477
+ raise NotImplementedError("Facets search not implemented for SyncAskAgent")
@@ -0,0 +1,12 @@
1
+ from typing import Literal
2
+
3
+ from pydantic import ConfigDict
4
+
5
+ from hyperforge_nucliadb.basic_ask_config import (
6
+ BasicAskAgentConfig,
7
+ )
8
+
9
+
10
+ class SyncAskAgentConfig(BasicAskAgentConfig):
11
+ model_config = ConfigDict(title="Knowledge Box Sync Service")
12
+ module: Literal["sync"] = "sync" # type: ignore[assignment]
@@ -0,0 +1,22 @@
1
+ from typing import Literal
2
+
3
+ from hyperforge.driver import DriverConfig
4
+ from pydantic.config import ConfigDict
5
+
6
+ from hyperforge_nucliadb.driver_config import (
7
+ NucliaDBConnection,
8
+ )
9
+
10
+
11
+ class SyncConnection(NucliaDBConnection):
12
+ connection_ids: list[str] # type: ignore
13
+
14
+ @property
15
+ def kb_url(self) -> str:
16
+ return f"{self.url}/v1/kb/{self.kbid}"
17
+
18
+
19
+ class SyncDriverConfig(DriverConfig[SyncConnection]):
20
+ model_config = ConfigDict(title="Knowledge Box Sync Service connection")
21
+ provider: Literal["sync"]
22
+ config: SyncConnection
@@ -0,0 +1,148 @@
1
+ import datetime
2
+ from typing import Any, Dict, List, cast
3
+ from urllib.parse import urlencode
4
+ from uuid import UUID
5
+
6
+ from httpx import AsyncClient
7
+ from hyperforge.configure import driver
8
+ from hyperforge.interaction import Provider
9
+ from hyperforge.utils.http import SafeTransport
10
+ from nucliadb_models import SyncMetadata
11
+ from pydantic import BaseModel
12
+
13
+ from hyperforge_nucliadb.driver import (
14
+ NucliaDBDriver,
15
+ connect,
16
+ manager_connect,
17
+ )
18
+ from hyperforge_nucliadb.driver_config import (
19
+ NucliaDBConnection,
20
+ )
21
+ from hyperforge_nucliadb.sync.config_driver import (
22
+ SyncConnection,
23
+ SyncDriverConfig,
24
+ )
25
+
26
+
27
+ async def sync_connect(conn: SyncConnection):
28
+ headers: Dict[str, str] = {}
29
+ if "http://localhost" in conn.kb_url:
30
+ headers = {
31
+ "X-NUCLIADB-ROLES": "READER",
32
+ }
33
+ else:
34
+ headers = {"AUTHORIZATION": "Bearer " + conn.key} # type: ignore
35
+ return AsyncClient(
36
+ headers=headers,
37
+ base_url=conn.kb_url,
38
+ transport=SafeTransport(),
39
+ )
40
+
41
+
42
+ class ExternalConnectionOutput(BaseModel):
43
+ """External connection output without sensitive credential data."""
44
+
45
+ id: UUID
46
+ kb_id: UUID
47
+ created_by: UUID
48
+ created_at: datetime.datetime
49
+ updated_at: datetime.datetime
50
+ provider: Provider
51
+
52
+
53
+ @driver(
54
+ id="sync",
55
+ title="Sync Driver",
56
+ description="Driver for interacting with the Sync API.",
57
+ config_schema=SyncDriverConfig,
58
+ )
59
+ class SyncDriver(NucliaDBDriver):
60
+ async_driver: AsyncClient
61
+ config: SyncConnection # type: ignore
62
+ information: Dict[str, ExternalConnectionOutput]
63
+ sync_configs: Dict[str, list[str]]
64
+
65
+ @classmethod
66
+ async def init(cls, driver: Any) -> "SyncDriver":
67
+ sync_driver = cast(SyncDriverConfig, driver)
68
+ client = await sync_connect(sync_driver.config)
69
+ information = {}
70
+ sync_configs: Dict[str, list[str]] = {}
71
+ for sync_config_id in sync_driver.config.connection_ids:
72
+ response = await client.get(f"/sync_config/{sync_config_id}")
73
+ response.raise_for_status()
74
+ data = response.json()
75
+ connection_id = data["external_connection"]["id"]
76
+ response = await client.get(f"/external_connection/{connection_id}")
77
+ response.raise_for_status()
78
+ sync_configs.setdefault(sync_config_id, []).append(connection_id)
79
+ data = response.json()
80
+ information[connection_id] = ExternalConnectionOutput(**data)
81
+ return cls(
82
+ provider=sync_driver.provider,
83
+ name=sync_driver.name,
84
+ async_driver=client,
85
+ information=information,
86
+ sync_configs=sync_configs,
87
+ config=sync_driver.config,
88
+ driver=await connect(cast(NucliaDBConnection, sync_driver.config)),
89
+ manager=await manager_connect(cast(NucliaDBConnection, sync_driver.config)),
90
+ _synonyms=None,
91
+ )
92
+
93
+ async def get_oauth_url(
94
+ self,
95
+ sync_config: str,
96
+ rao_redirect_url: str,
97
+ oauth_uuid: str,
98
+ connection_id: str,
99
+ question_id: str,
100
+ ) -> str:
101
+ sep = "&" if "?" in rao_redirect_url else "?"
102
+ rao_redirect_url = (
103
+ f"{rao_redirect_url}{sep}{urlencode({'question_id': question_id})}"
104
+ )
105
+
106
+ response = await self.async_driver.post(
107
+ f"{self.config.kb_url}/sync_config/{sync_config}/authorize",
108
+ json={
109
+ "connection_id": connection_id,
110
+ "rao_redirect_url": rao_redirect_url,
111
+ "oauth_uuid": oauth_uuid,
112
+ },
113
+ )
114
+ response.raise_for_status()
115
+ data = response.json()
116
+ return data["authorize_url"]
117
+
118
+ async def validate_resources(
119
+ self,
120
+ resource_ids: List[str],
121
+ credentials: str,
122
+ connection_id: str,
123
+ sync_config_id: str,
124
+ sync_metadata_by_resource: Dict[str, SyncMetadata],
125
+ ) -> List[str]:
126
+ response = await self.async_driver.post(
127
+ f"/sync_config/{sync_config_id}/validate_resources",
128
+ json={
129
+ "resources": [
130
+ x.model_dump() for x in sync_metadata_by_resource.values()
131
+ ],
132
+ "credentials": credentials,
133
+ },
134
+ )
135
+ response.raise_for_status()
136
+ data = response.json()
137
+ # The API validates by file_id (origin storage identifier) and returns those back,
138
+ # but resource_filters in AskRequest requires NucliaDB resource UUIDs, which are
139
+ # the keys of sync_metadata_by_resource.
140
+ file_id_to_resource_uuid = {
141
+ metadata.file_id: resource_uuid
142
+ for resource_uuid, metadata in sync_metadata_by_resource.items()
143
+ }
144
+ return [
145
+ file_id_to_resource_uuid[resource["file_id"]]
146
+ for resource in data.get("valid_resources", [])
147
+ if resource["file_id"] in file_id_to_resource_uuid
148
+ ]
@@ -0,0 +1,26 @@
1
+ Metadata-Version: 2.4
2
+ Name: hyperforge_nucliadb
3
+ Version: 1.0.0.post22
4
+ Summary: NucliaDB Hyperforge agent
5
+ Author-email: Nuclia <nucliadb@nuclia.com>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://progress.com
8
+ Project-URL: Repository, https://github.com/nuclia/forge
9
+ Classifier: Programming Language :: Python
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Requires-Python: <4,>=3.10
16
+ Description-Content-Type: text/markdown
17
+ Requires-Dist: hyperforge
18
+ Requires-Dist: lingua-language-detector
19
+ Requires-Dist: nucliadb_sdk
20
+
21
+ # NucliaDB Hyperforge agents
22
+
23
+ Basic ASK - Basic initial agent
24
+ Advanced ASK (deprecated)
25
+ ASK (experimental)
26
+ Sync (Sync Service oauth linked)