hyperforge-nucliadb-agentic 1.0.0.post64__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_agentic/__init__.py +3 -0
- hyperforge_nucliadb_agentic/agent.py +1642 -0
- hyperforge_nucliadb_agentic/ask/__init__.py +5 -0
- hyperforge_nucliadb_agentic/ask/audit.py +439 -0
- hyperforge_nucliadb_agentic/ask/exceptions.py +50 -0
- hyperforge_nucliadb_agentic/ask/lifespan.py +21 -0
- hyperforge_nucliadb_agentic/ask/model.py +1299 -0
- hyperforge_nucliadb_agentic/ask/predict.py +431 -0
- hyperforge_nucliadb_agentic/ask/predict_models.py +78 -0
- hyperforge_nucliadb_agentic/ask/search/__init__.py +0 -0
- hyperforge_nucliadb_agentic/ask/search/ask.py +1182 -0
- hyperforge_nucliadb_agentic/ask/search/graph_strategy.py +1138 -0
- hyperforge_nucliadb_agentic/ask/search/highlight.py +93 -0
- hyperforge_nucliadb_agentic/ask/search/hydrator.py +29 -0
- hyperforge_nucliadb_agentic/ask/search/metrics.py +112 -0
- hyperforge_nucliadb_agentic/ask/search/parsers/__init__.py +0 -0
- hyperforge_nucliadb_agentic/ask/search/parsers/ask.py +70 -0
- hyperforge_nucliadb_agentic/ask/search/parsers/fetcher.py +192 -0
- hyperforge_nucliadb_agentic/ask/search/parsers/find.py +729 -0
- hyperforge_nucliadb_agentic/ask/search/prompt.py +1298 -0
- hyperforge_nucliadb_agentic/ask/search/rank_fusion.py +157 -0
- hyperforge_nucliadb_agentic/ask/search/rerankers.py +161 -0
- hyperforge_nucliadb_agentic/ask/search/retrieval.py +750 -0
- hyperforge_nucliadb_agentic/ask/search/rpc.py +192 -0
- hyperforge_nucliadb_agentic/ask/settings.py +9 -0
- hyperforge_nucliadb_agentic/ask/utils/ids.py +188 -0
- hyperforge_nucliadb_agentic/ask/utils/proto.py +6 -0
- hyperforge_nucliadb_agentic/ask/utils/responses.py +6 -0
- hyperforge_nucliadb_agentic/ask/utils/text_blocks.py +51 -0
- hyperforge_nucliadb_agentic/config.py +47 -0
- hyperforge_nucliadb_agentic/internal_driver.py +87 -0
- hyperforge_nucliadb_agentic/py.typed +0 -0
- hyperforge_nucliadb_agentic-1.0.0.post64.dist-info/METADATA +24 -0
- hyperforge_nucliadb_agentic-1.0.0.post64.dist-info/RECORD +35 -0
- hyperforge_nucliadb_agentic-1.0.0.post64.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from collections.abc import AsyncGenerator
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
import backoff
|
|
8
|
+
import httpx
|
|
9
|
+
from nuclia_models.predict.generative_responses import GenerativeChunk
|
|
10
|
+
from nucliadb_models.internal.predict import (
|
|
11
|
+
QueryInfo,
|
|
12
|
+
RerankModel,
|
|
13
|
+
RerankResponse,
|
|
14
|
+
)
|
|
15
|
+
from nucliadb_protos.utils_pb2 import RelationNode
|
|
16
|
+
from nucliadb_telemetry import errors, metrics
|
|
17
|
+
from nucliadb_utils.exceptions import LimitsExceededError
|
|
18
|
+
from nucliadb_utils.settings import nuclia_settings
|
|
19
|
+
from nucliadb_utils.utilities import Utility, clean_utility, get_utility, set_utility
|
|
20
|
+
from pydantic import ValidationError
|
|
21
|
+
|
|
22
|
+
from hyperforge_nucliadb_agentic.ask import logger
|
|
23
|
+
from hyperforge_nucliadb_agentic.ask.model import ChatModel, RephraseModel
|
|
24
|
+
from hyperforge_nucliadb_agentic.ask.predict_models import QueryModel
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SendToPredictError(Exception):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ProxiedPredictAPIError(Exception):
|
|
32
|
+
def __init__(self, status: int, detail: str = ""):
|
|
33
|
+
self.status = status
|
|
34
|
+
self.detail = detail
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class NUAKeyMissingError(Exception):
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class RephraseError(Exception):
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class RephraseMissingContextError(Exception):
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
PUBLIC_PREDICT = "/api/v1/predict"
|
|
50
|
+
PRIVATE_PREDICT = "/api/internal/predict"
|
|
51
|
+
SENTENCE = "/sentence"
|
|
52
|
+
TOKENS = "/tokens"
|
|
53
|
+
QUERY = "/query"
|
|
54
|
+
SUMMARIZE = "/summarize"
|
|
55
|
+
CHAT = "/chat"
|
|
56
|
+
REPHRASE = "/rephrase"
|
|
57
|
+
FEEDBACK = "/feedback"
|
|
58
|
+
RERANK = "/rerank"
|
|
59
|
+
|
|
60
|
+
NUCLIA_LEARNING_ID_HEADER = "NUCLIA-LEARNING-ID"
|
|
61
|
+
NUCLIA_LEARNING_MODEL_HEADER = "NUCLIA-LEARNING-MODEL"
|
|
62
|
+
NUCLIA_LEARNING_CHAT_HISTORY_HEADER = "NUCLIA-LEARNING-CHAT-HISTORY"
|
|
63
|
+
|
|
64
|
+
predict_observer = metrics.Observer(
|
|
65
|
+
"predict_engine",
|
|
66
|
+
labels={"type": ""},
|
|
67
|
+
error_mappings={
|
|
68
|
+
"over_limits": LimitsExceededError,
|
|
69
|
+
"predict_api_error": SendToPredictError,
|
|
70
|
+
},
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
RETRIABLE_EXCEPTIONS = (httpx.ConnectError, httpx.ConnectTimeout)
|
|
75
|
+
MAX_TRIES = 2
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class AnswerStatusCode(str, Enum):
|
|
79
|
+
SUCCESS = "0"
|
|
80
|
+
ERROR = "-1"
|
|
81
|
+
NO_CONTEXT = "-2"
|
|
82
|
+
NO_RETRIEVAL_DATA = "-3"
|
|
83
|
+
|
|
84
|
+
def prettify(self) -> str:
|
|
85
|
+
return {
|
|
86
|
+
AnswerStatusCode.SUCCESS: "success",
|
|
87
|
+
AnswerStatusCode.ERROR: "error",
|
|
88
|
+
AnswerStatusCode.NO_CONTEXT: "no_context",
|
|
89
|
+
AnswerStatusCode.NO_RETRIEVAL_DATA: "no_retrieval_data",
|
|
90
|
+
}[self]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class RephraseResponse:
|
|
95
|
+
rephrased_query: str
|
|
96
|
+
use_chat_history: bool | None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def get_predict() -> "PredictEngine":
|
|
100
|
+
return get_utility(Utility.PREDICT)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
async def start_predict_engine():
|
|
104
|
+
predict_util = PredictEngine(
|
|
105
|
+
nuclia_settings.nuclia_inner_predict_url,
|
|
106
|
+
nuclia_settings.nuclia_public_url,
|
|
107
|
+
nuclia_settings.nuclia_service_account,
|
|
108
|
+
nuclia_settings.nuclia_zone,
|
|
109
|
+
nuclia_settings.onprem,
|
|
110
|
+
nuclia_settings.local_predict,
|
|
111
|
+
nuclia_settings.local_predict_headers,
|
|
112
|
+
)
|
|
113
|
+
await predict_util.initialize()
|
|
114
|
+
set_utility(Utility.PREDICT, predict_util)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
async def stop_predict_engine():
|
|
118
|
+
predict_util = get_utility(Utility.PREDICT)
|
|
119
|
+
if predict_util is None:
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
clean_utility(Utility.PREDICT)
|
|
123
|
+
await predict_util.finalize()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def convert_relations(data: dict[str, list[dict[str, str]]]) -> list[RelationNode]:
|
|
127
|
+
result = []
|
|
128
|
+
for token in data["tokens"]:
|
|
129
|
+
text = token["text"]
|
|
130
|
+
klass = token["ner"]
|
|
131
|
+
result.append(
|
|
132
|
+
RelationNode(value=text, ntype=RelationNode.NodeType.ENTITY, subtype=klass)
|
|
133
|
+
)
|
|
134
|
+
return result
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class PredictEngine:
|
|
138
|
+
def __init__(
|
|
139
|
+
self,
|
|
140
|
+
cluster_url: str | None = None,
|
|
141
|
+
public_url: str | None = None,
|
|
142
|
+
nuclia_service_account: str | None = None,
|
|
143
|
+
zone: str | None = None,
|
|
144
|
+
onprem: bool = False,
|
|
145
|
+
local_predict: bool = False,
|
|
146
|
+
local_predict_headers: dict[str, str] | None = None,
|
|
147
|
+
):
|
|
148
|
+
self.nuclia_service_account = nuclia_service_account
|
|
149
|
+
self.cluster_url = cluster_url
|
|
150
|
+
if public_url is not None:
|
|
151
|
+
self.public_url: str | None = public_url.format(zone=zone)
|
|
152
|
+
else:
|
|
153
|
+
self.public_url = None
|
|
154
|
+
self.zone = zone
|
|
155
|
+
self.onprem = onprem
|
|
156
|
+
self.local_predict = local_predict
|
|
157
|
+
self.local_predict_headers = local_predict_headers
|
|
158
|
+
self.session: httpx.AsyncClient | None = None
|
|
159
|
+
|
|
160
|
+
async def initialize(self):
|
|
161
|
+
limits = httpx.Limits(max_connections=150, max_keepalive_connections=50)
|
|
162
|
+
timeout = httpx.Timeout(30.0)
|
|
163
|
+
self.session = httpx.AsyncClient(
|
|
164
|
+
limits=limits,
|
|
165
|
+
timeout=timeout,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
async def finalize(self):
|
|
169
|
+
if self.session:
|
|
170
|
+
await self.session.aclose()
|
|
171
|
+
|
|
172
|
+
def check_nua_key_is_configured_for_onprem(self):
|
|
173
|
+
if self.onprem and (
|
|
174
|
+
self.nuclia_service_account is None and self.local_predict is False
|
|
175
|
+
):
|
|
176
|
+
raise NUAKeyMissingError()
|
|
177
|
+
|
|
178
|
+
def get_predict_url(self, endpoint: str, kbid: str) -> str:
|
|
179
|
+
if not endpoint.startswith("/"):
|
|
180
|
+
endpoint = "/" + endpoint
|
|
181
|
+
if self.onprem:
|
|
182
|
+
# On-prem NucliaDB uses the public URL for the predict API. Examples:
|
|
183
|
+
# /api/v1/predict/chat/{kbid}
|
|
184
|
+
# /api/v1/predict/rephrase/{kbid}
|
|
185
|
+
return f"{self.public_url}{PUBLIC_PREDICT}{endpoint}/{kbid}"
|
|
186
|
+
else:
|
|
187
|
+
return f"{self.cluster_url}{PRIVATE_PREDICT}{endpoint}"
|
|
188
|
+
|
|
189
|
+
def get_predict_headers(self, kbid: str) -> dict[str, str]:
|
|
190
|
+
if self.onprem:
|
|
191
|
+
headers = {"X-STF-NUAKEY": f"Bearer {self.nuclia_service_account}"}
|
|
192
|
+
if self.local_predict_headers is not None:
|
|
193
|
+
headers.update(self.local_predict_headers)
|
|
194
|
+
return headers
|
|
195
|
+
else:
|
|
196
|
+
return {"X-STF-KBID": kbid}
|
|
197
|
+
|
|
198
|
+
async def check_response(
|
|
199
|
+
self, kbid: str, resp: httpx.Response, expected_status: int = 200
|
|
200
|
+
) -> None:
|
|
201
|
+
if resp.status_code == expected_status:
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
# Ensure the body is loaded before reading it (needed for streaming responses)
|
|
205
|
+
if not resp.is_stream_consumed:
|
|
206
|
+
await resp.aread()
|
|
207
|
+
|
|
208
|
+
if resp.status_code == 402:
|
|
209
|
+
data = resp.json()
|
|
210
|
+
raise LimitsExceededError(402, data["detail"])
|
|
211
|
+
|
|
212
|
+
try:
|
|
213
|
+
data = resp.json()
|
|
214
|
+
try:
|
|
215
|
+
detail = data["detail"]
|
|
216
|
+
except (KeyError, TypeError):
|
|
217
|
+
detail = data
|
|
218
|
+
except (json.decoder.JSONDecodeError, ValueError):
|
|
219
|
+
detail = resp.text
|
|
220
|
+
|
|
221
|
+
is_5xx_error = resp.status_code > 499
|
|
222
|
+
# NOTE: 512 is a special status code sent by learning predict api indicating that the error
|
|
223
|
+
# is related to an external generative model, so we don't want to log it as an error
|
|
224
|
+
is_external_generative_error = resp.status_code == 512
|
|
225
|
+
log_level = (
|
|
226
|
+
logging.ERROR
|
|
227
|
+
if is_5xx_error and not is_external_generative_error
|
|
228
|
+
else logging.INFO
|
|
229
|
+
)
|
|
230
|
+
logger.log(
|
|
231
|
+
log_level,
|
|
232
|
+
"Predict API error",
|
|
233
|
+
extra=dict(
|
|
234
|
+
kbid=kbid,
|
|
235
|
+
url=str(resp.url),
|
|
236
|
+
status_code=resp.status_code,
|
|
237
|
+
detail=detail,
|
|
238
|
+
),
|
|
239
|
+
)
|
|
240
|
+
raise ProxiedPredictAPIError(status=resp.status_code, detail=detail)
|
|
241
|
+
|
|
242
|
+
@backoff.on_exception(
|
|
243
|
+
backoff.expo,
|
|
244
|
+
RETRIABLE_EXCEPTIONS,
|
|
245
|
+
jitter=backoff.random_jitter,
|
|
246
|
+
max_tries=MAX_TRIES,
|
|
247
|
+
)
|
|
248
|
+
async def make_request(self, method: str, **request_args) -> httpx.Response:
|
|
249
|
+
if not self.session:
|
|
250
|
+
raise RuntimeError("PredictEngine session is not initialized")
|
|
251
|
+
func = getattr(self.session, method.lower())
|
|
252
|
+
return await func(**request_args)
|
|
253
|
+
|
|
254
|
+
async def make_stream_request(self, method: str, **request_args) -> httpx.Response:
|
|
255
|
+
"""Open a streaming request and return the response without reading the body.
|
|
256
|
+
The caller is responsible for consuming and closing the response."""
|
|
257
|
+
if not self.session:
|
|
258
|
+
raise RuntimeError("PredictEngine session is not initialized")
|
|
259
|
+
request = self.session.build_request(method.upper(), **request_args)
|
|
260
|
+
return await self.session.send(request, stream=True)
|
|
261
|
+
|
|
262
|
+
@predict_observer.wrap({"type": "rephrase"})
|
|
263
|
+
async def rephrase_query(self, kbid: str, item: RephraseModel) -> RephraseResponse:
|
|
264
|
+
try:
|
|
265
|
+
self.check_nua_key_is_configured_for_onprem()
|
|
266
|
+
except NUAKeyMissingError:
|
|
267
|
+
error = "Nuclia Service account is not defined so could not rephrase query"
|
|
268
|
+
logger.warning(error)
|
|
269
|
+
raise SendToPredictError(error)
|
|
270
|
+
|
|
271
|
+
resp = await self.make_request(
|
|
272
|
+
"POST",
|
|
273
|
+
url=self.get_predict_url(REPHRASE, kbid),
|
|
274
|
+
json=item.model_dump(),
|
|
275
|
+
headers=self.get_predict_headers(kbid),
|
|
276
|
+
)
|
|
277
|
+
await self.check_response(kbid, resp, expected_status=200)
|
|
278
|
+
return _parse_rephrase_response(resp)
|
|
279
|
+
|
|
280
|
+
@predict_observer.wrap({"type": "chat_ndjson"})
|
|
281
|
+
async def chat_query_ndjson(
|
|
282
|
+
self, kbid: str, item: ChatModel, extra_headers: dict[str, str] | None = None
|
|
283
|
+
) -> tuple[str, str, AsyncGenerator[GenerativeChunk, None]]:
|
|
284
|
+
"""
|
|
285
|
+
Chat query using the new stream format
|
|
286
|
+
Format specs: https://github.com/ndjson/ndjson-spec
|
|
287
|
+
"""
|
|
288
|
+
try:
|
|
289
|
+
self.check_nua_key_is_configured_for_onprem()
|
|
290
|
+
except NUAKeyMissingError:
|
|
291
|
+
error = "Nuclia Service account is not defined so the chat operation could not be performed"
|
|
292
|
+
logger.warning(error)
|
|
293
|
+
raise SendToPredictError(error)
|
|
294
|
+
|
|
295
|
+
# The ndjson format is triggered by the Accept header
|
|
296
|
+
headers = self.get_predict_headers(kbid)
|
|
297
|
+
headers["Accept"] = "application/x-ndjson"
|
|
298
|
+
|
|
299
|
+
resp = await self.make_stream_request(
|
|
300
|
+
"POST",
|
|
301
|
+
url=self.get_predict_url(CHAT, kbid),
|
|
302
|
+
json=item.model_dump(),
|
|
303
|
+
headers={**headers, **(extra_headers or {})},
|
|
304
|
+
timeout=httpx.Timeout(180.0, read=120.0),
|
|
305
|
+
)
|
|
306
|
+
await self.check_response(kbid, resp, expected_status=200)
|
|
307
|
+
ident = resp.headers.get(NUCLIA_LEARNING_ID_HEADER) or "unknown"
|
|
308
|
+
model = resp.headers.get(NUCLIA_LEARNING_MODEL_HEADER) or "unknown"
|
|
309
|
+
return ident, model, get_chat_ndjson_generator(resp)
|
|
310
|
+
|
|
311
|
+
@predict_observer.wrap({"type": "query"})
|
|
312
|
+
async def query(
|
|
313
|
+
self,
|
|
314
|
+
kbid: str,
|
|
315
|
+
item: QueryModel,
|
|
316
|
+
) -> QueryInfo:
|
|
317
|
+
"""
|
|
318
|
+
Query endpoint: returns information to be used by NucliaDB at retrieval time, for instance:
|
|
319
|
+
- The embeddings
|
|
320
|
+
- The entities
|
|
321
|
+
- The stop words
|
|
322
|
+
- The semantic threshold
|
|
323
|
+
- etc.
|
|
324
|
+
|
|
325
|
+
:param kbid: KnowledgeBox ID
|
|
326
|
+
:param sentence: The query sentence
|
|
327
|
+
:param semantic_model: The semantic model to use to generate the embeddings
|
|
328
|
+
:param generative_model: The generative model that will be used to generate the answer
|
|
329
|
+
:param rephrase: If the query should be rephrased before calculating the embeddings for a better retrieval
|
|
330
|
+
:param rephrase_prompt: Custom prompt to use for rephrasing
|
|
331
|
+
"""
|
|
332
|
+
try:
|
|
333
|
+
self.check_nua_key_is_configured_for_onprem()
|
|
334
|
+
except NUAKeyMissingError:
|
|
335
|
+
error = (
|
|
336
|
+
"Nuclia Service account is not defined so could not ask query endpoint"
|
|
337
|
+
)
|
|
338
|
+
logger.warning(error)
|
|
339
|
+
raise SendToPredictError(error)
|
|
340
|
+
|
|
341
|
+
resp = await self.make_request(
|
|
342
|
+
"POST",
|
|
343
|
+
url=self.get_predict_url(QUERY, kbid),
|
|
344
|
+
json=item.model_dump(),
|
|
345
|
+
headers=self.get_predict_headers(kbid),
|
|
346
|
+
)
|
|
347
|
+
await self.check_response(kbid, resp, expected_status=200)
|
|
348
|
+
data = resp.json()
|
|
349
|
+
return QueryInfo(**data)
|
|
350
|
+
|
|
351
|
+
@predict_observer.wrap({"type": "entities"})
|
|
352
|
+
async def detect_entities(self, kbid: str, sentence: str) -> list[RelationNode]:
|
|
353
|
+
try:
|
|
354
|
+
self.check_nua_key_is_configured_for_onprem()
|
|
355
|
+
except NUAKeyMissingError:
|
|
356
|
+
logger.warning(
|
|
357
|
+
"Nuclia Service account is not defined so could not retrieve entities from the query"
|
|
358
|
+
)
|
|
359
|
+
return []
|
|
360
|
+
|
|
361
|
+
resp = await self.make_request(
|
|
362
|
+
"GET",
|
|
363
|
+
url=self.get_predict_url(TOKENS, kbid),
|
|
364
|
+
params={"text": sentence},
|
|
365
|
+
headers=self.get_predict_headers(kbid),
|
|
366
|
+
)
|
|
367
|
+
await self.check_response(kbid, resp, expected_status=200)
|
|
368
|
+
data = resp.json()
|
|
369
|
+
return convert_relations(data)
|
|
370
|
+
|
|
371
|
+
@predict_observer.wrap({"type": "rerank"})
|
|
372
|
+
async def rerank(self, kbid: str, item: RerankModel) -> RerankResponse:
|
|
373
|
+
try:
|
|
374
|
+
self.check_nua_key_is_configured_for_onprem()
|
|
375
|
+
except NUAKeyMissingError:
|
|
376
|
+
error = "Nuclia Service account is not defined. Rerank operation could not be performed"
|
|
377
|
+
logger.warning(error)
|
|
378
|
+
raise SendToPredictError(error)
|
|
379
|
+
resp = await self.make_request(
|
|
380
|
+
"POST",
|
|
381
|
+
url=self.get_predict_url(RERANK, kbid),
|
|
382
|
+
json=item.model_dump(),
|
|
383
|
+
headers=self.get_predict_headers(kbid),
|
|
384
|
+
)
|
|
385
|
+
await self.check_response(kbid, resp, expected_status=200)
|
|
386
|
+
data = resp.json()
|
|
387
|
+
return RerankResponse.model_validate(data)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def get_chat_ndjson_generator(
|
|
391
|
+
response: httpx.Response,
|
|
392
|
+
) -> AsyncGenerator[GenerativeChunk, None]:
|
|
393
|
+
async def _parse_generative_chunks(response: httpx.Response):
|
|
394
|
+
try:
|
|
395
|
+
async for line in response.aiter_lines():
|
|
396
|
+
if not line.strip():
|
|
397
|
+
continue
|
|
398
|
+
try:
|
|
399
|
+
yield GenerativeChunk.model_validate_json(line.strip())
|
|
400
|
+
except ValidationError as ex:
|
|
401
|
+
errors.capture_exception(ex)
|
|
402
|
+
logger.error(f"Invalid chunk received: {line}")
|
|
403
|
+
continue
|
|
404
|
+
finally:
|
|
405
|
+
await response.aclose()
|
|
406
|
+
|
|
407
|
+
return _parse_generative_chunks(response)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _parse_rephrase_response(
|
|
411
|
+
resp: httpx.Response,
|
|
412
|
+
) -> RephraseResponse:
|
|
413
|
+
"""
|
|
414
|
+
Predict api is returning a json payload that is a string with the following format:
|
|
415
|
+
<rephrased_query><status_code>
|
|
416
|
+
where status_code is "0" for success, "-1" for error and "-2" for no context
|
|
417
|
+
it will raise an exception if the status code is not 0
|
|
418
|
+
"""
|
|
419
|
+
content = resp.json()
|
|
420
|
+
|
|
421
|
+
if content.endswith("0"):
|
|
422
|
+
content = content[:-1]
|
|
423
|
+
elif content.endswith("-1"):
|
|
424
|
+
raise RephraseError(content[:-2])
|
|
425
|
+
elif content.endswith("-2"):
|
|
426
|
+
raise RephraseMissingContextError(content[:-2])
|
|
427
|
+
|
|
428
|
+
use_chat_history = None
|
|
429
|
+
if NUCLIA_LEARNING_CHAT_HISTORY_HEADER in resp.headers:
|
|
430
|
+
use_chat_history = resp.headers[NUCLIA_LEARNING_CHAT_HISTORY_HEADER] == "true"
|
|
431
|
+
return RephraseResponse(rephrased_query=content, use_chat_history=use_chat_history)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
from hyperforge_nucliadb_agentic.ask.model import Image
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FieldInfo(BaseModel):
|
|
9
|
+
"""
|
|
10
|
+
Model to represent the field information required
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
text: str = Field(..., title="The text of the field")
|
|
14
|
+
metadata: str = Field(
|
|
15
|
+
title="The metadata of the field as a base64 string serialized nucliadb_protos.resources.FieldMetadata protobuf",
|
|
16
|
+
)
|
|
17
|
+
field_id: str = Field(
|
|
18
|
+
...,
|
|
19
|
+
title="The field ID of the field (rid/field_type/field[/split]) or any unique identifier",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class OperationType(str, Enum):
|
|
24
|
+
graph = "graph"
|
|
25
|
+
label = "label"
|
|
26
|
+
ask = "ask"
|
|
27
|
+
qa = "qa"
|
|
28
|
+
extract = "extract"
|
|
29
|
+
prompt_guard = "prompt_guard"
|
|
30
|
+
llama_guard = "llama_guard"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class NameOperationFilter(BaseModel):
|
|
34
|
+
operation_type: OperationType = Field(..., description="Type of the operation")
|
|
35
|
+
task_names: list[str] = Field(
|
|
36
|
+
default_factory=list,
|
|
37
|
+
description="list of task names. If None or empty, all tasks for that operation are applied.",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class QueryModel(BaseModel):
|
|
42
|
+
"""
|
|
43
|
+
Model to represent a query request
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
text: str | None = Field(default=None, description="The query text to be processed")
|
|
47
|
+
query_image: Image | None = Field(
|
|
48
|
+
default=None,
|
|
49
|
+
description="Image to be considered as part of the query. Even if the `rephrase` parameter is set to `false`, the rephrasing process will occur, combining the provided text with the image's visual features in the rephrased query.",
|
|
50
|
+
)
|
|
51
|
+
rephrase: bool = Field(
|
|
52
|
+
default=False,
|
|
53
|
+
description="If true, the model will rephrase the input text before processing",
|
|
54
|
+
)
|
|
55
|
+
rephrase_prompt: str | None = Field(
|
|
56
|
+
default=None,
|
|
57
|
+
description="Custom prompt for rephrasing the input text",
|
|
58
|
+
examples=[
|
|
59
|
+
"""Rephrase this question so its better for retrieval, and keep the rephrased question in the same language as the original.
|
|
60
|
+
QUESTION: {question}
|
|
61
|
+
Please return ONLY the question without any explanation. Just the rephrased question.""",
|
|
62
|
+
"""Rephrase this question so its better for retrieval, if in the image there are any machinery components with numeric identifiers, append them to the end of the question separated by a commas.
|
|
63
|
+
QUESTION: {question}
|
|
64
|
+
Please return ONLY the question without any explanation.""",
|
|
65
|
+
],
|
|
66
|
+
)
|
|
67
|
+
generative_model: str | None = Field(
|
|
68
|
+
default=None,
|
|
69
|
+
description="The generative model to use for rephrasing",
|
|
70
|
+
)
|
|
71
|
+
semantic_models: list[str] | None = Field(
|
|
72
|
+
default=None,
|
|
73
|
+
description="Semantic models to compute the sentence vector for, if not provided, it will only compute the sentence vector for default semantic model in the Knowledge box's configuration.",
|
|
74
|
+
)
|
|
75
|
+
agentic_entities: bool = Field(
|
|
76
|
+
default=False,
|
|
77
|
+
description="If true, the model will return the entities detected in the sentence guided by an already defined Graph Extraction Agent in the Knowledge Box.",
|
|
78
|
+
)
|
|
File without changes
|