hyperforge-nucliadb 1.0.0.post46__tar.gz → 1.0.0.post58__tar.gz

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.
Files changed (41) hide show
  1. {hyperforge_nucliadb-1.0.0.post46 → hyperforge_nucliadb-1.0.0.post58}/PKG-INFO +1 -1
  2. {hyperforge_nucliadb-1.0.0.post46 → hyperforge_nucliadb-1.0.0.post58}/pyproject.toml +1 -1
  3. {hyperforge_nucliadb-1.0.0.post46 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/sync/config_driver.py +4 -0
  4. hyperforge_nucliadb-1.0.0.post58/src/hyperforge_nucliadb/sync/driver.py +254 -0
  5. {hyperforge_nucliadb-1.0.0.post46 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb.egg-info/PKG-INFO +1 -1
  6. hyperforge_nucliadb-1.0.0.post46/src/hyperforge_nucliadb/sync/driver.py +0 -148
  7. {hyperforge_nucliadb-1.0.0.post46 → hyperforge_nucliadb-1.0.0.post58}/README.md +0 -0
  8. {hyperforge_nucliadb-1.0.0.post46 → hyperforge_nucliadb-1.0.0.post58}/setup.cfg +0 -0
  9. {hyperforge_nucliadb-1.0.0.post46 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/__init__.py +0 -0
  10. {hyperforge_nucliadb-1.0.0.post46 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/advanced_ask_agent.py +0 -0
  11. {hyperforge_nucliadb-1.0.0.post46 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/advanced_ask_config.py +0 -0
  12. {hyperforge_nucliadb-1.0.0.post46 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/ask/__init__.py +0 -0
  13. {hyperforge_nucliadb-1.0.0.post46 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/ask/analysis.py +0 -0
  14. {hyperforge_nucliadb-1.0.0.post46 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/ask/ask.py +0 -0
  15. {hyperforge_nucliadb-1.0.0.post46 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/ask/config.py +0 -0
  16. {hyperforge_nucliadb-1.0.0.post46 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/ask/hydrate.py +0 -0
  17. {hyperforge_nucliadb-1.0.0.post46 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/ask/kb_analysis.py +0 -0
  18. {hyperforge_nucliadb-1.0.0.post46 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/ask/knowledge_scan.py +0 -0
  19. {hyperforge_nucliadb-1.0.0.post46 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/ask/models.py +0 -0
  20. {hyperforge_nucliadb-1.0.0.post46 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/ask/multi.py +0 -0
  21. {hyperforge_nucliadb-1.0.0.post46 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/ask/nucliadb.py +0 -0
  22. {hyperforge_nucliadb-1.0.0.post46 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/ask/prompt_analysis.py +0 -0
  23. {hyperforge_nucliadb-1.0.0.post46 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/ask/query_analysis.py +0 -0
  24. {hyperforge_nucliadb-1.0.0.post46 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/ask/rerank.py +0 -0
  25. {hyperforge_nucliadb-1.0.0.post46 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/ask/utils.py +0 -0
  26. {hyperforge_nucliadb-1.0.0.post46 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/ask_utils.py +0 -0
  27. {hyperforge_nucliadb-1.0.0.post46 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/basic_ask_agent.py +0 -0
  28. {hyperforge_nucliadb-1.0.0.post46 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/basic_ask_config.py +0 -0
  29. {hyperforge_nucliadb-1.0.0.post46 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/driver.py +0 -0
  30. {hyperforge_nucliadb-1.0.0.post46 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/driver_config.py +0 -0
  31. {hyperforge_nucliadb-1.0.0.post46 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/py.typed +0 -0
  32. {hyperforge_nucliadb-1.0.0.post46 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/sync/__init__.py +0 -0
  33. {hyperforge_nucliadb-1.0.0.post46 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/sync/agent.py +0 -0
  34. {hyperforge_nucliadb-1.0.0.post46 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/sync/config.py +0 -0
  35. {hyperforge_nucliadb-1.0.0.post46 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb.egg-info/SOURCES.txt +0 -0
  36. {hyperforge_nucliadb-1.0.0.post46 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb.egg-info/dependency_links.txt +0 -0
  37. {hyperforge_nucliadb-1.0.0.post46 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb.egg-info/requires.txt +0 -0
  38. {hyperforge_nucliadb-1.0.0.post46 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb.egg-info/top_level.txt +0 -0
  39. {hyperforge_nucliadb-1.0.0.post46 → hyperforge_nucliadb-1.0.0.post58}/tests/test_ask_utils.py +0 -0
  40. {hyperforge_nucliadb-1.0.0.post46 → hyperforge_nucliadb-1.0.0.post58}/tests/test_nucliadb.py +0 -0
  41. {hyperforge_nucliadb-1.0.0.post46 → hyperforge_nucliadb-1.0.0.post58}/tests/test_sync.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperforge_nucliadb
3
- Version: 1.0.0.post46
3
+ Version: 1.0.0.post58
4
4
  Summary: NucliaDB Hyperforge agent
5
5
  Author-email: Nuclia <nucliadb@nuclia.com>
6
6
  License-Expression: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "hyperforge_nucliadb"
7
- version = "1.0.0.post46"
7
+ version = "1.0.0.post58"
8
8
  license = "Apache-2.0"
9
9
  description = "NucliaDB Hyperforge agent"
10
10
  authors = [{ name = "Nuclia", email = "nucliadb@nuclia.com" }]
@@ -1,5 +1,6 @@
1
1
  from typing import Literal
2
2
 
3
+ from httpx import Timeout
3
4
  from hyperforge.driver import DriverConfig
4
5
  from pydantic.config import ConfigDict
5
6
 
@@ -8,6 +9,9 @@ from hyperforge_nucliadb.driver_config import (
8
9
  )
9
10
 
10
11
 
12
+ SYNC_HTTP_TIMEOUT = Timeout(connect=5.0, read=30.0, write=10.0, pool=5.0)
13
+
14
+
11
15
  class SyncConnection(NucliaDBConnection):
12
16
  connection_ids: list[str] # type: ignore
13
17
 
@@ -0,0 +1,254 @@
1
+ import datetime
2
+ import asyncio
3
+ import random
4
+ import time
5
+ from typing import Any, Dict, List, cast
6
+ from urllib.parse import urlencode
7
+ from uuid import UUID
8
+
9
+ from httpx import (
10
+ AsyncClient,
11
+ ConnectError,
12
+ ConnectTimeout,
13
+ ReadTimeout,
14
+ RemoteProtocolError,
15
+ Response,
16
+ )
17
+ from hyperforge import logger
18
+ from hyperforge.configure import driver
19
+ from hyperforge.interaction import Provider
20
+ from hyperforge.utils.http import SafeTransport
21
+ from nucliadb_models import SyncMetadata
22
+ from pydantic import BaseModel
23
+
24
+ from hyperforge_nucliadb.driver import (
25
+ NucliaDBDriver,
26
+ connect,
27
+ manager_connect,
28
+ )
29
+ from hyperforge_nucliadb.driver_config import (
30
+ NucliaDBConnection,
31
+ )
32
+ from hyperforge_nucliadb.sync.config_driver import (
33
+ SYNC_HTTP_TIMEOUT,
34
+ SyncConnection,
35
+ SyncDriverConfig,
36
+ )
37
+
38
+
39
+ SYNC_VALIDATE_ATTEMPTS = 2
40
+ SYNC_VALIDATE_RETRY_BASE_SECONDS = 0.5
41
+ SYNC_VALIDATE_RETRY_JITTER_SECONDS = 0.25
42
+ SYNC_VALIDATE_RETRY_STATUS_CODES = frozenset({502, 503, 504})
43
+ SYNC_VALIDATE_RETRY_EXCEPTIONS = (
44
+ ConnectError,
45
+ ConnectTimeout,
46
+ ReadTimeout,
47
+ RemoteProtocolError,
48
+ )
49
+
50
+
51
+ async def sync_connect(conn: SyncConnection):
52
+ headers: Dict[str, str] = {}
53
+ if "http://localhost" in conn.kb_url:
54
+ headers = {
55
+ "X-NUCLIADB-ROLES": "READER",
56
+ }
57
+ else:
58
+ headers = {"AUTHORIZATION": "Bearer " + conn.key} # type: ignore
59
+ return AsyncClient(
60
+ headers=headers,
61
+ base_url=conn.kb_url,
62
+ timeout=SYNC_HTTP_TIMEOUT,
63
+ transport=SafeTransport(),
64
+ )
65
+
66
+
67
+ class ExternalConnectionOutput(BaseModel):
68
+ """External connection output without sensitive credential data."""
69
+
70
+ id: UUID
71
+ kb_id: UUID
72
+ created_by: UUID
73
+ created_at: datetime.datetime
74
+ updated_at: datetime.datetime
75
+ provider: Provider
76
+
77
+
78
+ @driver(
79
+ id="sync",
80
+ title="Sync Source",
81
+ description="Source for interacting with the Sync API.",
82
+ config_schema=SyncDriverConfig,
83
+ )
84
+ class SyncDriver(NucliaDBDriver):
85
+ async_driver: AsyncClient
86
+ config: SyncConnection # type: ignore
87
+ information: Dict[str, ExternalConnectionOutput]
88
+ sync_configs: Dict[str, list[str]]
89
+
90
+ @classmethod
91
+ async def init(cls, driver: Any) -> "SyncDriver":
92
+ sync_driver = cast(SyncDriverConfig, driver)
93
+ client = await sync_connect(sync_driver.config)
94
+ information = {}
95
+ sync_configs: Dict[str, list[str]] = {}
96
+ for sync_config_id in sync_driver.config.connection_ids:
97
+ response = await client.get(f"/sync_config/{sync_config_id}")
98
+ response.raise_for_status()
99
+ data = response.json()
100
+ connection_id = data["external_connection"]["id"]
101
+ response = await client.get(f"/external_connection/{connection_id}")
102
+ response.raise_for_status()
103
+ sync_configs.setdefault(sync_config_id, []).append(connection_id)
104
+ data = response.json()
105
+ information[connection_id] = ExternalConnectionOutput(**data)
106
+ return cls(
107
+ provider=sync_driver.provider,
108
+ name=sync_driver.name,
109
+ async_driver=client,
110
+ information=information,
111
+ sync_configs=sync_configs,
112
+ config=sync_driver.config,
113
+ driver=await connect(cast(NucliaDBConnection, sync_driver.config)),
114
+ manager=await manager_connect(cast(NucliaDBConnection, sync_driver.config)),
115
+ _synonyms=None,
116
+ )
117
+
118
+ async def get_oauth_url(
119
+ self,
120
+ sync_config: str,
121
+ rao_redirect_url: str,
122
+ oauth_uuid: str,
123
+ connection_id: str,
124
+ question_id: str,
125
+ ) -> str:
126
+ sep = "&" if "?" in rao_redirect_url else "?"
127
+ rao_redirect_url = (
128
+ f"{rao_redirect_url}{sep}{urlencode({'question_id': question_id})}"
129
+ )
130
+
131
+ response = await self.async_driver.post(
132
+ f"{self.config.kb_url}/sync_config/{sync_config}/authorize",
133
+ json={
134
+ "connection_id": connection_id,
135
+ "rao_redirect_url": rao_redirect_url,
136
+ "oauth_uuid": oauth_uuid,
137
+ },
138
+ )
139
+ response.raise_for_status()
140
+ data = response.json()
141
+ return data["authorize_url"]
142
+
143
+ async def validate_resources(
144
+ self,
145
+ resource_ids: List[str],
146
+ credentials: str,
147
+ connection_id: str,
148
+ sync_config_id: str,
149
+ sync_metadata_by_resource: Dict[str, SyncMetadata],
150
+ ) -> List[str]:
151
+ path = f"/sync_config/{sync_config_id}/validate_resources"
152
+ payload = {
153
+ "resources": [x.model_dump() for x in sync_metadata_by_resource.values()],
154
+ "credentials": credentials,
155
+ }
156
+
157
+ response: Response | None = None
158
+ attempt = 0
159
+ overall_started_at = time.monotonic()
160
+ for attempt in range(1, SYNC_VALIDATE_ATTEMPTS + 1):
161
+ attempt_started_at = time.monotonic()
162
+ try:
163
+ response = await self.async_driver.post(path, json=payload)
164
+ if (
165
+ response.status_code not in SYNC_VALIDATE_RETRY_STATUS_CODES
166
+ or attempt == SYNC_VALIDATE_ATTEMPTS
167
+ ):
168
+ if response.status_code in SYNC_VALIDATE_RETRY_STATUS_CODES:
169
+ logger.warning(
170
+ "Sync resource validation got retriable status but reached max attempts",
171
+ extra={
172
+ "sync_config_id": sync_config_id,
173
+ "connection_id": connection_id,
174
+ "resource_count": len(resource_ids),
175
+ "attempt": attempt,
176
+ "duration_seconds": time.monotonic()
177
+ - attempt_started_at,
178
+ "status_code": response.status_code,
179
+ },
180
+ )
181
+ else:
182
+ logger.info(
183
+ "Sync resource validation received non-retriable status",
184
+ extra={
185
+ "sync_config_id": sync_config_id,
186
+ "connection_id": connection_id,
187
+ "resource_count": len(resource_ids),
188
+ "attempt": attempt,
189
+ "duration_seconds": time.monotonic()
190
+ - attempt_started_at,
191
+ "status_code": response.status_code,
192
+ },
193
+ )
194
+ break
195
+ error_type = f"HTTP {response.status_code}"
196
+ except SYNC_VALIDATE_RETRY_EXCEPTIONS as exc:
197
+ if attempt == SYNC_VALIDATE_ATTEMPTS:
198
+ logger.warning(
199
+ "Sync resource validation failed after retries",
200
+ extra={
201
+ "sync_config_id": sync_config_id,
202
+ "connection_id": connection_id,
203
+ "resource_count": len(resource_ids),
204
+ "attempt": attempt,
205
+ "duration_seconds": time.monotonic() - attempt_started_at,
206
+ "exception_type": type(exc).__name__,
207
+ },
208
+ )
209
+ raise
210
+ error_type = type(exc).__name__
211
+
212
+ logger.warning(
213
+ "Transient Sync resource validation failure, retrying",
214
+ extra={
215
+ "sync_config_id": sync_config_id,
216
+ "connection_id": connection_id,
217
+ "resource_count": len(resource_ids),
218
+ "attempt": attempt,
219
+ "duration_seconds": time.monotonic() - attempt_started_at,
220
+ "exception_type": error_type,
221
+ },
222
+ )
223
+ delay = SYNC_VALIDATE_RETRY_BASE_SECONDS + random.uniform(
224
+ 0, SYNC_VALIDATE_RETRY_JITTER_SECONDS
225
+ )
226
+ await asyncio.sleep(delay)
227
+
228
+ if response is None:
229
+ raise RuntimeError("Sync resource validation made no request attempts")
230
+
231
+ response.raise_for_status()
232
+ logger.info(
233
+ "Sync resource validation completed",
234
+ extra={
235
+ "sync_config_id": sync_config_id,
236
+ "connection_id": connection_id,
237
+ "resource_count": len(resource_ids),
238
+ "attempt": attempt,
239
+ "duration_seconds": time.monotonic() - overall_started_at,
240
+ },
241
+ )
242
+ data = response.json()
243
+ # The API validates by file_id (origin storage identifier) and returns those back,
244
+ # but resource_filters in AskRequest requires NucliaDB resource UUIDs, which are
245
+ # the keys of sync_metadata_by_resource.
246
+ file_id_to_resource_uuid = {
247
+ metadata.file_id: resource_uuid
248
+ for resource_uuid, metadata in sync_metadata_by_resource.items()
249
+ }
250
+ return [
251
+ file_id_to_resource_uuid[resource["file_id"]]
252
+ for resource in data.get("valid_resources", [])
253
+ if resource["file_id"] in file_id_to_resource_uuid
254
+ ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperforge_nucliadb
3
- Version: 1.0.0.post46
3
+ Version: 1.0.0.post58
4
4
  Summary: NucliaDB Hyperforge agent
5
5
  Author-email: Nuclia <nucliadb@nuclia.com>
6
6
  License-Expression: Apache-2.0
@@ -1,148 +0,0 @@
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 Source",
56
- description="Source 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
- ]