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,42 @@
|
|
|
1
|
+
from typing import List, Literal, Optional, Tuple
|
|
2
|
+
|
|
3
|
+
from hyperforge.context.agent import ContextAgentConfig
|
|
4
|
+
from hyperforge.utils import WidgetType
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
from pydantic.config import ConfigDict
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BasicAskAgentConfig(ContextAgentConfig):
|
|
10
|
+
model_config = ConfigDict(title="Knowledge Box basic Ask")
|
|
11
|
+
module: Literal["basic_ask"] = "basic_ask"
|
|
12
|
+
sources: List[str] = Field(
|
|
13
|
+
default_factory=list,
|
|
14
|
+
json_schema_extra={
|
|
15
|
+
"show_in_node": True,
|
|
16
|
+
},
|
|
17
|
+
)
|
|
18
|
+
generative_model: str = Field(
|
|
19
|
+
default="chatgpt-azure-4o-mini",
|
|
20
|
+
title="Generative model",
|
|
21
|
+
description="Model used to generate answers",
|
|
22
|
+
json_schema_extra={"widget": WidgetType.MODEL_SELECT},
|
|
23
|
+
)
|
|
24
|
+
published_functions: Optional[Tuple[str, ...]] = Field(
|
|
25
|
+
default=(
|
|
26
|
+
"search_by_title",
|
|
27
|
+
"ask_labels_list",
|
|
28
|
+
"ask_by_title",
|
|
29
|
+
"ask_agent",
|
|
30
|
+
"ask_labels",
|
|
31
|
+
"facets_count",
|
|
32
|
+
"facets_search",
|
|
33
|
+
"catalog_search",
|
|
34
|
+
"all_images_by_title",
|
|
35
|
+
"search_images",
|
|
36
|
+
),
|
|
37
|
+
title="Published functions",
|
|
38
|
+
description="List of functions published by this agent to be used by other agents in the chain",
|
|
39
|
+
json_schema_extra={
|
|
40
|
+
"widget": WidgetType.NOT_SHOWN,
|
|
41
|
+
},
|
|
42
|
+
)
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from asyncio import gather
|
|
3
|
+
from functools import reduce
|
|
4
|
+
from typing import Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
from hyperforge.configure import driver
|
|
8
|
+
from hyperforge.driver import Driver
|
|
9
|
+
from hyperforge.models import Facets
|
|
10
|
+
from hyperforge.utils.http import SafeTransport
|
|
11
|
+
from nuclia.lib.nua_responses import StoredLearningConfiguration
|
|
12
|
+
from nucliadb_models.graph.requests import (
|
|
13
|
+
GraphNodesSearchRequest,
|
|
14
|
+
GraphRelationsSearchRequest,
|
|
15
|
+
GraphSearchRequest,
|
|
16
|
+
)
|
|
17
|
+
from nucliadb_models.graph.responses import (
|
|
18
|
+
GraphNodesSearchResponse,
|
|
19
|
+
GraphRelationsSearchResponse,
|
|
20
|
+
GraphSearchResponse,
|
|
21
|
+
)
|
|
22
|
+
from nucliadb_models.resource import Resource
|
|
23
|
+
from nucliadb_models.search import (
|
|
24
|
+
AskRequest,
|
|
25
|
+
CatalogFacetsPrefix,
|
|
26
|
+
CatalogFacetsRequest,
|
|
27
|
+
CatalogFacetsResponse,
|
|
28
|
+
CatalogRequest,
|
|
29
|
+
CatalogResponse,
|
|
30
|
+
FindOptions,
|
|
31
|
+
FindRequest,
|
|
32
|
+
KnowledgeboxFindResults,
|
|
33
|
+
MinScore,
|
|
34
|
+
SearchOptions,
|
|
35
|
+
SearchRequest,
|
|
36
|
+
SyncAskResponse,
|
|
37
|
+
)
|
|
38
|
+
from nucliadb_sdk.v2 import NucliaDBAsync
|
|
39
|
+
|
|
40
|
+
from hyperforge import logger
|
|
41
|
+
from hyperforge_nucliadb.driver_config import (
|
|
42
|
+
ManagerConnection,
|
|
43
|
+
NucliaDBConfig,
|
|
44
|
+
NucliaDBConnection,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
DEFAULT_FIELD_FACETS = [
|
|
48
|
+
"/n/icon",
|
|
49
|
+
"/n/metadata.status",
|
|
50
|
+
"/metadata.language",
|
|
51
|
+
"/metadata.languages",
|
|
52
|
+
"/field",
|
|
53
|
+
"/f",
|
|
54
|
+
"/ml",
|
|
55
|
+
"/origin.tags",
|
|
56
|
+
"/origin.metadata",
|
|
57
|
+
"/origin.path",
|
|
58
|
+
"/origin.source-id",
|
|
59
|
+
]
|
|
60
|
+
DEFAULT_CHUNK_LABELS = ["/k"]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
async def connect(conn: NucliaDBConnection):
|
|
64
|
+
headers: Dict[str, str] = {}
|
|
65
|
+
if "http://localhost" in conn.url:
|
|
66
|
+
headers = {
|
|
67
|
+
"X-NUCLIADB-ROLES": "READER",
|
|
68
|
+
}
|
|
69
|
+
return NucliaDBAsync(
|
|
70
|
+
api_key=conn.key,
|
|
71
|
+
url=conn.url,
|
|
72
|
+
headers=headers,
|
|
73
|
+
_httpx_transport=SafeTransport(),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
async def manager_connect(conn: NucliaDBConnection):
|
|
78
|
+
headers: Dict[str, str] = {}
|
|
79
|
+
return ManagerConnection(api_key=conn.key, url=conn.manager, headers=headers)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@driver(
|
|
83
|
+
id="nucliadb",
|
|
84
|
+
title="NucliaDB Driver",
|
|
85
|
+
description="Driver for interacting with the NucliaDB API.",
|
|
86
|
+
config_schema=NucliaDBConfig,
|
|
87
|
+
)
|
|
88
|
+
class NucliaDBDriver(Driver):
|
|
89
|
+
driver: NucliaDBAsync
|
|
90
|
+
manager: ManagerConnection
|
|
91
|
+
config: NucliaDBConnection
|
|
92
|
+
_synonyms: Optional[dict[str, list[str]]]
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
async def init(cls, driver: NucliaDBConfig):
|
|
96
|
+
return cls(
|
|
97
|
+
provider=driver.provider,
|
|
98
|
+
name=driver.name,
|
|
99
|
+
config=driver.config,
|
|
100
|
+
driver=await connect(driver.config),
|
|
101
|
+
manager=await manager_connect(driver.config),
|
|
102
|
+
_synonyms=None,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
async def synonyms_raw(self) -> Dict[str, List[str]]:
|
|
106
|
+
synonyms_list = await self.driver.get_custom_synonyms(kbid=self.config.kbid)
|
|
107
|
+
return dict(
|
|
108
|
+
(k.lower(), [x.lower() for x in v])
|
|
109
|
+
for k, v in synonyms_list.synonyms.items()
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
async def labels(self) -> Dict[str, List[str]]:
|
|
113
|
+
labelsets = await self.driver.get_labelsets(kbid=self.config.kbid)
|
|
114
|
+
result = {}
|
|
115
|
+
for labelset in labelsets.labelsets:
|
|
116
|
+
labels = await self.driver.get_labelset(
|
|
117
|
+
kbid=self.config.kbid, labelset=labelset
|
|
118
|
+
)
|
|
119
|
+
result[labelset] = [x.title for x in labels.labels]
|
|
120
|
+
|
|
121
|
+
return result
|
|
122
|
+
|
|
123
|
+
async def field_facets(self) -> Dict[str, int]:
|
|
124
|
+
field_labels = {}
|
|
125
|
+
search_fulltext_classify = await self.driver.search(
|
|
126
|
+
kbid=self.config.kbid,
|
|
127
|
+
content=SearchRequest(
|
|
128
|
+
faceted=["/classification.labels"],
|
|
129
|
+
features=[SearchOptions.FULLTEXT],
|
|
130
|
+
),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
if (
|
|
134
|
+
search_fulltext_classify.fulltext is not None
|
|
135
|
+
and search_fulltext_classify.fulltext.facets is not None
|
|
136
|
+
and "/classification.labels" in search_fulltext_classify.fulltext.facets
|
|
137
|
+
):
|
|
138
|
+
fulltext_labelsets = [
|
|
139
|
+
x
|
|
140
|
+
for x in search_fulltext_classify.fulltext.facets[
|
|
141
|
+
"/classification.labels"
|
|
142
|
+
].keys()
|
|
143
|
+
]
|
|
144
|
+
else:
|
|
145
|
+
fulltext_labelsets = []
|
|
146
|
+
|
|
147
|
+
paths = [path for path in DEFAULT_FIELD_FACETS]
|
|
148
|
+
|
|
149
|
+
paths.extend(fulltext_labelsets)
|
|
150
|
+
|
|
151
|
+
search_results_second = await self.driver.search(
|
|
152
|
+
kbid=self.config.kbid,
|
|
153
|
+
content=SearchRequest(faceted=paths, features=[SearchOptions.FULLTEXT]),
|
|
154
|
+
)
|
|
155
|
+
if (
|
|
156
|
+
search_results_second.fulltext is not None
|
|
157
|
+
and search_results_second.fulltext.facets is not None
|
|
158
|
+
):
|
|
159
|
+
field_labels = search_results_second.fulltext.facets
|
|
160
|
+
|
|
161
|
+
field_labels = reduce(lambda a, b: {**a, **b}, field_labels.values())
|
|
162
|
+
return field_labels
|
|
163
|
+
|
|
164
|
+
async def field_labels(
|
|
165
|
+
self, labelsets: list[str] | None = None
|
|
166
|
+
) -> tuple[dict[str, dict[str, int]], int]:
|
|
167
|
+
"""
|
|
168
|
+
Returns a prettified and filtered list of the field labels facets.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
labelsets (list[str] | None): List of labelsets to filter the results.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
dict[str, dict[str, int]]: Dictionary of labelsets with their labels and counts
|
|
175
|
+
int: Total number of resources for the knowledge box.
|
|
176
|
+
"""
|
|
177
|
+
# Add the filter to the facets
|
|
178
|
+
prefixes = (
|
|
179
|
+
[f"/classification.labels/{ls}" for ls in labelsets]
|
|
180
|
+
if labelsets
|
|
181
|
+
else ["/classification.labels"]
|
|
182
|
+
)
|
|
183
|
+
catalog_result = await self.driver.catalog(
|
|
184
|
+
kbid=self.config.kbid,
|
|
185
|
+
faceted=prefixes,
|
|
186
|
+
page_size=0,
|
|
187
|
+
)
|
|
188
|
+
if not catalog_result.fulltext or not catalog_result.fulltext.facets:
|
|
189
|
+
raise Exception("Empty facets in catalog result")
|
|
190
|
+
|
|
191
|
+
# Remove /classification.labels/ prefix from the facets
|
|
192
|
+
result: dict[str, dict[str, int]] = {
|
|
193
|
+
labelset.split("/")[-1]: {
|
|
194
|
+
label.split(f"{labelset}/")[-1]: count
|
|
195
|
+
for label, count in facets.items()
|
|
196
|
+
}
|
|
197
|
+
for labelset, facets in catalog_result.fulltext.facets.items()
|
|
198
|
+
}
|
|
199
|
+
return result, catalog_result.fulltext.total
|
|
200
|
+
|
|
201
|
+
async def paragraph_facets(self) -> Dict[str, int]:
|
|
202
|
+
paragraphs_facets = {}
|
|
203
|
+
|
|
204
|
+
search_paragraph_classify = await self.driver.search(
|
|
205
|
+
kbid=self.config.kbid,
|
|
206
|
+
content=SearchRequest(
|
|
207
|
+
faceted=["/classification.labels"],
|
|
208
|
+
features=[SearchOptions.KEYWORD],
|
|
209
|
+
),
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
if (
|
|
213
|
+
search_paragraph_classify.paragraphs is not None
|
|
214
|
+
and search_paragraph_classify.paragraphs.facets is not None
|
|
215
|
+
and "/classification.labels" in search_paragraph_classify.paragraphs.facets
|
|
216
|
+
):
|
|
217
|
+
paragraph_labels = [
|
|
218
|
+
x
|
|
219
|
+
for x in search_paragraph_classify.paragraphs.facets[
|
|
220
|
+
"/classification.labels"
|
|
221
|
+
].keys()
|
|
222
|
+
]
|
|
223
|
+
else:
|
|
224
|
+
paragraph_labels = []
|
|
225
|
+
|
|
226
|
+
paths = [path for path in DEFAULT_CHUNK_LABELS]
|
|
227
|
+
|
|
228
|
+
if "/classification.labels" in paths:
|
|
229
|
+
paths.remove("/classification.labels")
|
|
230
|
+
|
|
231
|
+
paths.extend(paragraph_labels)
|
|
232
|
+
|
|
233
|
+
search_results_second = await self.driver.search(
|
|
234
|
+
kbid=self.config.kbid, content=SearchRequest(faceted=paths)
|
|
235
|
+
)
|
|
236
|
+
if (
|
|
237
|
+
search_results_second.paragraphs is not None
|
|
238
|
+
and search_results_second.paragraphs.facets is not None
|
|
239
|
+
):
|
|
240
|
+
paragraphs_facets = search_results_second.paragraphs.facets
|
|
241
|
+
|
|
242
|
+
try:
|
|
243
|
+
paragraphs_facets = reduce(
|
|
244
|
+
lambda a, b: {**a, **b}, paragraphs_facets.values()
|
|
245
|
+
)
|
|
246
|
+
except Exception as e:
|
|
247
|
+
logger.error(f"Error reducing paragraph facets: {e}")
|
|
248
|
+
paragraphs_facets = {}
|
|
249
|
+
|
|
250
|
+
return paragraphs_facets
|
|
251
|
+
|
|
252
|
+
async def facets(self) -> Facets:
|
|
253
|
+
chunks, fields = await gather(self.paragraph_facets(), self.field_facets())
|
|
254
|
+
return Facets(chunks=chunks, fields=fields)
|
|
255
|
+
|
|
256
|
+
async def facets_native(self) -> CatalogFacetsResponse:
|
|
257
|
+
"""
|
|
258
|
+
Get facets for the knowledge box.
|
|
259
|
+
This is a native implementation that returns the facets as a dictionary.
|
|
260
|
+
|
|
261
|
+
We restrict the types of facets to a predefined subset, as on big knowledge boxes, the response could be very large.
|
|
262
|
+
"""
|
|
263
|
+
facets = CatalogFacetsResponse(facets={})
|
|
264
|
+
|
|
265
|
+
# Run requests in parallel as otherwise the facets endpoint can take too long on large knowledge boxes.
|
|
266
|
+
tasks = []
|
|
267
|
+
for prefix, depth in [
|
|
268
|
+
("/n/i", 2), # content types
|
|
269
|
+
("/s/p", 2), # primary language
|
|
270
|
+
("/l", 2), # classification labels
|
|
271
|
+
]:
|
|
272
|
+
request = CatalogFacetsRequest(
|
|
273
|
+
prefixes=[CatalogFacetsPrefix(prefix=prefix, depth=depth)]
|
|
274
|
+
)
|
|
275
|
+
tasks.append(
|
|
276
|
+
self.driver.catalog_facets(kbid=self.config.kbid, content=request)
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
responses = await asyncio.gather(*tasks, return_exceptions=True)
|
|
280
|
+
for facets_response in responses:
|
|
281
|
+
if isinstance(facets_response, CatalogFacetsResponse):
|
|
282
|
+
facets.facets.update(facets_response.facets)
|
|
283
|
+
else:
|
|
284
|
+
logger.error(f"Error getting facets: {facets_response}")
|
|
285
|
+
return facets
|
|
286
|
+
|
|
287
|
+
async def description(self):
|
|
288
|
+
kb_obj = await self.driver.get_knowledge_box(kbid=self.config.kbid)
|
|
289
|
+
return f"""{self.config.description} {kb_obj.config.description if kb_obj.config else ""}"""
|
|
290
|
+
|
|
291
|
+
async def get_learning_configuration(self):
|
|
292
|
+
configuration = await self.driver.get_configuration(kbid=self.config.kbid)
|
|
293
|
+
if configuration is None:
|
|
294
|
+
raise Exception("No configuration found for the knowledge box")
|
|
295
|
+
|
|
296
|
+
return StoredLearningConfiguration.model_validate(configuration)
|
|
297
|
+
|
|
298
|
+
async def synonyms(self, sentence: str) -> str:
|
|
299
|
+
synonyms_obj = await self.driver.get_custom_synonyms(kbid=self.config.kbid)
|
|
300
|
+
self._synonyms = synonyms_obj.model_dump()
|
|
301
|
+
words = sentence.split()
|
|
302
|
+
result = []
|
|
303
|
+
if self._synonyms is not None:
|
|
304
|
+
for word in words:
|
|
305
|
+
if word in self._synonyms:
|
|
306
|
+
word = " ".join(self._synonyms[word])
|
|
307
|
+
result.append(word)
|
|
308
|
+
return " ".join(result)
|
|
309
|
+
|
|
310
|
+
async def ask(self, item: AskRequest, headers={}) -> SyncAskResponse:
|
|
311
|
+
return await self.driver.ask(
|
|
312
|
+
kbid=self.config.kbid, content=item, headers=headers
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
async def find(
|
|
316
|
+
self, q: str, filters: List[str] = [], rids: List[str] = []
|
|
317
|
+
) -> KnowledgeboxFindResults:
|
|
318
|
+
return await self.driver.find(
|
|
319
|
+
kbid=self.config.kbid,
|
|
320
|
+
content=FindRequest(
|
|
321
|
+
query=q,
|
|
322
|
+
resource_filters=rids,
|
|
323
|
+
features=[FindOptions.KEYWORD],
|
|
324
|
+
min_score=MinScore(bm25=1.0),
|
|
325
|
+
filters=list(set(self.config.filters + filters)),
|
|
326
|
+
),
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
async def find_raw(self, q: FindRequest) -> KnowledgeboxFindResults:
|
|
330
|
+
return await self.driver.find(
|
|
331
|
+
kbid=self.config.kbid,
|
|
332
|
+
content=q,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
async def graph_relations(
|
|
336
|
+
self, q: GraphRelationsSearchRequest
|
|
337
|
+
) -> GraphRelationsSearchResponse:
|
|
338
|
+
return await self.driver.graph_relations(kbid=self.config.kbid, content=q)
|
|
339
|
+
|
|
340
|
+
async def graph_search(self, q: GraphSearchRequest) -> GraphSearchResponse:
|
|
341
|
+
return await self.driver.graph_search(kbid=self.config.kbid, content=q)
|
|
342
|
+
|
|
343
|
+
async def graph_nodes(self, q: GraphNodesSearchRequest) -> GraphNodesSearchResponse:
|
|
344
|
+
return await self.driver.graph_nodes(kbid=self.config.kbid, content=q)
|
|
345
|
+
|
|
346
|
+
async def catalog_search_raw(self, q: CatalogRequest) -> CatalogResponse:
|
|
347
|
+
return await self.driver.catalog(content=q, kbid=self.config.kbid)
|
|
348
|
+
|
|
349
|
+
async def get_resource_by_id(
|
|
350
|
+
self, rid: str, query_params: Optional[Dict[str, str]] = None
|
|
351
|
+
) -> Optional[Resource]:
|
|
352
|
+
return await self.driver.get_resource_by_id(
|
|
353
|
+
kbid=self.config.kbid, rid=rid, query_params=query_params
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
async def get_ephemeral_token(self, path: Optional[str] = None) -> Optional[str]:
|
|
357
|
+
"""Get an ephemeral token scoped to the knowledge box, optionally restricted to a specific resource path.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
path: When provided, the token will only be valid for this specific path
|
|
361
|
+
(e.g. /kb/{kbid}/resource/{rid}). This prevents the token from
|
|
362
|
+
being used to access the entire knowledge box.
|
|
363
|
+
"""
|
|
364
|
+
async with httpx.AsyncClient(transport=SafeTransport()) as client:
|
|
365
|
+
headers = {
|
|
366
|
+
"Authorization": f"Bearer {self.config.key}",
|
|
367
|
+
"Content-Type": "application/json",
|
|
368
|
+
"Accept": "*/*",
|
|
369
|
+
}
|
|
370
|
+
url = f"{self.manager.url}/v1/ephemeral_token"
|
|
371
|
+
body: dict = {"ttl": 600}
|
|
372
|
+
if path is not None:
|
|
373
|
+
body["path"] = path
|
|
374
|
+
response = await client.post(url, headers=headers, json=body)
|
|
375
|
+
if response.status_code != 201:
|
|
376
|
+
raise Exception(f"Error getting ephemeral token: {response.text}")
|
|
377
|
+
data = response.json()
|
|
378
|
+
return data.get("token", None)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def format_ndb_labels(
|
|
382
|
+
labels: Dict[str, List[str]],
|
|
383
|
+
max_examples: int = 5,
|
|
384
|
+
) -> str:
|
|
385
|
+
"""Format labels and their examples into a string representation."""
|
|
386
|
+
labels_str = ""
|
|
387
|
+
for label, examples in labels.items():
|
|
388
|
+
labels_str += f"- {label}: {', '.join(examples[:max_examples]) + (', ...' if len(examples) > max_examples else '')}\n"
|
|
389
|
+
return labels_str
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def format_ndb_catalog(
|
|
393
|
+
catalog: CatalogResponse,
|
|
394
|
+
) -> str:
|
|
395
|
+
"""Format catalog results into a string representation."""
|
|
396
|
+
catalog_str = ""
|
|
397
|
+
# TODO: Consider converting to jinja2 template
|
|
398
|
+
for rid, resource in catalog.resources.items():
|
|
399
|
+
catalog_str += f"- Title: {resource.title}\n"
|
|
400
|
+
if resource.summary:
|
|
401
|
+
catalog_str += f" Summary: '{resource.summary[:100]}'\n"
|
|
402
|
+
catalog_str += f" Format: {resource.icon}\n"
|
|
403
|
+
if resource.metadata and resource.metadata.language:
|
|
404
|
+
catalog_str += f" Language: {resource.metadata.language}\n"
|
|
405
|
+
|
|
406
|
+
if resource.usermetadata and resource.usermetadata.classifications:
|
|
407
|
+
catalog_str += " User defined Labels: "
|
|
408
|
+
catalog_str += " | ".join(
|
|
409
|
+
f'{classification.labelset}:"{classification.label}"'
|
|
410
|
+
for classification in resource.usermetadata.classifications
|
|
411
|
+
# XXX: Don't know if this is needed
|
|
412
|
+
if not classification.cancelled_by_user
|
|
413
|
+
)
|
|
414
|
+
catalog_str += "\n"
|
|
415
|
+
|
|
416
|
+
if (
|
|
417
|
+
resource.computedmetadata
|
|
418
|
+
and resource.computedmetadata.field_classifications
|
|
419
|
+
):
|
|
420
|
+
catalog_str += " Computed Labels: "
|
|
421
|
+
catalog_str += " | ".join(
|
|
422
|
+
f'{classification.labelset}:"{classification.label}"'
|
|
423
|
+
for field_classification in resource.computedmetadata.field_classifications
|
|
424
|
+
for classification in field_classification.classifications
|
|
425
|
+
)
|
|
426
|
+
catalog_str += "\n"
|
|
427
|
+
catalog_str += "\n"
|
|
428
|
+
return catalog_str
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from typing import ClassVar, Dict, List, Literal, Optional
|
|
2
|
+
|
|
3
|
+
from hyperforge.driver import DriverConfig, EncryptedPayload
|
|
4
|
+
from nucliadb_models.filters import CatalogFilterExpression, FilterExpression
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
from pydantic.config import ConfigDict
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ManagerConnection(BaseModel):
|
|
10
|
+
api_key: Optional[str]
|
|
11
|
+
url: str
|
|
12
|
+
headers: Dict[str, str]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class NucliaDBConnection(EncryptedPayload):
|
|
16
|
+
encrypted_fields: ClassVar[list[str]] = ["key"]
|
|
17
|
+
key: Optional[str] = None
|
|
18
|
+
url: str
|
|
19
|
+
manager: str
|
|
20
|
+
filters: List[str] = Field(
|
|
21
|
+
default_factory=list,
|
|
22
|
+
deprecated=True,
|
|
23
|
+
description="Use filter_expression instead",
|
|
24
|
+
)
|
|
25
|
+
filter_expression: Optional[FilterExpression] = Field(
|
|
26
|
+
default=None,
|
|
27
|
+
description="Expression to filter fields/paragraphs when retrieving context. This will be combined with any other filters that are provided at query time or chosen by the agent, using an AND operator.",
|
|
28
|
+
)
|
|
29
|
+
catalog_filter_expression: Optional[CatalogFilterExpression] = Field(
|
|
30
|
+
default=None,
|
|
31
|
+
description="Expression to filter catalog items when retrieving context. This will be combined with any other filters that are provided at query time or chosen by the agent, using an AND operator.",
|
|
32
|
+
)
|
|
33
|
+
description: str = Field(
|
|
34
|
+
description="Description of the knowledge box, used to give context to the agent and assist in selecting the right knowledge box for a given query."
|
|
35
|
+
)
|
|
36
|
+
kbid: str
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class NucliaDBConfig(DriverConfig[NucliaDBConnection]):
|
|
40
|
+
model_config = ConfigDict(title="Knowledge Box")
|
|
41
|
+
provider: Literal["nucliadb"]
|
|
42
|
+
config: NucliaDBConnection
|
|
File without changes
|