nucliadb-agentic-api 1.0.0.post211__tar.gz → 1.0.0.post230__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 (46) hide show
  1. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/PKG-INFO +1 -1
  2. nucliadb_agentic_api-1.0.0.post230/VERSION +1 -0
  3. nucliadb_agentic_api-1.0.0.post230/alembic/versions/c4d5e6f7a8b9_add_agentic_config_soft_deletion.py +39 -0
  4. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/pyproject.toml +4 -3
  5. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/pyproject.toml.orig +4 -3
  6. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/src/nucliadb_agentic_api/agentic/ask_handler.py +3 -0
  7. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/src/nucliadb_agentic_api/agentic/ask_result.py +5 -2
  8. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/src/nucliadb_agentic_api/app.py +21 -7
  9. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/src/nucliadb_agentic_api/commands.py +15 -0
  10. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/src/nucliadb_agentic_api/db/agentic_configs.py +67 -25
  11. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/src/nucliadb_agentic_api/db/settings.py +2 -0
  12. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/src/nucliadb_agentic_api/db/transform.py +90 -87
  13. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/src/nucliadb_agentic_api/exceptions.py +4 -0
  14. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/src/nucliadb_agentic_api/models.py +4 -4
  15. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/src/nucliadb_agentic_api/server/server.py +0 -3
  16. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/src/nucliadb_agentic_api/server/session.py +6 -0
  17. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/src/nucliadb_agentic_api/v1/agentic_configs.py +25 -0
  18. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/src/nucliadb_agentic_api/v1/ask.py +6 -0
  19. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/src/nucliadb_agentic_api/v1/mcp_nucliadb.py +7 -1
  20. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/src/nucliadb_agentic_api/v1/sources.py +12 -7
  21. nucliadb_agentic_api-1.0.0.post211/VERSION +0 -1
  22. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/README.md +0 -0
  23. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/alembic/README +0 -0
  24. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/alembic/env.py +0 -0
  25. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/alembic/script.py.mako +0 -0
  26. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/alembic/versions/7f3b2f92a8c1_add_agentic_config_table.py +0 -0
  27. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/alembic/versions/b3c4d5e6f7a8_add_sources_table.py +0 -0
  28. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/alembic.ini +0 -0
  29. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/src/nucliadb_agentic_api/VERSION +0 -0
  30. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/src/nucliadb_agentic_api/__init__.py +0 -0
  31. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/src/nucliadb_agentic_api/agentic/__init__.py +0 -0
  32. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/src/nucliadb_agentic_api/agentic/ask_transform_to_interaction.py +0 -0
  33. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/src/nucliadb_agentic_api/db/__init__.py +0 -0
  34. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/src/nucliadb_agentic_api/db/sources.py +0 -0
  35. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/src/nucliadb_agentic_api/logging.py +0 -0
  36. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/src/nucliadb_agentic_api/py.typed +0 -0
  37. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/src/nucliadb_agentic_api/server/__init__.py +0 -0
  38. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/src/nucliadb_agentic_api/server/settings.py +0 -0
  39. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/src/nucliadb_agentic_api/settings.py +0 -0
  40. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/src/nucliadb_agentic_api/utils.py +0 -0
  41. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/src/nucliadb_agentic_api/v1/__init__.py +0 -0
  42. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/src/nucliadb_agentic_api/v1/ask_websocket.py +0 -0
  43. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/src/nucliadb_agentic_api/v1/mcp_content.py +0 -0
  44. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/src/nucliadb_agentic_api/v1/oauth.py +0 -0
  45. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/src/nucliadb_agentic_api/v1/router.py +0 -0
  46. {nucliadb_agentic_api-1.0.0.post211 → nucliadb_agentic_api-1.0.0.post230}/src/nucliadb_agentic_api/v1/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nucliadb_agentic_api
3
- Version: 1.0.0.post211
3
+ Version: 1.0.0.post230
4
4
  Summary: NucliaDB Agentic API for Hyperforge
5
5
  Author: AI Data Team
6
6
  Author-email: AI Data Team <learning@nuclia.com>
@@ -0,0 +1 @@
1
+ 1.0.0.post230
@@ -0,0 +1,39 @@
1
+ """Add agentic config soft deletion
2
+
3
+ Revision ID: c4d5e6f7a8b9
4
+ Revises: b3c4d5e6f7a8
5
+ Create Date: 2026-08-06 00:00:00.000000
6
+
7
+ """
8
+
9
+ from typing import Sequence, Union
10
+
11
+ import sqlalchemy as sa
12
+
13
+ from alembic import op
14
+
15
+ revision: str = "c4d5e6f7a8b9"
16
+ down_revision: Union[str, None] = "b3c4d5e6f7a8"
17
+ branch_labels: Union[str, Sequence[str], None] = None
18
+ depends_on: Union[str, Sequence[str], None] = None
19
+
20
+
21
+ def upgrade() -> None:
22
+ op.add_column(
23
+ "agentic_config_table",
24
+ sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True),
25
+ )
26
+ op.create_index(
27
+ op.f("ix_agentic_config_table_deleted_at"),
28
+ "agentic_config_table",
29
+ ["deleted_at"],
30
+ unique=False,
31
+ )
32
+
33
+
34
+ def downgrade() -> None:
35
+ op.drop_index(
36
+ op.f("ix_agentic_config_table_deleted_at"),
37
+ table_name="agentic_config_table",
38
+ )
39
+ op.drop_column("agentic_config_table", "deleted_at")
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "nucliadb_agentic_api"
7
- version = "1.0.0.post211"
7
+ version = "1.0.0.post230"
8
8
  description = "NucliaDB Agentic API for Hyperforge"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -28,6 +28,7 @@ nucliadb-agentic-api = "nucliadb_agentic_api.commands:run"
28
28
  nucliadb-agentic-server = "nucliadb_agentic_api.server.server:run"
29
29
  nucliadb-agentic-sandbox = "hyperforge.server.sandbox:run"
30
30
  nucliadb-agentic-api-extract-openapi = "nucliadb_agentic_api.commands:extract_openapi"
31
+ nucliadb-agentic-api-purge-deleted-configs = "nucliadb_agentic_api.commands:purge_deleted_agentic_configs"
31
32
 
32
33
  [tool.setuptools.packages.find]
33
34
  where = ["src/"]
@@ -65,13 +66,13 @@ server = [
65
66
  "aiohttp<3.14.0",
66
67
  "backoff",
67
68
  "nats-py",
68
- "nuclia>=4.11.5",
69
+ "nuclia>=4.14.0",
69
70
  "nuclia-models>=0.59.0",
70
71
  "nucliadb-protos",
71
72
  "nucliadb-sdk>=6.14.0.post6532",
72
73
  "nucliadb-telemetry[otel]>=6.14.0.post6534",
73
74
  "nucliadb-utils[storages,fastapi]>=6.15.0.post6671",
74
- "hyperforge>=1.0.0.post88",
75
+ "hyperforge>=1.0.0.post237",
75
76
  "hyperforge_conditional>=1.0.0.post27",
76
77
  "hyperforge_mcp>=1.0.0.post41",
77
78
  "hyperforge_nucliadb>=1.0.0.post46",
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "nucliadb_agentic_api"
7
- version = "1.0.0.post211"
7
+ version = "1.0.0.post230"
8
8
  authors = [{ name = "AI Data Team", email = "learning@nuclia.com" }]
9
9
  description = "NucliaDB Agentic API for Hyperforge"
10
10
  readme = "README.md"
@@ -52,6 +52,7 @@ nucliadb-agentic-api = "nucliadb_agentic_api.commands:run"
52
52
  nucliadb-agentic-server = "nucliadb_agentic_api.server.server:run"
53
53
  nucliadb-agentic-sandbox = "hyperforge.server.sandbox:run"
54
54
  nucliadb-agentic-api-extract-openapi = "nucliadb_agentic_api.commands:extract_openapi"
55
+ nucliadb-agentic-api-purge-deleted-configs = "nucliadb_agentic_api.commands:purge_deleted_agentic_configs"
55
56
 
56
57
  [dependency-groups]
57
58
 
@@ -60,13 +61,13 @@ server = [
60
61
  "aiohttp<3.14.0",
61
62
  "backoff",
62
63
  "nats-py",
63
- "nuclia>=4.11.5",
64
+ "nuclia>=4.14.0",
64
65
  "nuclia-models>=0.59.0",
65
66
  "nucliadb-protos",
66
67
  "nucliadb-sdk>=6.14.0.post6532",
67
68
  "nucliadb-telemetry[otel]>=6.14.0.post6534",
68
69
  "nucliadb-utils[storages,fastapi]>=6.15.0.post6671",
69
- "hyperforge>=1.0.0.post88",
70
+ "hyperforge>=1.0.0.post237",
70
71
  "hyperforge_conditional>=1.0.0.post27",
71
72
  "hyperforge_mcp>=1.0.0.post41",
72
73
  "hyperforge_nucliadb>=1.0.0.post46",
@@ -8,6 +8,7 @@ from hyperforge_nucliadb_agentic.ask.model import (
8
8
  from hyperforge_nucliadb_agentic.ask.search.ask import (
9
9
  handled_ask_exceptions,
10
10
  )
11
+ from nuclia.lib.nua_responses import CitationsType
11
12
  from nucliadb_models.search import (
12
13
  NucliaDBClientType,
13
14
  )
@@ -34,6 +35,8 @@ async def create_agentic_response(
34
35
  extra_predict_headers: dict[str, str] | None = None,
35
36
  ) -> Response:
36
37
  ask_request.max_tokens = parse_max_tokens(ask_request.max_tokens)
38
+ if ask_request.citations is None:
39
+ ask_request.citations = CitationsType.LLM_FOOTNOTES
37
40
  ask_result = AgenticAskResult(
38
41
  app=app,
39
42
  kbid=kbid,
@@ -27,8 +27,11 @@ from hyperforge_nucliadb_agentic.ask.model import (
27
27
  StatusAskResponseItem,
28
28
  TokensDetail,
29
29
  )
30
- from hyperforge_nucliadb_agentic.ask.predict import AnswerStatusCode
31
- from hyperforge_nucliadb_agentic.ask.search.ask import AskResult, RetrievalMatch
30
+ from hyperforge_nucliadb_agentic.ask.search.ask import (
31
+ AnswerStatusCode,
32
+ AskResult,
33
+ RetrievalMatch,
34
+ )
32
35
  from hyperforge_nucliadb_agentic.ask.search.metrics import (
33
36
  AskMetrics,
34
37
  )
@@ -8,23 +8,21 @@ from hyperforge.api.authentication import RaoAuthenticationBackend
8
8
  from hyperforge.broker import Broker
9
9
  from hyperforge.broker.redis import RedisBroker
10
10
  from hyperforge.driver import Driver
11
+ from hyperforge.manager import Manager
11
12
  from hyperforge_nucliadb_agentic.ask.audit import (
12
13
  AuditMiddleware,
13
14
  start_audit_utility,
14
15
  stop_audit_utility,
15
16
  )
16
- from hyperforge_nucliadb_agentic.ask.predict import (
17
- start_predict_engine,
18
- stop_predict_engine,
19
- )
20
17
  from lru import LRU
21
18
  from mcp.server.lowlevel.server import Server as MCPServer
22
19
  from mcp.server.streamable_http import (
23
20
  StreamableHTTPServerTransport,
24
21
  )
22
+ from nuclia.lib.nua import AsyncNuaClient
25
23
  from nucliadb_sdk import NucliaDBAsync
26
24
  from nucliadb_telemetry.utils import clean_telemetry, setup_telemetry
27
- from nucliadb_utils.settings import AuditSettings
25
+ from nucliadb_utils.settings import AuditSettings, nuclia_settings
28
26
  from prometheus_client import CONTENT_TYPE_LATEST
29
27
  from starlette.middleware.authentication import AuthenticationMiddleware
30
28
  from starlette.responses import PlainTextResponse
@@ -61,6 +59,7 @@ class HTTPApplication(FastAPI):
61
59
  source_manager: Sources
62
60
  broker: Broker
63
61
  hyperforge_drivers: dict[str, "Driver"]
62
+ predict_manager: Manager
64
63
 
65
64
  def __init__(
66
65
  self,
@@ -105,9 +104,24 @@ class HTTPApplication(FastAPI):
105
104
  async def startup(self) -> None:
106
105
  await setup_telemetry(SERVICE_NAME)
107
106
 
108
- await start_predict_engine()
109
107
  await start_audit_utility(SERVICE_NAME, self.audit_settings)
110
108
 
109
+ if nuclia_settings.onprem:
110
+ nua = AsyncNuaClient.onprem(
111
+ nuclia_settings.nuclia_public_url,
112
+ service_account=nuclia_settings.nuclia_service_account,
113
+ zone=nuclia_settings.nuclia_zone,
114
+ local_predict=nuclia_settings.local_predict,
115
+ local_predict_headers=nuclia_settings.local_predict_headers,
116
+ )
117
+ else:
118
+ nua = AsyncNuaClient.internal(nuclia_settings.nuclia_inner_predict_url)
119
+ self.predict_manager = await Manager.from_config(
120
+ drivers=[],
121
+ nua=nua,
122
+ send_rao_origin=False,
123
+ )
124
+
111
125
  self.broker = RedisBroker.from_url(
112
126
  url=self.settings.valkey_url,
113
127
  activate_subject=self.settings.activate_subject,
@@ -163,5 +177,5 @@ class HTTPApplication(FastAPI):
163
177
  await self.source_manager.finalize()
164
178
  await self.broker.finalize()
165
179
  await stop_audit_utility()
166
- await stop_predict_engine()
180
+ await self.predict_manager.aclose()
167
181
  await clean_telemetry(SERVICE_NAME)
@@ -1,3 +1,5 @@
1
+ import asyncio
2
+
1
3
  import uvicorn
2
4
  from hyperforge import openapi
3
5
  from hyperforge.feature_flag import get_flag_service
@@ -9,6 +11,7 @@ from nucliadb_utils.settings import AuditSettings
9
11
 
10
12
  from nucliadb_agentic_api import SERVICE_NAME
11
13
  from nucliadb_agentic_api.app import HTTPApplication
14
+ from nucliadb_agentic_api.db.agentic_configs import AgenticConfigs
12
15
  from nucliadb_agentic_api.db.settings import DataManagerSettings
13
16
  from nucliadb_agentic_api.logging import set_sentry
14
17
  from nucliadb_agentic_api.settings import Settings
@@ -63,3 +66,15 @@ def extract_openapi():
63
66
  openapi.extract_openapi_command(
64
67
  "nucliadb_agentic_api", "NucliaDB Agentic API", router
65
68
  )
69
+
70
+
71
+ def purge_deleted_agentic_configs():
72
+ async def purge() -> int:
73
+ manager = await AgenticConfigs.from_settings(DataManagerSettings()) # type: ignore
74
+ await manager.initialize()
75
+ try:
76
+ return await manager.hard_delete_expired_configs()
77
+ finally:
78
+ await manager.finalize()
79
+
80
+ asyncio.run(purge())
@@ -1,5 +1,4 @@
1
1
  import datetime
2
- import json
3
2
  from time import time
4
3
  from typing import Dict
5
4
 
@@ -46,6 +45,7 @@ agentic_config_table = sa.Table(
46
45
  sa.Column("agentic_id", sa.String, primary_key=True, nullable=False), # Agentic ID
47
46
  sa.Column("created", sa.DateTime, default=sa.func.now()),
48
47
  sa.Column("modified", sa.DateTime, onupdate=sa.func.now()),
48
+ sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True, index=True),
49
49
  sa.Column("title", sa.String, nullable=True),
50
50
  sa.Column("config", JSONB, nullable=False),
51
51
  )
@@ -120,6 +120,7 @@ class AgenticConfigs:
120
120
  agentic_config_table.c.account == account,
121
121
  agentic_config_table.c.kbid == kbid,
122
122
  agentic_config_table.c.agentic_id == agentic_id,
123
+ agentic_config_table.c.deleted_at.is_(None),
123
124
  )
124
125
  .returning(agentic_config_table.c.agentic_id)
125
126
  )
@@ -135,14 +136,30 @@ class AgenticConfigs:
135
136
  if source_ids:
136
137
  await self._validate_sources_exist(account, kbid, source_ids)
137
138
 
138
- query = sa.select(agentic_config_table.c.agentic_id).where(
139
+ query = sa.select(agentic_config_table.c.deleted_at).where(
139
140
  agentic_config_table.c.account == account,
140
141
  agentic_config_table.c.kbid == kbid,
141
142
  agentic_config_table.c.agentic_id == agentic_id,
142
143
  )
143
144
  existing = await self.database.fetch_one(query)
144
145
  if existing is not None:
145
- raise exceptions.Conflict("Agentic configuration already exists")
146
+ if existing["deleted_at"] is None:
147
+ raise exceptions.Conflict("Agentic configuration already exists")
148
+ await self.database.execute(
149
+ sa.update(agentic_config_table)
150
+ .values(
151
+ title=config.title,
152
+ config=_serialize_config(config),
153
+ deleted_at=None,
154
+ )
155
+ .where(
156
+ agentic_config_table.c.account == account,
157
+ agentic_config_table.c.kbid == kbid,
158
+ agentic_config_table.c.agentic_id == agentic_id,
159
+ )
160
+ )
161
+ CACHE[_cache_key(account, kbid, agentic_id)] = config
162
+ return
146
163
 
147
164
  insert_query = sa.insert(agentic_config_table).values(
148
165
  account=account,
@@ -172,35 +189,58 @@ class AgenticConfigs:
172
189
  f"Source(s) not found: {', '.join(missing)}"
173
190
  )
174
191
 
175
- async def delete_configs_referencing_source(
176
- self, account: str, kbid: str, source_id: str
177
- ) -> int:
178
- """Delete every agentic config that references source_id in its smart_agent
179
- sources list. Returns the number of deleted configs.
180
-
181
- Uses the JSONB containment operator (@>) to find matching rows:
182
- config->'smart_agent'->'sources' @> '[{"source_id": "<id>"}]'
183
- """
192
+ async def delete_agentic_config(
193
+ self, account: str, kbid: str, agentic_id: str
194
+ ) -> None:
184
195
  query = (
185
- sa.delete(agentic_config_table)
196
+ sa.update(agentic_config_table)
197
+ .values(deleted_at=utc_now())
186
198
  .where(
187
199
  agentic_config_table.c.account == account,
188
200
  agentic_config_table.c.kbid == kbid,
189
- agentic_config_table.c.config["smart_agent"]["sources"].op("@>")(
190
- sa.cast(
191
- json.dumps([{"source_id": source_id}]),
192
- JSONB,
193
- )
194
- ),
201
+ agentic_config_table.c.agentic_id == agentic_id,
202
+ agentic_config_table.c.deleted_at.is_(None),
195
203
  )
196
204
  .returning(agentic_config_table.c.agentic_id)
197
205
  )
198
- deleted_rows = await self.database.fetch_all(query)
199
- for row in deleted_rows:
200
- key = _cache_key(account, kbid, row["agentic_id"])
201
- if key in CACHE:
202
- del CACHE[key]
203
- return len(deleted_rows)
206
+ deleted = await self.database.fetch_one(query)
207
+ if deleted is None:
208
+ raise exceptions.NotFound("Agentic configuration not found")
209
+ CACHE.pop(_cache_key(account, kbid, agentic_id), None)
210
+
211
+ async def configs_referencing_source(
212
+ self, account: str, kbid: str, source_id: str
213
+ ) -> list[str]:
214
+ query = sa.select(agentic_config_table.c.agentic_id).where(
215
+ agentic_config_table.c.account == account,
216
+ agentic_config_table.c.kbid == kbid,
217
+ agentic_config_table.c.deleted_at.is_(None),
218
+ sa.func.jsonb_exists(
219
+ agentic_config_table.c.config["smart_agent"]["sources"], source_id
220
+ ),
221
+ )
222
+ return [row["agentic_id"] for row in await self.database.fetch_all(query)]
223
+
224
+ async def hard_delete_expired_configs(self) -> int:
225
+ cutoff = utc_now() - datetime.timedelta(
226
+ days=self.settings.agentic_config_deletion_retention_days
227
+ )
228
+ conditions = (
229
+ agentic_config_table.c.deleted_at.is_not(None),
230
+ agentic_config_table.c.deleted_at < cutoff,
231
+ )
232
+ count_query = (
233
+ sa.select(sa.func.count())
234
+ .select_from(agentic_config_table)
235
+ .where(*conditions)
236
+ )
237
+ count = await self.database.fetch_val(count_query)
238
+ if count == 0:
239
+ return 0
240
+
241
+ delete_query = sa.delete(agentic_config_table).where(*conditions)
242
+ await self.database.execute(delete_query)
243
+ return count
204
244
 
205
245
  async def get_agentic_config(
206
246
  self, account: str, kbid: str, agentic_id: str
@@ -211,6 +251,7 @@ class AgenticConfigs:
211
251
  agentic_config_table.c.account == account,
212
252
  agentic_config_table.c.kbid == kbid,
213
253
  agentic_config_table.c.agentic_id == agentic_id,
254
+ agentic_config_table.c.deleted_at.is_(None),
214
255
  )
215
256
  row = await self.database.fetch_one(query)
216
257
  if not row:
@@ -229,6 +270,7 @@ class AgenticConfigs:
229
270
  query = sa.select(agentic_config_table).where(
230
271
  agentic_config_table.c.account == account,
231
272
  agentic_config_table.c.kbid == kbid,
273
+ agentic_config_table.c.deleted_at.is_(None),
232
274
  )
233
275
  rows = await self.database.fetch_all(query)
234
276
  return {row["agentic_id"]: _config_from_row(row) for row in rows}
@@ -1,8 +1,10 @@
1
+ from pydantic import Field
1
2
  from pydantic_settings import BaseSettings
2
3
 
3
4
 
4
5
  class DataManagerSettings(BaseSettings):
5
6
  postgresql_dsn: str
7
+ agentic_config_deletion_retention_days: int = Field(default=30, ge=0)
6
8
  export_read_chunk_size: int = 1024 * 1024 # 1 MB
7
9
  export_read_max_size: int = 10 * 1024 * 1024 # 10 MB
8
10
 
@@ -1,11 +1,14 @@
1
- from typing import Any, Dict, Tuple
1
+ from typing import Dict, Tuple
2
2
  from uuid import uuid4
3
3
 
4
+ from hyperforge.agent import AgentConfig
5
+ from hyperforge.context.config import ContextAgentConfig
4
6
  from hyperforge.driver import DriverConfig
5
7
  from hyperforge.models import MemoryConfig, Rules
6
8
  from hyperforge.retrieval.config import RetrievalAgentConfig
7
9
  from hyperforge.workflows import WorkflowData
8
- from hyperforge_mcp.config import Transport
10
+ from hyperforge_google.config import GoogleAgentConfig
11
+ from hyperforge_mcp.config import MCPAgentConfig, Transport
9
12
  from hyperforge_mcp.config_driver import (
10
13
  MCPHTTPDriverConfig,
11
14
  MCPHTTPInnerConfig,
@@ -14,10 +17,15 @@ from hyperforge_nucliadb.driver_config import (
14
17
  NucliaDBConfig,
15
18
  NucliaDBConnection,
16
19
  )
20
+ from hyperforge_nucliadb_agentic.config import NucliaDBAgentConfig
17
21
  from hyperforge_nucliadb_agentic.internal_driver import (
18
22
  InternalNucliaDBConfig,
19
23
  InternalNucliaDBConnection,
20
24
  )
25
+ from hyperforge_perplexity.config import PerplexityAgentConfig
26
+ from hyperforge_rephrase.config import RephraseAgentConfig
27
+ from hyperforge_smart.config import SmartAgentConfig
28
+ from hyperforge_summarize.config import SummarizeAgentConfig
21
29
 
22
30
  from nucliadb_agentic_api.db.sources import Sources
23
31
  from nucliadb_agentic_api.models import AgenticConfigSchema
@@ -43,60 +51,35 @@ async def transform_agentic_config(
43
51
  global_driver = []
44
52
 
45
53
  title = agentic_config.title if agentic_config.title else "Default Agentic Config"
54
+ preprocess: list[AgentConfig] = []
55
+ context: list[AgentConfig] = []
56
+ generation: list[AgentConfig] = []
46
57
 
47
58
  if agentic_config.rephrase:
48
- config: Dict[str, Any] = {
49
- "title": f"{title} - Retrieval",
50
- }
51
- if agentic_config.rephrase.model:
52
- config["model"] = agentic_config.rephrase.model
53
- if agentic_config.rephrase.prompt:
54
- config["rules"] = [agentic_config.rephrase.prompt]
55
- if agentic_config.rephrase.ask_to:
56
- # Same KB with different layers
57
- config["kbid"] = agentic_config.rephrase.ask_to
58
- if agentic_config.rephrase.history:
59
- config["history"] = agentic_config.rephrase.history
60
-
61
- config["module"] = "rephrase"
62
- preprocess = [config]
63
- else:
64
- preprocess = []
59
+ rephrase = agentic_config.rephrase
60
+ rephrase_config = RephraseAgentConfig(
61
+ title=f"{title} - Retrieval",
62
+ rules=[rephrase.prompt] if rephrase.prompt else None,
63
+ kb=rephrase.ask_to,
64
+ history=rephrase.history if rephrase.history is not None else False,
65
+ )
66
+ if rephrase.model:
67
+ rephrase_config.model = rephrase.model
68
+ preprocess.append(rephrase_config)
65
69
 
66
70
  if agentic_config.smart_agent:
67
- config = {
68
- "title": f"{title} - Smart Agent",
69
- }
70
- if agentic_config.smart_agent.extra_prompt:
71
- config["extra_prompt"] = agentic_config.smart_agent.extra_prompt
72
- if agentic_config.smart_agent.models:
73
- if agentic_config.smart_agent.models.context_validation:
74
- config["context_validation_model"] = (
75
- agentic_config.smart_agent.models.context_validation
76
- )
77
- if agentic_config.smart_agent.models.planner:
78
- config["planner_model"] = agentic_config.smart_agent.models.planner
79
- if agentic_config.smart_agent.models.executor:
80
- config["executor_model"] = agentic_config.smart_agent.models.executor
81
- if agentic_config.smart_agent.history:
82
- config["history"] = agentic_config.smart_agent.history
83
- if agentic_config.smart_agent.extra_prompt:
84
- config["extra_prompt"] = agentic_config.smart_agent.extra_prompt
85
- registered_agents = []
86
- for source in agentic_config.smart_agent.sources:
71
+ smart_agent = agentic_config.smart_agent
72
+ registered_agents: list[ContextAgentConfig] = []
73
+ for source in smart_agent.sources:
87
74
  source_obj = await source_manager.get_source(account, kbid, source)
88
- source_config: Dict[str, Any] = {
89
- "title": f"{title} - Smart Agent - {source_obj.type.capitalize()} Source",
90
- }
75
+ source_title = (
76
+ f"{title} - Smart Agent - {source_obj.type.capitalize()} Source"
77
+ )
91
78
 
92
79
  if source_obj.type == "nucliadb":
93
80
  # Same KB different
94
81
  uid = uuid4().hex
95
82
 
96
- source_config["id"] = f"nucliadb-agent-{source}"
97
- source_config["sources"] = [uid]
98
- source_config["module"] = "nucliadb_agent"
99
-
100
83
  ndb_driver_config: DriverConfig
101
84
  if internal_nucliadb and internal_nucliadb_url:
102
85
  ndb_driver_config = InternalNucliaDBConfig(
@@ -137,15 +120,13 @@ async def transform_agentic_config(
137
120
  "No NucliaDB URL configured for internal or external access"
138
121
  )
139
122
  drivers[uid] = ndb_driver_config
140
- registered_agents.append(source_config)
123
+ registered_agents.append(
124
+ NucliaDBAgentConfig(
125
+ id=f"nucliadb-agent-{source}", title=source_title, sources=[uid]
126
+ )
127
+ )
141
128
  elif source_obj.type == "mcp":
142
129
  uid = uuid4().hex
143
- # See the nucliadb branch above for why this must be
144
- # deterministic rather than random.
145
- source_config["id"] = f"mcp-agent-{source}"
146
- source_config["sources"] = [uid]
147
- source_config["transport"] = Transport.HTTP
148
- source_config["module"] = "mcp"
149
130
  mcp_driver_config = MCPHTTPDriverConfig(
150
131
  identifier=uid,
151
132
  name="MCPHTTP",
@@ -156,46 +137,68 @@ async def transform_agentic_config(
156
137
  ),
157
138
  ) # TODO: pass real config if needed
158
139
  drivers[uid] = mcp_driver_config
159
- registered_agents.append(source_config)
140
+ registered_agents.append(
141
+ MCPAgentConfig(
142
+ id=f"mcp-agent-{source}",
143
+ title=source_title,
144
+ source=uid,
145
+ transport=Transport.HTTP,
146
+ tool_choice_model=source_obj.tool_choice_model or "chatgpt-4.1",
147
+ valid_headers=source_obj.valid_headers or [],
148
+ )
149
+ )
160
150
 
161
151
  elif source_obj.type == "google":
162
- source_config["id"] = f"google-agent-{source}"
163
- source_config["module"] = "google"
164
152
  global_driver.append("google")
165
- registered_agents.append(source_config)
153
+ registered_agents.append(
154
+ GoogleAgentConfig(id=f"google-agent-{source}", title=source_title)
155
+ )
166
156
 
167
157
  elif source_obj.type == "perplexity":
168
- source_config["id"] = f"perplexity-agent-{source}"
169
- source_config["module"] = "perplexity"
170
158
  global_driver.append("perplexity")
171
- registered_agents.append(source_config)
159
+ registered_agents.append(
160
+ PerplexityAgentConfig(
161
+ id=f"perplexity-agent-{source}",
162
+ title=source_title,
163
+ domain=source_obj.enabled_domains or [],
164
+ prompt=None,
165
+ )
166
+ )
172
167
 
173
- config["registered_agents"] = registered_agents
174
- config["module"] = "smart"
175
- context = [config]
176
- else:
177
- context = []
168
+ models = smart_agent.models
169
+ smart_config = SmartAgentConfig(
170
+ title=f"{title} - Smart Agent",
171
+ planning_mode=smart_agent.mode.value,
172
+ extra_prompt=smart_agent.extra_prompt,
173
+ history=smart_agent.history if smart_agent.history is not None else False,
174
+ # SmartAgentConfig's before-validator accepts serialized agent configs.
175
+ registered_agents=[agent.model_dump() for agent in registered_agents], # type: ignore[misc]
176
+ )
177
+ # Only set the models if they are provided; otherwise, leave them as None to use hyperforge's default models.
178
+ # TODO: Provide nucliadb agentic api level defaults for models
179
+ if models:
180
+ if models.context_validation:
181
+ smart_config.context_validation_model = models.context_validation
182
+ if models.planner:
183
+ smart_config.planner_model = models.planner
184
+ if models.executor:
185
+ smart_config.executor_model = models.executor
186
+ context.append(smart_config)
178
187
 
179
188
  if agentic_config.summarize:
180
- config = {
181
- "title": f"{title} - Summarize Agent",
182
- }
183
- if agentic_config.summarize.model:
184
- config["model"] = agentic_config.summarize.model
185
- if agentic_config.summarize.user_prompt:
186
- config["user_prompt"] = agentic_config.summarize.user_prompt
187
- if agentic_config.summarize.system_prompt:
188
- config["system_prompt"] = agentic_config.summarize.system_prompt
189
- if agentic_config.summarize.conversational:
190
- config["conversational"] = agentic_config.summarize.conversational
191
- if agentic_config.summarize.history:
192
- config["history"] = agentic_config.summarize.history
193
-
194
- config["module"] = "summarize"
195
-
196
- generation = [config]
197
- else:
198
- generation = []
189
+ summarize = agentic_config.summarize
190
+ summarize_config = SummarizeAgentConfig(
191
+ title=f"{title} - Summarize Agent",
192
+ prompt=summarize.user_prompt,
193
+ system_prompt=summarize.system_prompt,
194
+ conversational=summarize.conversational,
195
+ citations=True,
196
+ force_chunk_level_citations=False,
197
+ history=summarize.history if summarize.history is not None else False,
198
+ )
199
+ if summarize.model:
200
+ summarize_config.model = summarize.model
201
+ generation.append(summarize_config)
199
202
 
200
203
  return (
201
204
  RetrievalAgentConfig(
@@ -208,9 +211,9 @@ async def transform_agentic_config(
208
211
  description="Default workflow for agentic config transformation",
209
212
  parameters=None,
210
213
  ),
211
- preprocess=preprocess, # type: ignore
212
- context=context, # type: ignore
213
- generation=generation, # type: ignore
214
+ preprocess=preprocess,
215
+ context=context,
216
+ generation=generation,
214
217
  postprocess=[],
215
218
  ),
216
219
  drivers,
@@ -8,3 +8,7 @@ class NotFound(Exception):
8
8
 
9
9
  class InvalidReference(Exception):
10
10
  """Raised when a referenced resource does not exist."""
11
+
12
+
13
+ class InUse(Exception):
14
+ """Raised when a resource cannot be deleted while it is referenced."""
@@ -133,7 +133,7 @@ class AgenticRephraseConfiguration(BaseModel):
133
133
  ask_to: Optional[str] = None
134
134
  prompt: Optional[str] = None
135
135
  model: Optional[str] = None
136
- history: Optional[bool] = None
136
+ history: Optional[bool] = True
137
137
 
138
138
 
139
139
  class AgenticSmartAgentMode(str, Enum):
@@ -218,15 +218,15 @@ class AgenticSmartAgentConfiguration(BaseModel):
218
218
  extra_prompt: Optional[str] = None
219
219
  models: Optional[AgenticSmartAgentModels] = None
220
220
  sources: List[str] = Field(default_factory=list)
221
- history: Optional[bool] = None
221
+ history: Optional[bool] = True
222
222
 
223
223
 
224
224
  class AgenticSummarizeConfiguration(BaseModel):
225
225
  user_prompt: Optional[str] = None
226
226
  system_prompt: Optional[str] = None
227
- conversational: bool = False
227
+ conversational: bool = True
228
228
  model: Optional[str] = None
229
- history: Optional[bool] = None
229
+ history: Optional[bool] = True
230
230
 
231
231
 
232
232
  class AgenticConfigSchema(BaseModel):
@@ -7,7 +7,6 @@ from hyperforge.broker.redis import RedisBroker
7
7
  from hyperforge.feature_flag import get_flag_service
8
8
  from hyperforge.server.cache import ValkeyCache
9
9
  from hyperforge.server.run import run_metrics_server
10
- from hyperforge_nucliadb_agentic.ask.predict import start_predict_engine
11
10
  from nucliadb_telemetry.logs import setup_logging
12
11
  from nucliadb_telemetry.settings import LogFormatType, LogLevel, LogSettings
13
12
  from nucliadb_telemetry.utils import setup_telemetry
@@ -40,8 +39,6 @@ async def run_server(
40
39
  if tracer:
41
40
  await setup_telemetry(SERVICE_NAME)
42
41
 
43
- await start_predict_engine()
44
-
45
42
  audit_settings: AuditSettings = AuditSettings()
46
43
  # Connect to Valkey
47
44
  broker = RedisBroker.from_url(
@@ -255,6 +255,12 @@ class NucliaDBAgenticSessionManager(SessionManager):
255
255
  errors.capture_exception(e)
256
256
  error = ARAGException(detail=str(e))
257
257
  observation.set_status("error")
258
+ finally:
259
+ try:
260
+ await state.manager.aclose()
261
+ except Exception as e:
262
+ logger.exception("Error closing Manager")
263
+ errors.capture_exception(e)
258
264
 
259
265
  observation.end()
260
266
  answer_running.dec()
@@ -115,3 +115,28 @@ async def patch_agentic_config_endpoint(
115
115
  except exceptions.InvalidReference as exc:
116
116
  raise HTTPException(status_code=422, detail=str(exc)) from exc
117
117
  return Response(status_code=204)
118
+
119
+
120
+ @router.delete(
121
+ "/api/v1/kb/{kbid}/agentic_configs/{agentic_id}",
122
+ status_code=204,
123
+ summary="Delete agentic configuration",
124
+ tags=["Agentic configs"],
125
+ )
126
+ @requires(NucliaDBRoles.OWNER)
127
+ async def delete_agentic_config_endpoint(
128
+ request: Request,
129
+ kbid: str,
130
+ agentic_id: str,
131
+ x_nucliadb_account: str = Header(default="", include_in_schema=False),
132
+ ) -> Response:
133
+ app = cast("HTTPApplication", request.app)
134
+ try:
135
+ await app.agent_manager.delete_agentic_config(
136
+ account=x_nucliadb_account,
137
+ kbid=kbid,
138
+ agentic_id=agentic_id,
139
+ )
140
+ except exceptions.NotFound as exc:
141
+ raise HTTPException(status_code=404, detail=str(exc)) from exc
142
+ return Response(status_code=204)
@@ -2,6 +2,7 @@ import json
2
2
  from uuid import UUID
3
3
 
4
4
  from fastapi import Header, Request, Response
5
+ from hyperforge.manager import Manager
5
6
  from hyperforge_nucliadb_agentic.ask.exceptions import (
6
7
  AnswerJsonSchemaTooLong,
7
8
  )
@@ -108,6 +109,7 @@ async def ask_knowledgebox_endpoint(
108
109
  )
109
110
 
110
111
  return await create_ask_response(
112
+ predict_manager=request.app.predict_manager,
111
113
  kbid=kbid,
112
114
  ask_request=item,
113
115
  user_id=x_nucliadb_user,
@@ -169,6 +171,7 @@ async def resource_ask_endpoint_by_uuid(
169
171
  )
170
172
 
171
173
  return await create_ask_response(
174
+ predict_manager=request.app.predict_manager,
172
175
  kbid=kbid,
173
176
  ask_request=item,
174
177
  user_id=x_nucliadb_user,
@@ -238,6 +241,7 @@ async def resource_ask_endpoint_by_slug(
238
241
  )
239
242
 
240
243
  return await create_ask_response(
244
+ predict_manager=request.app.predict_manager,
241
245
  kbid=kbid,
242
246
  ask_request=item,
243
247
  user_id=x_nucliadb_user,
@@ -251,6 +255,7 @@ async def resource_ask_endpoint_by_slug(
251
255
 
252
256
  @handled_ask_exceptions
253
257
  async def create_ask_response(
258
+ predict_manager: Manager,
254
259
  kbid: str,
255
260
  ask_request: AskRequest,
256
261
  user_id: str,
@@ -266,6 +271,7 @@ async def create_ask_response(
266
271
  search_sdk=rpc.get_sdk("search"),
267
272
  reader_sdk=rpc.get_sdk("reader"),
268
273
  kbid=kbid,
274
+ predict_manager=predict_manager,
269
275
  ask_request=ask_request,
270
276
  user_id=user_id,
271
277
  client_type=client_type,
@@ -10,6 +10,7 @@ import anyio
10
10
  import pydantic_core
11
11
  from fastapi import Header
12
12
  from hyperforge.api.authentication import requires_one
13
+ from hyperforge.manager import Manager
13
14
  from mcp.server.fastmcp.exceptions import ResourceError
14
15
  from mcp.server.fastmcp.utilities.types import Image
15
16
  from mcp.server.lowlevel.helper_types import ReadResourceContents
@@ -34,7 +35,7 @@ from starlette.responses import Response
34
35
  from nucliadb_agentic_api import logger
35
36
 
36
37
  if TYPE_CHECKING:
37
- from hyperforge.api.app import HTTPApplication
38
+ from nucliadb_agentic_api.app import HTTPApplication
38
39
 
39
40
  from anyio.abc import TaskStatus
40
41
  from hyperforge_nucliadb_agentic.ask.exceptions import (
@@ -80,6 +81,7 @@ BATCH_GET_DOCUMENTS_MAX = 20
80
81
 
81
82
  @dataclass
82
83
  class MCPContext:
84
+ predict_manager: Manager
83
85
  ndb_reader: NucliaDBAsync
84
86
  ndb_search: NucliaDBAsync
85
87
  kbid: str
@@ -249,6 +251,7 @@ async def _tool_search_documents(
249
251
  search_sdk=context.ndb_search,
250
252
  reader_sdk=context.ndb_reader,
251
253
  kbid=context.kbid,
254
+ predict_manager=context.predict_manager,
252
255
  ask_request=ask_request,
253
256
  user_id=context.x_nucliadb_user,
254
257
  client_type=context.x_ndb_client,
@@ -469,6 +472,7 @@ async def list_tools(description: str) -> list[Tool]:
469
472
 
470
473
 
471
474
  async def call_tool(
475
+ predict_manager: Manager,
472
476
  x_stf_account: str,
473
477
  x_nucliadb_user: str,
474
478
  x_ndb_client: NucliaDBClientType,
@@ -492,6 +496,7 @@ async def call_tool(
492
496
  )
493
497
  raise ResourceError(f"Invalid arguments for '{name}': {error_details}") from e
494
498
  context = MCPContext(
499
+ predict_manager=predict_manager,
495
500
  ndb_reader=ndb_reader,
496
501
  ndb_search=ndb_search,
497
502
  kbid=kbid,
@@ -678,6 +683,7 @@ async def mcp_handler(
678
683
  list_resources_partial = partial(list_resources, ndb_reader, kbid)
679
684
  call_tool_partial = partial(
680
685
  call_tool,
686
+ app.predict_manager,
681
687
  x_stf_account,
682
688
  x_nucliadb_user,
683
689
  x_ndb_client,
@@ -141,6 +141,16 @@ async def delete_source_endpoint(
141
141
  ) -> Response:
142
142
  app = cast("HTTPApplication", request.app)
143
143
  try:
144
+ configs = await app.agent_manager.configs_referencing_source(
145
+ account=x_nucliadb_account,
146
+ kbid=kbid,
147
+ source_id=source_id,
148
+ )
149
+ if configs:
150
+ raise exceptions.InUse(
151
+ "Source is in use by agentic configuration(s): "
152
+ + ", ".join(sorted(configs))
153
+ )
144
154
  await app.source_manager.delete_source(
145
155
  account=x_nucliadb_account,
146
156
  kbid=kbid,
@@ -148,11 +158,6 @@ async def delete_source_endpoint(
148
158
  )
149
159
  except exceptions.NotFound as exc:
150
160
  raise HTTPException(status_code=404, detail=str(exc)) from exc
151
-
152
- # Cascade: remove every agentic config that references this source.
153
- await app.agent_manager.delete_configs_referencing_source(
154
- account=x_nucliadb_account,
155
- kbid=kbid,
156
- source_id=source_id,
157
- )
161
+ except exceptions.InUse as exc:
162
+ raise HTTPException(status_code=409, detail=str(exc)) from exc
158
163
  return Response(status_code=204)
@@ -1 +0,0 @@
1
- 1.0.0.post211