nucliadb-agentic-api 1.0.0.post296__tar.gz → 1.0.0.post303__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 (49) hide show
  1. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/PKG-INFO +1 -1
  2. nucliadb_agentic_api-1.0.0.post303/VERSION +1 -0
  3. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/pyproject.toml +1 -1
  4. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/pyproject.toml.orig +1 -1
  5. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/src/nucliadb_agentic_api/v1/mcp_nucliadb.py +151 -26
  6. nucliadb_agentic_api-1.0.0.post296/VERSION +0 -1
  7. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/README.md +0 -0
  8. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/alembic/README +0 -0
  9. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/alembic/env.py +0 -0
  10. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/alembic/script.py.mako +0 -0
  11. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/alembic/versions/0fe6c7b5f33d_migrate_sonnet_and_gpt_5_6_models.py +0 -0
  12. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/alembic/versions/7f3b2f92a8c1_add_agentic_config_table.py +0 -0
  13. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/alembic/versions/b3781ec6a8ad_migration_of_aws_claude_4_1_opus.py +0 -0
  14. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/alembic/versions/b3c4d5e6f7a8_add_sources_table.py +0 -0
  15. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/alembic/versions/c4d5e6f7a8b9_add_agentic_config_soft_deletion.py +0 -0
  16. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/alembic.ini +0 -0
  17. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/src/nucliadb_agentic_api/VERSION +0 -0
  18. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/src/nucliadb_agentic_api/__init__.py +0 -0
  19. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/src/nucliadb_agentic_api/agentic/__init__.py +0 -0
  20. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/src/nucliadb_agentic_api/agentic/ask_handler.py +0 -0
  21. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/src/nucliadb_agentic_api/agentic/ask_result.py +0 -0
  22. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/src/nucliadb_agentic_api/agentic/ask_transform_to_interaction.py +0 -0
  23. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/src/nucliadb_agentic_api/app.py +0 -0
  24. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/src/nucliadb_agentic_api/commands.py +0 -0
  25. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/src/nucliadb_agentic_api/db/__init__.py +0 -0
  26. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/src/nucliadb_agentic_api/db/agentic_configs.py +0 -0
  27. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/src/nucliadb_agentic_api/db/llm_migration_utils.py +0 -0
  28. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/src/nucliadb_agentic_api/db/settings.py +0 -0
  29. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/src/nucliadb_agentic_api/db/sources.py +0 -0
  30. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/src/nucliadb_agentic_api/db/transform.py +0 -0
  31. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/src/nucliadb_agentic_api/exceptions.py +0 -0
  32. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/src/nucliadb_agentic_api/logging.py +0 -0
  33. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/src/nucliadb_agentic_api/models.py +0 -0
  34. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/src/nucliadb_agentic_api/py.typed +0 -0
  35. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/src/nucliadb_agentic_api/server/__init__.py +0 -0
  36. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/src/nucliadb_agentic_api/server/server.py +0 -0
  37. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/src/nucliadb_agentic_api/server/session.py +0 -0
  38. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/src/nucliadb_agentic_api/server/settings.py +0 -0
  39. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/src/nucliadb_agentic_api/settings.py +0 -0
  40. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/src/nucliadb_agentic_api/utils.py +0 -0
  41. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/src/nucliadb_agentic_api/v1/__init__.py +0 -0
  42. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/src/nucliadb_agentic_api/v1/agentic_configs.py +0 -0
  43. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/src/nucliadb_agentic_api/v1/ask.py +0 -0
  44. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/src/nucliadb_agentic_api/v1/ask_websocket.py +0 -0
  45. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/src/nucliadb_agentic_api/v1/mcp_content.py +0 -0
  46. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/src/nucliadb_agentic_api/v1/oauth.py +0 -0
  47. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/src/nucliadb_agentic_api/v1/router.py +0 -0
  48. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/src/nucliadb_agentic_api/v1/sources.py +0 -0
  49. {nucliadb_agentic_api-1.0.0.post296 → nucliadb_agentic_api-1.0.0.post303}/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.post296
3
+ Version: 1.0.0.post303
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.post303
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "nucliadb_agentic_api"
7
- version = "1.0.0.post296"
7
+ version = "1.0.0.post303"
8
8
  description = "NucliaDB Agentic API for Hyperforge"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "nucliadb_agentic_api"
7
- version = "1.0.0.post296"
7
+ version = "1.0.0.post303"
8
8
  authors = [{ name = "AI Data Team", email = "learning@nuclia.com" }]
9
9
  description = "NucliaDB Agentic API for Hyperforge"
10
10
  readme = "README.md"
@@ -45,6 +45,7 @@ from hyperforge_nucliadb_agentic.ask.exceptions import (
45
45
  )
46
46
  from hyperforge_nucliadb_agentic.ask.model import (
47
47
  AskRequest,
48
+ ChatOptions,
48
49
  CitationsType,
49
50
  FieldExtensionStrategy,
50
51
  Filter,
@@ -52,6 +53,7 @@ from hyperforge_nucliadb_agentic.ask.model import (
52
53
  MetadataExtensionType,
53
54
  NeighbouringParagraphsStrategy,
54
55
  )
56
+ from hyperforge_nucliadb_agentic.ask.search import rpc
55
57
  from hyperforge_nucliadb_agentic.ask.search.ask import (
56
58
  AskResult,
57
59
  ask,
@@ -100,11 +102,11 @@ class SearchDocumentsArgs(BaseModel):
100
102
  )
101
103
  search_configuration: str | None = Field(
102
104
  None,
103
- description="Optional. The name of the Search Configuration to use. If not provided, default settings are used.",
105
+ description="Optional. The name of a saved Search Configuration (ask or find) whose settings (filters, retrieval and RAG strategies, prompts, model...) are applied to the search. If not provided, default settings are used.",
104
106
  )
105
107
  filters: list[Filter] | None = Field(
106
108
  None,
107
- description="Optional. A list of filter objects to narrow results. Each filter can include 'all' (AND), 'any' (OR), and/or 'not_all' (NOT AND) label paths like '/l/labelset/label'.",
109
+ description="Optional. A list of filter objects to narrow results. Each filter can include 'all' (AND), 'any' (OR), and/or 'not_all' (NOT AND) label paths like '/l/labelset/label'. When a Search Configuration is used, these filters are combined (AND) with its saved filters.",
108
110
  )
109
111
 
110
112
 
@@ -217,35 +219,155 @@ def _append_document_ids_to_answer(
217
219
  return f"{answer_text}\n\nDocument IDs:\n{document_ids_text}"
218
220
 
219
221
 
220
- async def _tool_search_documents(
221
- context: MCPContext,
222
- args: SearchDocumentsArgs,
223
- ) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
224
- ask_request = AskRequest(query=args.query)
225
- ask_request.audit_metadata = {"source": "mcp_tool"}
226
-
227
- ask_request.rag_strategies = [
228
- FieldExtensionStrategy(fields=["a/title", "a/summary"]),
229
- NeighbouringParagraphsStrategy(before=5, after=5),
230
- MetadataExtensionStrategy(
231
- types=[
232
- MetadataExtensionType.CLASSIFICATION_LABELS,
233
- MetadataExtensionType.ORIGIN,
234
- ]
235
- ),
222
+ def _mcp_default_ask_parameters() -> dict[str, Any]:
223
+ """Parameters used by the MCP tool when the search configuration does not set them."""
224
+ return {
225
+ "rag_strategies": [
226
+ FieldExtensionStrategy(fields=["a/title", "a/summary"]),
227
+ NeighbouringParagraphsStrategy(before=5, after=5),
228
+ MetadataExtensionStrategy(
229
+ types=[
230
+ MetadataExtensionType.CLASSIFICATION_LABELS,
231
+ MetadataExtensionType.ORIGIN,
232
+ ]
233
+ ),
234
+ ],
235
+ "debug": True,
236
+ }
237
+
238
+
239
+ # Parameters the MCP tool response format depends on (answer text + footnote
240
+ # citations mapped to documents), so they always win over the search configuration.
241
+ _MCP_FORCED_ASK_PARAMETERS: dict[str, Any] = {
242
+ "generate_answer": True,
243
+ "citations": CitationsType.LLM_FOOTNOTES,
244
+ }
245
+
246
+
247
+ async def _load_search_configuration(context: MCPContext, name: str) -> dict[str, Any]:
248
+ """Fetch a saved Search Configuration and return the ask parameters it sets.
249
+
250
+ Both `ask` and `find` configurations are supported: `search_documents` runs an
251
+ ask, so `find` configurations contribute their retrieval parameters (filters,
252
+ top_k, features, reranker, ...) and parameters with no ask equivalent are ignored.
253
+ """
254
+ try:
255
+ search_config = await rpc.get_search_configuration(
256
+ context.ndb_reader, context.kbid, name=name
257
+ )
258
+ except Exception as e:
259
+ logger.exception(f"Error fetching search configuration '{name}'")
260
+ raise ResourceError(
261
+ f"Failed to load search configuration '{name}'"
262
+ + (f": {e}" if str(e) else "")
263
+ ) from e
264
+ if search_config is None:
265
+ raise ResourceError(f"Search configuration not found: '{name}'")
266
+
267
+ # AskConfig/FindConfig are built with `create_model`, so mypy can't see them as models
268
+ config_model: BaseModel = search_config.config
269
+ params = config_model.model_dump(mode="json", exclude_unset=True)
270
+ params.pop("query", None) # the tool query always wins
271
+ ignored = sorted(set(params) - set(AskRequest.model_fields))
272
+ if "features" in params:
273
+ # Find supports features ask does not (e.g. `graph`)
274
+ ask_features = {o.value for o in ChatOptions}
275
+ features = [f for f in params["features"] if f in ask_features]
276
+ if len(features) != len(params["features"]):
277
+ ignored.append(f"features={sorted(set(params['features']) - ask_features)}")
278
+ if features:
279
+ params["features"] = features
280
+ else:
281
+ params.pop("features") # nothing left, use the defaults
282
+ if ignored:
283
+ logger.info(
284
+ f"Search configuration '{name}' ({search_config.kind}) parameters not "
285
+ f"applicable to search_documents are ignored: {ignored}"
286
+ )
287
+ return {k: v for k, v in params.items() if k in AskRequest.model_fields}
288
+
289
+
290
+ def _combine_filters(
291
+ config_params: dict[str, Any], tool_filters: list[Filter], name: str
292
+ ) -> list[dict[str, Any]]:
293
+ """AND the tool filters with the search configuration filters.
294
+
295
+ The tool filters can only narrow results further: they never replace the
296
+ filters saved in the search configuration.
297
+ """
298
+ if config_params.get("filter_expression") is not None:
299
+ raise ResourceError(
300
+ f"Search configuration '{name}' defines a filter expression, which "
301
+ "cannot be combined with the 'filters' argument. Retry without 'filters'."
302
+ )
303
+ combined: list[dict[str, Any]] = [
304
+ {"all": [f]} if isinstance(f, str) else f
305
+ for f in config_params.get("filters") or []
236
306
  ]
307
+ combined.extend(f.model_dump(mode="json", exclude_unset=True) for f in tool_filters)
308
+ return combined
237
309
 
238
- if args.filters is not None:
239
- ask_request.filters = args.filters
240
310
 
311
+ async def _build_ask_request(
312
+ context: MCPContext, args: SearchDocumentsArgs
313
+ ) -> AskRequest:
314
+ """Build the ask request for `search_documents`.
315
+
316
+ Precedence: tool query/filters and MCP output requirements > saved search
317
+ configuration > MCP defaults.
318
+ """
319
+ params: dict[str, Any] = {}
241
320
  if args.search_configuration is not None:
242
- ask_request.search_configuration = args.search_configuration
321
+ params = await _load_search_configuration(context, args.search_configuration)
322
+ if params.get("answer_json_schema") is not None:
323
+ # The MCP response only renders text answers with footnote citations
324
+ raise ResourceError(
325
+ f"Search configuration '{args.search_configuration}' uses a structured "
326
+ "answer (answer_json_schema), which search_documents does not support"
327
+ )
328
+ params["search_configuration"] = args.search_configuration
329
+
330
+ for key, value in _mcp_default_ask_parameters().items():
331
+ params.setdefault(key, value)
332
+
333
+ if args.filters is not None:
334
+ if args.search_configuration is not None:
335
+ params["filters"] = _combine_filters(
336
+ params, args.filters, args.search_configuration
337
+ )
338
+ else:
339
+ params["filters"] = args.filters
340
+
341
+ params.update(_MCP_FORCED_ASK_PARAMETERS)
342
+ params["audit_metadata"] = {
343
+ **(params.get("audit_metadata") or {}),
344
+ "source": "mcp_tool",
345
+ }
346
+ params["query"] = args.query
347
+
348
+ try:
349
+ return AskRequest.model_validate(params)
350
+ except ValidationError as e:
351
+ error_details = "; ".join(
352
+ f"'{'.'.join(str(p) for p in err['loc'])}': {err['msg']}"
353
+ for err in e.errors()
354
+ )
355
+ raise ResourceError(
356
+ "Invalid search parameters"
357
+ + (
358
+ f" from search configuration '{args.search_configuration}'"
359
+ if args.search_configuration
360
+ else ""
361
+ )
362
+ + f": {error_details}"
363
+ ) from e
243
364
 
244
- # citations does not work with rag strategies yet
245
365
 
246
- ask_request.generate_answer = True
247
- ask_request.citations = CitationsType.LLM_FOOTNOTES
248
- ask_request.debug = True
366
+ async def _tool_search_documents(
367
+ context: MCPContext,
368
+ args: SearchDocumentsArgs,
369
+ ) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
370
+ ask_request = await _build_ask_request(context, args)
249
371
  try:
250
372
  ask_result: AskResult = await ask(
251
373
  search_sdk=context.ndb_search,
@@ -640,10 +762,13 @@ async def mcp_protected_resource_metadata(
640
762
  mcp_url_https = str(mcp_url).replace(
641
763
  "http://", "https://"
642
764
  ) # Ensure the URL uses https
765
+ # Must match the `issuer` advertised by the authorization server exactly
766
+ # (RFC 8414 §3.3). Hydra always advertises its issuer with a trailing slash.
767
+ authorization_server = app.settings.hydra_public_url.rstrip("/") + "/"
643
768
  return {
644
769
  "resource": mcp_url_https,
645
770
  "scopes_supported": app.settings.hydra_scopes_supported,
646
- "authorization_servers": [app.settings.hydra_public_url],
771
+ "authorization_servers": [authorization_server],
647
772
  }
648
773
 
649
774
 
@@ -1 +0,0 @@
1
- 1.0.0.post296