hyperforge-nucliadb 1.0.0.post108__tar.gz → 1.0.0.post113__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 (44) hide show
  1. {hyperforge_nucliadb-1.0.0.post108 → hyperforge_nucliadb-1.0.0.post113}/PKG-INFO +11 -1
  2. hyperforge_nucliadb-1.0.0.post113/README.md +16 -0
  3. {hyperforge_nucliadb-1.0.0.post108 → hyperforge_nucliadb-1.0.0.post113}/pyproject.toml +1 -1
  4. {hyperforge_nucliadb-1.0.0.post108 → hyperforge_nucliadb-1.0.0.post113}/src/hyperforge_nucliadb/basic_ask_agent.py +16 -1
  5. {hyperforge_nucliadb-1.0.0.post108 → hyperforge_nucliadb-1.0.0.post113}/src/hyperforge_nucliadb/driver.py +8 -2
  6. {hyperforge_nucliadb-1.0.0.post108 → hyperforge_nucliadb-1.0.0.post113}/src/hyperforge_nucliadb.egg-info/PKG-INFO +11 -1
  7. {hyperforge_nucliadb-1.0.0.post108 → hyperforge_nucliadb-1.0.0.post113}/src/hyperforge_nucliadb.egg-info/SOURCES.txt +1 -0
  8. hyperforge_nucliadb-1.0.0.post113/tests/test_driver.py +99 -0
  9. hyperforge_nucliadb-1.0.0.post113/tests/test_facets_count.py +72 -0
  10. hyperforge_nucliadb-1.0.0.post108/README.md +0 -6
  11. hyperforge_nucliadb-1.0.0.post108/tests/test_driver.py +0 -34
  12. {hyperforge_nucliadb-1.0.0.post108 → hyperforge_nucliadb-1.0.0.post113}/setup.cfg +0 -0
  13. {hyperforge_nucliadb-1.0.0.post108 → hyperforge_nucliadb-1.0.0.post113}/src/hyperforge_nucliadb/__init__.py +0 -0
  14. {hyperforge_nucliadb-1.0.0.post108 → hyperforge_nucliadb-1.0.0.post113}/src/hyperforge_nucliadb/advanced_ask_agent.py +0 -0
  15. {hyperforge_nucliadb-1.0.0.post108 → hyperforge_nucliadb-1.0.0.post113}/src/hyperforge_nucliadb/advanced_ask_config.py +0 -0
  16. {hyperforge_nucliadb-1.0.0.post108 → hyperforge_nucliadb-1.0.0.post113}/src/hyperforge_nucliadb/ask/__init__.py +0 -0
  17. {hyperforge_nucliadb-1.0.0.post108 → hyperforge_nucliadb-1.0.0.post113}/src/hyperforge_nucliadb/ask/analysis.py +0 -0
  18. {hyperforge_nucliadb-1.0.0.post108 → hyperforge_nucliadb-1.0.0.post113}/src/hyperforge_nucliadb/ask/ask.py +0 -0
  19. {hyperforge_nucliadb-1.0.0.post108 → hyperforge_nucliadb-1.0.0.post113}/src/hyperforge_nucliadb/ask/config.py +0 -0
  20. {hyperforge_nucliadb-1.0.0.post108 → hyperforge_nucliadb-1.0.0.post113}/src/hyperforge_nucliadb/ask/hydrate.py +0 -0
  21. {hyperforge_nucliadb-1.0.0.post108 → hyperforge_nucliadb-1.0.0.post113}/src/hyperforge_nucliadb/ask/kb_analysis.py +0 -0
  22. {hyperforge_nucliadb-1.0.0.post108 → hyperforge_nucliadb-1.0.0.post113}/src/hyperforge_nucliadb/ask/knowledge_scan.py +0 -0
  23. {hyperforge_nucliadb-1.0.0.post108 → hyperforge_nucliadb-1.0.0.post113}/src/hyperforge_nucliadb/ask/models.py +0 -0
  24. {hyperforge_nucliadb-1.0.0.post108 → hyperforge_nucliadb-1.0.0.post113}/src/hyperforge_nucliadb/ask/multi.py +0 -0
  25. {hyperforge_nucliadb-1.0.0.post108 → hyperforge_nucliadb-1.0.0.post113}/src/hyperforge_nucliadb/ask/nucliadb.py +0 -0
  26. {hyperforge_nucliadb-1.0.0.post108 → hyperforge_nucliadb-1.0.0.post113}/src/hyperforge_nucliadb/ask/prompt_analysis.py +0 -0
  27. {hyperforge_nucliadb-1.0.0.post108 → hyperforge_nucliadb-1.0.0.post113}/src/hyperforge_nucliadb/ask/query_analysis.py +0 -0
  28. {hyperforge_nucliadb-1.0.0.post108 → hyperforge_nucliadb-1.0.0.post113}/src/hyperforge_nucliadb/ask/rerank.py +0 -0
  29. {hyperforge_nucliadb-1.0.0.post108 → hyperforge_nucliadb-1.0.0.post113}/src/hyperforge_nucliadb/ask/utils.py +0 -0
  30. {hyperforge_nucliadb-1.0.0.post108 → hyperforge_nucliadb-1.0.0.post113}/src/hyperforge_nucliadb/ask_utils.py +0 -0
  31. {hyperforge_nucliadb-1.0.0.post108 → hyperforge_nucliadb-1.0.0.post113}/src/hyperforge_nucliadb/basic_ask_config.py +0 -0
  32. {hyperforge_nucliadb-1.0.0.post108 → hyperforge_nucliadb-1.0.0.post113}/src/hyperforge_nucliadb/driver_config.py +0 -0
  33. {hyperforge_nucliadb-1.0.0.post108 → hyperforge_nucliadb-1.0.0.post113}/src/hyperforge_nucliadb/py.typed +0 -0
  34. {hyperforge_nucliadb-1.0.0.post108 → hyperforge_nucliadb-1.0.0.post113}/src/hyperforge_nucliadb/sync/__init__.py +0 -0
  35. {hyperforge_nucliadb-1.0.0.post108 → hyperforge_nucliadb-1.0.0.post113}/src/hyperforge_nucliadb/sync/agent.py +0 -0
  36. {hyperforge_nucliadb-1.0.0.post108 → hyperforge_nucliadb-1.0.0.post113}/src/hyperforge_nucliadb/sync/config.py +0 -0
  37. {hyperforge_nucliadb-1.0.0.post108 → hyperforge_nucliadb-1.0.0.post113}/src/hyperforge_nucliadb/sync/config_driver.py +0 -0
  38. {hyperforge_nucliadb-1.0.0.post108 → hyperforge_nucliadb-1.0.0.post113}/src/hyperforge_nucliadb/sync/driver.py +0 -0
  39. {hyperforge_nucliadb-1.0.0.post108 → hyperforge_nucliadb-1.0.0.post113}/src/hyperforge_nucliadb.egg-info/dependency_links.txt +0 -0
  40. {hyperforge_nucliadb-1.0.0.post108 → hyperforge_nucliadb-1.0.0.post113}/src/hyperforge_nucliadb.egg-info/requires.txt +0 -0
  41. {hyperforge_nucliadb-1.0.0.post108 → hyperforge_nucliadb-1.0.0.post113}/src/hyperforge_nucliadb.egg-info/top_level.txt +0 -0
  42. {hyperforge_nucliadb-1.0.0.post108 → hyperforge_nucliadb-1.0.0.post113}/tests/test_ask_utils.py +0 -0
  43. {hyperforge_nucliadb-1.0.0.post108 → hyperforge_nucliadb-1.0.0.post113}/tests/test_nucliadb.py +0 -0
  44. {hyperforge_nucliadb-1.0.0.post108 → hyperforge_nucliadb-1.0.0.post113}/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.post108
3
+ Version: 1.0.0.post113
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.post108"
7
+ version = "1.0.0.post113"
8
8
  license = "Apache-2.0"
9
9
  description = "NucliaDB Hyperforge agent"
10
10
  authors = [{ name = "Nuclia", email = "nucliadb@nuclia.com" }]
@@ -1554,6 +1554,17 @@ class BasicAskAgent(ContextAgent, Agent[BasicAskAgentConfig]):
1554
1554
  logger.info(
1555
1555
  f"No labels selected for faceted search on question {question}, skipping."
1556
1556
  )
1557
+ context = Context(
1558
+ agent_id=self.agent_id,
1559
+ original_question_uuid=memory.original_question_uuid,
1560
+ actual_question_uuid=None,
1561
+ missing=question,
1562
+ question=question,
1563
+ source=source.id,
1564
+ chunks=[],
1565
+ agent="basic_ask_facets",
1566
+ title=f"Faceted search on {source.id} Knowledge Box",
1567
+ )
1557
1568
  else:
1558
1569
  # We are assuming resources equals fields here
1559
1570
  facets, total = await ndb.field_labels(labelsets=labels_selected)
@@ -1593,7 +1604,11 @@ class BasicAskAgent(ContextAgent, Agent[BasicAskAgentConfig]):
1593
1604
  await memory.add_step(
1594
1605
  step_module=self.config.module,
1595
1606
  step_title=self.step_title("Faceted count"),
1596
- step_reason="Faceted search performed",
1607
+ step_reason=(
1608
+ "Faceted search performed"
1609
+ if labels_selected
1610
+ else "Faceted search skipped"
1611
+ ),
1597
1612
  step_value=selected_labels.get("reasoning", ""),
1598
1613
  timeit=time() - t0,
1599
1614
  input_nuclia_tokens=input_nt,
@@ -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(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperforge_nucliadb
3
- Version: 1.0.0.post108
3
+ Version: 1.0.0.post113
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.
@@ -35,5 +35,6 @@ src/hyperforge_nucliadb/sync/config_driver.py
35
35
  src/hyperforge_nucliadb/sync/driver.py
36
36
  tests/test_ask_utils.py
37
37
  tests/test_driver.py
38
+ tests/test_facets_count.py
38
39
  tests/test_nucliadb.py
39
40
  tests/test_sync.py
@@ -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
@@ -0,0 +1,72 @@
1
+ from unittest.mock import AsyncMock, Mock
2
+
3
+ import pytest
4
+ from hyperforge.models import Context, Source
5
+
6
+ from hyperforge_nucliadb.basic_ask_agent import BasicAskAgent
7
+ from hyperforge_nucliadb.basic_ask_config import BasicAskAgentConfig
8
+
9
+
10
+ @pytest.mark.asyncio
11
+ @pytest.mark.parametrize("label_sets", [[], ["topic"]])
12
+ async def test_facets_count_handles_label_selection(mocker, label_sets):
13
+ agent = BasicAskAgent(BasicAskAgentConfig(sources=["kb"]), agent_id="facets-agent")
14
+ source = Source.model_construct(id="kb")
15
+ mocker.patch(
16
+ "hyperforge_nucliadb.basic_ask_agent.choose_source",
17
+ new=AsyncMock(return_value=[source]),
18
+ )
19
+ driver = Mock(
20
+ labels=AsyncMock(return_value={"topic": ["science"]}),
21
+ field_labels=AsyncMock(return_value=({"topic": {"science": 2}}, 2)),
22
+ )
23
+ mocker.patch(
24
+ "hyperforge_nucliadb.basic_ask_agent.get_ndb_driver", return_value=driver
25
+ )
26
+ memory = Mock(original_question_uuid="question-id", add_step=AsyncMock())
27
+ manager = Mock(
28
+ execute_json=AsyncMock(
29
+ return_value=(
30
+ {"label_sets": label_sets, "reasoning": "Label selection"},
31
+ 1,
32
+ 2,
33
+ )
34
+ ),
35
+ execute=AsyncMock(return_value=("There are 2 science documents.", 3, 4, 0)),
36
+ )
37
+
38
+ contexts = await agent.facets_count(memory, manager, question="How many?")
39
+
40
+ assert len(contexts) == 1
41
+ context = contexts[0]
42
+ assert isinstance(context, Context)
43
+ assert context.agent_id == "facets-agent"
44
+ assert context.original_question_uuid == "question-id"
45
+ assert context.actual_question_uuid is None
46
+ assert context.question == "How many?"
47
+ assert context.source == "kb"
48
+ assert context.agent == "basic_ask_facets"
49
+ assert context.title == "Faceted search on kb Knowledge Box"
50
+ memory.add_step.assert_awaited_once()
51
+ step = memory.add_step.call_args.kwargs
52
+ assert step["step_value"] == "Label selection"
53
+ assert step["step_agent_path"] == "/context/facets-agent/facets_count"
54
+
55
+ if label_sets:
56
+ assert context.missing is None
57
+ assert len(context.chunks) == 1
58
+ assert context.chunks[0].chunk_id == "facets_count_result-0"
59
+ assert context.chunks[0].text == "There are 2 science documents."
60
+ driver.field_labels.assert_awaited_once_with(labelsets=label_sets)
61
+ manager.execute.assert_awaited_once()
62
+ assert step["step_reason"] == "Faceted search performed"
63
+ assert step["input_nuclia_tokens"] == 4
64
+ assert step["output_nuclia_tokens"] == 6
65
+ else:
66
+ assert context.missing == "How many?"
67
+ assert context.chunks == []
68
+ driver.field_labels.assert_not_awaited()
69
+ manager.execute.assert_not_awaited()
70
+ assert step["step_reason"] == "Faceted search skipped"
71
+ assert step["input_nuclia_tokens"] == 1
72
+ assert step["output_nuclia_tokens"] == 2
@@ -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()