nucliadb-agentic-api 1.0.0.post156__py3-none-any.whl

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 (37) hide show
  1. nucliadb_agentic_api/VERSION +1 -0
  2. nucliadb_agentic_api/__init__.py +20 -0
  3. nucliadb_agentic_api/agentic/__init__.py +0 -0
  4. nucliadb_agentic_api/agentic/ask_handler.py +66 -0
  5. nucliadb_agentic_api/agentic/ask_result.py +389 -0
  6. nucliadb_agentic_api/agentic/ask_transform_to_interaction.py +10 -0
  7. nucliadb_agentic_api/app.py +147 -0
  8. nucliadb_agentic_api/commands.py +66 -0
  9. nucliadb_agentic_api/db/__init__.py +0 -0
  10. nucliadb_agentic_api/db/agentic_configs.py +296 -0
  11. nucliadb_agentic_api/db/settings.py +11 -0
  12. nucliadb_agentic_api/db/sources.py +202 -0
  13. nucliadb_agentic_api/db/transform.py +208 -0
  14. nucliadb_agentic_api/exceptions.py +10 -0
  15. nucliadb_agentic_api/logging.py +18 -0
  16. nucliadb_agentic_api/models.py +321 -0
  17. nucliadb_agentic_api/py.typed +0 -0
  18. nucliadb_agentic_api/server/__init__.py +1 -0
  19. nucliadb_agentic_api/server/server.py +113 -0
  20. nucliadb_agentic_api/server/session.py +276 -0
  21. nucliadb_agentic_api/server/settings.py +22 -0
  22. nucliadb_agentic_api/settings.py +39 -0
  23. nucliadb_agentic_api/utils.py +284 -0
  24. nucliadb_agentic_api/v1/__init__.py +19 -0
  25. nucliadb_agentic_api/v1/agentic_configs.py +117 -0
  26. nucliadb_agentic_api/v1/ask.py +303 -0
  27. nucliadb_agentic_api/v1/ask_websocket.py +141 -0
  28. nucliadb_agentic_api/v1/mcp_content.py +310 -0
  29. nucliadb_agentic_api/v1/mcp_nucliadb.py +785 -0
  30. nucliadb_agentic_api/v1/oauth.py +60 -0
  31. nucliadb_agentic_api/v1/router.py +3 -0
  32. nucliadb_agentic_api/v1/sources.py +158 -0
  33. nucliadb_agentic_api/v1/utils.py +12 -0
  34. nucliadb_agentic_api-1.0.0.post156.dist-info/METADATA +150 -0
  35. nucliadb_agentic_api-1.0.0.post156.dist-info/RECORD +37 -0
  36. nucliadb_agentic_api-1.0.0.post156.dist-info/WHEEL +4 -0
  37. nucliadb_agentic_api-1.0.0.post156.dist-info/entry_points.txt +6 -0
@@ -0,0 +1,276 @@
1
+ import os
2
+ from asyncio import Task, create_task, timeout # type: ignore
3
+ from functools import partial
4
+
5
+ import nucliadb_telemetry.context
6
+ import nucliadb_telemetry.metrics
7
+ import prometheus_client
8
+ from hyperforge.broker import Broker
9
+ from hyperforge.configure import GLOBAL_REGISTRY, load_all_configurations, scan
10
+ from hyperforge.engine import State, get_state
11
+ from hyperforge.interaction import AnswerOperation, AragAnswer, ARAGException
12
+ from hyperforge.memory import QuestionMemory
13
+ from hyperforge.pubsub import AgentDone, StartInteraction
14
+ from hyperforge.server.cache import Cache
15
+ from hyperforge.server.session import SessionManager
16
+ from hyperforge.server.utils import get_memory
17
+ from hyperforge_nucliadb_agentic.ask.audit import (
18
+ start_audit_utility,
19
+ stop_audit_utility,
20
+ )
21
+ from hyperforge_nucliadb_agentic.ask.model import AskRequest
22
+ from lru import LRU
23
+ from nucliadb_telemetry import errors
24
+ from nucliadb_telemetry.utils import get_telemetry
25
+ from nucliadb_utils.settings import AuditSettings
26
+ from opentelemetry import trace
27
+
28
+ from nucliadb_agentic_api import logger
29
+ from nucliadb_agentic_api.db.agentic_configs import AgenticConfigs
30
+ from nucliadb_agentic_api.server import SERVICE_NAME
31
+ from nucliadb_agentic_api.server.settings import Settings as ServerSettings
32
+
33
+ HOSTNAME = os.environ.get("HOSTNAME", "nucliadb-agentic-api-server").encode()
34
+
35
+ answer_observer = nucliadb_telemetry.metrics.Observer("nucliadb_agentic_api_answer")
36
+ activation_observer = nucliadb_telemetry.metrics.Observer(
37
+ "nucliadb_agentic_api_activation"
38
+ )
39
+ answer_running = prometheus_client.Gauge(
40
+ "nucliadb_agentic_api_running_answers_count",
41
+ "Number of answering processess currently running",
42
+ )
43
+
44
+
45
+ def tracer():
46
+ provider = get_telemetry(SERVICE_NAME)
47
+ if provider:
48
+ return provider.get_tracer(__name__)
49
+ else:
50
+ return trace.NoOpTracer()
51
+
52
+
53
+ class NucliaDBAgenticSessionManager(SessionManager):
54
+ agent_manager: AgenticConfigs # type: ignore[assignment]
55
+
56
+ def __init__(
57
+ self,
58
+ settings: ServerSettings,
59
+ audit_settings: AuditSettings,
60
+ broker: Broker,
61
+ agent_manager: AgenticConfigs,
62
+ cache: Cache,
63
+ ):
64
+ self.settings = settings
65
+ self.audit_settings = audit_settings
66
+ self.agent_manager = agent_manager
67
+ self.broker = broker
68
+ self.memory: LRU = LRU(800)
69
+ self.activation_task: Task | None = None
70
+ self.tasks = []
71
+ self.cache = cache
72
+
73
+ async def initialize(self, health_check: bool = True):
74
+ await super().initialize(health_check)
75
+
76
+ await start_audit_utility(SERVICE_NAME, self.audit_settings)
77
+
78
+ for load_module in self.settings.load_modules:
79
+ try:
80
+ scan(load_module)
81
+ load_all_configurations(load_module)
82
+ except ImportError:
83
+ logger.error(f"Module {load_module} could not be loaded")
84
+
85
+ async def finalize(self):
86
+ await super().finalize()
87
+ await stop_audit_utility()
88
+ GLOBAL_REGISTRY.clear()
89
+
90
+ async def activate(self, message: StartInteraction):
91
+ topic = None
92
+
93
+ ask_request_json = message.arguments.get("ask_request")
94
+ ask_request = None
95
+ if ask_request_json:
96
+ ask_request = AskRequest.model_validate_json(ask_request_json)
97
+
98
+ logger.info("Activation message received: %s", message)
99
+ observation = activation_observer()
100
+ observation.start()
101
+ try:
102
+ nucliadb_telemetry.context.add_context(
103
+ {
104
+ "agent_id": message.agent_id,
105
+ "session_id": message.session,
106
+ "question_id": message.question_id,
107
+ }
108
+ )
109
+
110
+ topic = self.question_topic(
111
+ message.account,
112
+ message.agent_id,
113
+ message.session,
114
+ message.question_id,
115
+ message.workflow_id,
116
+ )
117
+
118
+ # Get or load session
119
+ config = await self.agent_manager.get_agent_config(
120
+ account=message.account,
121
+ kbid=message.agent_id,
122
+ internal_nucliadb_url=self.settings.internal_nucliadb_url,
123
+ internal_nucliadb=self.settings.internal_nucliadb,
124
+ external_nucliadb_url=self.settings.external_nucliadb_url,
125
+ external_nucliadb_key=self.settings.external_nucliadb_key,
126
+ workflow_id=message.workflow_id,
127
+ ask_request=ask_request,
128
+ )
129
+
130
+ state = await get_state(
131
+ agent_id=message.agent_id,
132
+ config=config,
133
+ internal_nua_api=self.settings.internal_nua_api,
134
+ internal_nua=self.settings.internal_nua,
135
+ local_openai=self.settings.local_openai,
136
+ external_nua_api_key=self.settings.external_nua_api_key,
137
+ account=message.account,
138
+ kbid=None if self.settings.standalone else message.agent_id,
139
+ )
140
+
141
+ if message.session not in self.memory:
142
+ memory = await get_memory(
143
+ settings=self.settings,
144
+ session=message.session,
145
+ cache=self.cache,
146
+ config=config.memory,
147
+ agent=message.agent_id,
148
+ workflow_id=message.workflow_id,
149
+ )
150
+ self.memory[message.session] = memory
151
+ else:
152
+ memory = self.memory[message.session]
153
+
154
+ memory.rules = config.rules.rules
155
+
156
+ question = memory.start_question(
157
+ message.question,
158
+ question_id=message.question_id,
159
+ headers=message.headers,
160
+ arguments=message.arguments,
161
+ streaming=message.streaming,
162
+ chat_history=message.chat_history,
163
+ )
164
+
165
+ task = create_task(
166
+ self.answer(
167
+ message.account,
168
+ message.agent_id,
169
+ message.workflow_id,
170
+ topic,
171
+ state,
172
+ question,
173
+ )
174
+ )
175
+ task.add_done_callback(self._remove_task)
176
+ self.tasks.append(task)
177
+
178
+ except Exception as e:
179
+ logger.exception("Activation exception")
180
+ errors.capture_exception(e)
181
+ observation.set_status("error")
182
+ if topic:
183
+ await self.callback(
184
+ topic,
185
+ AragAnswer(
186
+ exception=ARAGException(detail="Unable to start agent"),
187
+ operation=AnswerOperation.ERROR,
188
+ ),
189
+ )
190
+ await self.send_message(topic, AgentDone())
191
+
192
+ observation.end()
193
+
194
+ async def answer(
195
+ self,
196
+ account_id: str,
197
+ agent_id: str,
198
+ workflow_id: str,
199
+ topic: str,
200
+ state: State,
201
+ question_memory: QuestionMemory,
202
+ ):
203
+ error = None
204
+
205
+ keepalive = create_task(self.keep_alive(topic))
206
+ observation = answer_observer()
207
+ observation.start()
208
+ answer_running.inc()
209
+
210
+ try:
211
+ callback = partial(self.callback, topic)
212
+ question_memory.set_callback_fn(callback)
213
+
214
+ feedback = partial(self.feedback, topic)
215
+ question_memory.set_feedback_fn(feedback)
216
+
217
+ oauth = partial(self.oauth, topic)
218
+ question_memory.set_oauth_fn(oauth)
219
+
220
+ oauth_callback = partial(
221
+ self.get_oauth_callback,
222
+ account_id,
223
+ agent_id,
224
+ question_memory.session.id,
225
+ workflow_id,
226
+ )
227
+ question_memory.set_oauth_callback_fn(oauth_callback)
228
+
229
+ await self.callback(
230
+ topic,
231
+ AragAnswer(operation=AnswerOperation.START),
232
+ )
233
+
234
+ async with timeout(self.settings.question_timeout_seconds):
235
+ await state.agent(question_memory, state.manager)
236
+
237
+ except Exception as e:
238
+ logger.exception("Answering exception")
239
+ errors.capture_exception(e)
240
+ error = ARAGException(detail=str(e))
241
+ observation.set_status("error")
242
+
243
+ observation.end()
244
+ answer_running.dec()
245
+ keepalive.cancel()
246
+
247
+ await self.callback(
248
+ topic,
249
+ AragAnswer(
250
+ exception=error,
251
+ answer=question_memory.final_answer,
252
+ answer_citations=question_memory.final_answer_citations,
253
+ answer_urls=question_memory.final_answer_urls,
254
+ operation=AnswerOperation.ERROR
255
+ if error is not None
256
+ else AnswerOperation.ANSWER,
257
+ data_visualizations=question_memory.data_visualizations
258
+ if question_memory.data_visualizations
259
+ else None,
260
+ ),
261
+ )
262
+ await self.send_message(
263
+ topic,
264
+ AgentDone(),
265
+ )
266
+
267
+ try:
268
+ await question_memory.save()
269
+ self.process_event(
270
+ "memory_saved",
271
+ {"account_id": account_id, "question_memory": question_memory},
272
+ )
273
+ except Exception as e:
274
+ # Log memory errors but don't report them to the user
275
+ logger.exception("Error saving memory")
276
+ errors.capture_exception(e)
@@ -0,0 +1,22 @@
1
+ from hyperforge.server.settings import Settings as HyperforgeServerSettings
2
+
3
+ MODULES = [
4
+ "hyperforge_rephrase",
5
+ "hyperforge_nucliadb",
6
+ "hyperforge_nucliadb_agentic",
7
+ "hyperforge_summarize",
8
+ "hyperforge_smart",
9
+ "hyperforge_mcp",
10
+ "hyperforge_google",
11
+ "hyperforge_perplexity",
12
+ "hyperforge_nucliadb_agentic.internal_driver",
13
+ ]
14
+
15
+
16
+ class Settings(HyperforgeServerSettings):
17
+ load_modules: list[str] = MODULES
18
+ activate_subject: str = "ndb_agentic.activate"
19
+ answers_subject: str = (
20
+ "ndb_agentic.{account}.{agent_id}.{workflow_id}.{session}.{question}.answer"
21
+ )
22
+ oauth_subject: str = "ndb_agentic.{account}.{agent_id}.{workflow_id}.{session}.{question}.oauth.{oauth_uuid}"
@@ -0,0 +1,39 @@
1
+ from typing import Optional
2
+
3
+ from pydantic_settings import BaseSettings
4
+
5
+
6
+ class Settings(BaseSettings):
7
+ debug: bool = False
8
+ log_level: str = "ERROR"
9
+ http_host: str = "0.0.0.0"
10
+ http_port: int = 8080
11
+
12
+ sentry_url: Optional[str] = None
13
+ running_environment: str = "stage"
14
+ zone: str = "stashify"
15
+ grpc_port: int = 8030
16
+
17
+ valkey_url: str = "redis://ndb_agentic-valkey-cluster"
18
+ valkey_cluster_mode: bool = False
19
+ answers_subject: str = (
20
+ "ndb_agentic.{account}.{agent_id}.{workflow_id}.{session}.{question}.answer"
21
+ )
22
+ oauth_subject: str = "ndb_agentic.{account}.{agent_id}.{workflow_id}.{session}.{question}.oauth.{oauth_uuid}"
23
+ activate_subject: str = "ndb_agentic.activate"
24
+ pubsub_keepalive_seconds: float = 20
25
+
26
+ # Hydra settings for MCP Oauth
27
+ hydra_public_url: str = "https://oauth.progress.cloud"
28
+ hydra_scopes_supported: list[str] = ["offline_access", "openid"]
29
+
30
+ hyperforge_google_credentials: Optional[str] = None
31
+ hyperforge_google_location: Optional[str] = None
32
+ hyperforge_perplexity_key: Optional[str] = None
33
+ internal_nua_api: str = "http://predict.learning.svc.cluster.local:8080"
34
+
35
+ internal_nucliadb: bool = False
36
+ internal_nucliadb_url: Optional[str] = None
37
+
38
+ external_nucliadb_key: Optional[str] = None
39
+ external_nucliadb_url: Optional[str] = None
@@ -0,0 +1,284 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ from typing import Any, List, TypeVar
5
+
6
+ import pydantic
7
+ from typing_extensions import TypeGuard
8
+
9
+ _T = TypeVar("_T")
10
+
11
+ FLOW_PROPERTIES = [
12
+ "next_agent",
13
+ "fallback",
14
+ "then",
15
+ "else_",
16
+ "agents",
17
+ "registered_agents",
18
+ "agent",
19
+ ]
20
+
21
+
22
+ def to_strict_json_schema(
23
+ model: type[pydantic.BaseModel] | pydantic.TypeAdapter[Any],
24
+ exclude_properties: List[str] = [],
25
+ exclude_defs: List[str] = [],
26
+ ) -> dict[str, Any]:
27
+ if inspect.isclass(model) and is_basemodel_type(model):
28
+ schema = model.model_json_schema()
29
+ elif isinstance(model, pydantic.TypeAdapter):
30
+ schema = model.json_schema()
31
+ else:
32
+ raise TypeError(
33
+ f"Non BaseModel types are only supported with Pydantic v2 - {model}"
34
+ )
35
+
36
+ for exclude in exclude_properties:
37
+ if "properties" in schema and exclude in schema["properties"]:
38
+ del schema["properties"][exclude]
39
+ for exclude in exclude_defs:
40
+ if "$defs" in schema and exclude in schema["$defs"]:
41
+ del schema["$defs"][exclude]
42
+ return _ensure_strict_json_schema(schema, path=(), root=schema)
43
+
44
+
45
+ def _ensure_strict_json_schema(
46
+ json_schema: object,
47
+ *,
48
+ path: tuple[str, ...],
49
+ root: dict[str, object],
50
+ ) -> dict[str, Any]:
51
+ """Mutates the given JSON schema to ensure it conforms to the `strict` standard
52
+ that the API expects.
53
+ """
54
+ if not is_dict(json_schema):
55
+ raise TypeError(f"Expected {json_schema} to be a dictionary; path={path}")
56
+
57
+ # defs = json_schema.get("$defs")
58
+ # if is_dict(defs):
59
+ # for def_name, def_schema in defs.items():
60
+ # _ensure_strict_json_schema(
61
+ # def_schema, path=(*path, "$defs", def_name), root=root
62
+ # )
63
+
64
+ definitions = json_schema.get("definitions")
65
+ if is_dict(definitions):
66
+ for definition_name, definition_schema in definitions.items():
67
+ _ensure_strict_json_schema(
68
+ definition_schema,
69
+ path=(*path, "definitions", definition_name),
70
+ root=root,
71
+ )
72
+
73
+ typ = json_schema.get("type")
74
+ if typ == "object" and "additionalProperties" not in json_schema:
75
+ json_schema["additionalProperties"] = False
76
+
77
+ # object types
78
+ # { 'type': 'object', 'properties': { 'a': {...} } }
79
+ properties = json_schema.get("properties")
80
+ if is_dict(properties):
81
+ json_schema["required"] = [prop for prop in properties.keys()]
82
+ json_schema["properties"] = {
83
+ key: _ensure_strict_json_schema(
84
+ prop_schema, path=(*path, "properties", key), root=root
85
+ )
86
+ for key, prop_schema in properties.items()
87
+ }
88
+
89
+ # arrays
90
+ # { 'type': 'array', 'items': {...} }
91
+ items = json_schema.get("items")
92
+ if is_dict(items):
93
+ json_schema["items"] = _ensure_strict_json_schema(
94
+ items, path=(*path, "items"), root=root
95
+ )
96
+
97
+ # unions
98
+ one_of = json_schema.get("oneOf")
99
+ if isinstance(one_of, list):
100
+ json_schema["oneOf"] = [
101
+ _ensure_strict_json_schema(
102
+ variant, path=(*path, "oneOf", str(i)), root=root
103
+ )
104
+ for i, variant in enumerate(one_of)
105
+ ]
106
+
107
+ # unions
108
+ any_of = json_schema.get("anyOf")
109
+ if isinstance(any_of, list):
110
+ json_schema["anyOf"] = [
111
+ _ensure_strict_json_schema(
112
+ variant, path=(*path, "anyOf", str(i)), root=root
113
+ )
114
+ for i, variant in enumerate(any_of)
115
+ ]
116
+
117
+ # intersections
118
+ all_of = json_schema.get("allOf")
119
+ if isinstance(all_of, list):
120
+ if len(all_of) == 1:
121
+ json_schema.update(
122
+ _ensure_strict_json_schema(
123
+ all_of[0], path=(*path, "allOf", "0"), root=root
124
+ )
125
+ )
126
+ json_schema.pop("allOf")
127
+ else:
128
+ json_schema["allOf"] = [
129
+ _ensure_strict_json_schema(
130
+ entry, path=(*path, "allOf", str(i)), root=root
131
+ )
132
+ for i, entry in enumerate(all_of)
133
+ ]
134
+
135
+ # strip `None` defaults as there's no meaningful distinction here
136
+ # the schema will still be `nullable` and the model will default
137
+ # to using `None` anyway
138
+ if json_schema.get("default", "") is None:
139
+ json_schema.pop("default")
140
+
141
+ # we can't use `$ref`s if there are also other properties defined, e.g.
142
+ # `{"$ref": "...", "description": "my description"}`
143
+ #
144
+ # so we unravel the ref
145
+ # `{"type": "string", "description": "my description"}`
146
+ ref = json_schema.get("$ref")
147
+ if ref is not None:
148
+ assert isinstance(ref, str), f"Received non-string $ref - {ref}"
149
+
150
+ resolved = resolve_ref(root=root, ref=ref)
151
+ if not is_dict(resolved):
152
+ raise ValueError(
153
+ f"Expected `$ref: {ref}` to resolved to a dictionary but got {resolved}"
154
+ )
155
+
156
+ # properties from the json schema take priority over the ones on the `$ref`
157
+ json_schema.update({**resolved, **json_schema})
158
+ json_schema.pop("$ref")
159
+ # Since the schema expanded from `$ref` might not have `additionalProperties: false` applied,
160
+ # we call `_ensure_strict_json_schema` again to fix the inlined schema and ensure it's valid.
161
+ return _ensure_strict_json_schema(json_schema, path=path, root=root)
162
+
163
+ return json_schema
164
+
165
+
166
+ def resolve_ref(*, root: dict[str, object], ref: str) -> object:
167
+ if not ref.startswith("#/"):
168
+ raise ValueError(f"Unexpected $ref format {ref!r}; Does not start with #/")
169
+
170
+ path = ref[2:].split("/")
171
+ resolved = root
172
+ for key in path:
173
+ value = resolved[key]
174
+ assert is_dict(value), (
175
+ f"encountered non-dictionary entry while resolving {ref} - {resolved}"
176
+ )
177
+ resolved = value
178
+
179
+ return resolved
180
+
181
+
182
+ def is_basemodel_type(typ: type) -> TypeGuard[type[pydantic.BaseModel]]:
183
+ if not inspect.isclass(typ):
184
+ return False
185
+ return issubclass(typ, pydantic.BaseModel)
186
+
187
+
188
+ def is_dataclass_like_type(typ: type) -> bool:
189
+ """Returns True if the given type likely used `@pydantic.dataclass`"""
190
+ return hasattr(typ, "__pydantic_config__")
191
+
192
+
193
+ def is_dict(obj: object) -> TypeGuard[dict[str, object]]:
194
+ # just pretend that we know there are only `str` keys
195
+ # as that check is not worth the performance cost
196
+ return isinstance(obj, dict)
197
+
198
+
199
+ async def clean_up_items(items: dict[str, Any], filtered: list[str]) -> dict[str, Any]:
200
+ """Cleans up the items section of the schema by removing filtered agents and drivers from the references."""
201
+ if "discriminator" in items and "mapping" in items["discriminator"]:
202
+ for name, module in list(items["discriminator"]["mapping"].items()):
203
+ if filtered and module.split("/")[-1] in filtered:
204
+ del items["discriminator"]["mapping"][name]
205
+ if "oneOf" in items:
206
+ for item in list(items["oneOf"]):
207
+ if "$ref" in item:
208
+ module_name = item["$ref"].split("/")[-1]
209
+ if module_name in filtered:
210
+ items["oneOf"].remove(item)
211
+ return items
212
+
213
+
214
+ async def cleanup_anyof(anyof: list[dict[str, Any]], filtered: list[str]):
215
+ """Cleans up the anyof section of the schema by removing filtered agents and drivers from the references."""
216
+ mapping = {}
217
+ if "discriminator" in anyof[0]:
218
+ anyof_mapping = anyof[0]["discriminator"].get("mapping", {})
219
+ for name, module in anyof_mapping.items():
220
+ module_name = module.split("/")[-1]
221
+ if all(module_name not in fa for fa in filtered):
222
+ mapping[name] = module
223
+ anyof[0]["discriminator"]["mapping"] = mapping
224
+ if "oneOf" in anyof[0]:
225
+ for module in anyof[0]["oneOf"]:
226
+ if "$ref" in module:
227
+ module_name = module["$ref"].split("/")[-1]
228
+ if module_name in filtered:
229
+ anyof[0]["oneOf"].remove(module)
230
+ return anyof
231
+
232
+
233
+ async def cleanup_properties(
234
+ properties: dict[str, Any], filtered_agents: list[str], filtered_drivers: list[str]
235
+ ) -> dict[str, Any]:
236
+ """Cleans up the properties section of the schema by removing filtered agents and drivers from the references."""
237
+ filtered = filtered_agents + filtered_drivers
238
+ steps = ["preprocess", "context", "generation", "postprocess", "drivers"]
239
+ for step in steps:
240
+ if step not in properties:
241
+ continue
242
+ items = properties[step].get("items")
243
+ if items:
244
+ properties[step]["items"] = await clean_up_items(items, filtered)
245
+ return properties
246
+
247
+
248
+ async def cleanup_definitions(
249
+ definitions: dict[str, Any], filtered_agents: list[str]
250
+ ) -> dict[str, Any]:
251
+ """Cleans up the definitions section of the schema by removing filtered agents from the definitions."""
252
+ for item, item_schema in definitions.items():
253
+ if item.endswith("AgentConfig"):
254
+ if item in filtered_agents:
255
+ del definitions[item]
256
+ continue
257
+ else:
258
+ properties = item_schema.get("properties", {})
259
+ for property in FLOW_PROPERTIES:
260
+ if property in item_schema["properties"]:
261
+ if "anyOf" in properties[property]:
262
+ anyof = properties[property]["anyOf"]
263
+ if "items" in anyof[0]:
264
+ items = anyof[0]["items"]
265
+ properties[property]["anyOf"][0][
266
+ "items"
267
+ ] = await clean_up_items(items, filtered_agents)
268
+ else:
269
+ properties[property]["anyOf"] = await cleanup_anyof(
270
+ anyof, filtered_agents
271
+ )
272
+ elif "items" in properties[property]:
273
+ items = properties[property]["items"]
274
+ properties[property]["items"] = await clean_up_items(
275
+ items, filtered_agents
276
+ )
277
+ elif "discriminator" in properties[property]:
278
+ properties[property] = await clean_up_items(
279
+ properties[property], filtered_agents
280
+ )
281
+ item_schema["properties"] = properties
282
+ definitions[item] = item_schema
283
+
284
+ return definitions
@@ -0,0 +1,19 @@
1
+ from nucliadb_agentic_api.v1 import (
2
+ agentic_configs,
3
+ ask,
4
+ ask_websocket,
5
+ mcp_nucliadb,
6
+ oauth,
7
+ sources,
8
+ )
9
+ from nucliadb_agentic_api.v1.router import router
10
+
11
+ __all__ = [
12
+ "agentic_configs",
13
+ "ask_websocket",
14
+ "ask",
15
+ "mcp_nucliadb",
16
+ "oauth",
17
+ "sources",
18
+ "router",
19
+ ]