hyperforge-nucliadb 1.0.0.post22__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.
@@ -0,0 +1,485 @@
1
+ import uuid
2
+ from dataclasses import dataclass, field
3
+ from enum import Enum
4
+ from typing import Dict, List, Tuple
5
+
6
+ from hyperforge.models import Chunk, Context, FieldTypes
7
+ from nuclia.lib.nua_responses import Image
8
+ from nucliadb_models import PagePositions
9
+ from nucliadb_models.common import FieldTypeName, Paragraph
10
+ from nucliadb_models.graph.responses import (
11
+ GraphNodesSearchResponse,
12
+ GraphRelationsSearchResponse,
13
+ GraphSearchResponse,
14
+ )
15
+ from nucliadb_models.hydration import (
16
+ Hydrated,
17
+ HydrateRequest,
18
+ Hydration,
19
+ ImageParagraphHydration,
20
+ ParagraphHydration,
21
+ ParagraphPageHydration,
22
+ TableParagraphHydration,
23
+ )
24
+ from nucliadb_models.resource import FileFieldExtractedData, LinkFieldExtractedData
25
+ from nucliadb_models.search import KnowledgeboxFindResults
26
+
27
+ from hyperforge import logger
28
+ from hyperforge_nucliadb.ask.models import NDBChunk, SearchResults
29
+ from hyperforge_nucliadb.driver import NucliaDBDriver
30
+
31
+
32
+ class Relation(str, Enum):
33
+ NEXT = "next"
34
+ PREV = "prev"
35
+ PARENT = "parent"
36
+ REPLACEMENT = "replacement"
37
+ SIBLING = "sibling"
38
+
39
+
40
+ @dataclass
41
+ class HydrateData:
42
+ chunks: Dict[str, NDBChunk] = field(default_factory=dict)
43
+ paragraphs: Dict[str, Paragraph] = field(default_factory=dict)
44
+ links: Dict[Tuple[str, str], str] = field(default_factory=dict)
45
+ pages: Dict[int, PagePositions] = field(default_factory=dict)
46
+ images: List[str] = field(default_factory=list)
47
+ pages_image: List[str] = field(default_factory=list)
48
+
49
+
50
+ def generate_chunk(retrieval: KnowledgeboxFindResults, data: HydrateData, kbid: str):
51
+ if retrieval.resources is None:
52
+ return
53
+ for (
54
+ resource_id,
55
+ resource_obj,
56
+ ) in retrieval.resources.items():
57
+ resource_labels: List[str] = []
58
+ if resource_obj.usermetadata:
59
+ resource_labels = [
60
+ f"l/{label.labelset}/{label.label}"
61
+ for label in resource_obj.usermetadata.classifications
62
+ ]
63
+
64
+ for field_id, field_obj in resource_obj.fields.items():
65
+ chunk_data = field_id.split("/") # /t/page
66
+ field_type = chunk_data[1]
67
+ field_name = chunk_data[2]
68
+
69
+ # split = field_obj.split # TODO
70
+ if (
71
+ field_type == FieldTypeName.CONVERSATION.abbreviation()
72
+ and resource_obj.data
73
+ and resource_obj.data.conversations
74
+ ):
75
+ field: FieldTypes = resource_obj.data.conversations[field_name]
76
+ elif (
77
+ field_type == FieldTypeName.FILE.abbreviation()
78
+ and resource_obj.data
79
+ and resource_obj.data.files
80
+ ):
81
+ field = resource_obj.data.files[field_name]
82
+ elif (
83
+ field_type == FieldTypeName.TEXT.abbreviation()
84
+ and resource_obj.data
85
+ and resource_obj.data.texts
86
+ ):
87
+ field = resource_obj.data.texts[field_name]
88
+ elif (
89
+ field_type == FieldTypeName.GENERIC.abbreviation()
90
+ and resource_obj.data
91
+ and resource_obj.data.generics
92
+ ):
93
+ field = resource_obj.data.generics[field_name]
94
+ elif (
95
+ field_type == FieldTypeName.LINK.abbreviation()
96
+ and resource_obj.data
97
+ and resource_obj.data.links
98
+ ):
99
+ field = resource_obj.data.links[field_name]
100
+ else:
101
+ raise Exception(
102
+ f"Unknown field type {field_type} for field {field_name} in resource {resource_id}"
103
+ )
104
+
105
+ if (
106
+ isinstance(field.extracted, FileFieldExtractedData)
107
+ and field.extracted.file is not None
108
+ and field.extracted.file.file_pages_previews is not None
109
+ and field.extracted.file.file_pages_previews.positions is not None
110
+ ):
111
+ data.pages = {}
112
+ for page_num, page_obj in enumerate(
113
+ field.extracted.file.file_pages_previews.positions
114
+ ):
115
+ data.pages_image.append(
116
+ f"/v1/kb/{kbid}/resource/{resource_id}/{field_type}/{field_name}/download/extracted/extracted_images_{page_num}.png"
117
+ )
118
+ data.pages[page_num] = page_obj
119
+
120
+ if (
121
+ isinstance(field.extracted, LinkFieldExtractedData)
122
+ and field.extracted.link is not None
123
+ and field.extracted.link.link_image is not None
124
+ ):
125
+ data.images.append(
126
+ f"/v1/kb/{kbid}/resource/{resource_id}/{field_type}/{field_name}/download/extracted/link_thumbnail"
127
+ )
128
+
129
+ if field.extracted is not None and field.extracted.metadata is not None:
130
+ previous_paragraph = None
131
+ for paragraph in field.extracted.metadata.metadata.paragraphs:
132
+ actual_paragraph = (
133
+ f"{resource_id}/{field_id}/{paragraph.start}-{paragraph.end}"
134
+ )
135
+ if previous_paragraph is not None:
136
+ data.links[(previous_paragraph, Relation.NEXT)] = (
137
+ actual_paragraph
138
+ )
139
+ data.links[(actual_paragraph, Relation.PREV)] = (
140
+ previous_paragraph
141
+ )
142
+
143
+ if paragraph.relations is not None:
144
+ for parent in paragraph.relations.parents:
145
+ data.links[(actual_paragraph, Relation.PARENT)] = parent
146
+ for replacement in paragraph.relations.replacements:
147
+ data.links[(actual_paragraph, Relation.REPLACEMENT)] = (
148
+ replacement
149
+ )
150
+ for sibling in paragraph.relations.siblings:
151
+ data.links[(actual_paragraph, Relation.SIBLING)] = sibling
152
+
153
+ data.paragraphs[actual_paragraph] = paragraph
154
+ previous_paragraph = actual_paragraph
155
+
156
+ for chunk_id, chunk_obj in field_obj.paragraphs.items():
157
+ data.chunks[chunk_id] = NDBChunk(
158
+ chunk_id=chunk_id,
159
+ text=chunk_obj.text,
160
+ page_with_visual=chunk_obj.page_with_visual,
161
+ reference=chunk_obj.reference,
162
+ start=(
163
+ chunk_obj.position.start
164
+ if chunk_obj.position is not None
165
+ else None
166
+ ),
167
+ end=(
168
+ chunk_obj.position.end
169
+ if chunk_obj.position is not None
170
+ else None
171
+ ),
172
+ page=(
173
+ chunk_obj.position.page_number
174
+ if chunk_obj.position is not None
175
+ else None
176
+ ),
177
+ labels=chunk_obj.labels if chunk_obj.labels is not None else [],
178
+ resource_labels=resource_labels,
179
+ field=field,
180
+ link=resource_obj.origin.url or resource_obj.origin.path
181
+ if resource_obj.origin
182
+ else None,
183
+ )
184
+
185
+
186
+ def generate_graph_relations_chunk(
187
+ retrieval: GraphRelationsSearchResponse, data: HydrateData
188
+ ):
189
+ chunk_id = f"relations-{uuid.uuid4().hex}"
190
+ relation_chunk = "Existing relations that can be used to query:\n"
191
+ for relation in retrieval.relations:
192
+ relation_chunk += f"- {relation.type.value}: {relation.label} \n"
193
+
194
+ data.chunks[chunk_id] = NDBChunk(
195
+ chunk_id=chunk_id,
196
+ text=relation_chunk,
197
+ page_with_visual=False,
198
+ reference=None,
199
+ start=None,
200
+ end=None,
201
+ page=None,
202
+ labels=[],
203
+ resource_labels=[],
204
+ field=None,
205
+ link=None,
206
+ )
207
+
208
+
209
+ def generate_graph_search_chunk(retrieval: GraphSearchResponse, data: HydrateData):
210
+ chunk_id = f"paths-{uuid.uuid4().hex}"
211
+ relation_chunk = "Existing nodes and relations:\n"
212
+ for path in retrieval.paths:
213
+ relation_chunk += f"- Source node: {path.source.value} {path.source.type.value} {path.source.group}\n"
214
+ relation_chunk += (
215
+ f" Relation: {path.relation.label} {path.relation.type.value}\n"
216
+ )
217
+ relation_chunk += f" Destination node: {path.destination.value} {path.destination.type.value} {path.destination.group}\n"
218
+
219
+ data.chunks[chunk_id] = NDBChunk(
220
+ chunk_id=chunk_id,
221
+ text=relation_chunk,
222
+ page_with_visual=False,
223
+ reference=None,
224
+ start=None,
225
+ end=None,
226
+ page=None,
227
+ labels=[],
228
+ resource_labels=[],
229
+ field=None,
230
+ link=None,
231
+ )
232
+
233
+
234
+ def generate_graph_nodes_chunk(retrieval: GraphNodesSearchResponse, data: HydrateData):
235
+ chunk_id = f"nodes-{uuid.uuid4().hex}"
236
+ relation_chunk = "Existing Nodes that can be used at query time:\n"
237
+ for node in retrieval.nodes:
238
+ relation_chunk += f"- {node.group}: {node.type.value} - {node.value} \n"
239
+
240
+ data.chunks[chunk_id] = NDBChunk(
241
+ chunk_id=chunk_id,
242
+ text=relation_chunk,
243
+ page_with_visual=False,
244
+ reference=None,
245
+ start=None,
246
+ end=None,
247
+ page=None,
248
+ labels=[],
249
+ resource_labels=[],
250
+ field=None,
251
+ link=None,
252
+ )
253
+
254
+
255
+ async def hydrate_images(
256
+ chunk_ids: list[str],
257
+ context: Context,
258
+ nucliadb_driver: NucliaDBDriver,
259
+ vllm: bool,
260
+ visual: bool,
261
+ ) -> None:
262
+ """Image strategies
263
+
264
+ When using a vLLM, we can leverage images into the context. We can get
265
+ images from OCR and inception paragraphs, as well as table images and page
266
+ previews.
267
+
268
+ """
269
+ if not vllm:
270
+ return
271
+
272
+ resp = await nucliadb_driver.driver.session.post(
273
+ f"/v1/kb/{nucliadb_driver.config.kbid}/hydrate",
274
+ json=HydrateRequest(
275
+ data=chunk_ids,
276
+ hydration=Hydration(
277
+ paragraph=ParagraphHydration(
278
+ text=False,
279
+ image=ImageParagraphHydration(
280
+ source_image=(vllm and visual),
281
+ ),
282
+ table=TableParagraphHydration(
283
+ table_page_preview=vllm,
284
+ ),
285
+ page=ParagraphPageHydration(
286
+ page_with_visual=(vllm and visual),
287
+ ),
288
+ )
289
+ ),
290
+ ).model_dump(),
291
+ )
292
+ if resp.status_code != 200:
293
+ logger.warning(
294
+ f"Hydration API didn't succeed: {resp.status_code} {resp.content!r}"
295
+ )
296
+ return
297
+
298
+ hydrated = Hydrated.model_validate(resp.json())
299
+
300
+ for chunk_id, hydrated_paragraph in hydrated.paragraphs.items():
301
+ if (
302
+ hydrated_paragraph.image is not None
303
+ and hydrated_paragraph.image.source_image is not None
304
+ ):
305
+ image = hydrated_paragraph.image.source_image
306
+ context.images[chunk_id] = Image(
307
+ content_type=image.content_type,
308
+ b64encoded=image.b64encoded,
309
+ )
310
+ continue
311
+
312
+ hydrated_field = hydrated.fields[hydrated_paragraph.field]
313
+
314
+ if (
315
+ hydrated_paragraph.page is not None
316
+ and hydrated_paragraph.page.page_preview_ref is not None
317
+ and hasattr(hydrated_field, "previews")
318
+ ):
319
+ image = hydrated_field.previews[hydrated_paragraph.page.page_preview_ref] # type: ignore
320
+ context.images[chunk_id] = Image(
321
+ content_type=image.content_type,
322
+ b64encoded=image.b64encoded,
323
+ )
324
+ continue
325
+
326
+ if (
327
+ hydrated_paragraph.table is not None
328
+ and hydrated_paragraph.table.page_preview_ref is not None
329
+ and hasattr(hydrated_field, "previews")
330
+ ):
331
+ image = hydrated_field.previews[hydrated_paragraph.table.page_preview_ref] # type: ignore
332
+ context.images[chunk_id] = Image(
333
+ content_type=image.content_type,
334
+ b64encoded=image.b64encoded,
335
+ )
336
+ continue
337
+
338
+
339
+ async def hydrate(
340
+ context: Context,
341
+ search_results: SearchResults,
342
+ nucliadb_driver: NucliaDBDriver,
343
+ vllm: bool = True,
344
+ full_resource: bool = False,
345
+ after: int = 2,
346
+ before: int = 2,
347
+ visual: bool = False,
348
+ link: bool = False,
349
+ module: str = "ask",
350
+ ):
351
+ data = HydrateData()
352
+ if search_results.semantic_find_results is not None:
353
+ generate_chunk(
354
+ search_results.semantic_find_results, data, kbid=nucliadb_driver.config.kbid
355
+ )
356
+ if search_results.lexical_find_results is not None:
357
+ generate_chunk(
358
+ search_results.lexical_find_results, data, kbid=nucliadb_driver.config.kbid
359
+ )
360
+ if search_results.graph_find_results is not None:
361
+ generate_chunk(
362
+ search_results.graph_find_results, data, kbid=nucliadb_driver.config.kbid
363
+ )
364
+
365
+ if search_results.relations_results is not None:
366
+ generate_graph_relations_chunk(search_results.relations_results, data)
367
+ if search_results.graph_results is not None:
368
+ generate_graph_search_chunk(search_results.graph_results, data)
369
+ if search_results.nodes_results is not None:
370
+ generate_graph_nodes_chunk(search_results.nodes_results, data)
371
+
372
+ # ASK DETERMINISTIC RULES
373
+
374
+ # Image strategies: hydrate images for chunks when appropiate and we can
375
+ await hydrate_images(
376
+ [chunk_id for chunk_id in data.chunks],
377
+ context,
378
+ nucliadb_driver,
379
+ vllm,
380
+ visual,
381
+ )
382
+
383
+ # If there is generated fields add then on FieldExtensionStrategy RAG + extra_fields
384
+
385
+ # Has Conversations on the KB facets -> Conversational RAG strategy
386
+ for chunk_id, chunk in data.chunks.items():
387
+ if chunk_id.count("/") < 3:
388
+ context.chunks.append(
389
+ Chunk(
390
+ chunk_id=chunk_id,
391
+ text=chunk.text,
392
+ labels=chunk.labels + chunk.resource_labels,
393
+ source=nucliadb_driver.config.kbid,
394
+ origin_agent=module,
395
+ )
396
+ )
397
+ else:
398
+ # Its a field chunk
399
+ field_parts = chunk_id.split("/")
400
+ if len(field_parts) == 4:
401
+ rid, field_type, field_id, start_stop = field_parts
402
+ split = None
403
+ else:
404
+ rid, field_type, field_id, start_stop, split = field_parts
405
+
406
+ # If its conversational field get more info
407
+ # if field_type == "c":
408
+
409
+ # if vllm:
410
+ # attachments_images = True
411
+ # attachments_text = False
412
+ # else:
413
+ # attachments_images = False
414
+ # attachments_text = True
415
+
416
+ # Cut the proper text of the chunk
417
+ if (
418
+ chunk.field is not None
419
+ and chunk.field.extracted is not None
420
+ and chunk.field.extracted.text is not None
421
+ and chunk.field.extracted.text.text is not None
422
+ ):
423
+ text = ""
424
+ if full_resource:
425
+ text += chunk.field.extracted.text.text
426
+ else:
427
+ positions: List[Tuple[int, int]] = cut_the_proper_text(
428
+ chunk_id, chunk, before, after, data
429
+ )
430
+
431
+ for position in positions:
432
+ start, end = position
433
+ text += chunk.field.extracted.text.text[start:end]
434
+ text += "\n"
435
+ if chunk.link is not None and link:
436
+ text += f"\n\nLink: {chunk.link}\n"
437
+ else:
438
+ text = chunk.text
439
+
440
+ context.chunks.append(
441
+ Chunk(
442
+ chunk_id=chunk_id,
443
+ text=text,
444
+ labels=chunk.labels + chunk.resource_labels,
445
+ source=nucliadb_driver.config.kbid,
446
+ origin_agent=module,
447
+ )
448
+ )
449
+
450
+
451
+ def cut_the_proper_text(
452
+ chunk_id: str,
453
+ chunk: NDBChunk,
454
+ before: int,
455
+ after: int,
456
+ data: HydrateData,
457
+ ):
458
+ positions_to_cut: List[Tuple[int, int]] = []
459
+ # If add paragraphs
460
+ # Add before and after paragraphs
461
+ start = chunk.start or 0
462
+ end = chunk.end or 0
463
+ actual_id = chunk_id
464
+
465
+ # Before and after paragraphs
466
+ for _ in range(before):
467
+ prev_chunk = data.links.get((actual_id, Relation.PREV))
468
+ if prev_chunk is not None:
469
+ new_paragraph = data.paragraphs.get(prev_chunk)
470
+ if new_paragraph is not None:
471
+ actual_id = prev_chunk
472
+ if new_paragraph.start is not None and new_paragraph.start < start:
473
+ start = new_paragraph.start
474
+
475
+ for _ in range(after):
476
+ next_chunk = data.links.get((chunk_id, Relation.NEXT))
477
+ if next_chunk is not None:
478
+ new_paragraph = data.paragraphs.get(next_chunk)
479
+ if new_paragraph is not None:
480
+ actual_id = next_chunk
481
+ if new_paragraph.end is not None and new_paragraph.end > end:
482
+ end = new_paragraph.end
483
+ positions_to_cut.append((start, end))
484
+
485
+ return positions_to_cut
@@ -0,0 +1,48 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Dict, Optional
3
+
4
+ from hyperforge.models import Source
5
+ from nuclia.lib.nua_responses import SemanticConfig
6
+
7
+
8
+ @dataclass
9
+ class KnowledgeBoxInfo:
10
+ content_types: Dict[str, int]
11
+ languages: Dict[str, int]
12
+ facets: Dict[str, int]
13
+ paragraph_facets: Dict[str, int]
14
+ entity_model: str
15
+ generative_model: str
16
+ default_semantic_model: Optional[str] = None
17
+ semantic_models: Optional[list[str]] = None
18
+ semantic_model_configs: Dict[str, SemanticConfig] = field(default_factory=dict)
19
+
20
+
21
+ def get_knowledge_base_analysis(source: Source):
22
+ content_types = {}
23
+ languages: Dict[str, int] = {}
24
+
25
+ facets: Dict[str, int] = {}
26
+
27
+ for key, count in source.facets_native.facets.items():
28
+ if key.startswith("/n/i"):
29
+ content_type = key.replace("/n/i/", "")
30
+ if content_type.count("/") >= 1:
31
+ content_types[content_type] = count
32
+ elif key.startswith("/s/p/"):
33
+ language = key.replace("/s/p/", "")
34
+ languages[language] = count
35
+ else:
36
+ facets[key] = count
37
+
38
+ return KnowledgeBoxInfo(
39
+ paragraph_facets=source.paragraph_facets,
40
+ content_types=content_types,
41
+ languages=languages,
42
+ facets=facets,
43
+ semantic_models=source.learning_configuration.semantic_models,
44
+ entity_model=source.learning_configuration.ner_model,
45
+ generative_model=source.learning_configuration.generative_model,
46
+ default_semantic_model=source.learning_configuration.default_semantic_model,
47
+ semantic_model_configs=source.learning_configuration.semantic_model_configs,
48
+ )
@@ -0,0 +1,150 @@
1
+ from asyncio import gather
2
+ from typing import Awaitable, List, Optional, cast
3
+
4
+ from hyperforge.manager import Manager
5
+ from hyperforge.memory.memory import QuestionMemory
6
+ from nucliadb_models import RequestSecurity
7
+ from nucliadb_models.filters import And, Or
8
+ from nucliadb_models.graph.requests import (
9
+ GraphNodesQuery,
10
+ GraphNodesSearchRequest,
11
+ GraphPathQuery,
12
+ GraphRelationsQuery,
13
+ GraphRelationsSearchRequest,
14
+ GraphSearchRequest,
15
+ )
16
+ from nucliadb_models.graph.responses import (
17
+ GraphNodesSearchResponse,
18
+ GraphRelationsSearchResponse,
19
+ GraphSearchResponse,
20
+ )
21
+
22
+ from hyperforge_nucliadb.ask.config import AskAgentConfig
23
+ from hyperforge_nucliadb.ask.models import Analysis, SearchResults
24
+ from hyperforge_nucliadb.ask.utils import (
25
+ empty,
26
+ get_nodes,
27
+ get_relations,
28
+ )
29
+ from hyperforge_nucliadb.driver import NucliaDBDriver
30
+
31
+
32
+ def query_graph_search(
33
+ memory: QuestionMemory,
34
+ manager: Manager,
35
+ analysis: Analysis,
36
+ nucliadb_driver: NucliaDBDriver,
37
+ config: AskAgentConfig,
38
+ relations: List[GraphPathQuery],
39
+ nodes: List[GraphPathQuery],
40
+ ) -> Awaitable[GraphSearchResponse]:
41
+ """Query relations in NucliaDB based on the provided analysis."""
42
+
43
+ find_request = GraphSearchRequest(
44
+ top_k=50,
45
+ security=RequestSecurity(groups=config.security_groups),
46
+ query=And(
47
+ operands=[
48
+ Or(operands=relations),
49
+ Or(operands=nodes),
50
+ ]
51
+ ),
52
+ )
53
+ return nucliadb_driver.graph_search(find_request)
54
+
55
+
56
+ def query_graph_nodes(
57
+ memory: QuestionMemory,
58
+ manager: Manager,
59
+ analysis: Analysis,
60
+ nucliadb_driver: NucliaDBDriver,
61
+ config: AskAgentConfig,
62
+ nodes: List[GraphNodesQuery],
63
+ ) -> Awaitable[GraphNodesSearchResponse]:
64
+ """Query relations in NucliaDB based on the provided analysis."""
65
+
66
+ find_request = GraphNodesSearchRequest(
67
+ top_k=50,
68
+ security=RequestSecurity(groups=config.security_groups),
69
+ query=Or(operands=nodes),
70
+ )
71
+
72
+ return nucliadb_driver.graph_nodes(find_request)
73
+
74
+
75
+ def query_graph_relations(
76
+ memory: QuestionMemory,
77
+ manager: Manager,
78
+ analysis: Analysis,
79
+ nucliadb_driver: NucliaDBDriver,
80
+ config: AskAgentConfig,
81
+ relations: List[GraphRelationsQuery],
82
+ ) -> Awaitable[GraphRelationsSearchResponse]:
83
+ """Query relations in NucliaDB based on the provided analysis."""
84
+ find_request = GraphRelationsSearchRequest(
85
+ top_k=50,
86
+ security=RequestSecurity(groups=config.security_groups),
87
+ query=Or(operands=relations),
88
+ )
89
+ return nucliadb_driver.graph_relations(find_request)
90
+
91
+
92
+ async def knowledge_scan(
93
+ memory: QuestionMemory,
94
+ manager: Manager,
95
+ analysis: Analysis,
96
+ nucliadb_driver: NucliaDBDriver,
97
+ config: AskAgentConfig,
98
+ ) -> SearchResults:
99
+ nodes = cast(list[GraphPathQuery], get_nodes(analysis))
100
+ relations = cast(list[GraphPathQuery], get_relations(analysis))
101
+
102
+ graph: Awaitable[Optional[GraphSearchResponse]] = query_graph_search(
103
+ memory,
104
+ manager,
105
+ analysis,
106
+ nucliadb_driver,
107
+ config=config,
108
+ relations=relations,
109
+ nodes=nodes,
110
+ )
111
+ if len(nodes) > 0:
112
+ nodes_search: Awaitable[Optional[GraphNodesSearchResponse]] = query_graph_nodes(
113
+ memory,
114
+ manager,
115
+ analysis,
116
+ nucliadb_driver,
117
+ config=config,
118
+ nodes=cast(list[GraphNodesQuery], nodes),
119
+ )
120
+ else:
121
+ nodes_search = empty()
122
+
123
+ if len(relations) > 0:
124
+ relations_search: Awaitable[Optional[GraphRelationsSearchResponse]] = (
125
+ query_graph_relations(
126
+ memory,
127
+ manager,
128
+ analysis,
129
+ nucliadb_driver,
130
+ config=config,
131
+ relations=cast(list[GraphRelationsQuery], relations),
132
+ )
133
+ )
134
+ else:
135
+ relations_search = empty()
136
+
137
+ nodes_results, relations_results, graph_results = await gather(
138
+ nodes_search,
139
+ relations_search,
140
+ graph,
141
+ )
142
+
143
+ return SearchResults(
144
+ lexical_find_results=None,
145
+ semantic_find_results=None,
146
+ graph_find_results=None,
147
+ graph_results=graph_results,
148
+ nodes_results=nodes_results,
149
+ relations_results=relations_results,
150
+ )