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.
- nucliadb_agentic_api/VERSION +1 -0
- nucliadb_agentic_api/__init__.py +20 -0
- nucliadb_agentic_api/agentic/__init__.py +0 -0
- nucliadb_agentic_api/agentic/ask_handler.py +66 -0
- nucliadb_agentic_api/agentic/ask_result.py +389 -0
- nucliadb_agentic_api/agentic/ask_transform_to_interaction.py +10 -0
- nucliadb_agentic_api/app.py +147 -0
- nucliadb_agentic_api/commands.py +66 -0
- nucliadb_agentic_api/db/__init__.py +0 -0
- nucliadb_agentic_api/db/agentic_configs.py +296 -0
- nucliadb_agentic_api/db/settings.py +11 -0
- nucliadb_agentic_api/db/sources.py +202 -0
- nucliadb_agentic_api/db/transform.py +208 -0
- nucliadb_agentic_api/exceptions.py +10 -0
- nucliadb_agentic_api/logging.py +18 -0
- nucliadb_agentic_api/models.py +321 -0
- nucliadb_agentic_api/py.typed +0 -0
- nucliadb_agentic_api/server/__init__.py +1 -0
- nucliadb_agentic_api/server/server.py +113 -0
- nucliadb_agentic_api/server/session.py +276 -0
- nucliadb_agentic_api/server/settings.py +22 -0
- nucliadb_agentic_api/settings.py +39 -0
- nucliadb_agentic_api/utils.py +284 -0
- nucliadb_agentic_api/v1/__init__.py +19 -0
- nucliadb_agentic_api/v1/agentic_configs.py +117 -0
- nucliadb_agentic_api/v1/ask.py +303 -0
- nucliadb_agentic_api/v1/ask_websocket.py +141 -0
- nucliadb_agentic_api/v1/mcp_content.py +310 -0
- nucliadb_agentic_api/v1/mcp_nucliadb.py +785 -0
- nucliadb_agentic_api/v1/oauth.py +60 -0
- nucliadb_agentic_api/v1/router.py +3 -0
- nucliadb_agentic_api/v1/sources.py +158 -0
- nucliadb_agentic_api/v1/utils.py +12 -0
- nucliadb_agentic_api-1.0.0.post156.dist-info/METADATA +150 -0
- nucliadb_agentic_api-1.0.0.post156.dist-info/RECORD +37 -0
- nucliadb_agentic_api-1.0.0.post156.dist-info/WHEEL +4 -0
- 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)]
|