hyperforge-nucliadb 1.0.0.post105__tar.gz → 1.0.0.post111__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 (43) hide show
  1. {hyperforge_nucliadb-1.0.0.post105 → hyperforge_nucliadb-1.0.0.post111}/PKG-INFO +11 -1
  2. hyperforge_nucliadb-1.0.0.post111/README.md +16 -0
  3. {hyperforge_nucliadb-1.0.0.post105 → hyperforge_nucliadb-1.0.0.post111}/pyproject.toml +1 -1
  4. {hyperforge_nucliadb-1.0.0.post105 → hyperforge_nucliadb-1.0.0.post111}/src/hyperforge_nucliadb/driver.py +8 -2
  5. {hyperforge_nucliadb-1.0.0.post105 → hyperforge_nucliadb-1.0.0.post111}/src/hyperforge_nucliadb/sync/agent.py +2 -0
  6. {hyperforge_nucliadb-1.0.0.post105 → hyperforge_nucliadb-1.0.0.post111}/src/hyperforge_nucliadb.egg-info/PKG-INFO +11 -1
  7. hyperforge_nucliadb-1.0.0.post111/tests/test_driver.py +99 -0
  8. {hyperforge_nucliadb-1.0.0.post105 → hyperforge_nucliadb-1.0.0.post111}/tests/test_sync.py +54 -0
  9. hyperforge_nucliadb-1.0.0.post105/README.md +0 -6
  10. hyperforge_nucliadb-1.0.0.post105/tests/test_driver.py +0 -34
  11. {hyperforge_nucliadb-1.0.0.post105 → hyperforge_nucliadb-1.0.0.post111}/setup.cfg +0 -0
  12. {hyperforge_nucliadb-1.0.0.post105 → hyperforge_nucliadb-1.0.0.post111}/src/hyperforge_nucliadb/__init__.py +0 -0
  13. {hyperforge_nucliadb-1.0.0.post105 → hyperforge_nucliadb-1.0.0.post111}/src/hyperforge_nucliadb/advanced_ask_agent.py +0 -0
  14. {hyperforge_nucliadb-1.0.0.post105 → hyperforge_nucliadb-1.0.0.post111}/src/hyperforge_nucliadb/advanced_ask_config.py +0 -0
  15. {hyperforge_nucliadb-1.0.0.post105 → hyperforge_nucliadb-1.0.0.post111}/src/hyperforge_nucliadb/ask/__init__.py +0 -0
  16. {hyperforge_nucliadb-1.0.0.post105 → hyperforge_nucliadb-1.0.0.post111}/src/hyperforge_nucliadb/ask/analysis.py +0 -0
  17. {hyperforge_nucliadb-1.0.0.post105 → hyperforge_nucliadb-1.0.0.post111}/src/hyperforge_nucliadb/ask/ask.py +0 -0
  18. {hyperforge_nucliadb-1.0.0.post105 → hyperforge_nucliadb-1.0.0.post111}/src/hyperforge_nucliadb/ask/config.py +0 -0
  19. {hyperforge_nucliadb-1.0.0.post105 → hyperforge_nucliadb-1.0.0.post111}/src/hyperforge_nucliadb/ask/hydrate.py +0 -0
  20. {hyperforge_nucliadb-1.0.0.post105 → hyperforge_nucliadb-1.0.0.post111}/src/hyperforge_nucliadb/ask/kb_analysis.py +0 -0
  21. {hyperforge_nucliadb-1.0.0.post105 → hyperforge_nucliadb-1.0.0.post111}/src/hyperforge_nucliadb/ask/knowledge_scan.py +0 -0
  22. {hyperforge_nucliadb-1.0.0.post105 → hyperforge_nucliadb-1.0.0.post111}/src/hyperforge_nucliadb/ask/models.py +0 -0
  23. {hyperforge_nucliadb-1.0.0.post105 → hyperforge_nucliadb-1.0.0.post111}/src/hyperforge_nucliadb/ask/multi.py +0 -0
  24. {hyperforge_nucliadb-1.0.0.post105 → hyperforge_nucliadb-1.0.0.post111}/src/hyperforge_nucliadb/ask/nucliadb.py +0 -0
  25. {hyperforge_nucliadb-1.0.0.post105 → hyperforge_nucliadb-1.0.0.post111}/src/hyperforge_nucliadb/ask/prompt_analysis.py +0 -0
  26. {hyperforge_nucliadb-1.0.0.post105 → hyperforge_nucliadb-1.0.0.post111}/src/hyperforge_nucliadb/ask/query_analysis.py +0 -0
  27. {hyperforge_nucliadb-1.0.0.post105 → hyperforge_nucliadb-1.0.0.post111}/src/hyperforge_nucliadb/ask/rerank.py +0 -0
  28. {hyperforge_nucliadb-1.0.0.post105 → hyperforge_nucliadb-1.0.0.post111}/src/hyperforge_nucliadb/ask/utils.py +0 -0
  29. {hyperforge_nucliadb-1.0.0.post105 → hyperforge_nucliadb-1.0.0.post111}/src/hyperforge_nucliadb/ask_utils.py +0 -0
  30. {hyperforge_nucliadb-1.0.0.post105 → hyperforge_nucliadb-1.0.0.post111}/src/hyperforge_nucliadb/basic_ask_agent.py +0 -0
  31. {hyperforge_nucliadb-1.0.0.post105 → hyperforge_nucliadb-1.0.0.post111}/src/hyperforge_nucliadb/basic_ask_config.py +0 -0
  32. {hyperforge_nucliadb-1.0.0.post105 → hyperforge_nucliadb-1.0.0.post111}/src/hyperforge_nucliadb/driver_config.py +0 -0
  33. {hyperforge_nucliadb-1.0.0.post105 → hyperforge_nucliadb-1.0.0.post111}/src/hyperforge_nucliadb/py.typed +0 -0
  34. {hyperforge_nucliadb-1.0.0.post105 → hyperforge_nucliadb-1.0.0.post111}/src/hyperforge_nucliadb/sync/__init__.py +0 -0
  35. {hyperforge_nucliadb-1.0.0.post105 → hyperforge_nucliadb-1.0.0.post111}/src/hyperforge_nucliadb/sync/config.py +0 -0
  36. {hyperforge_nucliadb-1.0.0.post105 → hyperforge_nucliadb-1.0.0.post111}/src/hyperforge_nucliadb/sync/config_driver.py +0 -0
  37. {hyperforge_nucliadb-1.0.0.post105 → hyperforge_nucliadb-1.0.0.post111}/src/hyperforge_nucliadb/sync/driver.py +0 -0
  38. {hyperforge_nucliadb-1.0.0.post105 → hyperforge_nucliadb-1.0.0.post111}/src/hyperforge_nucliadb.egg-info/SOURCES.txt +0 -0
  39. {hyperforge_nucliadb-1.0.0.post105 → hyperforge_nucliadb-1.0.0.post111}/src/hyperforge_nucliadb.egg-info/dependency_links.txt +0 -0
  40. {hyperforge_nucliadb-1.0.0.post105 → hyperforge_nucliadb-1.0.0.post111}/src/hyperforge_nucliadb.egg-info/requires.txt +0 -0
  41. {hyperforge_nucliadb-1.0.0.post105 → hyperforge_nucliadb-1.0.0.post111}/src/hyperforge_nucliadb.egg-info/top_level.txt +0 -0
  42. {hyperforge_nucliadb-1.0.0.post105 → hyperforge_nucliadb-1.0.0.post111}/tests/test_ask_utils.py +0 -0
  43. {hyperforge_nucliadb-1.0.0.post105 → hyperforge_nucliadb-1.0.0.post111}/tests/test_nucliadb.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperforge_nucliadb
3
- Version: 1.0.0.post105
3
+ Version: 1.0.0.post111
4
4
  Summary: NucliaDB Hyperforge agent
5
5
  Author-email: Nuclia <nucliadb@nuclia.com>
6
6
  License-Expression: Apache-2.0
@@ -25,3 +25,13 @@ Basic ASK - Basic initial agent
25
25
  Advanced ASK (deprecated)
26
26
  ASK (experimental)
27
27
  Sync (Sync Service oauth linked)
28
+
29
+ ## Client identification
30
+
31
+ NucliaDB `/ask` and `/find` requests made by these agents use
32
+ `X-NDB-Client: agentic`, including when invoked as tools by a smart agent in a
33
+ RAO workflow. The Sync driver inherits the same behavior. Other request headers
34
+ are preserved, and caller-provided headers are not modified.
35
+
36
+ Workflow memory searches through `/find` also use the `agentic` client type.
37
+ The receiving NucliaDB API must support the `agentic` client type.
@@ -0,0 +1,16 @@
1
+ # NucliaDB Hyperforge agents
2
+
3
+ Basic ASK - Basic initial agent
4
+ Advanced ASK (deprecated)
5
+ ASK (experimental)
6
+ Sync (Sync Service oauth linked)
7
+
8
+ ## Client identification
9
+
10
+ NucliaDB `/ask` and `/find` requests made by these agents use
11
+ `X-NDB-Client: agentic`, including when invoked as tools by a smart agent in a
12
+ RAO workflow. The Sync driver inherits the same behavior. Other request headers
13
+ are preserved, and caller-provided headers are not modified.
14
+
15
+ Workflow memory searches through `/find` also use the `agentic` client type.
16
+ The receiving NucliaDB API must support the `agentic` client type.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "hyperforge_nucliadb"
7
- version = "1.0.0.post105"
7
+ version = "1.0.0.post111"
8
8
  license = "Apache-2.0"
9
9
  description = "NucliaDB Hyperforge agent"
10
10
  authors = [{ name = "Nuclia", email = "nucliadb@nuclia.com" }]
@@ -303,9 +303,13 @@ class NucliaDBDriver(Driver):
303
303
  result.append(word)
304
304
  return " ".join(result)
305
305
 
306
- async def ask(self, item: AskRequest, headers={}) -> SyncAskResponse:
306
+ async def ask(
307
+ self, item: AskRequest, headers: dict[str, str] | None = None
308
+ ) -> SyncAskResponse:
309
+ request_headers = httpx.Headers(headers)
310
+ request_headers["X-NDB-Client"] = "agentic"
307
311
  return await self.driver.ask(
308
- kbid=self.config.kbid, content=item, headers=headers
312
+ kbid=self.config.kbid, content=item, headers=dict(request_headers)
309
313
  )
310
314
 
311
315
  async def find(
@@ -313,6 +317,7 @@ class NucliaDBDriver(Driver):
313
317
  ) -> KnowledgeboxFindResults:
314
318
  return await self.driver.find(
315
319
  kbid=self.config.kbid,
320
+ headers={"X-NDB-Client": "agentic"},
316
321
  content=FindRequest(
317
322
  query=q,
318
323
  resource_filters=rids,
@@ -326,6 +331,7 @@ class NucliaDBDriver(Driver):
326
331
  return await self.driver.find(
327
332
  kbid=self.config.kbid,
328
333
  content=q,
334
+ headers={"X-NDB-Client": "agentic"},
329
335
  )
330
336
 
331
337
  async def graph_relations(
@@ -398,6 +398,8 @@ class SyncAskAgent(BasicAskAgent):
398
398
  connection_id,
399
399
  connection_credentials,
400
400
  ) in existing_credentials.items():
401
+ if connection_id not in connections_by_resource:
402
+ continue
401
403
  source = connections[kb_source_id]
402
404
  if (
403
405
  connection_id not in source.sync_configs
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperforge_nucliadb
3
- Version: 1.0.0.post105
3
+ Version: 1.0.0.post111
4
4
  Summary: NucliaDB Hyperforge agent
5
5
  Author-email: Nuclia <nucliadb@nuclia.com>
6
6
  License-Expression: Apache-2.0
@@ -25,3 +25,13 @@ Basic ASK - Basic initial agent
25
25
  Advanced ASK (deprecated)
26
26
  ASK (experimental)
27
27
  Sync (Sync Service oauth linked)
28
+
29
+ ## Client identification
30
+
31
+ NucliaDB `/ask` and `/find` requests made by these agents use
32
+ `X-NDB-Client: agentic`, including when invoked as tools by a smart agent in a
33
+ RAO workflow. The Sync driver inherits the same behavior. Other request headers
34
+ are preserved, and caller-provided headers are not modified.
35
+
36
+ Workflow memory searches through `/find` also use the `agentic` client type.
37
+ The receiving NucliaDB API must support the `agentic` client type.
@@ -0,0 +1,99 @@
1
+ import json
2
+ from types import SimpleNamespace
3
+ from unittest.mock import AsyncMock
4
+
5
+ import httpx
6
+ import pytest
7
+ from nucliadb_models.search import AskRequest, FindRequest
8
+ from nucliadb_sdk.v2 import NucliaDBAsync
9
+
10
+ from hyperforge_nucliadb.driver import NucliaDBDriver
11
+ from hyperforge_nucliadb.sync.driver import SyncDriver
12
+
13
+
14
+ def make_driver(client, driver_type=NucliaDBDriver):
15
+ return driver_type.model_construct(
16
+ provider="nucliadb",
17
+ name="test",
18
+ config=SimpleNamespace(kbid="test-kb", filters=[]),
19
+ driver=client,
20
+ manager=SimpleNamespace(),
21
+ _synonyms=None,
22
+ )
23
+
24
+
25
+ @pytest.mark.asyncio
26
+ async def test_labels_uses_complete_labelsets_response():
27
+ client = AsyncMock()
28
+ client.get_labelsets.return_value = SimpleNamespace(
29
+ labelsets={
30
+ "type": SimpleNamespace(labels=[SimpleNamespace(title="report")]),
31
+ "topic": SimpleNamespace(labels=[SimpleNamespace(title="science")]),
32
+ }
33
+ )
34
+ driver = make_driver(client)
35
+
36
+ assert await driver.labels() == {"type": ["report"], "topic": ["science"]}
37
+
38
+ client.get_labelsets.assert_awaited_once_with(kbid="test-kb")
39
+ client.get_labelset.assert_not_awaited()
40
+
41
+
42
+ @pytest.mark.asyncio
43
+ @pytest.mark.parametrize("driver_type", [NucliaDBDriver, SyncDriver])
44
+ @pytest.mark.parametrize("operation", ["ask", "find", "find_raw"])
45
+ async def test_search_requests_use_agentic_client_type(driver_type, operation):
46
+ def handle(request: httpx.Request) -> httpx.Response:
47
+ endpoint = "ask" if operation == "ask" else "find"
48
+ assert request.url.path.endswith(f"/kb/test-kb/{endpoint}")
49
+ assert request.headers.get_list("x-ndb-client") == ["agentic"]
50
+ assert request.headers["x-test-header"] == "preserved"
51
+ assert json.loads(request.content)["query"] == "question"
52
+ result = {"resources": {}, "total": 0}
53
+ if operation == "ask":
54
+ result = {
55
+ "status": "success",
56
+ "answer": "answer",
57
+ "retrieval_results": result,
58
+ }
59
+ return httpx.Response(200, json=result)
60
+
61
+ client = NucliaDBAsync(
62
+ url="http://nucliadb.test/api",
63
+ headers={"X-NDB-Client": "api", "x-test-header": "preserved"},
64
+ _httpx_transport=httpx.MockTransport(handle),
65
+ )
66
+ driver = make_driver(client, driver_type)
67
+
68
+ async with client.session:
69
+ if operation == "ask":
70
+ result = await driver.ask(AskRequest(query="question"))
71
+ assert result.answer == "answer"
72
+ elif operation == "find":
73
+ result = await driver.find("question")
74
+ assert result.total == 0
75
+ else:
76
+ result = await driver.find_raw(FindRequest(query="question"))
77
+ assert result.total == 0
78
+
79
+
80
+ @pytest.mark.asyncio
81
+ @pytest.mark.parametrize(
82
+ "client_header", ["X-NDB-Client", "x-ndb-client", "X-NdB-ClIeNt"]
83
+ )
84
+ async def test_ask_preserves_caller_headers_without_mutating_them(client_header):
85
+ client = AsyncMock()
86
+ driver = make_driver(client)
87
+ headers = {client_header: "widget", "X-Synchronous": "true"}
88
+ original_headers = headers.copy()
89
+ request = AskRequest(query="question")
90
+
91
+ await driver.ask(request, headers=headers)
92
+
93
+ kwargs = client.ask.await_args.kwargs
94
+ sent_headers = httpx.Headers(kwargs["headers"])
95
+ assert sent_headers.get_list("x-ndb-client") == ["agentic"]
96
+ assert sent_headers["X-Synchronous"] == "true"
97
+ assert kwargs["kbid"] == "test-kb"
98
+ assert kwargs["content"] is request
99
+ assert headers == original_headers
@@ -311,6 +311,60 @@ async def test_hybrid_dynamic_connection_is_authorized():
311
311
  assert result == {"source": {SYNC_CONFIG_ID: ["connected-resource"]}}
312
312
 
313
313
 
314
+ async def test_hybrid_dynamic_connection_ignores_unrequested_credentials():
315
+ agent = SyncAskAgent(SyncAskAgentConfig(sources=["source"]))
316
+ driver = SimpleNamespace(
317
+ config=SimpleNamespace(connection_ids=[]),
318
+ sync_configs={SYNC_CONFIG_ID: [EXTERNAL_CONNECTION_ID]},
319
+ information={
320
+ EXTERNAL_CONNECTION_ID: SimpleNamespace(provider="sharefile_oauth")
321
+ },
322
+ validate_resources=AsyncMock(return_value=["connected-resource"]),
323
+ )
324
+ resource = SimpleNamespace(
325
+ id="connected-resource",
326
+ origin=SimpleNamespace(
327
+ source_id=f"sync_config_{SYNC_CONFIG_ID}",
328
+ sync_metadata=SimpleNamespace(model_dump=MagicMock(return_value={})),
329
+ ),
330
+ )
331
+ ndb = SimpleNamespace(
332
+ config=SimpleNamespace(kbid="kb"),
333
+ driver=SimpleNamespace(get_resource_by_id=AsyncMock(return_value=resource)),
334
+ )
335
+ manager = SimpleNamespace(
336
+ drivers=SimpleNamespace(get=MagicMock(return_value=driver))
337
+ )
338
+ memory = MagicMock()
339
+ memory.get_session_id.return_value = "session"
340
+ memory.send_feedback = AsyncMock(
341
+ return_value=UserToAgentInteraction(
342
+ request_id="session",
343
+ response=json.dumps(
344
+ {
345
+ "existing_credentials": {
346
+ SYNC_CONFIG_ID: {EXTERNAL_CONNECTION_ID: "credential"},
347
+ "00000000-0000-0000-0000-000000000002": {
348
+ "unrequested-connection": "credential"
349
+ },
350
+ }
351
+ }
352
+ ),
353
+ )
354
+ )
355
+
356
+ with patch("hyperforge_nucliadb.sync.agent.get_ndb_driver", return_value=ndb):
357
+ result = await agent._post_filter_configured_resources(
358
+ memory=memory,
359
+ manager=manager, # type: ignore[arg-type]
360
+ resources={"source": [resource.id]},
361
+ allowed_connection_ids={SYNC_CONFIG_ID},
362
+ )
363
+
364
+ assert result == {"source": {SYNC_CONFIG_ID: ["connected-resource"]}}
365
+ driver.validate_resources.assert_awaited_once()
366
+
367
+
314
368
  async def test_hybrid_resources_without_connections_skip_authorization():
315
369
  agent = SyncAskAgent(SyncAskAgentConfig(sources=["source"]))
316
370
  driver = SimpleNamespace(config=SimpleNamespace(connection_ids=[]))
@@ -1,6 +0,0 @@
1
- # NucliaDB Hyperforge agents
2
-
3
- Basic ASK - Basic initial agent
4
- Advanced ASK (deprecated)
5
- ASK (experimental)
6
- Sync (Sync Service oauth linked)
@@ -1,34 +0,0 @@
1
- from types import SimpleNamespace
2
- from unittest.mock import AsyncMock
3
-
4
- import pytest
5
-
6
- from hyperforge_nucliadb.driver import NucliaDBDriver
7
-
8
-
9
- def make_driver(client):
10
- return NucliaDBDriver.model_construct(
11
- provider="nucliadb",
12
- name="test",
13
- config=SimpleNamespace(kbid="test-kb"),
14
- driver=client,
15
- manager=SimpleNamespace(),
16
- _synonyms=None,
17
- )
18
-
19
-
20
- @pytest.mark.asyncio
21
- async def test_labels_uses_complete_labelsets_response():
22
- client = AsyncMock()
23
- client.get_labelsets.return_value = SimpleNamespace(
24
- labelsets={
25
- "type": SimpleNamespace(labels=[SimpleNamespace(title="report")]),
26
- "topic": SimpleNamespace(labels=[SimpleNamespace(title="science")]),
27
- }
28
- )
29
- driver = make_driver(client)
30
-
31
- assert await driver.labels() == {"type": ["report"], "topic": ["science"]}
32
-
33
- client.get_labelsets.assert_awaited_once_with(kbid="test-kb")
34
- client.get_labelset.assert_not_awaited()