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,310 @@
1
+ """MCP content conversion utilities.
2
+
3
+ Converts AragAnswer and generic Python objects into MCP content blocks
4
+ (TextContent, ImageContent, EmbeddedResource) suitable for returning from
5
+ tool calls.
6
+ """
7
+
8
+ import json
9
+ from itertools import chain
10
+ from typing import Any, Sequence
11
+
12
+ import pydantic_core
13
+ from hyperforge.interaction import AragAnswer
14
+ from hyperforge.models import Answer, Chunk
15
+ from mcp.server.fastmcp.utilities.types import Image
16
+ from mcp.types import (
17
+ Annotations,
18
+ EmbeddedResource,
19
+ ImageContent,
20
+ TextContent,
21
+ TextResourceContents,
22
+ )
23
+
24
+
25
+ def _answer_obj_to_content(
26
+ answer: Answer,
27
+ ) -> list[TextContent | ImageContent | EmbeddedResource]:
28
+ """Convert a hyperforge Answer object to MCP content blocks.
29
+
30
+ Handles all fields that Answer can carry: text, citations, chunks,
31
+ structured items, images, image_urls, and visualizations.
32
+ """
33
+ contents: list[TextContent | ImageContent | EmbeddedResource] = []
34
+
35
+ if answer.answer:
36
+ contents.append(TextContent(type="text", text=answer.answer))
37
+
38
+ if answer.citations and answer.citations.metadata:
39
+ citations_json = json.dumps(pydantic_core.to_jsonable_python(answer.citations))
40
+ contents.append(
41
+ EmbeddedResource(
42
+ type="resource",
43
+ resource=TextResourceContents(
44
+ uri="rao-response://answer/citations", # type: ignore
45
+ text=citations_json,
46
+ mimeType="application/json",
47
+ ),
48
+ )
49
+ )
50
+
51
+ if answer.chunks:
52
+ for idx, chunk in enumerate(answer.chunks):
53
+ if chunk:
54
+ # Extract text from Chunk object or use string directly
55
+ chunk_text = chunk.text if isinstance(chunk, Chunk) else chunk
56
+ if chunk_text:
57
+ contents.append(
58
+ EmbeddedResource(
59
+ type="resource",
60
+ resource=TextResourceContents(
61
+ uri=f"rao-response://answer/chunk/{idx}", # type: ignore
62
+ text=chunk_text,
63
+ mimeType="text/plain",
64
+ ),
65
+ )
66
+ )
67
+
68
+ if answer.structured:
69
+ for idx, item in enumerate(answer.structured):
70
+ if item:
71
+ contents.append(
72
+ EmbeddedResource(
73
+ type="resource",
74
+ resource=TextResourceContents(
75
+ uri=f"rao-response://answer/structured/{idx}", # type: ignore
76
+ text=item,
77
+ mimeType="text/plain",
78
+ ),
79
+ )
80
+ )
81
+
82
+ if answer.images:
83
+ for image in answer.images.values():
84
+ contents.append(
85
+ ImageContent(
86
+ type="image",
87
+ data=image.b64encoded,
88
+ mimeType=image.content_type,
89
+ )
90
+ )
91
+
92
+ if answer.image_urls:
93
+ contents.append(
94
+ EmbeddedResource(
95
+ type="resource",
96
+ resource=TextResourceContents(
97
+ uri="rao-response://answer/image-urls", # type: ignore
98
+ text=json.dumps(answer.image_urls),
99
+ mimeType="application/json",
100
+ ),
101
+ )
102
+ )
103
+
104
+ if answer.data_visualizations:
105
+ for idx, viz in enumerate(answer.data_visualizations):
106
+ contents.append(
107
+ EmbeddedResource(
108
+ type="resource",
109
+ resource=TextResourceContents(
110
+ uri=f"rao-response://answer/visualization/{idx}", # type: ignore
111
+ text=json.dumps(
112
+ pydantic_core.to_jsonable_python(viz.vega_lite_obj)
113
+ ),
114
+ mimeType="application/vnd.vegalite.v5+json",
115
+ ),
116
+ )
117
+ )
118
+
119
+ return contents
120
+
121
+
122
+ def convert_arag_answer_to_content(
123
+ msg: AragAnswer,
124
+ ) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
125
+ """Convert an AragAnswer to a sequence of MCP content objects.
126
+
127
+ Parses **every** populated field independently — the same AragAnswer
128
+ message never carries all fields at once, but any combination is safe.
129
+
130
+ Emits (in order):
131
+ - possible_answer content (text, citations, chunks, structured, images,
132
+ image_urls, visualizations) via _answer_obj_to_content
133
+ - TextContent for the main answer text
134
+ - EmbeddedResource (application/json) for top-level citations
135
+ - EmbeddedResource (application/json) for answer URLs
136
+ - EmbeddedResource (text/plain) per context chunk, with metadata
137
+ - ImageContent per base64 image in the context
138
+ - EmbeddedResource (text/plain) per structured item in the context
139
+ - EmbeddedResource (application/json) for context image URLs
140
+ - EmbeddedResource (application/vnd.vegalite.v5+json) per visualization
141
+ - TextContent (assistant-only annotation) for the step, if present
142
+ - TextContent for exception detail, if present
143
+ """
144
+ contents: list[TextContent | ImageContent | EmbeddedResource] = []
145
+
146
+ # --- Possible answer (emitted by add_answer callbacks during generation) ---
147
+ if msg.possible_answer:
148
+ contents.extend(_answer_obj_to_content(msg.possible_answer))
149
+
150
+ # --- Main answer text (emitted by send_final_answer / session.py) ---
151
+ if msg.answer:
152
+ contents.append(TextContent(type="text", text=msg.answer))
153
+
154
+ # --- Top-level citations (from send_final_answer / session.py) ---
155
+ if msg.answer_citations and msg.answer_citations.metadata:
156
+ citations_json = json.dumps(
157
+ pydantic_core.to_jsonable_python(msg.answer_citations)
158
+ )
159
+ contents.append(
160
+ EmbeddedResource(
161
+ type="resource",
162
+ resource=TextResourceContents(
163
+ uri="rao-response://citations", # type: ignore
164
+ text=citations_json,
165
+ mimeType="application/json",
166
+ ),
167
+ )
168
+ )
169
+
170
+ # --- Answer URLs ---
171
+ if msg.answer_urls:
172
+ contents.append(
173
+ EmbeddedResource(
174
+ type="resource",
175
+ resource=TextResourceContents(
176
+ uri="rao-response://answer-urls", # type: ignore
177
+ text=json.dumps(msg.answer_urls),
178
+ mimeType="application/json",
179
+ ),
180
+ )
181
+ )
182
+
183
+ # --- Context chunks and images (emitted by save_context callbacks) ---
184
+ if msg.context:
185
+ context = msg.context
186
+ for chunk in context.chunks:
187
+ meta: dict[str, Any] = {}
188
+ if chunk.title:
189
+ meta["title"] = chunk.title
190
+ if chunk.source:
191
+ meta["source"] = chunk.source
192
+ if chunk.labels:
193
+ meta["labels"] = chunk.labels
194
+ if chunk.origin_url:
195
+ meta["origin_url"] = chunk.origin_url
196
+ if chunk.url:
197
+ meta["url"] = chunk.url
198
+ contents.append(
199
+ EmbeddedResource(
200
+ type="resource",
201
+ resource=TextResourceContents(
202
+ uri=f"rao-response://context/{context.id}/chunk/{chunk.chunk_id}", # type: ignore
203
+ text=chunk.text,
204
+ mimeType="text/plain",
205
+ **{"_meta": meta} if meta else {},
206
+ ),
207
+ )
208
+ )
209
+
210
+ for image in context.images.values():
211
+ contents.append(
212
+ ImageContent(
213
+ type="image",
214
+ data=image.b64encoded,
215
+ mimeType=image.content_type,
216
+ )
217
+ )
218
+
219
+ for idx, structured in enumerate(context.structured):
220
+ contents.append(
221
+ EmbeddedResource(
222
+ type="resource",
223
+ resource=TextResourceContents(
224
+ uri=f"rao-response://context/{context.id}/structured/{idx}", # type: ignore
225
+ text=structured,
226
+ mimeType="text/plain",
227
+ ),
228
+ )
229
+ )
230
+
231
+ if context.image_urls:
232
+ contents.append(
233
+ EmbeddedResource(
234
+ type="resource",
235
+ resource=TextResourceContents(
236
+ uri=f"rao-response://context/{context.id}/image-urls", # type: ignore
237
+ text=json.dumps(context.image_urls),
238
+ mimeType="application/json",
239
+ ),
240
+ )
241
+ )
242
+
243
+ # --- Top-level visualizations (from session.py final answer) ---
244
+ if msg.data_visualizations:
245
+ for idx, viz in enumerate(msg.data_visualizations):
246
+ contents.append(
247
+ EmbeddedResource(
248
+ type="resource",
249
+ resource=TextResourceContents(
250
+ uri=f"rao-response://visualization/{idx}", # type: ignore
251
+ text=json.dumps(
252
+ pydantic_core.to_jsonable_python(viz.vega_lite_obj)
253
+ ),
254
+ mimeType="application/vnd.vegalite.v5+json",
255
+ ),
256
+ )
257
+ )
258
+
259
+ # --- Step (assistant-only, not shown to user) ---
260
+ if msg.step:
261
+ step = msg.step
262
+ parts = [f"Step: {step.title}"]
263
+ if step.value:
264
+ parts.append(f"Value: {step.value}")
265
+ if step.reason:
266
+ parts.append(f"Reason: {step.reason}")
267
+ if step.error:
268
+ parts.append(f"Error: {step.error}")
269
+ contents.append(
270
+ TextContent(
271
+ type="text",
272
+ text="\n".join(parts),
273
+ annotations=Annotations(audience=["assistant"]),
274
+ )
275
+ )
276
+
277
+ # --- Exception ---
278
+ if msg.exception:
279
+ contents.append(TextContent(type="text", text=f"Error: {msg.exception.detail}"))
280
+
281
+ return contents
282
+
283
+
284
+ def convert_to_content(
285
+ result: Any,
286
+ ) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
287
+ """Convert a generic result to a sequence of MCP content objects.
288
+
289
+ Handles MCP content types directly, Image objects, lists/tuples
290
+ (recursively), and serialises everything else as JSON text.
291
+ """
292
+ if result is None:
293
+ return []
294
+
295
+ if isinstance(result, (TextContent, ImageContent, EmbeddedResource)):
296
+ return [result]
297
+
298
+ if isinstance(result, Image):
299
+ return [result.to_image_content()]
300
+
301
+ if isinstance(result, (list, tuple)):
302
+ return list(chain.from_iterable(convert_to_content(item) for item in result))
303
+
304
+ if not isinstance(result, str):
305
+ try:
306
+ result = json.dumps(pydantic_core.to_jsonable_python(result))
307
+ except Exception:
308
+ result = str(result)
309
+
310
+ return [TextContent(type="text", text=result)]