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.
- hyperforge_nucliadb/__init__.py +13 -0
- hyperforge_nucliadb/advanced_ask_agent.py +267 -0
- hyperforge_nucliadb/advanced_ask_config.py +241 -0
- hyperforge_nucliadb/ask/__init__.py +3 -0
- hyperforge_nucliadb/ask/analysis.py +310 -0
- hyperforge_nucliadb/ask/ask.py +326 -0
- hyperforge_nucliadb/ask/config.py +52 -0
- hyperforge_nucliadb/ask/hydrate.py +485 -0
- hyperforge_nucliadb/ask/kb_analysis.py +48 -0
- hyperforge_nucliadb/ask/knowledge_scan.py +150 -0
- hyperforge_nucliadb/ask/models.py +59 -0
- hyperforge_nucliadb/ask/multi.py +146 -0
- hyperforge_nucliadb/ask/nucliadb.py +238 -0
- hyperforge_nucliadb/ask/prompt_analysis.py +50 -0
- hyperforge_nucliadb/ask/query_analysis.py +83 -0
- hyperforge_nucliadb/ask/rerank.py +45 -0
- hyperforge_nucliadb/ask/utils.py +36 -0
- hyperforge_nucliadb/ask_utils.py +235 -0
- hyperforge_nucliadb/basic_ask_agent.py +1812 -0
- hyperforge_nucliadb/basic_ask_config.py +42 -0
- hyperforge_nucliadb/driver.py +428 -0
- hyperforge_nucliadb/driver_config.py +42 -0
- hyperforge_nucliadb/sync/__init__.py +0 -0
- hyperforge_nucliadb/sync/agent.py +477 -0
- hyperforge_nucliadb/sync/config.py +12 -0
- hyperforge_nucliadb/sync/config_driver.py +22 -0
- hyperforge_nucliadb/sync/driver.py +148 -0
- hyperforge_nucliadb-1.0.0.post22.dist-info/METADATA +26 -0
- hyperforge_nucliadb-1.0.0.post22.dist-info/RECORD +31 -0
- hyperforge_nucliadb-1.0.0.post22.dist-info/WHEEL +5 -0
- hyperforge_nucliadb-1.0.0.post22.dist-info/top_level.txt +1 -0
|
@@ -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
|
+
)
|